warren

package
v1.51.0 Latest Latest
Warning

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

Go to latest
Published: Sep 3, 2026 License: AGPL-3.0 Imports: 8 Imported by: 0

Documentation

Overview

Package warren is a lightweight lifecycle coordinator for Go applications.

It provides phased startup, tracked goroutine management, validated setter injection, typed test overrides, and component health checks. It is designed to sit alongside explicit constructor-based dependency wiring — not to replace it.

Philosophy

Warren is NOT a service registry. It never holds references to your services at runtime, resolves dependencies by reflection, or require you to annotate your structs. You write normal constructor functions; warren coordinates when they run and what happens when the process shuts down.

The three rules:

  1. Never pass *App to a service method — only to constructors and phase functions.
  2. Every background goroutine must be registered with Go() so Stop() can wait for it.
  3. Every required post-construction setter must be registered with Wire so Validate() can catch omissions.

Quick start

app := warren.New()

app.Phase("core", func(ctx context.Context, a *warren.App) error {
    cfg := config.Load()
    repo, err := db.Open(cfg)
    if err != nil {
        return err
    }
    a.OnStop("db", func(ctx context.Context) error { return repo.Close() })
    a.Health("db", repo.Ping)
    return nil
})

app.Phase("runtime", func(ctx context.Context, a *warren.App) error {
    a.Go("poller", func(ctx context.Context) {
        for {
            select {
            case <-ctx.Done():
                return
            case <-time.After(5 * time.Second):
                poll()
            }
        }
    })
    return nil
})

if err := app.Run(ctx); err != nil {
    log.Fatal(err)
}

Components

App — lifecycle coordinator, see App.

GoroutineGroup — standalone goroutine tracking for use inside services, see GoroutineGroup.

Binding — typed overridable component slot for test injection, see Binding.

Wire — validates that all required post-construction setters were called, see Wire and Set.

HealthReport — aggregate health status, see App.Check.

Index

Constants

View Source
const DefaultShutdownTimeout = 30 * time.Second

DefaultShutdownTimeout is the time Stop() waits for goroutines to exit before reporting them as leaks.

Variables

This section is empty.

Functions

func IsMultiError

func IsMultiError(err error) bool

IsMultiError reports whether err is or wraps a *MultiError.

func Set

func Set[T comparable](w *Wire, name string, setter func(T), value T)

Set calls setter(value) immediately and records the call as applied. It is a package-level generic function so that setter's type parameter is inferred from value, giving a compile-time guarantee that the correct type is passed to the correct setter.

If value is the zero value for T (nil for pointers and interfaces), the setter is NOT called and the entry is recorded as skipped. Call Validate() to surface any skipped setters as an error.

Example:

warren.Set(w, "StatusManager", svc.SetStatusManager, statusMgr)

func SetAlways

func SetAlways[T any](w *Wire, name string, setter func(T), value T)

SetAlways calls setter(value) unconditionally and records it as applied. Use this for setters that accept zero values (e.g. bool, int, empty slices).

Types

type App

type App struct {
	ShutdownTimeout time.Duration
	// contains filtered or unexported fields
}

App is the application lifecycle coordinator. It manages phased startup, background goroutines, ordered shutdown, and component health checks.

App is NOT a service registry. Never pass *App to service methods — it belongs only in the wiring layer (Phase functions and constructors called from Phase functions).

The typical lifecycle is:

  1. Declare phases with Phase()
  2. Call Run() (or Start() + Stop()) from main
  3. Inside each Phase fn: construct components, register goroutines with Go(), register cleanup with OnStop(), register checks with Health()

func New

func New() *App

New creates an App with DefaultShutdownTimeout.

func TestApp

func TestApp(t testing.TB) *App

TestApp creates an App for use in tests. It sets a short ShutdownTimeout (2 seconds) to make goroutine leak detection fast. Stop() is registered via t.Cleanup and called automatically when the test ends.

Usage:

func TestMyComponent(t *testing.T) {
    app := warren.TestApp(t)
    app.Phase("setup", func(ctx context.Context, a *warren.App) error {
        // construct components with test doubles
        return nil
    })
    if err := app.Start(context.Background()); err != nil {
        t.Fatal(err)
    }
    // test body — Stop() is called automatically via t.Cleanup
}

func (*App) Active

func (a *App) Active() map[string]int

Active returns the names and counts of currently running goroutines. Returns nil if Start() has not been called.

func (*App) Check

func (a *App) Check() HealthReport

Check runs all registered health checks and returns an aggregate report. Checks run sequentially. A failed check does not prevent subsequent checks from running.

func (*App) Go

func (a *App) Go(name string, fn func(ctx context.Context))

Go registers a named background goroutine that is tracked by the App. fn must return when ctx is cancelled. Stop() waits for all registered goroutines to exit before running stop hooks.

Go may only be called from within a Phase function (i.e. after Start() has initialised the goroutine group). Panics if called before Start().

Multiple goroutines may share a name; Active() reports counts per name.

func (*App) Health

func (a *App) Health(name string, fn func() error)

Health registers a named health check function. Check() runs all registered functions and returns an aggregate HealthReport.

func (*App) OnStop

func (a *App) OnStop(name string, fn func(ctx context.Context) error)

OnStop registers a named cleanup function. Stop() calls registered functions in reverse registration order so that components shut down in the opposite order they started.

OnStop may be called before or after Start().

func (*App) Phase

func (a *App) Phase(name string, fn func(ctx context.Context, app *App) error) *App

Phase registers a named startup phase. Phases execute sequentially in registration order when Start() is called.

fn receives the root context and the App itself. Inside fn:

  • construct your components
  • call a.Go() to register background goroutines
  • call a.OnStop() to register cleanup hooks
  • call a.Health() to register health checks

Panics if called after Start().

func (*App) Run

func (a *App) Run(ctx context.Context) error

Run is a convenience wrapper that calls Start(ctx), blocks until ctx is done, then calls Stop() with a fresh context bounded by ShutdownTimeout.

This is the typical entry point from main():

ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
defer cancel()
if err := app.Run(ctx); err != nil {
    log.Fatal(err)
}

func (*App) Start

func (a *App) Start(ctx context.Context) error

Start runs all registered phases sequentially using ctx as the lifecycle context. ctx is also the parent context for all goroutines registered with Go().

Returns on the first phase error. Phases are not retried. Returns an error if called more than once.

func (*App) Stop

func (a *App) Stop(ctx context.Context) error

Stop shuts down the application:

  1. Cancels the goroutine context so all registered goroutines receive a done signal.
  2. Waits up to ShutdownTimeout for goroutines to exit. Any still running are reported as leaks.
  3. Calls all OnStop functions in reverse registration order, using ctx (typically a short-deadline context) as the cleanup context.

Returns a *MultiError if any step produced errors, including goroutine leaks.

type Binding

type Binding[T any] struct {
	// contains filtered or unexported fields
}

Binding is a typed, optionally-overridable component slot.

It solves the problem of making specific components swappable in tests without passing the whole dependency tree or modifying constructor signatures. Declare a Binding as a package-level variable alongside the component that owns it:

// session/repo.go
var Repo = warren.NewBinding[Repository]("session.repo")

// In wiring (called once at startup):
warren.Set(w, "repo", func(r Repository) { session.Repo.Set(r) }, realRepo)

// In any consumer:
repo := session.Repo.Must()

// In a test:
session.Repo.Override(t, &fakeRepo{})   // auto-restored when t ends

Binding is safe for concurrent use. Override is only valid in tests and restores the previous value automatically via t.Cleanup.

func NewBinding

func NewBinding[T any](name string) *Binding[T]

NewBinding creates a new Binding with the given name. The name is used in error messages and has no runtime significance.

func (*Binding[T]) Get

func (b *Binding[T]) Get() (T, bool)

Get returns the bound value and whether it has been set.

func (*Binding[T]) IsSet

func (b *Binding[T]) IsSet() bool

IsSet reports whether the binding has been set.

func (*Binding[T]) Must

func (b *Binding[T]) Must() T

Must returns the bound value. Panics with a descriptive message if the binding has not been set. Use this in constructors that require the binding.

func (*Binding[T]) Name

func (b *Binding[T]) Name() string

Name returns the binding's descriptive name.

func (*Binding[T]) Override

func (b *Binding[T]) Override(t testing.TB, v T)

Override replaces the bound value for the duration of a test. The previous value (and isSet state) is automatically restored via t.Cleanup. Safe to call multiple times in the same test; each call stacks a restore.

Override is intentionally defined to accept testing.TB so it cannot accidentally be called outside of tests.

func (*Binding[T]) Set

func (b *Binding[T]) Set(v T)

Set stores value. Intended to be called once during the wiring phase. Calling Set again overwrites the previous value.

type CheckResult

type CheckResult struct {
	Name    string
	Healthy bool
	// Err is non-nil when the check failed.
	Err     error
	Latency time.Duration
}

CheckResult is the outcome of a single named health check.

type GoroutineGroup

type GoroutineGroup struct {
	// contains filtered or unexported fields
}

GoroutineGroup tracks named background goroutines spawned during application startup. It provides:

  • Context propagation: every goroutine receives a context that is cancelled when the group is stopped.
  • Leak detection: Wait() returns the names of any goroutines that did not exit within the configured timeout.
  • Multiplicity tracking: multiple goroutines may share a name; the Active() report shows each name once with a count.

Typical use is through App.Go. Use GoroutineGroup directly when you need goroutine tracking inside a service that is unaware of the App lifecycle.

func NewGoroutineGroup

func NewGoroutineGroup(parent context.Context) *GoroutineGroup

NewGoroutineGroup creates a GoroutineGroup whose context is derived from parent. Cancelling parent also cancels the group's internal context.

func (*GoroutineGroup) Active

func (g *GoroutineGroup) Active() map[string]int

Active returns a snapshot of currently running goroutine names and their counts, sorted alphabetically.

func (*GoroutineGroup) ActiveNames

func (g *GoroutineGroup) ActiveNames() []string

ActiveNames returns the sorted list of names with at least one running goroutine.

func (*GoroutineGroup) Context

func (g *GoroutineGroup) Context() context.Context

Context returns the group's context. Goroutines can use this directly, or they can use the context passed to Go's fn parameter (which is the same value).

func (*GoroutineGroup) Go

func (g *GoroutineGroup) Go(name string, fn func(ctx context.Context))

Go spawns a named tracked goroutine. fn receives the group's context; it must return when that context is cancelled.

Multiple goroutines may be registered under the same name (e.g. per-session pollers). Active() reports the count per name.

func (*GoroutineGroup) Stop

func (g *GoroutineGroup) Stop()

Stop cancels the group context. Goroutines that respect context cancellation will begin shutting down.

func (*GoroutineGroup) Wait

func (g *GoroutineGroup) Wait(timeout time.Duration) []string

Wait stops the group and blocks until all goroutines exit or timeout elapses. Returns the names of any goroutines still running after the timeout (leaks). An empty slice means clean shutdown.

type HealthReport

type HealthReport struct {
	// Healthy is true only when every registered check passed.
	Healthy bool
	// Checks contains one result per registered health check, in registration order.
	Checks []CheckResult
}

HealthReport is the aggregate result of running all registered health checks.

type MultiError

type MultiError struct {
	Errors []error
}

MultiError collects multiple errors from phased startup or shutdown. It implements the error interface and supports errors.Is / errors.As unwrapping.

func (*MultiError) Error

func (m *MultiError) Error() string

func (*MultiError) Unwrap

func (m *MultiError) Unwrap() []error

Unwrap returns all contained errors for use with errors.Is / errors.As.

type Wire

type Wire struct {
	// contains filtered or unexported fields
}

Wire validates that all required post-construction setters were called during component wiring. It solves the "forgotten Set*" class of bugs where a setter is silently omitted, leaving a component with a nil field that panics later.

Usage:

w := warren.NewWire("SessionService")
warren.Set(w, "StatusManager",    svc.SessionService.SetStatusManager,    statusMgr)
warren.Set(w, "ScrollbackManager", svc.SessionService.SetScrollbackManager, sbMgr)
warren.Set(w, "HistoryLinker",     svc.SessionService.SetHistoryLinker,     linker)
if err := w.Validate(); err != nil {
    return err
}

Set is a package-level generic function rather than a method because Go does not support generic methods. This is the standard Go pattern for typed helpers on non-generic types.

func NewWire

func NewWire(component string) *Wire

NewWire creates a Wire validator for the named component. The component name appears in validation error messages.

func (*Wire) Applied

func (w *Wire) Applied() int

Applied returns the count of setters that were successfully applied.

func (*Wire) Mark

func (w *Wire) Mark(name string)

Mark records that the setter named name was applied. Use in combination with Require() for conditional setter calls.

func (*Wire) MustValidate

func (w *Wire) MustValidate()

MustValidate panics with the validation error if any setter was not applied. Prefer Validate() in production code; use MustValidate() in tests or when a wiring failure is unrecoverable.

func (*Wire) Require

func (w *Wire) Require(name string) *Wire

Require declares that a named setter must be applied by the time Validate() is called. Use when the actual Set call is conditional (e.g. inside an if block) but must always happen for correct behaviour.

If the required entry has not been marked with Mark() when Validate() runs, Validate() returns an error.

func (*Wire) Total

func (w *Wire) Total() int

Total returns the total number of registered setters.

func (*Wire) Validate

func (w *Wire) Validate() error

Validate returns an error listing every setter that was not applied. Returns nil if all registered setters were applied.

Jump to

Keyboard shortcuts

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