app

package
v0.12.0 Latest Latest
Warning

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

Go to latest
Published: Jul 29, 2026 License: MIT Imports: 10 Imported by: 0

Documentation

Overview

Package app is a minimal application lifecycle manager for go-codex services: one root context with the observer pre-injected, supervised goroutines with fail-fast semantics, and ordered (LIFO) shutdown hooks.

It is a shutdown-ordering helper, not a framework: ports and adapters know nothing about it — App owns the context they are bound with and runs the teardown hooks main() would otherwise choreograph by hand.

a := app.New(app.Options{Observer: obs, Logger: logger})
ctx := a.Context() // observer pre-injected; cancelled on shutdown

exports.Bind(ctx, file.DrainWriteFileAdapter(exportFile, varsFor, opts))
exports.Start(ctx)
a.OnShutdown("exports", func(context.Context) error { return exports.Close() })

a.Go("alerts-feed", func(ctx context.Context) error {
    alerts.Feed(ctx, alertPayloads) // returns on ctx cancel
    return nil
})

if err := a.Run(context.Background()); err != nil { // SIGINT/SIGTERM → ordered teardown
    slog.Error("shutdown finished with errors", "error", err)
}

Demos and tests that are not signal-driven call App.Shutdown directly instead of App.Run — both share the same teardown path.

Error policy is fail-fast, errgroup-style: the first supervised goroutine that returns a non-nil error cancels the app; all goroutine and hook errors are still collected into the errors.Join result of Run/Shutdown. Adapters that should survive errors handle them internally (per-adapter OnError) and return nil.

Example

Example demonstrates the full lifecycle: supervised work, LIFO teardown, and a direct (non-signal) shutdown.

package main

import (
	"context"
	"fmt"
	"log/slog"

	"github.com/DaniDeer/go-codex/app"
)

func main() {
	a := app.New(app.Options{Logger: slog.New(slog.DiscardHandler)})

	a.OnShutdown("close-storage", func(context.Context) error {
		fmt.Println("storage closed")
		return nil
	})
	a.OnShutdown("close-server", func(context.Context) error {
		fmt.Println("server closed") // registered last → runs first
		return nil
	})

	a.Go("worker", func(ctx context.Context) error {
		<-ctx.Done() // do work until shutdown
		return nil
	})

	if err := a.Shutdown(); err != nil {
		fmt.Println("shutdown errors:", err)
	}
}
Output:
server closed
storage closed

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type App

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

App is a minimal application lifecycle manager: one cancelable root context with the observer pre-injected, supervised goroutines (fail-fast), and LIFO shutdown hooks. Construct with New; zero value is not usable.

Lifecycle:

  1. New — create; App.Context is live immediately (no signal handlers are installed until App.Run).
  2. Bind ports/adapters with App.Context; register teardown with App.OnShutdown; start long-lived work with App.Go.
  3. App.Run (services: blocks until SIGINT/SIGTERM, parent cancellation, or the first goroutine failure) or App.Shutdown (demos/tests: direct teardown). Both share one ordered teardown path.

func New

func New(opts Options) *App

New returns an App with a live, cancelable root context. Constructing an App installs no signal handlers — that happens inside App.Run only.

func (*App) Context

func (a *App) Context() context.Context

Context returns the app's root context: cancelable, with Options.Observer pre-injected. Use it for every Bind/Feed/Start call. It is cancelled when shutdown begins (signal, supervised-goroutine failure, parent cancellation, or a direct App.Shutdown call).

func (*App) Go

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

Go runs fn in a supervised goroutine. A non-nil return CANCELS the app (fail-fast, errgroup-style) and is collected — wrapped in GoroutineError — into the error returned by App.Run/App.Shutdown. A nil return just logs completion. fn should return when its ctx is done.

Calling Go after shutdown has begun is a safe no-op (logged).

func (*App) OnShutdown

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

OnShutdown registers a hook run during shutdown in LIFO order (last registered, first run — matching defer semantics: close what you opened last, first). Each hook receives a context bounded by Options.ShutdownTimeout. Hook errors are collected — wrapped in HookError — and logged; a failing hook never stops later hooks.

Calling OnShutdown after shutdown has begun is a safe no-op (logged) — the hook would never run.

func (*App) Run

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

Run blocks until SIGINT/SIGTERM, parent cancellation, or the first supervised-goroutine failure — then performs the ordered teardown: cancel App.Context, wait for all App.Go goroutines, run the App.OnShutdown hooks (LIFO, each bounded by Options.ShutdownTimeout), and return errors.Join of all goroutine and hook errors (nil on a clean shutdown).

Signal handlers are installed only for the duration of Run.

func (*App) Shutdown

func (a *App) Shutdown() error

Shutdown performs the ordered teardown directly, without signal-waiting: cancel App.Context, wait for supervised goroutines, run the shutdown hooks LIFO, and return errors.Join of all collected errors. For demos, tests, and callers that own their own run loop.

Shutdown is idempotent: the teardown executes once; concurrent and repeated calls block until it completes and return the same result.

func (*App) Supervise

func (a *App) Supervise(name string, start func(ctx context.Context) (done <-chan struct{}))

Supervise starts a non-blocking component (start is called once and returns immediately) and supervises its ACTUAL completion the same way App.Go supervises a blocking function: "finished" is reported only once the returned done channel closes, not when start itself returns.

Use this for components whose start call is fire-and-forget but expose a completion signal — e.g. a [ports.PipePort]:

a.Supervise("sensor-pipeline", func(ctx context.Context) <-chan struct{} {
    p.Connect(ctx)
    return p.Done()
})

Without Supervise, wiring such a component through Go directly (`a.Go(name, func(ctx) error { p.Connect(ctx); return nil })`) would report "finished" the instant Connect returns — essentially immediately, not when the component's internal goroutines have actually drained.

Supervise delegates to Go for all status/duration/error bookkeeping — the same "app.go" observer event, LIFO-independent fail-fast semantics, and after-shutdown no-op behavior apply identically.

type GoroutineError

type GoroutineError struct {
	// Name is the name passed to [App.Go].
	Name string
	// Err is the error the goroutine returned.
	Err error
}

GoroutineError wraps a supervised goroutine's non-nil return. The first GoroutineError cancels the app (fail-fast); all of them appear in the errors.Join result of App.Run/App.Shutdown.

func (GoroutineError) Error

func (e GoroutineError) Error() string

func (GoroutineError) LogValue

func (e GoroutineError) LogValue() slog.Value

LogValue implements slog.LogValuer for structured logging.

func (GoroutineError) Unwrap

func (e GoroutineError) Unwrap() error

Unwrap allows errors.Is and errors.As to traverse the inner error.

type HookError

type HookError struct {
	// Name is the name passed to [App.OnShutdown].
	Name string
	// Err is the error the hook returned.
	Err error
}

HookError wraps a shutdown hook's non-nil return — including context.DeadlineExceeded when the hook exceeded Options.ShutdownTimeout. A failing hook never stops later hooks; all HookErrors appear in the errors.Join result of App.Run/App.Shutdown.

func (HookError) Error

func (e HookError) Error() string

func (HookError) LogValue

func (e HookError) LogValue() slog.Value

LogValue implements slog.LogValuer for structured logging.

func (HookError) Unwrap

func (e HookError) Unwrap() error

Unwrap allows errors.Is and errors.As to traverse the inner error.

type Options

type Options struct {
	// Observer is injected into [App.Context] via [stats.WithObserver], so
	// every port/adapter bound with that context resolves it automatically.
	// Nil means no injection (ports fall back to NoopObserver as usual).
	// App itself reports lifecycle events through it: a
	// RecordRequest("app.go", name, 200|500, duration) per supervised
	// goroutine exit and RecordRequest("app.shutdown", name, 200|500,
	// duration) per shutdown hook.
	Observer stats.Observer

	// Logger receives lifecycle events (goroutine exits, hook results).
	// Nil means slog.Default().
	Logger *slog.Logger

	// ShutdownTimeout bounds the context passed to shutdown hooks.
	// Zero means 10 seconds.
	ShutdownTimeout time.Duration
}

Options configures New.

Jump to

Keyboard shortcuts

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