graceful

package
v0.6.0 Latest Latest
Warning

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

Go to latest
Published: Feb 18, 2026 License: MIT Imports: 11 Imported by: 0

README

graceful

graceful provides a small helper for running a function with reliable shutdown behavior triggered by OS signals. It removes the boilerplate required to coordinate context cancellation, timeouts, and exit codes.

At a high level:

  • You supply a function that accepts a context.Context
  • On the first SIGINT/SIGTERM, the context is canceled so your function can shut down cleanly
  • On a second signal, the process exits immediately (code 130)
  • Optional timeouts bound both the run duration and the shutdown period
  • Optionally, use WithImmediateTermination() to exit immediately on the first signal

This pattern is useful for HTTP servers, workers, CLIs, and batch jobs.

Installation

go get github.com/pressly/cli@latest

And import:

import "github.com/pressly/cli/graceful"

Usage

Basic example
graceful.Run(func(ctx context.Context) error {
    <-ctx.Done() // wait for shutdown signal
    return nil
})
HTTP server
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintln(w, "hello")
})

server := &http.Server{
    Addr:    ":8080",
    Handler: mux,
}

graceful.Run(
    graceful.ListenAndServe(server, 15*time.Second), // HTTP draining period
    graceful.WithTerminationTimeout(30*time.Second), // total shutdown limit
)
Batch job with a deadline
graceful.Run(func(ctx context.Context) error {
    return processBatch(ctx)
}, graceful.WithRunTimeout(1*time.Hour)) // max 1 hour run time

Options

WithRunTimeout(time.Duration)

Maximum time the run function may execute. Useful for batch jobs or preventing runaway processes.

WithTerminationTimeout(time.Duration)

Maximum time allowed for the process to shut down after the first signal. If exceeded, graceful exits with code 124.

WithImmediateTermination()

Disables the graceful shutdown phase. The first signal (SIGINT/SIGTERM) causes immediate termination with exit code 130, without waiting for a second signal. Use this when you need immediate process termination instead of the default two-signal behavior.

graceful.Run(func(ctx context.Context) error {
    return runTask(ctx)
}, graceful.WithImmediateTermination())
WithLogger(*slog.Logger)

Uses the provided structured logger for all messages. To disable all logging output, pass a logger with a discard handler:

graceful.Run(fn, graceful.WithLogger(slog.New(slog.DiscardHandler)))
WithStderr(io.Writer)

Redirects stderr output when no logger is used.

Exit Codes

  • 0 — success
  • 1 — run function returned an error
  • 124 — shutdown timeout exceeded
  • 130 — forced shutdown (second signal or immediate termination)

Signals

  • Unix: SIGINT, SIGTERM
  • Windows: os.Interrupt

The first signal triggers context cancellation; the second forces termination.

Gotchas

Kubernetes termination timing

Kubernetes defaults to a terminationGracePeriodSeconds of 30 seconds. If you rely on graceful draining (HTTP servers, workers), leave headroom:

  • Use a preStop hook (5-10 seconds) so load balancers stop routing
  • Set WithTerminationTimeout(20 * time.Second) to stay within the window

Tweak these values based on your environment and shutdown needs!

Propagating shutdown into handlers

http.Server.Shutdown does not cancel handler contexts immediately. This is the correct and expected behavior for normal HTTP serving.

If you need handlers to observe process shutdown (rare, usually for long-running streaming endpoints), set:

graceful.Run(func(ctx context.Context) error {
    mux := http.NewServeMux()

    mux.HandleFunc("/stream", func(w http.ResponseWriter, r *http.Request) {
        select {
        case <-time.After(30 * time.Second):
            fmt.Fprintln(w, "done")
        case <-r.Context().Done(): // will fire on shutdown
            http.Error(w, "shutting down", http.StatusServiceUnavailable)
        }
    })

    server := &http.Server{
        Addr: ":8080",
        Handler: mux,
        BaseContext: func(_ net.Listener) context.Context {
            return ctx // propagate shutdown into handlers
        },
    }

    return graceful.ListenAndServe(server, 10*time.Second)(ctx)
})

Handlers will then receive r.Context().Done() when shutdown begins.

Documentation

Overview

Package graceful provides utilities for running long-lived processes with predictable, well-behaved shutdown semantics. It wraps a user-provided function with signal handling, context cancellation, timeouts, and standardized exit codes.

On the first SIGINT/SIGTERM, the context passed to the run function is canceled, giving the process an opportunity to shut down cleanly. A second signal forces an immediate exit. Optional timeouts bound both the maximum run duration (WithRunTimeout) and the total shutdown period (WithTerminationTimeout). For scenarios requiring immediate termination on the first signal, use WithImmediateTermination to bypass the graceful shutdown phase.

Exit codes:

  • 0: successful completion
  • 1: run function returned an error
  • 124: shutdown timeout exceeded
  • 130: forced shutdown (second signal or immediate termination)

Example: HTTP server

server := &http.Server{
    Addr: ":8080",
    Handler: mux,
}

graceful.Run(
    graceful.ListenAndServe(server, 15*time.Second),       // HTTP draining period
    graceful.WithTerminationTimeout(30*time.Second),  // overall shutdown limit
)

Example: batch job with a hard deadline

graceful.Run(func(ctx context.Context) error {
    return processBatch(ctx)
}, graceful.WithRunTimeout(1*time.Hour))

Example: worker with both limits

graceful.Run(func(ctx context.Context) error {
    return runWorker(ctx)
},
    graceful.WithRunTimeout(24*time.Hour),
    graceful.WithTerminationTimeout(30*time.Second),
)

Example: immediate termination on first signal

graceful.Run(func(ctx context.Context) error {
    return runTask(ctx)
}, graceful.WithImmediateTermination())

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func ListenAndServe

func ListenAndServe(srv *http.Server, shutdownGrace time.Duration) func(context.Context) error

ListenAndServe runs an *http.Server under the lifecycle managed by graceful.Run. It starts the server, waits for ctx cancellation (SIGINT/SIGTERM), and then performs a graceful shutdown using http.Server.Shutdown.

Shutdown behavior follows standard net/http semantics:

  • new connections are refused once shutdown begins
  • in-flight requests are allowed to finish normally
  • shutdownGrace bounds how long the server waits for draining

ListenAndServe does not propagate the initial shutdown signal into handler contexts. Requests are only cancelled if the client disconnects or if shutdownGrace expires. This matches typical production environments and avoids mid-request interruptions.

Two timeouts are involved:

  • shutdownGrace: how long the HTTP server may drain connections
  • graceful.WithTerminationTimeout: the total process shutdown budget

Example:

server := &http.Server{
    Addr:    ":8080",
    Handler: mux,
}

graceful.Run(
    graceful.ListenAndServe(server, 15*time.Second), // server draining period
    graceful.WithTerminationTimeout(25*time.Second), // total shutdown limit
)

func Run

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

Run the provided function with signal handling and optional timeouts. See package documentation for details on signal handling, timeouts, and exit codes.

Types

type Option

type Option func(*config)

Option configures the Handle function.

func WithImmediateTermination

func WithImmediateTermination() Option

WithImmediateTermination configures the process to exit immediately on the first interrupt signal, without waiting for a second signal. By default, graceful shutdown allows a second Ctrl+C to force immediate termination. This option disables that behavior.

When enabled, the first SIGINT/SIGTERM will cause the process to exit with code 130 immediately, without waiting for the run function to complete gracefully.

Example:

graceful.Run(fn, graceful.WithImmediateTermination())

func WithLogger

func WithLogger(logger *slog.Logger) Option

WithLogger sets an optional slog.Logger for structured logging. When provided, the logger is used instead of fmt.Fprintln to stderr for all messages (shutdown notifications, errors, etc.).

To disable all logging output, pass a logger with a discard handler:

graceful.Run(fn, graceful.WithLogger(slog.New(slog.DiscardHandler)))

func WithRunTimeout

func WithRunTimeout(d time.Duration) Option

WithRunTimeout sets the maximum time the run function may execute. When the timeout expires, the context passed to the run function is canceled. If the function does not exit on cancellation, it will eventually be stopped by the termination timeout or a second interrupt signal.

A zero or negative duration means no limit.

Example:

graceful.Run(processBatch, graceful.WithRunTimeout(1*time.Hour))

func WithStderr

func WithStderr(w io.Writer) Option

WithStderr sets the writer for error output. Defaults to os.Stderr if not specified. If a logger is configured via WithLogger, the logger takes precedence over stderr for messages.

func WithTerminationTimeout

func WithTerminationTimeout(d time.Duration) Option

WithTerminationTimeout sets the maximum time the process may spend shutting down after the first interrupt signal. If this timeout expires, the process exits with code 124.

This bounds the total shutdown phase (server draining, cleanup, background work). A zero or negative duration means no limit.

Example:

graceful.Run(fn, graceful.WithTerminationTimeout(30*time.Second))

Jump to

Keyboard shortcuts

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