Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
28 changes: 17 additions & 11 deletions src/event_loop/EventLoopTimer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -205,18 +205,24 @@ pub enum Tag {
}

impl Tag {
/// Whether `jest.useFakeTimers()` captures this timer. Only the timers a
/// program schedules itself (`setTimeout`/`setInterval`,
/// `AbortSignal.timeout()`, and `Bun.cron`, which is documented as
/// mockable) go into the fake heap, where they fire on
/// `advanceTimersByTime()` and are dropped by `useRealTimers()` /
/// `clearAllTimers()`. Everything else is a runtime-internal timeout
/// (subprocess kill, connection timeouts, c-ares polling, ...) that keeps
/// running on the real clock, as in Jest.
///
/// This also decides which clock an owner arms with: a fakeable owner
/// computes its deadline with `TimespecMockMode::AllowMockedTime`; every
/// other owner must use `ForceRealTime`, because the real heap is drained
/// against the real clock (`timer::All::next`).
Comment thread
robobun marked this conversation as resolved.
Outdated
pub fn allow_fake_timers(self) -> bool {
match self {
Tag::WTFTimer // internal
| Tag::BunTest // for test timeouts
| Tag::EventLoopDelayMonitor // probably important
| Tag::StatWatcherScheduler
| Tag::GcRepeating // internal GC pacing
| Tag::QuicEndpoint
| Tag::DnsSdConnection // internal lookup pacing
=> false,
_ => true,
}
matches!(
self,
Tag::TimeoutObject | Tag::AbortSignalTimeout | Tag::CronJob
)
}
}

Expand Down
8 changes: 6 additions & 2 deletions src/event_loop/SpawnSyncEventLoop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -380,12 +380,16 @@ impl SpawnSyncEventLoop {

/// Tick the isolated event loop with an optional timeout
/// This is similar to the main event loop's tick but completely isolated
///
/// `timeout` is an absolute deadline on the real monotonic clock. Fake
/// timers cannot advance while the caller blocks in here, so the mocked
/// clock is never consulted.
Comment thread
robobun marked this conversation as resolved.
Outdated
pub fn tick_with_timeout(&mut self, timeout: Option<&Timespec>) -> TickState {
let duration_storage: Option<Timespec>;
let duration: Option<&Timespec> = match timeout {
Some(ts) => {
duration_storage =
Some(ts.duration(&Timespec::now(TimespecMockMode::AllowMockedTime)));
Some(ts.duration(&Timespec::now(TimespecMockMode::ForceRealTime)));
duration_storage.as_ref()
}
None => None,
Expand Down Expand Up @@ -454,7 +458,7 @@ impl SpawnSyncEventLoop {
#[cfg(not(windows))]
{
self.did_timeout.set(
Timespec::now(TimespecMockMode::AllowMockedTime).order(ts)
Timespec::now(TimespecMockMode::ForceRealTime).order(ts)
!= core::cmp::Ordering::Less,
);
}
Expand Down
14 changes: 10 additions & 4 deletions src/runtime/api/bun/js_bun_spawn_bindings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1657,8 +1657,7 @@ fn spawn_maybe_sync<const IS_SYNC: bool>(
// This must go before other things happen so that the exit handler is
// registered before onProcessExit can potentially be called.
if let Some(timeout_val) = timeout {
let ts =
Timespec::ms_from_now(TimespecMockMode::AllowMockedTime, i64::from(timeout_val));
let ts = Timespec::ms_from_now(TimespecMockMode::ForceRealTime, i64::from(timeout_val));
// Note: `EventLoopTimer.next` is a local-stub Timespec until
// `bun_event_loop` switches to `bun_core::Timespec`; copy fieldwise.
subprocess.event_loop_timer.with_mut(|t| {
Expand Down Expand Up @@ -1875,7 +1874,7 @@ fn spawn_maybe_sync<const IS_SYNC: bool>(
// This ensures JavaScript timers don't fire and stdin/stdout from the main process aren't affected
{
let mut absolute_timespec = Timespec::EPOCH;
let mut now = Timespec::now(TimespecMockMode::AllowMockedTime);
let mut now = Timespec::now(TimespecMockMode::ForceRealTime);
let mut user_timespec: Timespec = if let Some(timeout_ms) = timeout {
now.add_ms(i64::from(timeout_ms))
} else {
Expand All @@ -1889,8 +1888,15 @@ fn spawn_maybe_sync<const IS_SYNC: bool>(
if let Some(abort_signal_timeout) = signal.get_timeout() {
// Note: `AbortSignal::Timeout.event_loop_timer` uses the
// bun_event_loop-local `Timespec` stub; convert fieldwise.
//
// Under `jest.useFakeTimers()` the signal's timer sits in the
// fake heap with a deadline on the mocked clock, which cannot
// advance while this call blocks, so it can never fire here;
// only a real-heap deadline is comparable with `now`.
Comment thread
robobun marked this conversation as resolved.
Outdated
if abort_signal_timeout.event_loop_timer.state
== crate::timer::EventLoopTimerState::ACTIVE
&& abort_signal_timeout.event_loop_timer.in_heap
== crate::timer::InHeap::Regular
{
let next = &abort_signal_timeout.event_loop_timer.next;
let next_ts = Timespec {
Expand Down Expand Up @@ -1959,7 +1965,7 @@ fn spawn_maybe_sync<const IS_SYNC: bool>(
}) {
TickState::Completed => {}
TickState::Timeout => {
now = Timespec::now(TimespecMockMode::AllowMockedTime);
now = Timespec::now(TimespecMockMode::ForceRealTime);
let did_user_timeout = has_user_timespec
&& (absolute_timespec.eql(&user_timespec)
|| user_timespec.order(&now) == core::cmp::Ordering::Less);
Expand Down
13 changes: 13 additions & 0 deletions src/runtime/api/cron.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1640,6 +1640,19 @@ impl CronJob {
Self::remove_from_list(this, vm);
}

/// The fake timer heap dropped this job's timer (`jest.useRealTimers()` /
/// `jest.clearAllTimers()`), so it can never fire again: finish stopping
/// it like `stop()` would, instead of leaving it holding the event loop
/// open. The node is already unlinked, which `stop_internal` tolerates.
///
/// # Safety
/// `this` was recovered from a node just popped off the fake heap; a
/// scheduled job is kept alive by its JS wrapper, and no JS has run since
/// the pop.
Comment thread
robobun marked this conversation as resolved.
Outdated
pub(crate) unsafe fn stop_dropped_from_fake_heap(this: *mut Self) {
Self::self_stop(this, VirtualMachine::get());
}

fn self_stop(this: *mut Self, vm: &VirtualMachine) {
let this_ref = Self::from_ctx_ptr(this);
// While the callback is on the stack or its promise is pending, defer
Expand Down
2 changes: 1 addition & 1 deletion src/runtime/bake/dev_server/hmr_socket.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ impl HmrSocket {
// lives in `RuntimeState` (see jsc_hooks.rs).
let state = crate::jsc_hooks::runtime_state();
let next = bun_core::Timespec::ms_from_now(
bun_core::TimespecMockMode::AllowMockedTime,
bun_core::TimespecMockMode::ForceRealTime,
1000,
);
// SAFETY: `runtime_state()` is non-null after
Expand Down
2 changes: 1 addition & 1 deletion src/runtime/bake/dev_server/source_map_store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -541,7 +541,7 @@ impl SourceMapStore {
}

let expire = Timespec::ms_from_now(
TimespecMockMode::AllowMockedTime,
TimespecMockMode::ForceRealTime,
WEAK_REF_EXPIRY_SECONDS * 1000,
);
self.weak_refs
Expand Down
2 changes: 1 addition & 1 deletion src/runtime/dns_jsc/dns.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4132,7 +4132,7 @@ impl Resolver {
self.ref_();
let now_ts = now
.copied()
.unwrap_or_else(|| bun::timespec::now(bun::TimespecMockMode::AllowMockedTime));
.unwrap_or_else(|| bun::timespec::now(bun::TimespecMockMode::ForceRealTime));
let next = now_ts.add_ms(1000);
// `EventLoopTimer.next` uses the event-loop crate's local
// `Timespec` (distinct from `bun_core::Timespec`); convert by field.
Expand Down
2 changes: 1 addition & 1 deletion src/runtime/socket/UpgradedDuplex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -607,7 +607,7 @@ impl UpgradedDuplex {
// Note: `EventLoopTimer.next` is the lower-tier `ElTimespec` stub;
// bridge from `bun_core::Timespec` until the lower tier switches.
let next =
bun_core::Timespec::ms_from_now(bun_core::TimespecMockMode::AllowMockedTime, ms as i64);
bun_core::Timespec::ms_from_now(bun_core::TimespecMockMode::ForceRealTime, ms as i64);
self.event_loop_timer.with_mut(|t| {
t.next = ElTimespec {
sec: next.sec,
Expand Down
2 changes: 1 addition & 1 deletion src/runtime/socket/WindowsNamedPipe.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1023,7 +1023,7 @@ impl WindowsNamedPipe {
// reschedule the timer
// `EventLoopTimer.next` is the lower-tier `ElTimespec` stub;
// bridge from `bun_core::Timespec` until the lower tier switches.
let next = timespec::ms_from_now(bun_core::TimespecMockMode::AllowMockedTime, ms as i64);
let next = timespec::ms_from_now(bun_core::TimespecMockMode::ForceRealTime, ms as i64);
self.event_loop_timer.with_mut(|t| {
t.next = ElTimespec {
sec: next.sec,
Expand Down
29 changes: 27 additions & 2 deletions src/runtime/test_runner/timers/FakeTimers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use bun_threading::RwLock;
use bun_core::Environment;
use bun_core::Timespec;
use bun_jsc::{CallFrame, JSFunction, JSGlobalObject, JSHostFn, JSValue, JsResult};
use crate::api::cron::CronJob;
use crate::jsc::virtual_machine::VirtualMachine;
use crate::timer::{
AbortSignalTimeout, ElTimespec, EventLoopTimer, EventLoopTimerState, EventLoopTimerTag,
Expand Down Expand Up @@ -116,9 +117,14 @@ fn from_el_timespec(t: &ElTimespec) -> Timespec {
}

/// Owners of the nodes [`FakeTimers::clear`] popped, still to be told their
/// timer is gone. Released only once the `FakeTimers` borrow has ended: both
/// timer is gone. Released only once the `FakeTimers` borrow has ended: these
/// paths re-enter `timer::All` (`TimerObjectInternals::cancel` → `All::remove`,
/// `Timeout` deinit → `timer_remove`).
///
/// Every tag that [`EventLoopTimerTag::allow_fake_timers`] admits to the fake
/// heap needs an entry here: popping a node only unlinks it, and an owner that
/// is not told keeps believing it is armed (holding its event-loop ref, never
/// firing).
Comment thread
robobun marked this conversation as resolved.
Outdated
#[derive(Default)]
#[must_use]
struct ClearedTimers {
Expand All @@ -132,6 +138,9 @@ struct ClearedTimers {
/// pins an observed signal's wrapper for as long as that is set. Only the
/// signal's `cancelTimer()` clears it (and frees the box).
signal_timeouts: Vec<*mut AbortSignalTimeout>,
/// A `Bun.cron()` job holds the event loop open until it is stopped; with
/// its timer gone it would do so forever without ever firing.
Comment thread
robobun marked this conversation as resolved.
Outdated
cron_jobs: Vec<*mut CronJob>,
}

impl ClearedTimers {
Expand All @@ -145,6 +154,13 @@ impl ClearedTimers {
// ended before this call. `t` is freed by the call.
unsafe { AbortSignalTimeout::discard(t) };
}
for job in self.cron_jobs {
// SAFETY: `clear` popped `job`'s node from the fake heap, so the
// job was scheduled and its JS wrapper (strong while scheduled)
// keeps it alive; no JS has run since; the `FakeTimers` borrow
// ended before this call.
unsafe { CronJob::stop_dropped_from_fake_heap(job) };
}
}
}

Expand Down Expand Up @@ -197,7 +213,16 @@ impl FakeTimers {
.signal_timeouts
.push(AbortSignalTimeout::from_timer_ptr(timer));
}
_ => {}
EventLoopTimerTag::CronJob => {
cleared.cron_jobs.push(CronJob::from_timer_ptr(timer));
}
// `All::insert` only routes `allow_fake_timers()` tags here,
// and each of those has an arm above.
Comment thread
robobun marked this conversation as resolved.
Outdated
tag => debug_assert!(
false,
"{} timer in the fake heap has no release path",
<&'static str>::from(tag),
),
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/runtime/timer/Timer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ impl All {
vm,
// Be careful to avoid adding extra calls to bun.timespec.now()
// when it's not needed.
&Timespec::now(TimespecMockMode::AllowMockedTime),
&Timespec::now(TimespecMockMode::ForceRealTime),
);
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/runtime/timer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -400,7 +400,7 @@ impl DateHeaderTimer {
// separate allocation from `RuntimeState.timer` so no aliasing with
// `&mut self`).
let loop_ = vm.uws_loop_mut();
let now = Timespec::now(TimespecMockMode::AllowMockedTime);
let now = Timespec::now(TimespecMockMode::ForceRealTime);

// Record when we last ran it.
self.event_loop_timer.next = ElTimespec {
Expand Down
2 changes: 1 addition & 1 deletion src/runtime/valkey_jsc/js_valkey.rs
Original file line number Diff line number Diff line change
Expand Up @@ -410,7 +410,7 @@ impl RefCountedTimer {
return;
}
let now = bun_core::Timespec::ms_from_now(
bun_core::TimespecMockMode::AllowMockedTime,
bun_core::TimespecMockMode::ForceRealTime,
i64::from(ms),
);
self.event_loop_timer.with_mut(|t| {
Expand Down
4 changes: 2 additions & 2 deletions src/sql_jsc/mysql/JSMySQLConnection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ impl JSMySQLConnection {
}

self.timer.with_mut(|t| {
t.next = timespec::ms_from_now(TimespecMockMode::AllowMockedTime, interval.into());
t.next = timespec::ms_from_now(TimespecMockMode::ForceRealTime, interval.into());
});
// whole-struct provenance: the fire path recovers the container from this pointer.
let t = core::ptr::addr_of!(self.timer)
Expand Down Expand Up @@ -339,7 +339,7 @@ impl JSMySQLConnection {

self.max_lifetime_timer.with_mut(|t| {
t.next = timespec::ms_from_now(
TimespecMockMode::AllowMockedTime,
TimespecMockMode::ForceRealTime,
self.max_lifetime_interval_ms.into(),
);
});
Expand Down
4 changes: 2 additions & 2 deletions src/sql_jsc/postgres/PostgresSQLConnection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -390,7 +390,7 @@ impl PostgresSQLConnection {
return;
}
t.next = bun_core::Timespec::ms_from_now(
bun_core::TimespecMockMode::AllowMockedTime,
bun_core::TimespecMockMode::ForceRealTime,
i64::from(interval),
);
});
Expand Down Expand Up @@ -500,7 +500,7 @@ impl PostgresSQLConnection {
}
self.max_lifetime_timer.with_mut(|t| {
t.next = bun_core::Timespec::ms_from_now(
bun_core::TimespecMockMode::AllowMockedTime,
bun_core::TimespecMockMode::ForceRealTime,
i64::from(self.max_lifetime_interval_ms),
);
});
Expand Down
Loading