Nodejs - AsyncLocalStorage with Bottleneck

I wish to share a rare though painful issue when using 2 libraries together - the Bottleneck and AsyncLocalStorage.

AsyncLocalStorage help you keep track on your async context - https://nodejs.org/api/async_context.html

Unlike Java or C#, Nodejs runs a single thread for quick io. This same thread is "Jumping" between the async functions and its hard to keep track on a single flow in the logs since its the same thread.

AsyncLocalStorage helps you here by creating a context wrapping your flow and keeps a persistent store through your flow function stack. You can read more about it here https://nodejs.org/api/async_context.html

Bottleneck is mostly used when you want to run multiple instances of your service and you want them to be synchronized.
Lets say you don't want 2 instances handling the same userId at the same time. Bottleneck will lock userId in a shared memory (like redis) and the second request will wait for the first one to finish.
You can read more about it here:
https://www.npmjs.com/package/bottleneck

So what is the problem? Bottleneck is messing with the async store

When you use the bottleneck schedule
https://www.npmjs.com/package/bottleneck#schedule
The async context is not as expected.
The first request handled with its async context BUT the queued requests are also excuted with the first async context.

You can think of it maybe not as a bug (though I do) but as a feature that pushes the queued tasks on the first async stack.

The result is that the next requests will have the first context.
Since I use the asyncLocalStorage for log context here is a pseudo example of the problem:

Req1, ctx1 -> fn
Req2, ctx2 -> fn

limiter.schedule(fn ...

The logs in req2 will show ctx1.

Whats the solution?

Well the solution is either not to use both bottleneck schedule and AsyncLocalStorage together or to ensure the context before any schedule.

For example,
If you export your asynLocalStorage you can copy the orig context before the schedule and then set it back:

export const als = new AsyncLocalStorage();

const setStore = function (store) {
/*here set the store according to its type
For examle if the store is a map:
als.getStore().clear();
store.forEach((value, key) => als.getStore().set(key, value));
*/
}

const origCtx = als.getStore();

const fn = function(arg1, arg2) {
setStore(origCtx);
//....
};

limiter.schedule(fn, arg1, arg2)
.then((result) => {
/* ... */
});

I hope you find it helpful, if you have any comments I would love to read them out.

Cheers

H2
H3
H4
3 columns
2 columns
1 column
Join the conversation now
Ecency