Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
252a3f5
spawnSync: release polls and keep-alives on the loop they counted on
robobun Aug 23, 2026
f223887
Run the spawnSync finalizer test on release builds
robobun Aug 23, 2026
c9802f7
Make the spawnSync finalizer test independent of the allocation phase
robobun Aug 26, 2026
9a35ffb
FileSink::setup: leave the fd to the writer when start() fails on POSIX
robobun Aug 27, 2026
6dba38c
FileSink::setup: one-line note on the cfg(windows) close
robobun Aug 27, 2026
6727e1a
timer: take the EventLoopCtx for the loop ref instead of a raw loop p…
robobun Aug 28, 2026
a2256c5
test: report how many writers the probe spawnSync collected
robobun Aug 28, 2026
4f80ad9
PipeWriter: start() owns the fd on every failure path
robobun Aug 28, 2026
f7bca3b
test: hold the finalizer fixture's writers through a global
robobun Aug 28, 2026
390b22c
PipeWriter: one-line note on the reused-poll close
robobun Aug 28, 2026
e1159c8
test: keep the closed dup number occupied until the filesink fixture …
robobun Aug 28, 2026
404aaff
Merge main into the spawnSync loop-bit branch
robobun Sep 18, 2026
2cd5c71
test: the double close is gone on main, keep the EEXIST half of the c…
robobun Sep 18, 2026
bb97ecd
Merge main into the spawnSync loop-bit branch
robobun Sep 29, 2026
0dedb9f
Merge main into the spawnSync loop-bit branch (#43868)
robobun Sep 30, 2026
87f1190
Keep the loop bookkeeping of KeepAlive out of line
robobun Sep 30, 2026
2d3f147
Shorten the comment on EventLoopCtx::loop_ref
robobun Sep 30, 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
7 changes: 7 additions & 0 deletions src/event_loop/SpawnSyncEventLoop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,13 @@ impl Drop for SpawnSyncEventLoop {
impl SpawnSyncEventLoop {
/// Configure the event loop for a specific VM context
pub fn prepare(&mut self, vm: *mut () /* SAFETY: erased *mut VirtualMachine */) {
#[cfg(unix)]
debug_assert!(
self.uws_loop().num_polls == 0 && self.uws_loop().active == 0,
"spawnSync isolated loop has num_polls={} active={} before a spawn",
self.uws_loop().num_polls,
self.uws_loop().active,
);
__bun_spawn_sync_event_loop_set_vm(self.event_loop, vm);
self.did_timeout.set(false);
self.vm = vm;
Expand Down
9 changes: 2 additions & 7 deletions src/io/ParentDeathWatchdog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -297,13 +297,8 @@ pub fn install_on_event_loop(handle: EventLoopCtx) {
Default::default(),
Owner::new(poll_tag::PARENT_DEATH_WATCHDOG, instance_ptr.cast()),
);
// SAFETY: `poll` was just allocated by `FilePoll::init`; sole `&mut`
// borrow; `register` does not re-derive the loop.
match unsafe { &mut *poll }.register(
handle.loop_mut(),
crate::file_poll::Flags::Process,
true,
) {
// SAFETY: `poll` was just allocated by `FilePoll::init`; sole `&mut` borrow.
match unsafe { &mut *poll }.register(handle, crate::file_poll::Flags::Process, true) {
bun_sys::Result::Ok(()) => {
// Do not keep the event loop alive on this poll's behalf — the
// watchdog must never prevent Bun from exiting when there is no
Expand Down
6 changes: 3 additions & 3 deletions src/io/PipeReader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ impl BufferedReaderVTable {
self.link().event_loop()
}

#[cfg(windows)]
fn loop_(&self) -> *mut Loop {
self.link().loop_ptr()
}
Expand Down Expand Up @@ -360,7 +361,7 @@ impl PosixBufferedReader {
// Unregister the FilePoll if it's registered
if let PollOrFd::Poll(poll) = &mut self.handle {
if poll.is_registered() {
let _ = poll.unregister(self.vtable.loop_().cast(), false);
let _ = poll.unregister(self.vtable.event_loop(), false);
}
}
}
Expand Down Expand Up @@ -521,7 +522,6 @@ impl PosixBufferedReader {
// normalize self.handle to Poll before taking the single &mut borrow,
// so no raw-pointer escape is needed.
let ev = self.vtable.event_loop();
let lp = self.vtable.loop_();
let owner_ptr = std::ptr::from_mut(self).cast::<c_void>();

if let PollOrFd::Fd(fd) = self.handle {
Expand All @@ -544,7 +544,7 @@ impl PosixBufferedReader {
poll.enable_keeping_process_alive(ev);
}

match poll.register_with_fd(lp.cast(), FilePollKind::Readable, poll.fd()) {
match poll.register_with_fd(ev, FilePollKind::Readable, poll.fd()) {
sys::Result::Err(err) => Err(err),
sys::Result::Ok(()) => Ok(()),
}
Expand Down
30 changes: 7 additions & 23 deletions src/io/PipeWriter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -469,9 +469,7 @@ impl<Parent: PosixBufferedWriterParent> PosixBufferedWriter<Parent> {

pub fn register_poll(&mut self) {
let Some(poll) = self.get_poll() else { return };
// Use the event loop from the parent, not the global one
let loop_ = self.parent_event_loop().loop_();
match poll.register_with_fd(loop_, FilePollKind::Writable, poll.fd()) {
match poll.register_with_fd(self.parent_event_loop(), FilePollKind::Writable, poll.fd()) {
sys::Result::Err(err) => {
// Same report as a failed write (the streaming writer does the
// same): parents expect every error to be followed by `on_close`.
Expand Down Expand Up @@ -581,9 +579,7 @@ impl<Parent: PosixBufferedWriterParent> PosixBufferedWriter<Parent> {
p
}
};
let loop_ = self.parent_event_loop().loop_();

match poll.register_with_fd(loop_, FilePollKind::Writable, fd) {
match poll.register_with_fd(self.parent_event_loop(), FilePollKind::Writable, fd) {
sys::Result::Err(err) => {
// A poll from an earlier start() still holds that start's fd.
if existing_poll.is_none() {
Expand Down Expand Up @@ -631,9 +627,6 @@ pub trait PosixStreamingWriterParent {
/// # Safety
/// `this` must point to a live `Self`.
unsafe fn event_loop(this: *mut Self) -> EventLoopHandle;
/// # Safety
/// `this` must point to a live `Self`.
unsafe fn loop_(this: *mut Self) -> *mut bun_uws_sys::Loop;
}

pub struct PosixStreamingWriter<Parent: PosixStreamingWriterParent> {
Expand Down Expand Up @@ -827,8 +820,8 @@ impl<Parent: PosixStreamingWriterParent> PosixStreamingWriter<Parent> {
fn register_poll(&mut self) {
let Some(poll) = self.get_poll() else { return };
// SAFETY: parent BACKREF set via set_parent; outlives this writer.
let loop_ = unsafe { Parent::loop_(self.parent()) }.cast();
match poll.register_with_fd(loop_, FilePollKind::Writable, poll.fd()) {
let event_loop = unsafe { Parent::event_loop(self.parent()) };
match poll.register_with_fd(event_loop, FilePollKind::Writable, poll.fd()) {
sys::Result::Err(err) => {
// PORT_NOTES_PLAN R-2: `&mut self` carries LLVM `noalias`, but
// `Parent::on_error` (e.g. `FileSink::on_error`) re-enters via
Expand Down Expand Up @@ -1098,13 +1091,13 @@ impl<Parent: PosixStreamingWriterParent> PosixStreamingWriter<Parent> {
}

// SAFETY: parent BACKREF set via set_parent; outlives this writer.
let loop_ = unsafe { Parent::event_loop(self.parent()) };
let event_loop = unsafe { Parent::event_loop(self.parent()) };
let existing_poll = self.get_poll();
let poll = match existing_poll {
Some(p) => p,
None => {
let p = FilePollRef::init(
loop_,
event_loop,
fd,
Owner::new(Parent::POLL_OWNER_TAG, std::ptr::from_mut(self).cast()),
);
Expand All @@ -1113,7 +1106,7 @@ impl<Parent: PosixStreamingWriterParent> PosixStreamingWriter<Parent> {
}
};

match poll.register_with_fd(loop_.loop_(), FilePollKind::Writable, fd) {
match poll.register_with_fd(event_loop, FilePollKind::Writable, fd) {
sys::Result::Err(err) => {
// A poll from an earlier start() still holds that start's fd.
if existing_poll.is_none() {
Expand Down Expand Up @@ -2654,7 +2647,6 @@ pub mod __parent_macro {
pub use ::bun_sys::Error as SysError;
#[cfg(windows)]
pub use ::bun_sys::windows::libuv::Loop as UvLoop;
pub use ::bun_uws_sys::Loop as UwsLoop;
}

/// Stamp `PosixStreamingWriterParent` + `WindowsWriterParent` +
Expand All @@ -2676,7 +2668,6 @@ macro_rules! impl_streaming_writer_parent {
on_ready = $on_ready:ident,
on_close = $on_close:ident,
event_loop = |$el_this:ident| $el:expr,
uws_loop = |$uws_this:ident| $uws:expr,
uv_loop = |$uv_this:ident| $uv:expr,
ref_ = |$ref_this:ident| $ref_:expr,
deref = |$deref_this:ident| $deref:expr,
Expand Down Expand Up @@ -2715,13 +2706,6 @@ macro_rules! impl_streaming_writer_parent {
#[allow(unused_unsafe)]
unsafe { $el }
}
#[inline]
unsafe fn loop_(this: *mut Self) -> *mut $crate::pipe_writer::__parent_macro::UwsLoop {
// SAFETY: see on_write. Shared-only read.
let $uws_this = this;
#[allow(unused_unsafe)]
unsafe { $uws }
}
}

#[cfg(windows)]
Expand Down
10 changes: 8 additions & 2 deletions src/io/keep_alive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ use crate::posix_event_loop::js_vm_ctx;
#[derive(Default)]
pub struct KeepAlive {
status: Status,
/// `ref_` counted on `Bun.spawnSync`'s isolated loop, not the thread's loop.
spawn_sync_loop: bool,
}
Comment thread
robobun marked this conversation as resolved.
Resolved

#[derive(Copy, Clone, PartialEq, Eq, Default)]
Expand Down Expand Up @@ -46,7 +48,7 @@ impl KeepAlive {
}
self.status = Status::Inactive;
#[cfg(not(windows))]
event_loop_ctx.loop_unref();
event_loop_ctx.loop_unref_for(self.spawn_sync_loop);
#[cfg(windows)]
event_loop_ctx.loop_sub_active(1);
}
Expand All @@ -56,6 +58,10 @@ impl KeepAlive {
if self.status != Status::Active {
return;
}
debug_assert!(
!self.spawn_sync_loop,
"deferred unref of a spawnSync loop ref"
);
self.status = Status::Inactive;
// vm.pending_unref_counter +|= 1;
#[cfg(not(windows))]
Expand All @@ -70,7 +76,7 @@ impl KeepAlive {
return;
}
self.status = Status::Active;
event_loop_ctx.loop_ref();
self.spawn_sync_loop = event_loop_ctx.loop_ref();
}

/// Allow a poll to keep the process alive.
Expand Down
95 changes: 44 additions & 51 deletions src/io/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -331,33 +331,18 @@
}

impl EventLoopCtx {
/// SAFETY: caller must not hold another live `&mut` to the same loop
/// across this borrow (resolver-style accessor; the loop is per-thread).
#[inline]
pub unsafe fn platform_event_loop(&self) -> &'static mut bun_uws_sys::Loop {
// Route through the single nonnull-asref accessor below; the `unsafe`
// on this fn's signature is the caller-side aliasing contract — the
// body itself needs no extra `unsafe`.
self.loop_mut()
}

// ── safe leaf wrappers (nonnull-asref) ──────────────────────────────
// The platform loop / poll store are per-thread, set-once back-pointers
// (`BackRef`-shaped). [`platform_event_loop`] cannot be a safe fn because
// handing out `&mut Loop` from `&self` would let a caller alias two
// copies; these wrappers instead perform one counter adjustment and drop
// the borrow before returning, so no `&mut` escapes. Collapses N
// identical `ctx.platform_event_loop().op()` call sites into the single
// deref inside [`loop_mut`].
// (`BackRef`-shaped). Handing out `&mut Loop` from `&self` would let a
// caller alias two copies; these wrappers instead perform one counter
// adjustment and drop the borrow before returning, so no `&mut` escapes.
Comment thread
robobun marked this conversation as resolved.
Resolved
//
// `loop_mut` is the single nonnull-asref accessor: `pub(crate)`,
// `loop_mut` is the single nonnull-asref accessor: crate-private,
// `&self → &mut` (so it must NOT be called twice with overlapping live
// results). Every in-crate caller is a leaf op — counter bump,
// `FilePoll::activate`/`deactivate`, `unregister` — that consumes the
// borrow before returning and never re-enters `EventLoopCtx`, so no two
// `&mut Loop` ever coexist. Widened from impl-private to crate-private so
// `posix_event_loop`/`windows_event_loop` route their N identical
// `ctx.platform_event_loop()` derefs through this single accessor.
// `&mut Loop` ever coexist.
#[inline]
fn loop_mut(&self) -> &'static mut bun_uws_sys::Loop {
// SAFETY: per-thread set-once pointer (the uws loop singleton); the
Expand Down Expand Up @@ -387,25 +372,53 @@
// SAFETY: per-thread scratch owned by the VM/Mini loop, which outlives every read.
unsafe { (*self.pipe_read_scratch()).claim() }
}
/// True while `Bun.spawnSync` has its isolated loop installed as the current loop.
#[inline]
pub(crate) fn loop_ref(&self) {
self.loop_mut().ref_();
pub(crate) fn is_spawn_sync_loop(&self) -> bool {
self.platform_event_loop_ptr() != bun_uws_sys::Loop::get()
}
/// The loop a counter was taken on: the isolated `spawnSync` loop when its
/// owner recorded `spawn_sync_loop` from [`Self::is_spawn_sync_loop`], else
/// the thread's loop, even while `spawnSync` has swapped the current loop.
Comment thread
robobun marked this conversation as resolved.
Resolved
#[inline]
#[cfg(not(windows))]
pub(crate) fn loop_unref(&self) {
self.loop_mut().unref();
fn loop_for(&self, spawn_sync_loop: bool) -> &'static mut bun_uws_sys::Loop {
if spawn_sync_loop {
debug_assert!(
self.is_spawn_sync_loop(),
"a spawnSync loop counter is released after spawnSync returned"
);
return self.loop_mut();
}
// SAFETY: the thread's loop; same contract as `loop_mut`.
unsafe { &mut *bun_uws_sys::Loop::get() }
}
/// Refs the current loop and returns the bit for [`Self::loop_unref_for`].
#[inline(never)] // `KeepAlive::ref_` is inlined into several hundred callers.
#[must_use]
pub fn loop_ref(&self) -> bool {
let spawn_sync_loop = self.is_spawn_sync_loop();
self.loop_mut().ref_();
spawn_sync_loop
}
/// Releases a [`Self::loop_ref`] that returned `spawn_sync_loop`.
#[inline(never)]
#[cfg(not(windows))]
pub fn loop_unref_for(&self, spawn_sync_loop: bool) {
self.loop_for(spawn_sync_loop).unref();
}
#[inline]
#[cfg(windows)]
pub(crate) fn loop_dec(&self) {
self.loop_mut().dec();
}
#[inline]
#[cfg(windows)]
pub(crate) fn loop_add_active(&self, n: u32) {
self.loop_mut().add_active(n);
}
#[inline]
#[cfg(windows)]
pub(crate) fn loop_sub_active(&self, n: u32) {
self.loop_mut().sub_active(n);
}
Expand All @@ -421,7 +434,7 @@
self.is(EventLoopCtxKind::Js)
}
#[inline]
pub fn loop_(&self) -> *mut bun_uws_sys::Loop {

Check warning on line 437 in src/io/lib.rs

View workflow job for this annotation

GitHub Actions / mordant

method `bun_io::EventLoopCtx::loop_` is public, but nothing in the workspace uses it
self.platform_event_loop_ptr()
}
/// Platform-native loop pointer (`us_loop_t*` / `uv_loop_t*`); see
Expand Down Expand Up @@ -1829,41 +1842,25 @@
pub(crate) fn deinit_force_unregister(self) {
self.inner().deinit_force_unregister();
}
/// Single nonnull-asref accessor for the process-global uWS loop pointer.
///
/// Type invariant (encapsulated `unsafe`): every caller of
/// [`unregister`](Self::unregister) / [`register_with_fd`](Self::register_with_fd)
/// passes `Loop::get()` (the per-thread uWS loop singleton), which is
/// non-null after init and lives for the program. The event loop is
/// single-threaded so the returned `&mut` is the sole live borrow at the
/// point of use. Collapses the two identical `&mut *loop_` deref blocks in
/// those wrappers into one.
#[inline(always)]
fn uws_loop_mut<'a>(loop_: *mut bun_uws_sys::Loop) -> &'a mut bun_uws_sys::Loop {
debug_assert!(!loop_.is_null());
// SAFETY: type invariant — see doc comment above.
unsafe { &mut *loop_ }
}
#[inline]
pub(crate) fn unregister(self, loop_: *mut bun_uws_sys::Loop, force: bool) -> sys::Result<()> {
let loop_ = Self::uws_loop_mut(loop_);
pub(crate) fn unregister(self, ev: EventLoopHandle, force: bool) -> sys::Result<()> {
#[cfg(not(windows))]
{
self.inner().unregister(loop_, force)
self.inner().unregister(ev, force)
}
#[cfg(windows)]
{
let _ = force;
// The windows `FilePoll::unregister` always returns true — the
// bool is vestigial, so there is no error path to surface here.
let _ = self.inner().unregister(loop_);
let _ = self.inner().unregister(ev.loop_mut());
Ok(())
}
}
#[inline]
pub(crate) fn register_with_fd(
self,
loop_: *mut bun_uws_sys::Loop,
ev: EventLoopHandle,
kind: FilePollKind,
fd: Fd,
) -> sys::Result<()> {
Expand All @@ -1873,16 +1870,12 @@
};
#[cfg(not(windows))]
{
self.inner().register_with_fd(
Self::uws_loop_mut(loop_),
flag,
OneShotFlag::Dispatch,
fd,
)
self.inner()
.register_with_fd(ev, flag, OneShotFlag::Dispatch, fd)
}
#[cfg(windows)]
{
let _ = (loop_, flag, fd);
let _ = (ev, flag, fd);
unreachable!("FilePoll fd registration is POSIX-only");
}
}
Expand Down
Loading
Loading