mirror of
https://github.com/zap-proto/zip.git
synced 2026-10-19 15:38:57 +00:00
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.
78 lines
2.8 KiB
Go
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...)
|
|
}
|