graceful

July 7, 2026 · View on GitHub

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.

TriggerBehavior
SIGINT / SIGTERMstop accepting, drain every component, return nil
a start returns non-nildrain 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