diff --git a/server/src/honoUtils/handleHealthCheck.ts b/server/src/honoUtils/handleHealthCheck.ts index fb57dd907..4755a09d2 100644 --- a/server/src/honoUtils/handleHealthCheck.ts +++ b/server/src/honoUtils/handleHealthCheck.ts @@ -1,6 +1,31 @@ import type { Context } from "hono"; +import { logger } from "@/external/logtail/logtailUtils.js"; +import { redis } from "@/external/redis/initRedis.js"; +import { redisV2 } from "@/external/redis/initRedisV2.js"; import type { HonoEnv } from "./HonoEnv"; +let startupReady = false; + +const tryLatchStartupReady = () => { + if (startupReady) return; + if (redis.status !== "ready" || redisV2.status !== "ready") return; + startupReady = true; + logger.info("[health-check] startup gate latched (Redis ready)", { + redis_status: redis.status, + redis_v2_status: redisV2.status, + }); +}; + +redis.once("ready", tryLatchStartupReady); +redisV2.once("ready", tryLatchStartupReady); +tryLatchStartupReady(); + export const handleHealthCheck = async (c: Context) => { + if (!startupReady) { + tryLatchStartupReady(); + if (!startupReady) { + return c.text("Redis not ready", 503); + } + } return c.text("Hello from Autumn (test 1) 🍂🍂🍂"); }; diff --git a/server/tests/unit/honoUtils/handle-health-check.test.ts b/server/tests/unit/honoUtils/handle-health-check.test.ts new file mode 100644 index 000000000..302e5c038 --- /dev/null +++ b/server/tests/unit/honoUtils/handle-health-check.test.ts @@ -0,0 +1,78 @@ +import { describe, expect, mock, test } from "bun:test"; +import { EventEmitter } from "node:events"; + +class FakeRedis extends EventEmitter { + status: "wait" | "connecting" | "ready" | "end" = "wait"; + + setReady() { + this.status = "ready"; + this.emit("ready"); + } +} + +const fakeRedis = new FakeRedis(); +const fakeRedisV2 = new FakeRedis(); +const fakeLogger = { + info: mock(() => undefined), + warn: mock(() => undefined), + error: mock(() => undefined), + debug: mock(() => undefined), +}; + +mock.module("@/external/redis/initRedis.js", () => ({ redis: fakeRedis })); +mock.module("@/external/redis/initRedisV2.js", () => ({ + redisV2: fakeRedisV2, +})); +mock.module("@/external/logtail/logtailUtils.js", () => ({ + logger: fakeLogger, +})); + +const { handleHealthCheck } = await import("@/honoUtils/handleHealthCheck.js"); + +const callHealthCheck = async () => { + const responses: { status: number; body: string }[] = []; + const ctx = { + text: (body: string, status = 200) => { + responses.push({ status, body }); + return { status, body }; + }, + }; + await handleHealthCheck( + ctx as unknown as Parameters[0], + ); + return responses[0]!; +}; + +describe("handleHealthCheck startup gate", () => { + test("returns 503 when both Redis clients are not ready", async () => { + const res = await callHealthCheck(); + expect(res.status).toBe(503); + expect(res.body).toBe("Redis not ready"); + }); + + test("returns 503 when only one Redis client is ready", async () => { + fakeRedis.setReady(); + const res = await callHealthCheck(); + expect(res.status).toBe(503); + }); + + test("returns 200 once both Redis clients become ready", async () => { + fakeRedisV2.setReady(); + const res = await callHealthCheck(); + expect(res.status).toBe(200); + expect(res.body).toContain("Autumn"); + }); + + test("logs a single latch info message on flip", async () => { + expect(fakeLogger.info).toHaveBeenCalledTimes(1); + const firstCall = fakeLogger.info.mock.calls[0] as unknown as [string]; + expect(firstCall[0]).toContain("[health-check] startup gate latched"); + }); + + test("returns 200 unconditionally after latch — even if Redis regresses", async () => { + fakeRedis.status = "end"; + fakeRedisV2.status = "end"; + const res = await callHealthCheck(); + expect(res.status).toBe(200); + }); +});