Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
24 changes: 23 additions & 1 deletion src/jsc/VirtualMachine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,10 @@ pub struct VirtualMachine {
pub pending_internal_promise_is_protected: bool,
pub pending_internal_promise_reported_at: u32,
pub hot_reload_deferred: bool,
/// `hot_reload_counter` value at the last `--hot` orphan sweep. Gates
/// [`Self::report_exception_in_hot_reloaded_module_if_needed`] so each
/// generation sweeps at most once.
pub hot_reload_orphan_swept_at: u32,
pub entry_point_result: EntryPointResult,

pub auto_install_dependencies: bool,
Expand Down Expand Up @@ -1765,6 +1769,12 @@ pub struct RuntimeHooks {
/// the `.reload` mode preserves the next-fire schedule across the new
/// global so timers re-register instead of being torn down.
pub cron_clear_all_reload: fn(vm: &mut VirtualMachine),
/// Stop every `Bun.serve` instance the current `--hot` generation did not
/// re-adopt. The server types live in `bun_runtime` (forward-dep), so the
/// body is hoisted to the high tier; the low-tier caller is
/// [`VirtualMachine::report_exception_in_hot_reloaded_module_if_needed`],
/// once the reloaded entry-point promise fulfills.
pub hot_stop_orphaned_servers: fn(vm: &mut VirtualMachine),
/// Standalone-graph sourcemap load.
/// The concrete `bun_standalone_graph::Graph` / `File` / `LazySourceMap`
/// live above `bun_jsc`; the high tier reaches them via the graph's own
Expand Down Expand Up @@ -3385,7 +3395,19 @@ impl VirtualMachine {
crate::JSPromise::opaque_mut(promise).set_handled();
}
}
crate::js_promise::Status::Fulfilled => {}
crate::js_promise::Status::Fulfilled => {
// The reloaded module finished evaluating: any `Bun.serve` a
// previous `--hot` generation registered but this one did not
// adopt is an orphan. Sweep at most once per generation.
if self.hot_reload == HOT_RELOAD_HOT
&& self.hot_reload_orphan_swept_at != self.hot_reload_counter
{
self.hot_reload_orphan_swept_at = self.hot_reload_counter;
if let Some(hooks) = runtime_hooks() {
(hooks.hot_stop_orphaned_servers)(self);
}
}
}
}

if self.hot_reload_deferred {
Expand Down
24 changes: 24 additions & 0 deletions src/jsc/rare_data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,12 +63,17 @@ pub struct HotMap {
pub struct HotMapEntry {
pub tag: u8,
pub ptr: *mut (),
/// `vm.hot_reload_counter` at the time this entry was last inserted or
/// adopted by `Bun.serve`. An entry whose generation is older than the
/// current counter was not re-adopted by the latest module generation.
pub generation: u32,
}
impl Default for HotMapEntry {
fn default() -> Self {
Self {
tag: 0,
ptr: core::ptr::null_mut(),
generation: 0,
}
}
}
Expand Down Expand Up @@ -107,6 +112,25 @@ impl HotMap {
debug_assert!(!is_same_slice);
self._map.swap_remove(key);
}

/// Stamp the entry at `key` (if any) with `generation`.
pub fn touch(&mut self, key: &[u8], generation: u32) {
if let Some(v) = self._map.get_mut(key) {
v.generation = generation;
}
}

/// Copy out every entry whose `generation` is older than `current`. The
/// entries themselves are left in place; callers remove them via the
/// object's own teardown path (which calls [`HotMap::remove`]).
pub fn collect_stale(&self, current: u32) -> Vec<HotMapEntry> {
self._map
.values()
.iter()
.filter(|v| v.generation < current)
.copied()
.collect()
}
}

// ──────────────────────────────────────────────────────────────────────────
Expand Down
9 changes: 8 additions & 1 deletion src/runtime/api/BunObject.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1601,12 +1601,16 @@ pub(crate) fn serve(global_object: &JSGlobalObject, callframe: &CallFrame) -> Js
use bun_jsc::rare_data::HotMapEntry;

if config.allow_hot {
let generation = vm.hot_reload_counter;
if let Some(hot) = vm.hot_map() {
if config.id.is_empty() {
config.id = config.compute_id().into();
}

if let Some(entry) = hot.get_entry(&config.id) {
// Mark the entry as adopted by the current module generation
// so the post-reload orphan sweep leaves it running.
hot.touch(&config.id, generation);
macro_rules! reload {
($T:ty) => {{
// SAFETY: tag was matched; ptr was inserted as `*mut $T` below.
Expand Down Expand Up @@ -1670,12 +1674,15 @@ pub(crate) fn serve(global_object: &JSGlobalObject, callframe: &CallFrame) -> Js
if server_ref.config.allow_hot {
// SAFETY: same VM pointer; re-borrow after the earlier `vm` mut
// borrow was released by the `hot_map()` arm above.
if let Some(hot) = global_object.bun_vm().as_mut().hot_map() {
let vm = global_object.bun_vm().as_mut();
let generation = vm.hot_reload_counter;
if let Some(hot) = vm.hot_map() {
hot.insert_raw(
&server_ref.config.id,
HotMapEntry {
tag: $tag as u8,
ptr: server.cast::<()>(),
generation,
},
);
}
Expand Down
33 changes: 33 additions & 0 deletions src/runtime/jsc_hooks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1411,6 +1411,7 @@ pub(crate) static __BUN_RUNTIME_HOOKS: RuntimeHooks = RuntimeHooks {
parse_worker_exec_argv_allow_addons,
cron_clear_all_teardown,
cron_clear_all_reload,
hot_stop_orphaned_servers,
terminate_all_workers_and_wait,
retroactively_report_discovered_tests,
cancel_all_timers,
Expand Down Expand Up @@ -1505,6 +1506,38 @@ fn cron_clear_all_reload(vm: &mut VirtualMachine) {
CronJob::clear_all_for_vm::<{ ClearMode::Reload }>(vm);
}

/// Stop every `Bun.serve` instance registered by a previous `--hot` generation
/// that the current generation did not adopt (by calling `Bun.serve` again
/// with the same computed id). Entries are stale when their stamped
/// `generation` predates `vm.hot_reload_counter`; inserting and adopting both
/// stamp the current counter (see `crate::api::bun_object::serve`).
fn hot_stop_orphaned_servers(vm: &mut VirtualMachine) {
use crate::server::{AnyServer, AnyServerTag};
let current = vm.hot_reload_counter;
// Snapshot the stale entries first: `NewServer::stop` removes its own key
// from the hot map, which would invalidate an in-place iterator.
let stale = match vm.hot_map() {
Some(hot) => hot.collect_stale(current),
None => return,
};
for entry in stale {
let tag = match entry.tag {
t if t == AnyServerTag::HTTPServer as u8 => AnyServerTag::HTTPServer,
t if t == AnyServerTag::HTTPSServer as u8 => AnyServerTag::HTTPSServer,
t if t == AnyServerTag::DebugHTTPServer as u8 => AnyServerTag::DebugHTTPServer,
t if t == AnyServerTag::DebugHTTPSServer as u8 => AnyServerTag::DebugHTTPSServer,
_ => continue,
};
let mut server = AnyServer {
tag,
ptr: entry.ptr,
};
// Graceful stop: close the listener so in-flight requests on the old
// handler can drain, matching `server.stop()` from JS.
server.stop(false);
}
}

/// `webcore.WebWorker.terminateAllAndWait(timeout_ms)` —
/// forwards to the in-crate `bun_jsc::web_worker`
/// implementation; routed through `RuntimeHooks` because `virtual_machine.rs`
Expand Down
Loading
Loading