Skip to content

Commit

Permalink
fix(dev-server): Terminate turbopack HMR websocket connections during…
Browse files Browse the repository at this point in the history
… exit (#75344)

Without this change, an open browser tab with an HMR websocket subscription (which is very common) can prevent next.js's dev server from exiting cleanly (falls back to a SIGKILL after a 100ms timeout).

That's bad (aside from the extra 100ms delay) because it prevents

```
            await Promise.all([
              nextServer?.close().catch(console.error),
              cleanupListeners?.runAll().catch(console.error),
            ])
```

from running, which can prevent us from flushing task statistics (#67164).

You can test this with:

```
NEXT_EXIT_TIMEOUT_MS=200000 node_modules/.bin/next dev
```

or

```
NEXT_EXIT_TIMEOUT_MS=200000 node_modules/.bin/next dev --turbo
```

Prior to this PR, after hitting ctrl+c, the processes will hang if there's a connected browser window open.

After this PR, the processes exit instantly.
  • Loading branch information
bgw authored Jan 30, 2025
1 parent 28ac628 commit 0f7251f
Show file tree
Hide file tree
Showing 8 changed files with 44 additions and 15 deletions.
19 changes: 7 additions & 12 deletions packages/next/src/server/dev/hot-middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,16 +72,11 @@ class EventStream {
this.clients = new Set()
}

everyClient(fn: (client: ws) => void) {
for (const client of this.clients) {
fn(client)
}
}

close() {
this.everyClient((client) => {
client.close()
})
for (const wsClient of this.clients) {
// it's okay to not cleanly close these websocket connections, this is dev
wsClient.terminate()
}
this.clients.clear()
}

Expand All @@ -93,9 +88,9 @@ class EventStream {
}

publish(payload: any) {
this.everyClient((client) => {
client.send(JSON.stringify(payload))
})
for (const wsClient of this.clients) {
wsClient.send(JSON.stringify(payload))
}
}
}

Expand Down
7 changes: 7 additions & 0 deletions packages/next/src/server/dev/hot-reloader-turbopack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1059,6 +1059,13 @@ export async function createHotReloaderTurbopack(
}
})
},
close() {
for (const wsClient of clients) {
// it's okay to not cleanly close these websocket connections, this is dev
wsClient.terminate()
}
clients.clear()
},
}

handleEntrypointsSubscription().catch((err) => {
Expand Down
1 change: 1 addition & 0 deletions packages/next/src/server/dev/hot-reloader-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,4 +182,5 @@ export interface NextJsHotReloaderInterface {
definition: RouteDefinition | undefined
url?: string
}): Promise<void>
close(): void
}
4 changes: 4 additions & 0 deletions packages/next/src/server/dev/hot-reloader-webpack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1595,4 +1595,8 @@ export default class HotReloaderWebpack implements NextJsHotReloaderInterface {
})
})
}

public close() {
this.webpackHotMiddleware?.close()
}
}
4 changes: 4 additions & 0 deletions packages/next/src/server/lib/dev-bundler-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,4 +103,8 @@ export class DevBundlerService {
data: this.appIsrManifest,
})
}

public close() {
this.bundler.hotReloader.close()
}
}
5 changes: 5 additions & 0 deletions packages/next/src/server/lib/render-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ export type ServerInitResult = {
requestHandler: RequestHandler
upgradeHandler: UpgradeHandler
server: NextServer
// Make an effort to close upgraded HTTP requests (e.g. Turbopack HMR websockets)
closeUpgraded: () => void
}

let initializations: Record<string, Promise<ServerInitResult> | undefined> = {}
Expand Down Expand Up @@ -109,6 +111,9 @@ async function initializeImpl(opts: {
requestHandler,
upgradeHandler,
server,
closeUpgraded() {
opts.bundlerService?.close()
},
}
}

Expand Down
9 changes: 8 additions & 1 deletion packages/next/src/server/lib/router-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -751,5 +751,12 @@ export async function initialize(opts: {
}
}

return { requestHandler, upgradeHandler, server: handlers.server }
return {
requestHandler,
upgradeHandler,
server: handlers.server,
closeUpgraded() {
developmentBundler?.hotReloader?.close()
},
}
}
10 changes: 8 additions & 2 deletions packages/next/src/server/lib/start-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,7 @@ export async function startServer(

try {
let cleanupStarted = false
let closeUpgraded: (() => void) | null = null
const cleanup = () => {
if (cleanupStarted) {
// We can get duplicate signals, e.g. when `ctrl+c` is used in an
Expand All @@ -303,12 +304,16 @@ export async function startServer(

// first, stop accepting new connections and finish pending requests,
// because they might affect `nextServer.close()` (e.g. by scheduling an `after`)
await new Promise<void>((res) =>
await new Promise<void>((res) => {
server.close((err) => {
if (err) console.error(err)
res()
})
)
if (isDev) {
server.closeAllConnections()
closeUpgraded?.()
}
})

// now that no new requests can come in, clean up the rest
await Promise.all([
Expand Down Expand Up @@ -360,6 +365,7 @@ export async function startServer(
requestHandler = initResult.requestHandler
upgradeHandler = initResult.upgradeHandler
nextServer = initResult.server
closeUpgraded = initResult.closeUpgraded

const startServerProcessDuration =
performance.mark('next-start-end') &&
Expand Down

0 comments on commit 0f7251f

Please sign in to comment.