fx.StopTimeout is never set, so the bounded-shutdown fix does not fire under a default docker stop #134
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Found in the independent review of #130. Not milestoned 1.0.0: it is not reachable by an unauthenticated attacker, so it belongs to the next cycle rather than blocking the tag.
cmd/webhooker/main.go:30never passesfx.StopTimeout, so shutdown is bounded by fx's 15 s default.Docker's default
docker stopgrace is 10 s. So the process is SIGKILLed at 10 s (exit 137) BEFORE the 15 s bound expires, and #130's bounded shutdown — including its operator-facing "goroutines still running" error log, which is the part that actually survives — only materialises where the grace period exceeds 15 s. Under a stock Docker deployment it never fires.That does not make #130 wrong; it makes it inert in the common deployment.
Definition of done
fx.StopTimeoutset explicitly incmd/webhooker/main.goat a value below the typical deployment grace period, so the bound is reached and the warning is logged before any external SIGKILL.--stop-timeoutbelow it reintroduces the same silent truncation.Second, unrelated item from the same review
internal/lifecycle/is the onlyinternal/*package missing from the README "Package Layout" tree (README.md:930-982) — it was added by #130 and the tree was not updated. Add it. The tree is already stale at file granularity, which is out of scope here; only the missing package needs fixing.Implementation requirements
next, PR based onnext, single commit, title ending(closes #N).TODO.md(see #112).make fmtand include the result.make checkplus the Docker lint path with the cache defeated (#119).#130 has landed on
next, so the bound it built is now shipped but inert under the deployment the repo actually produces. That makes this release-relevant rather than next-cycle.Two additions to scope, both from the review of #130:
fx.StopTimeoutmust sit inside Docker's default 10s grace. fx'sDefaultTimeoutis 15s and the Dockerfile sets noSTOPSIGNALor grace override, so Docker SIGKILLs 5s before the current bound can fire.WaitForShutdownininternal/lifecycle/lifecycle.goselects ondoneagainstctx.Done()with no preamble, so a component that drained cleanly against an already-expired context returnsshutdown timed outabout half the time. Unreachable through fx today — fx re-checksctx.Err()before each remaining hook — so it is latent only for direct callers of this new shared helper. Fix with aselect { case <-done: return nil; default: }preamble and a test that pins it.Also worth writing down in the same change: bounding shutdown means a wedge in the first-stopped component aborts the rest of the sequence, because fx returns outright on an expired stop context and skips the remaining hooks. That is fx's behaviour, not a defect introduced here, but nothing in the repo records it.
Plan, one commit on
issue-134-fx-stop-timeout:cmd/webhooker/main.go: extract the option set intonewApp() *fx.Appand addfx.StopTimeout(stopTimeout)withstopTimeout = 5s. 5s sits inside Docker's 10s default grace with headroom for signal delivery and process exit, and matches the HTTP server's own 5s drain budget, so the first hook can use its full budget without the bound truncating it.cmd/webhooker/main_test.go:fxexposes(*fx.App).StopTimeout(), andfx.Newapplies options before running invokes, so the test assertsnewApp().StopTimeout() == stopTimeoutand that it is below the 10s grace. Dropping the option makes it read fx's 15sDefaultTimeoutand the test fails — will mutation-verify and report the observed failure.internal/lifecycle/lifecycle.go: split the channel wait into an unexportedwaitDone(ctx, log, component, done)carrying theselect { case <-done: return nil; default: }preamble, exposed to the external test package viaexport_test.go. The deterministic test driveswaitDonewith an already-closeddoneand an already-cancelledctxin a loop —selectpicks uniformly among ready cases, so a single pass proves nothing, and no wall-clock sleeps are involved. Testing this through the publicWaitForShutdowncannot be deterministic: whether the internal waiter goroutine has closeddoneby the time the preamble runs is scheduler-dependent.internal/lifecycle/entry to the Package Layout tree.Gate:
docker build --no-cache-filter=lint --no-cache-filter=builder .as the authoritative run,make checkon the host as a secondary signal, plus an end-to-enddocker run/docker stopshowing a clean exit inside the grace period.TODO.mduntouched.