/ opencodehub / src / lib / graceful-shutdown.ts
Swadhin Biswas Feature/ai code reviewer (#80) · 8/17/2026
9740d3d
86 lines · 2.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
/**
 * Graceful shutdown handler for the main OpenCodeHub process.
 * Import this at the top of the entry point or via NODE_OPTIONS --require.
 *
 * Handles SIGTERM, SIGINT, and uncaught errors to ensure:
 * - In-flight HTTP requests complete
 * - Database connections drain
 * - Redis connections close
 * - Worker queues stop accepting new jobs
 */

import { closeDatabase } from "@/db";
import { logger } from "@/lib/logger";

let isShuttingDown = false;
const SHUTDOWN_TIMEOUT_MS = 30_000;

async function gracefulShutdown(signal: string) {
  if (isShuttingDown) return;
  isShuttingDown = true;

  logger.info({ signal }, "Received shutdown signal, draining...");

  // Set flag so health checks return "shutting down"
  process.env.OPCODEHUB_SHUTTING_DOWN = "1";

  // Force exit after timeout
  const forceTimer = setTimeout(() => {
    logger.error("Forced shutdown after timeout");
    process.exit(1);
  }, SHUTDOWN_TIMEOUT_MS);
  forceTimer.unref();

  try {
    // Close database connections
    await closeDatabase();
    logger.info("Database connections closed");
  } catch (err) {
    logger.error({ err }, "Error closing database connections");
  }

  try {
    // Close Redis if available — import dynamically to avoid circular deps
    const { closeRedis } = await import("@/lib/redis");
    await closeRedis();
  } catch {
    // Redis not available, skip
  }

  logger.info("Shutdown complete, exiting...");

  // Let the process exit naturally once all handles are drained
  setTimeout(() => process.exit(0), 1000);
}

// Handle signals
process.on("SIGTERM", () => gracefulShutdown("SIGTERM"));
process.on("SIGINT", () => gracefulShutdown("SIGINT"));

// Handle uncaught errors — log and exit to let process manager restart
process.on("uncaughtException", (err) => {
  logger.fatal({ err }, "Uncaught exception — exiting to prevent corrupted state");
  process.exit(1);
});

process.on("unhandledRejection", (reason) => {
  logger.fatal({ reason }, "Unhandled rejection — exiting to let process manager restart");
  process.exit(1);
});

// Memory pressure warning
if (process.env.NODE_ENV === "production") {
  const memCheck = setInterval(() => {
    const usage = process.memoryUsage();
    const heapUsedMB = Math.round(usage.heapUsed / 1024 / 1024);
    const rssUsedMB = Math.round(usage.rss / 1024 / 1024);

    if (heapUsedMB > 1024) {
      logger.warn({ heapUsedMB, rssUsedMB }, "High memory usage detected");
    }
  }, 60_000);
  memCheck.unref();
}

export { isShuttingDown };