bake: retract a file's bundling failure when the file is deleted - #37888
bake: retract a file's bundling failure when the file is deleted#37888robobun wants to merge 1 commit into
Conversation
IncrementalGraph::on_file_deleted disconnected the node's imports and re-enqueued its importers, but left the node's `failed` flag set and its SerializedFailure in dev.bundling_failures, and never pushed it onto incremental_result.failures_removed. Only receive_chunk clears a node's failure, and a deleted file is never received again, so the failure stayed registered for the rest of the process unless the file came back: error overlays kept showing it next to the importers' new resolution errors, every later "Build Failed" page listed it, and an error page never reloaded once the importers were fixed because one entry remained. Clear the flag and move the entry onto failures_removed in on_file_deleted; index_failures publishes it at the end of the same bundle, alongside the failures the re-bundled importers report.
|
Warning Review limit reached
Next review available in: 1 minute Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the 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 configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (2)
Comment |
|
Updated 3:05 PM PT - Aug 12th, 2026
❌ @robobun, your commit 1a64af5 has 4 failures in
🧪 To try this PR locally: bunx bun-pr 37888That installs a local version of the PR into your bun-37888 --bun |
There was a problem hiding this comment.
LGTM — small, well-tested fix that mirrors the existing failure-retraction pattern from receive_chunk.
What was reviewed:
- The new block in
on_file_deletedis byte-for-byte the same bookkeepingreceive_chunkdoes on both sides (clearfailed,fetch_swap_removefrombundling_failures, push tofailures_removed), so thefailed⇔ map-entry invariant is preserved and the panic guard matches. - Confirmed
on_file_deletedruns mid-bundle viahandle_parse_task_failure, andindex_failuresdrainsfailures_removedat the end of that bundle — the removal ships in the same errors packet as the importer's new failure. - Checked that the sibling-projection helpers (
dev_bundling_failures,dev_incremental_result) are the same ones already used fromreceive_chunk/insert_failure; no new unsafe. - Tests cover both client and server graphs, the incremental errors packet, a fresh page load, error-page reload once the last failure clears, and node reuse on recreation; win32 skip matches the neighboring deletion test.
Extended reasoning...
Overview
The PR adds 13 lines to IncrementalGraph::on_file_deleted in src/runtime/bake/dev_server/incremental_graph.rs and two dev-server tests in test/bake/dev/bundle.test.ts. When a file that previously failed to bundle is deleted, its stale SerializedFailure is now retracted from dev.bundling_failures and pushed onto incremental_result.failures_removed, so the error overlay and the "Build Failed" page stop listing it and the error page can reload once the remaining failures clear.
Security risks
None. This is dev-server error-overlay bookkeeping; no user input parsing, no auth, no filesystem or network surface changes.
Level of scrutiny
Low-to-medium. The dev server's incremental graph is a stateful subsystem, but the change is a verbatim copy of the failure-clearing block that already runs in receive_chunk (both the Side::Client and Side::Server branches do exactly this: clear failed, fetch_swap_remove the OwnerPacked(SIDE, index) entry, push it onto failures_removed, panic if the invariant is broken). The failed flag and the bundling_failures entry always move together everywhere else in the file (insert_failure, receive_chunk, trace_imports), and this change keeps that invariant. The call site (handle_parse_task_failure → ENOENT branch) is inside a bundle, and index_failures drains failures_removed at the end of that bundle, so the removal is published exactly as the PR describes.
Other factors
- The two new tests are placed next to the existing
deleting imported file shows error then recoverstest, share its win32 skip and its fixture shape, and exercise both the client graph (HTML route) and the server graph (minimalFramework). They assert the overlay contents after the delete, a fresh page load's failure list, that the error page reloads once the last failure is fixed, and that recreating the file (which reuses the node) recovers cleanly — the last check guards against clearing only the flag or only the map entry. - The PR description states both tests fail against the released build at the delete step with the stale error still listed, and pass on a debug/ASAN build with the fix; the rest of
bundle.test.ts(23 tests) still passes. - The related sibling path (
disconnect_and_delete_filefor demoted client components, #37862) is explicitly called out as independent; the diffs do not overlap. - No new
unsafe, no allocation or lifetime changes, no cross-graph access — the sibling-field projections used are the existingdev_bundling_failures()/dev_incremental_result()helpers already exercised from the same file.
Problem
other.ts:1:22: error: Unexpected ;), then delete it. The importer'sCould not resolve: "./other"error is added, but the deleted file's error is never retracted: the overlay and every fresh "Build Failed" page list both, and the error page never reloads once the importer drops the import. Reproduces on the released build.Fix
Background
failedflag; the failure text lives in one process-wide map keyed by (side, node index), which overlays and fresh "Build Failed" pages are rendered from.Original description
Problem
The remaining
Batch::from(..)sites that hand an intrusive task to a thread pool projected the*mut Taskout of a reference to the object instead of out of the object's pointer:PatchTask::schedule(&mut self, ..)(src/install/patch_install.rs)Batch::from(&raw mut self.task)AsyncHTTP::schedule(&mut self, ..)andpreconnect(src/http/AsyncHTTP.rs)Batch::from(addr_of_mut!(self.task)),addr_of_mut!(async_http.task)withasync_http: &mut AsyncHTTPInstaller::start_task(src/install/isolated_install/Installer.rs)Batch::from(&raw mut task.task)withtask = &mut self.tasks[i]ThreadPool::each/each_ptr(src/threading/ThreadPool.rs)Batch::from(addr_of_mut!(runner_task.task))fromtasks.iter_mut()compute_data_for_source_map(src/bundler/LinkerContext.rs)Batch::from(&raw mut line_offset.thread_task)fromiter_mut(), twicegenerate_chunks_in_parallel(src/bundler/linker_context/generateChunksInParallel.rs)push(..); Batch::from(&raw mut tasks.last_mut().unwrap().task)per element, four loopsIn every case the pool calls back with exactly that pointer and the callback recovers the object from it with container-of and then uses the rest of the object:
PatchTask::from_task_ptr(run_from_thread_pool),from_field_ptr!(Task, ..)(isolated install, which also writesresultand linksnext),from_field_ptr!(RunnerTask, ..)(each, readsiandctx),from_field_ptr!(SourceMapDataTask, ..),from_field_ptr!(PrepareCssAstTask, ..)andpending_part_range_prologue. ForAsyncHTTPit happens before the task is even queued:HTTPThread::schedulecontainer-ofs every task in the batch on the calling thread, links the object intoqueued_tasksthrough the result, andstart_queued_tasklaterptr::reads the whole struct through it.bun_core::container_ofdocuments that this requires a pointer derived from the object's pointer ("a&mut fieldreborrow does not suffice"), andWorkPool::schedule_ownedprojects out of theBox::into_rawpointer for that reason. The otherBatch::fromsites in the tree already do this, either inline (&raw mut (*parse_task).task,addr_of_mut!((*task).task)) or by passing a pointer a helper projected that way (&raw mut (*task).threadpool_taskin the package manager's enqueue helpers);WorkPool::scheduleforwards its argument, and the one site that narrows through an accessor is #37865's.The reference-based spelling does not meet that contract. rustc retags the result of
&raw mut <place>unless the place is based on a raw pointer, and under Stacked Borrows that retag covers the projected field only, so the callback's first sibling-field access is out of range. Thepush+last_mut()loops ingenerate_chunks_in_parallelhave a second problem: everylast_mut()goes throughVec::deref_mut, which reborrows the whole buffer and invalidates the pointers already in the batch, soBatch::pushitself trips when it links the next task onto the previous one (projecting throughptr::from_mut(last_mut())is not enough there; the batch has to be built after the Vec is filled). Reduction below. Tree Borrows, which is whatbun run rust:mirichecks, accepts all of these shapes, none of the crates involved are in its crate set, and no crash is known or expected from today's codegen: the addresses are right, and this only matters to the aliasing model. It is the same contract violation as #37768 (theWorkPool::schedulepopulation, which deliberately leavesBatch::fromout of its lint) and #37865 (NewTaskQueue::push, the oneBatch::fromsite that narrows through an accessor; not touched here), in the one spelling neither of them covers.Reduction under Miri (miri 0.1.0 9f36de775b), one mode per shape
refis the&raw mut task.task/&raw mut self.taskshape,iter_muttheaddr_of_mut!(runner_task.task)shape,last_mutthe chunk loops;last_mut_from_mutis the chunk loop with only the projection fixed;from_mut,as_mut_ptrandraware the three shapes this PR converts to. The pool side iscontainer_offollowed by a sibling-field read and write, run on another thread. The trailing comments on the-->lines name the statement at each location.Fix
Same pointer values as before at every site, so no behaviour change; what changes is which pointer they are projected from.
PatchTask::schedulebecomesunsafe fn schedule(this: *mut Self, batch)and projects out ofthis. Its only caller,flush_patch_task_queue, already holds the*mut PatchTaskit popped from the fifo and now reads the callback kind through it instead of forming a&mut PatchTask.AsyncHTTP::schedulekeeps&mut self(its callers,send_sync,NetworkTask,FetchTasklet, S3, hold the object by reference or value, andHTTPThread::schedulerecovers anAsyncHTTP, which a&mut AsyncHTTPdoes cover) and projects throughptr::from_mut(self), a reborrow of the whole object.preconnectgoes through it instead of projecting by hand, so the http crate has one projection site.Installer::start_taskandcompute_data_for_source_mapproject throughptr::from_mutof the slot they just wrote.ThreadPool::each_impland the two batches ingenerate_chunks_in_parallelfill theirVecfirst and then build the batch in one loop fromas_mut_ptr().add(i); the three copies of thelast_mut()push in the chunk loop collapse into that one loop, and the with-capacity comments that justified taking pointers mid-fill go away with the pattern.Verification
test/internal/source-lints/thread-pool-batch-projection.test.tsbans&raw mut/&raw const/addr_of_mut!/addr_of!of a field path rooted at a binding as the argument of anyBatch::from(call (however the type is spelled, including thePoolBatch/ThreadPoolBatchaliases), with a self-test over the spellings of every site in the tree and the converted shapes. Against main it reports exactly the eleven sites above:and passes on this branch with no allowlist. It is deliberately disjoint from #37865's lint, which bans the reference / accessor /
from_mutshapes in the same argument and reports onlysrc/install/PackageInstall.rs:475both on main and on this branch (checked by running it here), so the two can land in either order without sharing an allowlist; #37768's lint coversWorkPool::schedule, and both of those also report nothing on this branch.The converted paths are exercised by existing tests, all run against the debug build of this branch:
test/cli/install/bun-install-patch.test.ts(18 pass) andbun-patch.test.ts(31 pass) forPatchTask::scheduleand the registry downloads that go throughNetworkTask→AsyncHTTP::schedule;test/cli/install/isolated-install.test.ts(62 pass) forstart_task;test/cli/install/bun-audit.test.ts(17 pass) for thesend_syncpath;test/js/web/fetch/fetch-redirect.test.ts(30 pass) andclient-fetch.test.ts(34 pass) for the fetch path;test/bundler/bundler_html.test.ts(22 pass; JS, CSS and HTML part ranges plus the CSS AST batch),test/bundler/css/css-modules.test.ts(6 pass),test/bundler/bundler_splitting.test.ts(11 pass; many chunks througheach_ptr),test/bundler/bun-build-api.test.ts(52 pass; source maps) andtest/js/bun/sourcemap/internal-sourcemap-roundtrip.test.ts(22 pass).fetch.preconnectwas checked directly with a local listener (the preconnect opens the connection before the firstfetch);test/js/web/fetch/fetch-preconnect.test.tsitself cannot run in this container becauselocalhostresolves to::1first here, which makes it fail identically on the released binary, and that is tracked separately.cargo clippy --no-depsonbun_threading,bun_http,bun_installandbun_bundleris clean andcargo fmtis clean.