1. Introduction
Thread-local storage (TLS) is a widely used concept, which allows sharing a variable within a single thread.
In this article we are going to look at what is a TLS, why should we use it. Last but not least, we are going to see how can we implement it in Node.js.
2. Thread-local storage (TLS)
In a programming language, the most well-known variable scopes are either block scope or a global variable.
A block scope variable is accessible within a single block. For example, a block could be a function, if-else statement or a for-loop.
A global variable is accessible throughout the lifecycle of a program. There are no restrictions on who and when can access the variable. The critical point is that in a multithreaded environment, all threads will have the same reference to the global variable.
TLS is almost like a global variable with a slight difference. The scope of the variable is a single thread. If we take a traditional multithreaded environment, such as Java, every thread will have its’ own reference to a variable. For instance, Java provides ThreadLocal API to support TLS.
3. Use cases of TLS
Why do we need TLS? One of the most common scenarios is logging. Imagine we have a web server with authentication. We extract the user information from the request and do some actions with the user information passed to our server.
When we are logging about the actions the user is performing in our server, we would always like to have user information in the resulting log output. How can we achieve this? Seemingly the most straightforward option is to pass down the user information to where we are invoking the log statements.
However, this gets very quickly very complicated. Especially if we want to log something down in a function deep down in the call stack.
An alternative to passing down the user information to every method would be to use TLS. Before starting to process the request, we set the value of a thread-local variable and later on use it in the log statement.
4. Thread-local storage in Node.js
As we might know, Node.js is not a multithreaded environment. When we write a web server in Node.js, only a single thread is responsible for processing all the requests.
When several requests arrive at the server, a single thread handles all of them at the same time. The control from one request to another is passed on using callbacks or async-await.
Considering that, it’s hard to talk about thread-local variables in Node.js. Or is it? The good news is that starting from Node version 12, it’s possible to use TLS-like concept using AsyncLocalStorage.
Let’s imagine we have a code which is responsible for fetching all the orders of a user. Below you can see a sample controller which processes requests from the users.
The controller invokes function retrieveAllOrdersForUser
which is responsible for fetching all the orders for a single user. We also have a function doSomethingElse
which is not specific to any single user. At the end of doSomethingElse
, we would like to log down the line for the access logs
with the user context - we need to invoke logDoSomethingElse
.
To get the user information for the log record, we need to pass user
as an argument to logDoSomethingElse
. We need to modify the signatures of several functions just to enrich a log statement with the user information. That’s bad and increases the chance of an accident.
This is the place where AsyncLocalStorage
comes to the rescue. AsyncLocalStorage
is the way to store values over the lifetime of an asynchronous call. Let’s see how to take advantage of it.
The core idea of AsyncLocalStorage
is that the values of the variables persist within a single callback or promise chain. For example, we could introduce middleware which initializes the context for every request with the user ID. We can see a traditional Express middleware:
We initialize a global variable asyncLocalStorage
, and for every incoming request, we invoke asyncLocalStorage.run
with a Map
. The Map
itself contains a key user
with the value extracted from the request headers.
We need to invoke app.use to install the middleware.
Looking into the controller, we can see that we don’t need the argument user
for doSomething
and logDoSomethingElse
. That’s way better. We can get the ID of the user using asyncLocalStorage.getStore()
which returns the Map
unique to a specific request. We’re able to get the ID of the user using Map.get
and log it down.
5. Conclusion
All in all, thread-local storage (TLS) comes in handy when we want to pass down context deep down in the call stack.