lifecycle

package
v1.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 27, 2026 License: MIT Imports: 8 Imported by: 0

Documentation

Overview

Package lifecycle provides NornicDB's canonical process supervisor.

The package is deliberately a leaf in the import graph — it depends only on the Go standard library and golang.org/x/sync/errgroup. It never imports observability, storage, Cypher, Bolt, server, or any other NornicDB business package. This separation lets every current and future NornicDB binary (cmd/nornicdb/, planned cmd/metrics-doc-gen/, future CLI utilities) reuse the same supervision discipline without pulling in the OpenTelemetry SDK or Prometheus registry.

Architecture

A Component is anything that can be started and shut down. Run takes the parent context plus a forward-ordered slice of components and:

  1. Wraps the parent in signal.NotifyContext(SIGINT, SIGTERM) so any OS signal cleanly cancels the supervisor (replaces the legacy channel-leak-prone signal.Notify(chan)+<-chan idiom).
  2. Drives every Component.Start on a single golang.org/x/sync/errgroup, so the FIRST non-nil Start error cancels the whole group.
  3. After every Start has returned, drains components in REVERSE registration order (the OBS-08 contract — last to start, first to stop).
  4. Drives every Shutdown on a FRESH context.WithTimeout( context.Background(), ShutdownTimeout). This is the OBS-09 keystone: deriving from the cancelled errgroup ctx would return immediately and break the 30s flush budget.
  5. Returns errors.Join(runErr, shutdownErr) so callers see both the trigger and any cleanup failures.

Wiring contract

Callers register components in startup order. Reverse-order shutdown is the OBS-08 mechanism — components like the telemetry/metrics listener (started first) drain LAST so kubelet keeps scraping during graceful shutdown, while the application HTTP edge (started last) drains FIRST.

See ADR-0001 §2.8.1 (amendments A10a + A10c) for the architectural contract this package implements.

Index

Constants

View Source
const ShutdownTimeout = 30 * time.Second

ShutdownTimeout bounds the total drain budget across every component. Per ADR-0001 §2.8.1 / A10a this MUST be 30s — the OpenTelemetry batch span processor needs ~5s to flush, and the remaining headroom covers in-flight HTTP/Bolt requests.

Variables

This section is empty.

Functions

func DrainReverse

func DrainReverse(ctx context.Context, components []Component) error

DrainReverse calls Shutdown on each component in REVERSE slice order (last → first), accumulating per-component errors with errors.Join.

One Shutdown returning a non-nil error does NOT abort the loop — every remaining component is still given a chance to flush. This is the OBS-08 mechanism: drain order is contractual, and a faulty component cannot strand the others.

The supplied ctx should be a FRESH context.WithTimeout( context.Background(), ShutdownTimeout) per OBS-09; DrainReverse itself does not enforce that — Run is the canonical caller and constructs the fresh ctx before invoking DrainReverse.

func Run

func Run(parent context.Context, components ...Component) error

Run supervises components until SIGINT/SIGTERM, parent ctx cancellation, or the first non-nil error from any Component.Start.

Forward order = startup order. Reverse order = drain order (the OBS-08 contract — encoded by the caller's slice order).

Run returns errors.Join(runErr, shutdownErr) so callers observe both the trigger and any cleanup failures. A clean signal-driven exit returns nil.

Critical correctness properties enforced here:

  1. signal.NotifyContext (Go 1.16+) replaces the channel-leak-prone signal.Notify(chan, ...) + <-chan pattern. The deferred stop() releases the signal handler.
  2. errgroup.WithContext cancels gctx on the FIRST non-nil Start error. Components observe gctx and exit cleanly.
  3. The shutdown ctx is derived from context.Background(), NOT gctx. gctx is already cancelled by the time we reach the drain loop; deriving from it would return immediately and break the OBS-09 fresh-context guarantee.
  4. Components are drained in REVERSE slice order (OBS-08). DrainReverse continues past per-component Shutdown errors so a single faulty component cannot strand others.

Types

type Component

type Component interface {
	Name() string
	Start(ctx context.Context) error
	Shutdown(ctx context.Context) error
}

Component is anything the supervisor can start concurrently and shut down on demand.

Start blocks until the component exits naturally or ctx is cancelled. Returning a non-nil error from Start cancels the entire errgroup and triggers the reverse-order drain.

Shutdown is invoked once Start has returned for ALL components, in reverse registration order. The ctx passed to Shutdown has a fresh ~30s deadline whose lifetime is INDEPENDENT of the cancelled supervisor context (ADR-0001 §2.8.1 A10a / OBS-09). Implementations must honor the deadline but should NOT assume it is the same ctx that cancelled Start.

Name is used to tag wrapped errors and enable structured logging. It must be stable for the lifetime of the component.

type FakeComponent

type FakeComponent struct {

	// OnStart, when non-nil, replaces the default Start behavior
	// (which blocks on <-ctx.Done() and returns nil).
	OnStart func(ctx context.Context) error

	// OnShutdown, when non-nil, replaces the default Shutdown behavior
	// (which returns nil immediately).
	OnShutdown func(ctx context.Context) error
	// contains filtered or unexported fields
}

FakeComponent is a deterministic test fixture implementing Component. It records start/shutdown call counts and monotonic timestamps so tests can assert ordering invariants (e.g. reverse-drain order).

FakeComponent lives in production source rather than _test.go so that downstream packages — pkg/observability adapters in Plan 01-03, integration tests in Plan 01-04 — can reuse it without duplicating the fixture. This mirrors the colocated `MemoryEngine` pattern in pkg/storage/memory.go.

All counters/timestamps are atomic; the struct is safe for concurrent use across the supervisor's errgroup goroutines.

func NewFakeComponent

func NewFakeComponent(name string) *FakeComponent

NewFakeComponent returns a fresh FakeComponent with the given name. Override OnStart / OnShutdown on the returned pointer to inject test behavior.

func (*FakeComponent) Name

func (f *FakeComponent) Name() string

Name implements Component.

func (*FakeComponent) Shutdown

func (f *FakeComponent) Shutdown(ctx context.Context) error

Shutdown records the invocation timestamp + count, then either calls the caller-supplied OnShutdown override or returns nil.

Order matters: shutdownAt + shutdownSeq are stamped BEFORE shutdownCount is incremented (same reasoning as Start above).

func (*FakeComponent) ShutdownAtNanos

func (f *FakeComponent) ShutdownAtNanos() int64

ShutdownAtNanos returns the UnixNano timestamp of the first Shutdown invocation, or 0 if Shutdown has not yet been called.

func (*FakeComponent) ShutdownBefore

func (f *FakeComponent) ShutdownBefore(other *FakeComponent) bool

ShutdownBefore reports whether this component's Shutdown was entered strictly before other's. Uses a monotonic process-wide sequence counter to remain deterministic when consecutive Shutdowns land within the same wall-clock nanosecond (e.g. the supervisor's tight reverse-drain loop). Returns false if either component has not recorded a Shutdown.

func (*FakeComponent) ShutdownCount

func (f *FakeComponent) ShutdownCount() int32

ShutdownCount returns how many times Shutdown has been entered.

func (*FakeComponent) Start

func (f *FakeComponent) Start(ctx context.Context) error

Start records the invocation timestamp + count, then either calls the caller-supplied OnStart override or blocks on ctx.

Order matters: startedAt + startSeq are stamped BEFORE startCount is incremented, so any reader polling Eventually(StartCount()==1) and then reading StartedAtNanos()/startSeq is guaranteed to observe the stamped values. Inverting the order causes a 40%-repro race under -count=10.

func (*FakeComponent) StartCount

func (f *FakeComponent) StartCount() int32

StartCount returns how many times Start has been entered.

func (*FakeComponent) StartedAtNanos

func (f *FakeComponent) StartedAtNanos() int64

StartedAtNanos returns the UnixNano timestamp of the first Start invocation, or 0 if Start has not yet been called.

func (*FakeComponent) StartedBefore

func (f *FakeComponent) StartedBefore(other *FakeComponent) bool

StartedBefore reports whether this component's Start was entered strictly before other's. Uses a monotonic process-wide sequence counter to remain deterministic when consecutive Starts land within the same wall-clock nanosecond. Returns false if either component has not recorded a Start.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL