From 1dcde908022187b24de7794b0b87069ec4c01cbe Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 14 Aug 2026 13:10:52 +0000 Subject: [PATCH 1/3] fix: shut down gracefully within Docker's stop grace period On SIGTERM the close-with-grace callback could hang past the default 10s delay - the same as Docker's default stop grace period - so the container was routinely SIGKILLed after ~10s instead of exiting cleanly. Two causes: - app and adminApp each had an onClose hook closing the other, so app.close() could re-enter adminApp.close() (and vice versa) while it was already in progress and never settle. closeWithGrace already closes both apps, so drop the hooks. - Each close step rethrew on failure, aborting the remaining cleanup. Log and continue instead so one failing step can't block the rest. Also lower the close-with-grace delay to 1.5s so a close that still hangs (e.g. on a stuck in-flight request) force-exits well before the container runtime's SIGKILL. Verified locally: clean SIGTERM exits in ~20ms with code 0; with a held in-flight request the process force-exits at 1.5s instead of hanging 10s. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01Gj9ospbnecaS2ccYt2MayL --- src/server/server.ts | 51 ++++++++------------------------------------ 1 file changed, 9 insertions(+), 42 deletions(-) diff --git a/src/server/server.ts b/src/server/server.ts index 6ebb2d06..b025b49f 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -160,52 +160,19 @@ if (EXPORT_DOCS) { } else if (GENERATE_TYPES) { console.log(await getTypeOutput()) } else { - const closeListeners = closeWithGrace(async ({ err, signal, manual }) => { + // The delay must stay well under container runtimes' stop grace period + // (10s by default in Docker), or the process gets SIGKILLed before it can + // exit cleanly. + closeWithGrace({ delay: 1500 }, async ({ err, signal, manual }) => { if (err) { app.log.error({ err }, 'server closing with error') } else { - app.log.error( - { err: new Error('Signal Received') }, - `${signal} signal received, server closing, close manual received: ${manual}` - ) - } - try { - await app.close() - } catch (err) { - app.log.error({ err }, `Failed to close app`) - throw err - } - try { - await adminApp.close() - } catch (err) { - app.log.error({ err }, `Failed to close adminApp`) - throw err - } - try { - // worker threads keep the event loop alive, so the process would not exit - await destroyFormatPool() - } catch (err) { - app.log.error({ err }, `Failed to destroy format pool`) - throw err - } - }) - app.addHook('onClose', async () => { - try { - closeListeners.uninstall() - await adminApp.close() - } catch (err) { - app.log.error({ err }, `Failed to close adminApp in app onClose hook`) - throw err - } - }) - adminApp.addHook('onClose', async () => { - try { - closeListeners.uninstall() - await app.close() - } catch (err) { - app.log.error({ err }, `Failed to close app in adminApp onClose hook`) - throw err + app.log.info(`${signal} signal received, server closing, close manual received: ${manual}`) } + await app.close().catch((err) => app.log.error({ err }, 'Failed to close app')) + await adminApp.close().catch((err) => app.log.error({ err }, 'Failed to close adminApp')) + // worker threads keep the event loop alive, so the process would not exit + await destroyFormatPool().catch((err) => app.log.error({ err }, 'Failed to destroy format pool')) }) app.listen({ port: PG_META_PORT, host: PG_META_HOST }, (err) => { From d89a274924e17a25e1a405fdf475e610e0f433be Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 14 Aug 2026 13:12:57 +0000 Subject: [PATCH 2/3] feat: make shutdown grace period configurable via env Replace the hardcoded 1.5s close-with-grace delay with PG_META_SHUTDOWN_GRACE_PERIOD_SECS, defaulting to 10s (close-with-grace's own default, so production behavior is unchanged). Local dev and deployments with a longer container stop grace period can lower it to get fast, clean shutdowns. Verified locally: with a held in-flight request, SIGTERM force-exits at ~2s with PG_META_SHUTDOWN_GRACE_PERIOD_SECS=2 and at ~10s when unset. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01Gj9ospbnecaS2ccYt2MayL --- CLAUDE.md | 1 + src/server/constants.ts | 7 +++++++ src/server/server.ts | 6 ++---- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 3878722d..85d2c42c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -151,6 +151,7 @@ PG_CONN_TIMEOUT_SECS=15 # Connection timeout (default: 15) PG_QUERY_TIMEOUT_SECS=55 # Query timeout (default: 55) PG_META_MAX_RESULT_SIZE_MB=20 # Max query result size in MB (default: 2048MB) PG_META_MAX_BODY_LIMIT_MB=3 # Max request body size in MB (default: 3MB) +PG_META_SHUTDOWN_GRACE_PERIOD_SECS=10 # Shutdown wait on in-flight work before force-exit (default: 10) ``` Type generation formatting (opt-in): diff --git a/src/server/constants.ts b/src/server/constants.ts index 69f5118a..a87b9593 100644 --- a/src/server/constants.ts +++ b/src/server/constants.ts @@ -104,3 +104,10 @@ export const FORMAT_TIMEOUT_MS = Number(process.env.PG_META_FORMAT_TIMEOUT_SECS // (and tests can finish) without an explicit teardown. export const FORMAT_IDLE_TIMEOUT_MS = Number(process.env.PG_META_FORMAT_IDLE_TIMEOUT_SECS || 30) * 1000 + +// How long a shutdown may wait on in-flight work before the process +// force-exits. Note the container runtime SIGKILLs at its own stop grace +// period (10s by default in Docker, same as this default), so set this lower +// than that for the force-exit to actually run. +export const SHUTDOWN_GRACE_PERIOD_MS = + Number(process.env.PG_META_SHUTDOWN_GRACE_PERIOD_SECS || 10) * 1000 diff --git a/src/server/server.ts b/src/server/server.ts index b025b49f..abd6084f 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -14,6 +14,7 @@ import { PG_META_HOST, PG_META_PORT, POSTGREST_VERSION, + SHUTDOWN_GRACE_PERIOD_MS, } from './constants.js' import { destroyFormatPool } from './format-pool.js' import { apply as applyTypescriptTemplate } from './templates/typescript.js' @@ -160,10 +161,7 @@ if (EXPORT_DOCS) { } else if (GENERATE_TYPES) { console.log(await getTypeOutput()) } else { - // The delay must stay well under container runtimes' stop grace period - // (10s by default in Docker), or the process gets SIGKILLed before it can - // exit cleanly. - closeWithGrace({ delay: 1500 }, async ({ err, signal, manual }) => { + closeWithGrace({ delay: SHUTDOWN_GRACE_PERIOD_MS }, async ({ err, signal, manual }) => { if (err) { app.log.error({ err }, 'server closing with error') } else { From ed09961448b2c30498c9b6d98de4e3e88252fd84 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 14 Aug 2026 13:19:03 +0000 Subject: [PATCH 3/3] chore: fix prettier formatting Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01Gj9ospbnecaS2ccYt2MayL --- src/server/server.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/server/server.ts b/src/server/server.ts index abd6084f..785e293d 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -170,7 +170,9 @@ if (EXPORT_DOCS) { await app.close().catch((err) => app.log.error({ err }, 'Failed to close app')) await adminApp.close().catch((err) => app.log.error({ err }, 'Failed to close adminApp')) // worker threads keep the event loop alive, so the process would not exit - await destroyFormatPool().catch((err) => app.log.error({ err }, 'Failed to destroy format pool')) + await destroyFormatPool().catch((err) => + app.log.error({ err }, 'Failed to destroy format pool') + ) }) app.listen({ port: PG_META_PORT, host: PG_META_HOST }, (err) => {