From 4ee131f2411da4e85bd7a622a7d8fb9f49e028b1 Mon Sep 17 00:00:00 2001 From: Kieran Klukas Date: Wed, 3 Jun 2026 19:58:20 -0400 Subject: [PATCH] feat: proper shutdown sequence --- src/cache.ts | 38 ++++++++++++++++++++++++++++++-------- src/index.ts | 9 +++++++-- 2 files changed, 37 insertions(+), 10 deletions(-) diff --git a/src/cache.ts b/src/cache.ts index 7e929cf..8faa8c3 100644 --- a/src/cache.ts +++ b/src/cache.ts @@ -1,5 +1,5 @@ import { Database } from "bun:sqlite"; -import { schedule } from "node-cron"; +import { schedule, type ScheduledTask } from "node-cron"; import { bucketAnalyticsMigration } from "./migrations/bucketAnalyticsMigration"; import { endpointGroupingMigration } from "./migrations/endpointGroupingMigration"; import { logGroupingMigration } from "./migrations/logGroupingMigration"; @@ -48,6 +48,10 @@ class Cache { private analytics: AnalyticsQueryService; private healthMonitor: HealthMonitor; + // Scheduled task handles for cleanup + private cronTasks: ScheduledTask[] = []; + private queueIntervalId?: ReturnType; + // Prepared statements for cache lookups private stmtGetUser!: import("bun:sqlite").Statement; private stmtGetEmoji!: import("bun:sqlite").Statement; @@ -204,16 +208,16 @@ class Cache { private setupPurgeSchedule() { const cronOptions = { timezone: "Etc/UTC" }; - schedule("45 * * * *", async () => { + this.cronTasks.push(schedule("45 * * * *", async () => { try { await this.purgeExpiredItems(); await this.lazyUserCleanup(); } catch (error) { console.error("Error during purge schedule:", error); } - }, cronOptions); + }, cronOptions)); - schedule("0 * * * *", async () => { + this.cronTasks.push(schedule("0 * * * *", async () => { try { console.log("Scheduled emoji update starting..."); if (this.onEmojiExpired) { @@ -223,9 +227,9 @@ class Cache { } catch (error) { console.error("Error during emoji update schedule:", error); } - }, cronOptions); + }, cronOptions)); - schedule("0 8 * * *", () => { + this.cronTasks.push(schedule("0 8 * * *", () => { try { console.log("Running scheduled VACUUM..."); this.db.run("VACUUM"); @@ -233,7 +237,7 @@ class Cache { } catch (error) { console.error("Error during VACUUM:", error); } - }, cronOptions); + }, cronOptions)); } private async runMigrations() { @@ -350,7 +354,7 @@ class Cache { } private startQueueProcessor() { - setInterval(async () => { + this.queueIntervalId = setInterval(async () => { await this.processUserUpdateQueue(); }, 30 * 1000); } @@ -629,6 +633,24 @@ class Cache { async getReferers(): Promise> { return this.analytics.getReferers(); } + /** + * Closes all resources: stops cron jobs, clears intervals, closes database. + * Call this during graceful shutdown. + */ + close() { + for (const task of this.cronTasks) { + task.stop(); + } + this.cronTasks = []; + + if (this.queueIntervalId) { + clearInterval(this.queueIntervalId); + this.queueIntervalId = undefined; + } + + this.healthMonitor.endUptimeSession(); + this.db.close(); + } } export { Cache as SlackCache }; diff --git a/src/index.ts b/src/index.ts index 690276a..b308ece 100644 --- a/src/index.ts +++ b/src/index.ts @@ -157,22 +157,27 @@ const server = serve({ console.log(`🚀 Server running on http://localhost:${server.port}`); // Graceful shutdown handling +let shuttingDown = false; const shutdown = () => { + if (shuttingDown) return; + shuttingDown = true; console.log("Shutting down gracefully..."); - cache.endUptimeSession(); + server.stop(); + cache.close(); + console.log("Shutdown complete"); process.exit(0); }; process.on("SIGINT", shutdown); process.on("SIGTERM", shutdown); -// Prevent unhandled errors from crashing the process process.on("unhandledRejection", (reason) => { console.error("Unhandled promise rejection:", reason); }); process.on("uncaughtException", (error) => { console.error("Uncaught exception:", error); + process.exit(1); }); export { cache, slackApp }; -- 2.51.2