Files
Janka UrygaandGitHub 4055dd0c7c feat(after): wait for after-callbacks before server shutdown (#72590)
### What

This PR improves the reliability of `unstable_after` in a self-hosted
context, i.e. on a long-running server (`next start`).
when the server receives a signal to gracefully shutdown, we'll wait for
any operations scheduled with `unstable_after` to finish, and then exit.

### Why

It's less surprising than dropping pending operations on the floor if an
instance is getting shut down. This mirrors the standard behavior of
finishing handling pending requests before exiting -- we're just
extending this a bit to include `after` tasks.

### How

Previously `waitUntil` was just a noop in `next start`, so passing a
promise to it did nothing. This PR changes the implementation to instead
use an "awaiter" that stores promises passed to it and lets us wait for
all of them to complete. We then use some existing `onCleanup` machinery
to make sure that they're actually awaited before shutdown.

Note that I've now made `NextServer.close()` execute before shutdown.
This is notable because AFAICT this wasn't being called anywhere before,
which is a bit suspicious.

### How (in a bit more detail)

This is all a bit hard to follow so here's an overview of how it works:

- We have two entrypoints: `startServer` and `NextCustomServer`
- `startServer`:
  - creates a cleanup set (`AsyncCallbackSet`)
  - calls `createRequestHandlers`, which...
- passes `onDevServerCleanup` to turbopack's hot reloader (previously
`onCleanup`)
      - creates a `NextServer`
        - which will create a `NextNodeServer`
- which sets up an `Awaiter` to ensure all `after` callbacks finished
running.
  - adds a call to `NextServer.close()` to the cleanup set
- when we receive SIGINT/SIGTERM, we run `NextServer.close()`, which
runs `NextNodeServer.close()`, which waits for pending `after` callbacks
to finish using the Awaiter.
  
 - `NextCustomServer`:
- does basically the same setup with a cleanup set and
`createRequestHandlers`
- but it does not set up a SIGINT/SIGTERM handler to run the cleanups.
instead, it expects the user to call its `cleanup()` method when
appropriate (basically like we do in `startServer`)
- i'm gonna do a follow up and update the custom server docs to reflect
this
- also going to try to warn if a cleanup was needed but the process
exited without it calling it
2024-11-27 19:08:01 +00:00

113 lines
2.5 KiB
JavaScript

import next from 'next'
import * as http from 'node:http'
const debug = (...args) => {
if (!process.env.DEBUG) return
console.log('debug ::', ...args)
}
async function main() {
debug('starting custom server...')
const currentPort = parseInt(process.env.PORT, 10) || 3000
const isDev = process.env.NODE_ENV !== 'production'
const nextServer = next({
hostname: 'localhost',
port: currentPort,
dev: isDev,
quiet: false,
})
debug('nextServer.prepare()')
await nextServer.prepare()
const nextRequestHandler = nextServer.getRequestHandler()
debug('http.createServer()')
const httpServer = http.createServer(async (req, res) => {
try {
await nextRequestHandler(req, res)
} catch (err) {
console.error(err)
res.statusCode = 500
res.end('Internal Server Error')
}
})
const cleanup = once(async () => {
// in dev, there might be a devserver websocket connection
// that prevents `httpServer.close` from completing, so skip it
if (!isDev) {
debug('closing HTTP server...')
await new Promise((resolve) =>
httpServer.close((err) => {
if (err) {
console.error(err)
}
resolve()
})
)
}
debug('closing Next server...')
await nextServer.close()
debug('cleanup finished')
})
/** @type {NodeJS.SignalsListener} */
const onShutdownSignal = async (signal) => {
debug('onShutdownSignal', signal)
try {
await cleanup()
} finally {
process.exit(0)
}
}
process.on('SIGTERM', onShutdownSignal)
process.on('SIGINT', onShutdownSignal)
try {
await new Promise((resolve, reject) => {
httpServer.on('error', reject)
debug('httpServer.listen()')
httpServer.listen(currentPort, (err) => {
if (err) {
reject(err)
} else {
resolve()
}
httpServer.off('error', reject)
})
})
console.log(
[
`Custom server started`,
// next test modes parse this to find the appUrl
`- Local: http://${nextServer.hostname}:${nextServer.port}`,
].join('\n')
)
} catch (err) {
console.error('Failed to start server:', err)
process.exit(1)
}
}
/**
* @template T
* @param {() => T} cb
* @returns {() => T} cb
* */
function once(cb) {
let state = { didRun: false, value: undefined }
return () => {
if (!state.didRun) {
state = { didRun: true, value: cb() }
}
return state.value
}
}
main()