Skip to content

install: fail instead of hanging when a tarball download task cannot be created - #39672

Open
robobun wants to merge 1 commit into
mainfrom
farm/96430ae8/tarball-enqueue-failure-hang
Open

install: fail instead of hanging when a tarball download task cannot be created#39672
robobun wants to merge 1 commit into
mainfrom
farm/96430ae8/tarball-enqueue-failure-hang

Conversation

@robobun

@robobun robobun commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator

Problem

  • bun install with the isolated linker hangs forever when a tarball download task cannot be created, for example a tarball URL that is not http. Two cases reach it: the second store entry of the package (peer variants), and any retry of the install once the manifest is in the manifest cache, because the resolve phase already failed the same way.
  • The cause is in generate_network_task_for_tarball (src/install/PackageManager/runTasks.rs). It records the task id in network_dedupe_map before for_tarball can fail. A later enqueue for the same id then sees a created task and queues its callback behind it. No task exists, so the callback never runs and the store entry's pending-task slot is never released.
  • The vended NetworkTask slot was also leaked on that path.

Fix

  • On the for_tarball error path, mark the task id failed and return the slot to the pool. Later enqueues get AlreadyFailed, which every caller (isolated, hoisted, runtime auto-install) already handles by releasing its own bookkeeping, and the install exits 1 with the existing error.
  • This is the function that creates the dedupe record, so all six callers are covered, including the resolve-phase prefetch that causes the manifest cache case.
  • Verified: two new tests in test/cli/install/isolated-install.test.ts. Both hang on the released bun (the test times out) and pass in under a second with the fix. Also the install suites listed in Notes.

Background

  • The install phase reserves one pending-task slot per store entry and waits until all slots are released. An entry that needs a download releases its slot when the download's callbacks run.
  • task_queue maps a task id to the callbacks waiting for it. network_dedupe_map records which task ids already have a task, so one tarball is downloaded once. failed on that record (install: don't re-download a tarball that already failed #34103) makes later enqueues fail fast with AlreadyFailed.
  • The hoisted linker does not reserve slots, so it already exited 1 in both cases. Only the isolated linker hung.
Notes

Repro of the manifest cache case (the realistic one: any failed install caches the manifest, and the retry hangs). Registry manifest with dist.tarball: "ftp://...", isolated linker. Run 1: error: Expected tarball URL to start with https:// or http:// and exit 1 (the error propagates out of the resolve phase). Run 2 within the manifest max-age: the dependency resolves synchronously from the cache, the prefetch fails and is only logged, the install phase enqueues the tarball, has_created_network_task says it exists, and the process never exits. With the fix run 2 prints the same error and exits 1. The hoisted linker exits 1 in run 2 before and after.

Repro of the peer variant case. Two workspaces depend on peer-deps@1.0.0 with different no-deps versions. Install, replace the tarball URL of peer-deps in bun.lock with ftp://..., remove node_modules, install with an empty cache. Released bun prints InvalidURL: failed to enqueue package for download: peer-deps@1.0.0 once and never exits.

Why here and not in the callers. An earlier version of this change removed the queued callback in enqueue_package_for_download and enqueue_tarball_for_download. That fixes the peer variant case only. The manifest cache case fails in the resolve-phase prefetch (PackageManagerEnqueue.rs, get_or_put_resolved_package), which never touches task_queue, so the install phase still parked the entry. Marking the record where it is created covers both. The callbacks that the two enqueue functions queued before the failure stay in task_queue. Nothing reads them: both functions check network_task_has_failed before they look at the queue.

Slot return. All error returns in for_tarball happen before it initializes unsafe_http_client, so put (which drops the rest of the NetworkTask) is the whole cleanup.

Suites run with the debug build: isolated-install (84), bun-install-tarball-integrity, bun-install-retry, bun-add, bun-install-patch, bun-install-git-deps, bun-install (the same 14 failures as on main in this sandbox: they need the public internet or an IPv6 localhost). cargo fmt is clean.

Split out of #39640, which keeps the unrelated isolated installer drain.


no test proof · iteration 0 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/cli/install/isolated-install.test.ts

…be created

generate_network_task_for_tarball records the task id in the dedupe
map before for_tarball can fail, for example on a tarball URL that is
not http. After such a failure every later enqueue for the same id saw
a task that was already created and queued its callback behind it. No
task existed, so the isolated installer waited for the callback
forever. This happened for the second store entry of a package, and
for any package whose manifest came from the manifest cache, because
the resolve phase had already failed the same way.

On that error path, mark the task id failed and return the network
task slot to the pool. Later enqueues get AlreadyFailed, which every
caller already handles, and the install exits with an error.
@coderabbitai

coderabbitai Bot commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

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

Next review available in: 2 minutes

Limit details: You’ve used all 5 included reviews currently available.

You can run this review on demand instead of waiting.

On-demand reviews are free until September 18, 2026. After that, they cost $0.25 per reviewed file.

  • Run review for free
How can I continue?

Run this review now using the option above, or comment @coderabbitai review --use-credits.

You can also wait for the limit to reset, then comment @coderabbitai review or push new commits to the PR.

An organization admin can change what happens after included review limits in Billing.

How do review limits work?

CodeRabbit enforces per-developer PR review limits within each organization.

For paid Pro and Pro+ reviews, CodeRabbit uses a developer's included PR review attempts over the past 7 days to set the current hourly allowance. At typical activity levels, the full plan allowance applies. Higher sustained activity can lower the allowance until earlier attempts leave the 7-day window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 5a2dffcf-1a1a-424a-82a5-39f2a62f2318

📥 Commits

Reviewing files that changed from the base of the PR and between 0a4e3b1 and 0f3d4ed.

📒 Files selected for processing (2)
  • src/install/PackageManager/runTasks.rs
  • test/cli/install/isolated-install.test.ts

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

@robobun

robobun commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator Author

Status: ready for review.

Reproduced both cases on the released bun: the second store entry of a package whose tarball URL in bun.lock is not http, and a retry of a failed install once the manifest is in the manifest cache. Both hang the isolated linker forever and exit 1 with this change. The two new tests time out on the released bun and pass with the fix. The isolated installer drain that was bundled with an earlier version of this fix stays in #39640.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM — targeted hang fix that reuses the existing mark_network_task_failed path and returns the pool slot on the one error branch that leaked it.

What was reviewed:

  • Verified all three for_tarball error returns (build_url()?, InvalidURL, header_builder.allocate()?) occur before unsafe_http_client is written at NetworkTask.rs:926, so the SAFETY comment on put is accurate.
  • Confirmed write_init populates every field with a drop-safe value and MaybeUninit has no drop glue, so put's drop_in_place is sound on this path.
  • Checked that enqueue_package_for_download / enqueue_tarball_for_download gate on network_task_has_failed before touching task_queue, and every isolated/hoisted caller already handles AlreadyFailed by releasing its bookkeeping.
  • Tests are hermetic (local Verdaccio + Bun.serve, pinned cache dirs), drain pipes concurrently, and assert the specific error message + exit code.
Extended reasoning...

Overview

The PR changes one error path in generate_network_task_for_tarball (src/install/PackageManager/runTasks.rs): instead of propagating for_tarball's error via ?, it explicitly marks the task id failed in network_dedupe_map and returns the vended NetworkTask slot to the pool before returning the error. Two regression tests in test/cli/install/isolated-install.test.ts cover the peer-variant case (two store entries share one tarball) and the manifest-cache case (resolve-phase prefetch fails, install-phase enqueue parks behind it).

Security risks

None. The change is internal error-path bookkeeping in the package manager. It doesn't touch validation, credentials, network parsing, or any user-facing surface. The only unsafe block is a pool put whose SAFETY invariant I verified against for_tarball and write_init — every field is drop-safe at the point put runs, and the MaybeUninit unsafe_http_client has no drop glue.

Level of scrutiny

Moderate — this is native package-manager code with an unsafe block, but the change is 10 lines that reuse two existing helpers (mark_network_task_failed, preallocated_network_tasks.put) in exactly the way they're already used on the download-failure and extract-failure paths in the same file. The fix sits at the layer that creates the dedupe record, so all six callers are covered without per-caller changes. mark_network_task_failed only touches network_dedupe_map, disjoint from the pool slot pointed at by net_ptr, matching the disjointness pattern already relied on by the streaming-setup tail of the same function.

Other factors

The PR description is unusually thorough: it explains the mechanism, why the fix belongs here rather than in the two enqueue callers (the resolve-phase prefetch never touches task_queue), and confirms both tests hang on the released bun and pass with the fix. I verified the AlreadyFailed handling in isolated_install.rs (three sites) and PackageInstaller.rs (three sites) — each releases its slot/tree count. The tests follow harness conventions (Verdaccio registry, tempDir, {...bunEnv}, concurrent pipe drain, using for cleanup) and assert the specific error text and exitCode === 1. The bug hunting system found no issues.

@robobun

robobun commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator Author

Nothing is outstanding from the automated review. It checked the two facts this change rests on: every for_tarball error return happens before unsafe_http_client is written, and both enqueue functions consult network_task_has_failed before the callback queue. Ready for a maintainer.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants