Summary
On v0.5.586 (and v0.5.572+), any outbound TCP via net.createConnection
panics on a tokio worker thread before any 'connect'/'error'/'close'
event fires. The script appears to hang (or exits 1 with the panic on
stderr if a top-level await is keeping the runtime alive).
The crate self-diagnoses the cause via an eprintln! left in
crates/perry-ext-net/src/lib.rs:359:
[perry-ext-net] BUG: spawn_socket_runner Handle::try_current returned Err — LTO has likely dead-stripped tokio's CONTEXT statics. This will panic on the subsequent Handle::current().
perry-ext-net is a separate crate from perry-stdlib; each links its
own copy of tokio. LTO sees that perry-ext-net's tokio CONTEXT
thread-locals aren't reachable from a static call graph (the runtime
context is entered over in perry-stdlib's tokio copy) and strips
them. When the spawned socket task on a worker thread reaches into
perry-ext-net's Handle::try_current(), it sees the stripped/null
CONTEXT and returns Err. The next Handle::current() panics with
"there is no reactor running, must be called from the context of a
Tokio 1.x runtime".
Net effect: outbound TCP via perry-ext-net is unusable, blocking any
program that opens a socket — drivers (@perryts/mongodb, postgres,
mysql), HTTP clients, fastify HTTP server in any direction that
back-fetches, etc.
Repro
net-lto.ts:
import * as net from 'net';
async function main() {
console.log('createConnection...');
const result = await new Promise<string>((resolve) => {
const s = net.createConnection(27017 as never, '127.0.0.1' as never) as {
on(ev: 'connect', cb: () => void): void;
on(ev: 'error', cb: (e: unknown) => void): void;
end(): void;
};
s.on('connect', () => { console.log('CONNECTED'); s.end(); resolve('ok'); });
s.on('error', (e) => { console.log('ERROR: ' + String(e)); resolve('err'); });
});
console.log('done: ' + result);
}
main().catch((e) => console.error('ERR:', e));
Runs against any reachable TCP target — 27017 here is a local mongod
but anything listening works equivalently. The remote being closed
would normally just produce an 'error' event; here it produces a
worker-thread panic instead.
Observed (perry 0.5.586)
Running net-lto...
createConnection...
[perry-ext-net] BUG: spawn_socket_runner Handle::try_current returned Err — \
LTO has likely dead-stripped tokio's CONTEXT statics. This will panic on \
the subsequent `Handle::current()`.
thread 'tokio-rt-worker' (5202840) panicked at crates/perry-ext-net/src/lib.rs:366:22:
there is no reactor running, must be called from the context of a Tokio 1.x runtime
Exit code 1. No JS-level event ever fires.
A pre-port build (v0.5.527, before perry-ext-net default-on at v0.5.573)
prints CONNECTED followed by done: ok and exits 0 on the same input.
Background — fixes already attempted
The maintainer has tried four passes under the #421 / #420 umbrella
tracking issues (Hono / Drizzle compile targets):
- v0.5.578 —
fix(perry-ext-net+ffi): tokio features + reactor
- v0.5.579 —
fix(perry-stdlib+ext-net): net pump registration + LTO black-box
- v0.5.580 —
fix(perry-ffi): alloc_buffer through extern js_buffer_alloc
- v0.5.581 —
fix(perry-stdlib+ext-net): HANDLE_METHOD_DISPATCH
The current source contains two std::hint::black_box(...) calls and an
eprintln of try_current()'s debug result specifically to keep the
CONTEXT static from being dead-stripped (lines 358 and 371). Despite
all three, LTO still strips the statics on a release build.
Likely-fruitful directions:
- Force perry-ext-net and perry-stdlib to share the same tokio
compilation unit (single tokio crate dependency + same feature flags
resolved through cargo's unification, no static-link duplication).
- Or have perry-ext-net obtain its handle from a perry-ffi-exported
function that lives in perry-stdlib's compilation unit, instead of
calling Handle::current() from its own (LTO-stripped) tokio copy.
- Or mark the CONTEXT-touching statics with
#[used] / linker
retention attributes so LTO is forbidden from stripping them.
Environment
- perry 0.5.586 (built from
main, today)
- macOS 26.4 (arm64)
- Reference baseline: v0.5.527 (pre-perry-ext-net) — same script connects
and exits 0.
Where this came from
@perryts/mongodb benchmark cell on Perry-AOT — bench's first
MongoClient.connect(...) triggers the panic before the hello round-trip
completes, so the bench can't run on v0.5.572+. Filed alongside
#471 (which can only be
debugged on a perry build where outbound TCP works).
Summary
On v0.5.586 (and v0.5.572+), any outbound TCP via
net.createConnectionpanics on a tokio worker thread before any
'connect'/'error'/'close'event fires. The script appears to hang (or exits 1 with the panic on
stderr if a top-level
awaitis keeping the runtime alive).The crate self-diagnoses the cause via an
eprintln!left incrates/perry-ext-net/src/lib.rs:359:perry-ext-netis a separate crate fromperry-stdlib; each links itsown copy of tokio. LTO sees that
perry-ext-net's tokio CONTEXTthread-locals aren't reachable from a static call graph (the runtime
context is entered over in
perry-stdlib's tokio copy) and stripsthem. When the spawned socket task on a worker thread reaches into
perry-ext-net'sHandle::try_current(), it sees the stripped/nullCONTEXT and returns
Err. The nextHandle::current()panics with"there is no reactor running, must be called from the context of a
Tokio 1.x runtime".
Net effect: outbound TCP via
perry-ext-netis unusable, blocking anyprogram that opens a socket — drivers (
@perryts/mongodb, postgres,mysql), HTTP clients, fastify HTTP server in any direction that
back-fetches, etc.
Repro
net-lto.ts:Runs against any reachable TCP target —
27017here is a local mongodbut anything listening works equivalently. The remote being closed
would normally just produce an
'error'event; here it produces aworker-thread panic instead.
Observed (perry 0.5.586)
Exit code 1. No JS-level event ever fires.
A pre-port build (v0.5.527, before perry-ext-net default-on at v0.5.573)
prints
CONNECTEDfollowed bydone: okand exits 0 on the same input.Background — fixes already attempted
The maintainer has tried four passes under the
#421/#420umbrellatracking issues (Hono / Drizzle compile targets):
fix(perry-ext-net+ffi): tokio features + reactorfix(perry-stdlib+ext-net): net pump registration + LTO black-boxfix(perry-ffi): alloc_buffer through extern js_buffer_allocfix(perry-stdlib+ext-net): HANDLE_METHOD_DISPATCHThe current source contains two
std::hint::black_box(...)calls and aneprintln of
try_current()'s debug result specifically to keep theCONTEXT static from being dead-stripped (lines 358 and 371). Despite
all three, LTO still strips the statics on a release build.
Likely-fruitful directions:
compilation unit (single tokio crate dependency + same feature flags
resolved through cargo's unification, no static-link duplication).
function that lives in perry-stdlib's compilation unit, instead of
calling
Handle::current()from its own (LTO-stripped) tokio copy.#[used]/ linkerretention attributes so LTO is forbidden from stripping them.
Environment
main, today)and exits 0.
Where this came from
@perryts/mongodbbenchmark cell on Perry-AOT — bench's firstMongoClient.connect(...)triggers the panic before the hello round-tripcompletes, so the bench can't run on v0.5.572+. Filed alongside
#471 (which can only be
debugged on a perry build where outbound TCP works).