diff --git a/.changeset/cuddly-otters-repeat.md b/.changeset/cuddly-otters-repeat.md new file mode 100644 index 000000000000..83c94f362189 --- /dev/null +++ b/.changeset/cuddly-otters-repeat.md @@ -0,0 +1,5 @@ +--- +"@sveltejs/adapter-node": minor +--- + +feat: add shutdown event diff --git a/.changeset/gold-turkeys-pump.md b/.changeset/gold-turkeys-pump.md new file mode 100644 index 000000000000..5dc24388408d --- /dev/null +++ b/.changeset/gold-turkeys-pump.md @@ -0,0 +1,5 @@ +--- +"@sveltejs/adapter-node": patch +--- + +fix: close keep-alive connections as soon as possible during graceful shutdown rather than accepting new requests diff --git a/documentation/docs/25-build-and-deploy/40-adapter-node.md b/documentation/docs/25-build-and-deploy/40-adapter-node.md index 41be8063af1b..b08e5505faed 100644 --- a/documentation/docs/25-build-and-deploy/40-adapter-node.md +++ b/documentation/docs/25-build-and-deploy/40-adapter-node.md @@ -186,6 +186,21 @@ By default `adapter-node` gracefully shuts down the HTTP server when a `SIGTERM` > If you want to customize this behaviour you can use a [custom server](#custom-server). +You can listen to the `sveltekit:shutdown` event which is emitted after the HTTP server has closed all connections. Unlike Node's `exit` event, the `sveltekit:shutdown` event supports asynchronous operations and is always emitted when all connections are closed even if the server has dangling work such as open database connections. + +```js +process.on('sveltekit:shutdown', async (reason) => { + await jobs.stop(); + await db.close(); +}); +``` + +The parameter `reason` has one of the following values: + +- `SIGINT` - shutdown was triggered by a `SIGINT` signal +- `SIGTERM` - shutdown was triggered by a `SIGTERM` signal +- `IDLE` - shutdown was triggered by [`IDLE_TIMEOUT`](#environment-variables-idle-timeout) + ## Socket activation Most Linux operating systems today use a modern process manager called systemd to start the server and run and manage services. You can configure your server to allocate a socket and start and scale your app on demand. This is called [socket activation](http://0pointer.de/blog/projects/socket-activated-containers.html). In this case, the OS will pass two environment variables to your app — `LISTEN_PID` and `LISTEN_FDS`. The adapter will then listen on file descriptor 3 which refers to a systemd socket unit that you will have to create. @@ -242,19 +257,3 @@ app.listen(3000, () => { console.log('listening on port 3000'); }); ``` - -## Troubleshooting - -### Is there a hook for cleaning up before the app exits? - -There's nothing built-in to SvelteKit for this, because such a cleanup hook depends highly on the execution environment you're on. For Node, you can use its built-in `process.on(...)` to implement a callback that runs before the app exits: - -```js -// @errors: 2304 2580 -function shutdownGracefully() { - // anything you need to clean up manually goes in here - db.shutdown(); -} - -process.on('exit', shutdownGracefully); -``` diff --git a/packages/adapter-node/src/index.js b/packages/adapter-node/src/index.js index 929610362166..423bd634f296 100644 --- a/packages/adapter-node/src/index.js +++ b/packages/adapter-node/src/index.js @@ -42,19 +42,28 @@ if (socket_activation) { }); } -function shutdown() { +/** @param {'SIGINT' | 'SIGTERM' | 'IDLE'} reason */ +function graceful_shutdown(reason) { if (shutdown_timeout_id) return; + // If a connection was opened with a keep-alive header close() will wait for the connection to + // time out rather than close it even if it is not handling any requests, so call this first // @ts-expect-error this was added in 18.2.0 but is not reflected in the types server.server.closeIdleConnections(); - server.server.close(() => { + server.server.close((error) => { + // occurs if the server is already closed + if (error) return; + if (shutdown_timeout_id) { shutdown_timeout_id = clearTimeout(shutdown_timeout_id); } if (idle_timeout_id) { idle_timeout_id = clearTimeout(idle_timeout_id); } + + // @ts-expect-error custom events cannot be typed + process.emit('sveltekit:shutdown', reason); }); shutdown_timeout_id = setTimeout( @@ -77,19 +86,19 @@ server.server.on( req.on('close', () => { requests--; - if (requests === 0 && shutdown_timeout_id) { - // when all requests are done, close the connections, so the app shuts down without delay + if (shutdown_timeout_id) { + // close connections as soon as they become idle, so they don't accept new requests // @ts-expect-error this was added in 18.2.0 but is not reflected in the types server.server.closeIdleConnections(); } if (requests === 0 && socket_activation && idle_timeout) { - idle_timeout_id = setTimeout(shutdown, idle_timeout * 1000); + idle_timeout_id = setTimeout(() => graceful_shutdown('IDLE'), idle_timeout * 1000); } }); } ); -process.on('SIGTERM', shutdown); -process.on('SIGINT', shutdown); +process.on('SIGTERM', graceful_shutdown); +process.on('SIGINT', graceful_shutdown); export { server };