🐛 fix: wire up readiness cancellation and stabilise graceful shutdown test
Three related issues fixed together:
1. Readiness context was never cancelled during shutdown
server.Run() had a type assertion for a Cancel() method that no standard
context.Context implements, so readiness stayed "ready" through the entire
shutdown window. Added CancelableContext to pkg/server — a thin wrapper that
exposes Cancel() — and switched cmd/server/main.go to use it. Test servers
and CLI continue passing context.Background() unchanged.
2. "Server exited" log was never emitted
The test script expected it; main.go had no log after server.Run() returned.
Added log.Trace().Msg("Server exited") after the Run() call.
3. Double-SIGTERM caused non-JSON "signal: terminated" in server.log
test-graceful-shutdown.sh sent SIGTERM, then called $SERVER_CMD stop which
sent a second SIGTERM. After signal.NotifyContext is cancelled, the second
signal hits the default handler and Go prints "signal: terminated" to stderr,
breaking the all-JSON-lines assertion. Fixed by waiting for the PID to exit
ourselves instead of re-invoking the stop script.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -33,6 +33,28 @@ import (
|
||||
//go:embed docs/swagger.json
|
||||
var swaggerJSON embed.FS
|
||||
|
||||
// CancelableContext wraps a context.Context and exposes a Cancel() method so
|
||||
// that Server.Run() can cancel readiness during graceful shutdown via the type
|
||||
// assertion it already performs. Callers that don't need controlled cancellation
|
||||
// (tests, CLI) can pass a plain context.Background() — the assertion silently
|
||||
// fails and readiness is never explicitly cancelled, which is harmless.
|
||||
type CancelableContext struct {
|
||||
context.Context
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
// NewCancelableContext creates a CancelableContext whose Cancel() method will
|
||||
// be invoked by Server.Run() at the start of graceful shutdown, before the
|
||||
// 1-second readiness propagation window. The returned CancelFunc is a no-op
|
||||
// after Cancel() has been called, so it is safe to defer in main.
|
||||
func NewCancelableContext(parent context.Context) (*CancelableContext, context.CancelFunc) {
|
||||
ctx, cancel := context.WithCancel(parent)
|
||||
return &CancelableContext{Context: ctx, cancel: cancel}, cancel
|
||||
}
|
||||
|
||||
// Cancel satisfies the interface checked in Run() and cancels the context.
|
||||
func (c *CancelableContext) Cancel() { c.cancel() }
|
||||
|
||||
type Server struct {
|
||||
router *chi.Mux
|
||||
readyCtx context.Context
|
||||
|
||||
Reference in New Issue
Block a user