Skip to content

Commit 79a74fc

Browse files
fix: only marshal promise resolution when created on the runtime loop
The promise proxy captures the run loop at construction and, when a promise resolves on a different thread, marshals the resolution back to that captured run loop. When a promise is constructed while native code runs JS on a background thread, the captured run loop belongs to that background thread, which is dormant once the call returns. A resolution that arrives later on the runtime loop (for example from a setTimeout) is scheduled onto the dormant run loop and never runs, so the promise hangs forever. Only marshal the resolution when the promise was created on the runtime loop, which is always being pumped. A promise created on any other thread resolves inline on whichever thread settles it. The same gate is applied to the get trap on the returned promise. Fixes #330
1 parent 736365c commit 79a74fc

2 files changed

Lines changed: 55 additions & 7 deletions

File tree

NativeScript/runtime/PromiseProxy.cpp

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,36 @@
11
#include "PromiseProxy.h"
2+
3+
#include <CoreFoundation/CoreFoundation.h>
4+
25
#include "Helpers.h"
6+
#include "Runtime.h"
37

48
using namespace v8;
59

610
namespace tns {
711

12+
// Reports whether the calling thread runs the isolate's runtime loop. That loop
13+
// is where timers fire and is always being pumped, so a promise resolution is
14+
// marshaled back to its creating thread only when that thread is the runtime
15+
// loop; a promise created elsewhere settles on whichever thread resolves it.
16+
static void IsRuntimeRunloopCallback(const FunctionCallbackInfo<Value>& args) {
17+
Runtime* runtime = Runtime::GetRuntime(args.GetIsolate());
18+
bool isRuntimeLoop = runtime != nullptr && CFRunLoopGetCurrent() == runtime->RuntimeLoop();
19+
args.GetReturnValue().Set(isRuntimeLoop);
20+
}
21+
822
void PromiseProxy::Init(v8::Local<v8::Context> context) {
923
std::string source = R"(
10-
// Ensure that Promise callbacks are executed on the
11-
// same thread on which they were created
12-
(() => {
24+
// Run a Promise's callbacks on the thread that created it, but only when
25+
// that thread is the runtime loop. A Promise created on a background
26+
// thread settles on whichever thread resolves it, because the background
27+
// run loop may be dormant and marshaling a resolution to it would hang.
28+
(function(isRuntimeRunloop) {
1329
global.Promise = new Proxy(global.Promise, {
1430
construct: function(target, args) {
1531
let origFunc = args[0];
1632
let runloop = CFRunLoopGetCurrent();
33+
let originIsRuntimeLoop = isRuntimeRunloop();
1734
1835
let promise = new target(function(resolve, reject) {
1936
function isFulfilled() {
@@ -29,7 +46,7 @@ void PromiseProxy::Init(v8::Local<v8::Context> context) {
2946
return;
3047
}
3148
const resolveCall = resolve.bind(this, value);
32-
if (runloop === CFRunLoopGetCurrent()) {
49+
if (!originIsRuntimeLoop || runloop === CFRunLoopGetCurrent()) {
3350
markFulfilled();
3451
resolveCall();
3552
} else {
@@ -42,7 +59,7 @@ void PromiseProxy::Init(v8::Local<v8::Context> context) {
4259
return;
4360
}
4461
const rejectCall = reject.bind(this, reason);
45-
if (runloop === CFRunLoopGetCurrent()) {
62+
if (!originIsRuntimeLoop || runloop === CFRunLoopGetCurrent()) {
4663
markFulfilled();
4764
rejectCall();
4865
} else {
@@ -60,7 +77,7 @@ void PromiseProxy::Init(v8::Local<v8::Context> context) {
6077
return orig.bind(target);
6178
}
6279
return typeof orig === 'function' ? function(x) {
63-
if (runloop === CFRunLoopGetCurrent()) {
80+
if (!originIsRuntimeLoop || runloop === CFRunLoopGetCurrent()) {
6481
orig.bind(target, x)();
6582
return target;
6683
}
@@ -72,7 +89,7 @@ void PromiseProxy::Init(v8::Local<v8::Context> context) {
7289
});
7390
}
7491
});
75-
})();
92+
})
7693
)";
7794

7895
Isolate* isolate = context->GetIsolate();
@@ -83,6 +100,17 @@ void PromiseProxy::Init(v8::Local<v8::Context> context) {
83100

84101
Local<Value> result;
85102
success = script->Run(context).ToLocal(&result);
103+
tns::Assert(success && result->IsFunction(), isolate);
104+
105+
Local<v8::Function> installProxy = result.As<v8::Function>();
106+
107+
Local<v8::Function> isRuntimeRunloop;
108+
success = v8::Function::New(context, IsRuntimeRunloopCallback).ToLocal(&isRuntimeRunloop);
109+
tns::Assert(success, isolate);
110+
111+
Local<Value> installArgs[] = { isRuntimeRunloop };
112+
Local<Value> installResult;
113+
success = installProxy->Call(context, context->Global(), 1, installArgs).ToLocal(&installResult);
86114
tns::Assert(success, isolate);
87115
}
88116

TestRunner/app/tests/Promises.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,4 +130,24 @@ describe("Promise scheduling", function () {
130130
done();
131131
});
132132
});
133+
134+
it("the 'then' callback runs for a Promise created on a background thread and resolved on the runtime loop", done => {
135+
// https://github.com/NativeScript/ios/issues/330
136+
// The promise is constructed on a background dispatch queue whose run
137+
// loop is dormant, then resolve() is invoked on the runtime loop. The
138+
// resolution must run there instead of being marshaled back to the
139+
// parked background loop, otherwise the promise never settles.
140+
const backgroundQueue = dispatch_get_global_queue(qos_class_t.QOS_CLASS_DEFAULT, 0);
141+
dispatch_async(backgroundQueue, () => {
142+
new Promise(resolve => {
143+
NSOperationQueue.mainQueue.addOperationWithBlock(() => resolve("settled"));
144+
}).then(value => {
145+
expect(value).toBe("settled");
146+
done();
147+
}).catch(error => {
148+
expect(true).toBe(false, "The promise rejected unexpectedly: " + error);
149+
done();
150+
});
151+
});
152+
});
133153
});

0 commit comments

Comments
 (0)