Skip to content
Open
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
fa2e27a
threading: make GuardedLock \!Send/\!Sync like MutexGuard
alii Jun 2, 2026
7934b18
fetch: make ignore_data atomic, mirror is_http2 for lock-free reads, …
alii Jun 2, 2026
0093fb3
fetch: partition FetchTasklet into JsState and HttpHandoff (rename-only)
alii Jun 2, 2026
561266a
fetch: start FetchTasklet refcount at 2 and name each ref release
alii Jun 2, 2026
fb8ad8a
fetch: document FetchTasklet's cross-thread ref and lock protocol
alii Jun 2, 2026
92e88ed
fetch: convert FetchTasklet's mutex to Guarded<HttpHandoff> with spli…
alii Jun 2, 2026
b21b592
fetch: make Weak::create_ptr unsafe so callers must prove ctx liveness
alii Jun 3, 2026
92ec278
Merge origin/main (resolve TODO-sweep conflicts in FetchTasklet.rs)
robobun Jun 4, 2026
08158a0
fetch: cover process.exit() with in-flight requests (release_at_shutd…
robobun Jun 4, 2026
bee55b6
fetch: balance streaming-upload refs in release_at_shutdown
robobun Jun 4, 2026
eab71d3
[autofix.ci] apply automated fixes
autofix-ci[bot] Jun 4, 2026
2ceb902
test: drop stale ASAN stderr filter in fetch-exit-in-flight
robobun Jun 4, 2026
0d2ddc2
fetch: balance sink/drain refs in callback's shutdown branch too
robobun Jun 4, 2026
2bb34e8
fetch: take streaming-upload exit refs atomically across all three paths
robobun Jun 4, 2026
a09b295
test: document why fetch-exit-in-flight overrides the default timeout
robobun Jun 4, 2026
4722a5a
ci: retrigger
robobun Jun 4, 2026
cbd7951
Merge branch 'main' into ali/fetch-lifetime-clarity
alii Jun 4, 2026
e6abfab
fetch: take streaming exit refs in on_progress_update's shutdown path…
robobun Jun 4, 2026
acde789
fetch: claim streaming-upload ref releases against exit-window takes
alii Jun 4, 2026
e1254e6
test: suppress macOS ParkingLot one-time leak in exit-in-flight tests
alii Jun 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion src/http/HTTPThread.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1067,7 +1067,16 @@ impl HttpThread {
if let Some(ctx) = client.custom_ssl_ctx.take() {
ctx.deref();
}
drop(core::mem::take(&mut client.state));
let mut state = core::mem::take(&mut client.state);
// A streaming request body holds the HTTP-side ref on the
// `ThreadSafeStreamBuffer` (`Stream` has no `Drop` — the body
// is bitwise-shared with the JS-thread original). Normal
// teardown releases it in `InternalState::reset`; mirror that
// here or the buffer outlives both of its owners' releases
// and LSan reports it at exit. Idempotent: `detach` takes the
// `Option`.
state.original_request_body.deinit();
drop(state);
if let Some(f) = release.release_at_shutdown {
f(release.ctx);
}
Comment thread
robobun marked this conversation as resolved.
Expand Down
24 changes: 23 additions & 1 deletion src/jsc/Weak.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,14 +115,36 @@ impl<T> Weak<T> {
global_this: &JSGlobalObject,
ref_type: WeakRefType,
ctx: &mut T,
) -> Self {
// SAFETY: `ctx` is derived from a live `&mut T`, so it satisfies the
// liveness contract at creation; the caller's `&mut T` is the same
// proof `create` has always demanded.
unsafe { Self::create_ptr(value, global_this, ref_type, NonNull::from(ctx)) }
}

/// Like [`create`](Self::create), but takes `ctx` as a raw pointer for
/// callers that cannot form `&mut T` (e.g. while disjoint field borrows of
/// the owner are live).
///
/// # Safety
///
/// `ctx` must point to a live `T` and remain valid for as long as the
/// weak ref's finalize callback can fire — the GC finalizer dereferences
/// it. (`create` enforces this via `&mut T`; here the caller must prove
/// liveness instead.)
pub unsafe fn create_ptr(
value: JSValue,
global_this: &JSGlobalObject,
ref_type: WeakRefType,
ctx: NonNull<T>,
) -> Self {
if !value.is_empty() {
return Self {
r#ref: Some(WeakImpl::init(
global_this,
value,
ref_type,
Some(NonNull::from(ctx).cast::<c_void>()),
Some(ctx.cast::<c_void>()),
)),
global_this: Some(global_this.into()),
_ctx: PhantomData,
Expand Down
28 changes: 21 additions & 7 deletions src/runtime/dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1184,16 +1184,30 @@
use bun_event_loop::task_tag;
match task.tag {
// `callback` (HTTP thread) won the `has_schedule_callback` CAS and
// posted this entry, then deref'd its own +1 if final; the JS-side
// +1 it expected `on_progress_update` to drop is the one we release
// here. Runs on the JS thread, so the plain `deref` (→ `deinit` on
// 1→0) is the right teardown path; the HTTP daemon is already
// parked (`shutdown_for_exit` precedes `destroy`), so the
// `Box<AsyncHTTP>` and any `metadata` it owns are exclusively ours.
// posted this entry, then released its own ref if final; the JS-side
// ref it expected `on_progress_update` to drop is the one we release
// here. Runs on the JS thread, so plain derefs (→ `deinit` on 1→0)
// are the right teardown path; the HTTP daemon is already parked
// (`shutdown_for_exit` precedes `destroy`), so the `Box<AsyncHTTP>`
// and any `metadata` it owns are exclusively ours.
task_tag::FetchTasklet => {
let tasklet = task.ptr.cast::<FetchTasklet>();
// If a *final* `callback` landed in the exit window while this
// node was queued (lost the `has_schedule_callback` CAS), the
// entry left `in_flight` with only its HTTP-side ref released —
// neither `release_at_shutdown` nor `callback`'s shutdown branch
// balanced a streaming upload's sink/drain refs. Take them here;
// the take is a no-op when another exit path already claimed
// them (or there was no streaming upload).
for _ in 0..FetchTasklet::take_streaming_refs_for_exit(tasklet) {
// SAFETY: `tasklet` is the live heap `FetchTasklet`; the taken
// markers prove the refs are still held, and the HTTP daemon
// is parked so we release them exclusively.
FetchTasklet::deref(tasklet);
}

Check warning on line 1207 in src/runtime/dispatch.rs

View check run for this annotation

Claude / Claude Code Review

on_progress_update's shutdown branch is a fourth exit path that skips take_streaming_refs_for_exit

Completeness follow-up to the now-resolved threads at HTTPThread.rs:1082 and dispatch.rs (fixed via `take_streaming_refs_for_exit` in 2bb34e8): there is a fourth `is_shutting_down()` exit path — `on_progress_update` at FetchTasklet.rs:1168-1183 — that releases only `release_js_ref(t.task)` when `is_done`, without calling `take_streaming_refs_for_exit`. Today the JS loop never ticks in that window (per the FIXME at VirtualMachine.rs:1541-1543), so this is defensive-completeness only; but the take
Comment thread
robobun marked this conversation as resolved.
// SAFETY: `task.ptr` is the live heap `FetchTasklet`; HTTP daemon is
// already parked so we hold the sole reference.
FetchTasklet::deref(task.ptr.cast::<FetchTasklet>());
FetchTasklet::release_js_ref(tasklet);
true
}
// `AsyncFSTask`s are `Box::leak`'d in `create()` and freed by
Expand Down
Loading
Loading