Skip to content

Commit 6f54a44

Browse files
committed
fix(worker): keep a termination from being re-armed as an error on its way out
A termination that interrupts a CommonJS entry unwinds through the native require callback, whose catch re-threw the message-only failure onto the isolate as an ordinary Error: V8 then reported an exception rather than a termination to RunModule's TryCatch, which built a full error report before the worker boundary dropped it. ReThrowToV8 now leaves a terminating isolate alone, since the pending termination is the failure, and RunModule consults the same three termination signals as the settle pump. The pumped graph walk ahead of an entry's compile bails on termination but reports nothing, so LoadESModule went on to the synchronous HTTP loader, which can block on the network. Both walks are now followed by a termination check. Also refreshes the drain comment that described the pre-publication isolate window, widens the startup-sweep spec's spread so its rounds reach the settle pump, and corrects the resourceLimits RangeError wording.
1 parent 1f141da commit 6f54a44

5 files changed

Lines changed: 54 additions & 17 deletions

File tree

‎NativeScript/runtime/ModuleInternal.mm‎

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,10 @@ static bool IsHttpModulePath(const std::string& path) {
258258
moduleNamespace =
259259
ModuleInternal::LoadESModule(isolate, path, BootEntryEvaluationOptions(isHttpModule));
260260
} catch (const NativeScriptException& ex) {
261-
if (RuntimeConfig.IsDebug) {
261+
Runtime* runtime = Runtime::GetRuntime(isolate);
262+
bool terminating = isolate->IsExecutionTerminating() ||
263+
(runtime != nullptr && runtime->IsTerminationRequested());
264+
if (RuntimeConfig.IsDebug && !terminating) {
262265
Log(@"***** JavaScript exception occurred *****");
263266
Log(@"Error loading ES module: %s", path.c_str());
264267
Log(@"Exception: %s", ex.getMessage().c_str());
@@ -294,8 +297,12 @@ throw NativeScriptException(
294297

295298
if (!success || tc.HasCaught()) {
296299
// A termination is caught like an exception but carries no error value;
297-
// naming it as the failure keeps it from being reported as one.
298-
if (tc.HasTerminated()) {
300+
// naming it as the failure keeps it from being reported as one. All three
301+
// signals, as in the settle pump: a native frame between here and the
302+
// interrupted JS may have swallowed the sentinel on its way out.
303+
Runtime* runtime = Runtime::GetRuntime(isolate);
304+
if (tc.HasTerminated() || isolate->IsExecutionTerminating() ||
305+
(runtime != nullptr && runtime->IsTerminationRequested())) {
299306
throw NativeScriptException("Module evaluation interrupted by isolate termination: " + path);
300307
}
301308
if (RuntimeConfig.IsDebug) {
@@ -1461,6 +1468,21 @@ throw NativeScriptException("Module evaluation interrupted by isolate terminatio
14611468
return MaybeLocal<Promise>();
14621469
}
14631470

1471+
// A pumped graph walk bails on a termination request but reports nothing;
1472+
// what follows it would compile, fetch synchronously or evaluate on an
1473+
// isolate that must not run anything more.
1474+
static void ThrowIfLoadInterruptedByTermination(Isolate* isolate,
1475+
const std::string& canonicalPath) {
1476+
Runtime* runtime = Runtime::GetRuntime(isolate);
1477+
if (!isolate->IsExecutionTerminating() &&
1478+
(runtime == nullptr || !runtime->IsTerminationRequested())) {
1479+
return;
1480+
}
1481+
LogEsmPhase(canonicalPath, "load", "terminated");
1482+
throw NativeScriptException("Module evaluation interrupted by isolate termination: " +
1483+
canonicalPath);
1484+
}
1485+
14641486
Local<Value> ModuleInternal::LoadESModule(Isolate* isolate, const std::string& path,
14651487
const ModuleEvaluationOptions& options) {
14661488
bool isHttpModule = IsHttpModulePath(path);
@@ -1540,6 +1562,7 @@ throw NativeScriptException("Module evaluation interrupted by isolate terminatio
15401562
// registry hit and instantiation resolves as pure lookup. On timeout or
15411563
// partial coverage the legacy synchronous path still owns correctness.
15421564
RunModuleGraphLoadPumped(isolate, context, requestPath, kModuleEvaluateDeadlineSeconds);
1565+
ThrowIfLoadInterruptedByTermination(isolate, canonicalPath);
15431566
MaybeLocal<Module> maybeMod = LoadHttpModuleForUrl(isolate, context, requestPath);
15441567
if (!maybeMod.ToLocal(&module)) {
15451568
logPhase("compile", "fail", "http-loader");
@@ -1563,6 +1586,7 @@ throw NativeScriptException("Module evaluation interrupted by isolate terminatio
15631586
// async fetch, so it legitimately waits here and then evaluates
15641587
// synchronously.
15651588
RunModuleGraphLoadPumped(isolate, context, canonicalPath, kModuleEvaluateDeadlineSeconds);
1589+
ThrowIfLoadInterruptedByTermination(isolate, canonicalPath);
15661590
auto walkedIt = registry.find(canonicalPath);
15671591
if (walkedIt != registry.end()) {
15681592
Local<Module> walked = walkedIt->second.Get(isolate);

‎NativeScript/runtime/NativeScriptException.mm‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -714,6 +714,15 @@ static bool GiveWorkerOnErrorAChance(Isolate* isolate, Local<Context> context, L
714714
}
715715

716716
void NativeScriptException::ReThrowToV8(Isolate* isolate) {
717+
// An isolate that is terminating keeps its termination pending until the JS
718+
// frames below unwind; throwing on it would replace that with an ordinary
719+
// error, and building the error allocates on a heap that may have just hit
720+
// its cap. The failure being re-armed here is the termination itself.
721+
Runtime* runtime = Runtime::GetRuntime(isolate);
722+
if (isolate->IsExecutionTerminating() ||
723+
(runtime != nullptr && runtime->IsTerminationRequested())) {
724+
return;
725+
}
717726
@try {
718727
// The Isolate::Scope here is necessary because the Exception::Error method internally relies on
719728
// the Isolate::GetCurrent method which might return null if we do not use the proper scope

‎NativeScript/runtime/WorkerWrapper.mm‎

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -154,12 +154,14 @@ static void PostToLoop(const std::shared_ptr<EventLoop>& loop, std::function<voi
154154

155155
void WorkerWrapper::DrainPendingTasks() {
156156
// The drain source is armed (and can be signaled by a main-thread
157-
// PostMessage) BEFORE `workerIsolate_` is assigned in BackgroundLooper, and
158-
// worker creation spins this runloop inside that window: the entry's module
159-
// graph load pumps it while waiting on fetches, which fires this source with
160-
// a null isolate (crash in v8::Locker::Initialize). Bail until the isolate
161-
// exists — the messages stay queued and the explicit DrainPendingTasks()
162-
// call right after isolate creation delivers them.
157+
// PostMessage) before the worker's runtime exists, and the entry's module
158+
// graph load pumps this runloop while waiting on fetches. A fire that
159+
// arrives before the isolate is published has nothing to lock (a null
160+
// isolate crashes v8::Locker::Initialize); one that arrives during the entry
161+
// locks and then leaves at the messagesEnabled_ gate below, since the queue
162+
// is only enabled once the entry has finished evaluating. Either way the
163+
// messages stay queued for the explicit DrainPendingTasks() call after the
164+
// startup function returns.
163165
if (this->workerIsolate_ == nullptr) {
164166
return;
165167
}

‎TestRunner/app/tests/WorkerTerminateTests.js‎

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,7 @@ describe("Worker terminate during entry evaluation", function () {
1515
});
1616

1717
// Resolves once the worker's thread has ended; rejects when it has not
18-
// within `limitMs`, which is the old behaviour for an entry that never
19-
// returns.
18+
// within `limitMs`.
2019
function waitForEnd(worker, limitMs) {
2120
return new Promise(function (resolve, reject) {
2221
var timer = setTimeout(function () {
@@ -68,9 +67,11 @@ describe("Worker terminate during entry evaluation", function () {
6867
};
6968
});
7069

71-
// Round i terminates i milliseconds after construction, which lands
72-
// anywhere from before the thread has started, through runtime setup, to
73-
// inside the entry's settle pump.
70+
// Round i terminates 25·i milliseconds after construction. Runtime setup
71+
// takes a few hundred milliseconds on a simulator and the local entry's
72+
// settle pump lasts one second after it, so the rounds land anywhere from
73+
// before the thread has started, through runtime setup, to inside the
74+
// pump.
7475
it("ends a worker terminated at any point of its startup", function (done) {
7576
var ROUNDS = 16;
7677
(function round(i) {
@@ -84,7 +85,7 @@ describe("Worker terminate during entry evaluation", function () {
8485
if (i === 0) {
8586
worker.terminate();
8687
} else {
87-
setTimeout(function () { worker.terminate(); }, i);
88+
setTimeout(function () { worker.terminate(); }, i * 25);
8889
}
8990
ended.then(function () { round(i + 1); }, settle(done));
9091
})(0);

‎docs/worker-threads.md‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -307,8 +307,9 @@ new Worker("./w.js", {
307307

308308
Node's `stackSizeMb` and `codeRangeSizeMb` are ignored like any other unknown
309309
key. A `resourceLimits` that is not an object, or a key that is not a number,
310-
throws a `TypeError`; a non-finite, non-positive or oversized value throws a
311-
`RangeError`, as does a fractional or out-of-range `jsDispatchTableSizeMb`.
310+
throws a `TypeError`; a non-finite value, one worth less than a byte, or one
311+
too large to hold in bytes throws a `RangeError`, as does a fractional or
312+
out-of-range `jsDispatchTableSizeMb`.
312313
`resourceLimits: null` is the same as omitting it.
313314

314315
Exhausting a worker's heap does not take the process down. Every worker

0 commit comments

Comments
 (0)