Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
12 changes: 11 additions & 1 deletion src/bundler/BundleThread.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,16 @@ pub trait CompletionStruct: Node + Send + 'static {
transpiler: &mut Transpiler<'a>,
bump: &'a Arena,
) -> Result<(), crate::Error>;
/// Enqueues the finished completion to the owning JS thread and releases
/// the VM pin taken at creation (worker terminate waits for it, so the
/// enqueue lands on a live VM). Plugin builds run unpinned — see
/// `begin_run` — and may lose the completion at terminate.
fn complete_on_bundle_thread(&mut self);
/// Called before the run starts. Plugin builds drop their VM pin here:
/// they round-trip through the owning JS thread mid-bundle, and holding
/// the pin across that would deadlock worker terminate. FIXME: give
/// plugin builds a cancel signal instead.
fn begin_run(&mut self);
fn set_result(&mut self, result: BundleV2Result);
fn set_log(&mut self, log: bun_ast::Log);
fn set_transpiler(&mut self, this: *mut BundleV2<'_>);
Expand All @@ -75,7 +84,7 @@ pub trait CompletionStruct: Node + Send + 'static {
fn file_map(&mut self) -> Option<NonNull<FileMap>>;
/// Returns a §Dispatch handle (erased owner + `&'static` vtable) the impl
/// provides, so the bundler can read `result == .err` /
/// `jsc_event_loop.enqueueTaskConcurrent` without naming the concrete
/// `vm.enqueue_task_concurrent` without naming the concrete
/// struct.
fn as_js_bundle_completion_task(&mut self) -> dispatch::CompletionHandle;

Expand Down Expand Up @@ -226,6 +235,7 @@ impl<C: CompletionStruct> BundleThread<C> {
// `panic = "abort"` → a Rust panic on this thread enters the
// crash-handler hook and aborts the whole process.
// No `catch_unwind` — there is nothing to catch.
completion.begin_run();
match Self::generate_in_new_thread(completion, generation) {
Ok(()) => {}
Err(err) => {
Expand Down
6 changes: 5 additions & 1 deletion src/bundler/bundle_v2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1120,6 +1120,8 @@ pub mod bv2_impl {
std::ptr::from_mut::<Self>(self).cast::<core::ffi::c_void>(),
),
callback: Self::run_on_js_thread_wrap,
// Owned by the bundler dispatch chain, not the queue.
dispose: None,
};
let task =
bun_event_loop::ConcurrentTask::ConcurrentTask::create(self.js_task.task());
Expand Down Expand Up @@ -1250,6 +1252,8 @@ pub mod bv2_impl {
std::ptr::from_mut::<Self>(self).cast::<core::ffi::c_void>(),
),
callback: Self::run_on_js_thread_wrap,
// Owned by the bundler dispatch chain, not the queue.
dispose: None,
};
let concurrent_task =
bun_event_loop::ConcurrentTask::ConcurrentTask::create(self.js_task.task());
Expand Down Expand Up @@ -1501,7 +1505,7 @@ pub mod bv2_impl {
) {
debug_assert!(self.plugins.is_some());
if let Some(completion) = self.completion {
// From Bun.build — `completion.jsc_event_loop.enqueueTaskConcurrent(task)`.
// From Bun.build — `completion.vm.enqueue_task_concurrent(task)`.
completion.enqueue_task_concurrent(task);
return;
}
Expand Down
21 changes: 21 additions & 0 deletions src/event_loop/AnyTask.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ pub type JsResult<T> = core::result::Result<T, ErasedJsError>;
pub struct AnyTask {
pub ctx: Option<NonNull<c_void>>,
pub callback: fn(*mut c_void) -> JsResult<()>,
/// Releases a queue-owned `ctx` when the task is reclaimed unrun by the
/// worker-terminate drain (JS thread, VM alive — plain drop suffices).
/// `None` ⇒ the ctx is owned elsewhere and must not be freed here.
pub dispose: Option<fn(*mut c_void)>,
}

impl Default for AnyTask {
Expand All @@ -27,6 +31,7 @@ impl Default for AnyTask {
Self {
ctx: None,
callback: |_| unreachable!("AnyTask.callback was undefined"),
dispose: None,
}
}
}
Expand Down Expand Up @@ -64,6 +69,22 @@ impl AnyTask {
callback,
)
},
dispose: None,
}
}

/// [`Self::from_typed`] plus a release for `ctx` if the task is
/// reclaimed unrun by the terminate drain (see the `dispose` field).
#[inline]
pub fn from_typed_with_dispose<T>(
ctx: *mut T,
callback: fn(*mut T) -> JsResult<()>,
dispose: fn(*mut T),
) -> Self {
let mut task = Self::from_typed(ctx, callback);
// SAFETY: same ABI argument as `from_typed`'s callback cast.
task.dispose =
Some(unsafe { bun_ptr::cast_fn_ptr::<fn(*mut T), fn(*mut c_void)>(dispose) });
task
}
}
14 changes: 14 additions & 0 deletions src/event_loop/ConcurrentTask.rs
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,20 @@ impl ConcurrentTask {
Self::create(ManagedTask::ManagedTask::new(ptr, callback))
}

/// [`Self::from_callback`] plus a cleanup run if the task is reclaimed
/// unrun at VM teardown (`EventLoop::deinit`); the cleanup must release
/// whatever the callback would have (drain runs pre-teardown, VM alive).
pub fn from_callback_with_cleanup<T>(
ptr: *mut T,
callback: fn(*mut T) -> crate::JsResult<()>,
cleanup: fn(*mut T),
) -> core::ptr::NonNull<ConcurrentTask> {
bun_core::mark_binding!();
Self::create(ManagedTask::ManagedTask::new_with_cleanup(
ptr, callback, cleanup,
))
}

pub fn from<T: Taskable>(
&mut self,
of: *mut T,
Expand Down
23 changes: 23 additions & 0 deletions src/event_loop/ManagedTask.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,29 @@ impl ManagedTask {
ManagedTask::task(managed)
}

/// [`Self::new`] plus a cleanup run when the task is reclaimed unrun at
/// VM teardown (`EventLoop::deinit`); the cleanup must free `ctx`
/// (it runs on the JS thread; at worker terminate the VM is still alive
/// during the drain, only `deinit` runs post-teardown).
pub fn new_with_cleanup<T>(
ctx: *mut T,
callback: fn(*mut T) -> JsResult<()>,
cleanup: fn(*mut T),
) -> Task {
let managed = bun_core::heap::into_raw(Box::new(ManagedTask {
// SAFETY: same fn-pointer ABI cast as `new`.
callback: unsafe {
bun_ptr::cast_fn_ptr::<fn(*mut T) -> JsResult<()>, fn(*mut c_void) -> JsResult<()>>(
callback,
)
},
ctx: NonNull::new(ctx.cast::<c_void>()),
// SAFETY: same fn-pointer ABI cast as `callback` above.
cleanup: Some(unsafe { bun_ptr::cast_fn_ptr::<fn(*mut T), fn(*mut c_void)>(cleanup) }),
}));
ManagedTask::task(managed)
}

pub fn new_owned<T>(ctx: *mut T, callback: fn(*mut T) -> JsResult<()>) -> Task {
fn drop_ctx<T>(p: *mut c_void) {
// SAFETY: `p` is the `heap::into_raw(Box<T>)` stored in `ctx` by `new_owned`.
Expand Down
3 changes: 3 additions & 0 deletions src/jsc/AsyncModule.rs
Original file line number Diff line number Diff line change
Expand Up @@ -686,6 +686,9 @@ impl AsyncModule {
Self::on_done(p.cast());
Ok(())
},
// Same-thread enqueue; reclaimed by the shutdown drain while
// the VM is alive.
dispose: None,
};
jsc_vm.enqueue_task(Task::init(&raw mut (*clone).any_task));
}
Expand Down
16 changes: 14 additions & 2 deletions src/jsc/ConcurrentPromiseTask.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,12 @@ pub struct ConcurrentPromiseTask<'a, Context: ConcurrentPromiseTaskContext> {
pub ctx: Box<Context>,
pub task: WorkPoolTask,
/// BACKREF — captured from the JS-thread VM at create time; the VM (and its
/// `EventLoop`) outlives every task scheduled on it.
/// `EventLoop`) outlives every task scheduled on it (the pin below is
/// what enforces that for worker VMs).
pub event_loop: BackRef<EventLoop>,
/// Holds the VM's shutdown gate open until the completion is enqueued
/// (dropped on the pool thread in `on_finish`); see `VirtualMachine::pin`.
pub vm_pin: Option<bun_threading::GateGuest>,
pub promise: JSPromiseStrong,
pub global_this: &'a JSGlobalObject,
pub concurrent_task: ConcurrentTask,
Expand All @@ -59,9 +63,11 @@ impl<'a, Context: ConcurrentPromiseTaskContext> ConcurrentPromiseTask<'a, Contex
pub fn create_on_js_thread(global_this: &'a JSGlobalObject, value: Box<Context>) -> Box<Self> {
// `VirtualMachine::get()` returns the JS-thread singleton; the VM and
// its `EventLoop` outlive every task scheduled on it.
let event_loop = BackRef::new(VirtualMachine::get().as_mut().event_loop_shared());
let vm = VirtualMachine::get();
let event_loop = BackRef::new(vm.as_mut().event_loop_shared());
let mut this = Box::new(Self {
event_loop,
vm_pin: Some(vm.pin()),
ctx: value,
task: WorkPoolTask {
node: Default::default(),
Expand Down Expand Up @@ -109,6 +115,9 @@ impl<'a, Context: ConcurrentPromiseTaskContext> ConcurrentPromiseTask<'a, Contex
// the pointer (does not dereference it).
let this_ref = unsafe { &mut *this };
let event_loop = this_ref.event_loop;
// Take the pin into a local first — the enqueue hands the job to the
// JS thread, which may free it before this thread returns.
let pin = this_ref.vm_pin.take();
let task = core::ptr::NonNull::from(
this_ref
.concurrent_task
Expand All @@ -117,6 +126,9 @@ impl<'a, Context: ConcurrentPromiseTaskContext> ConcurrentPromiseTask<'a, Contex
// `task` is the live `concurrent_task` field of the heap-allocated
// job; the queue takes ownership of its intrusive `next` link.
event_loop.enqueue_task_concurrent(task);
// Completion enqueued: dropping the pin lets terminate proceed to
// drain it with the VM still alive.
drop(pin);
}

/// Frees the heap allocation backing this task.
Expand Down
17 changes: 14 additions & 3 deletions src/jsc/RuntimeTranspilerStore.rs
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,7 @@ impl RuntimeTranspilerStore {
global_this: BackRef::new(global_object),
non_threadsafe_referrer: OwnedString::new(referrer),
vm,
vm_pin: None,
log: bun_ast::Log::init(),
loader,
promise: StrongOptional::create(JSValue::from_cell(promise), global_object),
Expand Down Expand Up @@ -377,6 +378,10 @@ pub struct TranspilerJob {
// raw pointers/BackRefs are used (BACKREF — VM owns the
// store and outlives every job).
pub vm: *mut VirtualMachine,
/// Holds the VM's shutdown gate open from `schedule` until the
/// completion is enqueued (dropped on the pool thread) — worker
/// terminate waits for in-flight transpiles instead of racing them.
pub vm_pin: Option<bun_threading::GateGuest>,
pub global_this: BackRef<JSGlobalObject>,
pub fetcher: Fetcher,
pub poll_ref: KeepAlive,
Expand Down Expand Up @@ -486,16 +491,19 @@ impl TranspilerJob {

pub(crate) fn dispatch_to_main_thread(&mut self) {
let vm = self.vm;
// SAFETY: vm outlives the job (BACKREF — VM owns the store).
// Take the pin (held since `schedule`) into a local first — another
// thread may free `self` the moment the push below publishes it.
let pin = self.vm_pin.take();
// SAFETY: `vm` is kept alive by `pin` (BACKREF — VM owns the store).
let transpiler_store: *mut RuntimeTranspilerStore =
unsafe { ptr::addr_of_mut!((*vm).transpiler_store) };
let job = NonNull::from(&mut *self);
// SAFETY: queue is concurrent-safe (UnboundedQueue uses atomics).
unsafe { (*transpiler_store).queue.push(job) };
// Another thread may free `self` at any time after .push, so we cannot use it any more.
// SAFETY: vm outlives the job; event_loop() returns the live self-pointer.
// SAFETY: vm is kept alive by `pin`; event_loop() returns the live self-pointer.
unsafe { &*(*vm).event_loop() }
.enqueue_task_concurrent(ConcurrentTask::create_from(transpiler_store));
drop(pin);
}

pub(crate) fn run_from_js_thread(&mut self) -> JsResult<()> {
Expand Down Expand Up @@ -559,6 +567,9 @@ impl TranspilerJob {
// `EventLoopCtx` vtable; resolve it via the `get_vm_ctx` hook (registered by
// `bun_runtime::init`).
self.poll_ref.ref_(get_vm_ctx(AllocatorType::Js));
// Terminate blocks until this transpile's completion is enqueued.
// SAFETY: `vm` is the live per-thread VM (schedule runs on it).
self.vm_pin = Some(unsafe { (*self.vm).pin() });
WorkPool::schedule(&raw mut self.work_task);
}

Expand Down
59 changes: 59 additions & 0 deletions src/jsc/VirtualMachine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,19 @@ pub struct VirtualMachine {
pub regular_event_loop: EventLoop,
pub event_loop: *mut EventLoop, // BORROW_FIELD — points at sibling regular_event_loop/macro_event_loop

/// Counted gate async producers hold open (see [`Self::pin`]) from job
/// creation until their completion is enqueued back to this VM. Worker
/// terminate closes it — blocking until every producer has finished —
/// before draining the queue and freeing the VM box. Lives in an `Arc`
/// so a guest may outlive the box; `Option` so [`Self::destroy`] can
/// drop the VM's ref explicitly (worker boxes are freed without `Drop`).
pub shutdown_gate: Option<std::sync::Arc<bun_threading::ShutdownGate>>,

/// In-flight abortable transfers; see
/// [`crate::terminate_abort::TerminateAbortRegistry`].
pub terminate_abort_registry:
core::cell::RefCell<crate::terminate_abort::TerminateAbortRegistry>,

pub ref_strings: crate::ref_string::Map,
pub ref_strings_mutex: bun_threading::Mutex,

Expand Down Expand Up @@ -971,6 +984,16 @@ impl VirtualMachine {
self.is_shutting_down
}

/// Pin this VM's shutdown gate open: [`Self::destroy`] / worker terminate
/// will wait for the returned guest before tearing the VM down, so the
/// holder may enqueue completions from other threads without racing
/// teardown. Call on the JS thread when scheduling async work; drop on
/// the completing thread right after the completion enqueue.
pub fn pin(&self) -> bun_threading::GateGuest {
bun_threading::GateGuest::enter(self.shutdown_gate.as_ref().expect("pin() after destroy()"))
.expect("pin() on a closed gate: producers are created on the live JS thread")
}

pub fn has_run_cleanup_hooks(&self) -> bool {
self.has_run_cleanup_hooks
}
Expand Down Expand Up @@ -1572,6 +1595,14 @@ impl VirtualMachine {
(hooks.close_dns_for_terminate)();
}

// Abort registered in-flight transfers while JSC is alive so
// their producer pins drop and their completions land before the
// queue drain below.
if let Some(hooks) = runtime_hooks() {
// SAFETY: live per-thread VM on the JS thread, pre-teardown.
unsafe { (hooks.abort_pending_transfers)(core::ptr::from_mut(self)) };
}

// The HTTP daemon thread holds a `Box<ThreadlocalAsyncHTTP>` per
// in-flight request; with the JS thread exiting those never reach
// a terminal state. Ask it to reclaim them now (waits up to 1s).
Expand Down Expand Up @@ -1809,6 +1840,13 @@ pub struct RuntimeHooks {
/// never lazily created. Called from `WebWorker::shutdown` / `global_exit`
/// right after `close_all_socket_groups`.
pub close_dns_for_terminate: fn(),
/// Worker-terminate / process-exit walk over
/// `vm.terminate_abort_registry`: aborts each registered transfer while
/// the VM is still alive so producer pins drop promptly.
///
/// # Safety
/// `vm` is the live per-thread VM on its own JS thread, pre-teardown.
pub abort_pending_transfers: unsafe fn(vm: *mut VirtualMachine),
}

/// Canonical `EventLoopCtx` vtable for a `*mut VirtualMachine` owner — the JS
Expand Down Expand Up @@ -2115,6 +2153,12 @@ impl VirtualMachine {
let _ = (*regular).tasks.ensure_unused_capacity(64);
addr_of_mut!((*vm).event_loop).write(regular);

addr_of_mut!((*vm).terminate_abort_registry)
.write(core::cell::RefCell::new(Default::default()));
addr_of_mut!((*vm).shutdown_gate).write(Some(std::sync::Arc::new(
bun_threading::ShutdownGate::new(),
)));

// `source_mappings.map` is a sibling-field backref onto
// `saved_source_map_table`.
addr_of_mut!((*vm).saved_source_map_table)
Expand Down Expand Up @@ -4372,6 +4416,21 @@ impl VirtualMachine {
}
/// Worker-thread teardown.
pub fn destroy(&mut self) {
// Backstop for non-worker teardown paths (global_exit, bake): refuse
// new producer pins. Main-VM exit must NOT wait — a pin stranded by
// an un-acked HTTP park would hang the process, and the main VM's
// box is never freed anyway. Worker shutdown already did the full
// close-and-wait before its drain.
if let Some(gate) = &self.shutdown_gate {
if self.is_main_thread {
gate.close_without_waiting();
} else {
gate.close_and_wait();
}
}
// Drop the VM's ref explicitly — worker boxes are freed without
// running `Drop`, which would leak the `Arc` allocation.
drop(self.shutdown_gate.take());
self.regular_event_loop.deinit();
self.macro_event_loop.deinit();

Expand Down
Loading
Loading