Skip to content
This repository was archived by the owner on Oct 15, 2020. It is now read-only.

Conversation

@kfarnung
Copy link
Contributor

@kfarnung kfarnung commented Feb 20, 2018

The promisify function calls into native code to create a new
Promise::Resolver object and returns it back to script. In script it's
expected to be a promise object, but in our case it wasn't.

The solution is to hide the resolver data in an external object and
attach it to the promise object.

Checklist
  • make -j4 test (UNIX), or vcbuild test (Windows) passes
  • commit message follows commit guidelines
Affected core subsystem(s)

chakrashim

@kfarnung
Copy link
Contributor Author

kfarnung commented Feb 20, 2018

@kfarnung
Copy link
Contributor Author

kfarnung commented Feb 20, 2018

I still need to look at a strategy for testing this behavior, it's somewhat odd since the native code returns a Promise::Resolver, but the script gets back a promise. That's why we didn't catch this earlier.

Native code:

void CreatePromise(const FunctionCallbackInfo<Value>& args) {
Local<Context> context = args.GetIsolate()->GetCurrentContext();
auto maybe_resolver = Promise::Resolver::New(context);
if (!maybe_resolver.IsEmpty())
args.GetReturnValue().Set(maybe_resolver.ToLocalChecked());
}

Script usage:
function fn(...args) {
const promise = createPromise();
try {
original.call(this, ...args, (err, ...values) => {
if (err) {
promiseReject(promise, err);
} else if (argumentNames !== undefined && values.length > 1) {
const obj = {};
for (var i = 0; i < argumentNames.length; i++)
obj[argumentNames[i]] = values[i];
promiseResolve(promise, obj);
} else {
promiseResolve(promise, values[0]);
}
});
} catch (err) {
promiseReject(promise, err);
}
return promise;
}

Resolver* Resolver::Cast(Value* obj) {
return static_cast<Resolver*>(obj);
PromiseResolverData* data = new PromiseResolverData(resolve, reject);
if (jsrt::AddExternalData(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are chakrashim's news throwing? If not, need a check here that data is not null

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question, I believe they are, but I'm not sure. Looking at the other instances of new I don't see any null checks, so if it's no-throw then we'll have to clean them all up.

@digitalinfinity
Copy link
Contributor

This is very similar to what @MSLaguana fixed upstream with nodejs/node#18765. Maybe a better fix is to just fix CreatePromise in node_util.cc to do the following:

 void CreatePromise(const FunctionCallbackInfo<Value>& args) { 
   Local<Context> context = args.GetIsolate()->GetCurrentContext(); 

   auto maybe_resolver = Promise::Resolver::New(context); 
   if (!maybe_resolver.IsEmpty()) 
     args.GetReturnValue().Set(maybe_resolver->GetPromise().ToLocalChecked()); 
 } 

(Or something like that 😄)

Where are we hitting this btw?

@kfarnung
Copy link
Contributor Author

@digitalinfinity Unfortunately that won't work, I did consider Jimmy's upstream fix, but the object that's returned need to be both:

  • A valid promise object
  • Able to carry the resolve/reject functions along

Jimmy's fix requires that someone keep track of the Promise::Resolver, but in this case it's not stored anywhere outside of the calling script. The change he made upstream was the right fix there, nobody was ever attempting to take a promise and get a resolver from it.

I've been making a point to use node-chakracore for basically everything and I hit some really weird behavior in https://github.com/nodejs/node-core-utils which uses util.promisify to wrap the fs calls:

https://github.com/nodejs/node-core-utils/blob/a15715ad1434ed12287adf15d00ed35123a654a8/lib/auth.js#L9-L11

I think there may also be another bug because when createPromise was returning the wrapper object, the code seemed to get into an infinite loop.

@digitalinfinity
Copy link
Contributor

Presumably because of code like the following? There seems to be widespread assumptions that a Promise is a Resolver, which is not our model I guess.

Local<Promise::Resolver> resolver = promise.As<Promise::Resolver>(); // sic

@kfarnung
Copy link
Contributor Author

kfarnung commented Feb 20, 2018

Yeah, that's the problematic pattern. The good news is that with this change a Resolver is a Promise (but not the other way around). Looking at the V8 API surface this appears to be the pattern as well. There's no way to create Promise object directly, you create a resolver and then get the promise from it.

The thing we didn't have yet was being able to cast that Promise back to a resolver (which is what this change enables).

@MSLaguana
Copy link
Contributor

Ugh, it pains me that this is necessary, but it looks good to me.

@MSLaguana
Copy link
Contributor

Also by my understanding of this change, a v8::Promise (constructed via the shim) is equivalent to a v8::Promise::Resolver. I was just about to say that node code probably wouldn't get a value and go "Oh it's a promise, let me get the resolver" but now that I think about it, there is that whole IsPromise property that is available...

@kfarnung
Copy link
Contributor Author

@MSLaguana I don't believe there was a way to create a promise via the shim prior to your change. The V8 API requires you to create a Promise::Resolver and then get the Promise from it (there is no New method and the constructor is private). Even though their own documentation doesn't indicate it, they seem to allow casting back and forth between them. Further, if a resolver is returns to script it seems to implicitly cast it to a Promise.

If you create the promise some other way (via N-API or script), then it won't be equivalent, but on the typical path where you are only using the V8 surface, that's correct.

@kfarnung
Copy link
Contributor Author

There appears to be a problem with the Windows CI /cc @joaocgreis

I've run on the VSTS CI and everything passes, so I'm going to land this change.

The promisify function calls into native code to create a new
Promise::Resolver object and returns it back to script. In script it's
expected to be a promise object, but in our case it wasn't.

The solution is to hide the resolver data in an external object and
attach it to the promise object.

PR-URL: nodejs#470
Reviewed-By: Jimmy Thomson <jithomso@microsoft.com>
Reviewed-By: Hitesh Kanwathirtha <hiteshk@microsoft.com>
@kfarnung kfarnung merged commit 90c554d into nodejs:master Feb 20, 2018
@kfarnung kfarnung deleted the resolver branch February 20, 2018 04:25
@joaocgreis
Copy link
Member

The CI issue was an infra problem, one of the machines connected to jenkins to run jobs ran out of memory. This has already been fixed, and I did not see the problem again with chakracore-test-windows.

kfarnung added a commit to kfarnung/node-chakracore that referenced this pull request Feb 23, 2018
The promisify function calls into native code to create a new
Promise::Resolver object and returns it back to script. In script it's
expected to be a promise object, but in our case it wasn't.

The solution is to hide the resolver data in an external object and
attach it to the promise object.

PR-URL: nodejs#470
Reviewed-By: Jimmy Thomson <jithomso@microsoft.com>
Reviewed-By: Hitesh Kanwathirtha <hiteshk@microsoft.com>
kfarnung added a commit to kfarnung/node-chakracore that referenced this pull request Mar 6, 2018
The promisify function calls into native code to create a new
Promise::Resolver object and returns it back to script. In script it's
expected to be a promise object, but in our case it wasn't.

The solution is to hide the resolver data in an external object and
attach it to the promise object.

PR-URL: nodejs#470
Reviewed-By: Jimmy Thomson <jithomso@microsoft.com>
Reviewed-By: Hitesh Kanwathirtha <hiteshk@microsoft.com>
kfarnung added a commit to kfarnung/node-chakracore that referenced this pull request Mar 7, 2018
The promisify function calls into native code to create a new
Promise::Resolver object and returns it back to script. In script it's
expected to be a promise object, but in our case it wasn't.

The solution is to hide the resolver data in an external object and
attach it to the promise object.

PR-URL: nodejs#470
Reviewed-By: Jimmy Thomson <jithomso@microsoft.com>
Reviewed-By: Hitesh Kanwathirtha <hiteshk@microsoft.com>
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants