How Caddy's Context Cancellation Works: Three-Stage Cleanup Explained
Caddy triggers context cancellation through a three-stage process that executes standard context cancellation, registered OnCancel callbacks, and module-specific Cleanup() methods whenever configurations reload or servers shut down.
Caddy's context cancellation mechanism provides deterministic resource cleanup during configuration reloads and graceful shutdowns. This architecture, implemented in the caddyserver/caddy repository, extends Go's standard context.Context to handle the complex lifecycle of modular server components. Understanding how Caddy's context cancellation works is essential for developing custom modules that require proper teardown logic and resource management.
The Architecture of Caddy's Context System
Caddy builds its own caddy.Context type on top of the standard library's context.Context. When provisioning a new configuration, the NewContext function in context.go creates a cancelable context using context.WithCancel, deriving it from a parent context.
The implementation stores the returned CancelFunc directly on the Config struct as cfg.cancelFunc (caddy.go lines 95-104). This design allows the server to trigger cancellation later by invoking the stored function, ensuring that all downstream operations derived from that context receive the cancellation signal.
The Three-Stage Cancellation Process
When the stored cancelFunc is invoked—typically during a configuration reload or server shutdown—the system executes three coordinated cleanup stages defined in context.go:
-
Standard Context Cancellation (lines 66-68): The underlying
context.CancelFuncfires immediately, unblocking any goroutine waiting onctx.Done(). -
OnCancel Callback Execution (lines 71-73): The system executes all functions registered via
ctx.OnCancel(f). This Caddy-specific hook allows modules to register custom teardown code at runtime. -
Module Cleanup Iteration (lines 75-84): The context iterates over all modules loaded within its scope and, if they implement the
CleanerUpperinterface, invokes theirCleanup()method to release resources like file handles and network sockets.
Configuration Lifecycle and Cancellation Triggers
The cancellation lifecycle follows the configuration provisioning flow in caddy.go. During the provisioning phase (lines 95-104), provisionContext creates a fresh caddy.Context and saves the cancel function to newCfg.cancelFunc.
When a new configuration loads or the server initiates shutdown, the previous configuration's cancelFunc is explicitly invoked. This occurs in the run function (lines 418-424) and during error handling in provisionContext (lines 730-740), ensuring that resources from the old configuration are released before or during the transition to the new state.
Practical Implementation Examples
Registering Cleanup Hooks with ctx.OnCancel()
Modules can register custom teardown logic using the OnCancel method during the provisioning phase. This approach is ideal for closing database connections, flushing buffers, or releasing temporary resources.
func (m *MyModule) Provision(ctx caddy.Context) error {
// Perform module initialization...
ctx.OnCancel(func() {
// Module-specific teardown logic
m.db.Close()
m.cache.Flush()
})
return nil
}
This callback mechanism is implemented in context.go (lines 91-94), which appends the function to an internal slice executed during the second cancellation stage.
Implementing the CleanerUpper Interface
For modules that require structured cleanup logic, implement the CleanerUpper interface. The system automatically detects and invokes this method during the third cancellation stage.
type MyModule struct {
listener net.Listener
}
// Cleanup is called automatically when the context is cancelled
func (m *MyModule) Cleanup() error {
if m.listener != nil {
return m.listener.Close()
}
return nil
}
Handling QUIC Listener Cancellation
Subsystems often wrap the standard cancel function to perform additional bookkeeping. In listeners.go, the sharedQUICState.addState method (lines 53-71) creates its own context.WithCancel and wraps the cancel function to remove TLS configuration entries from internal maps.
When fakeCloseQuicListener.Close is called (lines 38-44), it invokes the stored contextCancel function, which triggers both the standard context cancellation and the TLS config cleanup simultaneously.
func (l *fakeCloseQuicListener) Close() error {
if atomic.CompareAndSwapInt32(&l.closed, 0, 1) {
// Cancel the QUIC state context, removing TLS config entries
l.contextCancel()
}
return nil
}
Summary
- Caddy's context system wraps
context.ContextusingNewContextincontext.goto create cancelable contexts for each configuration. - Three-stage cancellation executes standard context cancellation,
OnCancelcallbacks (lines 71-73), andCleanerUpper.Cleanup()methods (lines 75-84) to ensure complete resource release. - Cancel function storage occurs in
Config.cancelFuncduring provisioning (caddy.golines 95-104) and is triggered during reloads (lines 418-424) or shutdowns. - Module integration supports both callback-based cleanup via
ctx.OnCancel()and interface-based cleanup viaCleanerUpper. - Subsystem extensions like QUIC listeners in
listeners.godemonstrate how to wrap cancel functions for domain-specific resource management.
Frequently Asked Questions
What triggers context cancellation in Caddy?
Context cancellation triggers when Caddy reloads configurations or initiates server shutdown. The run function in caddy.go (lines 418-424) invokes currentCfg.cancelFunc() when replacing an active configuration, and similar invocations occur during error handling in provisionContext (lines 730-740).
How do modules register custom cleanup code during cancellation?
Modules call ctx.OnCancel(f) during their Provision method, registering a function that executes during the second cancellation stage. Alternatively, modules can implement the CleanerUpper interface, which provides a formal Cleanup() method invoked during the third stage (lines 75-84 of context.go).
What is the difference between OnCancel callbacks and the CleanerUpper interface?
OnCancel accepts arbitrary functions registered at runtime, suitable for ad-hoc cleanup logic specific to a particular instance. CleanerUpper defines a structured interface (Cleanup() error) that types implement to guarantee cleanup is called for all instances of that module type during context cancellation.
How does Caddy ensure QUIC listeners clean up TLS configurations?
The QUIC subsystem in listeners.go creates a wrapped cancel function (lines 53-71) that, when invoked by fakeCloseQuicListener.Close (lines 38-44), deletes the associated TLS configuration from the internal tlsConfs map before executing the standard context cancellation.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →