-
Notifications
You must be signed in to change notification settings - Fork 29.8k
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
AsyncLocalStorage without Async Hooks #46265
Comments
tl;dr I'm +1 on this
Bit more context about the APIs that would go in this case:
Side node, as of today, only |
Do you have already an idea how to integrate this into non promise use cases where
Could you point to the location where a map is copied? actually only a reference to the store should be set on new resources (assuming here the store in an object not e.g. a
Is there some documentation available for this? In v8 headers I see only |
I'm +1 on reducing the overhead of AsyncLocalStorage to minimum. It would be a massive progress for Node.js. I'm worrying about potential breakages. Unless you want to put the new implementation behind a flag, the best time to land this semver-major change is for v21, so we have ~1 year to iron out all the bugs before it goes into a LTS line. |
I'd be fine with putting the new implementation behind a flag. Doing so might make the implementation a bit trickier with keeping both models implemented simultaneously but the risk of breakage is non-trivial. |
Yes, that's the part that is fairly complicated. In workerd we do this by capturing a counted reference to the current async context and setting the scope before entering the relevant callbacks, much like we do with the internal callback scope in Node.js. The advantage we have in workerd currently is that we have a much smaller set of async resource types so it's pretty simple. We'd effectively do the same with the new Node.js implementation but I still have to work out all of the implementation details.
Unfortunately no, the documentation for this API is severely lacking. I'm hoping the v8 folks will expand the documentation on this relatively soon. |
It's easy to implement.
This is a side-effect of V8 not fully implementing the spec's
In the spec, these are the equivalent to exposing the If you're comfortable with code splunking, the relevant links are:
That also points us to the unimplemented other job types: |
If I understand this correct the context seen in the For |
Yes. It should be unobservably the same because the
Correct.
Yup. |
Testing with my implementation of this proposal, thenables are broken: const ctx = new AsyncContext()
const queue = [];
const thenable = {
then(onRes, _onRej) {
queue.push("thenable: " + ctx.get());
onRes();
},
};
const out = ctx.run(1, () => {
queue.push("new Promise");
const p = new Promise(res => res(thenable));
queue.push("p.then");
const p2 = p.then(() => thenable);
queue.push("p2.then");
return p2.then(() => {
queue.push("promise: " + ctx.get());
});
});
queue.push("out.then");
out.then(() => {
queue.push("done");
//hook.disable();
console.log(queue);
}); [
'new Promise',
'p.then',
'p2.then',
'out.then',
'thenable: undefined',
'thenable: undefined',
'promise: 1',
'done'
] Because it uses |
I understand the goal here is to eliminate the dependence on |
I would definitely be very supportive of such a change. :) This overlaps a lot with the work I started in #42393 too, but I haven't had time to continue work on it. It's also worth noting that my changes for TracingChannel in #44943 includes some efforts to eliminate the need for I think we have a clear path to a good state here, there's just a bunch of work that needs to be done to get there. |
This would seem to point a way forward for actually, finally, honest-to-god deprecation of
@jasnell Could we perhaps have this live somewhere other than on cc @nodejs/modules @nodejs/loaders ; related: #45711 |
@GeoffreyBooth I don't agree that this provides a path to deprecating async_hooks. There are other uses outside of context management. It would get the biggest use case off the costly and at times unstable core of async_hooks which I see as a major advantage though. Also makes it easier to restructure async_hooks if we get the highly depended on use case of context management off of it, making breaking changes less ecosystem impacting. |
Would the experimental |
Hmmm good question. I'm not sure to be honest... I don't think so? At least not in its current form. In the new model that I'm proposing, the async context is not propagated every time a new AsyncResource is created but every time the context is mutated using What I'm thinking would likely be a better solution (and more likely to be something we could implement with this new model) is to associate a tag/namespace with the store that must be echoed back in the const { AsyncLocalStorage } = require('async_hooks');
const als = new AsyncLocalStorage({ tag: 'abc' });
const { 0: fn1, 1: fn2 } = als.run(123, () => {
return [
AsyncResource.bind(() => als.getStore('abc')),
AsyncResource.bind(() => als.getStore()),
]
});
console.log(fn1()); // 123, because the tag passed in to als.getStore matched
console.log(ln2()); // undefined, because the tag did not match. |
Sounds pretty similar to @jridgewell's investigation tc39/proposal-async-context#15 on the mutation of the global state and the possible optimization based on the assumption. |
I honestly think we should consider re-thinking |
I'll open an issue specifically to discuss |
@jasnell corrected your link above. |
I've recently gone through and implemented a subset of the
AsyncLocalStorage
API for Cloudflare's workerd runtime, and I've done so without implementing any part of the async_hooks API under the covers, proving that it's not only possible but results in a significant performance improvement and greatly simplified implementation. I propose that we could greatly improve our implementation by eliminating dependency on async_hooks and adopting a model similar to what we've implemented in workerd.First, an overview of how things currently work in Node.js...
AsyncLocalStorage
instance, there is an associated hidden property on the resource associated with the current execution context. Wheneverals.run(...)
is called, we temporarily mutate the value associated with that key and run a function. Any async resources that are created while that function is running ends up receiving a full copy of that map.This design ends up being very inefficient as it means we are allocating and copying the context for every single object. For applications that are particularly promise heavy, this gets very expensive very quickly.
The way we implemented this in workerd is far more efficient:
At any given time, there is a current AsyncContextFrame. By default there is a logical "root" frame but we do not actually need to allocate anything to represent it.
Whenever we call
als.run(...)
, we create a newAsyncContextFrame
that inherits the context of the current, and set the new frame as current. Any async resource that is created simply acquires a reference to the currentAsyncContextFrame
.The context is only copied when a new
AsyncContextFrame
is created, which happens far less frequently than creating a new async resource.Further, we do not need to rely on V8's PromiseHooks API to propagate context through promises. There is a built-in v8 API
v8::Context::SetContinuationPreservedEmbedderData()
that can do the propagation for us much more efficiently. Whenever we enter anAsyncContextFrame
, we set theContinuationPreservedEmbedderData
to an opaque object that wraps a reference to the frame. V8 takes care of properly associating that with all promise continuations.There's a lot more to it that I'm glossing over here but the basic idea is that
AsyncLocalStorage
is absolutely possible to implement withoutasync_hooks
and I think there's a ton of value in doing so.There are some caveats:
als.enterWith(...)
andals.disable()
APIs. With the new model, these would need to be removed as they make things way more complicated than they are worth.unhandledRejection
event would need to change (see AsyncLocalStorage and deferred promises #46262)The point of this issue is to see if there is interest in an approach to AsyncLocalStorage that is completely decoupled from
async_hooks
before @flakey5 and I decide whether or not to work on it./cc @mcollina @vdeturckheim @bmeck @bengl @nodejs/async_hooks
The text was updated successfully, but these errors were encountered: