package lifecycle import ( "context" "errors" "fmt" "sync" ) // Runtime owns resources registered during dependency initialization. A server // must finish draining before calling Close. Embed *Runtime in the project's svc. // The logger and any other borrowed resources must not be registered here. type Runtime struct { Lifecycle *Controller mu sync.Mutex cleanups []func() error closed bool once sync.Once closeErr error } func NewRuntime(ctx context.Context) *Runtime { return &Runtime{Lifecycle: New(ctx)} } func (r *Runtime) Context() context.Context { return r.Lifecycle.Context() } func (r *Runtime) RequestShutdown(err error) { r.Lifecycle.RequestShutdown(err) } // OnClose transfers cleanup ownership. Register immediately after creation, // before exposing resources to other goroutines. Registration after Close is a // programming error. Callbacks may request shutdown, but must not call Close. func (r *Runtime) OnClose(cleanup func() error) { if cleanup == nil { return } r.mu.Lock() defer r.mu.Unlock() if r.closed { panic("lifecycle: register cleanup after Close") } r.cleanups = append(r.cleanups, cleanup) } // Close releases all resources in reverse order, including after a failed // initialization. All callbacks are attempted even if one fails or panics. func (r *Runtime) Close() error { if r == nil { return nil } r.once.Do(func() { r.RequestShutdown(nil) r.mu.Lock() r.closed = true cleanups := r.cleanups r.cleanups = nil r.mu.Unlock() for i := len(cleanups) - 1; i >= 0; i-- { r.closeErr = errors.Join(r.closeErr, runCleanup(cleanups[i])) } }) return r.closeErr } func runCleanup(fn func() error) (err error) { defer func() { if p := recover(); p != nil { err = fmt.Errorf("lifecycle: cleanup panic: %v", p) } }() return fn() } // Finish closes resources and preserves the operation, cleanup and shutdown // errors. Read the final cause only after monitors have been stopped by Close. func (r *Runtime) Finish(err error) error { if r == nil { return err } err = errors.Join(err, r.Close()) if cause := r.Lifecycle.Err(); cause != nil && !errors.Is(err, cause) { err = errors.Join(err, cause) } return err }