rusty_racer 0.2.1 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +80 -1
- data/ext/rusty_racer/Cargo.toml +1 -1
- data/ext/rusty_racer/src/lib.rs +239 -39
- data/lib/rusty_racer/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9a799692e0bcc6c18bbba590786cd92b0f3cd5c05c65be4da085ec3a739ae2ee
|
|
4
|
+
data.tar.gz: 0ca41e25987f9f721437abbfc1278c43d7aa688c996a1824cb342d4b1672ed70
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c63a41df38fbacdfc440a4bb61ff8f780315d2ce267a55d698505a7601557ba76a3762896dd12a4989aaf34d27ba2a7eb9c751a7249e7a71ae572a37d5af6133
|
|
7
|
+
data.tar.gz: e8deb0c3a86b25577996ee84d15f2163f6a2a8eab952d3c8592e4513f6e2cb483f3734ec7947dff22bad19cea9c5239284edd910473058baea5761921c510b8f
|
data/README.md
CHANGED
|
@@ -258,7 +258,12 @@ to one native thread (rusty_v8 exposes no `v8::Locker`), so using it from anothe
|
|
|
258
258
|
thread raises `RustyRacer::WrongThreadError` rather than corrupting the VM.
|
|
259
259
|
|
|
260
260
|
- **`Isolate#terminate` is the one exception** — it is safe to call from any
|
|
261
|
-
thread (it stops a runaway script on the owner thread).
|
|
261
|
+
thread (it stops a runaway script on the owner thread). It stops JavaScript at
|
|
262
|
+
V8's next interrupt check, which a runaway script reaches almost immediately
|
|
263
|
+
and a *short* one may never reach: terminating from inside a host function that
|
|
264
|
+
then returns to a script which simply finishes lets that script finish, and the
|
|
265
|
+
request is dropped rather than carried into the next operation. Use it to stop
|
|
266
|
+
code that is running away, not as a way to fail the call you are inside.
|
|
262
267
|
- **Dispose on the owner thread.** `Isolate#dispose` must run on the owner
|
|
263
268
|
thread. If the last reference to an isolate is instead garbage-collected on a
|
|
264
269
|
*different* thread (e.g. its owner thread already exited), it cannot be
|
|
@@ -290,6 +295,80 @@ process on the next GC or thrown exception. So **don't call into an isolate from
|
|
|
290
295
|
inside a Fiber on a worker thread**; drive isolate ops directly on the thread, or
|
|
291
296
|
keep Fiber/Enumerator-mediated JS calls on the main thread.
|
|
292
297
|
|
|
298
|
+
The other limit is on *where* a Fiber may switch. Operations on one isolate have
|
|
299
|
+
to nest: everything V8 keeps per isolate — the frame chain, the handle scopes,
|
|
300
|
+
the description of the stack — unwinds in the reverse of the order it was
|
|
301
|
+
entered. Resuming a Fiber from inside a host function keeps that (the outer
|
|
302
|
+
operation is simply blocked until the inner one returns, which is why
|
|
303
|
+
`Capybara::Result#find` inside a callback works), but **yielding out of a host
|
|
304
|
+
function** does not: it strands that operation mid-flight. That covers
|
|
305
|
+
`Fiber.yield` and `Fiber#transfer`, an `Enumerator::Yielder#<<` from inside a
|
|
306
|
+
callback, and any blocking call made under a Fiber scheduler (`async`, `falcon`),
|
|
307
|
+
which yields out of the Fiber for you — the last of those without a `Fiber`
|
|
308
|
+
keyword anywhere in your code.
|
|
309
|
+
|
|
310
|
+
What happens next depends on what the stranded operation's Fiber does:
|
|
311
|
+
|
|
312
|
+
- **Resumed while another operation is still running** — the two can now only
|
|
313
|
+
finish in the order they started, which V8 has no room for. There is no
|
|
314
|
+
exception to raise: raising *is* that unwind. So the binding prints what
|
|
315
|
+
happened, which host function did it, and where — then stops the process with
|
|
316
|
+
`SIGABRT` (exit 134). It is not rescuable and there is no opt-out; on a
|
|
317
|
+
threaded server that takes the whole worker with it.
|
|
318
|
+
|
|
319
|
+
```
|
|
320
|
+
[rusty_racer] fatal: operations on this isolate stopped nesting.
|
|
321
|
+
|
|
322
|
+
The host function `hop` handed control to Ruby.
|
|
323
|
+
By the time it came back, the operations in flight on this isolate had
|
|
324
|
+
changed (now 2, was 1) — so they no longer nest. …
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
- **Resumed only after the other operation has finished** — memory-safe, and it
|
|
328
|
+
keeps working. But note that *while* an operation is stranded, every other
|
|
329
|
+
operation on that isolate runs as a **nested** one, and only an outermost
|
|
330
|
+
operation drains microtasks. So a promise queued in the meantime does not
|
|
331
|
+
settle until the stranded operation finishes. **This is the shape a Fiber
|
|
332
|
+
scheduler produces**: it resumes the stranded Fiber once the other fiber's
|
|
333
|
+
operation is done, so `async`/`falcon` get the quiet variant, not the abort.
|
|
334
|
+
|
|
335
|
+
- **Never resumed** — Ruby frees an unresumed Fiber's stack without unwinding it,
|
|
336
|
+
so the operation's frames vanish while the isolate is still entered by them.
|
|
337
|
+
The isolate cannot be disposed after that; it is leaked, with a warning saying
|
|
338
|
+
why. If you still hold the Fiber, **`Fiber#kill` recovers it**: it resumes the
|
|
339
|
+
Fiber to unwind it, so the stranded operation finishes in order and the isolate
|
|
340
|
+
goes back to normal. The operation itself fails on the way out — see below for
|
|
341
|
+
why a kill reaches it as an ordinary error.
|
|
342
|
+
|
|
343
|
+
### Killing a thread or Fiber inside a host function
|
|
344
|
+
|
|
345
|
+
**A `Thread#kill` or `Fiber#kill` that lands while a host function's Ruby is
|
|
346
|
+
running does not kill it.** It surfaces as `RustyRacer::RuntimeError: Error:
|
|
347
|
+
Fatal` from the `eval`/`call` that was in flight, and the thread keeps going —
|
|
348
|
+
and CRuby has already marked that thread as killed, so **a second `Thread#kill`
|
|
349
|
+
is a no-op**. `Thread#raise` still works, and is the way out if you have to
|
|
350
|
+
reach in from outside.
|
|
351
|
+
|
|
352
|
+
A kill inside a **module resolver** is quieter still: the specifier it was
|
|
353
|
+
resolving simply fails to link, so the error names the import
|
|
354
|
+
(`failed to resolve module specifier "./dep.js" imported from /app.js`) with no
|
|
355
|
+
mention of a kill at all.
|
|
356
|
+
|
|
357
|
+
This is not a choice, it is the boundary. Ruby unwinds a kill with `TAG_FATAL`,
|
|
358
|
+
which arrives while V8's C++ frames are on the stack — and a `longjmp` through
|
|
359
|
+
those is not survivable, so the callback has to catch it like any other error.
|
|
360
|
+
Nor can it be put back afterwards: `rb_jump_tag` does not carry the unwind with
|
|
361
|
+
it, it re-enters whatever the VM still holds in `$!`, and anything protected
|
|
362
|
+
that ran in between (one more host function raising is enough) has cleared it.
|
|
363
|
+
Jumping into that is a segfault in the interpreter rather than a late kill. 0.2.2
|
|
364
|
+
shipped an attempt at this and had to be withdrawn in 0.2.3.
|
|
365
|
+
|
|
366
|
+
So: if you need to stop work that calls into an isolate, **stop it at a Ruby
|
|
367
|
+
boundary you control** — a flag the host function checks, a `Queue` it reads —
|
|
368
|
+
rather than by killing the thread from outside. `Isolate#terminate` stops the
|
|
369
|
+
*JavaScript* (at V8's next interrupt check, see above), which is the other half
|
|
370
|
+
of the same job.
|
|
371
|
+
|
|
293
372
|
## Installation
|
|
294
373
|
|
|
295
374
|
Precompiled gems bundle V8 — no V8 build, no Rust toolchain — for Ruby 3.3, 3.4,
|
data/ext/rusty_racer/Cargo.toml
CHANGED
data/ext/rusty_racer/src/lib.rs
CHANGED
|
@@ -122,11 +122,14 @@ macro_rules! istate {
|
|
|
122
122
|
pub(crate) use istate;
|
|
123
123
|
|
|
124
124
|
// One attach()'d host fn: the realm it was attached into — so resetting or
|
|
125
|
-
// disposing that realm can release the GC root —
|
|
126
|
-
//
|
|
125
|
+
// disposing that realm can release the GC root — the rooted proc itself (None
|
|
126
|
+
// once released; the slot index stays valid as a host_fn_id), and the name it
|
|
127
|
+
// was attached under, kept only so a diagnostic can say which host function is
|
|
128
|
+
// involved (one String per attach, never per call).
|
|
127
129
|
struct ProcSlot {
|
|
128
130
|
context_id: i32,
|
|
129
131
|
proc: Option<RootedProc>,
|
|
132
|
+
name: String,
|
|
130
133
|
}
|
|
131
134
|
|
|
132
135
|
// The isolate's host-fn registry, indexed by host_fn_id. `free` lists slots
|
|
@@ -282,6 +285,39 @@ where
|
|
|
282
285
|
job.r.unwrap()
|
|
283
286
|
}
|
|
284
287
|
|
|
288
|
+
// What handed control to Ruby. Carried through with_gvl for one reason: when
|
|
289
|
+
// the op nesting breaks, the diagnostic can point at the caller's own code
|
|
290
|
+
// instead of saying "a host function". Rendered only on that path, so an
|
|
291
|
+
// ordinary callback pays for a pointer and nothing else.
|
|
292
|
+
enum RubyCall<'a> {
|
|
293
|
+
// An attach()'d host function, by host_fn_id — the name it was attached
|
|
294
|
+
// under lives in the isolate's proc table.
|
|
295
|
+
HostFn(usize),
|
|
296
|
+
// A module resolver (a static instantiate block, a dynamic auto-link, or
|
|
297
|
+
// import()), named by the specifier it was handed.
|
|
298
|
+
Resolver(&'a str),
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
impl RubyCall<'_> {
|
|
302
|
+
fn describe(&self, core: &Core) -> String {
|
|
303
|
+
match self {
|
|
304
|
+
// try_lock, not lock: nothing holds this across a callback (call_proc
|
|
305
|
+
// takes the proc out under the lock and releases it before calling),
|
|
306
|
+
// but blocking here would turn a diagnostic into a hang.
|
|
307
|
+
Self::HostFn(id) => match core
|
|
308
|
+
.procs
|
|
309
|
+
.try_lock()
|
|
310
|
+
.ok()
|
|
311
|
+
.and_then(|procs| procs.slots.get(*id).map(|slot| slot.name.clone()))
|
|
312
|
+
{
|
|
313
|
+
Some(name) => format!("The host function `{name}`"),
|
|
314
|
+
None => "A host function".to_string(),
|
|
315
|
+
},
|
|
316
|
+
Self::Resolver(spec) => format!("The module resolver, resolving `{spec}`,"),
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
285
321
|
// The inverse trampoline: REACQUIRE the GVL to run |f| (a Ruby callback), then
|
|
286
322
|
// release it again. Called from inside a V8 host callback / module resolver,
|
|
287
323
|
// which runs GVL-released under the in-thread runner's `without_gvl`; the proc
|
|
@@ -289,20 +325,41 @@ where
|
|
|
289
325
|
// the proc re-enters the runner, releasing the GVL again) is sound. |f| must
|
|
290
326
|
// NOT let a Ruby exception escape as a longjmp — the callers convert proc
|
|
291
327
|
// errors to a Result value and throw on the JS side instead.
|
|
292
|
-
|
|
328
|
+
//
|
|
329
|
+
// This is also the only place where control can leave an operation WITHOUT it
|
|
330
|
+
// finishing: the Ruby we hand it to may switch Fibers, and a yield out of a
|
|
331
|
+
// callback strands this op mid-flight while the resumer goes on to use the
|
|
332
|
+
// isolate again. Everything V8 keeps per isolate nests strictly — the frame
|
|
333
|
+
// chain, the handle scopes, the stack description — so `core.depth` (the ops in
|
|
334
|
+
// flight on this isolate) coming back unchanged is exactly the condition that
|
|
335
|
+
// they still do: every op started while Ruby held control has also finished.
|
|
336
|
+
// Two atomic loads per callback, and a mismatch is fatal — see
|
|
337
|
+
// fatal_nesting_broken. Both loads and the check live INSIDE the trampoline, so
|
|
338
|
+
// they run with the GVL held and on the stranded op's own stack, which is what
|
|
339
|
+
// lets the diagnostic there read a Ruby backtrace without taking the GVL again.
|
|
340
|
+
fn with_gvl<F, R>(core: &Core, call: RubyCall<'_>, f: F) -> R
|
|
293
341
|
where
|
|
294
342
|
F: FnOnce() -> R,
|
|
295
343
|
{
|
|
296
|
-
struct Job<F, R> {
|
|
344
|
+
struct Job<'a, F, R> {
|
|
345
|
+
core: &'a Core,
|
|
346
|
+
call: RubyCall<'a>,
|
|
297
347
|
f: Option<F>,
|
|
298
348
|
r: Option<R>,
|
|
299
349
|
}
|
|
300
350
|
unsafe extern "C" fn run<F: FnOnce() -> R, R>(data: *mut c_void) -> *mut c_void {
|
|
301
351
|
let job = unsafe { &mut *(data as *mut Job<F, R>) };
|
|
352
|
+
let entry_depth = job.core.depth.load(Ordering::SeqCst);
|
|
302
353
|
job.r = Some((job.f.take().unwrap())());
|
|
354
|
+
let exit_depth = job.core.depth.load(Ordering::SeqCst);
|
|
355
|
+
if exit_depth != entry_depth {
|
|
356
|
+
fatal_nesting_broken(job.core, &job.call, entry_depth, exit_depth);
|
|
357
|
+
}
|
|
303
358
|
null_mut()
|
|
304
359
|
}
|
|
305
360
|
let mut job = Job::<F, R> {
|
|
361
|
+
core,
|
|
362
|
+
call,
|
|
306
363
|
f: Some(f),
|
|
307
364
|
r: None,
|
|
308
365
|
};
|
|
@@ -312,6 +369,101 @@ where
|
|
|
312
369
|
job.r.unwrap()
|
|
313
370
|
}
|
|
314
371
|
|
|
372
|
+
// Ruby handed control back to a callback whose operation is no longer the
|
|
373
|
+
// innermost one on its isolate: the Ruby code switched Fibers, and an operation
|
|
374
|
+
// that started later is still running. The two would now have to unwind in the
|
|
375
|
+
// order they were STARTED rather than the reverse, and V8 has no room for that —
|
|
376
|
+
// finishing this one pops its HandleScope out from under the other's live
|
|
377
|
+
// handles, restores a frame chain that still has the other's frames linked into
|
|
378
|
+
// it, and (at depth 0) exits an isolate the other is still running in.
|
|
379
|
+
//
|
|
380
|
+
// None of the gentler endings exist. Reporting it as a Ruby exception IS that
|
|
381
|
+
// unwind, so raising is the corruption, not a report of it. Waiting for the
|
|
382
|
+
// other operation deadlocks: it is blocked on the Fiber this one is standing in.
|
|
383
|
+
// And refusing the second operation before it started was never possible either
|
|
384
|
+
// — "a Fiber resumed from inside a callback" (supported, and common: every
|
|
385
|
+
// Enumerator is a Fiber) and "a Fiber that yielded out of one" leave Ruby in the
|
|
386
|
+
// same observable state, and CRuby exposes no way to tell a resuming Fiber from
|
|
387
|
+
// a suspended one. The difference only shows up here, when control comes back.
|
|
388
|
+
//
|
|
389
|
+
// So the process stops, while the state is still intact and the cause can still
|
|
390
|
+
// be named, rather than letting V8 abort later on `Check failed:
|
|
391
|
+
// IsOnCentralStack()` — or worse, running JavaScript on freed handles. rb_raise
|
|
392
|
+
// and rb_fatal would longjmp through V8's C++ frames, so this doesn't go through
|
|
393
|
+
// Ruby's error path at all.
|
|
394
|
+
fn fatal_nesting_broken(core: &Core, call: &RubyCall<'_>, entry_depth: u32, exit_depth: u32) -> ! {
|
|
395
|
+
eprintln!(
|
|
396
|
+
"\n[rusty_racer] fatal: operations on this isolate stopped nesting.\n\
|
|
397
|
+
\n\
|
|
398
|
+
{who} handed control to Ruby.\n\
|
|
399
|
+
By the time it came back, the operations in flight on this isolate had\n\
|
|
400
|
+
changed (now {exit_depth}, was {entry_depth}) — so they no longer nest. The Ruby code\n\
|
|
401
|
+
switched Fibers and left this one stranded while the isolate was used\n\
|
|
402
|
+
again: `Fiber.yield` or `Fiber#transfer` out of the callback, or an\n\
|
|
403
|
+
`Enumerator::Yielder#<<` from inside it — anything that suspends the\n\
|
|
404
|
+
callback rather than returning from it.\n\
|
|
405
|
+
\n\
|
|
406
|
+
V8 requires the operations on one isolate to unwind in the reverse of the\n\
|
|
407
|
+
order they started, and that is now impossible: finishing this one would\n\
|
|
408
|
+
pop its handles out from under the other's. Raising a Ruby exception here\n\
|
|
409
|
+
would BE that unwind, and waiting is a deadlock — the other operation is\n\
|
|
410
|
+
blocked on the Fiber this one is standing in. Stopping now, while the\n\
|
|
411
|
+
state is still intact, is what is left.\n\
|
|
412
|
+
\n\
|
|
413
|
+
Resuming a Fiber from inside a callback IS supported, including calling\n\
|
|
414
|
+
back into the isolate from it. What isn't is yielding OUT of the callback\n\
|
|
415
|
+
and using the isolate before the Fiber is resumed again — let the Fiber\n\
|
|
416
|
+
run to completion inside the callback, or finish the operation first.\n\
|
|
417
|
+
\n\
|
|
418
|
+
See https://github.com/ursm/rusty_racer#fibers\n",
|
|
419
|
+
who = call.describe(core),
|
|
420
|
+
);
|
|
421
|
+
print_stranded_backtrace();
|
|
422
|
+
// Put SIGABRT back to the default. Ruby installs a handler for it that prints
|
|
423
|
+
// a [BUG] report — a Ruby-level backtrace, the loaded features, the whole
|
|
424
|
+
// memory map — which buries the explanation above under a page of noise that
|
|
425
|
+
// also reads as "Ruby crashed", which this is not. The process then dies of
|
|
426
|
+
// SIGABRT (exit 134); whether that leaves a core is the operator's ulimit to
|
|
427
|
+
// decide, not ours to override.
|
|
428
|
+
unsafe { libc::signal(libc::SIGABRT, libc::SIG_DFL) };
|
|
429
|
+
std::process::abort()
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// The Ruby frames of the operation whose callback just came back — the one named
|
|
433
|
+
// in the message above, which is usually the stranded one (it regains control
|
|
434
|
+
// first) but is the resumer's if that one got here first. We hold the GVL and are
|
|
435
|
+
// standing on that operation's own stack, so `caller` here is its eval/call. The
|
|
436
|
+
// proc itself has already RETURNED — that return is what got us here — so the
|
|
437
|
+
// yield's own line is not among these frames; naming the callback is what stands
|
|
438
|
+
// in for it. rb_backtrace() would do this in one call, but prints
|
|
439
|
+
// outermost-first, backwards from every backtrace a Ruby reader has seen.
|
|
440
|
+
fn print_stranded_backtrace() {
|
|
441
|
+
use magnus::rb_sys::FromRawValue;
|
|
442
|
+
let Ok(raw) = magnus::rb_sys::protect(|| unsafe { rb_sys::rb_make_backtrace() }) else {
|
|
443
|
+
return;
|
|
444
|
+
};
|
|
445
|
+
let frames = magnus::RArray::from_value(unsafe { magnus::Value::from_raw(raw) })
|
|
446
|
+
.and_then(|a| a.to_vec::<String>().ok())
|
|
447
|
+
.unwrap_or_default();
|
|
448
|
+
// Drop the binding's own frames (`_eval` and the `eval` wrapping it, both at
|
|
449
|
+
// the same line of rusty_racer.rb) so the first line is the caller's. Keep
|
|
450
|
+
// everything if that would leave nothing to print.
|
|
451
|
+
let own = |f: &String| f.contains("/rusty_racer.rb:");
|
|
452
|
+
let user: Vec<&String> = frames.iter().filter(|f| !own(f)).collect();
|
|
453
|
+
let shown: Vec<&String> = if user.is_empty() {
|
|
454
|
+
frames.iter().collect()
|
|
455
|
+
} else {
|
|
456
|
+
user
|
|
457
|
+
};
|
|
458
|
+
if shown.is_empty() {
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
eprintln!("That operation is here:");
|
|
462
|
+
for (i, frame) in shown.iter().enumerate() {
|
|
463
|
+
eprintln!("{}{frame}", if i == 0 { "\t" } else { "\tfrom " });
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
315
467
|
// Identity of the calling RUBY thread (its Thread VALUE, stable for the thread's
|
|
316
468
|
// life) — used to bind an isolate to its owner thread. Unlike a native ThreadId,
|
|
317
469
|
// this survives Ruby's M:N scheduler moving the thread between native threads.
|
|
@@ -381,9 +533,9 @@ fn host_fn_callback(
|
|
|
381
533
|
throw_js_error(scope, "host function has no owner");
|
|
382
534
|
return;
|
|
383
535
|
}
|
|
384
|
-
let
|
|
536
|
+
let core = unsafe { &*core_ptr };
|
|
537
|
+
let result: Result<JsVal, String> = with_gvl(core, RubyCall::HostFn(host_fn_id), || {
|
|
385
538
|
let ruby = Ruby::get().unwrap();
|
|
386
|
-
let core = unsafe { &*core_ptr };
|
|
387
539
|
// rb_protect so a Ruby exception raised during arg/return marshalling
|
|
388
540
|
// (ary_new_capa / jsval_to_ruby / ruby_to_jsval can raise on OOM etc.)
|
|
389
541
|
// is CAUGHT here instead of longjmp-ing through V8's C++ frames. The
|
|
@@ -940,6 +1092,7 @@ fn resolve_imported<'s>(
|
|
|
940
1092
|
if core_ptr.is_null() {
|
|
941
1093
|
return None;
|
|
942
1094
|
}
|
|
1095
|
+
let core = unsafe { &*core_ptr };
|
|
943
1096
|
// The static instantiate block parked for THIS op (Some) vs a dynamic
|
|
944
1097
|
// import's auto-link (None -> dynamic_import_resolver, with the initiating
|
|
945
1098
|
// realm so it can resolve per-realm).
|
|
@@ -950,8 +1103,8 @@ fn resolve_imported<'s>(
|
|
|
950
1103
|
// The error conversion happens INSIDE with_gvl: error_to_exception
|
|
951
1104
|
// calls into Ruby (so it can allocate, so it can GC) and BoxValue::new
|
|
952
1105
|
// registers a GC root — neither is safe with the GVL released.
|
|
953
|
-
match with_gvl(|| {
|
|
954
|
-
resolve_module_via_ruby(
|
|
1106
|
+
match with_gvl(core, RubyCall::Resolver(&spec), || {
|
|
1107
|
+
resolve_module_via_ruby(core, resolve, &spec, &ref_url, None)
|
|
955
1108
|
.map_err(|e| error_to_exception(&e).map(BoxValue::new))
|
|
956
1109
|
}) {
|
|
957
1110
|
Ok(id) => id,
|
|
@@ -965,24 +1118,18 @@ fn resolve_imported<'s>(
|
|
|
965
1118
|
}
|
|
966
1119
|
}
|
|
967
1120
|
None => {
|
|
968
|
-
let resolver =
|
|
1121
|
+
let resolver = core
|
|
969
1122
|
.dynamic_import_resolver
|
|
970
1123
|
.lock()
|
|
971
1124
|
.unwrap()
|
|
972
1125
|
.as_ref()
|
|
973
1126
|
.map(|r| r.get());
|
|
974
1127
|
match resolver {
|
|
975
|
-
Some(p) => match with_gvl(|| {
|
|
976
|
-
resolve_module_via_ruby(
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
&ref_url,
|
|
981
|
-
Some(here.unwrap_or(0)),
|
|
982
|
-
)
|
|
983
|
-
// Rendering the message reads the Ruby exception, so it too
|
|
984
|
-
// stays inside with_gvl.
|
|
985
|
-
.map_err(|e| e.to_string())
|
|
1128
|
+
Some(p) => match with_gvl(core, RubyCall::Resolver(&spec), || {
|
|
1129
|
+
resolve_module_via_ruby(core, p, &spec, &ref_url, Some(here.unwrap_or(0)))
|
|
1130
|
+
// Rendering the message reads the Ruby exception, so it too
|
|
1131
|
+
// stays inside with_gvl.
|
|
1132
|
+
.map_err(|e| e.to_string())
|
|
986
1133
|
}) {
|
|
987
1134
|
Ok(id) => id,
|
|
988
1135
|
// Unlike the static branch there is no Ruby frame waiting to
|
|
@@ -1087,7 +1234,8 @@ fn dynamic_import_cb<'s>(
|
|
|
1087
1234
|
reject(scope, "dynamic import has no owner");
|
|
1088
1235
|
return Some(promise);
|
|
1089
1236
|
}
|
|
1090
|
-
let
|
|
1237
|
+
let core = unsafe { &*core_ptr };
|
|
1238
|
+
let resolver_proc = core
|
|
1091
1239
|
.dynamic_import_resolver
|
|
1092
1240
|
.lock()
|
|
1093
1241
|
.unwrap()
|
|
@@ -1096,8 +1244,8 @@ fn dynamic_import_cb<'s>(
|
|
|
1096
1244
|
let id = match resolver_proc {
|
|
1097
1245
|
// A raising resolver only fails the import() (it rejects generically);
|
|
1098
1246
|
// it must NOT abort the surrounding eval, so swallow the Err here.
|
|
1099
|
-
Some(p) => with_gvl(|| {
|
|
1100
|
-
resolve_module_via_ruby(
|
|
1247
|
+
Some(p) => with_gvl(core, RubyCall::Resolver(&spec), || {
|
|
1248
|
+
resolve_module_via_ruby(core, p, &spec, &referrer, Some(initiating))
|
|
1101
1249
|
})
|
|
1102
1250
|
.unwrap_or(None),
|
|
1103
1251
|
None => None,
|
|
@@ -2290,7 +2438,7 @@ impl Core {
|
|
|
2290
2438
|
&self.installed_stack_limit,
|
|
2291
2439
|
stack_top,
|
|
2292
2440
|
);
|
|
2293
|
-
if depth == 0 {
|
|
2441
|
+
let reply = if depth == 0 {
|
|
2294
2442
|
let mut reply = std::panic::catch_unwind(AssertUnwindSafe(|| {
|
|
2295
2443
|
v8::scope!(let scope, unsafe { &mut *iso });
|
|
2296
2444
|
service_request(scope, request, true)
|
|
@@ -2359,15 +2507,38 @@ impl Core {
|
|
|
2359
2507
|
unsafe { (*iso).exit() };
|
|
2360
2508
|
}
|
|
2361
2509
|
reply
|
|
2510
|
+
};
|
|
2511
|
+
// Everything that has to happen whatever else does goes HERE, inside
|
|
2512
|
+
// the closure, rather than after without_gvl returns:
|
|
2513
|
+
// rb_thread_call_without_gvl checks the thread's pending interrupts as
|
|
2514
|
+
// it re-acquires the GVL, and a Thread#raise, a Timeout, or a Ctrl-C
|
|
2515
|
+
// that lands there longjmps straight past everything written after the
|
|
2516
|
+
// call.
|
|
2517
|
+
//
|
|
2518
|
+
// Balancing the fetch_add is one: a missed decrement is permanent and
|
|
2519
|
+
// silent — the isolate then looks like it has an op in flight for the
|
|
2520
|
+
// rest of its life, so no later op is ever the outermost one
|
|
2521
|
+
// (microtasks stop draining, terminate flags stop being cleared) and
|
|
2522
|
+
// dispose refuses forever. The op is finished by this point: the scope
|
|
2523
|
+
// is dropped and the isolate exited in both branches above.
|
|
2524
|
+
//
|
|
2525
|
+
// Poisoning a panicked isolate is the other: None means the op left V8
|
|
2526
|
+
// possibly inconsistent, and an interrupt arriving on top of that must
|
|
2527
|
+
// not be what decides whether later ops are allowed to touch it. The
|
|
2528
|
+
// raise below still reports it — the lock is safe with the GVL
|
|
2529
|
+
// released, since every other holder takes it briefly and none waits
|
|
2530
|
+
// on an op.
|
|
2531
|
+
if reply.is_none() {
|
|
2532
|
+
self.shared.lock().unwrap().disposed = true;
|
|
2362
2533
|
}
|
|
2534
|
+
self.depth.fetch_sub(1, Ordering::SeqCst);
|
|
2535
|
+
reply
|
|
2363
2536
|
});
|
|
2364
|
-
self.depth.fetch_sub(1, Ordering::SeqCst);
|
|
2365
2537
|
match reply {
|
|
2366
2538
|
Some(reply) => Ok(reply),
|
|
2367
2539
|
None => {
|
|
2368
|
-
// The op panicked
|
|
2369
|
-
//
|
|
2370
|
-
self.shared.lock().unwrap().disposed = true;
|
|
2540
|
+
// The op panicked; the isolate was poisoned inside the closure
|
|
2541
|
+
// above (every later op refuses) rather than risk using it.
|
|
2371
2542
|
Err(Error::new(
|
|
2372
2543
|
err_class(ruby, "InternalError"),
|
|
2373
2544
|
"internal error: operation panicked; the isolate has been disposed",
|
|
@@ -2555,6 +2726,7 @@ impl Core {
|
|
|
2555
2726
|
let host_fn_id = self.procs.lock().unwrap().alloc(ProcSlot {
|
|
2556
2727
|
context_id,
|
|
2557
2728
|
proc: Some(RootedProc(BoxValue::new(proc))),
|
|
2729
|
+
name: name.clone(),
|
|
2558
2730
|
});
|
|
2559
2731
|
let reply = self.run(
|
|
2560
2732
|
ruby,
|
|
@@ -2592,6 +2764,7 @@ impl Core {
|
|
|
2592
2764
|
let id = procs.alloc(ProcSlot {
|
|
2593
2765
|
context_id,
|
|
2594
2766
|
proc: Some(RootedProc(BoxValue::new(proc))),
|
|
2767
|
+
name: name.clone(),
|
|
2595
2768
|
});
|
|
2596
2769
|
(name, id)
|
|
2597
2770
|
})
|
|
@@ -2892,7 +3065,10 @@ impl Core {
|
|
|
2892
3065
|
if self.depth.load(Ordering::SeqCst) != 0 {
|
|
2893
3066
|
return Err(Error::new(
|
|
2894
3067
|
err_class(ruby, "Error"),
|
|
2895
|
-
"RustyRacer: cannot dispose an isolate
|
|
3068
|
+
"RustyRacer: cannot dispose an isolate with an operation still in \
|
|
3069
|
+
flight — from inside a host callback, or from anywhere at all if a \
|
|
3070
|
+
host callback yielded out of a Fiber that has not been resumed \
|
|
3071
|
+
(see https://github.com/ursm/rusty_racer#fibers)",
|
|
2896
3072
|
));
|
|
2897
3073
|
}
|
|
2898
3074
|
shared.disposed = true;
|
|
@@ -2908,13 +3084,7 @@ impl Drop for Core {
|
|
|
2908
3084
|
if self.shared.lock().unwrap().disposed {
|
|
2909
3085
|
return;
|
|
2910
3086
|
}
|
|
2911
|
-
if current_ruby_thread()
|
|
2912
|
-
// Last wrapper dropped on the owner thread: full teardown. depth is 0
|
|
2913
|
-
// (a running op holds a wrapper alive, so the last drop can't race
|
|
2914
|
-
// one), and Drop can't raise — so just tear down.
|
|
2915
|
-
self.shared.lock().unwrap().disposed = true;
|
|
2916
|
-
self.teardown();
|
|
2917
|
-
} else {
|
|
3087
|
+
if current_ruby_thread() != self.owner {
|
|
2918
3088
|
// Foreign-thread GC drop: a thread-bound isolate CANNOT be disposed
|
|
2919
3089
|
// off its owner thread (that would SEGV) and Drop CANNOT raise — so
|
|
2920
3090
|
// LEAK the OwnedIsolate (it stays in the owner thread's ISOLATES until
|
|
@@ -2923,15 +3093,45 @@ impl Drop for Core {
|
|
|
2923
3093
|
// this leak; the counter makes it observable (RustyRacer.leaked_isolate_count).
|
|
2924
3094
|
LEAKED_ISOLATES.fetch_add(1, Ordering::Relaxed);
|
|
2925
3095
|
self.watchdog.request_shutdown();
|
|
3096
|
+
return;
|
|
2926
3097
|
}
|
|
3098
|
+
// An op still in flight means a host callback yielded out of a Fiber that
|
|
3099
|
+
// was then never resumed — Ruby frees an unresumed Fiber's stack without
|
|
3100
|
+
// unwinding it, so the op's frames simply vanish and its wrapper with
|
|
3101
|
+
// them. The isolate is still ENTERED by those frames, and disposing an
|
|
3102
|
+
// entered isolate is a V8 fatal ("Disposing the isolate that is entered by
|
|
3103
|
+
// a thread"), which is the unexplained abort this binding exists to avoid.
|
|
3104
|
+
// Leak it instead: the process is on its way out anyway, and a leak that
|
|
3105
|
+
// says why beats a fatal that doesn't. This is the quiet twin of
|
|
3106
|
+
// fatal_nesting_broken — same cause, but nothing ever came back to detect
|
|
3107
|
+
// it, so it surfaces here at teardown.
|
|
3108
|
+
if self.depth.load(Ordering::SeqCst) != 0 {
|
|
3109
|
+
eprintln!(
|
|
3110
|
+
"\n[rusty_racer] warning: an isolate is being discarded with an \
|
|
3111
|
+
operation still in flight,\n\
|
|
3112
|
+
so it cannot be disposed and is leaked.\n\
|
|
3113
|
+
The usual cause is a host function that yielded out of a Fiber \
|
|
3114
|
+
which was never resumed;\n\
|
|
3115
|
+
`Fiber#kill` recovers one if you still hold it. See\n\
|
|
3116
|
+
https://github.com/ursm/rusty_racer#fibers\n"
|
|
3117
|
+
);
|
|
3118
|
+
LEAKED_ISOLATES.fetch_add(1, Ordering::Relaxed);
|
|
3119
|
+
self.watchdog.request_shutdown();
|
|
3120
|
+
return;
|
|
3121
|
+
}
|
|
3122
|
+
// Last wrapper dropped on the owner thread with nothing in flight: full
|
|
3123
|
+
// teardown. Drop can't raise, so just tear down.
|
|
3124
|
+
self.shared.lock().unwrap().disposed = true;
|
|
3125
|
+
self.teardown();
|
|
2927
3126
|
}
|
|
2928
3127
|
}
|
|
2929
3128
|
|
|
2930
3129
|
// RustyRacer.live_isolate_count -> Integer: isolates currently in the registry
|
|
2931
3130
|
// (created, not yet disposed). RustyRacer.leaked_isolate_count -> Integer:
|
|
2932
|
-
// isolates that could not be disposed because their last wrapper was dropped
|
|
2933
|
-
// the owner thread
|
|
2934
|
-
//
|
|
3131
|
+
// isolates that could not be disposed — because their last wrapper was dropped
|
|
3132
|
+
// off the owner thread, or because an operation was still in flight (see Drop
|
|
3133
|
+
// for both) — a workload that churns owner threads should keep this at 0 by
|
|
3134
|
+
// disposing on the owner thread.
|
|
2935
3135
|
fn live_isolate_count() -> usize {
|
|
2936
3136
|
isolates().lock().unwrap().len()
|
|
2937
3137
|
}
|
data/lib/rusty_racer/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rusty_racer
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.2.
|
|
4
|
+
version: 0.2.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Keita Urashima
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-08-
|
|
11
|
+
date: 2026-08-26 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rb_sys
|