lifecycle

package module
v1.4.1 Latest Latest
Warning

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

Go to latest
Published: Feb 1, 2026 License: AGPL-3.0 Imports: 18 Imported by: 0

README

lifecycle

Go Report Card Go Doc License Release

lifecycle is a Go library for managing application shutdown signals and interactive terminal I/O robustly. It centralizes the "Dual Signal" logic and "Interruptible I/O" patterns originally extracted from Trellis and designed for any tool needing robust signal handling.

Vision

To provide a standard, leak-free way to handle CLI interruptions (Ctrl+C) and graceful shutdowns across Go CLI applications, handling OS idiosyncrasies (especially Windows CONIN$) transparently.

Project Status & Versioning

[!IMPORTANT] v1.x (Current - LTS): Focuses strictly on Death Management (Graceful Shutdown, Signals, Leak Prevention). This branch is in Maintenance Mode. New features target v2.0.

v2.x (Upcoming): Will introduce the Application Control Plane, generalizing "Signals" into "Events" (Hot Reload, Health Checks, etc).

Installation

go get github.com/aretw0/lifecycle

Features

  • SignalContext: Differentiates between SIGINT (User Interrupt) and SIGTERM (System Shutdown).
    • SIGINT: Captured but doesn't cancel context immediately (allows "Wait, are you sure?" logic).
    • SIGTERM: Cancels context immediately (standard graceful shutdown).
  • TermIO:
    • InterruptibleReader: Wraps io.Reader to allow Read() calls to be abandoned when a context is cancelled (avoids goroutine leaks).
    • Platform Aware: Automatically uses CONIN$ on Windows.
      • Why? On Windows, standard os.Stdin closes immediately upon receiving a signal (like Ctrl+C), causing a fatal EOF before the application can gracefully handle the signal. lifecycle switches to CONIN$, which keeps the handle open, allowing the SignalContext to process the event.
    • UpgradeTerminal: Helper to upgrade an arbitrary existing io.Reader (if it identifies as a terminal) to the safe platform-specific reader.
  • Observability & Introspection:
    • Unified Dashboard: SystemDiagram synthesizes Signal and Worker states into a single Mermaid visualization.
    • Rich Metrics: Built-in providers for tracking shutdown health, data loss, and shutdown latency.
    • Stall Detection: Automatically detects and warns if a shutdown hook is stalled (runs > 5s).
  • Reliability Primitives (v1.4):
    • Critical Sections: lifecycle.Do(ctx, fn) shields atomic operations from cancellation (deferring the signal) until completion.
    • Introspection: SignalContext.Reason() to differentiate between "Manual Stop", "Interrupt", or "Timeout".
  • Worker & Supervisor (v1.3):
    • Unified Interface: Standard Start, Stop, Wait contract for Processes, Goroutines, and Containers.
    • Supervision Tree: Supervisor manages hierarchical worker clusters with restart policies (OneForOne, OneForAll).
    • Dynamic Topology: Add or remove workers at runtime.
    • Functional Workers: Turn any Go function into a managed Worker.
    • Process Hygiene: Automatic cleanup of child processes if the parent dies (Job Objects/PDeathSig).
    • Handover Protocol: Standardized environment variables (LIFECYCLE_RESUME_ID, LIFECYCLE_PREV_EXIT) to pass context across restarts.
    • Container Abstraction: Generic interface to manage containerized workloads without direct SDK dependencies.
  • DX Helpers (v1.4):
    • Run: One-line main entry point (Context + Signal Handling + Cleanup).
    • Sleep: Context-aware sleep (returns immediately on cancel).
    • OnShutdown: Type-safe hook registration without casting.

Usage

Signal Context
package main

import (
    "context"
    "fmt"
    "time"
    "github.com/aretw0/lifecycle"
)

func main() {
    // lifecycle.Run handles context creation, signal listening, and cleanup.
    // It automatically waits for hooks if a signal is received.
    lifecycle.Run(runApp)
}

func runApp(ctx context.Context) error {
    // 1. Frictionless Hook Registration
    lifecycle.OnShutdown(ctx, func() {
        fmt.Println("Cleanup: Database closed")
    })

    // 2. Safe Sleep (Regret Window)
    // Returns immediately if Ctrl+C is pressed.
    if err := lifecycle.Sleep(ctx, 10*time.Second); err != nil {
        return err
    }
    
    return nil
}
Interruptible I/O
package main

import (
    "context"
    "fmt"
    "github.com/aretw0/lifecycle"
)

func main() {
    ctx := context.Background() // or SignalContext
    
    // Smart Open (handles Windows CONIN$)
    reader, _ := lifecycle.OpenTerminal()
    
    // Wrap to respect context cancellation
    r := lifecycle.NewInterruptibleReader(reader, ctx.Done())

    buf := make([]byte, 1024)
    n, err := r.Read(buf)
    if lifecycle.IsInterrupted(err) {
        fmt.Println("Read cancelled!")
        return
    }
    fmt.Printf("Read: %s\n", buf[:n])
}
Worker Protocol (v1.3)

Manage long-running processes, containers, or goroutines with a uniform interface, hygiene, and handover support.

package main

import (
    "context"
    "fmt"
    "github.com/aretw0/lifecycle"
)

func main() {
    ctx := lifecycle.NewSignalContext(context.Background())
    defer ctx.Stop()

    // 1. Process Worker (Fail-Closed hygiene automatically applied)
    worker := lifecycle.NewProcessWorker("pinger", "ping", "127.0.0.1")

    // 2. Handover Protocol (Access resume info in child process via env)
    // resumeID := os.Getenv(lifecycle.EnvResumeID)

    // Async Start
    worker.Start(ctx)

    // Wait for shutdown or worker exit
    select {
    case <-ctx.Done():
        worker.Stop(context.Background()) // Graceful stop
    case <-worker.Wait():
        fmt.Println("Worker finished!")
    }
}
System Introspection (v1.3)

Generate live architecture diagrams of your running application.

// Get current snapshots
sigState := ctx.State()
workState := supervisor.State()

// Generate Mermaid "Unified Dashboard"
diagram := lifecycle.SystemDiagram(sigState, workState)
fmt.Println(diagram)

[!NOTE] We use state diagrams (stateDiagram-v2) for behavior/FSM and flowcharts (graph TD) for topology/trees.

Metrics Palette

The library uses a consistent color palette for all generated diagrams:

  • 🟡 Pending: Defined but not yet active.
  • 🔵 Running: Active and healthy.
  • 🟢 Stopped: Successfully terminated.
  • 🔴 Failed: Crashed or terminated with error.

I/O Safety

The library implements Context-Aware I/O to balance data preservation and responsiveness:

  • Read() (Pipeline Safe): Uses a Shielded Return strategy. If data arrives simultaneously with a cancellation signal, it returns the data (nil error). This guarantees no data loss in pipelines or logs.
  • ReadInteractive() (Interactive Safe): Uses a Strict Discard strategy. If the user hits Ctrl+C while typing, any partial input is discarded to prevent accidental execution of commands.

Documentation

Documentation

Overview

Package lifecycle provides a centralized library for managing application lifecycles and interactive I/O.

Dual Signal Context

Standard Go `signal.NotifyContext` cancels on the first signal. `lifecycle` distinguishes between:

  • SIGINT (Ctrl+C): "Soft" interrupt. Captures the signal but keeps the Context active. Allows the application to decide whether to pause, confirm exit, or ignore.
  • SIGTERM: "Hard" stop. Cancels the Context immediately, triggering graceful shutdown.

Interruptible I/O

On many systems (especially Windows), reading from `os.Stdin` blocks the goroutine indefinitely, preventing clean cancellation. Furthermore, on Windows, receiving a signal can close the standard input handle, causing an unexpected EOF. `lifecycle` provides `OpenTerminal` (using `CONIN$`) and `NewInterruptibleReader` to ensure I/O operations respect `context.Context` cancellation and signals are handled gracefully without premature termination.

Shutdown Timeouts

Graceful shutdown often involves waiting for background goroutines to finish (e.g., closing database connections, flushing logs). To prevent the application from hanging indefinitely if a cleanup operation stalls, `lifecycle` provides `BlockWithTimeout`. This ensures the process exits deterministically even if some components are stuck.

Reliability Primitives (v1.4)

For "Durable Execution" patterns, `lifecycle` provides `Do(ctx, fn)`. This creates a "Critical Section" where value-critical operations (like state commits) are shielded from cancellation. If a user hits Ctrl+C during a critical section, the signal is captured but deferred until the section completes.

Worker Protocol & Supervision (v1.3)

For complex applications, `lifecycle` provides a `Worker` interface and a `Supervisor`. This allows managing hierarchies of processes, goroutines, and containers with automatic restarts, session persistence (Handover Protocol), and unified introspection.

DX Helpers & Boilerplate (v1.4)

To reduce friction, `lifecycle` provides helpers that standardizes common patterns:

  • Run: Standardizes the `main` function (Context creation -> Run -> Stop -> Wait).
  • Sleep: Replaces `select { case <-time.After... }` with a single context-aware call.
  • OnShutdown: Registers hooks without manual type assertions.

Usage

func main() {
	err := lifecycle.Run(runApp)
	if err != nil {
		// handle err
	}
}

func runApp(ctx context.Context) error {
	// Register cleanup
	lifecycle.OnShutdown(ctx, func() {
		fmt.Println("Closing DB...")
	})

	// Safe Sleep (Regret Window)
	if err := lifecycle.Sleep(ctx, 5*time.Second); err != nil {
		return err
	}
	return nil
}

See examples/hooks for a full application structure.

Index

Examples

Constants

View Source
const (
	WorkerStatusPending = worker.StatusPending
	WorkerStatusRunning = worker.StatusRunning
	WorkerStatusStopped = worker.StatusStopped
	WorkerStatusFailed  = worker.StatusFailed
)
View Source
const (
	// EnvResumeID is the unique session identifier for a worker.
	EnvResumeID = worker.EnvResumeID
	// EnvPrevExit is the exit code of the previous execution of this worker.
	EnvPrevExit = worker.EnvPrevExit
)

Handover Constants

View Source
const (
	// StrategyOneForOne: If a child process terminates, only that process is restarted.
	StrategyOneForOne = supervisor.StrategyOneForOne
	// StrategyOneForAll: If a child process terminates, all other child processes are terminated.
	StrategyOneForAll = supervisor.StrategyOneForAll
)

Variables

View Source
var Version string

Functions

func BlockWithTimeout added in v1.1.0

func BlockWithTimeout(done <-chan struct{}, timeout time.Duration) error

BlockWithTimeout blocks until the done channel is closed or the timeout expires. Alias for pkg/runtime.BlockWithTimeout.

Example

ExampleBlockWithTimeout demonstrates how to enforce a deadline on shutdown cleanup.

package main

import (
	"fmt"
	"time"

	"github.com/aretw0/lifecycle"
)

func main() {
	done := make(chan struct{})

	// Simulate a cleanup task
	go func() {
		defer close(done)
		// Simulate fast cleanup
		time.Sleep(10 * time.Millisecond)
	}()

	// Wait for cleanup, but give up after 1 second
	err := lifecycle.BlockWithTimeout(done, 1*time.Second)
	if err != nil {
		fmt.Println("Cleanup timed out!")
	} else {
		fmt.Println("Cleanup finished successfully")
	}

}
Output:

Cleanup finished successfully

func Do added in v1.4.0

func Do(parent context.Context, fn func(ctx context.Context)) error

Do executes a function in a "Critical Section" that delays context cancellation. It wraps the provided function in a shielded context that ignores the parent's cancellation. Alias for internal/reliability.Do.

func IsInterrupted

func IsInterrupted(err error) bool

IsInterrupted checks if an error indicates an interruption (Context Canceled, EOF, etc.). Alias for pkg/termio.IsInterrupted.

func NewInterruptibleReader

func NewInterruptibleReader(base io.Reader, cancel <-chan struct{}) *termio.InterruptibleReader

NewInterruptibleReader returns a reader that checks the cancel channel before/after blocking reads. Alias for pkg/termio.NewInterruptibleReader.

func NewLogMetricsProvider added in v1.1.0

func NewLogMetricsProvider() metrics.Provider

NewLogMetricsProvider returns a metrics provider that logs to the current logger. Useful for development and local verification. Alias for pkg/metrics.LogProvider.

func NewSignalContext

func NewSignalContext(parent context.Context, opts ...signal.Option) *signal.Context

NewSignalContext creates a context that cancels on SIGTERM/SIGINT. On the first signal, context is cancelled. On the second, it force exits. Behavior can be customized via functional options. Alias for pkg/signal.NewContext.

Example

ExampleNewSignalContext demonstrates how to use the Dual Signal context. Note: This example is illustrative; in a real run, it waits for SIGINT/SIGTERM.

package main

import (
	"context"
	"fmt"
	"time"

	"github.com/aretw0/lifecycle"
)

func main() {
	// Create a context that listens for signals.
	ctx := lifecycle.NewSignalContext(context.Background())

	// For checking output deterministically in this example, we cancel manually
	// after a short delay, allowing "work" to happen first.
	go func() {
		time.Sleep(50 * time.Millisecond)
		ctx.Cancel()
	}()

	// Simulate work
	select {
	case <-ctx.Done():
		fmt.Println("Context cancelled too early")
	case <-time.After(10 * time.Millisecond):
		fmt.Println("Doing work...")
	}

}
Output:

Doing work...

func OnShutdown added in v1.4.0

func OnShutdown(ctx context.Context, fn func())

OnShutdown safely registers a shutdown hook on the context if it supports it. It abstracts the type assertion for *signal.Context.

func OpenTerminal

func OpenTerminal() (io.ReadCloser, error)

OpenTerminal checks for text input capability and returns a Reader. On Windows, it tries to open CONIN$. Alias for pkg/termio.Open.

Example

ExampleOpenTerminal demonstrates how to open the terminal safely.

package main

import (
	"fmt"

	"github.com/aretw0/lifecycle"
)

func main() {
	// OpenTerminal handles OS-specific logic (like CONIN$ on Windows)
	reader, err := lifecycle.OpenTerminal()
	if err != nil {
		fmt.Printf("Error opening terminal: %v\n", err)
		return
	}
	defer reader.Close()

	fmt.Println("Terminal opened successfully")

	// Wrap with InterruptibleReader to respect context cancellation
	// r := lifecycle.NewInterruptibleReader(reader, ctx.Done())

}
Output:

Terminal opened successfully

func Run added in v1.4.0

func Run(fn func(context.Context) error, opts ...Option) error

Run executes the application logic with a managed SignalContext. Alias for pkg/runtime.Run.

func SetLogger added in v1.1.0

func SetLogger(l *slog.Logger)

SetLogger overrides the global logger used by the library. Alias for pkg/log.SetLogger.

func SetMetricsProvider added in v1.1.0

func SetMetricsProvider(p metrics.Provider)

SetMetricsProvider overrides the global metrics provider. This allowing bridging library metrics to Prometheus, OTEL, etc. Alias for pkg/metrics.SetProvider.

func SetStrictMode added in v1.1.0

func SetStrictMode(strict bool)

SetStrictMode sets whether to block on unsupported platforms for process hygiene. Alias for pkg/proc.StrictMode.

func SignalStateDiagram added in v1.3.0

func SignalStateDiagram(s SignalState) string

SignalStateDiagram returns a Mermaid state diagram string representing the signal context configuration. Alias for pkg/signal.MermaidState.

func Sleep added in v1.4.0

func Sleep(ctx context.Context, d time.Duration) error

Sleep pauses the current goroutine for at least the duration d. Alias for pkg/runtime.Sleep.

func StartProcess added in v1.1.0

func StartProcess(cmd *exec.Cmd) error

StartProcess starts the specified command with process hygiene (auto-kill on parent exit). Alias for pkg/proc.Start.

func SystemDiagram added in v1.3.0

func SystemDiagram(sig SignalState, work WorkerState) string

SystemDiagram returns a unified Mermaid diagram representing the entire application lifecycle. It combines the SignalContext (Control Plane) and the Worker hierarchy (Data Plane).

func UpgradeTerminal added in v0.1.1

func UpgradeTerminal(r io.Reader) (io.Reader, error)

UpgradeTerminal checks if the provided reader is a terminal and returns a safe reader (e.g. CONIN$ on Windows). If not a terminal, returns the original reader.

func WithForceExit added in v1.1.0

func WithForceExit(threshold int) signal.Option

WithForceExit configures the threshold of signals required to trigger an immediate os.Exit(1). Set to 0 to disable forced exit. Alias for pkg/signal.WithForceExit.

func WithHookTimeout added in v1.2.0

func WithHookTimeout(d time.Duration) signal.Option

WithHookTimeout configures the duration after which a running hook produces a warning log. Alias for pkg/signal.WithHookTimeout.

func WithInterrupt added in v1.1.0

func WithInterrupt(cancel bool) signal.Option

WithInterrupt configures whether SIGINT (Ctrl+C) should cancel the context. Alias for pkg/signal.WithInterrupt.

func WorkerStateDiagram added in v1.3.0

func WorkerStateDiagram(s WorkerState) string

WorkerStateDiagram returns a Mermaid state diagram string representing the worker state transitions. Alias for pkg/worker.MermaidState.

func WorkerTreeDiagram added in v1.3.0

func WorkerTreeDiagram(s WorkerState) string

WorkerTreeDiagram returns a Mermaid diagram string representing the worker structure (Tree). Alias for pkg/worker.MermaidTree.

Types

type Container added in v1.3.0

type Container = container.Container

Container represents a generic container interface. Alias for container.Container.

func NewMockContainer added in v1.3.0

func NewMockContainer(id string) Container

NewMockContainer creates a new MockContainer for testing. Alias for pkg/container.NewMockContainer.

type ContainerStatus added in v1.3.0

type ContainerStatus = container.Status

ContainerStatus represents the lifecycle state of a container. Alias for container.Status.

type Context added in v1.3.0

type Context = signal.Context

Context represents the signal context.

type Option added in v1.3.0

type Option = signal.Option

Option is a functional option for signal configuration.

type SignalState added in v1.3.0

type SignalState = signal.State

SignalState represents the configuration state of the SignalContext.

type State added in v1.2.0

type State = SignalState

State is an alias for SignalState (backward compatibility).

type Supervisor added in v1.3.0

type Supervisor = supervisor.Supervisor

Supervisor defines the interface for a supervisor. Alias for pkg/supervisor.Supervisor.

func NewSupervisor added in v1.3.0

func NewSupervisor(name string, strategy SupervisorStrategy, specs ...SupervisorSpec) Supervisor

NewSupervisor creates a new Supervisor for the given workers. Alias for pkg/supervisor.New.

type SupervisorBackoff added in v1.3.1

type SupervisorBackoff = supervisor.Backoff

SupervisorBackoff defines the retry policy for failed children. Alias for pkg/supervisor.Backoff.

type SupervisorFactory added in v1.3.0

type SupervisorFactory = supervisor.Factory

SupervisorFactory is a function that creates a new worker instance. Alias for pkg/supervisor.Factory.

type SupervisorSpec added in v1.3.0

type SupervisorSpec = supervisor.Spec

SupervisorSpec defines the configuration for a supervised child worker. Alias for pkg/supervisor.Spec.

type SupervisorStrategy added in v1.3.0

type SupervisorStrategy = supervisor.Strategy

SupervisorStrategy defines how the supervisor handles child failures. Alias for pkg/supervisor.Strategy.

type Worker added in v1.3.0

type Worker = worker.Worker

Worker defines the interface for a managed unit of work. Alias for pkg/worker.Worker.

func NewContainerWorker added in v1.3.0

func NewContainerWorker(name string, c Container) Worker

NewContainerWorker creates a new Worker from a Container interface. Alias for pkg/worker.NewContainerWorker.

func NewProcessWorker added in v1.3.0

func NewProcessWorker(name string, nameCmd string, args ...string) Worker

NewProcessWorker creates a new Process worker for the given command. Alias for pkg/worker.NewProcessWorker.

func NewWorkerFromFunc added in v1.3.1

func NewWorkerFromFunc(name string, fn func(context.Context) error) Worker

NewWorkerFromFunc creates a Worker from a function. Alias for pkg/worker.FromFunc.

type WorkerState added in v1.3.0

type WorkerState = worker.State

WorkerState represents the snapshot of a worker's state.

type WorkerStatus added in v1.3.0

type WorkerStatus = worker.Status

WorkerStatus represents the lifecycle state of a worker.

Directories

Path Synopsis
examples
container command
demo command
hooks command
interactive_dx command
introspection command
observability command
reliability command
supervisor command
worker command
zombie command
internal
pkg
container
Package container defines a generic interface for managing containerized workloads.
Package container defines a generic interface for managing containerized workloads.
log
Package log provides a lightweight, structured logging interface for the lifecycle library.
Package log provides a lightweight, structured logging interface for the lifecycle library.
metrics
Package metrics provides a decoupled interface for collecting library metrics.
Package metrics provides a decoupled interface for collecting library metrics.
proc
Package proc provides primitives for managing process lifecycle and hygiene.
Package proc provides primitives for managing process lifecycle and hygiene.
runtime
Package runtime provides utilities for deterministic process management and boilerplate reduction.
Package runtime provides utilities for deterministic process management and boilerplate reduction.
signal
Package signal provides a stateful signal context with introspection and LIFO hooks.
Package signal provides a stateful signal context with introspection and LIFO hooks.
supervisor
Package supervisor implements the Supervisor Pattern for managing process and worker lifecycles.
Package supervisor implements the Supervisor Pattern for managing process and worker lifecycles.
termio
Package termio provides interruptible I/O primitives and terminal handling.
Package termio provides interruptible I/O primitives and terminal handling.
worker
Package worker defines interfaces and implementations for managed units of work.
Package worker defines interfaces and implementations for managed units of work.

Jump to

Keyboard shortcuts

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