This repository has been archived by the owner. It is now read-only.

implement domain-like hooks (asynclistener) for userland #6011

Closed
wants to merge 9 commits into
base: master
from

Conversation

Projects
None yet
@trevnorris

trevnorris commented Aug 7, 2013

Here's the current API planned for implementation:

// Class that will store information about a specific request.
function Storage() { }

// This will be called every time asynchronous work is queued.
// The returned data will propagate to the callbackObject's callbacks.
function onCreation() {
  return new Storage();
}

// Set of methods that will run at specific points in time according to
// their cooresponding callbacks. These methods are always run FIFO if
// multiple are queued.
// "context" is the "this" of the request object.
var callbackObject = {
  before: function asyncBefore(context, storageValue) {
  },
  // "returnValue" is the value returned from the callback.
  after: function asyncAfter(context, storageValue) {
  },
  // If this callback returns "true" then the error handler will assume
  // the error was properly handled, and process will continue normally.
  // If multiple error handlers are queued, and any one of those returns
  // true, then Node will assume the error was properly handled.
  // This will not currently be passed the context (or "this") of the
  // callback that threw. A simple way of achieving this is currently
  // being investigated, and the feature will be added when one is found.
  error: function asyncError(storageValue, err) {
  }
};

/**
 * process.addAsyncListener(callback[, object[, value]]);
 *
 * Arguments:
 *
 * callback - Function that will be run when an asynchronous job is
 *    queued.
 *
 * object - Object with the optional callbacks set on:
 *    before - Callback called just before the asynchronous callback
 *        will be called.
 *    after - Callback called directly after the asynchronous callback.
 *    error - Callback called if there was an error.
 *
 * The returned key is an Object that serves as the unique key for the
 * call (much like how Timers work).
 */
var key = process.addAsyncListener(onAsync, callbackObject);


/**
 * process.createAsyncListener(callback[, object[, value]]);
 *
 * Adding an async listener will immediately add it to the queue and
 * being listening for events. If you wish to create the listener in
 * advance, to say attach to the returned value object, it's possible
 * to get the key and pass it to process.addAsyncListener() later.
 */
var key = process.createAsyncListener(onAsync, callbackObject, value);

// Then activate like so:
process.addAsyncListener(key);


/**
 * process.removeAsyncListener(key);
 *
 * Remove any async listeners and associated callbackObjects. All
 * listeners will live independent of each other, and there will be no
 * method given to clear all async listeners.
 */
process.removeAsyncListener(key);
@othiym23

This comment has been minimized.

Show comment
Hide comment
@othiym23

othiym23 Aug 7, 2013

This would meet my requirements, as long as the async listener also fires for the timer functions and is invoked from Make(Domain)Callback. Also, I'm pretty sure that if you're going to expose a generic hook like this, it's going to need the ability to accept multiple listeners. If those are pieces you plan to add to this, 👍 from me.

othiym23 commented Aug 7, 2013

This would meet my requirements, as long as the async listener also fires for the timer functions and is invoked from Make(Domain)Callback. Also, I'm pretty sure that if you're going to expose a generic hook like this, it's going to need the ability to accept multiple listeners. If those are pieces you plan to add to this, 👍 from me.

@trevnorris

This comment has been minimized.

Show comment
Hide comment
@trevnorris

trevnorris Aug 7, 2013

@othiym23 thanks for the feedback. that was along the lines of what I was attempting. exposing multiple listeners is easy. already have a patch for it, but removed it when I realized that users could easily wrap callbacks in callbacks in callbacks. and there's no guaranteed order of firing. also, do I only allow the same listener to be added once, or can they add multiple? it was getting complicated fast so it was dropped so I could get something solid posted.

i'll happily add it back if there's a clear road of where it should go.

trevnorris commented Aug 7, 2013

@othiym23 thanks for the feedback. that was along the lines of what I was attempting. exposing multiple listeners is easy. already have a patch for it, but removed it when I realized that users could easily wrap callbacks in callbacks in callbacks. and there's no guaranteed order of firing. also, do I only allow the same listener to be added once, or can they add multiple? it was getting complicated fast so it was dropped so I could get something solid posted.

i'll happily add it back if there's a clear road of where it should go.

@othiym23

This comment has been minimized.

Show comment
Hide comment
@othiym23

othiym23 Aug 7, 2013

I think the most sensible, intuitive way for this to work would be to run the listeners in the order that they were registered via addAsyncListener, with each listener receiving the callback returned by the previous listener. It's up for the writers of the listeners to ensure that they're safe in how they create the closures. Regardless, the order in which they're run must be deterministic and stable (i.e. using an array for storage, not an object).

It's hard to avoid some difficulty with ordering dependencies, though. It might be enough to offer two calls to add a listener -- insertAsyncListener and addAsyncListener. If that's insufficient, giving modules that register listeners the power to dump and reorder the list of listeners might be necessary. I can't imagine a situation in which you'd want to have more than a couple of these listeners at a time (oh, the performance costs!), but you're right that there's an important semantic distinction to draw between outer and inner wrappers.

I don't really have it clear in my head, but it seems like it could be possible to use that object you were talking about yesterday as a means for these wrappers to share their state, and maybe externalize whatever data might otherwise subject to dependencies. But down that road lies a whole bunch of other annoying complications and complexity.

othiym23 commented Aug 7, 2013

I think the most sensible, intuitive way for this to work would be to run the listeners in the order that they were registered via addAsyncListener, with each listener receiving the callback returned by the previous listener. It's up for the writers of the listeners to ensure that they're safe in how they create the closures. Regardless, the order in which they're run must be deterministic and stable (i.e. using an array for storage, not an object).

It's hard to avoid some difficulty with ordering dependencies, though. It might be enough to offer two calls to add a listener -- insertAsyncListener and addAsyncListener. If that's insufficient, giving modules that register listeners the power to dump and reorder the list of listeners might be necessary. I can't imagine a situation in which you'd want to have more than a couple of these listeners at a time (oh, the performance costs!), but you're right that there's an important semantic distinction to draw between outer and inner wrappers.

I don't really have it clear in my head, but it seems like it could be possible to use that object you were talking about yesterday as a means for these wrappers to share their state, and maybe externalize whatever data might otherwise subject to dependencies. But down that road lies a whole bunch of other annoying complications and complexity.

@creationix

This comment has been minimized.

Show comment
Hide comment
@creationix

creationix Aug 31, 2013

I just sent pull requests to CLS to use this API and to CLS-Glue to prollyfill this API.

@trevnorris did I implement the proposed API correctly in the polyfill?

creationix commented Aug 31, 2013

I just sent pull requests to CLS to use this API and to CLS-Glue to prollyfill this API.

@trevnorris did I implement the proposed API correctly in the polyfill?

@creationix

This comment has been minimized.

Show comment
Hide comment
@creationix

creationix Aug 31, 2013

@othiym23 Also, once/if this lands in core and CLS becomes peer to Domains instead of domains being implemented on top of CLS, I recommend simplifying the CLS API and implementation to be just data storage. https://gist.github.com/creationix/d531157ad587a4af9f4e#file-multi-cls-js

creationix commented Aug 31, 2013

@othiym23 Also, once/if this lands in core and CLS becomes peer to Domains instead of domains being implemented on top of CLS, I recommend simplifying the CLS API and implementation to be just data storage. https://gist.github.com/creationix/d531157ad587a4af9f4e#file-multi-cls-js

@othiym23

This comment has been minimized.

Show comment
Hide comment
@othiym23

othiym23 Sep 2, 2013

@creationix: I went ahead and split your proposed changes out into an entirely separate module, async-listener. I then went and included that polyfill directly in continuation-local-storage, putting the relevant tests in relevant places. I pulled Trevor's tests from this PR, and a few of his test cases are currently failing, but I'll bang on that some more tomorrow.

@trevnorris, what this means is that we now have a polyfill for versions of node < (whatever version of Node this lands in). I'll keep it up to date with your changes, and you can grab a copy of continuation-local-storage and run its tests against your fork if you want another source of test coverage.

othiym23 commented Sep 2, 2013

@creationix: I went ahead and split your proposed changes out into an entirely separate module, async-listener. I then went and included that polyfill directly in continuation-local-storage, putting the relevant tests in relevant places. I pulled Trevor's tests from this PR, and a few of his test cases are currently failing, but I'll bang on that some more tomorrow.

@trevnorris, what this means is that we now have a polyfill for versions of node < (whatever version of Node this lands in). I'll keep it up to date with your changes, and you can grab a copy of continuation-local-storage and run its tests against your fork if you want another source of test coverage.

@trevnorris

This comment has been minimized.

Show comment
Hide comment
@trevnorris

trevnorris Sep 2, 2013

@othiym23 Thanks. Trying to have basics working by NodeConf.eu, but working around everything already in place has been a little painful. To be honest one problem is that I'm catching more asynchronous callbacks then expected. Which make the tests unpredictable. So I need to trace down where every one is occurring so they can be accounted for.

trevnorris commented Sep 2, 2013

@othiym23 Thanks. Trying to have basics working by NodeConf.eu, but working around everything already in place has been a little painful. To be honest one problem is that I'm catching more asynchronous callbacks then expected. Which make the tests unpredictable. So I need to trace down where every one is occurring so they can be accounted for.

@bnoordhuis

View changes

Show outdated Hide outdated lib/timers.js
@bnoordhuis

View changes

Show outdated Hide outdated src/async_wrap.h
@bnoordhuis

View changes

Show outdated Hide outdated src/async_wrap.h
@bnoordhuis

View changes

Show outdated Hide outdated src/async_wrap.h
@bnoordhuis

View changes

Show outdated Hide outdated src/async_wrap.h
@bnoordhuis

View changes

Show outdated Hide outdated src/env-inl.h
@bnoordhuis

View changes

Show outdated Hide outdated src/env.h
@bnoordhuis

View changes

Show outdated Hide outdated src/fs_event_wrap.cc
@bnoordhuis

View changes

Show outdated Hide outdated src/node.cc
@bnoordhuis

View changes

Show outdated Hide outdated src/node.cc
@bnoordhuis

View changes

Show outdated Hide outdated src/node.cc
@bnoordhuis

View changes

Show outdated Hide outdated src/node.cc
@bnoordhuis

View changes

Show outdated Hide outdated src/env-inl.h
@bnoordhuis

View changes

Show outdated Hide outdated src/node.cc
@bnoordhuis

View changes

Show outdated Hide outdated src/node.cc
@bnoordhuis

View changes

Show outdated Hide outdated src/node_crypto.cc
@bnoordhuis

View changes

Show outdated Hide outdated src/node_crypto.cc
@othiym23

View changes

Show outdated Hide outdated lib/domain.js
if (this._disposed)
return;
this.enter();
var ret = fn.call(this);

This comment has been minimized.

@othiym23

othiym23 Oct 4, 2013

I don't know about the JIT implications of this (my understanding is that single-call try clauses don't inhibit optimization, but I could be wrong), but I use this idiom in the asyncListener polyfill:

try {
  return fn.call(this);
}
finally {
  this.exit();
}
@othiym23

othiym23 Oct 4, 2013

I don't know about the JIT implications of this (my understanding is that single-call try clauses don't inhibit optimization, but I could be wrong), but I use this idiom in the asyncListener polyfill:

try {
  return fn.call(this);
}
finally {
  this.exit();
}

This comment has been minimized.

@trevnorris

trevnorris Oct 4, 2013

The listener is supposed to stay active if there's an error. That's how _fatalException() knows what error callbacks to call.

@trevnorris

trevnorris Oct 4, 2013

The listener is supposed to stay active if there's an error. That's how _fatalException() knows what error callbacks to call.

This comment has been minimized.

@isaacs

isaacs Oct 28, 2013

@trevnorris It could still work, but it would mean moving the domain.exit() logic out of _fatalException() and having it run in the finally block.

@isaacs

isaacs Oct 28, 2013

@trevnorris It could still work, but it would mean moving the domain.exit() logic out of _fatalException() and having it run in the finally block.

This comment has been minimized.

@isaacs

isaacs Oct 28, 2013

That is, if you have a throwing fn, the flow would be:

try block
enter domain
fn() up until the throw
!throw!
error handling logic (_fatalException, etc)
finally block
this.exit()

For a non-throwing fn, it'd be:

try block
enter domain
fn()
set return value
finally block
this.exit()
return
@isaacs

isaacs Oct 28, 2013

That is, if you have a throwing fn, the flow would be:

try block
enter domain
fn() up until the throw
!throw!
error handling logic (_fatalException, etc)
finally block
this.exit()

For a non-throwing fn, it'd be:

try block
enter domain
fn()
set return value
finally block
this.exit()
return
@othiym23

View changes

Show outdated Hide outdated lib/events.js
@isaacs

This comment has been minimized.

Show comment
Hide comment
@isaacs

isaacs Nov 1, 2013

Everything relevant is landed in master now, right? Reopen if there's still work to do here.

Thanks for all the work on this, @othiym23, @jacobgroundwater, and especially @trevnorris.

isaacs commented Nov 1, 2013

Everything relevant is landed in master now, right? Reopen if there's still work to do here.

Thanks for all the work on this, @othiym23, @jacobgroundwater, and especially @trevnorris.

@isaacs isaacs closed this Nov 1, 2013

@trevnorris trevnorris deleted the trevnorris:flippin-tick-thing branch Nov 1, 2013

@refack

This comment has been minimized.

Show comment
Hide comment
@refack

refack Jun 25, 2014

Member

@trevnorris why arenet incomming events created out-of band?
for the following code the second assert fails

                namespace.set('test', TEST_VALUE);
                server = net.createServer();
                server.on('connection', function OnServerConnection(socket) {
                    expect(namespace.get('test')).equal(TEST_VALUE, "state should be preserved");  // Still have state
                    socket.on("data", function OnServerSocketData(data) {
                        expect(namespace.get('test')).equal(TEST_VALUE, "state should still be preserved :(");  // cls is lost
Member

refack commented Jun 25, 2014

@trevnorris why arenet incomming events created out-of band?
for the following code the second assert fails

                namespace.set('test', TEST_VALUE);
                server = net.createServer();
                server.on('connection', function OnServerConnection(socket) {
                    expect(namespace.get('test')).equal(TEST_VALUE, "state should be preserved");  // Still have state
                    socket.on("data", function OnServerSocketData(data) {
                        expect(namespace.get('test')).equal(TEST_VALUE, "state should still be preserved :(");  // cls is lost
@bnoordhuis

This comment has been minimized.

Show comment
Hide comment
@bnoordhuis

bnoordhuis Jun 25, 2014

Member

@refack I'm not sure why you direct that question to me.

Member

bnoordhuis commented Jun 25, 2014

@refack I'm not sure why you direct that question to me.

@refack

This comment has been minimized.

Show comment
Hide comment
@refack

refack Jun 25, 2014

Member

Sorry ment to ping @trevnorris

Member

refack commented Jun 25, 2014

Sorry ment to ping @trevnorris

@trevnorris

This comment has been minimized.

Show comment
Hide comment
@trevnorris

trevnorris Jun 25, 2014

@refack If I understand the question, it's because internally we don't know whether you're creating a Pipe or a TCP connection until you call listen(). It's a total PITA. I have another PR open that does some work on this, but still under debate how it should be merged.

trevnorris commented Jun 25, 2014

@refack If I understand the question, it's because internally we don't know whether you're creating a Pipe or a TCP connection until you call listen(). It's a total PITA. I have another PR open that does some work on this, but still under debate how it should be merged.

@RobinQu

This comment has been minimized.

Show comment
Hide comment
@RobinQu

RobinQu Jun 1, 2015

Is it landed on any released version?
I still cannot find it in the latest(v0.12.2)

RobinQu commented Jun 1, 2015

Is it landed on any released version?
I still cannot find it in the latest(v0.12.2)

@trevnorris

This comment has been minimized.

Show comment
Hide comment
@trevnorris

trevnorris commented Jun 1, 2015

@RobinQu Never landed.

@gobwas

This comment has been minimized.

Show comment
Hide comment
@gobwas

gobwas Jun 30, 2015

So, will node have this feature some day?

gobwas commented Jun 30, 2015

So, will node have this feature some day?

@trevnorris

This comment has been minimized.

Show comment
Hide comment
@trevnorris

trevnorris Jun 30, 2015

@gobwas It partially exists as process.binding('async_wrap'). Though the ability to capture exceptions was removed. Became a massive hair ball. Would still like to do this, but time constraints.

EDIT: It's in io.js, not node.js.

trevnorris commented Jun 30, 2015

@gobwas It partially exists as process.binding('async_wrap'). Though the ability to capture exceptions was removed. Became a massive hair ball. Would still like to do this, but time constraints.

EDIT: It's in io.js, not node.js.

@gobwas

This comment has been minimized.

Show comment
Hide comment
@gobwas

gobwas Jun 30, 2015

Thank you, @trevnorris!

Does this feature present in iojs docs?

Any way, I am asking about async-listener functionality at the moment..?

gobwas commented Jun 30, 2015

Thank you, @trevnorris!

Does this feature present in iojs docs?

Any way, I am asking about async-listener functionality at the moment..?

@trevnorris

This comment has been minimized.

Show comment
Hide comment
@trevnorris

trevnorris Jun 30, 2015

Oh, that died a horrible death. After 3 months of work I still couldn't figure out a few key edge cases. So instead I've been working on the set of hooks necessary to implement a feature like async-listener. This way others can experiment and publish their own modules.

trevnorris commented Jun 30, 2015

Oh, that died a horrible death. After 3 months of work I still couldn't figure out a few key edge cases. So instead I've been working on the set of hooks necessary to implement a feature like async-listener. This way others can experiment and publish their own modules.

@gergelyke

This comment has been minimized.

Show comment
Hide comment
@gergelyke

gergelyke Nov 13, 2015

Hi @trevnorris ,

could you give us an update on the current status of this? What would be your suggestion to use these days if we want to active the same functionality?

Thanks!

cc @Peteyy

gergelyke commented Nov 13, 2015

Hi @trevnorris ,

could you give us an update on the current status of this? What would be your suggestion to use these days if we want to active the same functionality?

Thanks!

cc @Peteyy

@trevnorris

This comment has been minimized.

Show comment
Hide comment
@trevnorris

trevnorris Dec 1, 2015

@gergelyke Unfortunately there isn't an alternative. Will have to continue using domains for the time being. Though discussion about reimplementing this has started back up. Hopefully it will lead to something.

trevnorris commented Dec 1, 2015

@gergelyke Unfortunately there isn't an alternative. Will have to continue using domains for the time being. Though discussion about reimplementing this has started back up. Hopefully it will lead to something.

@gergelyke

This comment has been minimized.

Show comment
Hide comment
@gergelyke

gergelyke commented Dec 1, 2015

thanks!

@trevnorris trevnorris referenced this pull request Dec 3, 2015

Closed

AsyncWrap for LTS Argon #59

@naorye

This comment has been minimized.

Show comment
Hide comment
@naorye

naorye Dec 10, 2015

What is wrong with the Domain solution?

naorye commented Dec 10, 2015

What is wrong with the Domain solution?

@trevnorris

This comment has been minimized.

Show comment
Hide comment
@trevnorris

trevnorris Dec 10, 2015

@naorye It's a complete hack that injects itself all over core code. By design it can't properly catch everything. There are edge cases it's missing. This is because it's injected at the JS API level, not at the native level where the asynchronous requests are actually made.

trevnorris commented Dec 10, 2015

@naorye It's a complete hack that injects itself all over core code. By design it can't properly catch everything. There are edge cases it's missing. This is because it's injected at the JS API level, not at the native level where the asynchronous requests are actually made.

@Olegas

This comment has been minimized.

Show comment
Hide comment
@Olegas

Olegas Dec 12, 2015

What alternatives are now being discussed?

Olegas commented Dec 12, 2015

What alternatives are now being discussed?

@trevnorris trevnorris changed the title from implement domain-like hooks for userland to implement domain-like hooks (asynclistener) for userland Jul 25, 2016

@jiaqifeng

This comment has been minimized.

Show comment
Hide comment
@jiaqifeng

jiaqifeng Aug 30, 2017

@trevnorris Is there any other solutions based on native layer of this problem currently available? I am a Java coder, I think if this should be done on VM layer which make things easier and more efficient.

jiaqifeng commented Aug 30, 2017

@trevnorris Is there any other solutions based on native layer of this problem currently available? I am a Java coder, I think if this should be done on VM layer which make things easier and more efficient.

@trevnorris

This comment has been minimized.

Show comment
Hide comment
@trevnorris

trevnorris Sep 18, 2017

@jiaqifeng In master there's a native API that can be used (see all functions that use async_context in node.h). This will be backported to v8.x, but no further.

trevnorris commented Sep 18, 2017

@jiaqifeng In master there's a native API that can be used (see all functions that use async_context in node.h). This will be backported to v8.x, but no further.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.