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 6ebb2d06..785e293d 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,52 +161,18 @@ if (EXPORT_DOCS) { } else if (GENERATE_TYPES) { console.log(await getTypeOutput()) } else { - const closeListeners = closeWithGrace(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 { - 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) => {