// Package lifecycle coordinates service shutdown without owning project resources. package lifecycle import ( "context" "errors" "sync" ) // Controller connects dependency failures to the server's graceful shutdown. // Construct it with New. RequestShutdown never waits for draining or cleanup. type Controller struct { ctx context.Context cancel context.CancelCauseFunc mu sync.Mutex cause error } func New(parent context.Context) *Controller { if parent == nil { parent = context.Background() } ctx, cancel := context.WithCancelCause(parent) return &Controller{ctx: ctx, cancel: cancel} } func (c *Controller) Context() context.Context { return c.ctx } // RequestShutdown asks the service to stop. nil is a normal stop; a non-nil // error is fatal. The first fatal error is retained even if a normal signal // already began draining. Concurrent and repeated calls are safe. func (c *Controller) RequestShutdown(err error) { c.mu.Lock() defer c.mu.Unlock() if c.cause == nil && err != nil { c.cause = err } c.cancel(err) } // Err is read after resources have stopped reporting failures. It returns the // first dependency failure, or an abnormal parent cancellation cause. func (c *Controller) Err() error { if c == nil { return nil } c.mu.Lock() defer c.mu.Unlock() if c.cause != nil { return c.cause } return Cause(c.ctx) } // Cause treats cancellation-compatible errors (including signal causes in // newer Go versions) as normal stops. Deadlines and other causes are failures. // Controller.Err additionally retains every explicitly reported non-nil error. func Cause(ctx context.Context) error { if ctx == nil { return nil } cause := context.Cause(ctx) if errors.Is(cause, context.Canceled) { return nil } return cause }