Skip to content

Commit c80d962

Browse files
feat(webhook): configure shutdown timeout wiring
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
1 parent 5cea770 commit c80d962

3 files changed

Lines changed: 23 additions & 3 deletions

File tree

internal/cli/serve.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ type ServeConfig struct {
3535
RepoRoot string
3636
WebhookMaxTrackedRepos int
3737
WebhookAttemptTimeout time.Duration
38+
WebhookShutdownTimeout time.Duration
3839
WebhookRetryAttempts int
3940
WebhookRetryBaseDelay time.Duration
4041
WebhookRetryMaxDelay time.Duration
@@ -59,6 +60,9 @@ func validateServeConfig(cfg ServeConfig) error {
5960
if cfg.WebhookAttemptTimeout <= 0 {
6061
return fmt.Errorf("--webhook-attempt-timeout must be > 0")
6162
}
63+
if cfg.WebhookShutdownTimeout <= 0 {
64+
return fmt.Errorf("--webhook-shutdown-timeout must be > 0")
65+
}
6266
if cfg.WebhookRetryAttempts <= 0 {
6367
return fmt.Errorf("--webhook-retry-attempts must be > 0")
6468
}
@@ -147,6 +151,7 @@ func newServeCmd(deps *Deps) *cobra.Command {
147151
cmd.Flags().IntVar(&cfg.WebhookWorkers, "webhook-workers", envInt("CCG_WEBHOOK_WORKERS", 4), "Number of webhook sync workers")
148152
cmd.Flags().IntVar(&cfg.WebhookMaxTrackedRepos, "webhook-max-tracked-repos", envInt("CCG_WEBHOOK_MAX_TRACKED_REPOS", 1024), "Maximum repositories tracked by the webhook sync queue")
149153
cmd.Flags().DurationVar(&cfg.WebhookAttemptTimeout, "webhook-attempt-timeout", envDuration("CCG_WEBHOOK_ATTEMPT_TIMEOUT", 15*time.Minute), "Timeout for one webhook sync attempt")
154+
cmd.Flags().DurationVar(&cfg.WebhookShutdownTimeout, "webhook-shutdown-timeout", envDuration("CCG_WEBHOOK_SHUTDOWN_TIMEOUT", 30*time.Second), "Timeout for graceful webhook queue shutdown and HTTP drain")
150155
cmd.Flags().IntVar(&cfg.WebhookRetryAttempts, "webhook-retry-attempts", envInt("CCG_WEBHOOK_RETRY_ATTEMPTS", 3), "Maximum webhook sync attempts per queued item")
151156
cmd.Flags().DurationVar(&cfg.WebhookRetryBaseDelay, "webhook-retry-base-delay", envDuration("CCG_WEBHOOK_RETRY_BASE_DELAY", time.Second), "Initial webhook sync retry delay")
152157
cmd.Flags().DurationVar(&cfg.WebhookRetryMaxDelay, "webhook-retry-max-delay", envDuration("CCG_WEBHOOK_RETRY_MAX_DELAY", 30*time.Second), "Maximum webhook sync retry delay")

internal/cli/serve_test.go

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -444,14 +444,15 @@ func TestServeCmdFlags_WebhookOperationalTuning(t *testing.T) {
444444
err := executeCmd(deps, stdout, stderr, "serve",
445445
"--webhook-max-tracked-repos", "8",
446446
"--webhook-attempt-timeout", "2m",
447+
"--webhook-shutdown-timeout", "45s",
447448
"--webhook-retry-attempts", "5",
448449
"--webhook-retry-base-delay", "2s",
449450
"--webhook-retry-max-delay", "20s",
450451
)
451452
if err != nil {
452453
t.Fatalf("unexpected error: %v", err)
453454
}
454-
if got.WebhookMaxTrackedRepos != 8 || got.WebhookAttemptTimeout != 2*time.Minute ||
455+
if got.WebhookMaxTrackedRepos != 8 || got.WebhookAttemptTimeout != 2*time.Minute || got.WebhookShutdownTimeout != 45*time.Second ||
455456
got.WebhookRetryAttempts != 5 || got.WebhookRetryBaseDelay != 2*time.Second ||
456457
got.WebhookRetryMaxDelay != 20*time.Second {
457458
t.Fatalf("unexpected webhook tuning config: %+v", got)
@@ -461,6 +462,7 @@ func TestServeCmdFlags_WebhookOperationalTuning(t *testing.T) {
461462
func TestServeCmd_UsesWebhookTuningFromEnv(t *testing.T) {
462463
t.Setenv("CCG_WEBHOOK_MAX_TRACKED_REPOS", "9")
463464
t.Setenv("CCG_WEBHOOK_ATTEMPT_TIMEOUT", "3m")
465+
t.Setenv("CCG_WEBHOOK_SHUTDOWN_TIMEOUT", "40s")
464466
t.Setenv("CCG_WEBHOOK_RETRY_ATTEMPTS", "6")
465467
t.Setenv("CCG_WEBHOOK_RETRY_BASE_DELAY", "3s")
466468
t.Setenv("CCG_WEBHOOK_RETRY_MAX_DELAY", "30s")
@@ -476,13 +478,25 @@ func TestServeCmd_UsesWebhookTuningFromEnv(t *testing.T) {
476478
if err != nil {
477479
t.Fatalf("unexpected error: %v", err)
478480
}
479-
if got.WebhookMaxTrackedRepos != 9 || got.WebhookAttemptTimeout != 3*time.Minute ||
481+
if got.WebhookMaxTrackedRepos != 9 || got.WebhookAttemptTimeout != 3*time.Minute || got.WebhookShutdownTimeout != 40*time.Second ||
480482
got.WebhookRetryAttempts != 6 || got.WebhookRetryBaseDelay != 3*time.Second ||
481483
got.WebhookRetryMaxDelay != 30*time.Second {
482484
t.Fatalf("unexpected webhook env config: %+v", got)
483485
}
484486
}
485487

488+
func TestServeCmd_RejectsNonPositiveWebhookShutdownTimeout(t *testing.T) {
489+
deps, stdout, stderr := newTestDeps()
490+
deps.ServeFunc = func(cfg ServeConfig) error { return nil }
491+
492+
for _, timeout := range []string{"0s", "-1s"} {
493+
err := executeCmd(deps, stdout, stderr, "serve", "--transport", "streamable-http", "--webhook-shutdown-timeout", timeout)
494+
if err == nil || !strings.Contains(err.Error(), "--webhook-shutdown-timeout must be > 0") {
495+
t.Fatalf("expected webhook shutdown timeout validation error for %s, got %v", timeout, err)
496+
}
497+
}
498+
}
499+
486500
func TestServeCmd_RejectsNonPositiveWebhookWorkers(t *testing.T) {
487501
deps, stdout, stderr := newTestDeps()
488502
deps.ServeFunc = func(cfg ServeConfig) error { return nil }

internal/server/serve.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,7 @@ func RunStreamableHTTP(deps *cli.Deps, srv *mcpgo.MCPServer, cfg cli.ServeConfig
310310
BaseDelay: cfg.WebhookRetryBaseDelay,
311311
MaxDelay: cfg.WebhookRetryMaxDelay,
312312
},
313+
ShutdownTimeout: cfg.WebhookShutdownTimeout,
313314
MaxTrackedRepos: cfg.WebhookMaxTrackedRepos,
314315
})
315316
mux.Handle("/webhook", webhook.NewWebhookHandlerWithConfig(webhook.WebhookHandlerConfig{
@@ -358,7 +359,7 @@ func RunStreamableHTTP(deps *cli.Deps, srv *mcpgo.MCPServer, cfg cli.ServeConfig
358359
return nil
359360
case <-ctx.Done():
360361
deps.Logger.Info("shutting down HTTP server")
361-
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
362+
shutdownCtx, cancel := context.WithTimeout(context.Background(), cfg.WebhookShutdownTimeout)
362363
defer cancel()
363364
if err := httpServer.Shutdown(shutdownCtx); err != nil {
364365
return trace.Wrap(err, "HTTP server shutdown")

0 commit comments

Comments
 (0)