Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Microtask queue is not flushed when promises are fulfilled in C++ #5691

Closed
vkurchatkin opened this issue Mar 13, 2016 · 22 comments
Closed

Microtask queue is not flushed when promises are fulfilled in C++ #5691

vkurchatkin opened this issue Mar 13, 2016 · 22 comments
Labels
addons Issues and PRs related to native addons. c++ Issues and PRs that require attention from people who are familiar with C++. confirmed-bug Issues with confirmed bugs. promises Issues and PRs related to ECMAScript promises.

Comments

@vkurchatkin
Copy link
Contributor

Reproduction:

#include <node.h>
#include <uv.h>
#include <unistd.h>

static v8::Persistent<v8::Promise::Resolver> persistent;

void run(uv_work_t* req) {
  sleep(1);
}

void callback(uv_work_t* req, int i) {
  v8::Isolate* isolate = v8::Isolate::GetCurrent();
  v8::HandleScope scope(isolate);

  v8::Local<v8::Promise::Resolver> local = v8::Local<v8::Promise::Resolver>::New(isolate, persistent);
  local->Resolve(v8::Undefined(isolate));
}

void test(const v8::FunctionCallbackInfo<v8::Value>& args) {
  v8::Isolate* isolate = args.GetIsolate();

  if (persistent.IsEmpty()) {
    persistent.Reset(isolate, v8::Promise::Resolver::New(isolate));

    uv_work_t * req = (uv_work_t*) malloc(sizeof(uv_work_t));

    uv_queue_work(uv_default_loop(), req, run, callback);
  }

  v8::Local<v8::Promise::Resolver> local = v8::Local<v8::Promise::Resolver>::New(isolate, persistent);

  args.GetReturnValue().Set(local->GetPromise());
}

void init(v8::Local<v8::Object> exports) {
  NODE_SET_METHOD(exports, "test", test);
}

NODE_MODULE(addon, init)
var r = require('./build/Release/addon.node');

r.test().then(function() {
  console.log('done');
});


setTimeout(() => {}, 5000);

done should be printed after 1 seconds, but is printed after 5 seconds instead.

@vkurchatkin vkurchatkin added the promises Issues and PRs related to ECMAScript promises. label Mar 13, 2016
@vkurchatkin
Copy link
Contributor Author

It seems that calling isolate->SetAutorunMicrotasks(true); doesn't help with this problem

@vkurchatkin
Copy link
Contributor Author

One possible solution: add CallbackScope class that extends functionality of MakeCallback:

v8::Isolate* isolate = v8::Isolate::GetCurrent();
v8::HandleScope scope(isolate);
node::CallbackScope callbackScope(isolate);

 v8::Local<v8::Promise::Resolver> local = v8::Local<v8::Promise::Resolver>::New(isolate, persistent);
 local->Resolve(v8::Undefined(isolate));

// resolve more promises, call functions, whatever

When CallbackScope is exited, env->KickNextTick is called automatically.

@IdeaHunter
Copy link

Another possible solution suggested by @ranisalt:

local->Resolve(v8::Undefined(isolate));
//give it a kick
Isolate::GetCurrent()->RunMicrotasks();

But it still doesn't clear - should it be our responsibility to add RunMicrotasks or CallbackScope or v8 should handle it itself...

@vkurchatkin
Copy link
Contributor Author

Just running microtasks is not enough, you need to run next tick queue as well.

@ranisalt
Copy link

@IdeaHunter I couldn't get it to work on a more complex example, I guess promise resolving/rejecting is not considered a microtask.

How's a simple way to get the current environment? I searched through code but couldn't find it. In fact I'm only getting a forward declaration of Environment in my source and thus can not use the class at all.

@benjamingr
Copy link
Member

@vkurchatkin would you mind clarifying how things run in particular and why running the microtask queue is not enough and what "kicks" the continuation?

When I checked comparable code in blink I didn't find anything similar required (again, I'm far far far from an experts in these parts).

@vkurchatkin
Copy link
Contributor Author

@benjamingr the proper way to call JS from C++ is to use MakeCallback. It does all necessary things before and after calling the function, including running next tick and microtask queues.

@benjamingr
Copy link
Member

@vkurchatkin and just to make sure I understand:

We never actually have any callbacks here because everything is in C++ code, we're just returning a promise and resolve/reject that doesn't run nextTick and the microtask queue so nothing happens until that's triggered some other way.

I'm pondering about the possibility of asking V8 for something akin to bluebird's setScheduler (only in C++ land) that will allow consumers to choose how to schedule the callback and will solve this in addition to a lot of other issues we have with promises (like AsyncWrap). It would default to the same behavior it has now - what do you think?

@vkurchatkin
Copy link
Contributor Author

@benjamingr correct. It also seems that it is implied by v8 authors that you should run microtasks manually when you work with promises from C++.

I'm pondering about the possibility of asking V8 for something akin to bluebird's setScheduler

@chrisdickinson is working on something like this. It won't solve this problem, though. Users still need to flush scheduled callbacks explicitly at some point.

@benjamingr
Copy link
Member

Users still need to flush scheduled callbacks explicitly at some point.

Basically what we'd like (optimally) is for resolve and reject to schedule a microtask flush if one is not already present. I get not doing so automatically though and this is a change we can't really ask for anyway.

If we can't get there I agree it needs to be documented very clearly at least.

@vkurchatkin
Copy link
Contributor Author

I get not doing so automatically though and this is a change we can't really ask for anyway.

I'm not 100% sure, I expected that V8 does that, but seems like it does not. Running microtasks each would be suboptimal and can be confusing, because promise handlers are expected to be executed asynchronously and they will be synchronous in this case.

@benjamingr
Copy link
Member

I meant as with a trampoline - as in "set a boolean flag to true and later if it's true run the handlers" and keep running them until none are left.

There is no reason to assume they'd do it like that since the API in C++ is lower level. I just assumed it since that's what promise libraries do.

@vkurchatkin
Copy link
Contributor Author

s in "set a boolean flag to true and later if it's true run the handlers" and keep running them until none are left.

@benjamingr the problem is that there is no definite "later" in this case

@vkurchatkin
Copy link
Contributor Author

Proof of concept:

diff --git a/src/node.cc b/src/node.cc
index c0e8dbc..c713509 100644
--- a/src/node.cc
+++ b/src/node.cc
@@ -1287,6 +1287,14 @@ Local<Value> MakeCallback(Isolate* isolate,
         MakeCallback(env, recv.As<Value>(), callback, argc, argv)));
 }

+CallbackScope::CallbackScope(Isolate* isolate): isolate_(isolate) {
+}
+
+CallbackScope::~CallbackScope() {
+  Environment* env = Environment::GetCurrent(isolate_);
+  Environment::AsyncCallbackScope callback_scope(env);
+  env->KickNextTick(&callback_scope);
+}

 enum encoding ParseEncoding(const char* encoding,
                             enum encoding default_encoding) {
diff --git a/src/node.h b/src/node.h
index fcbb450..2ab3768 100644
--- a/src/node.h
+++ b/src/node.h
@@ -41,6 +41,7 @@

 #include "v8.h"  // NOLINT(build/include_order)
 #include "node_version.h"  // NODE_MODULE_VERSION
+#include "util.h"

 #define NODE_MAKE_VERSION(major, minor, patch)                                \
   ((major) * 0x1000 + (minor) * 0x100 + (patch))
@@ -150,6 +151,17 @@ NODE_EXTERN v8::Local<v8::Value> MakeCallback(
     int argc,
     v8::Local<v8::Value>* argv);

+class NODE_EXTERN CallbackScope {
+   public:
+    explicit CallbackScope(v8::Isolate* isolate);
+    ~CallbackScope();
+
+   private:
+    v8::Isolate* isolate_;
+
+    DISALLOW_COPY_AND_ASSIGN(CallbackScope);
+};
+
 }  // namespace node

 #if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS

Then in addon:

void callback(uv_work_t* req, int i) {
  v8::Isolate* isolate = v8::Isolate::GetCurrent();
  v8::HandleScope scope(isolate);
  node::CallbackScope callback_scope(isolate);

  v8::Local<v8::Promise::Resolver> local = v8::Local<v8::Promise::Resolver>::New(isolate, persistent);
  local->Resolve(v8::Undefined(isolate));
}

We can probably combine HandleScope and CallbackScope for convenience. The best thing about this approach is that it doesn't matter if we use callbacks or promises or both.

@benjamingr
Copy link
Member

I like the RAII here, the only issue is that it's always coupled with Resolve anyway - that is, you want that CallbackScope there if and only if you call Resolve and Reject in that scope - if the call to resolution is conditional, or happens "later" (as in a C++ callback) then the RAII doesn't really work.

It might as well be a

ResolvePromise(local, v8::Undefined(isolate))

and save me the extra line - no?

@vkurchatkin
Copy link
Contributor Author

ResolvePromise(local, v8::Undefined(isolate))

This works only for single promise, CallbackScope works for multiple calls to js, any calls.

you want that CallbackScope there if and only if you call Resolve and Reject in that scope

You can set up scope conditionally, but that seems to be rarely useful.

@Fishrock123 Fishrock123 added the c++ Issues and PRs that require attention from people who are familiar with C++. label Mar 14, 2016
@jasnell jasnell added the confirmed-bug Issues with confirmed bugs. label Aug 5, 2016
@benjamingr
Copy link
Member

@vkurchatkin any update on this?

DemiMarie added a commit to DemiMarie/node that referenced this issue May 23, 2017
All credit to Vladimir Kurchatkin, who created the original patch in
nodejs#5691 (comment).
@Trott
Copy link
Member

Trott commented Jul 15, 2017

Should this remain open?

@benjamingr
Copy link
Member

@Trott I believe so - waiting for an update from @vkurchatkin

@addaleax
Copy link
Member

I think this has not been fixed, and my guess would be that it’ll never really be fixed. I’m not sure whether it was around when this issue was opened, but maybe v8::Isolate::RunMicrotasks() is the best answer we can get here?

@vkurchatkin
Copy link
Contributor Author

v8::Isolate::RunMicrotasks() is the best answer we can get here?

We should expose a proper way to flush microtask and nextTick queues for old API as well as N-API.

@addaleax addaleax added the addons Issues and PRs related to native addons. label Sep 5, 2017
@addaleax
Copy link
Member

addaleax commented Sep 5, 2017

I think #14697 would be a valid fix for this, added a regression test for it in a4505910d29597ff8fd9f95d4effcbf3196f63ca

addaleax added a commit to addaleax/node that referenced this issue Sep 6, 2017
With `CallbackScope`, this has become possible to do properly.

Fixes: nodejs#5691
PR-URL: nodejs#14697
Reviewed-By: James M Snell <jasnell@gmail.com>
addaleax added a commit to addaleax/node that referenced this issue Sep 14, 2017
With `CallbackScope`, this has become possible to do properly.

Fixes: nodejs#5691
PR-URL: nodejs#14697
Reviewed-By: James M Snell <jasnell@gmail.com>
addaleax added a commit to addaleax/ayo that referenced this issue Sep 17, 2017
With `CallbackScope`, this has become possible to do properly.

Fixes: nodejs/node#5691
PR-URL: nodejs/node#14697
Reviewed-By: James M Snell <jasnell@gmail.com>
jasnell pushed a commit that referenced this issue Sep 20, 2017
With `CallbackScope`, this has become possible to do properly.

Fixes: #5691
PR-URL: #14697
Reviewed-By: James M Snell <jasnell@gmail.com>
Qard pushed a commit to Qard/ayo that referenced this issue Sep 21, 2017
With `CallbackScope`, this has become possible to do properly.

Fixes: nodejs/node#5691
PR-URL: nodejs/node#14697
Reviewed-By: James M Snell <jasnell@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
addons Issues and PRs related to native addons. c++ Issues and PRs that require attention from people who are familiar with C++. confirmed-bug Issues with confirmed bugs. promises Issues and PRs related to ECMAScript promises.
Projects
None yet
Development

No branches or pull requests

8 participants