You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Ability for async functions to retreive their CLS context #32062
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 the AsyncLocalStorage object under which this asynchronous call was initiated. Return undefined, if it was not run under AsyncLocalStorage semantics.
Illustration:
const{ AsyncLocalStorage }=require('async_hooks');functionmain(){constcls1=newAsyncLocalStorage();constcls2=newAsyncLocalStorage();cls1.run(()=>{conststore=cls1.getStore()store.set('x','y')setTimeout(foo,1000)})cls2.run(()=>{conststore=cls2.getStore()store.set('x','z')setTimeout(foo,1000)})}functionfoo(){// obtain the cls object here that this async call was part of// for example:// const cls = getContextCLS()// const store = cls.getStore()// console.log(store.get('x')// prints 'y' and 'z' as and when those are called.}main()
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)
My understanding is that there is no particular AsyncLocalStorage instance associated with any async call, so I’m not sure what getContextCLS() would actually return?
Alternative is to share these objects globally (which is not very attractive)
Why not? I think that’s the thing that conceptually makes the most sense here.
Reacted by Vladimir de Turckheim, Alex Yang and Andrei Pechkurov
We did not want to make the AsyncLocalStorages globally availabed by default to let users hide their instance and prevent anyone from tempering with it.
As there might be multiple AsyncLocalStorage running on the same code, a method like getContextCLS() would return an array:
@addaleax - the problem with the global approach is that we need to explicitly maintain the association of AsyncLocalStorage (let us call it ALS) with an async function. For example, in the code in the first comment, foo will be called from both the contexts, in arbitrary order. So even if we make cls1 and cls2 global, how do one decide which ALS to use (to access the store etc.)?
@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')consthttp=require('http')constcls=newAsyncLocalStorage()letstoreletindex=0constserver=http.createServer((q,r)=>{r.end((index++).toString().repeat(1024*1024*10))})server.listen(12000,()=>{cls.run(newMap(),()=>{for(leti=0;i<10;i++){constreq=http.get('http://localhost:12000',(res)=>{constdata=''store=cls.getStore()store.set('data',data)res.on('data',ondata)res.on('end',onend)})req.end()}})})functionondata(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')){letchunk=store.get('data')chunk+=dstore.set('data',chunk)}else{console.log('error...')}}functiononend(){// let store = cls.getStore()if(store&&store.has('data')){letchunk=store.get('data')varre=newRegExp(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:
sets up a server, that sends large buffers of data, filled with cardinal numbers, increasing per request
sets up a single ALS object, and runs 10 clients inside
sets up an empty store
in the data callbacks, accumulate the instantaneous data with the store data
in the end callbacks, retrieve the store data, and make sure they are homogeneous
the output shows they are not, instead the data is all clobbered between various responses
ok, I can test this with #32063 only, because with this change, we will have 10 stores, and with the getStore bug, I will have to manage them separately. Will test and get back, thanks!
@addaleax - with the patch of your PR, and with your suggested patch, the code is working as expected, thanks!
so a single instance of AsyncLocalStorage is capable of handling multiple callback chains (families, groups, etc. whatever we call those), through giving access to the appropriate store when invoked within the asynchronous routines! then why we would multiple instances of AsyncLocalStorage in the first place (why the new operator) ?
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.
then why we would multiple instances of AsyncLocalStorage in the first place (why the new operator) ?
Because different AsyncLocalStorage instances can be used for different purposes or even from different npm modules, which should not conflict with each other. And in particular, the scopes that they use might not match; for example, in
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.
But I agree that it would be nice for class AsyncLocalStorage to have static methods like create(key) and get(key) that create & get a named AsyncLocalStorage instance, respectively. I'd say the best practice would be to pass a Symbol for the key, but a string would work, too, at the risk of multiple libraries using the same string key.
@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.
(If you are creating a lib, you can just pass a symbol instead of a string. )
@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 AsyncLocalStorage supports our features. 😊
Getting the let cls = AsyncLocalStorage.create('someKey'); feature seems to me to be as straightforward as an ecosystem module used as a proxy for providing AsyncLocalStorage instances. I still believe having this in core would break the point of allowing people to hide their own instances from the outside (and the Symbol based solution does not really bring anything to the table IMO).
I like your "Await instead of run" feature -- that's a new one on me.
I guess we'll just have to Roll Our Own™️ until such time as AsyncLocalStorage supports our features. 😊
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.
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 AsyncLocalStorage.
The proposal, at its simplest, would be to add static methods to AsyncLocalStorage that allow for easy creation & retrieval of instances using either namespaced string keys, Symbol keys, or both. I'd expect library authors that leverage ALS to use Symbols and application authors to use Symbols or strings.
I'm totally open to an explanation of how our packages' behaviors are implementable using AsyncLocalStorage natively, rather than leaving it to userland code maintain a static map of instances.
@matthewadams@vdeturckheim
There is a slight difference between enterWith and start. Start() returns a promise and sets the context on a child resource - not the current resource. EnterWith() will set the context on current resource / asyncId.
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
you were porbably meaning to ping me in your last message. That's an interesting point. I'd tend to think that doing await context.enterWith() would do the same then. 🤔
@matthewadams@vdeturckheim
It would make developer life easier. You don't need to pass around/require the reference to the singleton instance everywhere. And if you have a deep file hierarcy, you need to require it by relative path. I suspect application developers will seek for a workaround anyway - either by using some user land module or implementing it themself. Perhaps as a local file package (which has it's problems in combo with npm). So the "workaround" might as well be in core rather than in user land. In my opinion.
@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.
Thanks for taking your time.
It would make developer life easier. You don't need to pass around/require the reference to the singleton instance everywhere. And if you have a deep file hierarcy, you need to require it by relative path. I suspect application developers will seek for a workaround anyway - either by using some user land module or implementing it themself. Perhaps as a local file package (which has it's problems in combo with npm). So the "workaround" might as well be in core rather than in user land. In my opinion.
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 (create vs get, what about multiple calls?). We are always very excited by any opportunity of welcoming a new contributor to the project!
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 .
I just want emphasize (as mentioned earlier in this thread), as a library developer you would use a Symbol as key. Then nobody can tamper with your instance of AsyncLocalStorage.
@vdeturckheim@lroal
I forgot to mention a use case that we leverage that uses a static map containing Strings as keys instead of Symbols. We sometimes use JavaScript decorators that dictate that certain contextual information (classically, user & role information) be placed into continuation local storage so that the decorator can access it later to execute logic (again, classically, access control decisions). In this case, Strings work great as keys to the static map of AsyncLocalStorage instances because the decorator library provider intends to use the shared context information to do its job.
Activity
gireeshpunathil commentedon Mar 3, 2020
/cc @vdeturckheim
addaleax commentedon Mar 3, 2020
My understanding is that there is no particular
AsyncLocalStorageinstance associated with any async call, so I’m not sure whatgetContextCLS()would actually return?Why not? I think that’s the thing that conceptually makes the most sense here.
vdeturckheim commentedon Mar 3, 2020
We did not want to make the
AsyncLocalStorages globally availabed by default to let users hide their instance and prevent anyone from tempering with it.As there might be multiple
AsyncLocalStoragerunning on the same code, a method likegetContextCLS()would return an array:I don't have strong opinions here appart that I'd like to leave the owner of an
AsyncLocalStoragethe ability to keep it private.gireeshpunathil commentedon Mar 3, 2020
@addaleax - the problem with the global approach is that we need to explicitly maintain the association of
AsyncLocalStorage(let us call it ALS) with an async function. For example, in the code in the first comment,foowill be called from both the contexts, in arbitrary order. So even if we makecls1andcls2global, how do one decide whichALSto use (to access the store etc.)?addaleax commentedon Mar 3, 2020
@gireeshpunathil I think there might be a misunderstanding about how ALS works. In particular,
AsyncLocalStorageinstances are not async contexts.I don’t quite understand this question… can you maybe give a more concrete use case as an example?
gireeshpunathil commentedon Mar 3, 2020
sure:
ALS.runcontext, so all the callbacks emanating from that context are grouped under one ALS object.Is it possible to achieve this using
ALSAPIs?[ edit: couple of typos]
vdeturckheim commentedon Mar 3, 2020
@gireeshpunathil in that case you would use one single ASL:
Would that make sense in your example?
gireeshpunathil commentedon Mar 3, 2020
@vdeturckheim - here is a complete example that exemplifies my use case, but using your suggestion of using a single ALS:
the test does these:
the output shows they are not, instead the data is all clobbered between various responses
expected output:
addaleax commentedon Mar 3, 2020
@gireeshpunathil
should be
otherwise all the requests share a single store.
gireeshpunathil commentedon Mar 3, 2020
ok, I can test this with #32063 only, because with this change, we will have 10 stores, and with the
getStorebug, I will have to manage them separately. Will test and get back, thanks!gireeshpunathil commentedon Mar 3, 2020
@addaleax - with the patch of your PR, and with your suggested patch, the code is working as expected, thanks!
so a single instance of
AsyncLocalStorageis capable of handling multiple callback chains (families, groups, etc. whatever we call those), through giving access to the appropriate store when invoked within the asynchronous routines! then why we would multiple instances ofAsyncLocalStoragein the first place (why the new operator) ?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!
addaleax commentedon Mar 3, 2020
Because different
AsyncLocalStorageinstances can be used for different purposes or even from different npm modules, which should not conflict with each other. And in particular, the scopes that they use might not match; for example, inYou’ll see
x()andy()sharing a store as far ascls1is concerned but having different stores as far ascls2is concerned.(But I mostly think it’s the fact that we don’t want ALS from different modules to conflict.)
test: add new scenario for async-local storage
22 remaining items
matthewadams commentedon Aug 10, 2020
@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
AsyncLocalStorage.matthewadams commentedon Aug 10, 2020
But I agree that it would be nice for class
AsyncLocalStorageto have static methods likecreate(key)andget(key)that create & get a namedAsyncLocalStorageinstance, respectively. I'd say the best practice would be to pass aSymbolfor the key, but astringwould work, too, at the risk of multiple libraries using the same string key.lroal commentedon Aug 11, 2020
@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.
(If you are creating a lib, you can just pass a symbol instead of a string. )
matthewadams commentedon Aug 11, 2020
@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
AsyncLocalStoragesupports our features. 😊vdeturckheim commentedon Aug 11, 2020
@matthewadams @lroal
I am not sure I get this discussion here.
Getting the
let cls = AsyncLocalStorage.create('someKey');feature seems to me to be as straightforward as an ecosystem module used as a proxy for providingAsyncLocalStorageinstances. I still believe having this in core would break the point of allowing people to hide their own instances from the outside (and theSymbolbased solution does not really bring anything to the table IMO).Is there a difference between this and
enterWith?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.
matthewadams commentedon Aug 11, 2020
@vdeturckheim
It appears not. I wasn't aware of
node-clsand I hadn't noticed it onAsyncLocalStorageuntil I started participating in this thread.matthewadams commentedon Aug 11, 2020
@vdeturckheim
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
AsyncLocalStorage.The proposal, at its simplest, would be to add static methods to
AsyncLocalStoragethat allow for easy creation & retrieval of instances using either namespacedstringkeys,Symbolkeys, or both. I'd expect library authors that leverage ALS to useSymbols and application authors to useSymbols orstrings.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
AsyncLocalStorage.I'm totally open to an explanation of how our packages' behaviors are implementable using
AsyncLocalStoragenatively, rather than leaving it to userland code maintain a static map of instances.lroal commentedon Aug 12, 2020
@matthewadams @vdeturckheim
There is a slight difference between enterWith and start. Start() returns a promise and sets the context on a child resource - not the current resource. EnterWith() will set the context on current resource / asyncId.
vdeturckheim commentedon Aug 12, 2020
@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
you were porbably meaning to ping me in your last message. That's an interesting point. I'd tend to think that doing
await context.enterWith()would do the same then. 🤔lroal commentedon Aug 13, 2020
@matthewadams @vdeturckheim
It would make developer life easier. You don't need to pass around/require the reference to the singleton instance everywhere. And if you have a deep file hierarcy, you need to require it by relative path. I suspect application developers will seek for a workaround anyway - either by using some user land module or implementing it themself. Perhaps as a local file package (which has it's problems in combo with npm). So the "workaround" might as well be in core rather than in user land. In my opinion.
@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.
Thanks for taking your time.
vdeturckheim commentedon Aug 13, 2020
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 (
createvsget, what about multiple calls?). We are always very excited by any opportunity of welcoming a new contributor to the project!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.
lroal commentedon Aug 13, 2020
Thanks @vdeturckheim .
I just want emphasize (as mentioned earlier in this thread), as a library developer you would use a Symbol as key. Then nobody can tamper with your instance of AsyncLocalStorage.
matthewadams commentedon Aug 16, 2020
@vdeturckheim @lroal
I forgot to mention a use case that we leverage that uses a static map containing
Strings as keys instead ofSymbols. We sometimes use JavaScript decorators that dictate that certain contextual information (classically, user & role information) be placed into continuation local storage so that the decorator can access it later to execute logic (again, classically, access control decisions). In this case,Strings work great as keys to the static map ofAsyncLocalStorageinstances because the decorator library provider intends to use the shared context information to do its job.matthewadams commentedon Aug 16, 2020
FYI, we just released an update to
@northscaler/continuation-local-storagethat includes support forAsyncLocalStoragein addition tocls-hooked&zone.js! 😊