-
Notifications
You must be signed in to change notification settings - Fork 29.7k
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
Should EventEmitter extend AsyncResource? #34430
Comments
I'm not an expert, but events are dispatched synchronously. So if you emit an event in some async code, the context should already be correct? |
Not at all. Depends on what the root of the event is. e.g. most stream events would need a runinasynccontext when backed by e.g. a socket. Do fs methods properly propagate async context? |
I think this generally makes sense, for the same reason that the Also, just for context, there is some previous discussion in #33723. |
oh is this about propagating the context that was active when the emitter instance was created? |
That’s a valid question. Either we propagate the context of when the emitter was created, or the context of when the listener was registered. My understanding of how async_hooks works, and my understanding of what makes sense here for e.g. ALS, would be to use the async id of the listener registration, but the async trigger id of the emitter creation. In particular, for this: http.createServer((req, res) => {
asyncLocalStorage.run(store, () => {
req.on('end', () => {
const store = asyncLocalStorage.getStore();
});
});
}).listen(8080); I think it would be a common expectation that the inner |
This has always confused me. What is the practical difference of these two? i.e. how would I consume them differently? |
@ronag My understanding is that the async id propagates the when of an async call, and the trigger async id propagates the why. I think we’re definitely lacking a clear definition that can be applied programatically here, though. |
@addaleax Could http.createServer((req, res) => {
asyncLocalStorage.enterWith(store);
req.on('end', () => {
const store = asyncLocalStorage.getStore();
});
}).listen(8080); On v12.18.2 the store is still available in the callback. |
@kjarmicki That might work in your case, but it probably only does so by accident – in order to actually consume the content of |
@addaleax oh, ok 🙂 So the right way would be to wrap the http.createServer((req, res) => {
asyncLocalStorage.run(store, () => {
new Promise(resolve => req.on('end', resolve))
.then(() => {
const store = asyncLocalStorage.getStore();
});
});
}).listen(8080); |
@kjarmicki I’m not sure what you mean by “the right way”, but that should work (and using |
@addaleax by "the right way" I meant that it wouldn't be an accident that it works. |
That answers part of my question. Though I'm still unsure when & how I would practically use e.g. trigger id. I think some motivating examples in the doc would be nice in this case. |
Fwiw, I tried this once and performance of EventEmitter dropped tenfold. A better approach (albeit less ergonomic) would be to either emit from within the context you wish to propagate, or bind the handler function to it. |
The problem with this is that we basically end up recommending that everybody adds async tracking manually, which will:
|
It's a fair point, but really, not everyone is going to need the async tracking and shouldn't need to incur the cost. Perhaps an additional option would be feasible here? Similar to |
Maybe an opt-out option? i.e. default to the most "correct" option but allow to opt-out when performance is critical and the user explicitly accepts the "risks". |
Let's implement and benchmark. If the performance hit is too significant, then let's make it opt-in for 14 and 15 with the plan to make it opt-out for 16. This gives folks a reasonable transition period. |
I don't think this is feasible, but it's probably worthwhile to try anyway. Firstly, there are a lot of events that are sync in nature, don't need to forward the async context for which this would be pure overhead: an example of this are all the I think one of the critical failures in domains was monkey-patching EE to work. I think this is likely to lead with similar problems. |
@mcollina I think it might be helpful if you could explain why you consider this a problem? Is the problem the monkey-patching, or the integration of domains and EEs at all? |
This does bring an interesting scenario though. Should we have flag for emit or alternative method with a flag where a performance sensitive implementation can indicate whether the active async context is already the correct one? |
I think it will be extremely hard to think of all possible consequences if this new behavior would be added by default to all EE. |
I think the triggerId is of help if you are interested in the resource tree whereas executionId is of help to follow the code execution. All APMs I know (and AsyncLocalStorage) follow the executionId path. I haven't seen something like the triggerId path in other awaiting languages like .NET. |
I don't think that the caller of |
Good point. Considering this, it's more correct to bind each listener, not the whole |
So current consensus is that one should not expect context to propagate over events? That does make things easier but is also (at least for me) a fundamental change of practice/expectation. |
Yes, that's the consensus thus far. The recently landed const resource = new AsyncResource('foo')
const ee = new EventEmitter()
ee.on('foo', resource.bind(() => { /** Invoked within the right context **/ })) |
I think the main problem is that emitters are that generic that it's hard to come up with a correct soluation. An HTTP server is an event emitter but all APM tools I know interpret the |
Well, with const asyncResource = new AsyncResource('foo')
const ee = new EventEmitter({ asyncResource })
ee.on('bar', () => { /** implicitly bound to asyncResource **/ }); This would incur a small performance penalty when the event handler is added in order to check for the presence of the |
This would essentially move async context propagation responsibility from the implementer to the user. Is this the practice we go with in general, or just with event emitters? |
I think we have to take it case by case as I'm not sure there's a general rule that would work. |
Can we at least try to outline some kind of rules to refer to? Right now I find it personally very confusing what is expected from me as a library developer and also what I can expect as a user. We need some form of guideline here... e.g. IMO it's better to say to users that we don't propagate across events, rather than say that we sometimes propagate across events. |
To be blunt, I don’t think we can get any kind of reasonable async tracking for event emitters that isn’t implemented by the built-in And I know it’s not great to go and decrease EE performance, but if we end up recommending everybody to do the changes on the consumer side, the only difference is the massive amount of work that needs to be done in the ecosystem. |
@addaleax ... it's questionable even if it were built-in to |
Considering what's written above, we could create an But the main concern here is a certain performance penalty, as this approach will mean some litter and |
We needed context propagation on EventEmitters, and we couldn't easily bind all the In our case, we ended up overriding the asyncLocalStorage.run({}, () => {
req.emit = AsyncResource.bind(req.emit.bind(req));
/* ... */
}); |
@ronag since you're the OP and this issue has been open for 2.5 years with no movement: do you foresee this happening? |
I have no idea. I'm no longer on top of this. Feel free to close. |
Okay, I'll go ahead and close it. Holler if it should be reopened. |
fwiw a while ago |
Would simplify some things and make it more ergonomic to ensure async context propagation in various API's. Though probably with some performance impact?
The text was updated successfully, but these errors were encountered: