Skip to content

Commit e146d4e

Browse files
fix(worker): isolate and wrapper lifetime fixes around startup and teardown (#478)
* fix(worker): isolate and wrapper lifetime fixes around startup and teardown A throwing onclose run from the entry script read the worker isolate before it was published and crashed. Terminate() used the worker isolate with no synchronization against the worker thread deleting its runtime. EndWrapperLifetime kept using the wrapper after dispatching nsworkerended, whose listeners can shut the runtime down and delete it. * fix(worker): serialize the message queue's run loop source with its teardown Push() and Signal() read the run loop source and run loop with no lock while Terminate(), on another thread, invalidates and releases the source. They now go through the mutex Terminate() holds, and the worker thread terminates its queue before it leaves, since the queue borrows that thread's run loop. Also removes the unused GetWorkerIsolate() getter, which handed out the isolate without the mutex that now guards it. * refactor(worker): rename the queue's run loop mutex and scope the isolate lock initializationMutex_ guards the run loop source and run loop for the queue's whole life now that Signal() takes it, so it is named runLoopMutex_. Terminate() holds workerIsolateMutex_ in a scoped block, like the inspectorMutex_ block next to it, instead of a unique_lock released by hand. The comment in Push() said Terminate() flips the terminated flag under the queue mutex; it sets it under the run loop mutex, before emptying the queue. --------- Co-authored-by: Eduardo Speroni <edusperoni@gmail.com>
1 parent ee77abb commit e146d4e

7 files changed

Lines changed: 101 additions & 58 deletions

File tree

‎NativeScript/runtime/ConcurrentQueue.cpp‎

Lines changed: 16 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -4,33 +4,29 @@
44
namespace tns {
55

66
void ConcurrentQueue::Initialize(CFRunLoopRef runLoop, void (*performWork)(void*), void* info) {
7-
std::unique_lock<std::mutex> lock(initializationMutex_);
8-
if (terminated) {
9-
return;
10-
}
7+
std::unique_lock<std::mutex> lock(runLoopMutex_);
8+
if (terminated) {
9+
return;
10+
}
1111
this->runLoop_ = runLoop;
1212
CFRunLoopSourceContext sourceContext = { 0, info, 0, 0, 0, 0, 0, 0, 0, performWork };
1313
this->runLoopTasksSource_ = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &sourceContext);
1414
CFRunLoopAddSource(this->runLoop_, this->runLoopTasksSource_, kCFRunLoopCommonModes);
1515
}
1616

1717
void ConcurrentQueue::Push(std::shared_ptr<worker::Message> message) {
18-
if (this->runLoopTasksSource_ != nullptr && !CFRunLoopSourceIsValid(this->runLoopTasksSource_)) {
19-
return;
20-
}
21-
2218
{
23-
// Checked under the queue mutex, where Terminate() also flips it while
24-
// emptying the queue: a push that loses the race is dropped rather than
25-
// landing in a queue nothing will ever pop again.
19+
// Terminate() sets the flag before it empties the queue under this
20+
// mutex, so a push either sees it here or lands ahead of that sweep:
21+
// never in a queue nothing will pop again.
2622
std::unique_lock<std::mutex> mlock(this->mutex_);
2723
if (this->terminated) {
2824
return;
2925
}
3026
this->messagesQueue_.push(message);
3127
}
3228

33-
this->SignalAndWakeUp();
29+
this->Signal();
3430
}
3531

3632
std::vector<std::shared_ptr<worker::Message>> ConcurrentQueue::PopAll() {
@@ -52,20 +48,14 @@ bool ConcurrentQueue::IsEmpty() {
5248
}
5349

5450
void ConcurrentQueue::Signal() {
55-
// Mirrors Push()'s validity handling instead of SignalAndWakeUp()'s
56-
// assert: a retry racing Terminate() must be a silent no-op.
57-
if (this->runLoopTasksSource_ == nullptr ||
58-
!CFRunLoopSourceIsValid(this->runLoopTasksSource_)) {
59-
return;
51+
// Serializes signaling and waking with Initialize() and Terminate().
52+
// Terminate() clears both pointers and invalidates and releases the source;
53+
// the run loop is borrowed from the worker thread, which terminates the
54+
// queue before it leaves.
55+
std::unique_lock<std::mutex> lock(runLoopMutex_);
56+
if (this->runLoopTasksSource_ != nullptr) {
57+
CFRunLoopSourceSignal(this->runLoopTasksSource_);
6058
}
61-
this->SignalAndWakeUp();
62-
}
63-
64-
void ConcurrentQueue::SignalAndWakeUp() {
65-
if (this->runLoopTasksSource_ != nullptr) {
66-
tns::Assert(CFRunLoopSourceIsValid(this->runLoopTasksSource_));
67-
CFRunLoopSourceSignal(this->runLoopTasksSource_);
68-
}
6959

7060
if (this->runLoop_ != nullptr) {
7161
CFRunLoopWakeUp(this->runLoop_);
@@ -78,7 +68,7 @@ void ConcurrentQueue::Terminate() {
7868
// its sibling group's lock and posts to the sibling's loop.
7969
std::queue<std::shared_ptr<worker::Message>> dropped;
8070
{
81-
std::unique_lock<std::mutex> lock(initializationMutex_);
71+
std::unique_lock<std::mutex> lock(runLoopMutex_);
8272
terminated = true;
8373
CFRunLoopRef runLoop = this->runLoop_;
8474
CFRunLoopSourceRef source = this->runLoopTasksSource_;

‎NativeScript/runtime/ConcurrentQueue.h‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,7 @@ struct ConcurrentQueue {
3131
CFRunLoopRef runLoop_ = nullptr;
3232
std::atomic<bool> terminated{false};
3333
std::mutex mutex_;
34-
std::mutex initializationMutex_;
35-
void SignalAndWakeUp();
34+
std::mutex runLoopMutex_;
3635
};
3736

3837
}

‎NativeScript/runtime/DataWrapper.h‎

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -553,12 +553,16 @@ class WorkerWrapper : public BaseDataWrapper {
553553
void Start(std::shared_ptr<v8::Persistent<v8::Value>> poWorker,
554554
std::function<v8::Isolate*()> func,
555555
std::optional<int> qualityOfService = std::nullopt);
556-
void CallOnErrorHandlers(v8::TryCatch& tc);
556+
// Both reporters take the isolate from their caller, which is running on
557+
// it: they are reachable while the entry script is still evaluating, before
558+
// workerIsolate_ is published.
559+
void CallOnErrorHandlers(v8::Isolate* isolate, v8::TryCatch& tc);
557560
// Reports a rejected entry-evaluation promise. A rejection carries a reason
558561
// rather than a TryCatch, so it cannot go through CallOnErrorHandlers, but it
559562
// follows the same web order: the worker scope's `onerror` first, then — only
560563
// if that did not handle it — the parent's Worker error event.
561-
void ReportEntryEvaluationRejection(v8::Local<v8::Context> context,
564+
void ReportEntryEvaluationRejection(v8::Isolate* isolate,
565+
v8::Local<v8::Context> context,
562566
v8::Local<v8::Value> reason);
563567
void PassUncaughtExceptionFromWorkerToMain(v8::Local<v8::Context> context,
564568
v8::TryCatch& tc,
@@ -622,7 +626,6 @@ class WorkerWrapper : public BaseDataWrapper {
622626
const inline v8::Isolate* GetMainIsolate() { return mainIsolate_; }
623627
// The only route from the worker thread to the parent: see mainLoop_.
624628
std::weak_ptr<EventLoop> MainLoop() const { return mainLoop_; }
625-
const inline v8::Isolate* GetWorkerIsolate() { return workerIsolate_; }
626629

627630
// Deletion is decided by who still holds the wrapper. The parent's Worker
628631
// object holds it from construction; Start() adds the worker thread. Each
@@ -647,7 +650,11 @@ class WorkerWrapper : public BaseDataWrapper {
647650
enum class Holders : uint8_t { Parent, Both, WorkerThread };
648651

649652
v8::Isolate* mainIsolate_;
653+
// Written by the worker thread only: published once the worker's startup
654+
// function returns, withdrawn before the worker's runtime is deleted. Any
655+
// other thread reads and uses it under workerIsolateMutex_.
650656
v8::Isolate* workerIsolate_;
657+
std::mutex workerIsolateMutex_;
651658
std::atomic<bool> isRunning_;
652659
std::atomic<bool> isClosing_;
653660
std::atomic<bool> isTerminating_;

‎NativeScript/runtime/Worker.mm‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -564,7 +564,7 @@ throw NativeScriptException(
564564
? info[0]
565565
: Local<Value>(v8::Exception::Error(tns::ToV8String(
566566
iso, "Worker entry module evaluation rejected")));
567-
w->ReportEntryEvaluationRejection(ctx, reason);
567+
w->ReportEntryEvaluationRejection(iso, ctx, reason);
568568
};
569569
Local<v8::Function> onFulfilled;
570570
Local<v8::Function> onRejected;
@@ -798,7 +798,7 @@ throw NativeScriptException(
798798
TryCatch tc(isolate);
799799
success = onCloseFunc->Call(context, v8::Undefined(isolate), 0, args).ToLocal(&result);
800800
if (!success && tc.HasCaught()) {
801-
worker->CallOnErrorHandlers(tc);
801+
worker->CallOnErrorHandlers(isolate, tc);
802802
}
803803
}
804804
}

‎NativeScript/runtime/WorkerWrapper.mm‎

Lines changed: 56 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -122,17 +122,26 @@ static void PostToLoop(const std::shared_ptr<EventLoop>& loop, std::function<voi
122122
}
123123

124124
void WorkerWrapper::EndWrapperLifetime() {
125-
Local<Value> worker =
126-
this->poWorker_ != nullptr ? this->poWorker_->Get(this->mainIsolate_) : Local<Value>();
125+
// The dispatch below runs listeners, and a listener may shut the runtime
126+
// down, whose teardown deletes this wrapper. Everything the dispatch needs is
127+
// read first, and the liveness token says afterwards whether `this` is still
128+
// there to unroot.
129+
Isolate* isolate = this->mainIsolate_;
130+
std::shared_ptr<std::atomic<WorkerWrapper*>> selfRef = this->selfRef_;
131+
Local<Value> worker = this->poWorker_ != nullptr ? this->poWorker_->Get(isolate) : Local<Value>();
127132
if (!worker.IsEmpty() && worker->IsObject()) {
128-
TryCatch tc(this->mainIsolate_);
129-
Worker::EmitEnded(this->mainIsolate_, worker.As<Object>());
133+
TryCatch tc(isolate);
134+
Worker::EmitEnded(isolate, worker.As<Object>());
130135
if (tc.HasCaught()) {
131136
Local<Value> error = tc.Exception();
132-
Log(@"%s", tns::ToString(this->mainIsolate_, error).c_str());
133-
this->mainIsolate_->ThrowException(error);
137+
Log(@"%s", tns::ToString(isolate, error).c_str());
138+
isolate->ThrowException(error);
134139
}
135140
}
141+
if (selfRef->load(std::memory_order_acquire) == nullptr) {
142+
// Deleted during the dispatch; that teardown released the Worker object.
143+
return;
144+
}
136145
this->UnrootWorkerObject();
137146
}
138147

@@ -183,7 +192,7 @@ static void PostToLoop(const std::shared_ptr<EventLoop>& loop, std::function<voi
183192
this->onMessage_(this->workerIsolate_, globalTarget, message);
184193

185194
if (tc.HasCaught()) {
186-
this->CallOnErrorHandlers(tc);
195+
this->CallOnErrorHandlers(this->workerIsolate_, tc);
187196
}
188197
}
189198

@@ -234,7 +243,11 @@ static void PostThreadEndedNotification(Isolate* mainIsolate, std::weak_ptr<Even
234243
},
235244
this);
236245

237-
this->workerIsolate_ = func();
246+
Isolate* workerIsolate = func();
247+
{
248+
std::lock_guard<std::mutex> lock(this->workerIsolateMutex_);
249+
this->workerIsolate_ = workerIsolate;
250+
}
238251

239252
this->DrainPendingTasks();
240253

@@ -244,6 +257,20 @@ static void PostThreadEndedNotification(Isolate* mainIsolate, std::weak_ptr<Even
244257
}
245258
}
246259

260+
// The queue borrows this thread's run loop, so it lets go of it before the
261+
// thread leaves: a terminate() that made this thread skip the loop above
262+
// only reaches its own queue_.Terminate() later, possibly after the thread
263+
// and its run loop are gone. A second Terminate() finds nothing set.
264+
this->queue_.Terminate();
265+
266+
// Withdrawn before the runtime and its isolate go away below. Terminate()
267+
// uses the isolate under this mutex, so a terminate that already read it has
268+
// finished with it by the time this returns, and a later one finds null.
269+
{
270+
std::lock_guard<std::mutex> lock(this->workerIsolateMutex_);
271+
this->workerIsolate_ = nullptr;
272+
}
273+
247274
// The inspector must be gone before the Runtime (and with it the isolate)
248275
// is deleted below.
249276
this->DestroyInspector();
@@ -315,20 +342,25 @@ static void PostThreadEndedNotification(Isolate* mainIsolate, std::weak_ptr<Even
315342
// set terminating to true atomically
316343
bool wasTerminating = this->isTerminating_.exchange(true);
317344
if (!wasTerminating) {
318-
if (this->workerIsolate_ != nullptr) {
319-
// Flagged before the request so a pump that is between iterations sees
320-
// it on its next check, rather than only once V8 has some JS to
321-
// interrupt — which a parked graph never provides.
322-
//
323-
// NOTE: `workerIsolate_` is assigned only after the worker's ENTRY has
324-
// finished evaluating, so a worker still parked in its entry is not
325-
// reachable from here at all and terminate() does nothing for it. That
326-
// is a pre-existing worker-lifecycle gap, not something this flag can
327-
// close — see the follow-up filed for it.
328-
if (Runtime* workerRuntime = Runtime::GetRuntime(this->workerIsolate_)) {
329-
workerRuntime->RequestTermination();
345+
{
346+
// Held across the use, not just the read: the worker thread withdraws
347+
// the isolate under the same mutex before deleting its runtime.
348+
std::lock_guard<std::mutex> lock(this->workerIsolateMutex_);
349+
if (this->workerIsolate_ != nullptr) {
350+
// Flagged before the request so a pump that is between iterations sees
351+
// it on its next check, rather than only once V8 has some JS to
352+
// interrupt — which a parked graph never provides.
353+
//
354+
// NOTE: `workerIsolate_` is assigned only after the worker's ENTRY has
355+
// finished evaluating, so a worker still parked in its entry is not
356+
// reachable from here at all and terminate() does nothing for it. That
357+
// is a pre-existing worker-lifecycle gap, not something this flag can
358+
// close — see the follow-up filed for it.
359+
if (Runtime* workerRuntime = Runtime::GetRuntime(this->workerIsolate_)) {
360+
workerRuntime->RequestTermination();
361+
}
362+
this->workerIsolate_->TerminateExecution();
330363
}
331-
this->workerIsolate_->TerminateExecution();
332364
}
333365
{
334366
// A worker paused at a breakpoint sits in the inspector's nested pause
@@ -437,11 +469,10 @@ static void PostThreadEndedNotification(Isolate* mainIsolate, std::weak_ptr<Even
437469
delete client;
438470
}
439471

440-
void WorkerWrapper::CallOnErrorHandlers(TryCatch& tc) {
472+
void WorkerWrapper::CallOnErrorHandlers(Isolate* isolate, TryCatch& tc) {
441473
if (this->isTerminating_) {
442474
return;
443475
}
444-
Isolate* isolate = this->workerIsolate_;
445476
Local<Context> context = Caches::Get(isolate)->GetContext();
446477
Local<Object> global = context->Global();
447478

@@ -470,11 +501,11 @@ static void PostThreadEndedNotification(Isolate* mainIsolate, std::weak_ptr<Even
470501
this->PassUncaughtExceptionFromWorkerToMain(context, tc);
471502
}
472503

473-
void WorkerWrapper::ReportEntryEvaluationRejection(Local<Context> context, Local<Value> reason) {
504+
void WorkerWrapper::ReportEntryEvaluationRejection(Isolate* isolate, Local<Context> context,
505+
Local<Value> reason) {
474506
if (this->isTerminating_) {
475507
return;
476508
}
477-
Isolate* isolate = this->workerIsolate_;
478509
Local<Object> global = context->Global();
479510

480511
Local<Value> onErrorVal;

‎TestRunner/app/tests/MessagingTests.js‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,16 @@ describe("Messaging runtime edges", function () {
252252
}
253253
};
254254
});
255+
256+
it("reports an error onclose threw while the entry script was still running", function (done) {
257+
var worker = new Worker("./messaging/throwingOncloseWorker.js");
258+
worker.onerror = function (event) {
259+
event.preventDefault();
260+
expect(event.message).toContain("boom from onclose");
261+
worker.terminate();
262+
done();
263+
};
264+
});
255265
});
256266

257267
describe("AbortSignal handler attribute accounting", function () {
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
// Closes from inside the entry script, so onclose runs before the entry has
2+
// finished evaluating.
3+
onclose = function () {
4+
throw new Error("boom from onclose");
5+
};
6+
close();

0 commit comments

Comments
 (0)