Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: graceful shutdown and systemd socket activation in adapter-node #11653

Merged
merged 51 commits into from
Mar 8, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
e6999dc
adapter-node: add systemd socket activation
karimfromjordan Jan 17, 2024
5694536
fix quotes
karimfromjordan Jan 17, 2024
8821470
cleanup and handle graceful shutdown
karimfromjordan Jan 18, 2024
13f4875
Update packages/adapter-node/src/index.js
Rich-Harris Jan 19, 2024
b941f87
changeset
Rich-Harris Jan 19, 2024
c7574f3
fix variables
karimfromjordan Jan 19, 2024
62daceb
add `SIGINT` handler
karimfromjordan Jan 19, 2024
bf5d4f8
improve docs
karimfromjordan Jan 19, 2024
a4b3a16
more improvements to the docs
karimfromjordan Jan 19, 2024
68d8e4f
`-1` is not truthy!
karimfromjordan Jan 19, 2024
08d253c
cleanup
karimfromjordan Jan 19, 2024
5140d10
rename `TIMEOUT` to `IDLE_TIMEOUT`
karimfromjordan Jan 19, 2024
be06b58
rename `LISTEN_FDS` to `SD_LISTEN_FDS_START`
karimfromjordan Jan 19, 2024
546fccb
finalize `shutdown` function
karimfromjordan Jan 21, 2024
5d53900
format
karimfromjordan Jan 22, 2024
c396050
format
karimfromjordan Jan 22, 2024
af74d37
update docs
karimfromjordan Jan 22, 2024
03177fb
fix spelling
karimfromjordan Jan 22, 2024
a6d31a0
improve wording
karimfromjordan Jan 22, 2024
2088465
fix spelling
karimfromjordan Jan 22, 2024
ebcbc5d
improve wording
karimfromjordan Jan 22, 2024
aafb019
format
karimfromjordan Jan 22, 2024
2a21e62
add code comment
karimfromjordan Jan 22, 2024
6230fc0
fix typo
karimfromjordan Jan 22, 2024
a22310f
improve wording again
karimfromjordan Jan 22, 2024
16cd536
improve wording again
karimfromjordan Jan 22, 2024
5718705
Merge branch 'master' into systemd-socket-activation
karimfromjordan Jan 22, 2024
bcd183c
update changeset
karimfromjordan Jan 22, 2024
37ffcfa
call `process.exit()`
karimfromjordan Jan 22, 2024
b3b6faf
just use arrow functions
karimfromjordan Jan 22, 2024
ecd6e06
remove `process.exit()`
karimfromjordan Jan 24, 2024
8f65872
Update documentation/docs/25-build-and-deploy/40-adapter-node.md
karimfromjordan Jan 24, 2024
eedf863
Update documentation/docs/25-build-and-deploy/40-adapter-node.md
karimfromjordan Jan 24, 2024
02c0ce9
Update documentation/docs/25-build-and-deploy/40-adapter-node.md
karimfromjordan Jan 25, 2024
a521c0d
condense docs
karimfromjordan Jan 25, 2024
ed3cdcf
update changeset
karimfromjordan Jan 25, 2024
fda2354
fix typo
karimfromjordan Jan 25, 2024
ecd7d63
update changeset
karimfromjordan Jan 25, 2024
9aa8fdd
Update documentation/docs/25-build-and-deploy/40-adapter-node.md
karimfromjordan Jan 25, 2024
872b99e
throw error when `LISTEN_PID` or `LISTEN_FDS` cannot be validated
karimfromjordan Jan 25, 2024
0d88612
Update documentation/docs/25-build-and-deploy/40-adapter-node.md
karimfromjordan Jan 25, 2024
382189e
fix typo
karimfromjordan Jan 25, 2024
4ad6a7c
optimize shutdown
karimfromjordan Jan 25, 2024
d8b0c53
update code comment
karimfromjordan Jan 26, 2024
58c5bb1
make if conditions clearer
karimfromjordan Jan 26, 2024
d3a1d77
Update .changeset/stale-donkeys-mix.md
Rich-Harris Jan 30, 2024
9b8dfde
ignore `envPrefix` for `LISTEN_PID` and `LISTEN_FDS`
karimfromjordan Mar 3, 2024
4515415
Update documentation/docs/25-build-and-deploy/40-adapter-node.md
benmccann Mar 8, 2024
c6b72e2
Update documentation/docs/25-build-and-deploy/40-adapter-node.md
benmccann Mar 8, 2024
80e7566
Update documentation/docs/25-build-and-deploy/40-adapter-node.md
benmccann Mar 8, 2024
d8c8a90
Update documentation/docs/25-build-and-deploy/40-adapter-node.md
benmccann Mar 8, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions documentation/docs/25-build-and-deploy/40-adapter-node.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,44 @@ MY_CUSTOM_ORIGIN=https://my.site \
node build
```

## 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 verify that these variables are correct and then listen on file descriptor 3 which refers to a socket unit that you will have to create.

To take advantage of socket activation make sure your app is running as a systemd service. It can either run directly on the host system or inside a container (using Docker or a systemd portable service for example).

```ini
# /etc/systemd/system/myapp.service

[Service]
Environment=NODE_ENV=production
ExecStart=/usr/bin/node /usr/bin/myapp/build
```

Then create an accompanying [socket unit](https://www.freedesktop.org/software/systemd/man/latest/systemd.socket.html). The adapter only accepts a single socket.

```ini
# /etc/systemd/system/myapp.socket

[Socket]
ListenStream=3000

[Install]
WantedBy=sockets.target
```

Make sure systemd has recognised both units by running `sudo systemctl daemon-reload`. Then enable the socket on boot and start it immediately using `sudo systemctl enable --now myapp.socket`.

The app will then automatically start once the first request is made to `localhost:3000`. Additionally, if you pass a `TIMEOUT=x` environment variable to your app the adapter will terminate it after receiving no requests for `x` seconds.

```ini
[Service]
Environment=NODE_ENV=production TIMEOUT=60
ExecStart=/usr/bin/node /usr/bin/myapp/build
```

If `myapp.socket` later again receives requests it will automatically trigger your `myapp.service` again and hand over the requests to your SvelteKit app.

## Custom server

The adapter creates two files in your build directory — `index.js` and `handler.js`. Running `index.js` — e.g. `node build`, if you use the default build directory — will start a server on the configured port.
Expand Down
5 changes: 4 additions & 1 deletion packages/adapter-node/src/env.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ const expected = new Set([
'PROTOCOL_HEADER',
'HOST_HEADER',
'PORT_HEADER',
'BODY_SIZE_LIMIT'
'BODY_SIZE_LIMIT',
'LISTEN_PID',
'LISTEN_FDS',
'TIMEOUT'
]);

if (ENV_PREFIX) {
Expand Down
51 changes: 48 additions & 3 deletions packages/adapter-node/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,55 @@ export const path = env('SOCKET_PATH', false);
export const host = env('HOST', '0.0.0.0');
export const port = env('PORT', !path && '3000');

const listen_pid = parseInt(env('LISTEN_PID', '-1'));
const listen_fds = parseInt(env('LISTEN_FDS', '-1'));
const timeout = parseInt(env('TIMEOUT', '-1'));
const systemd_socket_fd = 3;

const server = polka().use(handler);

server.listen({ path, host, port }, () => {
console.log(`Listening on ${path ? path : host + ':' + port}`);
});
function close() {
server.server.closeIdleConnections();
Rich-Harris marked this conversation as resolved.
Show resolved Hide resolved
server.server.close();
}

if (listen_pid === process.pid && listen_fds === 1) {
server.listen({ fd: systemd_socket_fd }, () => {
console.log('Listening on file descriptor 3');
Rich-Harris marked this conversation as resolved.
Show resolved Hide resolved
});

if (timeout) {
/** @type {NodeJS.Timeout | void} */
let timeout_id;
let requests = 0;

/** @param {import('node:http').IncomingMessage} req */
function on_request(req) {
requests++;

if (timeout_id) {
timeout_id = clearTimeout(timeout_id);
}

req.on('close', on_request_close);
}

function on_request_close() {
requests--;

if (requests === 0) {
timeout_id = setTimeout(close, timeout * 1000);
}
}

server.server.on('request', on_request);
}
} else {
server.listen({ path, host, port }, () => {
console.log(`Listening on ${path ? path : host + ':' + port}`);
});
}

process.on('SIGTERM', close);
karimfromjordan marked this conversation as resolved.
Show resolved Hide resolved

export { server };
Loading