-
Notifications
You must be signed in to change notification settings - Fork 30k
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
Integrate AsyncLocalStorage with EventEmitter #33723
Comments
The goal of the method is to set the context on an existing emitter? I'm a bit confused about why this is needed for the server case, wouldn't the request handler itself already run in the context? Edit: closed (and reopened) due to butterfingers. |
Yes, that's right.
The handler itself is run in the context, but some of events emitted for 'use strict';
require('../common');
const assert = require('assert');
const { AsyncLocalStorage } = require('async_hooks');
const http = require('http');
const asyncLocalStorage = new AsyncLocalStorage();
const server = http.createServer((req, res) => {
asyncLocalStorage.run(42, () => {
req.on('close', () => {
assert.strictEqual(asyncLocalStorage.getStore(), 42); // fails
});
assert.strictEqual(asyncLocalStorage.getStore(), 42);
res.end('ok');
});
});
server.listen(0, () => {
http.get({ host: 'localhost', port: server.address().port }, () => {
server.close();
});
});
No problems at all. Happens with everyone. |
@puzpuzpuz Have you investigated where the context gets lost here? Besides the HTTP example I agree that this seems helpful - in special as there are a lot user space modules out there using emitters but don't care that much about async context used. I'm not sure if binding a complete emitter is the right solution. Maybe this should at least allow to limit it to some events. |
Wouldn't it make more sense to expose something that allows the user to bind any function / object method instead of specifically EventEmitter#emit ? |
I don't think the same context for req/res's event listeners as for the request listener was even guaranteed, was it? Some of them are emitted as a part of Line 546 in 32b641e
An optional filter argument for events could probably help. |
Probably, yes. Something similar to const store = { foo: 'bar' };
als.bind(req.emit, store);
req.on('close', () => {
console.log(als.getStore()); // prints "{ foo: 'bar' }"
}); |
Not the same context but I would assume that context passing done via async hooks is able to link them similar as link via e.g. |
So … trying to sort my thoughts here, and I’ll apologize for the long comment in advance. Firstly, the idea to bind an existing Secondly, unless I’m misunderstanding something, it feels like ALS is not the right level to approach this, because the problem is that the async tracking does not work as expected. In particular, in the example in #33723 (comment), the problem is that Thirdly, we bascially kind of ran into this kind of problem with That makes it seem to me like we want to do any of:
As an alternative, or additionally, we could also:
If I were to put myself in a position where I didn’t know a lot about Node.js internals, I think Option 2 is something that would make sense to be, because I would expect process.nextTick(() => doSomething()); and ee.on('event', () => doSomething()); to behave similarly – i.e. to run the callback in an async context that corresponds to where it was first registered. I think all of these options would come with some perf impact, but also mean that we need to do less async tracking in the Node.js C++ internals overall, because we can let the wrapper objects (which often are Then, finally, I just had a short conversation with @ronag about how this would work in streams, and basically, the conclusion that I came to was that it would make sense for streams to also use custom |
@addaleax Yep, idea 3 is basically what I’ve had in mind for awhile. I’m currently working on making a callback trampoline for InternalMakeCallback which I plan on being able to reuse for this sort of scenario to wrap JavaScript-side callbacks and emit the related events as much as possible purely on the JS-side. By avoiding the barrier cross performance hit we should be able to make the cost of tracking additional relevant barriers like event emitters and streams less significant by relying on V8 to inline what it needs. |
I'm also a fan of option 3. |
No need to apologize. Your comment is really valuable. 👍
With the default behavior of EE consumers (listeners) of the same event will be executed within the same context, as the execution happen synchronously when BTW if one of listeners needs a separate store, it can be wrapped with
EE itself has nothing to do with async resources, so I'm not sure where it's correct to say " That's why I'm not sure what should be the right level to approach this. But certainly EE API itself is not a problem and shouldn't be changed. But we may consider changing behavior of certain EEs returned by core APIs, like
Yes, that won't solve the discussed issue.
That could work if we properly integrate (say, by monkey-patching them) certain EEs used in the core with
That won't work in my case (and probably in other users's cases). I'm not in control of user code and other middlewares, so I can't force them to wrap EE listeners with something like
I would have such expectations as EEs themselves do not imply async operations (resources), only CPS. But, once again, some of EEs (or all of them?) used in core APIs may be treated in a special way.
Yes, streams (certain streams used in core APIs, of course) are close to EEs. |
@addaleax what would you say of |
BTW Such API is close to |
I think the correct place for this is in |
Many thanks for your inputs! I'm going to submit a PR for |
Here it goes: #33736 |
Closing this one as it can be implemented via language features and the consensus is to avoid bloating the API. |
@puzpuzpuz I’ll reopen this since I’d still like to discuss whether we want to implement some form of built-in async tracking for EventEmitters in general, i.e. option 1 or 2 from above. |
Just copying in @jasnell's comment from #33749. I feel it might be relevant here:
While simultaneously highlighting @addaleax's comment from earlier in this thread:
|
@jasnell I wonder what was the approach? Did you try to implicitly create an |
I just want to add how important that this. Consider this common use case of writing a simple express application and using body-parser to parse the JSON payload: const express = require('express');
const bodyParser = require('body-parser');
const als = new AsyncLocalStorage();
const app = express();
app.use((req, res, next) => als.run({ requestId: generateId() }, next) );
app.use(bodyParser.json());
app.use((req, res, next) => {
als.getStore(); // fails
next();
}); Since body-parser uses event emitters, the This extends to many other libraries that use events and streams -- remote calls, db, etc. |
@eyalroth this is known and it's a problem inside express. If you would like some more details on this: nestjs/nest#8837 That's the exact same issue you are facing. The problem is in body-parser and not Node.js. The fix is also linked in the issues and ideally should be added to express too. |
@mcollina Interesting. I tried to replace const ee = new EventEmitter();
app.use((req, res, next) => {
ee.prependOnceListener('foo', next);
setTimeout(() => ee.emit('foo'), 100);
}); and indeed it worked. It looks like under the hood body-parser is calling raw-body which only invokes app.use((req, res, next) => {
req.on('data', () => console.log('data'));
req.prependOnceListener('end', next);
}); which indeed doesn't work. Perhaps this breaks something with express which expects all middlewares to complete before the |
No. It's because the |
@mcollina I see. I'm not familiar with Is that the intended usage? const resources = {};
app.use((req, res, next) => {
const requestId = generateId();
res.locals.requestId = requestId;
als.run({ requestId }, () => {
resources[requestId] = new AsyncResource('foo');
next();
});
});
app.use(bodyParser.json());
app.use((req, res, next) => {
const requestId = res.locals.requestId;
resources[requestId].runInAsyncScope(() => {
als.getStore(); // works
next();
});
}); If so, I'm assuming that there's no way to guarantee that my code -- that may invoke third-party async/callback functions -- will continue to run in the same execution context? After all, any third party async function might be resolved under the hood by a different execution context, and I have to manually handle each of these cases myself? |
Either it works or it doesn't, it won't be "random". Most libraries handle this internally, so you don't need to worry about it. Express is a fairly old codebase that was written before none of the mechanism existed and it has not been significantly updated. |
@mcollina Yep I didn't mean randomly but more like "it could potentially happen with any library". Thank you for taking the time to explain all of this! |
@Qard are you still interested in pursuing this issue or it could be closed? |
Maybe at some point, but I think the intent of this issue is dealt with so can probably close this. |
Is your feature request related to a problem? Please describe.
I'm going to port this library to
AsyncLocalStorage
. As a part of this migration I need to make ALS play nicely with events emitted by request and response objects. Current version of the library usesns.bindEmitter()
method available incls-hooked
(it's also present incontinuation-local-storage
).Describe the solution you'd like
It's enough to add
.bindEmitter(emitter, store)
method into ALS:Describe alternatives you've considered
Of course, this integration can be implemented as a user-land module. But I believe that this will be a common case among ALS users, so it's probably worth to consider integrating ALS with
EventEmitter
in the core.As more of such changes can bloat ALS API, I'd like to hear opinions from @nodejs/async_hooks before starting to work on the implementation.
Update. The consensus is to add something like
AsyncResource#bind()
method that would allow to do the following:The text was updated successfully, but these errors were encountered: