grace
Coordinate graceful startup and shutdown of concurrent, long-running tasks in Go.
grace runs a set of tasks together under a shared context. When any task
fails, or the process receives an interrupt, every task is signaled to stop and
grace waits for them all to return before exiting.
A companion httpserver package adapts net/http to this model with sane timeouts
and connection draining.
Concepts
A Runner is any long-running task that blocks until its context is
canceled or it hits an error which should trigger a graceful shutdown:
type Runner interface {
Run(ctx context.Context) error
}
grace.Run starts each Runner concurrently on a shared context.
It blocks until either the parent context is canceled or any Runner returns a
non-nil error, then cancels the shared context so the rest can shut down and
waits for all of them to return. A clean, cancellation-triggered shutdown
reports success (nil error); otherwise the first real error is returned.
grace.StopOnInterrupt derives a context that is canceled on SIGINT
(Ctrl-C) or SIGTERM, giving you a one-line signal handler:
ctx, stop := grace.StopOnInterrupt(context.Background())
defer stop()
Usage
package main
import (
"context"
"log/slog"
"net/http"
"os"
"github.com/quells/grace"
"github.com/quells/grace/pkg/httpserver"
)
func main() {
ctx, stop := grace.StopOnInterrupt(context.Background())
defer stop()
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("hello"))
})
api := httpserver.New(":8080", handler,
httpserver.WithLogger(slog.Default()),
httpserver.WithLoggerLabel("api"),
)
// A separate server for pprof, bound to loopback only.
debug := httpserver.New("127.0.0.1:8888", httpserver.DebugHandler(),
httpserver.WithLogger(slog.Default()),
httpserver.WithLoggerLabel("debug"),
)
if err := grace.Run(ctx, api, debug); err != nil {
slog.Error("server error", slog.String("error", err.Error()))
os.Exit(1)
}
}
grace.Run blocks until you press Ctrl-C (or send SIGTERM), then drains both
servers within their grace periods and returns.
A complete, testable variant lives in
examples/httpserver-example.
httpserver
httpserver.New builds an HTTP server that satisfies grace.Runner.
On shutdown it stops accepting new connections and waits up to a grace period for
in-flight requests to finish before closing.
srv := httpserver.New(addr, handler, opts...)
Defaults: 15s ReadHeaderTimeout, 30s WriteTimeout, and a 5s shutdown grace
period. If addr uses port 0, a listener is bound immediately so the assigned
port is known before Run is called.
Debug endpoints
httpserver.DebugHandler returns a mux wired to the net/http/pprof
endpoints under /debug/pprof/. Serve it on a private, loopback-only address —
never expose it publicly.
Endpoints served by DebugHandler can stream for the full profiling
duration; give the debug server a longer WithWriteTimeout (e.g. several
minutes) so profiles aren't truncated.
Integrating with other long-running task types
The same Runner pattern can be applied to servers of other protocols (gRPC, etc.),
task workers (e.g. Temporal workers), and more. Integrating those into the grace.Run
lifecycle is just a matter of writing a wrapper implementing the Runner interface
and adding it to the list of runners.
License
Released under the MIT License.