graceful

package module
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Jul 7, 2026 License: MIT Imports: 12 Imported by: 0

README

graceful

Doc Go Release Test Report Card Stars License

Lifecycle orchestration for long-running Go components: start everything together, wait for a shutdown signal or the first failure, drain everything in reverse order — with optional zero-downtime binary upgrades on SIGHUP via tableflip.

Features

  • Any component, not just HTTP. Add(name, start, stop) takes a pair of functions: start may block for the component's whole life (an accept loop) or return immediately after spawning its own work (a scheduler). A non-nil error from any start shuts the whole group down.
  • Listeners built for upgrades. Listen(name, addr, srv) creates the listener inside Run — through tableflip when upgrades are enabled — so an upgraded process inherits the socket without dropping connections. *http.Server satisfies the Server interface directly.
  • One shutdown story. SIGINT/SIGTERM, a component failure, or an upgrade handoff all funnel into the same drain: every started component is stopped in reverse registration order, under one shared deadline.
  • Zero-downtime upgrades. WithUpgrade() makes SIGHUP re-exec the binary and hand off listeners; it silently falls back to plain listeners on Windows.
  • Structured logging of every lifecycle event through log/slog.

Install

go get github.com/libtnb/graceful

Requires Go 1.25+.

Quick start

package main

import (
	"net/http"
	"time"

	"github.com/libtnb/graceful"
)

func main() {
	srv := &http.Server{Handler: mux}

	g := graceful.New(
		graceful.WithUpgrade(),                      // SIGHUP = hot upgrade
		graceful.WithShutdownTimeout(30*time.Second),
	)
	g.Add("cron", cron.Start, cron.Stop)             // any start/stop pair
	g.Listen("http", ":8080", srv)                   // upgrade-aware listener

	if err := g.Run(); err != nil {                  // blocks until shutdown
		log.Fatal(err)
	}
}

Run starts entries in registration order and drains them in reverse: the HTTP listener above stops accepting before the scheduler is asked to finish, so in-flight requests can still schedule work.

Trigger Behavior
SIGINT / SIGTERM stop accepting, drain every component, return nil
a start returns non-nil drain every component, return name: err
SIGHUP (with WithUpgrade) re-exec the binary, hand listeners to the child, drain, return nil

Design notes

  • start errors are fatal, stop errors are logged. A component that cannot run means the process is broken — everything comes down. A component that cannot stop cleanly must not block the rest from draining.
  • A scheduler-style start that returns nil is a successful launch, not a failure — only non-nil errors trigger shutdown. This makes Add fit both blocking accept loops and fire-and-forget starters without adapters.
  • Registration order is the dependency order. Register infrastructure first, entry points last; reverse-order draining then closes the front door before the back office.
  • The group owns signals, not resources. Database pools, log writers and the like belong to whatever built them (a DI container's cleanup); the group only coordinates starting and stopping.

License

MIT

Documentation

Overview

Package graceful orchestrates the lifecycle of long-running components. A Group starts every registered component together, waits for a shutdown signal (SIGINT/SIGTERM) or the first failure, then drains everything in reverse registration order within a bounded timeout. With WithUpgrade a SIGHUP performs a zero-downtime binary upgrade via tableflip: listeners created through Listen are inherited by the new process.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Group

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

Group runs components together. Register with Add and Listen, then call Run once; Group is not safe for concurrent registration.

func New

func New(opts ...Option) *Group

func (*Group) Add

func (g *Group) Add(name string, start func() error, stop func(ctx context.Context) error)

Add registers a component. start runs in a goroutine and may block for the component's whole life (an accept loop) or return nil immediately after spawning its own work (a scheduler): returning a non-nil error shuts the whole group down. stop is called during shutdown, bounded by the group's shutdown timeout, in reverse registration order.

func (*Group) Listen

func (g *Group) Listen(name, addr string, srv Server)

Listen registers srv to serve on a TCP listener bound to addr. The listener is created by Run — through tableflip under WithUpgrade, so upgraded processes inherit it. http.ErrServerClosed is treated as a clean exit.

func (*Group) Run

func (g *Group) Run() error

Run starts every entry in registration order, then blocks until a shutdown signal arrives, a component fails, or an upgrade hands off. It drains all started entries in reverse order and returns the failure that caused the shutdown, if any. Call it once.

type Option

type Option func(*options)

func WithLogger

func WithLogger(log *slog.Logger) Option

WithLogger sets the logger for lifecycle events. Default slog.Default().

func WithShutdownTimeout

func WithShutdownTimeout(d time.Duration) Option

WithShutdownTimeout bounds the total drain time on shutdown. Default 30s.

func WithUpgrade

func WithUpgrade() Option

WithUpgrade enables zero-downtime binary upgrades on SIGHUP via tableflip. It is a no-op on Windows, where the group falls back to plain listeners.

type Server

type Server interface {
	Serve(ln net.Listener) error
	Shutdown(ctx context.Context) error
}

Server is the accepting half of an HTTP(ish) server: Serve blocks on the listener until Shutdown drains it. *http.Server satisfies it directly.

Jump to

Keyboard shortcuts

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