Async Hooks — A whole new world of opportunities
Lately Node foundation released, as part of Node.js 9, a new and interesting feature called Async Hooks.
It’s important to say that as I’m writing this, the feature is considered experimental and isn’t recommended for a production usage, but we as developers — are curious beings, and probably more than the average human. We’re not afraid of some experimenting.
After I was done with the above light reading, I was ready to dive in deep to solve a personal problem I had — managing a request context.
The problem is quiet simple and known. While my team and I started to monitor our Node app, our main pain point was to group the logs by requests. The common solution is to add a transaction id to each log row. Once you have a transaction id you can filter your log by requests and even send it to your client in case of an error.
This pattern is pretty easy on synchronized platforms such as Ruby on rails and Python Django. Each request is handled by a thread. Every thing is synchronized so you can save any request context on your thread context.
On the other hand, on a platform like Node, this becomes quiet challenging.
As we all know to declaim, Node is an event-driven and single-threaded platform. This means we have no thread context we can relay on, since we only have one thread.
So what can we do about it?
We can pass a context object to all functions but this doesn’t scale well and is very difficult to maintain in large codebases.
We found a better solution — node-continuation-local-storage. To use the package we need to create a namespace. Inside our namespace we can get and set variables easily. But sometimes, mainly when we are dealing with external packages we found the package unreliable and context was lost.
Async Hooks module provides an API for tracking the lifetime of asynchronous resources created inside Node.js.
If you are not familiar with asynchronous resources, it is better to start with the famous YouTube about Node’s event loop.
How can we use it to maintain a request context like we want? Let’s take a brief look on the API. Using async hooks is quiet easy:
For our purpose we will use
Each time an asynchronous resource is created,
init function is called with an
asyncId— A unique ID for the async resource,
asyncIdof the async resource in whose execution context this async resource was created, or in simple words, the
asyncIdwho caused this.
type— The type of
resource. All types are available here
resource— The async resource itself
As you can see, when keeping track of the
triggerId for each async resource, we can link it to the actual context it was created with!
The last brick we are missing is a way to start each request chain.
Scrolling down the API will introduce us
async_hooks.executionAsyncId() which is exactly what we need.
executionAsyncId is the
asyncId of the current execution context.
Let’s sum it all by the following example:
Here’s the output:
When we are running our program,
1. This is why the
1 as well.
When timeout is past and Node runs our callback, the execution is
6 , which is the
Timeout resource. At the end we can see the garbage collector destroying our async resources and we are done.
Now that we have it all, Let’s connect the dots together. You can find all the following code in an NPM package I’ve created called node-request-context.
Firstly, we want to have an API to declare the start of our async callback chain. In other words — when should we create our context. We’re gonna try to have the API as close to node-continuation-local-storage as possible.
Second step, we will create
Namespace object and keep it in a
namespaces global object
Third step, and the most existing one, combining
async_hooks and implementing
init will be used to track our context by tracking
destroy, on the other hand, will be used to clean our
init we can watch how we keep track the request using the new capabilities of
Our package is done! now let’s use it in our Node app!
For our example, we will have
someResource with async callback and we want to maintain a
transactionId for each request.
and here is our
That’s it! we are done. We can
get any variable we want during a request.
As I’ve mentioned before, I’ve created a NPM package with all the code and called it node-request-context. You can clone it, run
npm run example and watch it all in action.
P.S — Developers in Israel? Autodesk TLV is looking for you. Apply here.