• Node practice source code: 40 lines of code to implement a compact version of KOA
  • Node Practice Log: Asynchronous resource Listening and CLS implementation

More articles:The Node into the order

Why do I need to listen for asynchronous resources?

The most common asynchronous resource listening scenarios in a Node application are:

  • The user information must be provided during exception capture to keep the same user information in each client request
  • Full link log tracking, design each request for third-party services, databases, Redis to carry consistent traceId

The following figure shows zipkin’s full-link tracking based on traceId positioning:

Let’s look at an example of configuring user information in exception handling:

const session = new Map()

app.use((ctx, next) = > {
  try {
    await next()
  } catch (e) {
    const user = session.get('user')

    // Report the user to the exception monitoring system
  }
})

app.use((ctx, next) = > {
  // Set user information
  const user = getUserById()
  session.set('user', user)
})
Copy the code

Configure user information globally for backend services for exception and log tracking. Because the session used at this time is asynchronous, user information is extremely easy to be overwritten by subsequent requests, so how to obtain user information correctly?

async_hooks

The official documentation describes async_hooks as follows: It is used to track asynchronous resources, that is, to listen for the lifetime of asynchronous resources.

The async_hooks module provides an API to track asynchronous resources.

Since it is used to track asynchronous resources, there are two ids in each asynchronous resource:

  • asyncId: INDICATES the ID of the current life cycle of the asynchronous resource
  • trigerAsyncId: indicates the ID of the parent asynchronous resource, that isparentAsyncId

Call from the following API

const async_hooks = require('async_hooks');

const asyncId = async_hooks.executionAsyncId();

const trigerAsyncId = async_hooks.triggerAsyncId();
Copy the code

See the official documentation: Async_hooks API for more details

Asynchronous resources

Now that we’re talking about async_hooks listening for asynchronous resources, what are those asynchronous resources? We often use the following in our daily projects:

  • Promise
  • setTimeout
  • fs/net/processEtc based on the underlying API

However, async_hooks lists this much on the official website. In addition to the several mentioned above, console.log is also an asynchronous resource: TickObject.

FSEVENTWRAP, FSREQCALLBACK, GETADDRINFOREQWRAP, GETNAMEINFOREQWRAP, HTTPINCOMINGMESSAGE,
HTTPCLIENTREQUEST, JSSTREAM, PIPECONNECTWRAP, PIPEWRAP, PROCESSWRAP, QUERYWRAP,
SHUTDOWNWRAP, SIGNALWRAP, STATWATCHER, TCPCONNECTWRAP, TCPSERVERWRAP, TCPWRAP,
TTYWRAP, UDPSENDWRAP, UDPWRAP, WRITEWRAP, ZLIB, SSLCONNECTION, PBKDF2REQUEST,
RANDOMBYTESREQUEST, TLSWRAP, Microtask, Timeout, Immediate, TickObject
Copy the code

async_hooks.createHook

We can use asyncId to listen on an asynchronous resource. How can we listen on the creation and destruction of an asynchronous resource?

Create a hook using async_links. createHook.

const asyncHook = async_hooks.createHook({
  // asyncId: indicates the Id of an asynchronous resource
  // type: indicates the type of the asynchronous resource
  // triggerAsyncId: parent asynchronous resource Id
  init (asyncId, type, triggerAsyncId, resource) {},
  before (asyncId) {},
  after (asyncId) {},
  destroy(asyncId) {}
})
Copy the code

Let’s just focus on the four most important apis:

  • init: Listens for the creation of an asynchronous resource. In this function, we can get the call chain of the asynchronous resource and also the type of the asynchronous resource, which are important.
  • destory: Listens for the destruction of asynchronous resources. Pay attention tosetTimeoutCan be destroyed, andPromiseCannot destroy, CLS could leak here if implemented via async_hooks!
  • before
  • after
setTimeout((a)= > {
  // The after lifecycle begins with the callback function
  console.log('Async Before')
  op()
  op()
  op()
  op()
  // After lifecycle is at the end of the callback
  console.log('Async After')})Copy the code

Async_hooks debugging and testing

Is it important to debug tools and keep breaking points and Step In?

No, the debug method is console.log.

However, if you debug async_hooks using console.log, problems arise because console.log is also an asynchronous resource: TickObject. Is there an alternative to console.log?

You can use the write system call to print characters to standard output (STDOUT), which has a file descriptor of 1. Thus, the importance of operating system knowledge for server-side development is self-evident.

Node calls the following API:

fs.writeSync(1.'hello, world')
Copy the code

What is a file descriptor?

The complete debugging code is as follows:

function log (. args) {
  fs.writeSync(1, args.join(' ') + '\n')
}

async_hooks.createHook({
  init(asyncId, type, triggerAsyncId, resource) {
    log('Init: '.`${type}(asyncId=${asyncId}, parentAsyncId: ${triggerAsyncId}) `)
  },
  before(asyncId) {
    log('Before: ', asyncId)
  },
  after(asyncId) {
    log('After: ', asyncId)
  },
  destroy(asyncId) {
    log('Destory: ', asyncId);
  }
}).enable()
Copy the code

Continuation Local Storage implementation

Continuation-local storage works like thread-local storage in threaded programming, but is based on chains of Node-style callbacks instead of threads.

CLS is a key-value pair store that exists throughout the life cycle of an asynchronous resource. One copy of data is maintained for the same asynchronous resource and is not modified by other asynchronous resources. There are many good implementations in the community, and in older versions of Node (>=8.2.1) async_hooks are implemented directly.

  • Node – continuation – local – storage: implementation of github.com/joyent/node…
  • Cls-hooked: CLS using AsynWrap or async_hooks instead of async-listener for node 4.7+

I implemented a CLS myself using async_hooks: [clS-session](github.com/shfshanyue/… ]

const Session = require('cls-session')

const session = new Session()

function timeout (id) {
  session.scope((a)= > {
    session.set('a', id)
    setTimeout((a)= > {
      const a = session.get('a')
      console.log(a)
    })
  })
}

timeout(1)
timeout(2)
timeout(3)

// Output:
/ / 1
/ / 2
/ / 3
Copy the code

summary

This article explains the usage scenario and implementation of asynchronous resource listening, which can be summarized as the following three points:

  1. CLS is asynchronous resource life-cycle based storage, implemented via asynC_hooks
  2. After asynC_hooks is enabled, each asynchronous resource has an asyncId and trigerAsyncId, from which you can check the asynchronous call relationship
  3. CLS common scenarios are used in exception monitoring and full-link log processing