Skip to content

fix(dev): keep serving during a hard restart using SO_REUSEPORT - #1404

Merged
danielroe merged 7 commits into
mainfrom
feat/reuse-port
Jul 27, 2026
Merged

fix(dev): keep serving during a hard restart using SO_REUSEPORT#1404
danielroe merged 7 commits into
mainfrom
feat/reuse-port

Conversation

@danielroe

Copy link
Copy Markdown
Member

🔗 Linked issue

📚 Description

rather than kill a fork, then start again and hope that the port is the same, this keeps the port, and hands it across to the new fork

@pkg-pr-new

pkg-pr-new Bot commented Jul 27, 2026

Copy link
Copy Markdown
  • nuxt-cli-playground

    npm i https://pkg.pr.new/create-nuxt@1404
    
    npm i https://pkg.pr.new/nuxi@1404
    
    npm i https://pkg.pr.new/@nuxt/cli@1404
    

commit: e8d6ea1

@codspeed-hq

codspeed-hq Bot commented Jul 27, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 2 untouched benchmarks


Comparing feat/reuse-port (e8d6ea1) with main (1730419)

Open in CodSpeed

@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@danielroe, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 9 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 6b626802-9da4-481b-ae65-9790914ecc24

📥 Commits

Reviewing files that changed from the base of the PR and between 668796d and e8d6ea1.

📒 Files selected for processing (4)
  • packages/nuxt-cli/src/dev/index.ts
  • packages/nuxt-cli/src/dev/pool.ts
  • packages/nuxt-cli/test/unit/pool.spec.ts
  • packages/nuxt-cli/test/unit/restart-hook.spec.ts
📝 Walkthrough

Walkthrough

The dev command now supports reuse-port handovers during forked restarts, waits for replacement forks to serve, and defers cascading restart handling until readiness. Listener binding adds handover and reuse-port probing. ForkPool exposes serving-aware fork controls and per-fork listen overrides. Dev context and lock acquisition carry the outgoing process PID for takeover. Unit tests cover listener, fork lifecycle, restart hooks, and lock behavior.

Estimated code review effort: 4 (Complex) | ~45 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: preserving the serving process during hard restarts with SO_REUSEPORT.
Description check ✅ Passed The description is clearly about handing the listening port from one dev fork to another during restart.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/reuse-port

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/nuxt-cli/src/dev/pool.ts (1)

208-215: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Outgoing fork stays serving: true, so its shutdown exit code can kill the whole session.

promote() sets serving but nothing ever clears it. After a handover, dev.ts calls closePrevious() on the previously promoted fork; if that fork exits with a non-zero code while shutting down (rather than being reaped cleanly by the signal), this handler calls process.exit(errorCode) and takes down the dev session even though the new fork is now serving. Clear the flag when the fork is deliberately killed.

🐛 Clear `serving` on deliberate shutdown
   private killFork(fork: PooledFork, signal: NodeJS.Signals | number = 'SIGTERM'): Promise<void> {
     const wasAlive = fork.state !== 'dead' && !!fork.process && fork.process.exitCode === null
     fork.state = 'dead'
+    // A fork we are shutting down on purpose must not end the session, even if
+    // it exits non-zero on the way out.
+    fork.serving = false
     if (fork.process) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/nuxt-cli/src/dev/pool.ts` around lines 208 - 215, Update the
deliberate shutdown path used by closePrevious() to clear the promoted fork’s
serving flag before killing it. Ensure pooledFork.serving is false before the
close handler evaluates it, while preserving the existing process.exit behavior
for unexpected crashes of active serving forks.
🧹 Nitpick comments (1)
packages/nuxt-cli/src/dev/listen.ts (1)

336-359: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Probe servers are closed but not awaited, and a failed probe can leave the first socket bound briefly.

server.close() is asynchronous; the cached result resolves before the ephemeral ports are actually released. Harmless in practice (ephemeral ports, process-lifetime cache), but awaiting the closes makes the probe self-contained and avoids surprising a test that asserts on open handles.

♻️ Await the probe teardown
     finally {
-      for (const server of [first, second]) {
-        if (server.listening) {
-          server.close()
-        }
-      }
+      await Promise.all([first, second].map(server => new Promise<void>((resolve) => {
+        if (!server.listening) {
+          resolve()
+          return
+        }
+        server.close(() => resolve())
+      })))
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/nuxt-cli/src/dev/listen.ts` around lines 336 - 359, Update the
finally block in isReusePortSupported to await completion of server.close() for
each listening probe server before the cached promise resolves, including the
failed-bind path. Preserve cleanup for both first and second servers and keep
non-listening servers skipped.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/nuxt-cli/src/commands/dev.ts`:
- Around line 251-261: Update the failed-handover handling around the
`onRestart(restart)` call so it re-arms the existing restart subscription
instead of registering new process-level and `devServer` listeners. Adjust
`initialize` and its returned restart-handling API as needed to remove or reuse
prior handlers while preserving restart behavior after a failed handover.
- Around line 204-216: Serialize restartWithFork invocations with a single
in-flight promise so concurrent fork restart/rejection messages and
shortcut-triggered restarts cannot overlap. Update restartWithFork and its
callers to reuse or await the active restart operation, while preserving the
existing handover, closeCurrent, and currentPid sequencing for each restart.
- Around line 226-249: Guard the pool.getFork() acquisition inside
restartWithFork with the same recovery handling used when fork.serving fails.
Catch readiness or IPC-send rejections, retain the current server when available
(especially in non-handover mode), and return without allowing errors from the
unawaited restartWithFork or onRestart paths to become unhandled rejections.

---

Outside diff comments:
In `@packages/nuxt-cli/src/dev/pool.ts`:
- Around line 208-215: Update the deliberate shutdown path used by
closePrevious() to clear the promoted fork’s serving flag before killing it.
Ensure pooledFork.serving is false before the close handler evaluates it, while
preserving the existing process.exit behavior for unexpected crashes of active
serving forks.

---

Nitpick comments:
In `@packages/nuxt-cli/src/dev/listen.ts`:
- Around line 336-359: Update the finally block in isReusePortSupported to await
completion of server.close() for each listening probe server before the cached
promise resolves, including the failed-bind path. Preserve cleanup for both
first and second servers and keep non-listening servers skipped.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: c22c6610-4d3b-4908-b2c7-7d5271307ee6

📥 Commits

Reviewing files that changed from the base of the PR and between 1730419 and 5201155.

📒 Files selected for processing (9)
  • packages/nuxt-cli/src/commands/dev.ts
  • packages/nuxt-cli/src/dev/index.ts
  • packages/nuxt-cli/src/dev/listen.ts
  • packages/nuxt-cli/src/dev/pool.ts
  • packages/nuxt-cli/src/dev/utils.ts
  • packages/nuxt-cli/src/utils/lockfile.ts
  • packages/nuxt-cli/test/unit/listen.spec.ts
  • packages/nuxt-cli/test/unit/lockfile.spec.ts
  • packages/nuxt-cli/test/unit/pool.spec.ts

Comment thread packages/nuxt-cli/src/commands/dev.ts Outdated
Comment thread packages/nuxt-cli/src/commands/dev.ts Outdated
Comment thread packages/nuxt-cli/src/commands/dev.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/nuxt-cli/src/dev/pool.ts (1)

206-219: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Reject ready when a fork exits before readiness.

getFork() awaits fork.ready at Line 95, but this promise is only rejected on error. A child can emit close without error, leaving restart callers and warming logic blocked forever while the fork is removed from the pool. Reject ready from the close handler before removing the fork.

Proposed fix
    childProc.on('close', (errorCode) => {
+     readyReject(new Error('Dev server fork exited before sending fork-ready.'))
      if (pooledFork.serving && errorCode) {
        // Active fork crashed
        process.exit(errorCode)
      }
      this.removeFork(pooledFork)
    })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/nuxt-cli/src/dev/pool.ts` around lines 206 - 219, Update the
childProc close handler near the existing error handler to reject
pooledFork.ready when the fork closes before becoming ready, including clean
closes without an error event. Perform this rejection before
this.removeFork(pooledFork), while preserving the existing serving-fork exit
behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/nuxt-cli/src/dev/index.ts`:
- Around line 191-235: Update restart() in createRestartHook to remove the
pending source 'restart' listener with source.off('restart', restart) when
handling any restart, including error-triggered restarts. Extend coverage as
needed to verify repeated error-triggered re-arming does not accumulate source
listeners.

---

Outside diff comments:
In `@packages/nuxt-cli/src/dev/pool.ts`:
- Around line 206-219: Update the childProc close handler near the existing
error handler to reject pooledFork.ready when the fork closes before becoming
ready, including clean closes without an error event. Perform this rejection
before this.removeFork(pooledFork), while preserving the existing serving-fork
exit behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 07364a55-c6e3-4255-9b9d-9b278bb6665a

📥 Commits

Reviewing files that changed from the base of the PR and between 5201155 and 668796d.

📒 Files selected for processing (5)
  • packages/nuxt-cli/src/commands/dev.ts
  • packages/nuxt-cli/src/dev/index.ts
  • packages/nuxt-cli/src/dev/pool.ts
  • packages/nuxt-cli/test/unit/pool.spec.ts
  • packages/nuxt-cli/test/unit/restart-hook.spec.ts
💤 Files with no reviewable changes (1)
  • packages/nuxt-cli/test/unit/pool.spec.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/nuxt-cli/src/commands/dev.ts

Comment thread packages/nuxt-cli/src/dev/index.ts
@danielroe
danielroe enabled auto-merge July 27, 2026 16:02
@danielroe
danielroe disabled auto-merge July 27, 2026 16:02
@danielroe
danielroe merged commit 169f917 into main Jul 27, 2026
19 checks passed
@danielroe
danielroe deleted the feat/reuse-port branch July 27, 2026 16:02
@github-actions github-actions Bot mentioned this pull request Jul 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant