application

package module
v0.6.0 Latest Latest
Warning

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

Go to latest
Published: Jul 15, 2026 License: MIT Imports: 25 Imported by: 0

README

application

Go Reference Test

application is a Go package for composing services from ordered lifecycle modules. It provides deterministic startup and shutdown, transactional HCL configuration, explicit cancellation, and structured lifecycle errors without taking ownership of behavior that belongs in individual modules.

Features

  • Ordered, single-use application lifecycle with explicit state.
  • Required Start and Stop hooks plus optional initialize, install, pre/post start, and pre/post stop phases.
  • Reverse-order teardown for every module whose Start hook was entered.
  • One application-wide graceful shutdown deadline.
  • Hierarchical settings backed by github.com/renevo/config.
  • Native HCL and JSON configuration with transactional validation and commit.
  • Automatic environment configuration with optional application prefixes.
  • Typed structured HCL bindings for repeated, labeled, and nested blocks.
  • Atomic runtime reload across registered scalar configuration sources.
  • Opt-in interrupt and termination signal handling.
  • Structured errors compatible with errors.Is and errors.As.

Requirements

  • Go 1.26 or later.

Installation

go get github.com/renevo/application

Quick Start

A module implements Start and Stop. It can optionally implement lifecycle interfaces such as Initializer to register settings before configuration is loaded.

package main

import (
	"context"
	"log/slog"
	"os"
	"time"

	"github.com/renevo/application"
	"github.com/renevo/config"
)

type worker struct {
	interval time.Duration
}

func (module *worker) Initialize(ctx *application.Context) error {
	ctx.Settings().Subset("worker").Setting(
		"interval",
		&module.interval,
		"Delay between work cycles",
	)
	return nil
}

func (module *worker) Start(ctx *application.Context) error {
	ctx.Logger().Info("worker started", "interval", module.interval)
	return nil
}

func (*worker) Stop(ctx *application.Context) error {
	ctx.Logger().Info("worker stopped")
	return nil
}

func main() {
	app, err := application.New(
		"example",
		"1.0.0",
		application.WithConfigSources(
			application.ConfigFileSource("application.hcl"),
			config.EnvironmentSource(""),
		),
		application.WithModule("worker", &worker{interval: 30 * time.Second}),
	)
	if err != nil {
		slog.Error("create application", "error", err)
		os.Exit(1)
	}

	if err := app.Run(context.Background(), application.WithSignals()); err != nil {
		slog.Error("run application", "error", err)
		os.Exit(1)
	}
}
worker {
  interval = "10s"
}

See examples/simple for a runnable two-module application that demonstrates settings, structured HCL, signal handling, and reverse-order shutdown.

The complete public API is available on pkg.go.dev.

Lifecycle

Module registration order defines lifecycle order. Registration is frozen before initialization begins.

Phase Order Interface
Initialize Forward Initializer
Install Forward, opt-in Installer
Pre-start Forward PreStarter
Start Forward Module
Post-start Forward PostStarter
Pre-stop Reverse PreStopper
Stop Reverse Module
Post-stop Reverse PostStopper

The application is single-use. Validate may prepare an application before one call to Run. Install is a separate terminal workflow. Concurrent or invalid operations return sentinel errors rather than waiting indefinitely.

If Start fails, the module whose hook was entered is included in teardown; later modules whose Start hooks were not entered are excluded. Shutdown hooks continue after errors, and failures are combined with errors.Join.

Configuration

Settings

Modules register scalar settings during Initialize through Context.Settings. Dot-separated setting paths map to nested singleton HCL blocks. For example, http.server.read_timeout maps to:

http {
  server {
    read_timeout = "5s"
  }
}

Settings are decoded through their registered codecs and committed atomically. By default, environment values are loaded for every registered scalar setting. Dots and other separators become underscores and names are uppercased, so Http.Address maps to HTTP_ADDRESS and Http.Server.Read_timeout maps to HTTP_SERVER_READ_TIMEOUT.

WithConfigSources replaces the complete ordered source list. Later sources override earlier sources. Use config.EnvironmentSource with a prefix to namespace generated environment variable names:

app, err := application.New(
	"example",
	"1.0.0",
	application.WithConfigSources(config.EnvironmentSource("MYAPP")),
)

The prefixed address setting is read from MYAPP_HTTP_ADDRESS. Prefixes are uppercased and otherwise preserved. They may contain ASCII letters, digits, and underscores, and must start with a letter or underscore. Invalid prefixes fail when configuration loads. Calling WithConfigSources() with no arguments disables external sources and loads registered defaults only.

For the common ConfigFileSource(file), EnvironmentSource(prefix) ordering, scalar precedence is registered default, then HCL or JSON, then environment. Reversing those source arguments makes the file override the environment. Environment text is decoded by the setting's registered codec, so values such as durations and booleans use the same validation as file values. A present environment variable with an empty value is an explicit override. If two setting paths normalize to the same environment name, configuration loading fails rather than choosing one.

Application.Reload atomically reloads the configured scalar sources in the same order. Removing an environment variable reveals an earlier source value or the registered default. Failed reloads retain the last committed settings.

Structured HCL

Use Context.BindConfig during Initialize for configuration that does not fit a scalar setting tree, including repeated blocks, labels, and composite values:

type route struct {
	Name   string `config:"name,label"`
	Target string `config:"target"`
}

type routerConfig struct {
	Routes []route `config:"route,block"`
}

func (module *router) Initialize(ctx *application.Context) error {
	return ctx.BindConfig(&module.config)
}

Structured bindings are staged and published only after the complete initial configuration succeeds. They are startup-only and are not changed by runtime reload. File reload still validates structured HCL before committing scalar settings.

Both native HCL (.hcl) and JSON (.json) files are supported.

Starter configuration

Application.WriteConfigTemplate writes a complete native HCL starter file from defaults registered during module initialization. It does not read the configured sources, so it can create a file selected by ConfigFileSource before that file exists.

filename := "application.hcl"
file, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o600)
if err != nil {
	return err
}

if err := app.WriteConfigTemplate(context.Background(), file); err != nil {
	_ = file.Close()
	_ = os.Remove(filename)
	return err
}
if err := file.Close(); err != nil {
	_ = os.Remove(filename)
	return err
}

Scalar setting descriptions and structured description tags become HCL comments. Formatted defaults for boolean and numeric value types use native HCL literals when valid; other formatted defaults use quoted HCL strings. Strings and time.Duration therefore remain readable, and custom codec text such as 7 units is preserved. Nil or empty structured block fields produce one commented example block. Template generation is terminal for the Application; construct a new application before calling Run.

Shutdown and Signals

Application.Shutdown is idempotent and nonblocking. The first shutdown cause is preserved and returned by Run; Shutdown(nil) is normal termination. Teardown receives a fresh context with one overall deadline configured by WithShutdownTimeout.

Signal ownership is opt-in:

err := app.Run(ctx, application.WithSignals())

With no arguments, WithSignals handles SIGTERM, SIGABRT, SIGQUIT, SIGINT, SIGHUP, and signal 21. SIGHUP always triggers scalar configuration reload; the other signals request graceful shutdown. Use explicit arguments to select a different set, including WithSignals(syscall.SIGHUP) for reload-only signal handling. Without this option, the package responds only to the parent context and explicit Shutdown or Reload calls.

Error Handling

Lifecycle hook failures are wrapped in *application.PhaseError, which exposes the module, phase, and underlying error:

var phaseErr *application.PhaseError
if errors.As(err, &phaseErr) {
	slog.Error("module lifecycle failed",
		"module", phaseErr.Module,
		"phase", phaseErr.Phase,
		"error", phaseErr.Err,
	)
}

Package sentinel errors support errors.Is for invalid state, concurrent operations, duplicate registration, missing configuration, and related API conditions.

Project Status

The module follows semantic versioning. Releases before v1 may introduce breaking API changes. Pin a specific version in shared or production systems.

Contributing

Issues and pull requests are welcome. See CONTRIBUTING.md for development setup, required checks, and contribution expectations.

License

This project is available under the MIT License.

Documentation

Overview

Package application coordinates configuration and deterministic lifecycle execution for an ordered set of modules.

Modules start serially in registration order and stop serially in reverse order. During initialization, modules may register scalar settings or bind typed HCL structures; the complete initial configuration is validated before the application starts. Scalar configuration sources are ordered explicitly, with later sources overriding earlier sources. Applications are single-use, and process signal handling is opt-in.

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrApplicationBusy indicates that another lifecycle operation is active.
	ErrApplicationBusy = errors.New("application operation already in progress")
	// ErrConfigBindingPhase indicates that BindConfig was called outside Initialize.
	ErrConfigBindingPhase = errors.New("configuration bindings may only be registered during module initialization")
	// ErrDuplicateModule indicates that a module name is already registered.
	ErrDuplicateModule = errors.New("module already registered")
	// ErrInvalidConfigTarget indicates that a binding target is not a non-nil struct or map pointer.
	ErrInvalidConfigTarget = errors.New("configuration target must be a non-nil pointer to a struct or map")
	// ErrInvalidObjectPath indicates that a dotted configuration variable path is malformed.
	ErrInvalidObjectPath = errors.New("invalid object path")
	// ErrInvalidState indicates that an operation is not legal in the current lifecycle state.
	ErrInvalidState = errors.New("invalid application state")
	// ErrModulesFrozen indicates that registration was attempted after preparation began.
	ErrModulesFrozen = errors.New("module registration is frozen")
	// ErrNotRunning indicates that shutdown was requested while Run was inactive.
	ErrNotRunning = errors.New("application is not running")
)

Functions

func ConfigFileSource added in v0.6.0

func ConfigFileSource(filename string) config.Source

ConfigFileSource reads scalar settings and structured bindings from an HCL or JSON file. The filename is read each time configuration is loaded.

func Run

func Run(name, version string, opts ...Option) error

Run constructs an Application and runs it with a background context. It is a convenience for callers that do not need RunOption values such as WithSignals or WithInstall.

Types

type Application

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

Application coordinates configuration and the ordered lifecycle of a fixed set of modules. An Application is single-use: Validate may prepare it for a later Run, while Install and Run are terminal workflows.

Application methods serialize lifecycle operations. A concurrent operation returns ErrApplicationBusy instead of waiting for the active operation.

func FromContext

func FromContext(ctx context.Context) *Application

FromContext returns the Application carried by a framework-derived context. It returns nil when ctx is nil or does not contain an Application.

func New

func New(name, version string, opts ...Option) (*Application, error)

New constructs an Application and applies opts in order. Empty names and versions default to "application" and "0.0.0". Nil options are ignored.

The application starts with a new settings set, slog.Default, and a 30-second shutdown timeout. Option failures are returned with construction context, and the final logger is scoped with the application name.

func (*Application) Exit

func (a *Application) Exit(cause error) error

Exit is an alias for Shutdown.

func (*Application) Install

func (a *Application) Install(ctx context.Context) error

Install prepares the application, then invokes each Installer in registration order. It stops at the first error and does not perform rollback or module teardown because Start has not been entered.

Install is terminal for this Application, ending in StateStopped on success or StateFailed on failure.

func (*Application) Logger

func (a *Application) Logger() *slog.Logger

Logger returns the application-scoped logger. Module lifecycle contexts derive a child logger that also includes the module name.

func (*Application) Module added in v0.3.0

func (a *Application) Module(name string) (Module, bool)

Module returns the module registered under name. Names are matched exactly.

func (*Application) Modules added in v0.3.0

func (a *Application) Modules() iter.Seq2[string, Module]

Modules returns an insertion-ordered snapshot of registered modules. The sequence is unaffected by later lifecycle state changes and may be stopped early by the iterator consumer.

func (*Application) Name

func (a *Application) Name() string

Name returns the application name established during construction.

func (*Application) Reload added in v0.3.0

func (a *Application) Reload(ctx context.Context) error

Reload atomically reloads registered settings from all configured scalar sources in precedence order. Structured targets registered with Context.BindConfig are startup-only and are not modified. A failed reload preserves the last committed settings.

Reload requires StateRunning and returns ErrInvalidState otherwise.

func (*Application) Run

func (a *Application) Run(parent context.Context, runOpts ...RunOption) error

Run prepares and starts the application, waits for cancellation, and tears down started modules. Startup phases run serially in registration order; PreStop, Stop, and PostStop each run serially in reverse order.

A module becomes eligible for teardown immediately before its Start method is called, so a module whose Start fails is cleaned up while later modules are not. Teardown continues after hook failures, and startup, cancellation, and teardown errors are combined with errors.Join.

Run is single-use and terminal. A nil parent is treated as context.Background. Run options are applied before preparation begins.

func (*Application) Settings

func (a *Application) Settings() *config.Set

Settings returns the application's shared configuration set. Modules should register settings during Initialize, before configuration is loaded.

func (*Application) Shutdown added in v0.3.0

func (a *Application) Shutdown(cause error) error

Shutdown requests termination of a running application without waiting for teardown. It is safe for concurrent use; the first request determines the cancellation cause and subsequent requests have no effect.

A nil cause requests normal termination. Shutdown returns ErrNotRunning when Run has not installed its cancellation function or has already returned.

func (*Application) State added in v0.3.0

func (a *Application) State() State

State returns a concurrency-safe snapshot of the current lifecycle state. The application may transition again immediately after State returns.

func (*Application) String

func (a *Application) String() string

String returns the application identity in name/version form.

func (*Application) Validate

func (a *Application) Validate(ctx context.Context) error

Validate initializes all modules and loads the initial configuration without installing or starting modules. A successful call leaves the application in StateReady and may be followed by another Validate or one Run or Install.

A nil context is treated as context.Background. Concurrent lifecycle work returns ErrApplicationBusy.

func (*Application) Version

func (a *Application) Version() string

Version returns the application version established during construction.

func (*Application) WriteConfigTemplate added in v0.5.0

func (a *Application) WriteConfigTemplate(ctx context.Context, writer io.Writer) error

WriteConfigTemplate initializes modules and writes a native HCL starter configuration containing their registered defaults. It does not read the configured file or environment sources. The operation is terminal for the Application once initialization begins, leaving it stopped on success or failed on initialization, rendering, or writing errors. Precondition errors such as a nil writer or invalid state do not change the application state.

A nil context is treated as context.Background. Concurrent lifecycle work returns ErrApplicationBusy.

type Configuration

type Configuration struct{}

Configuration decodes native HCL or JSON into an application's registered settings and structured bindings. Decoding requires a context carrying an Application, normally one created for a lifecycle hook.

func (Configuration) Decode

func (c Configuration) Decode(ctx context.Context, filename string, src []byte) hcl.Diagnostics

Decode performs an initial configuration load from src followed by unprefixed environment values. It does not use sources configured with WithConfigSources. The filename selects HCL or JSON syntax and identifies diagnostic source ranges. The context must carry an Application.

func (Configuration) DecodeFile

func (c Configuration) DecodeFile(ctx context.Context, filename string) hcl.Diagnostics

DecodeFile reads filename and performs an initial configuration load using the file followed by unprefixed environment values. It does not use sources configured with WithConfigSources. The filename extension selects native HCL or JSON syntax. Failures are returned as HCL diagnostics.

func (Configuration) EvalContext

func (Configuration) EvalContext(ctx context.Context) *hcl.EvalContext

EvalContext returns the expression environment used by configuration loads. It contains Stdlib functions and host.name. When ctx carries an Application, application.name and application.version are also available.

func (Configuration) ReloadFile added in v0.3.0

func (c Configuration) ReloadFile(ctx context.Context, filename string) hcl.Diagnostics

ReloadFile reads filename and atomically reloads registered settings using the file followed by unprefixed environment values. It does not use sources configured with WithConfigSources. Structured targets registered with Context.BindConfig are validated but not reassigned. Failures are returned as diagnostics and preserve committed settings.

type Context added in v0.3.0

type Context struct {
	// Context carries cancellation, deadlines, and values for the current hook.
	context.Context
	// contains filtered or unexported fields
}

Context is the framework-created context passed to module lifecycle hooks. It embeds the hook's cancellation and deadline context and exposes application capabilities scoped to the current module.

func (*Context) Application added in v0.3.0

func (c *Context) Application() *Application

Application returns the application invoking the lifecycle hook.

func (*Context) BindConfig added in v0.3.0

func (c *Context) BindConfig(target any) error

BindConfig registers target for structured HCL decoding during initial configuration. It is valid only from a module's Initialize method and target must be a non-nil pointer to a struct or map.

Targets are assigned after the initial configuration has decoded and settings have committed successfully. Runtime reload does not modify bound targets.

func (*Context) Cause added in v0.3.0

func (c *Context) Cause() error

Cause returns the cancellation cause of the embedded lifecycle context.

func (*Context) Logger added in v0.3.0

func (c *Context) Logger() *slog.Logger

Logger returns the application logger scoped with the current module name. Contexts not associated with a module return the application logger directly.

func (*Context) Settings added in v0.3.0

func (c *Context) Settings() *config.Set

Settings returns the application's shared configuration set. Settings should be registered during Initialize so they are included in the initial load.

func (*Context) Shutdown added in v0.3.0

func (c *Context) Shutdown(cause error) error

Shutdown requests application termination and preserves the first cause. It has the same concurrency and state semantics as Application.Shutdown.

func (*Context) State added in v0.3.0

func (c *Context) State() State

State returns a snapshot of the application's current lifecycle state.

type Initializer

type Initializer interface {
	// Initialize prepares the module and registers its configuration surface.
	Initialize(ctx *Context) error
}

Initializer prepares a module before configuration is loaded. Initialize runs once in registration order and is the phase in which modules register settings and structured configuration bindings.

type Installer

type Installer interface {
	// Install performs optional setup that precedes module startup.
	Install(ctx *Context) error
}

Installer performs optional setup before startup. Install methods run in registration order through Application.Install or Run with WithInstall. The first error stops installation; the application does not perform rollback.

type Module

type Module interface {
	// Start begins the module's runtime work.
	Start(ctx *Context) error
	// Stop releases resources acquired after Start was entered.
	Stop(ctx *Context) error
}

Module is the required lifecycle contract for an application component. Start runs in registration order. Stop runs in reverse order for every module whose Start method was entered, including a module whose Start returned an error. Implementations should honor cancellation and deadlines on ctx.

type Option

type Option func(app *Application) error

Option configures an Application during New. Returning an error aborts construction; nil options are ignored.

func WithConfigSources added in v0.6.0

func WithConfigSources(sources ...config.Source) Option

WithConfigSources replaces the ordered scalar configuration sources. Later sources override earlier sources. Calling WithConfigSources with no sources disables external configuration and loads registered defaults only.

func WithLogger

func WithLogger(logger *slog.Logger) Option

WithLogger replaces the default logger. New derives an application-scoped logger from it after all options have been applied. A nil logger is rejected.

func WithModule

func WithModule(name string, m Module) Option

WithModule registers a module under an exact, nonempty name. Registration rejects nil modules, duplicate names, and changes after preparation freezes the registry. Sentinel failures remain inspectable with errors.Is.

func WithShutdownTimeout added in v0.3.0

func WithShutdownTimeout(timeout time.Duration) Option

WithShutdownTimeout sets the single deadline shared by the complete PreStop, Stop, and PostStop sequence. The timeout must be positive.

type PhaseError added in v0.3.0

type PhaseError struct {
	// Module is the registered name of the module whose hook failed.
	Module string
	// Phase identifies the lifecycle hook, such as "start" or "post-stop".
	Phase string
	// Err is the error returned by the module hook.
	Err error
}

PhaseError identifies a module lifecycle hook failure. It may appear inside an errors.Join result and can be located with errors.As. Err is the original hook error.

func (*PhaseError) Error added in v0.3.0

func (e *PhaseError) Error() string

Error formats the module, lifecycle phase, and underlying error.

func (*PhaseError) Unwrap added in v0.3.0

func (e *PhaseError) Unwrap() error

Unwrap returns the lifecycle hook error for errors.Is and errors.As.

type PostStarter

type PostStarter interface {
	// PostStart performs work after all modules have started successfully.
	PostStart(ctx *Context) error
}

PostStarter runs in registration order after every Module.Start method has succeeded. An error triggers teardown of all started modules.

type PostStopper added in v0.3.0

type PostStopper interface {
	// PostStop performs teardown work after all modules have stopped.
	PostStop(ctx *Context) error
}

PostStopper runs in reverse registration order after every Module.Stop method. Errors are accumulated with earlier teardown failures.

type PreStarter

type PreStarter interface {
	// PreStart performs work that must finish before any module starts.
	PreStart(ctx *Context) error
}

PreStarter runs in registration order before any Module.Start method. The first error aborts startup.

type PreStopper added in v0.3.0

type PreStopper interface {
	// PreStop performs teardown work before any module stops.
	PreStop(ctx *Context) error
}

PreStopper runs in reverse registration order before any Module.Stop method. Errors are accumulated and do not prevent remaining teardown hooks.

type RunOption added in v0.3.0

type RunOption func(*runOptions) error

RunOption configures one call to Application.Run. Returning an error aborts the call before application preparation; nil options are ignored.

func WithInstall added in v0.3.0

func WithInstall() RunOption

WithInstall runs Installer hooks before startup. Installation stops at the first error and does not trigger module teardown because Start was not entered.

func WithSignals added in v0.3.0

func WithSignals(signals ...os.Signal) RunOption

WithSignals enables process signal handling for a Run call. With no explicit signals, it handles SIGTERM, SIGABRT, SIGQUIT, SIGINT, SIGHUP, and signal 21. Nil signals are rejected. SIGHUP always reloads scalar configuration; all other signals request graceful shutdown.

type State added in v0.3.0

type State uint8

State identifies the current phase or terminal outcome of an Application. Values are observational snapshots; callers do not drive transitions directly.

const (
	// StateNew indicates that preparation has not started and modules may still
	// be registered through construction options.
	StateNew State = iota
	// StateInitializing indicates that module Initializer hooks are running.
	StateInitializing
	// StateConfiguring indicates that registered settings and bindings are being loaded.
	StateConfiguring
	// StateReady indicates successful preparation and readiness for Run or Install.
	StateReady
	// StateInstalling indicates that optional Installer hooks are running.
	StateInstalling
	// StateStarting covers the PreStart, Start, and PostStart phases.
	StateStarting
	// StateRunning indicates that startup completed and the application is waiting for shutdown.
	StateRunning
	// StateStopping covers the reverse PreStop, Stop, and PostStop phases.
	StateStopping
	// StateStopped is the terminal outcome of a successful workflow.
	StateStopped
	// StateFailed is the terminal outcome of a failed workflow.
	StateFailed
)

func (State) String added in v0.3.0

func (s State) String() string

String returns the stable lowercase name of s, or "unknown" for an unrecognized value.

Directories

Path Synopsis
Package confighcl allows decoding HCL configurations into config data structures.
Package confighcl allows decoding HCL configurations into config data structures.
funcs
Package funcs provides the expression functions exposed by application HCL evaluation contexts.
Package funcs provides the expression functions exposed by application HCL evaluation contexts.
examples
simple command
Command simple demonstrates a two-module application loaded from HCL.
Command simple demonstrates a two-module application loaded from HCL.
simple/modules/http
Package httpmodule demonstrates a module with settings and structured HCL.
Package httpmodule demonstrates a module with settings and structured HCL.
simple/modules/poller
Package poller demonstrates a module configured only through application settings.
Package poller demonstrates a module configured only through application settings.

Jump to

Keyboard shortcuts

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