Skip to content
Merged
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
14 changes: 14 additions & 0 deletions crates/perry-ext-http-server/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1521,6 +1521,20 @@ pub extern "C" fn js_node_http_server_process_pending() -> i32 {
}
count += crate::http2_server::process_pending_h2_events();

// #5010 — drain perry-ext-net's own pending-event queue. A raw
// `'upgrade'` (#4973) hands the listener a real `net.Socket` adopted into
// perry-ext-net (`adopt_upgraded_tcp_stream`); when user code destroys it,
// the socket task queues a `Close` event in perry-ext-net's queue. For an
// http-only program perry-stdlib runs with its OWN bundled net (so its
// `external-net-pump` arm is OFF and never touches ext-net's queue), and
// the perry-ext-net aux pump proved unreliable across workspace link
// layouts. The http-server pump, by contrast, runs every tick
// (external-http-server-pump) and directly depends on perry-ext-net, so
// draining here — through the UNIQUE `js_ext_net_drain_pending` symbol
// (no stdlib twin) — reliably empties that queue so the destroyed upgrade
// socket stops pinning the event loop. Cheap (one mutex peek) when empty.
count += unsafe { perry_ext_net::js_ext_net_drain_pending() };

count
}

Expand Down
14 changes: 12 additions & 2 deletions crates/perry-ext-net/src/dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,12 @@ extern "C" {
}

extern "C" fn process_pending_aux() -> i32 {
unsafe { crate::js_net_process_pending() }
// Drain via the DISTINCT `js_ext_net_drain_pending` symbol — NOT the
// `js_net_process_pending` extern, whose symbol the bundled stdlib net
// twin shadows in a workspace build (that twin drains stdlib's queue,
// leaving ext-net's own adopted-socket events — e.g. the raw-`'upgrade'`
// `Close` — stuck and the loop pinned). #5010.
unsafe { crate::js_ext_net_drain_pending() }
}

pub(crate) fn ensure_runtime_dispatch_registered() {
Expand Down Expand Up @@ -186,7 +191,12 @@ unsafe fn socket_method(handle: i64, method: &str, args: &[f64]) -> Option<f64>
undefined()
}
"destroy" | "destroySoon" => {
crate::js_net_socket_destroy(handle);
// Drive teardown through the DISTINCT `js_ext_net_destroy_socket`
// symbol — NOT the `js_net_socket_destroy` extern, whose symbol the
// bundled stdlib net twin shadows in a workspace build (it would
// mark the socket destroyed in stdlib's registry, leaving the
// adopted raw-`'upgrade'` socket alive in ext-net's). #5010.
crate::js_ext_net_destroy_socket(handle);
undefined()
}
"on" | "addListener" if args.len() >= 2 => {
Expand Down
21 changes: 21 additions & 0 deletions crates/perry-ext-net/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1499,6 +1499,27 @@ pub unsafe extern "C" fn js_net_socket_upgrade_tls(
/// capacity retained → zero steady-state allocation).
#[no_mangle]
pub unsafe extern "C" fn js_net_process_pending() -> i32 {
js_ext_net_drain_pending()
}

/// Drain ext-net's own pending-event queue.
///
/// This carries a DISTINCT `#[no_mangle]` symbol (`js_ext_net_drain_pending`),
/// deliberately NOT the `js_net_process_pending` name that the bundled stdlib
/// net ALSO exports. In a workspace/auto-optimize build both crates are
/// linked, so `js_net_process_pending` is a duplicate symbol; the link binds
/// every reference to whichever twin wins (stdlib's). The aux pump
/// (`process_pending_aux`) and the extern wrapper above therefore call THIS
/// uniquely-named entry point instead — a symbol with no twin and nothing to
/// fold against — so the adopted raw-`'upgrade'` socket's `Close` event in
/// ext-net's own queue is actually drained rather than left to pin the event
/// loop forever. Without this the loop hung, and the behavior flipped with
/// unrelated code-size changes (link-order roulette). (#5010)
///
/// # Safety
/// Fires user JS closures (listeners); callers must hold a valid runtime.
#[no_mangle]
pub unsafe extern "C" fn js_ext_net_drain_pending() -> i32 {
thread_local! {
static SCRATCH: std::cell::RefCell<Vec<PendingNetEvent>> =
const { std::cell::RefCell::new(Vec::new()) };
Expand Down
17 changes: 17 additions & 0 deletions crates/perry-ext-net/src/lifecycle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,23 @@ pub unsafe extern "C" fn js_net_socket_end(handle: i64, chunk_bits: i64) {
/// `handle` must be a registered socket id (raw, NOT NaN-boxed).
#[no_mangle]
pub unsafe extern "C" fn js_net_socket_destroy(handle: i64) {
js_ext_net_destroy_socket(handle);
}

/// Destroy an `ext-net` socket by id, operating directly on this crate's
/// socket registry.
///
/// This carries a DISTINCT `#[no_mangle]` symbol (`js_ext_net_destroy_socket`),
/// deliberately NOT the `js_net_socket_destroy` name that the bundled stdlib
/// net ALSO exports. In a workspace/auto-optimize build both are linked, so
/// `js_net_socket_destroy` is a duplicate symbol bound to whichever twin
/// wins (stdlib's). The handle-dispatch `socket_method` "destroy" arm and the
/// extern wrapper above call THIS uniquely-named entry point instead — a
/// symbol with no twin — so an adopted raw-`'upgrade'` socket is actually
/// marked destroyed in ext-net's own registry rather than in stdlib's empty
/// one, which is what let the event loop drain. (#5010)
#[no_mangle]
pub extern "C" fn js_ext_net_destroy_socket(handle: i64) {
let mut sockets = statics::sockets().lock().unwrap();
if let Some(s) = sockets.get_mut(&handle) {
s.destroyed = true;
Expand Down
Loading