diff --git a/admin.go b/admin.go index 2eb9c3b04..5ceb3daeb 100644 --- a/admin.go +++ b/admin.go @@ -749,10 +749,14 @@ func stopAdminServer(srv *http.Server) error { if srv == nil { return fmt.Errorf("no admin server") } - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + timeout := 10 * time.Second + ctx, cancel := context.WithTimeoutCause(context.Background(), timeout, fmt.Errorf("stopping admin server: %ds timeout", int(timeout.Seconds()))) defer cancel() err := srv.Shutdown(ctx) if err != nil { + if cause := context.Cause(ctx); cause != nil && errors.Is(err, context.DeadlineExceeded) { + err = cause + } return fmt.Errorf("shutting down admin server: %v", err) } Log().Named("admin").Info("stopped previous server", zap.String("address", srv.Addr)) diff --git a/caddy.go b/caddy.go index 1c08de8a8..c27ae4a68 100644 --- a/caddy.go +++ b/caddy.go @@ -88,7 +88,7 @@ type Config struct { storage certmagic.Storage eventEmitter eventEmitter - cancelFunc context.CancelFunc + cancelFunc context.CancelCauseFunc // fileSystems is a dict of fileSystems that will later be loaded from and added to. fileSystems FileSystems @@ -433,7 +433,7 @@ func run(newCfg *Config, start bool) (Context, error) { // partially copied from provisionContext if err != nil { globalMetrics.configSuccess.Set(0) - ctx.cfg.cancelFunc() + ctx.cfg.cancelFunc(fmt.Errorf("configuration start error: %w", err)) if currentCtx.cfg != nil { certmagic.Default.Storage = currentCtx.cfg.storage @@ -509,7 +509,7 @@ func provisionContext(newCfg *Config, replaceAdminServer bool) (Context, error) // cleanup occurs when we return if there // was an error; if no error, it will get // cleaned up on next config cycle - ctx, cancel := NewContext(Context{Context: context.Background(), cfg: newCfg}) + ctx, cancelCause := NewContextWithCause(Context{Context: context.Background(), cfg: newCfg}) defer func() { if err != nil { globalMetrics.configSuccess.Set(0) @@ -518,7 +518,7 @@ func provisionContext(newCfg *Config, replaceAdminServer bool) (Context, error) // since the associated config won't be used; // this will cause all modules that were newly // provisioned to clean themselves up - cancel() + cancelCause(fmt.Errorf("configuration error: %w", err)) // also undo any other state changes we made if currentCtx.cfg != nil { @@ -526,7 +526,7 @@ func provisionContext(newCfg *Config, replaceAdminServer bool) (Context, error) } } }() - newCfg.cancelFunc = cancel // clean up later + newCfg.cancelFunc = cancelCause // clean up later // set up logging before anything bad happens if newCfg.Logging == nil { @@ -746,7 +746,7 @@ func unsyncedStop(ctx Context) { } // clean up all modules - ctx.cfg.cancelFunc() + ctx.cfg.cancelFunc(fmt.Errorf("stopping apps")) } // Validate loads, provisions, and validates @@ -754,7 +754,7 @@ func unsyncedStop(ctx Context) { func Validate(cfg *Config) error { _, err := run(cfg, false) if err == nil { - cfg.cancelFunc() // call Cleanup on all modules + cfg.cancelFunc(fmt.Errorf("validation complete")) // call Cleanup on all modules } return err } diff --git a/context.go b/context.go index a12cdcad4..980027275 100644 --- a/context.go +++ b/context.go @@ -63,10 +63,17 @@ type Context struct { // modules which are loaded will be properly unloaded. // See standard library context package's documentation. func NewContext(ctx Context) (Context, context.CancelFunc) { + newCtx, cancelCause := NewContextWithCause(ctx) + return newCtx, func() { cancelCause(nil) } +} + +// NewContextWithCause is like NewContext but returns a context.CancelCauseFunc. +// EXPERIMENTAL: This API is subject to change. +func NewContextWithCause(ctx Context) (Context, context.CancelCauseFunc) { newCtx := Context{moduleInstances: make(map[string][]Module), cfg: ctx.cfg, metricsRegistry: prometheus.NewPedanticRegistry()} - c, cancel := context.WithCancel(ctx.Context) - wrappedCancel := func() { - cancel() + c, cancel := context.WithCancelCause(ctx.Context) + wrappedCancel := func(cause error) { + cancel(cause) for _, f := range ctx.cleanupFuncs { f() diff --git a/listeners.go b/listeners.go index 0639b16b7..84ebaaaba 100644 --- a/listeners.go +++ b/listeners.go @@ -512,7 +512,7 @@ func ListenerUsage(network, addr string) int { // contextAndCancelFunc groups context and its cancelFunc type contextAndCancelFunc struct { context.Context - context.CancelFunc + context.CancelCauseFunc } // sharedQUICState manages GetConfigForClient @@ -542,17 +542,17 @@ func (sqs *sharedQUICState) getConfigForClient(ch *tls.ClientHelloInfo) (*tls.Co // addState adds tls.Config and activeRequests to the map if not present and returns the corresponding context and its cancelFunc // so that when cancelled, the active tls.Config will change -func (sqs *sharedQUICState) addState(tlsConfig *tls.Config) (context.Context, context.CancelFunc) { +func (sqs *sharedQUICState) addState(tlsConfig *tls.Config) (context.Context, context.CancelCauseFunc) { sqs.rmu.Lock() defer sqs.rmu.Unlock() if cacc, ok := sqs.tlsConfs[tlsConfig]; ok { - return cacc.Context, cacc.CancelFunc + return cacc.Context, cacc.CancelCauseFunc } - ctx, cancel := context.WithCancel(context.Background()) - wrappedCancel := func() { - cancel() + ctx, cancel := context.WithCancelCause(context.Background()) + wrappedCancel := func(cause error) { + cancel(cause) sqs.rmu.Lock() defer sqs.rmu.Unlock() @@ -608,13 +608,13 @@ func fakeClosedErr(l interface{ Addr() net.Addr }) error { // indicating that it is pretending to be closed so that the // server using it can terminate, while the underlying // socket is actually left open. -var errFakeClosed = fmt.Errorf("listener 'closed' 😉") +var errFakeClosed = fmt.Errorf("QUIC listener 'closed' 😉") type fakeCloseQuicListener struct { closed int32 // accessed atomically; belongs to this struct only *sharedQuicListener // embedded, so we also become a quic.EarlyListener context context.Context - contextCancel context.CancelFunc + contextCancel context.CancelCauseFunc } // Currently Accept ignores the passed context, however a situation where @@ -637,7 +637,7 @@ func (fcql *fakeCloseQuicListener) Accept(_ context.Context) (*quic.Conn, error) func (fcql *fakeCloseQuicListener) Close() error { if atomic.CompareAndSwapInt32(&fcql.closed, 0, 1) { - fcql.contextCancel() + fcql.contextCancel(errFakeClosed) } else if atomic.CompareAndSwapInt32(&fcql.closed, 1, 2) { _, _ = listenerPool.Delete(fcql.sharedQuicListener.key) } diff --git a/modules/caddyhttp/app.go b/modules/caddyhttp/app.go index 8058dbf33..74f1466be 100644 --- a/modules/caddyhttp/app.go +++ b/modules/caddyhttp/app.go @@ -18,6 +18,7 @@ import ( "cmp" "context" "crypto/tls" + "errors" "fmt" "maps" "net" @@ -711,9 +712,10 @@ func (app *App) Stop() error { // enforce grace period if configured if app.GracePeriod > 0 { var cancel context.CancelFunc - ctx, cancel = context.WithTimeout(ctx, time.Duration(app.GracePeriod)) + timeout := time.Duration(app.GracePeriod) + ctx, cancel = context.WithTimeoutCause(ctx, timeout, fmt.Errorf("server graceful shutdown %ds timeout", int(timeout.Seconds()))) defer cancel() - app.logger.Info("servers shutting down; grace period initiated", zap.Duration("duration", time.Duration(app.GracePeriod))) + app.logger.Info("servers shutting down; grace period initiated", zap.Duration("duration", timeout)) } else { app.logger.Info("servers shutting down with eternal grace period") } @@ -739,6 +741,9 @@ func (app *App) Stop() error { } if err := server.server.Shutdown(ctx); err != nil { + if cause := context.Cause(ctx); cause != nil && errors.Is(err, context.DeadlineExceeded) { + err = cause + } app.logger.Error("server shutdown", zap.Error(err), zap.Strings("addresses", server.Listen)) @@ -762,6 +767,9 @@ func (app *App) Stop() error { } if err := server.h3server.Shutdown(ctx); err != nil { + if cause := context.Cause(ctx); cause != nil && errors.Is(err, context.DeadlineExceeded) { + err = cause + } app.logger.Error("HTTP/3 server shutdown", zap.Error(err), zap.Strings("addresses", server.Listen)) diff --git a/modules/caddyhttp/reverseproxy/httptransport.go b/modules/caddyhttp/reverseproxy/httptransport.go index 8d2b99e9e..c65bd6185 100644 --- a/modules/caddyhttp/reverseproxy/httptransport.go +++ b/modules/caddyhttp/reverseproxy/httptransport.go @@ -448,7 +448,7 @@ func (h *HTTPTransport) NewTransport(caddyCtx caddy.Context) (*http.Transport, e // complete the handshake before returning the connection if rt.TLSHandshakeTimeout != 0 { var cancel context.CancelFunc - ctx, cancel = context.WithTimeout(ctx, rt.TLSHandshakeTimeout) + ctx, cancel = context.WithTimeoutCause(ctx, rt.TLSHandshakeTimeout, fmt.Errorf("HTTP transport TLS handshake %ds timeout", int(rt.TLSHandshakeTimeout.Seconds()))) defer cancel() } err = tlsConn.HandshakeContext(ctx)