About author: May Jun, Software Designer, author of the public account “Nodejs Technology Stack”.

A practical use scenario for Async Hooks is to store request context and share data between asynchronous calls. Use the Async Hooks module of Node.js to trace asynchronous resources.

In this section, we will introduce how to implement an AsyncLocalStorage class from scratch based on the API provided by Async hooks and traceId that associate logs in HTTP requests. This is also a practical case of Async Hooks.

What is asynchronous local storage?

What we call asynchronous local storage is similar to thread-local storage in multithreaded programming languages. For example, the ThreadLocal class in Java, which I wrote about earlier, can create separate copies of different threads using the same variable to avoid conflicts over shared resources. Get or set the copy value of this variable in the current thread through the get()/set() method within a thread request. The copies created between threads do not affect each other during the concurrent access of multiple threads.

In Node.js our business usually works on the main thread (except with work_threads) and there is no ThreadLocal class. And event-driven manner to deal with all the HTTP requests, each request to come over and then are asynchronous, also hard to track the context information between asynchronous, we want to do is to start in the asynchronous events, from receiving HTTP requests to the response, for example, can have a kind of machine can make us anytime and anywhere to get some Shared data during this period, This is the asynchronous local storage technique described in this section.

Next, I will explain the four ways to implement AsyncLocalStorage, from the initial manual implementation to the official V14.x supported AsyncLocalStorage class, you can also learn the implementation principle.

Existing business issues

Assume that there is a requirement to modify the existing log system and add traceId to all log places to achieve full-link log tracing.

One scenario is that if you use some enterprise-level framework like egg.js, you can rely on the middleware capabilities provided by the framework to mount traceId on requests. As you can see from a previous article, logging link tracing based on the egg.js framework is also possible. At that time, however, a plug-in based on egg did its own inheritance implementation. Now, it is no longer necessary to configure a custom log format to implement eggjs.org/zh-cn/core/… .

On the other hand, if you are using a basic framework like Express or Koa, all business is called as a modeload function. If you manually pass the traceId of each request between Controller -> Service -> Model, this will be too intrusive to the business code. The log is too coupled with the business.

The following code is an example of my simplification. There is a requirement that every log print should output the traceId field carried by the current HTTP request processing Headers without changing the business code. How would you do that?

// logger.js
const logger = {
  info: (. args) = > {
    console.log(...args);
  }
}
module.exports = { logger }

// app.js
const express = require('express');
const app = express();
const PORT = 3000;
const { logger } = require('./logger');
global.logger = contextLogger;

app.use((req, res, next) = > contextLogger.run(req, next));

app.get('/logger'.async (req, res, next) => {
  try {
  	const users = await getUsersController();
  	res.json({ code: 'SUCCESS'.message: ' '.data: users });
  } catch (error) {
    res.json({ code: 'ERROR'.message: error.message })
  }
});

app.listen(PORT, () = > console.log(`server is listening on ${PORT}`));

async function getUsersController() {
  logger.info('Get user list at controller layer.');
  return getUsersService();
}

async function getUsersService() {
  logger.info('Get user list at service layer.');
  setTimeout(function() { logger.info('setTimeout 2s at service layer.')},3000);
  return getUsersModel();
}

async function getUsersModel() {
  logger.info('Get user list at model layer.');
  return [];
}
Copy the code

Method 1: Implement asynchronous local storage

The solution is to implement local storage of the request context, which can be retrieved from the current scoped code and cleaned up after processing. These requirements can be implemented via an API provided by Async Hooks.

Create AsyncLocalStorage class

  • Line {1} creates a Map collection to store context information.
  • The init callback in line {2} is important. The init callback is received before an asynchronous event is triggered. TriggerAsyncId is the trigger of the current asynchronous resource, and we can get the information from the last asynchronous resource and store it in the current asynchronous resource. The destroy callback is received when asyncId’s corresponding asynchronous resource is destroyed, so remember to destroy the information stored in the current asyncId callback.
  • Line {3} takes the asyncId of the current request context as the Key of the Map collection and stores the incoming context information.
  • Line {4} gets the context information for asyncId to get the current code.
// AsyncLocalStorage.js
const asyncHooks = require('async_hooks');
const { executionAsyncId } = asyncHooks;
class AsyncLocalStorage {
  constructor() {
    this.storeMap = new Map(a);/ / {1}
    this.createHook(); / / {2}
  }
  createHook() {
    const ctx = this;
    const hooks = asyncHooks.createHook({
      init(asyncId, type, triggerAsyncId) {
        if(ctx.storeMap.has(triggerAsyncId)) { ctx.storeMap.set(asyncId, ctx.storeMap.get(triggerAsyncId)); }},destroy(asyncId){ ctx.storeMap.delete(asyncId); }}); hooks.enable(); }run(store, callback) { / / {3}
    this.storeMap.set(executionAsyncId(), store);
    callback();
  }
  getStore() { / / {4}
    return this.storeMap.get(executionAsyncId()); }}module.exports = AsyncLocalStorage;
Copy the code

Note that in the createHook() method we defined there are hooks. Enable (); This is because promises are not turned on by default, and the invocation shown enables asynchronous tracing of promises.

Modify logger.js files

Get the context information corresponding to the current code at the place where we need to print the log and take out the traceId we have stored. In this way, we only need to modify the middle of our log without changing our business code.

const { v4: uuidV4 } = require('uuid');
const AsyncLocalStorage = require('./AsyncLocalStorage');
const asyncLocalStorage = new AsyncLocalStorage();

const logger = {
  info: (. args) = > {
    const traceId = asyncLocalStorage.getStore();
    console.log(traceId, ... args); },run: (req, callback) = >{ asyncLocalStorage.run(req.headers.requestId || uuidV4(), callback); }}module.exports = {
  logger,
}
Copy the code

Modify the app.js file

Register a piece of middleware to deliver the request information.

app.use((req, res, next) = > logger.run(req, next));
Copy the code

Output the result after running

e82d1a1f-5038-4ac9-a9c8-2aa5abb0f96a Get user list at router layer.
e82d1a1f-5038-4ac9-a9c8-2aa5abb0f96a Get user list at controller layer.
e82d1a1f-5038-4ac9-a9c8-2aa5abb0f96a Get user list at service layer.
e82d1a1f-5038-4ac9-a9c8-2aa5abb0f96a Get user list at model layer.
e82d1a1f-5038-4ac9-a9c8-2aa5abb0f96a setTimeout 2s at service layer.
Copy the code

This approach is implemented solely on the API provided by Async Hooks, which requires maintaining an additional Map object and handling destruction operations if you don’t understand the implementation.

ExecutionAsyncResource () returns the currently executing asynchronous resource

ExecutionAsyncResource () returns the currently executing asynchronous resource, which is useful for continuous local storage without having to create a Map object to store metadata as in method one.

const asyncHooks = require('async_hooks');
const { executionAsyncId, executionAsyncResource } = asyncHooks;

class AsyncLocalStorage {
  constructor() {
    this.createHook();
  }
  createHook() {
    const hooks = asyncHooks.createHook({
      init(asyncId, type, triggerAsyncId, resource) {
        const cr = executionAsyncResource();
        if(cr) { resource[asyncId] = cr[triggerAsyncId]; }}}); hooks.enable(); }run(store, callback) {
    executionAsyncResource()[executionAsyncId()] = store;
    callback();
  }
  getStore() {
    returnexecutionAsyncResource()[executionAsyncId()]; }}module.exports = AsyncLocalStorage;
Copy the code

Method 3: Create AsyncLocalStorage class based on ResourceAsync

ResourceAysnc can be used to customize asynchronous resources. The introduction here is also the implementation of AsyncLocalStorage by referring to node.js source code.

One notable change is that each call to the run() method creates a resource that calls its runInAsyncScope() method, so that the code executed (the callback passed in) in the asynchronous scope of the resource is traceable to the store we set.

const asyncHooks = require('async_hooks');
const { executionAsyncResource, AsyncResource } = asyncHooks;

class AsyncLocalStorage {
  constructor() {
    this.kResourceStore = Symbol('kResourceStore');
    this.enabled = false;
    const ctx = this;
    this.hooks = asyncHooks.createHook({
      init(asyncId, type, triggerAsyncId, resource) {
        const currentResource = executionAsyncResource();
        ctx._propagate(resource, currentResource)
      }
    });
  }

  // Propagate the context from a parent resource to a child one
  _propagate(resource, triggerResource) {
    const store = triggerResource[this.kResourceStore];
    if (store) {
      resource[this.kResourceStore] = store; }}_enable() {
    if (!this.enabled) {
      this.enabled = true;
      this.hooks.enable(); }}enterWith(store) {
    this._enable();
    const resource = executionAsyncResource();
    resource[this.kResourceStore] = store;
  }

  run(store, callback) {
    const resource = new AsyncResource('AsyncLocalStorage', {
      requireManualDestroy: true});return resource.emitDestroy().runInAsyncScope(() = > {
      this.enterWith(store);
      return callback();
    });
  }

  getStore() {
    return executionAsyncResource()[this.kResourceStore]; }}module.exports = AsyncLocalStorage;
Copy the code

Mode 4: AsyncLocalStorage class

Node.js v13.10.0 async_hooks module adds the AsyncLocalStorage class, which instantiates an object and calls the Run () method for local storage. There is no need to maintain an additional AsyncLocalStorage class.

The AsyncLocalStorage class is implemented as described above, so we don’t need the call to links.enable () shown externally to enable Promise asynchronous tracking, because it’s already implemented internally.

const { AsyncLocalStorage } = require('async_hooks');
Copy the code

Performance overhead for Async Hooks

If you enable Async Hooks (which are enabled by calling the enable() method of the Async Hooks instance) for every asynchronous operation or Promise operation, Anything that is asynchronous, including Console, triggers hooks, which also have performance overhead.

Referring to Kuzzle’s performance benchmark, the difference between using AsyncLocalStorage and not using it is ~ 8%.

 —- Log with AsyncLocalStorage Log classic difference
req/s   2613 2842 ~ 8%

Of course, it varies from business to business, so if you’re worried about the performance overhead, you can benchmark your own business.

Reference

  • Nodejs.org/api/async_h…
  • Node.js 14 & AsyncLocalStorage: Share a context between asynchronous calls
  • Request scope is implemented in Node via Async Hooks
  • Async Hooks performance impact
  • Kuzzle benchmarking