fix(readiness): /api/ready svarade 200 med en död motor i produktion
CI (SIAX Cloud) / integration (push) Skipped
masterplan-lock / check (push) Successful in 6s
CI (SIAX Cloud) / security (push) Successful in 15s
masterplan-lock / check (pull_request) Successful in 4s
Security Scan (SIAX Cloud) / security (push) Successful in 27s
CI (SIAX Cloud) / security (pull_request) Successful in 23s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 24s
CI (SIAX Cloud) / quality (push) Successful in 45s
CI (SIAX Cloud) / integration (pull_request) Successful in 31s
CI (SIAX Cloud) / quality (pull_request) Successful in 40s
CI (SIAX Cloud) / integration (push) Skipped
masterplan-lock / check (push) Successful in 6s
CI (SIAX Cloud) / security (push) Successful in 15s
masterplan-lock / check (pull_request) Successful in 4s
Security Scan (SIAX Cloud) / security (push) Successful in 27s
CI (SIAX Cloud) / security (pull_request) Successful in 23s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 24s
CI (SIAX Cloud) / quality (push) Successful in 45s
CI (SIAX Cloud) / integration (pull_request) Successful in 31s
CI (SIAX Cloud) / quality (pull_request) Successful in 40s
Mätt live 2026-09-25 mot produktionsdeployen (0pt.app / api.0pt.app,
container shxd6lepxuxfsjsp0ath3z1g-172514162615 på server6, uppe 17,9 h,
Docker-status "healthy" hela tiden):
GET /api/health -> HTTP/1.1 200 OK
{"ok":false,"service":"0pt-web","db":"ok","queue":"not_configured",
"engine":"error","stripe":"not_configured",
"detail":{"engine":"This operation was aborted"}}
GET /api/ready -> HTTP/1.1 200 OK
{"ready":true,"service":"0pt-web","db":"ok","engine":"error",...}
ENGINE_URL pekade på http://0pt-engine:8000. Mätt inifrån containern:
`getent hosts 0pt-engine` ger ingen post, och det finns ingen sådan
container på någon av server5-11. Ett direktanrop mot motorn misslyckas
efter ~5 s (DNS). Datamotorn — 0pt:s hela produkt — var alltså BORTA, och
samtliga signaler ovanför den var gröna: Docker healthy, /api/health 200,
/api/ready 200.
`ok:false` i en 200-kropp är ingen signal. Ingenting i kedjan läser
kroppar; allt läser statuskoder och exitkoder.
isReady skilde tidigare bara på "db ok". Den skillnad som saknades är inte
konfigurerad-vs-ej-konfigurerad utan FRÅNVARANDE-vs-TRASIG:
* not_configured / not_integrated -> fortsatt ready. 0pt är byggt för att
kunna köra utan motor, Stripe och kö (se .env.example).
* error -> INTE ready. Statusen "error" betyder att någon medvetet
konfigurerade beroendet och att en riktig round trip mot det just
misslyckades. Det är inte längre en valfri integration.
Ändrar ingenting i driften i dag: container-HEALTHCHECK pekar på
/api/health (liveness) och Dockerfilen säger uttryckligen att en
readiness-probe inte ska peka dit. Den här ändringen gör bara att den
ärliga ytan faktiskt blir ärlig, så en yttre övervakning kan använda den.
Regressionstest tillagt; verifierat att det FALLER mot den gamla isReady
(1 failed | 18 passed) och passerar mot den nya (24 passed över
health-checks/ready/health).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -10,9 +10,11 @@ import { checkAllDependencies, isReady } from "@/lib/health-checks";
|
||||
// readiness probe at this route, not /api/health, to stop routing traffic to
|
||||
// an instance whose database is down without restarting it.
|
||||
//
|
||||
// Today the only load-bearing dependency is the database (see
|
||||
// Load-bearing by absence: only the database (see
|
||||
// `lib/health-checks.ts#isReady`) -- engine/stripe/queue are optional
|
||||
// integrations 0pt is designed to run without.
|
||||
// integrations 0pt is designed to run without. Load-bearing by FAILURE: all
|
||||
// of them. A dependency somebody configured and that then fails a live round
|
||||
// trip makes this instance not-ready, whichever one it is.
|
||||
export async function GET() {
|
||||
const deps = await checkAllDependencies();
|
||||
const ready = isReady(deps);
|
||||
|
||||
+25
-4
@@ -165,10 +165,31 @@ export function hasDegradedDependency(deps: DependencyResults): boolean {
|
||||
|
||||
/**
|
||||
* Readiness (unlike liveness) treats "not configured" for the deps this app
|
||||
* cannot serve real traffic without as not-ready too. Only `db` is load-bearing
|
||||
* today -- engine/stripe/queue are optional integrations the app is designed
|
||||
* to run without (see .env.example).
|
||||
* cannot serve real traffic without as not-ready too. `db` is the only
|
||||
* dependency that is load-bearing merely by being absent -- engine/stripe/
|
||||
* queue are optional integrations the app is designed to run WITHOUT (see
|
||||
* .env.example), so "not_configured" for those stays ready.
|
||||
*
|
||||
* But "optional to run without" is not the same as "optional to run BROKEN".
|
||||
* A dependency in the "error" state was deliberately configured by an
|
||||
* operator AND then failed a live round trip -- somebody asked for it and it
|
||||
* is not answering. Readiness fails closed on that too.
|
||||
*
|
||||
* Measured 2026-09-25 on the production deployment (0pt.app / api.0pt.app),
|
||||
* which is exactly the hole this closes:
|
||||
*
|
||||
* GET /api/health -> HTTP 200 {"ok":false, ..., "engine":"error",
|
||||
* "detail":{"engine":"This operation was aborted"}}
|
||||
* GET /api/ready -> HTTP 200 {"ready":true, "db":"ok", "engine":"error"}
|
||||
*
|
||||
* ENGINE_URL pointed at `http://0pt-engine:8000`, a hostname with no DNS
|
||||
* record anywhere on the fleet (`getent hosts 0pt-engine` -> nothing; no such
|
||||
* container on server5-11). The data engine -- 0pt's whole product -- had
|
||||
* been absent for the container's entire 17.9 h uptime, and every signal
|
||||
* above it was green. `ok:false` in a 200 body is not a signal; nothing reads
|
||||
* bodies.
|
||||
*/
|
||||
export function isReady(deps: DependencyResults): boolean {
|
||||
return deps.db.status === "ok";
|
||||
if (deps.db.status !== "ok") return false;
|
||||
return !Object.values(deps).some((d) => d.status === "error");
|
||||
}
|
||||
|
||||
@@ -179,7 +179,31 @@ describe("hasDegradedDependency / isReady", () => {
|
||||
expect(isReady(deps({ db: { status: "error" } }))).toBe(false);
|
||||
});
|
||||
|
||||
it("isReady is true once db is ok, regardless of optional integrations", () => {
|
||||
it("isReady is true once db is ok and no configured integration is failing", () => {
|
||||
expect(isReady(deps({ db: { status: "ok" } }))).toBe(true);
|
||||
});
|
||||
|
||||
it("isReady stays true for integrations that are merely absent (0pt boots without them)", () => {
|
||||
expect(
|
||||
isReady(
|
||||
deps({
|
||||
db: { status: "ok" },
|
||||
engine: { status: "not_configured" },
|
||||
stripe: { status: "not_configured" },
|
||||
queue: { status: "not_integrated" },
|
||||
}),
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("REGRESSION: isReady is false when a CONFIGURED integration is failing its live round trip", () => {
|
||||
// Mätt i prod 2026-09-25: ENGINE_URL pekade på 0pt-engine:8000, en värd
|
||||
// utan DNS-post någonstans i flottan, och /api/ready svarade ändå
|
||||
// {"ready":true,...,"engine":"error"}. En konfigurerad-men-död motor är
|
||||
// inte "valfri" -- någon har bett om den och den svarar inte.
|
||||
expect(isReady(deps({ db: { status: "ok" }, engine: { status: "error", detail: "fetch failed" } }))).toBe(
|
||||
false,
|
||||
);
|
||||
expect(isReady(deps({ db: { status: "ok" }, stripe: { status: "error", detail: "bad key" } }))).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user