-
Notifications
You must be signed in to change notification settings - Fork 30.2k
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: inconsistent propagation of nested async context when resolving outer promise #45848
Comments
Based on my reading of nodejs/help#3041, it sounds like the nested async context should not propagate to the outer async context. So it seems like:
|
Some more examples copied/adapted from this comment:
Note that 2 and 3 are identical, except that 2 does some other ALS stuff beforehand. Also worth pointing out that behavior doesn't change with different async styles: async function main() {
await (async () => { await 42; als.enterWith("inner nested async"); })();
console.log(als.getStore()); // "inner nested async" 😱
} and even deeply nested async: async function main() {
await (async () => {
await 42;
await (async () => {
await 42;
await (async () => {
await 42;
als.enterWith("inner nested async");
})();
})();
})();
console.log(als.getStore()); // "inner nested async" 😱
} |
Demonstration of how it only works the very first time: const { AsyncLocalStorage } = require("async_hooks");
const als = new AsyncLocalStorage();
async function main() {
await Promise.resolve().then(() => als.enterWith("inner nested async"));
console.log(als.getStore());
await Promise.resolve().then(() => als.enterWith("something else"));
console.log(als.getStore());
}
main().then(main); Outputs:
If the desired behavior is propagation, then we should expect alternating |
You can blame async_hooks for that. Because AsyncLocalStorage doesn't enable async_hooks until run or enterWith is called, async_hooks will not be producing events until that point. Basically when the resource stack unwinds there will be nothing to unwind to so it will just leave it as-is because enterWith doesn't have an exit condition. This is one of the many reasons I recommend against using enterWith and have been working on a safer replacement. |
Thanks for the context! Is it correct to assume that "fixing" this Looking forward to the safer replacement! 🙌 |
The list of gotchas with The alternative I'm working on for the currently known use cases for it, which is not immediately obvious, is in #44943. Look at the |
Maybe we should add an option to Once Sure, AsyncHooks have overhead. But I assume that most applications instantiating |
Probably sufficient to just enable the hooks in AsyncLocalStorage construction rather than run. |
I think that could work for the jest case, though it's a smidge more verbose than just using (the problematic) const contextStore = new AsyncLocalStorage();
function testWithContext(name, context, fn) {
test(name, () => contextStore.run(context, fn));
}
//...
testWithContext("my test", "some context", () => {
expect(contextStore.getStore()).toEqual("some context");
}); |
Version
v18.12.1
Platform
Darwin nfml-jonj5MM 21.6.0 Darwin Kernel Version 21.6.0: Wed Aug 10 14:25:27 PDT 2022; root:xnu-8020.141.5~2/RELEASE_X86_64 x86_64
Subsystem
async_hooks
What steps will reproduce the bug?
Run this repro:
How often does it reproduce? Is there a required condition?
The test fails every time, unless you tweak it as indicated in the comments, in which case it always passes.
What is the expected behavior?
It should behave consistently independent of other ALSes. It should either always propagate the inner async context, or it never should.
What do you see instead?
Additional information
This was originally reported as a Jest bug, but further debugging indicates it's an issue in Node.js. The basic summary is if you:
enterWith
orrun
) (e.g. in a test suite'sbeforeEach
)beforeEach
)I think an argument could be made that this is (mostly) working as designed since the store is being set in an inner async context and shouldn't apply to other operations in the
testHarness
(see repro). But if that's the case, why does it propagate when there is no other ALS? Ideally the behavior should be consistent independent of the unrelated ALS, and the docs could probably be clarified a bit.Here's a Jest repro that might be more representative of how an end-user could encounter this issue. Since Jest has no
aroundEach
functionality, end-users might be tempted to try to set a store along these lines, which could then encounter context loss:From a Jest end-user perspective the test runner code is a black box, so it would be nice if the ALS context propagated to those outer chained async operations. While there are workarounds like
AsyncResource.bind()
, they may not be ergonomic or practical for end-users. That said, I could see that kind of propagation being problematic and having unintended consequences, so it might be better to make it never work (i.e. even in the no-other-ALS case), and make the docs a little more clear.The text was updated successfully, but these errors were encountered: