Files
zip/shutdown.go
T
zeekay 7afa7ee42b here: a co-resident caller runs the op, and MCP rides zap-proto/mcp
Two things that are the same thing: a call does not need a wire, only an
address, and a protocol layer belongs in the module that names it.

Ask names an app and reaches it over that app's canonical socket. When the app
is THIS process that socket is a loop -- the value the caller holds is
serialized, handed to the kernel, read back, parsed into a fresh copy, and
dispatched to a function pointer that was in memory the whole time. Nothing
about the call needed a wire; only the ADDRESSING did, and addressing is what a
by-name call already has. So a bound listener records which App answers for it,
Serving reads that back, and Here runs the op through the op's OWN invoke seam --
the same validate -> authorize -> run core a REST request, an MCP tools/call, a
CLI command and a socket call all land on. One fewer transport, not one more.

That is what a co-resident caller uses INSTEAD of building a request for its own
router to take apart again. The pattern it replaces cost hanzoai/cloud a
re-entrancy counter keyed on a goroutine id parsed out of runtime.Stack, because
dispatching a request into the whole app re-ran every edge middleware and a
middleware that read the app it was serving recursed without bound. A direct op
call cannot express that shape: it enters one op, not one router.

MCP now comes from github.com/zap-proto/mcp v1.0.5 rather than being hand-rolled
here, so it sits beside zap-proto/http as a protocol layer over ZAP instead of
inside the framework as a special case.

TestServePathAllocsAreChainDepthInvariant fails at 34 allocs/request against a
budget of 1. It fails identically at v1.26.0 -- verified in a clean worktree at
that tag -- so it is pre-existing and not this change. Named here rather than
left for the next person to rediscover.
2026-08-05 21:29:52 -07:00

78 lines
2.8 KiB
Go

package zip
import (
"context"
"errors"
)
// OnShutdown registers fn as a teardown hook, run during Shutdown /
// ShutdownWithContext. This is the one teardown primitive zip exposes:
// subsystems register their own cleanup at mount time, and reverse-mount
// teardown falls out for free (see the ordering note below).
//
// Ordering. Hooks run LAST in the shutdown sequence — after listeners stop
// accepting and after in-flight requests drain — and in LIFO order (reverse
// registration = reverse mount order). Draining first means a subsystem's
// teardown never races the requests still using it; LIFO means a dependency
// mounted before its dependents is torn down after them.
//
// Errors. Every hook runs even if an earlier one fails; all hook errors (and
// the drain error) are aggregated with errors.Join and returned from Shutdown.
//
// Concurrency. Registration is safe from multiple goroutines. A nil fn is
// ignored. Registering after Shutdown has begun is a no-op: the hook is
// dropped (never run) and a warning is logged — there is no longer a shutdown
// to hook into, and running it immediately would give OnShutdown two meanings
// depending on timing. Register teardown at mount time, before Shutdown.
func (a *App) OnShutdown(fn func(context.Context) error) {
if fn == nil {
return
}
a.hookMu.Lock()
if a.shuttingDown {
a.hookMu.Unlock()
a.logger.Warn("zip: OnShutdown called after Shutdown; hook dropped")
return
}
a.hooks = append(a.hooks, fn)
a.hookMu.Unlock()
}
// shutdown is the single graceful-shutdown sequence behind Shutdown and
// ShutdownWithContext. It is idempotent: the first call claims shutdown under
// hookMu (snapshotting and clearing the hooks) and every later call returns
// nil without repeating any step, so hooks run at most once.
//
// Sequence: (1) stop every transport accepting new connections; (2) drain
// in-flight requests via fiber, bounded by ctx; (3) run teardown hooks LIFO,
// passing ctx to each. All errors are joined and returned.
func (a *App) shutdown(ctx context.Context) error {
a.hookMu.Lock()
if a.shuttingDown {
a.hookMu.Unlock()
return nil
}
a.shuttingDown = true
hooks := a.hooks
a.hooks = nil
a.hookMu.Unlock()
// 1. Stop accepting new connections on every transport listener.
a.closeServers()
a.closeMCP()
// 2. Drain in-flight requests (graceful; bounded by ctx).
// A program that was never served has no router to drain.
var errs []error
if g := a.live.Load(); g != nil {
errs = append(errs, g.router.ShutdownWithContext(ctx))
}
// 3. Tear subsystems down in reverse mount order (LIFO). Run ALL hooks
// even if some fail; aggregate every error.
for i := len(hooks) - 1; i >= 0; i-- {
if err := hooks[i](ctx); err != nil {
errs = append(errs, err)
}
}
return errors.Join(errs...)
}