You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(runtime): carve liveness out of the identity step so a config fault cannot restart a pod (#16561)
`HttpDispatcher.dispatch()` resolved a per-request identity before any route
handler ran, and that step reads the tenancy posture for every request —
credentialed or not. A `tenancy` service that is registered and fails to build
is re-raised as 503 rather than absorbed into "there is no posture", so an
uncredentialed liveness probe was answered 503 for the length of the outage: a
liveness 503 means "restart me", the service fails to build again on the new
pod, and the restart loop hides the fault the 503 exists to make loud.
A route may now declare `liveness: true` on its registry entry. `dispatch()`
runs such a route's handler directly — no identity resolution, no gate, nothing
that reads configuration or credentials. `/health` declares it; its payload was
already process-local. `/ready` is untouched and keeps the full identity step
and its 503 body, so traffic is still withheld until the fault is fixed.
Which routes count as liveness is DERIVED from the dispatcher's own route
table: `DomainHandlerRegistry.resolveLiveness()` is `resolve()` plus one field
read, so it answers through the same matcher that picks the handler and cannot
drift from the routes that exist. There is no second list of paths.
Claude-Session: https://claude.ai/code/session_01YFY46JydE1gMxQG1TqBcMZ
Co-authored-by: Claude <noreply@anthropic.com>
`GET /api/v1/health` answers 200 whenever the process can serve HTTP, even while a configuration fault is making every other route 503.
6
+
7
+
The dispatcher resolves a per-request identity before any route handler runs, and that step reads the tenancy posture for every request — credentialed or not. Since a `tenancy` service that is registered and fails to build is (correctly) re-raised as a 503 rather than absorbed into "there is no posture", an uncredentialed liveness probe was answered 503 for the length of the outage. A liveness probe reads 503 as *restart me*; the service then fails to build again on the new pod. A restart cannot fix a service that cannot build, so the result was a restart loop that hid the very fault the 503 exists to make loud.
8
+
9
+
**Liveness is now carved out of the identity step.**`GET /health` runs its handler directly: no identity resolution, no configuration read, no credential read — the payload it answers (`status`, `timestamp`, `version`, `uptime`) was already process-local. Wire it to `livenessProbe`.
10
+
11
+
**Readiness is unchanged, deliberately.**`GET /ready` keeps the full identity step and its 503 body, so traffic is withheld until the fault is fixed and existing operator dashboards keep the signal they have. Wire it to `readinessProbe`. Nothing else about the 503 moved: every other route, and an environment-scoped `/environments/:id/health`, answers exactly as before.
12
+
13
+
Which routes count as liveness is **derived from the dispatcher's own route table** rather than listed anywhere: a route declares `liveness: true` on its registry entry, and `DomainHandlerRegistry.resolveLiveness()` answers through the same matcher that picks the handler — so the set cannot drift from the routes that exist. `DomainRoute.liveness` and `resolveLiveness()` are additive public surface on `@objectstack/runtime`; a route that does not declare the flag is untouched.
0 commit comments