{"id":51436489,"url":"https://github.com/ubgo/shutdown","last_synced_at":"2026-07-05T07:02:09.452Z","repository":{"id":355175380,"uuid":"1227069782","full_name":"ubgo/shutdown","owner":"ubgo","description":"Phased, parallel-within-phase, observable graceful shutdown manager for Go services — k8s-style, zero deps in core, observer-based telemetry, watchdog hard-exit, force-exit on second signal.","archived":false,"fork":false,"pushed_at":"2026-06-24T17:18:23.000Z","size":100,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-24T19:08:34.759Z","etag":null,"topics":["cloud-native","devops","draining","go","golang","graceful-shutdown","k8s","kubernetes","lifecycle","microservices","observability","otel","resilience","sdk","server","shutdown","signals","sigterm","watchdog","zero-dependency"],"latest_commit_sha":null,"homepage":"https://pkg.go.dev/github.com/ubgo/shutdown","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ubgo.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":".github/CODEOWNERS","security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":"NOTICE","maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2026-05-02T06:47:33.000Z","updated_at":"2026-06-24T17:18:30.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/ubgo/shutdown","commit_stats":null,"previous_names":["ubgo/shutdown"],"tags_count":11,"template":false,"template_full_name":null,"purl":"pkg:github/ubgo/shutdown","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ubgo%2Fshutdown","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ubgo%2Fshutdown/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ubgo%2Fshutdown/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ubgo%2Fshutdown/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ubgo","download_url":"https://codeload.github.com/ubgo/shutdown/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ubgo%2Fshutdown/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35145900,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-05T02:00:06.290Z","response_time":100,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cloud-native","devops","draining","go","golang","graceful-shutdown","k8s","kubernetes","lifecycle","microservices","observability","otel","resilience","sdk","server","shutdown","signals","sigterm","watchdog","zero-dependency"],"created_at":"2026-07-05T07:02:07.837Z","updated_at":"2026-07-05T07:02:09.296Z","avatar_url":"https://github.com/ubgo.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# shutdown\n\nPhased, parallel-within-phase, observable graceful shutdown manager for long-running Go services. Designed for Kubernetes, systemd, and any orchestrator that delivers SIGTERM with a grace period.\n\nZero third-party dependencies in the core. Eight adapter modules — five HTTP frameworks (`nethttp`, `gin`, `chi`, `echo`, `fiber`) and three observers (`zap`, `otel`, `prom`) — ship under `contrib/`. Each contrib is its own Go module so importing one doesn't pull in the others.\n\n## Install\n\n```sh\ngo get github.com/ubgo/shutdown\n```\n\n## Quick start — production-ready in 30 lines\n\n```go\npackage main\n\nimport (\n    \"context\"\n    \"log\"\n    \"net/http\"\n    \"time\"\n\n    \"github.com/ubgo/shutdown\"\n    shutdownnethttp \"github.com/ubgo/shutdown/contrib/shutdown-nethttp\"\n)\n\nfunc main() {\n    mgr := shutdown.New(shutdown.WithBudget(30 * time.Second))\n\n    // 1. HTTP server stops accepting first.\n    srv := \u0026http.Server{Addr: \":8080\", Handler: yourMux()}\n    _ = shutdownnethttp.Register(mgr, srv)\n\n    // 2. DB / cache / queue clients close after traffic drains.\n    _ = mgr.Register(\"db\",    closeFn(db.Close),    shutdown.WithPhase(shutdown.PhaseCloseClients))\n    _ = mgr.Register(\"redis\", closeFn(rdb.Close),   shutdown.WithPhase(shutdown.PhaseCloseClients))\n    _ = mgr.Register(\"nats\",  closeFn(nc.Drain),    shutdown.WithPhase(shutdown.PhaseDrainTraffic))\n\n    // 3. OpenTelemetry flushes last so the prior phases' spans actually leave.\n    _ = mgr.Register(\"otel\",  tp.Shutdown,          shutdown.WithPhase(shutdown.PhaseFlushLogs))\n\n    go func() { _ = srv.ListenAndServe() }()\n\n    if err := mgr.Listen(context.Background()); err != nil {\n        log.Fatal(err)\n    }\n}\n\n// closeFn adapts a `func() error` Close method into the manager's\n// `func(ctx) error` HandlerFunc shape.\nfunc closeFn(fn func() error) shutdown.HandlerFunc {\n    return func(_ context.Context) error { return fn() }\n}\n```\n\n`Listen` blocks until SIGTERM/SIGINT, then runs every registered handler in phase order. Handlers in the same phase run in parallel. The whole thing is bounded by `WithBudget`; a watchdog hard-exits if budget plus grace expires.\n\n## Strategy: which phase does each thing go in?\n\nThe seven predefined phases match the typical Kubernetes preStop drain pattern. Lower phases run first. Within a phase, handlers run in parallel — order them across phases, not within.\n\n| Phase | Value | What goes here | Common mistakes |\n|-------|-------|----------------|-----------------|\n| `PhasePreShutdown` | -100 | Flip readiness to Down so the load balancer stops sending. | Don't close anything yet; the LB still has a few seconds of in-flight traffic. |\n| `PhaseStopAccepting` | 0 | Close HTTP / gRPC listeners. | Don't drain in-flight here — that's the next phase. `srv.Shutdown` already does both, but contribs put it here so dependencies live to serve those drains. |\n| `PhaseDrainTraffic` | 100 | Wait for in-flight work to finish. NATS `Drain()`. Worker pools that own queue items. | Closing the DB here will fail in-flight requests. |\n| `PhaseFlushQueues` | 200 | Flush async producers (Kafka, log shippers, batchers). | Don't close the underlying client until next phase. |\n| `PhaseCloseClients` | 300 | Close DB / cache / messaging client connections. | Putting OTEL flush here loses spans for prior phases. |\n| `PhaseFlushLogs` | 400 | Flush logs and traces last so prior errors actually leave the process. | Closing the logger before this phase silences the rest of the shutdown. |\n| `PhasePostShutdown` | 500 | Final cleanup, exit-code reporting. | (Most apps don't need this.) |\n\nPhases are plain `int` — pass any value to `WithPhase` if you need finer-grained ordering between the predefined ones.\n\n## Recipes\n\n### HTTP server with database and Redis\n\n```go\nmgr := shutdown.New(shutdown.WithBudget(30 * time.Second))\n\nsrv := \u0026http.Server{Addr: \":8080\", Handler: r}\n_ = shutdownnethttp.Register(mgr, srv)\n\n_ = mgr.Register(\"db\",    closeFn(db.Close),  shutdown.WithPhase(shutdown.PhaseCloseClients))\n_ = mgr.Register(\"redis\", closeFn(rdb.Close), shutdown.WithPhase(shutdown.PhaseCloseClients))\n\ngo func() { _ = srv.ListenAndServe() }()\n_ = mgr.Listen(ctx)\n```\n\n`db` and `redis` close in parallel (same phase), but only after `srv.Shutdown` has fully returned in the previous phase.\n\n### Background worker (actor pattern)\n\nWhen the run loop and the cancel mechanism are distinct goroutines:\n\n```go\nstop := make(chan struct{})\n\nhandle, _ := mgr.RegisterActor(\"worker\", func(_ error) {\n    close(stop) // tell the run loop to exit\n}, shutdown.WithActorPhase(shutdown.PhaseDrainTraffic))\n\ngo func() {\n    err := worker.Run(stop) // blocks until stop is closed\n    handle.Done(err)         // tells the manager the actor finished\n}()\n```\n\n`mgr.Listen` will fire the interrupt, then wait up to `WithActorTimeout` for `handle.Done`.\n\n### Programmatic shutdown — no OS signals\n\nTests, panic-recovery middleware, custom `/admin/shutdown` endpoints, and health-failure paths can drive shutdown directly:\n\n```go\nmux.HandleFunc(\"/admin/shutdown\", func(w http.ResponseWriter, r *http.Request) {\n    if !authorized(r) { w.WriteHeader(http.StatusForbidden); return }\n    go func() {\n        _ = mgr.Shutdown(context.Background()) // same execution path as a signal\n    }()\n    w.WriteHeader(http.StatusAccepted)\n})\n```\n\n### Reload signal (SIGHUP / SIGUSR1) — Gunicorn-style\n\n```go\nmgr.OnSignal(syscall.SIGHUP, func(ctx context.Context, _ os.Signal) {\n    if err := config.Reload(); err != nil {\n        log.Println(\"reload failed:\", err)\n    }\n})\nmgr.OnSignal(syscall.SIGUSR1, func(ctx context.Context, _ os.Signal) {\n    rotateLogFile()\n})\n```\n\nThe hook fires; shutdown is NOT triggered. The hooked signal is automatically added to the listened set — no need to also pass it to `WithSignals`.\n\n### Stack three observers at once\n\nThe observer pattern is composable: subscribe as many as you like.\n\n```go\nmgr.Subscribe(shutdownzap.Observer(zapLogger))\nmgr.Subscribe(shutdownotel.Observer(tracer))\nmgr.Subscribe(shutdownprom.Observer(promMetrics))\n```\n\nYou get structured logs, distributed traces, and metrics from a single shutdown sequence — without any of the contribs knowing about each other.\n\n### Custom observer for ad-hoc telemetry\n\nDon't want a whole contrib for a one-off webhook? Subscribe inline:\n\n```go\nmgr.Subscribe(shutdown.Observer{\n    OnComplete: func(total time.Duration, err error) {\n        status := \"success\"\n        if err != nil { status = \"fail\" }\n        _ = postWebhook(map[string]any{\n            \"event\":    \"shutdown_complete\",\n            \"duration\": total.String(),\n            \"status\":   status,\n        })\n    },\n})\n```\n\n## Error handling\n\nEvery handler can return an error. By default the manager runs every phase to completion and returns `errors.Join(...)` of the lot:\n\n```go\nerr := mgr.Shutdown(ctx)\nif err != nil {\n    var pe *shutdown.PanicError\n    if errors.As(err, \u0026pe) {\n        log.Printf(\"handler %q panicked: %v\", pe.Name, pe.Value)\n    }\n    log.Printf(\"shutdown errors: %v\", err)\n}\n```\n\nA panicking handler is converted to a `*PanicError` (wrapped in the aggregate) rather than crashing the shutdown goroutine.\n\nSwitch to fail-fast with `WithErrorPolicy(StopOnError)` if you'd rather skip subsequent phases on the first failure.\n\n### Exit codes\n\nBy default `Listen` returns the error to your caller and you control `os.Exit`. To make the manager exit for you:\n\n```go\nmgr := shutdown.New(\n    shutdown.WithExitOnComplete(0, 1), // success, failure\n)\n_ = mgr.Listen(ctx) // never returns to caller; os.Exit(0|1) at end\n```\n\nA second SIGTERM during shutdown calls `os.Exit(130)` immediately — the operator's escape hatch when something hangs. Tune via `WithForceOnSecondSignal(true, 130)` (or disable with `false, 0`).\n\n## Watchdog\n\n`WithBudget(d)` is the wall-clock budget across all phases. After budget plus a 1-second grace period (`WithWatchdogGrace`), the watchdog calls `os.Exit(failureCode)` even if handlers are still mid-execution. Stuck handler names are logged so you have something to grep for.\n\n```go\nmgr := shutdown.New(\n    shutdown.WithBudget(25*time.Second),    // soft limit\n    shutdown.WithWatchdogGrace(2*time.Second), // after which: os.Exit\n    shutdown.WithExitOnComplete(0, 1),\n)\n```\n\nThis is the answer to \"k8s SIGKILLs us at `terminationGracePeriodSeconds`\": set the budget a few seconds shorter and exit clean before the kernel is involved.\n\n### Tuning the timeout cascade\n\nWhen you wrap a library that has its own internal deadline (e.g. `gocron.WithStopTimeout`, `redis.Options.PoolTimeout`, gRPC client `WithTimeout`), three layers compete:\n\n```\n[underlying lib's own deadline]   \u003c   per-handler shutdown.WithTimeout   \u003c   manager.WithBudget\n```\n\nEach layer must outlive the one below it. Reverse the order and you get bugs:\n\n| Mistake | What happens |\n|---------|--------------|\n| `WithBudget` \u003c library's deadline | Watchdog hard-exits before the library finishes its drain. In-flight work killed; orchestrator sees a non-zero exit. |\n| `WithTimeout` \u003c library's deadline | Manager cancels the per-handler context mid-call. The library's `Shutdown` returns `ctx.Err`; whatever it was draining keeps running in a goroutine no one is waiting on. |\n| Both equal | Race condition — sometimes works, sometimes the watchdog wins. |\n\nFor typical service shapes:\n\n| Service | Library deadline | `WithTimeout` | `WithBudget` |\n|---------|------------------|---------------|--------------|\n| HTTP API + DB pool | n/a | 10–15s per handler | 30s |\n| HTTP API + slow downstream calls | call-level timeout (5–30s) | call timeout + 1s | 60s |\n| gocron with hour-long jobs | `WithStopTimeout(24h)` | `24h 30m` | `25h` |\n| Batch job that must not be interrupted | n/a | per-handler enough for cleanup | `WithBudget(0)` (disable; rely on orchestrator) |\n\nThe 24-hour gocron case is shown end-to-end in [`shutdown-examples/11-cron-gocron`](https://github.com/ubgo/shutdown-examples/tree/main/11-cron-gocron).\n\n## Adapters\n\nAdapter modules ship as separate Go modules under `contrib/`. Import only the ones you use; each pulls only its own dependencies.\n\n| Adapter | Module path | Role |\n|---------|-------------|------|\n| [`shutdown-nethttp`](contrib/shutdown-nethttp) | `github.com/ubgo/shutdown/contrib/shutdown-nethttp` | `*http.Server.Shutdown` registered as a phase handler |\n| [`shutdown-gin`](contrib/shutdown-gin) | `github.com/ubgo/shutdown/contrib/shutdown-gin` | Same, for the `*http.Server` wrapping a Gin engine |\n| [`shutdown-chi`](contrib/shutdown-chi) | `github.com/ubgo/shutdown/contrib/shutdown-chi` | Same, for the `*http.Server` wrapping a Chi router |\n| [`shutdown-echo`](contrib/shutdown-echo) | `github.com/ubgo/shutdown/contrib/shutdown-echo` | `*echo.Echo.Shutdown` (Echo owns its server) |\n| [`shutdown-fiber`](contrib/shutdown-fiber) | `github.com/ubgo/shutdown/contrib/shutdown-fiber` | `*fiber.App.ShutdownWithContext` |\n| [`shutdown-zap`](contrib/shutdown-zap) | `github.com/ubgo/shutdown/contrib/shutdown-zap` | Observer that emits structured logs via `go.uber.org/zap` |\n| [`shutdown-otel`](contrib/shutdown-otel) | `github.com/ubgo/shutdown/contrib/shutdown-otel` | Observer that emits OpenTelemetry spans (root + phase + handler) |\n| [`shutdown-prom`](contrib/shutdown-prom) | `github.com/ubgo/shutdown/contrib/shutdown-prom` | Observer that exports Prometheus metrics |\n\nRunnable end-to-end demos for each pattern live in [`ubgo/shutdown-examples`](https://github.com/ubgo/shutdown-examples).\n\n## Comparison\n\n| Feature | uber-fx | oklog/run | tokio-graceful-shutdown | terminus | **`ubgo/shutdown`** |\n|---------|:-------:|:---------:|:------------------------:|:--------:|:--------------------:|\n| Phase-based ordering | ❌ | ❌ | ❌ | ❌ | **✅** |\n| Parallel within phase | ❌ | partial | ✅ | ❌ | **✅** |\n| Force-exit on second signal | ❌ | ❌ | ✅ | ❌ | **✅** |\n| Watchdog hard-exit | ❌ | ❌ | ✅ | ❌ | **✅** |\n| Observer pattern | ❌ | ❌ | ❌ | ❌ | **✅** |\n| Actor (run+interrupt) pairs | ❌ | ✅ | partial | ❌ | **✅** |\n| Reload signal hook | ❌ | ❌ | ❌ | ❌ | **✅** |\n| Panic in handler → aggregate err | ❌ | ❌ | ❌ | ❌ | **✅** |\n| Zero-dep core | ❌ | ✅ | ❌ | ❌ | **✅** |\n\n## Compatibility\n\nRequires Go 1.24 or later.\n\n## License\n\nApache License 2.0. See [`LICENSE`](./LICENSE) and [`NOTICE`](./NOTICE).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fubgo%2Fshutdown","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fubgo%2Fshutdown","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fubgo%2Fshutdown/lists"}