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 ¶
- type App
- func (a *App) Context() context.Context
- func (a *App) Go(name string, fn func(ctx context.Context) error)
- func (a *App) OnShutdown(name string, fn func(ctx context.Context) error)
- func (a *App) Run(parent context.Context) error
- func (a *App) Shutdown() error
- func (a *App) Supervise(name string, start func(ctx context.Context) (done <-chan struct{}))
- type GoroutineError
- type HookError
- type Options
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:
- New — create; App.Context is live immediately (no signal handlers are installed until App.Run).
- Bind ports/adapters with App.Context; register teardown with App.OnShutdown; start long-lived work with App.Go.
- 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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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) LogValue ¶
LogValue implements slog.LogValuer for structured logging.
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.