Skip to content

Ownership and teardown

Every route, named middleware, and event subscription is tagged with the service that registered it. That bookkeeping exists for one reason: a single service can be shut down at runtime without restarting the process.

app.CloseService("auth") // everything auth registered now returns 503
app.RestartService("auth") // re-initializes and re-arms it

Everything the service registered is replaced with a static 503 in the unified error shape. That means its routes, and also every named middleware it registered, wherever that middleware is referenced, including on routes owned by services that are still running.

router.Register("auth", "require-auth", requireAuth) // owned by "auth"
router.Route("/api", func(sub *gas.Router) {
sub.Use(gas.MiddlewareByName("require-auth"))
sub.Handle("billing", http.MethodGet, "/invoices", handler) // owned by "billing"
})
app.CloseService("auth")
// GET /api/invoices -> 503, even though "billing" is still running.

Other services can respond, which is how a system degrades deliberately rather than by accident.

gas.SubscribeWithOwner(bus, s.Name(), gas.SystemServiceClosed,
func(data gas.SystemServiceClosedPayload) {
// enter degraded mode if data.ServiceName was a dependency
})

Events are typed, so payloads are checked at compile time rather than asserted at runtime.

var UserCreated = gas.Event[UserCreatedPayload]{Name: "user:created"}
type UserCreatedPayload struct{ Email string }
gas.SubscribeWithOwner(bus, s.Name(), UserCreated, func(data UserCreatedPayload) {
// provision a billing account for data.Email
})
gas.Emit(bus, UserCreated, UserCreatedPayload{Email: "user@example.com"}).Wait()

Emit returns a *sync.WaitGroup, so the caller decides whether to wait for handlers or move on. Subscribing with SubscribeWithOwner is what ties the subscription to a service so teardown can remove it; an unowned subscription lives until the process exits.

Event Fired when
gas.SystemServiceClosed A service is closed via CloseService
gas.SystemServiceInitialized A service finishes Init, including on restart
gas.SystemAllServicesInitialized Every service has initialized successfully
gas.SystemShuttingDown A Worker or App begins shutdown; always fires
gas.SystemServerShuttingDown The HTTP server begins graceful shutdown; App only
gas.AppConfigUpdated App config is updated after binding; App only

Rarely, and that is fine. The kill-switch earns its keep when one subsystem is failing in a way that threatens the rest, a dependency is down and its routes should fail fast rather than pile up, or you are draining one feature ahead of a deploy. If you never call CloseService, ownership tracking still pays for itself by making the route map attributable: router.Routes() reports who owns what.