-
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
Ability for async functions to retreive their CLS context #32062
Comments
/cc @vdeturckheim |
My understanding is that there is no particular
Why not? I think that’s the thing that conceptually makes the most sense here. |
We did not want to make the As there might be multiple const { AsyncLocalStorage } = require('async_hooks');
const asl1 = new AsyncLocalStorage();
const asl2 = new AsyncLocalStorage();
asl1.run(new Map(), () => {
asl2.run(new Map(), () => {
AsyncLocalStorage.getActive(); // returns [asl1, asl2];
});
}); I don't have strong opinions here appart that I'd like to leave the owner of an |
@addaleax - the problem with the global approach is that we need to explicitly maintain the association of |
@gireeshpunathil I think there might be a misunderstanding about how ALS works. In particular,
I don’t quite understand this question… can you maybe give a more concrete use case as an example? |
sure:
Is it possible to achieve this using [ edit: couple of typos] |
@gireeshpunathil in that case you would use one single ASL: const asl = new AsyncLocalStorage();
server.on('request', (req, res) => {
asl.run({ req, res}, () => {
// all calls to asl.getStore() wil return {req, res}
});
}); Would that make sense in your example? |
@vdeturckheim - here is a complete example that exemplifies my use case, but using your suggestion of using a single ALS: const { AsyncLocalStorage } = require('async_hooks')
const http = require('http')
const cls = new AsyncLocalStorage()
let store
let index = 0
const server = http.createServer((q, r) => {
r.end((index++).toString().repeat(1024 * 1024 * 10))
})
server.listen(12000, () => {
cls.run(new Map(), () => {
for(let i = 0; i < 10; i++) {
const req = http.get('http://localhost:12000', (res) => {
const data = ''
store = cls.getStore()
store.set('data', data)
res.on('data', ondata)
res.on('end', onend)
})
req.end()
}
})
})
function ondata(d) {
// keep store globally due to a bug, ref: https://github.com/nodejs/node/issues/32060
// const store = cls.getStore()
if (store && store.has('data')) {
let chunk = store.get('data')
chunk += d
store.set('data', chunk)
} else {
console.log('error...')
}
}
function onend() {
// let store = cls.getStore()
if (store && store.has('data')) {
let chunk = store.get('data')
var re = new RegExp(chunk[0], 'g')
console.log(`stream type: ${chunk[0]}`)
console.log(`stream length: ${chunk.replace(re, '').length}`)
} else {
console.log('ended, but in error...')
}
} the test does these:
the output shows they are not, instead the data is all clobbered between various responses
expected output:
|
cls.run(new Map(), () => {
for(let i = 0; i < 10; i++) { should be for(let i = 0; i < 10; i++) {
cls.run(new Map(), () => { otherwise all the requests share a single store. |
ok, I can test this with #32063 only, because with this change, we will have 10 stores, and with the |
@addaleax - with the patch of your PR, and with your suggested patch, the code is working as expected, thanks! so a single instance of that is what confused me. However, with a single object able to anchor all the stores in an application, keeping it global makes sense to me, and that addresses my use case as well. Not sure if there are other scenarios where this is not the case. Keeping it open for a couple of days to see if any. thanks once again! |
Because different cls1.run(new Map(), () => {
cls2.run(new Map(), () => {
x()
});
cls2.run(new Map(), () => {
y()
});
}); You’ll see (But I mostly think it’s the fact that we don’t want ALS from different modules to conflict.) |
Add a new scenario of multiple clients sharing a single data callback function managing their response data through AsyncLocalStorage APIs Refs: nodejs#32063 Refs: nodejs#32060 Refs: nodejs#32062 (comment) Co-authored-by: Gireesh Punathil <[email protected]>
Add a new scenario of multiple clients sharing a single data callback function managing their response data through AsyncLocalStorage APIs Refs: #32063 Refs: #32060 Refs: #32062 (comment) Co-authored-by: Gireesh Punathil <[email protected]> PR-URL: #32082 Reviewed-By: Anna Henningsen <[email protected]> Reviewed-By: James M Snell <[email protected]>
Add a new scenario of multiple clients sharing a single data callback function managing their response data through AsyncLocalStorage APIs Refs: #32063 Refs: #32060 Refs: #32062 (comment) Co-authored-by: Gireesh Punathil <[email protected]> PR-URL: #32082 Reviewed-By: Anna Henningsen <[email protected]> Reviewed-By: James M Snell <[email protected]>
|
@lroal the goal here is more to clarify the use and the features of the new core API serving that goal I believe :) |
Add a new scenario of multiple clients sharing a single data callback function managing their response data through AsyncLocalStorage APIs Refs: #32063 Refs: #32060 Refs: #32062 (comment) Co-authored-by: Gireesh Punathil <[email protected]> PR-URL: #32082 Reviewed-By: Anna Henningsen <[email protected]> Reviewed-By: James M Snell <[email protected]>
Wouldn't it be convenient to use a static dictionary object instead of passing around the cls instance to all modules. It would just be helper methods. Something like this: //to create a store on a key
let cls = AsyncLocalStorage.create('someKey'); //to get the current store on a key.
let cls = AsyncLocalStorage.get('someKey'); |
@lroal I think it mighe be room for an ecosystem module to do this. In general, I would want to ensure someone can prevent anyone else from accessing their instance within the process. |
I understand your argument, but I still think it should be implemented directly in nodejs - not in an ecosystem module. If you have both alternatives you still have the option to go for the safer new AsyncLocalStorage. I think the pros outweigh the cons of such a helper function. |
Once such feature is implemented in the core, library authors won't have isolation enforcement for their ALS instances. And isolation is a really strong guarantee (I'd say, a must have) when you, for instance, implement an APM agent. Although, it doesn't prevent user-land modules for implementing such feature, as well as a different overall API. As for the performance, the main cost is related with the first active ALS instance. Subsequent instances (if there are not lots of them, of course) should be significantly "cheaper" than the first one. I have no benchmark results that would demonstrate this, but if you're really curious you could slightly modify |
isolation guarantee - aren't the stores inherently isolated from out of sequence accesses? i.e, an ALS store is only visible to the asynchronous chain in which it is created? even with the current isolation, cross-module access is possible? (a store created by an asynchronous sequence in module A, accessed by a consuming module B) Is visibility / modularity / security an ALS theme at all? |
Yes, stores are isolated from outer async call chain accesses. But my point was about instance isolation. Let's consider a middleware library A that has an ALS instance and stores something in the context at the beginning of the request processing. If a library B has the access to library A instance, nothing prevents that library from calling
Nothing prevents users from sharing ALS instance with other modules or any user code. But that has to be explicit, i.e. done by library authors/app developers themselves. ALS instances are self-contained and once you create one, it's up to you to decide whether you want to share it or not.
I believe that visibility and modularity are necessary for ALS, but only in terms of instance isolation. Security is probably out of scope. |
If you create a library, like a middleware, then new AsyncLocalStorage() is the safe way to go. |
Why do you think it would be clunky? Sharing an ALS instance between modules is a matter of exporting the object in one module and importing it in others. Also, most applications shouldn't require more than one ALS instance and, probably, don't even need to share ALS across modules. In a simple request id logging case, the logging module can encapsulate ALS and only expose a middleware and logging functions. If you can describe concrete examples when ALS seems clunky, it would be easier to suggest on approaches and best practices. |
Add a new scenario of multiple clients sharing a single data callback function managing their response data through AsyncLocalStorage APIs Refs: nodejs#32063 Refs: nodejs#32060 Refs: nodejs#32062 (comment) Co-authored-by: Gireesh Punathil <[email protected]> PR-URL: nodejs#32082 Reviewed-By: Anna Henningsen <[email protected]> Reviewed-By: James M Snell <[email protected]>
When wrapping the ALS instance in a module, you typically don't want to require it by relative path in multiple places down the hierarchy. So, you go on create a separate package.json for it and reference it as a file module. E.g. in "requestContext": "file:../requestContext" . (Don't get me wrong. I am really excited about the new Async Local Storage. And I am looking forward to finally getting rid of Domain. ) To avoid name collisions you could always use a symbol as key. (But it would be great if it worked with string keys as well): let key = Symbol();
let cls = AsyncLocalStorage.create(key); let cls = AsyncLocalStorage.get(key); |
@lroal this is a lot of great points!
Indeed, ASL should help us get rid of domain in 99% of the current usages of domain. When working on this feature, error management was one of my first-class design goals. If you have time and are a user of domain, it would be really interesting to get transition feedback. As you say, it is still time to change the API.
You would need to share this symbol somehow right? Isn't that the same as placing the ASL instance in a package? |
Yes, I have been using domain a lot - both in open source libs and in production code at work. I am interested in giving feedback. |
Add a new scenario of multiple clients sharing a single data callback function managing their response data through AsyncLocalStorage APIs Refs: nodejs#32063 Refs: nodejs#32060 Refs: nodejs#32062 (comment) Co-authored-by: Gireesh Punathil <[email protected]> PR-URL: nodejs#32082 Reviewed-By: Anna Henningsen <[email protected]> Reviewed-By: James M Snell <[email protected]>
Add a new scenario of multiple clients sharing a single data callback function managing their response data through AsyncLocalStorage APIs Refs: #32063 Refs: #32060 Refs: #32062 (comment) Co-authored-by: Gireesh Punathil <[email protected]> PR-URL: #32082 Reviewed-By: Anna Henningsen <[email protected]> Reviewed-By: James M Snell <[email protected]>
@lroal I think something like https://www.npmjs.com/package/@northscaler/continuation-local-storage is what you're looking for. I'll see about enhancing it to also support |
But I agree that it would be nice for class |
@matthewadams thanks. I wrote my own cls library some time ago. node-cls . Though, I was hoping AsyncLocalStorage would replace it entirely. But for me this is not the case yet. |
@lroal I like your "Await instead of run" feature -- that's a new one on me. I guess in the meantime, I guess we'll just have to Roll Our Own™️ until such time as |
I am not sure I get this discussion here. Getting the
Is there a difference between this and
I would be happy to review any PR adding features on this API, also, my opinions can be wrong and an healthy discussion with more collaborators can probably bring ideas here. |
It appears not. I wasn't aware of |
The only thing to understand here, IMHO, is that two authors of similar packages (myself and @lroal) utilize a static map of context instances available from anywhere, as opposed to being required to have a reference to a particular instance of one, and that we were surprised not to see something similar in The proposal, at its simplest, would be to add static methods to I'd be curious to your thoughts, @vdeturckheim, after reviewing node-cls and @northscaler/continuation-local-storage to get a feel of what we expected in I'm totally open to an explanation of how our packages' behaviors are implementable using |
@matthewadams @vdeturckheim |
@matthewadams thanks for your response. As mentionned earlier in the thread, I don't see any gain in such static map. Do you have examples of situations where such a map in core would solve specific problems? @lroal |
@matthewadams @vdeturckheim @vdeturckheim , about the enterWith() vs start(). I am not sure myself. The case I had in mind was if current "thread" was busy doing something async and start() should not interfere with that context - but rather create it's own child "thread". But, maybe that's purely hypothetical. It is more like synctactical sugar perhaps. 😊 Do you think it is worth creating a PR ? Will it be considered ? It would need tests i guess. I have never create PRs in node before. |
Once again, I think the ability to hide an instance from other and that is a must have feature for me (I don't want anyone external to tamper with my instance of AsyncLocalStorage). Be ready for UX discussions :) It will needs the opinion from other collaborators and my concerns can't be the only thing taken in account. @lroal a PR would, for sure, be considered! There are also considerations about what the UX would look like ( Housekeeping part, I think this issue has derived a bit from its original point. I will close it, feel free to reopen if needed and let's follow-up either in a dedicated issue or PR. |
Thanks @vdeturckheim . |
@vdeturckheim @lroal |
FYI, we just released an update to |
Is your feature request related to a problem? Please describe.
Please describe the problem you are trying to solve.
Right now, the
AsyncLocalStorage
object needs to be externally available for the async functions to avail the store.Describe the solution you'd like
Please describe the desired behavior.
Implement a
getContextCLS
API that returns theAsyncLocalStorage
object under which this asynchronous call was initiated. Return undefined, if it was not run underAsyncLocalStorage
semantics.Illustration:
Use case: in a concurrent workload scenario, it is not easy to maintain
AsyncLocalStorage
objects globally, and across multiple async function families.Describe alternatives you've considered
Please describe alternative solutions or features you have considered.
Alternative is to share these objects globally (which is not very attractive)
The text was updated successfully, but these errors were encountered: