Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📜 Recent review details⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
📝 WalkthroughWalkthroughThe PR ensures runPnpm() always awaits finishWorkers() in a finally block so worker pools are terminated on all exit paths. Adds a regression test for ChangesWorker Pool Cleanup on All Exit Paths
sequenceDiagram
participant runPnpm
participant main as main(argv)
participant errorHandler
participant finishWorkers as finishWorkers
runPnpm->>main: call main(argv)
alt main throws
main-->>runPnpm: throw
runPnpm->>errorHandler: call errorHandler(err)
else main returns
main-->>runPnpm: return
end
runPnpm->>finishWorkers: finally -> await finishWorkers()
finishWorkers-->>runPnpm: resolved
🎯 3 (Moderate) | ⏱️ ~20 minutes
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Pull request overview
This PR fixes pnpm processes hanging after completing work by scoping undici’s optimized global dispatcher and the worker pool lifecycle to the CLI entrypoint, ensuring they’re always torn down even on short-circuit returns (e.g. pnpm --version, --help, config errors).
Changes:
- Remove
@pnpm/network.fetch’s module-load side effect that installed a global undici dispatcher; replace with explicitinstallGlobalDispatcher()/closeGlobalDispatcher()exports. - Call
installGlobalDispatcher()at CLI startup and runfinishWorkers()+closeGlobalDispatcher()in the CLI entry’sfinallyblock. - Wire
@pnpm/network.fetchinto pnpm’s workspace refs/deps and add a changeset documenting the fix.
Reviewed changes
Copilot reviewed 6 out of 7 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| pnpm/tsconfig.json | Adds TS project reference so the CLI can consume @pnpm/network.fetch. |
| pnpm/src/pnpm.ts | Installs dispatcher at startup and tears down dispatcher + workers in finally. |
| pnpm/package.json | Adds @pnpm/network.fetch dependency to the pnpm package. |
| pnpm-lock.yaml | Records the new workspace dependency link for @pnpm/network.fetch. |
| network/fetch/src/index.ts | Re-exports new dispatcher lifecycle functions from the package entry. |
| network/fetch/src/dispatcher.ts | Introduces explicit install/close APIs and removes global-dispatcher side effect. |
| .changeset/dispatcher-lifecycle.md | Publishes patch release notes for pnpm + @pnpm/network.fetch. |
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 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 `@network/fetch/src/dispatcher.ts`:
- Around line 65-75: The dispose hook for DISPATCHER_CACHE must await dispatcher
shutdown instead of fire-and-forgetting it; update the hook implementation so it
is async and returns/awaits the Promise from installed.close() (which calls
Agent.close()/ProxyAgent.close()) rather than using "void installed.close()";
ensure the hook awaits close() before removing entries from DISPATCHER_CACHE so
closeGlobalDispatcher() (and its call to DISPATCHER_CACHE.clear()) does not
resolve while keep-alive sockets are still open—refer to the DISPATCHER_CACHE
dispose hook, installed.close(), closeGlobalDispatcher,
installedGlobalDispatcher, previousGlobalDispatcher and setGlobalDispatcher when
making the change.
In `@pnpm/src/pnpm.ts`:
- Around line 31-33: The finally block currently awaits finishWorkers() and
closeGlobalDispatcher() directly which can overwrite the original error from
main(); change the flow to capture the primary outcome (store any error thrown
by main() in a variable, e.g. mainError) and in finally run finishWorkers() and
closeGlobalDispatcher() inside their own try/catch so their rejections are
logged/handled but do not replace mainError; after teardown, rethrow mainError
if present, otherwise if teardown produced an error throw that one—ensure you
reference the existing main()/errorHandler() flow and the functions
finishWorkers() and closeGlobalDispatcher() when implementing this preservation
of the original outcome.
🪄 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: d2954ba1-ed57-4138-bd5a-1461de17beb1
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (6)
.changeset/dispatcher-lifecycle.mdnetwork/fetch/src/dispatcher.tsnetwork/fetch/src/index.tspnpm/package.jsonpnpm/src/pnpm.tspnpm/tsconfig.json
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: Agent
- GitHub Check: Compile & Lint
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{js,ts,tsx,jsx}
📄 CodeRabbit inference engine (AGENTS.md)
**/*.{js,ts,tsx,jsx}: Use trailing commas in code following Standard Style with modifications
Prefer functions over classes
Declare functions after they are used, relying on hoisting
Functions should have no more than two or three arguments; use a single options object for functions needing more parameters
Organize imports in order: standard libraries, external dependencies (sorted alphabetically), then relative imports
Files:
network/fetch/src/index.tspnpm/src/pnpm.tsnetwork/fetch/src/dispatcher.ts
🧠 Learnings (1)
📚 Learning: 2026-05-05T23:03:04.286Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11479
File: __utils__/scripts/package.json:6-9
Timestamp: 2026-05-05T23:03:04.286Z
Learning: The pattern cross-env NODE_OPTIONS="$NODE_OPTIONS ..." in package.json scripts is an established convention in the pnpm/pnpm repository and is used across many packages (e.g., fs/hard-link-dir, worker, __utils__/scripts). Do not flag this as a cross-platform issue in individual files; if a change is needed, apply it as a repo-wide change in a separate PR. Scope this guidance to all package.json files in the repo; use the minimatch pattern '**/package.json' to identify relevant files and review changes at the repository level rather than per-file.
Applied to files:
pnpm/package.json
Benchmark Results
Run 25644260272 · 30 runs per scenario · triggered by @zkochan |
There was a problem hiding this comment.
♻️ Duplicate comments (1)
pnpm/src/pnpm.ts (1)
30-32:⚠️ Potential issue | 🟠 Major | ⚡ Quick winCleanup failure in
finallycan still replace the main command outcome.If
finishWorkers()rejects here, it can override whatever happened inmain()/errorHandler()and change the final failure surface. This was raised earlier and still applies.Suggested minimal safeguard
async function runPnpm (): Promise<void> { const { finishWorkers } = await import('@pnpm/worker') const { errorHandler } = await import('./errorHandler.js') + let primaryError: unknown try { const { main } = await import('./main.js') await main(argv) } catch (err: any) { // eslint-disable-line + primaryError = err await errorHandler(err) } finally { - await finishWorkers() + try { + await finishWorkers() + } catch (cleanupError) { + if (primaryError == null) throw cleanupError + } } }#!/bin/bash # Verify the current flow still awaits finishWorkers() directly in finally. rg -n -C3 'catch \(err: any\)|finally \{|await finishWorkers\(\)' pnpm/src/pnpm.ts🤖 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 `@pnpm/src/pnpm.ts` around lines 30 - 32, The finally block currently does `await finishWorkers()` which can throw and overwrite the original main()/errorHandler() outcome; wrap the call in its own try/catch inside the finally so cleanup errors are caught and logged but not rethrown (e.g., in the finally of the top-level function that calls main()/errorHandler(), catch errors from finishWorkers() and call the logger/errorHandler but do not throw), referencing the existing finishWorkers(), main(), and errorHandler() symbols so you locate and replace the direct await finishWorkers() with a guarded try { await finishWorkers() } catch (err) { processLogger.error('finishWorkers failed', err) } (or equivalent) to preserve the original exit semantics.
🤖 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.
Duplicate comments:
In `@pnpm/src/pnpm.ts`:
- Around line 30-32: The finally block currently does `await finishWorkers()`
which can throw and overwrite the original main()/errorHandler() outcome; wrap
the call in its own try/catch inside the finally so cleanup errors are caught
and logged but not rethrown (e.g., in the finally of the top-level function that
calls main()/errorHandler(), catch errors from finishWorkers() and call the
logger/errorHandler but do not throw), referencing the existing finishWorkers(),
main(), and errorHandler() symbols so you locate and replace the direct await
finishWorkers() with a guarded try { await finishWorkers() } catch (err) {
processLogger.error('finishWorkers failed', err) } (or equivalent) to preserve
the original exit semantics.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 6b2f87b1-2f01-466b-a971-b379b3af2845
📒 Files selected for processing (3)
.changeset/version-exit-finishes-workers.mdpnpm/src/pnpm.tspnpm/test/packageManagerCheck.test.ts
✅ Files skipped from review due to trivial changes (1)
- .changeset/version-exit-finishes-workers.md
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Compile & Lint
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{js,ts,tsx,jsx}
📄 CodeRabbit inference engine (AGENTS.md)
**/*.{js,ts,tsx,jsx}: Use trailing commas in code following Standard Style with modifications
Prefer functions over classes
Declare functions after they are used, relying on hoisting
Functions should have no more than two or three arguments; use a single options object for functions needing more parameters
Organize imports in order: standard libraries, external dependencies (sorted alphabetically), then relative imports
Files:
pnpm/test/packageManagerCheck.test.tspnpm/src/pnpm.ts
**/*.test.{js,ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
Do not use
instanceof Errorfor error type checking in Jest tests; useutil.types.isNativeError()instead to ensure compatibility across realms
Files:
pnpm/test/packageManagerCheck.test.ts
🔇 Additional comments (1)
pnpm/test/packageManagerCheck.test.ts (1)
179-205: Strong regression coverage for the short-circuit hang path.This test is well-targeted: it reproduces the previously hanging
--versionflow and asserts both timely exit and correct output.
`pnpm --version` short-circuited `main.ts` after `switchCliVersion` had already spawned the integrity-resolution worker pool, then returned before reaching the command-handler `finally` block that calls `finishWorkers()`. The pool stayed alive and kept the Node event loop open long after the intended exit. Reproducer: `pnpm --version` in a workspace whose `devEngines.packageManager.version` already matches the running pnpm + `onFail: "download"`. `switchCliVersion` resolves the integrity (spawning workers), finds nothing to swap, returns through main's `--version` short-circuit. Workers stay alive. The CLI entry now runs `finishWorkers()` in its own `finally`, covering this and any other early-return path that bypasses the in-main cleanup. Cleanup rejections are swallowed so the `finally` never masks the real command result.
The "--version exits promptly" test ran `execPnpmSync(['--version'])` from the previous test's prepared dir (`devEngines.packageManager: yarn, onFail: error`). That manifest's pm check runs before main.ts's `--version` short-circuit, so the call exited 1 with empty stdout and `pnpmVersion` was the empty string. Read the version from a fresh empty dir instead.
Summary
pnpmcould hang for the lifetime of the worker pool after a command logically finished whenever the code path returned throughmainwithoutprocess.exit(...).main.tsonly ranfinishWorkers()inside the command-handlerfinallyblock. Short-circuit returns that came earlier —--version,--help, fatal config errors that surface before a command runs — bypassed it and left the integrity-resolution worker pool active.The CLI entry (
pnpm/src/pnpm.ts) now runsfinishWorkers()in its ownfinally, so every exit path tears down the pool. Cleanup rejections are swallowed so thefinallynever masks the real command result.Reproducer
switchCliVersionresolves the integrity (spawning workers), finds nothing to swap (range/version already matches running pnpm), and returns through main's--versionshort-circuit. The unterminated workers used to keep the event loop alive.Surfaced in pnpm/action-setup#254 — bumping the action's bootstrap to 11.0.9 made every CI job using
devEngines.packageManagerwithonFail: "download"hang.Test plan
pnpm/test/packageManagerCheck.test.ts(pnpm --version exits promptly when devEngines.packageManager matches the running pnpm). WithfinishWorkers()removed frompnpm.tsthe test times out and fails on Linux; with the fix it passes in ~6 s.pnpm/test/switchingVersions+packageManagerChecksuites still pass (27 + 28 tests).node:24linux/amd64 container.Summary by CodeRabbit
--versionand--helpexit promptly by ensuring worker cleanup runs on all exit paths.pnpm --versionexits quickly when workspace settings match the running pnpm version.