fix(readiness): /api/ready svarade 200 med en död motor i produktion #116
@@ -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