lane

package module
v1.0.0 Latest Latest
Warning

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

Go to latest
Published: Jun 9, 2026 License: MIT Imports: 12 Imported by: 0

README

Lane

CI Go Reference Go Report Card GitHub release codecov License 1 dependency

🤔 What is lane?

lane manages the startup, health, and graceful shutdown of one or more service components — HTTP servers, background workers, schedulers — under a unified context. Register your runners, call Run, and lane handles the rest.

🎯 Why use it?

Wiring SIGINT/SIGTERM handling, concurrent startup, health probes, and ordered shutdown correctly is tedious boilerplate. lane does it once, predictably, so services stay focused on their own logic.

🚫 What it does NOT do

  • It is not a dependency injection framework
  • It does not wire, store, or own your service dependencies
  • It does not prescribe configuration, logging format, or database access patterns
  • It does not define application structure beyond the Runner interface

Dependency wiring is the caller's responsibility.

⚡ Quick Example

package main

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

    "github.com/rluders/lane"
    "github.com/rluders/lane/runners"
)

func main() {
    lane.RunHealthCheck(":8080")

    log := slog.New(slog.NewTextHandler(os.Stderr, nil))

    mux := http.NewServeMux()

    l := lane.New(log, lane.WithShutdownTimeout(10*time.Second))

    mux.Handle("GET /ready", lane.ReadinessHandler(l.Health()))
    mux.Handle("GET /live", lane.LivenessHandler())

    server := &http.Server{Addr: ":8080", Handler: mux}
    l.AddRunner(runners.NewHTTPRunner("api", server, log))

    if err := l.Run(context.Background()); err != nil {
        log.Error("lane error", "error", err)
        os.Exit(1)
    }
}

🔄 Lifecycle

flowchart TD
    A[lane.New] --> B[AddRunner x N]
    B --> C[l.Run]
    C --> D[Start all runners concurrently]
    D --> E[health.SetReady true]
    E --> F{waiting}
    F -- SIGINT/SIGTERM --> G[Signal received]
    F -- runner error --> H[Context cancelled]
    G --> I[health.SetReady false]
    H --> I
    I --> J[Stop runners in LIFO order]
    J --> K[Shutdown timeout context]
    K --> L[Return first error or nil]

Runners are started concurrently. Shutdown is sequential in reverse registration order (LIFO), so dependents stop before the services they depend on.

🧩 Runner Interface

Any component that can start and stop is a Runner:

type Runner interface {
    Name() string
    Start(ctx context.Context) error
    Stop(ctx context.Context) error
}

Contract:

  • Start must block until the runner stops or fails
  • Stop initiates graceful shutdown and must respect the context deadline
  • If Start returns a non-nil error, lane cancels all other runners

📦 Built-in Runners

All implementations live in the runners/ sub-package.

Runner Constructor Description
HTTP runners.NewHTTPRunner(name, server, log) Wraps *http.Server. Calls Shutdown on stop.
HTTPS runners.NewHTTPSRunner(name, server, certFile, keyFile, log) Same as HTTPRunner with TLS.
Worker runners.NewWorkerRunner(name, fn, log) Runs a WorkFn in a loop until ctx is cancelled.
Scheduler runners.NewSchedulerRunner(name, interval, fn, log) Runs a JobFn on a fixed interval. Missed ticks are dropped.

Worker example

worker := runners.NewWorkerRunner("processor", func(ctx context.Context) error {
    return processNextMessage(ctx)
}, log)
l.AddRunner(worker)

Scheduler example

sched := runners.NewSchedulerRunner("cleanup", 5*time.Minute, func(ctx context.Context) error {
    return purgeExpiredSessions(ctx)
}, log)
l.AddRunner(sched)

🏥 Health Checks

HealthState tracks readiness. Lane sets it to ready after all runners start, and back to not-ready at the beginning of shutdown.

mux.Handle("GET /ready", lane.ReadinessHandler(l.Health(), db.PingContext))
mux.Handle("GET /live", lane.LivenessHandler())

ReadinessHandler accepts zero or more probe functions of type func(ctx context.Context) error. All probes must pass for the endpoint to return 200. If any probe fails, the endpoint returns 503.

LivenessHandler always returns 200 — a running process is a live process.

Container health probe

Call RunHealthCheck at the very top of main() to support Docker HEALTHCHECK CMD-based probes:

func main() {
    lane.RunHealthCheck(":8080")
    // ... rest of main
}

When invoked as myservice healthcheck, the process exits 0 (healthy) or 1 (unhealthy) immediately without starting the service.

🛡️ Recovery

Goroutine recovery

lane.Go runs a goroutine with structured panic recovery. On panic it logs the stack trace and calls the provided cancel function to propagate the failure up to lane.

lane.Go(ctx, log, "worker-name", cancel, func(ctx context.Context) {
    // critical goroutine — a panic here cancels the application context
})

HTTP handler recovery

RecoverMiddleware wraps HTTP handlers with panic recovery. On panic it logs structured context (method, path, stack trace) and returns HTTP 500. The service continues — a single handler panic is recoverable.

handler = lane.RecoverMiddleware(log)(handler)

⏱️ Shutdown Timeout

Default shutdown timeout is 30 seconds. Override with WithShutdownTimeout:

l := lane.New(log, lane.WithShutdownTimeout(10*time.Second))

Each runner's Stop is called with a context that respects this deadline. If a runner does not stop within the timeout, shutdown proceeds without it.

🗓️ When to use lane

Use it when you want:

  • ✅ Go services with multiple concurrent components (API server + worker + scheduler)
  • ✅ Predictable SIGTERM handling and readiness probes in containers
  • ✅ Correct lifecycle management without pulling in a full service framework

Avoid it if you need:

  • ❌ A dependency injection container
  • ❌ A full application framework with routing conventions
  • ❌ Managed configuration, observability, or deployment tooling

📄 License

MIT — see LICENSE.

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Go

func Go(ctx context.Context, log *slog.Logger, name string, cancel context.CancelFunc, fn func(ctx context.Context))

Go runs fn in a goroutine with structured panic recovery. On panic: logs stack trace and calls cancel to propagate failure up. Use this for goroutines that are critical to service operation.

func LivenessHandler

func LivenessHandler() http.HandlerFunc

LivenessHandler always returns 200. A running process is a live process.

func ReadinessHandler

func ReadinessHandler(state *HealthState, checks ...func(ctx context.Context) error) http.HandlerFunc

ReadinessHandler returns 200 when the app is ready and all checks pass, 503 otherwise. Pass check functions for external dependencies (e.g., db.PingContext).

func RecoverMiddleware

func RecoverMiddleware(log *slog.Logger) func(http.Handler) http.Handler

RecoverMiddleware wraps HTTP handlers with panic recovery. On panic: logs structured context and returns 500. Does NOT cancel app context — a single handler panic is recoverable; the service continues.

func RunHealthCheck

func RunHealthCheck(addr string)

RunHealthCheck exits the process with 0 (healthy) or 1 (unhealthy) when "healthcheck" is passed as the first argument. Call at the top of main() before any initialization to support CMD-based container health probes.

Types

type HealthState

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

HealthState tracks the application's readiness. Liveness is always true — if the process is running, it's alive.

func (*HealthState) IsReady

func (h *HealthState) IsReady() bool

func (*HealthState) SetReady

func (h *HealthState) SetReady(v bool)

type Lane

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

Lane coordinates the lifecycle of one or more Runners. It handles signal interception, concurrent startup, health state, and ordered graceful shutdown. Dependency wiring is the caller's responsibility.

func New

func New(log *slog.Logger, opts ...Option) *Lane

New constructs a Lane. log must not be nil.

func (*Lane) AddRunner

func (l *Lane) AddRunner(runners ...Runner)

AddRunner appends runners. Registration order determines startup order and reverse-order (LIFO) shutdown order.

func (*Lane) Health

func (l *Lane) Health() *HealthState

Health returns the HealthState so callers can pass it to ReadinessHandler.

func (*Lane) Run

func (l *Lane) Run(ctx context.Context) error

Run starts all runners concurrently, blocks until a signal or runner failure, then shuts down runners in reverse registration order. Returns the first runner error, or nil on clean shutdown.

type Option

type Option func(*Lane)

Option configures a Lane.

func WithShutdownTimeout

func WithShutdownTimeout(d time.Duration) Option

WithShutdownTimeout overrides the default 30s shutdown timeout.

type Runner

type Runner interface {
	Name() string
	Start(ctx context.Context) error
	Stop(ctx context.Context) error
}

Runner is the core lifecycle abstraction. Start MUST block until the runner stops or fails. Stop initiates a graceful shutdown; it MUST respect ctx deadline.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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