From 790d5469b6fb453149090684cfa77e87a9cdc326 Mon Sep 17 00:00:00 2001 From: derekwalter999 Date: Fri, 31 Jul 2026 16:18:43 +0100 Subject: [PATCH] feat: cache warm audit Implements cache-warm for /api/audit on startup to avoid cold-cache spikes. Fixes buffer2 #2. --- src/__tests__/services/auditCacheWarm.test.ts | 87 +++++++++++++++++++ src/index.ts | 5 ++ src/services/auditCacheWarm.ts | 39 +++++++++ 3 files changed, 131 insertions(+) create mode 100644 src/__tests__/services/auditCacheWarm.test.ts create mode 100644 src/services/auditCacheWarm.ts diff --git a/src/__tests__/services/auditCacheWarm.test.ts b/src/__tests__/services/auditCacheWarm.test.ts new file mode 100644 index 00000000..789e419f --- /dev/null +++ b/src/__tests__/services/auditCacheWarm.test.ts @@ -0,0 +1,87 @@ +import { warmAuditCache } from "../../services/auditCacheWarm"; +import { logger } from "../../config/logger"; +import { env } from "../../config/env"; + +jest.mock("../../config/logger", () => ({ + logger: { + info: jest.fn(), + warn: jest.fn(), + }, +})); + +describe("auditCacheWarm", () => { + let originalFetch: typeof global.fetch; + + beforeAll(() => { + originalFetch = global.fetch; + }); + + afterAll(() => { + global.fetch = originalFetch; + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it("should warm the audit cache successfully", async () => { + global.fetch = jest.fn().mockResolvedValue({ + ok: true, + status: 200, + }); + + await warmAuditCache(); + + expect(global.fetch).toHaveBeenCalledWith( + `http://localhost:${env.PORT}/api/audit?limit=10`, + expect.objectContaining({ + method: "GET", + headers: expect.objectContaining({ + "x-audit-cache-warm": "true", + "x-correlation-id": expect.any(String), + }), + }) + ); + + expect(logger.info).toHaveBeenCalledWith( + expect.objectContaining({ url: expect.any(String) }), + "Starting audit cache warm" + ); + + expect(logger.info).toHaveBeenCalledWith( + expect.objectContaining({ status: 200 }), + "Audit cache warm completed successfully" + ); + }); + + it("should handle failed cache warm response", async () => { + global.fetch = jest.fn().mockResolvedValue({ + ok: false, + status: 500, + }); + + await warmAuditCache(); + + expect(logger.warn).toHaveBeenCalledWith( + expect.objectContaining({ + err: expect.any(Error), + url: expect.any(String), + }), + "Audit cache warm failed" + ); + }); + + it("should handle fetch error", async () => { + global.fetch = jest.fn().mockRejectedValue(new Error("Network Error")); + + await warmAuditCache(); + + expect(logger.warn).toHaveBeenCalledWith( + expect.objectContaining({ + err: expect.any(Error), + url: expect.any(String), + }), + "Audit cache warm failed" + ); + }); +}); diff --git a/src/index.ts b/src/index.ts index 42614e05..ac2872b4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -87,6 +87,7 @@ import { gracefulShutdown } from "./lifecycle/shutdown"; import { DrizzleWebhookStore } from "./services/drizzleWebhookStore"; import type { IWebhookDispatcher } from "./services/webhookDispatcher"; import type { WebhookStore } from "./services/webhookStore"; +import { warmAuditCache } from "./services/auditCacheWarm"; const docsEnabled = @@ -276,6 +277,10 @@ if (require.main === module) { if (env.ENABLE_DOCS) { logger.info(`Swagger UI available at http://localhost:${env.PORT}/docs`); } + + warmAuditCache().catch((err) => { + logger.error({ err }, "Unhandled error in warmAuditCache"); + }); }); const handleShutdown = async (signal: string) => { diff --git a/src/services/auditCacheWarm.ts b/src/services/auditCacheWarm.ts new file mode 100644 index 00000000..2a6bd40c --- /dev/null +++ b/src/services/auditCacheWarm.ts @@ -0,0 +1,39 @@ +import { env } from "../config/env"; +import { logger } from "../config/logger"; +import { v4 as uuidv4 } from "uuid"; + +/** + * Warms the /api/audit cache at startup. + * Makes an HTTP request to the local audit endpoint to ensure + * caching layers are primed, mitigating cold-cache latency spikes. + */ +export async function warmAuditCache(): Promise { + const correlationId = uuidv4(); + const url = `http://localhost:${env.PORT}/api/audit?limit=10`; + + try { + logger.info({ correlationId, url }, "Starting audit cache warm"); + + // Make an internal request to our own server, using native fetch. + const response = await fetch(url, { + method: "GET", + headers: { + "x-correlation-id": correlationId, + "x-audit-cache-warm": "true", + }, + // Timeout relatively short to prevent hanging indefinitely + signal: AbortSignal.timeout(5000), + }); + + if (!response.ok) { + throw new Error(`Failed to warm audit cache, status: ${response.status}`); + } + + logger.info( + { correlationId, status: response.status }, + "Audit cache warm completed successfully", + ); + } catch (err) { + logger.warn({ err, correlationId, url }, "Audit cache warm failed"); + } +}