Root background loops at context.Background() (closes #97) (#100)
All checks were successful
check / check (push) Successful in 5s

The delivery engine worker pool and the retention reaper both rooted their
goroutines in the fx OnStart hook context, which fx cancels 15s into startup.
Both now use context.WithCancel(context.Background()), bounded by OnStop.
This commit was merged in pull request #100.
This commit is contained in:
2026-08-10 15:44:56 +02:00
parent 4f5ecb18e5
commit 62481a6f1a
8 changed files with 518 additions and 37 deletions

View File

@@ -149,18 +149,7 @@ func New(
Transport: NewSSRFSafeTransport(),
})
lc.Append(fx.Hook{
OnStart: func(ctx context.Context) error {
e.start(ctx)
return nil
},
OnStop: func(_ context.Context) error {
e.stop()
return nil
},
})
e.registerHooks(lc)
return e
}
@@ -210,8 +199,40 @@ func (e *Engine) ScheduleRetry(
})
}
func (e *Engine) start(ctx context.Context) {
ctx, cancel := context.WithCancel(ctx)
// registerHooks wires the engine's start and stop into the fx
// lifecycle. The start hook's context is deliberately ignored:
// see start for why the worker pool must not inherit it.
func (e *Engine) registerHooks(lc fx.Lifecycle) {
lc.Append(fx.Hook{
//nolint:contextcheck // Not inheriting the hook context
// is the point: see start.
OnStart: func(_ context.Context) error {
e.start()
return nil
},
OnStop: func(_ context.Context) error {
e.stop()
return nil
},
})
}
// start launches the worker pool, restart recovery, and the
// periodic retry sweep.
//
// Their context is derived from context.Background(), NOT from
// the fx OnStart hook context. The hook context carries fx's
// start timeout (15s by default) and is cancelled once the start
// phase completes, so goroutines derived from it stop a few
// seconds into the process: every worker would return and the
// engine would silently stop delivering webhooks entirely. A
// long-lived goroutine must outlive the startup phase, so its
// lifetime is bounded by OnStop instead: stop cancels this
// context and waits on the WaitGroup.
func (e *Engine) start() {
ctx, cancel := context.WithCancel(context.Background())
e.cancel = cancel
for range e.workers {