Files
go-omni/lifecycle/runtime.go

82 lines
2.2 KiB
Go

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
}