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.
Because it’s such a new feature, there’s not a lot of information about it, but a good place to start is by reading the documentation and a blog post by Irina Shestak.
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.
Why?
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 init
and destroy
.
Each time an asynchronous resource is created, init
function is called with an asyncId
, type
, triggerId
and resource
.
asyncId
— A unique ID for the async resource,triggerId
— TheasyncId
of the async resource in whose execution context this async resource was created, or in simple words, theasyncId
who caused this.type
— The type ofresource
. All types are available hereresource
— 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, execution
is 1
. This is why the triggerId
for Timeout
and TIMERWRAP
is 1
as well.
When timeout is past and Node runs our callback, the execution is6
, which is the asyncId
of 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 createHooks
function!
init
will be used to track our context by tracking triggerId
and asyncId
. destroy
, on the other hand, will be used to clean our context
object.
On init
we can watch how we keep track the request using the new capabilities of async_hooks
.
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 someResource
That’s it! we are done. We can set
and 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.
Enjoy!
P.S — Developers in Israel? Autodesk TLV is looking for you. Apply here.