-
Notifications
You must be signed in to change notification settings - Fork 408
[WIP] feat(async): fix #740, use async hooks to wrap nodejs async api #795
base: master
Are you sure you want to change the base?
Conversation
test/common/Promise.spec.ts
Outdated
class MicroTaskQueueZoneSpec implements ZoneSpec { | ||
name: string = 'MicroTaskQueue'; | ||
queue: MicroTask[] = []; | ||
properties = {queue: this.queue, flush: this.flush.bind(this)}; | ||
|
||
flush() { | ||
if (!useZoneAwarePromise) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we need to disable the tests? I would imagine that the tests should still work even with async_hook?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@mhevery , sorry, I just began to debug those test codes, all tests should be able to run with async_hook, but some tests need to be modified.
So please ignore those test case files.
And I found to make all tests work may cause some time and some test will still wait nodejs
to make some changes, so I just make the PR to let you review the zone.ts
and zone_async_hooks
core code.
I will finish those test codes and let you review later.
test/common/Promise.spec.ts
Outdated
@@ -126,11 +136,14 @@ describe( | |||
expect(queue.length).toEqual(1); | |||
expect(log).toEqual([]); | |||
flush(); | |||
expect(log).toEqual(['RValue', 'second value']); | |||
setTimeout(() => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why are these needed? Is it because the flush()
no longer works?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes, just like the above one, I am just began to debug those tests, I will finish modifying those tests and let you review. Please ignore those files for now.
lib/zone.ts
Outdated
@@ -323,6 +329,8 @@ interface _ZonePrivate { | |||
patchEventTargetMethods: | |||
(obj: any, addFnName?: string, removeFnName?: string, metaCreator?: any) => boolean; | |||
patchOnProperties: (obj: any, properties: string[]) => void; | |||
beforeRunTask: (zone: Zone, task: Task) => BeforeRunTaskStatus; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not a fan of beforeRunTask
and afterRunTask
, but I do understand why you need them. But I think it would mean that we should also add them to ZoneDelegate
? Or are you planning on making them private?
How do we communicate to the developer that this particular Task can't be wrapped? It never has runTask
executed on it. It can never be in fakeAsync
zone.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes, those APIs are private.
And I will check fakeAsync
, I think the runTask
will be called because in fakeAsyncZone
, the callback is task.invoke
and it will finally call task.zone.runTask
. I will check it.
lib/node/node_asynchooks.ts
Outdated
} | ||
}); | ||
|
||
global[Zone.__symbol__('setTimeout')] = global.setTimeout; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why are these needed here? Seems out of place
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes, those will be deleted finally, I just want to make some current test case pass easily, and I can debug them easily. I will remove them later.
lib/zone.ts
Outdated
@@ -1294,7 +1318,13 @@ const Zone: ZoneType = (function(global: any) { | |||
scheduleMicroTask: scheduleMicroTask, | |||
showUncaughtError: () => !(Zone as any)[__symbol__('ignoreConsoleErrorUncaughtError')], | |||
patchEventTargetMethods: () => false, | |||
patchOnProperties: noop | |||
patchOnProperties: noop, | |||
beforeRunTask: (zone: Zone, task: Task) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see you are making them private, but than we should somehow hide them from the public API.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I want to confirm I understand hiding from public API
correctly.
We should hide beforeRunTask
, runTask
, afterRunTask
from Zone if we use async_hooks
, because beforeRunTask
and afterRunTask
should only be called from async_hooks
, so runTask
should never be called from user code, otherwise it will also call beforeRunTask
and afterRunTask
and cause issue.
so in async_hooks
mode, all those 3 methods are not available, is that right?
For compatibility with non async_hooks
, should we just add a flag to check we are using async_hooks
or not, and if we are using async_hooks
, we just return when user call runTask
, is that ok? I will make a commit to describe it!
lib/node/node_asynchooks.ts
Outdated
if (!task) { | ||
return; | ||
} | ||
const beforeRunTask: BeforeRunTaskStatus = (task as any)[Zone.__symbol__(BEFORE_RUN_TASK_STATUS)]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Task
has data
. We should put BeforeRunTaskStatus
there, rather than monkey patching it on Task
directly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Got it, I will change it.
waiting for currently for example
both @mhevery , please help to check this is correct or not, thank you ! I made a PR to get This PR only correct the And it will be implemented soon. |
@JiaLiPassion We are also interested in getting Zones working on top of async hooks avoiding monkey patching if possible and would be happy to help. It looks like some zone APIs require a handle to the callback associated with an async task (onIntercept, onInvoke, etc). Did you have thoughts on how these callbacks could be observed using async hooks without monkey patching? |
@matthewloring, thank you for reply!
thank you and I definitely need your help on this changes.
Yes, you are right, currently I have not figure out how to get the
and other API such as But if And if @mhevery , if we use
|
@mhevery , @matthewloring , other things that may change when use
in the current
and if we use
Because in
just like @matthewloring said, we have no
we have to use
|
@matthewloring , continue to discuss about the so in setTimeout(() => {
console.log('timeout callback')'
}); will only trigger but in
So I think from the design, |
That is correct, async hooks allows for observation of async events while zones relies on being able to reschedule async work. This was my primary concern with refactoring zones to use async hooks. One possibility would be to have two "modes" supported by zones. A fast, observation-only mode based on async hooks that supported async/await and a slower, monkey-patching based mode which allows rescheduling async tasks but does not have async/await support. This might be too invasive a change (@mhevery what are your thoughts?) but would that address your use case @JiaLiPassion? |
Yeah, your suggestion could be a good solution. So it will be
And about the original |
I am on vacation till end of month. But your discussion sounds reasonable to me. |
If it has two 'modes', would it make sense to split the library into two? |
@ofrobots, yeah, maybe build a |
@JiaLiPassion we could already do this with |
So you mean we load Zone.__load_patch('node');
Zone.__load_patch('async_hooks`); and add some checks to prevent both them are loaded, is that correct? |
@JiaLiPassion that is what I had in mind |
@mhevery , got it, I will continue to work on it after the performance one is done. |
@mhevery Is it expected behavior for zones to only support a subset of its API depending on the set of patches loaded using |
In my understanding, it is, you can find all the |
@JiaLiPassion Looking at the list of patches, it seems like they make zones aware of more async constructs but still support the full zones API. That is, the set of tasks that zones are aware of increases but if I attach an onIntercept function it will still behave correctly for all tasks. If an async hooks plugin were added and a user attempts to reschedule all task callbacks using onIntercept then all tasks corresponding to async hook events will ignore the results of the onIntercept callback if I understand correctly. |
@matthewloring , I want to make sure I understand your meaning correctly.
const foo = function() {
console.log('foo');
};
const fooInZone = Zone.current.fork({
name: 'test',
onIntercept: function (..., zone, callback) {
console.log('callback intercepted');
return callback;
}
}).wrap(foo);
fooInZone(); And in this code, const fooInZone = Zone.current.fork({
name: 'test',
onIntercept: function (..., zone, callback) {
console.log('callback intercepted');
return callback;
}
}).run(() => {
setTimeout(() => {
}, 1000);
}); In this code,
About reschedule, if you talk about const foo = function() {
console.log('foo');
};
const fooInZone = Zone.current.fork({
name: 'test',
onIntercept: function (..., zone, callback) {
console.log('callback intercepted');
// here return a new callback
return function() {
console.log('new foo');
};
}
}).wrap(foo);
fooInZone(); do you mean some use case like the above code? And for const barZone = Zone.current.fork({
name: 'bar'
});
const fooInZone = Zone.current.fork({
name: 'foo',
onScheduleTask: function (zoneDelegate, currentZone, targetZone, task) {
if (task.source === 'something') {
// I want the task to be scheduled in bar zone
task.cancelScheduleRequest();
return barZone.scheduleTask(task);
}
}
}).wrap(foo);
fooInZone(); And I would like to confirm
what is the |
I will summarize the known issues here.
1. Native async/await issue.Now with The issue can be described as case1: promise's parent is an resolved promise.const p1 = Promise.resolve();
const p2 = p1.then(r => r); In this case, current function init(id: number, provider: string, triggerId: number, parentHandle: any) {
if (provider === PROMISE_PROVIDER) {
if (!parentHandle) {
return;
}
const parentId = parentHandle.parentId;
if (!parentId) {
return;
}
const task = Zone.current.scheduleMicroTask(PROMISE_SOURCE, noop, null, noop);
promiseTasks[id] = task;
}
} case2: the promise's parent is an unresolved promise.const p1 = new Promise(() => {}); // This will never be resolved
const p2 = new Promise((resolve) => setTimeout(resolve, 1000, p1)); // resolve p2 with p1 after 1s In this case, the And only expose
in this case, 2. Async hooks only support notification mode.As we discussed, async hooks can't get the control of 3. onHandleErrorBecause we can't get the control of callback, so we are not be able to do monkey-patch like try {
task.callback.apply(target, args);
} catch (err) {
zone.handleError(err);
} we have to use process.on('uncaughtException', (err: any) => {
// get current zone
Zone.current.handleError(err);
} It will become a little risky. |
Hello from |
@AndreasMadsen , thank you very much, and to implement const p1 = new Promise(() => {}); // This will never be resolved
const p2 = new Promise((resolve) => setTimeout(resolve, 1000, p1)); // resolve p2 with p1 after 1s In this case, the in my understanding the function then(onFulfilled, onRejected) {
const chainedPromise = new Promise();
if (this.state == UNRESOLVED) {
// current promise not resolved, keep callback into a list
this[thenList].push(chainPromise, onFulfilled, onRejected);
} else {
// current promise is resolved, we can enqueue the chained promise to microTask queue
// here we really schedule an async microTask
// in zone.js I need to know when promise get here
enqueue(chainedPromise);
}
}
function resolvePromise(promise, value) {
// here the resolve hook will be triggered
resolveHook(promise);
if (isPromise(value) && value.state == UNRESOLVED) {
// promise still unresolved
value.then(...);
return;
}
// value is thenable, so promise is not really resolved
if (isThenable(value)) {
value.then(...);
return;
}
chainedPromiseList = getThenList(promise);
// promise was really resolved, we enqueue to start a microTask for chained promise
// in zone.js I need to know when promise get here
chainedPromiseList.forEach(chainedPromise => enqueue(chainedPromise))
} **so I wonder is that possible to
|
I think you are using a technical vocabulary that is specific to zone.js, so I don't really understand what you are saying about
yes, this is what nodejs/node#13437 is about.
The const binding = process.binding('util');
const state2string = new Map([
[binding.kPending, 'pending'],
[binding.kFulfilled, 'fulfilled'],
[binding.kRejected, 'rejected']
]);
const [state, result] = binding.getPromiseDetails(resource.promise);
const readableState = state2string.get(state);
console.log(readableState); // fulfilled I think this was explained in nodejs/node#14038 (comment) I have no idea if |
@AndreasMadsen , thank you very much!
sorry for poor explanation. const p1 = new Promise(() => {}); // This will never be resolved
const p2 = new Promise((resolve) => setTimeout(resolve, 1000, p1)); // resolve p2 with p1 after 1s in this case, And about get state of promise. const binding = process.binding('util');
const state2string = new Map([
[binding.kPending, 'pending'],
[binding.kFulfilled, 'fulfilled'],
[binding.kRejected, 'rejected']
]);
const [state, result] = binding.getPromiseDetails(resource.promise);
const readableState = state2string.get(state);
console.log(readableState); // fulfilled Thank you for the information,
|
I see, have you experimented with using
You should assume so. If you have a good use case, you should open an issue in node.js where you request this feature is made public.
Most C++ calls have, but you will have to benchmark it to be sure. The internal function is used in |
the timing of promise.then() is late, and a really want is in my understandings, the code look like this. resolvePromise(promise, value) {
promise.state = RESOLVED;
// here I want a trigger.
trigger(PROMISE_RESOLVED);
// push the chained promise to microTask (process.tick?) queue.
enqueueToMicroTask(getChainedPromises(promise));
} And
Got it, I will try to make some cases to describe my requirements.
Got it, thank you, I will make benchmark to test it. |
Any news on this? |
@jpsfs, currently I am waiting for https://bugs.chromium.org/p/v8/issues/detail?id=6862 to resolve the |
8d64c10
to
4b52ea0
Compare
Update on 2018/01/04Support es2017 async/awaitBecause currently the idea can be described as the following code. init(id, triggerId, parentHandle) {
const promise = parentHandle.promise;
idPromises[id] = Zone.current; // save id and zone mapping
promise.then = function(onResolve: any, onReject: any) {
const task = zone.scheduleMicroTask(PROMISE_PROVIDER, noop, null, noop);
isPromiseTick = true;
process.nextTick(() => {
task.zone.runTask(task, null, null);
originalThen.call(this, onResolve, onReject);
});
};
}
before(id) {
const zone = getZoneById();
setCurrentZoneFrame(zone); // retrieve the zone by id
}
after(id) {
setCurrentZoneFrame(null); // leave zone
} and consider the following use case. const zone = Zone.current.fork({name: 'promise'});
async function asyncOutside() {
return 'asyncOutside';
}
zone.run(async() => {
const outside = await asyncOutside();
expect(outside).toEqual('asyncOutside');
expect(Zone.current.name).toEqual(zone.name);
}) with
Because gulp test/node/es2017 will run |
Update on 2018/01/04Support
|
@JiaLiPassion Be aware that the V8 team would like to remove The tracking won't be as easy as getting a specific event, but I think it is much easier for the V8 team to implement and I would like to avoid adding another promise-specific event to |
@AndreasMadsen , thank you for the information. If a And |
That is correct. |
@AndreasMadsen , got it, now |
fix #740, use new async_hooks to wrap nodejs instead of monkey-patch all native APIs.
The basic idea is in
async_hooks
, there are 4 hook apis.in this PR, the API will do those work.
init
will callZone.scheduleTask
to schedule a ZoneTaskbefore
will callZone.beforeRunTask
to create a new ZoneFrame and other work just likeZone.runTask
after
will callZone.afterRunTask
to go back toparent
ZoneFrame and do other work just likeZone.runTask
try to
cancelTask
if not cancelled.use
process.unCaughtException
to redirect toZoneDelegate.handleError
.this PR still need a lot of work to do, so I post the PR here just ask for review.
@mhevery , could you review that node_asynchooks.ts the whether the idea is ok or not? Thank you!