fix: await flushDirtyEntries during shutdown to prevent usage data loss
Some checks failed
Build & Deploy to Staging / Build & Deploy to Staging (push) Has been cancelled
Some checks failed
Build & Deploy to Staging / Build & Deploy to Staging (push) Has been cancelled
Remove fire-and-forget SIGTERM/SIGINT handlers from usage.ts (race condition with pool.end() in index.ts). Instead, await flushDirtyEntries() in the index.ts shutdown orchestrator between stopping the server and closing the DB pool.
This commit is contained in:
parent
b964b98a8b
commit
2b4fa0c690
3 changed files with 41 additions and 4 deletions
30
src/__tests__/usage-shutdown.test.ts
Normal file
30
src/__tests__/usage-shutdown.test.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||||
|
|
||||||
|
vi.unmock("../middleware/usage.js");
|
||||||
|
|
||||||
|
describe("usage shutdown race condition fix", () => {
|
||||||
|
it("should NOT register SIGTERM/SIGINT handlers as module-level side effects", async () => {
|
||||||
|
const onSpy = vi.spyOn(process, "on");
|
||||||
|
vi.resetModules();
|
||||||
|
|
||||||
|
// Track which signals usage.ts registers
|
||||||
|
const signalsBefore = onSpy.mock.calls.map(c => c[0]);
|
||||||
|
|
||||||
|
await import("../middleware/usage.js");
|
||||||
|
|
||||||
|
const signalsAfter = onSpy.mock.calls.map(c => c[0]);
|
||||||
|
const newSignals = signalsAfter.slice(signalsBefore.length);
|
||||||
|
|
||||||
|
// usage.ts should NOT register SIGTERM or SIGINT handlers
|
||||||
|
const usageSignals = newSignals.filter(s => s === "SIGTERM" || s === "SIGINT");
|
||||||
|
expect(usageSignals).toEqual([]);
|
||||||
|
|
||||||
|
onSpy.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should export flushDirtyEntries for external shutdown orchestration", async () => {
|
||||||
|
vi.resetModules();
|
||||||
|
const usageMod = await import("../middleware/usage.js");
|
||||||
|
expect(typeof usageMod.flushDirtyEntries).toBe("function");
|
||||||
|
});
|
||||||
|
});
|
||||||
10
src/index.ts
10
src/index.ts
|
|
@ -18,7 +18,7 @@ import { recoverRouter } from "./routes/recover.js";
|
||||||
import { emailChangeRouter } from "./routes/email-change.js";
|
import { emailChangeRouter } from "./routes/email-change.js";
|
||||||
import { billingRouter } from "./routes/billing.js";
|
import { billingRouter } from "./routes/billing.js";
|
||||||
import { authMiddleware } from "./middleware/auth.js";
|
import { authMiddleware } from "./middleware/auth.js";
|
||||||
import { usageMiddleware, loadUsageData } from "./middleware/usage.js";
|
import { usageMiddleware, loadUsageData, flushDirtyEntries } from "./middleware/usage.js";
|
||||||
import { getUsageStats } from "./middleware/usage.js";
|
import { getUsageStats } from "./middleware/usage.js";
|
||||||
import { pdfRateLimitMiddleware, getConcurrencyStats } from "./middleware/pdfRateLimit.js";
|
import { pdfRateLimitMiddleware, getConcurrencyStats } from "./middleware/pdfRateLimit.js";
|
||||||
import { initBrowser, closeBrowser } from "./services/browser.js";
|
import { initBrowser, closeBrowser } from "./services/browser.js";
|
||||||
|
|
@ -406,6 +406,14 @@ async function start() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 1.5. Flush dirty usage entries while DB pool is still alive
|
||||||
|
try {
|
||||||
|
await flushDirtyEntries();
|
||||||
|
logger.info("Usage data flushed");
|
||||||
|
} catch (err) {
|
||||||
|
logger.error({ err }, "Error flushing usage data during shutdown");
|
||||||
|
}
|
||||||
|
|
||||||
// 2. Close Puppeteer browser pool
|
// 2. Close Puppeteer browser pool
|
||||||
try {
|
try {
|
||||||
await closeBrowser();
|
await closeBrowser();
|
||||||
|
|
|
||||||
|
|
@ -73,9 +73,8 @@ export async function flushDirtyEntries(): Promise<void> {
|
||||||
// Periodic flush
|
// Periodic flush
|
||||||
setInterval(flushDirtyEntries, FLUSH_INTERVAL_MS);
|
setInterval(flushDirtyEntries, FLUSH_INTERVAL_MS);
|
||||||
|
|
||||||
// Flush on process exit
|
// Note: SIGTERM/SIGINT flush is handled by the shutdown orchestrator in index.ts
|
||||||
process.on("SIGTERM", () => { flushDirtyEntries().catch(() => {}); });
|
// to avoid race conditions with pool.end().
|
||||||
process.on("SIGINT", () => { flushDirtyEntries().catch(() => {}); });
|
|
||||||
|
|
||||||
export function usageMiddleware(req: any, res: any, next: any): void {
|
export function usageMiddleware(req: any, res: any, next: any): void {
|
||||||
const keyInfo = req.apiKeyInfo;
|
const keyInfo = req.apiKeyInfo;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue