Uso de enlaces asíncronos para el manejo de contexto de solicitudes en Node.js

Los ganchos asíncronos de Node.js proporcionan una API para realizar un seguimiento de la vida útil de los recursos asíncronos en una aplicación de nodo. Usemos Async Hooks para rastrear solicitudes HTTP.

Introducción

Ganchos asíncronos son un módulo central en Node.js que proporciona una API para rastrear la vida útil de los recursos asíncronos en una aplicación de Node. Un recurso asíncrono se puede considerar como un objeto que tiene una devolución de llamada asociada.

Los ejemplos incluyen, pero no se limitan a: Promises, Timeouts, TCPWrap, UDP, etc. La lista completa de recursos asíncronos que podemos rastrear usando esta API se puede encontrar [aquí.](https://nodejs.org/api/async_hooks .html#async_hooks_type)

La función Async Hooks se introdujo en 2017, en la versión 8 de Node.js, y aún es experimental. Esto significa que aún se pueden realizar cambios incompatibles con versiones anteriores en versiones futuras de la API. Dicho esto, actualmente no se considera apto para la producción.

En este artículo, analizaremos en profundidad los Async Hooks: qué son, por qué son importantes, dónde podemos usarlos y cómo podemos aprovecharlos para un caso de uso particular, es decir, solicitar el manejo de contexto en un Node. js y aplicación Express.

¿Qué son los ganchos asíncronos?

Como se indicó anteriormente, la clase Async Hooks es un módulo central de Node.js que proporciona una API para rastrear recursos asíncronos en su aplicación Node.js. Esto también incluye el seguimiento de los recursos creados por los módulos nativos de Node, como fs y net.

Durante la vida útil de un recurso asíncrono, hay 4 eventos que se activan y podemos rastrear, con Async Hooks. Éstos incluyen:

  1. init - Llamado durante la construcción del recurso asíncrono
  2. before - Llamado antes de que se llame la devolución de llamada del recurso
  3. después - Llamado después de que se haya invocado la devolución de llamada del recurso
  4. destroy: se llama después de que se destruye el recurso asíncrono
  5. promiseResolve: se llama cuando se invoca la función resolve() de una Promesa.

A continuación se muestra un fragmento resumido de la API Async Hooks de la descripción general en la documentación de Node.js:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
const async_hooks = require('async_hooks');

const exec_id = async_hooks.executionAsyncId();
const trigger_id = async_hooks.triggerAsyncId();
const asyncHook = async_hooks.createHook({
  init: function (asyncId, type, triggerAsyncId, resource) { },
  before: function (asyncId) { },
  after: function (asyncId) { },
  destroy: function (asyncId) { },
  promiseResolve: function (asyncId) { }
});
asyncHook.enable();
asyncHook.disable();

El método executionAsyncId() devuelve un identificador del contexto de ejecución actual.

El método triggerAsyncId() devuelve el identificador del recurso principal que desencadenó la ejecución del recurso asíncrono.

El método createHook() crea una instancia de gancho asíncrono, tomando los eventos antes mencionados como devoluciones de llamada opcionales.

Para habilitar el seguimiento de nuestros recursos, llamamos al método enable() de nuestra instancia de gancho asíncrono que creamos con el método createHook().

También podemos desactivar el seguimiento llamando a la función disable().

Habiendo visto lo que implica la API Async Hooks, veamos por qué deberíamos usarla.

Cuándo usar ganchos asíncronos

La adición de Async Hooks a la API central ha tenido muchas ventajas y casos de uso. Algunos de ellos incluyen:

  1. Mejor depuración: mediante el uso de Async Hooks, podemos mejorar y enriquecer los rastros de pila de las funciones asíncronas.
  2. Potentes capacidades de seguimiento, especialmente cuando se combina con la API de rendimiento de Node. Además, dado que la API Async Hooks es nativa, la sobrecarga de rendimiento es mínima.
  3. Manejo del contexto de la solicitud web: para capturar la información de una solicitud durante el tiempo de vida de esa solicitud, sin pasar el objeto de la solicitud a todas partes. El uso de Async Hooks se puede hacer en cualquier parte del código y podría ser especialmente útil al rastrear el comportamiento de los usuarios en un servidor.

En este artículo, veremos cómo manejar el seguimiento de ID de solicitud utilizando Async Hooks en una aplicación Express.

Uso de enlaces asíncronos para el manejo de contexto de solicitudes

En esta sección, ilustraremos cómo podemos aprovechar Async Hooks para realizar un seguimiento de ID de solicitud simple en una aplicación Node.js.

Configuración de controladores de contexto de solicitud

Comenzaremos creando un directorio donde residirán los archivos de nuestra aplicación, luego pasaremos a él:

1
mkdir async_hooks && cd async_hooks 

A continuación, necesitaremos inicializar nuestra aplicación Node.js en este directorio con npm y la configuración predeterminada:

1
npm init -y

Esto crea un archivo package.json en la raíz del directorio.

A continuación, necesitaremos instalar los paquetes Express y uuid como dependencias. Usaremos el paquete uuid para generar una identificación única para cada solicitud entrante.

Finalmente, instalamos el módulo esm para que las versiones de Node.js anteriores a v14 puedan ejecutar este ejemplo:

1
npm install express uuid esm --save

A continuación, cree un archivo hooks.js en la raíz del directorio:

1
touch hooks.js

Este archivo contendrá el código que interactúa con el módulo async_hooks. Exporta dos funciones:

  • Uno que habilita un Async Hook para una solicitud HTTP, haciendo un seguimiento de su ID de solicitud dada y cualquier información de solicitud que nos gustaría conservar.
  • El otro devuelve los datos de solicitud gestionados por el gancho dado su ID de gancho asíncrono.

Pongamos eso en código:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
require = require('esm')(module);
const asyncHooks = require('async_hooks');
const { v4 } = require('uuid');
const store = new Map();

const asyncHook = asyncHooks.createHook({
    init: (asyncId, _, triggerAsyncId) => {
        if (store.has(triggerAsyncId)) {
            store.set(asyncId, store.get(triggerAsyncId))
        }
    },
    destroy: (asyncId) => {
        if (store.has(asyncId)) {
            store.delete(asyncId);
        }
    }
});

asyncHook.enable();

const createRequestContext = (data, requestId = v4()) => {
    const requestInfo = { requestId, data };
    store.set(asyncHooks.executionAsyncId(), requestInfo);
    return requestInfo;
};

const getRequestContext = () => {
    return store.get(asyncHooks.executionAsyncId());
};

module.exports = { createRequestContext, getRequestContext };

En este fragmento de código, primero requerimos que el módulo esm proporcione compatibilidad con versiones anteriores de Node que no tienen soporte nativo para exportaciones de módulos experimentales. Esta característica es utilizada internamente por el módulo uuid.

A continuación, también necesitamos los módulos async_hooks y uuid. Desde el módulo uuid, desestructuramos la función v4, que usaremos más adelante para generar los UUID de la versión 4.

A continuación, creamos una tienda que asignará cada recurso asíncrono a su contexto de solicitud. Para esto, utilizamos un mapa de JavaScript simple.

A continuación, llamamos al método createHook() del módulo async_hooks e implementamos las devoluciones de llamada init() y destroy(). En la implementación de nuestra devolución de llamada init(), verificamos si triggerAsyncId está presente en la tienda.

Si existe, creamos una asignación de asyncId a los datos de solicitud almacenados en triggerAsyncId. En efecto, esto garantiza que almacenemos el mismo objeto de solicitud para los recursos asíncronos secundarios.

La devolución de llamada destroy() verifica si la tienda tiene el asyncId del recurso y lo elimina si es verdadero.

Para usar nuestro gancho, lo habilitamos llamando al método enable() de la instancia asyncHook que hemos creado.

A continuación, creamos 2 funciones: createRequestContext() y getRequestContext que usamos para crear y obtener nuestro contexto de solicitud, respectivamente.

La función createRequestContext() recibe los datos de la solicitud y una ID única como argumentos. A continuación, crea un objeto requestInfo a partir de ambos argumentos e intenta actualizar la tienda con el ID asíncrono del contexto de ejecución actual como clave y requestInfo como valor.

La función getRequestContext(), por otro lado, verifica si la tienda contiene una ID correspondiente a la ID del contexto de ejecución actual.

Finalmente exportamos ambas funciones usando la sintaxis module.exports().

Hemos configurado con éxito nuestra funcionalidad de manejo de contexto de solicitud. Procedamos a configurar nuestro servidor Express que recibirá las solicitudes.

Configuración del servidor Express

Habiendo configurado nuestro contexto, ahora procederemos a crear nuestro servidor Express para que podamos capturar solicitudes HTTP. Para hacerlo, cree un archivo server.js en la raíz del directorio de la siguiente manera:

1
touch server.js

Nuestro servidor aceptará una solicitud HTTP en el puerto 3000. Crea un gancho asíncrono para rastrear cada solicitud llamando a createRequestContext() en una función middleware, una función que tiene acceso a los objetos de solicitud y respuesta de HTTP. . Luego, el servidor envía una respuesta JSON con los datos capturados por Async Hook.

Dentro del archivo server.js, ingresa el siguiente código:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const express = require('express');
const ah = require('./hooks');
const app = express();
const port = 3000;

app.use((request, response, next) => {
    const data = { headers: request.headers };
    ah.createRequestContext(data);
    next();
});

const requestHandler = (request, response, next) => {
    const reqContext = ah.getRequestContext();
    response.json(reqContext);
    next()
};

app.get('/', requestHandler)

app.listen(port, (err) => {
    if (err) {
        return console.error(err);
    }
    console.log(`server is listening on ${port}`);
});

En este fragmento de código, requerimos express y nuestros módulos hooks como dependencias. Luego creamos una aplicación Express llamando a la función express().

A continuación, configuramos un middleware que desestructura los encabezados de las solicitudes y los guarda en una variable llamada data. Luego llama a la función createRequestContext() pasando data como argumento. Esto garantiza que los encabezados de la solicitud se conservarán durante todo el ciclo de vida de la solicitud con Async Hook.

Finalmente, llamamos a la función next() para ir al siguiente middleware en nuestra canalización de middleware o invocar el siguiente controlador de ruta.

Después de nuestro middleware, escribimos la función requestHandler() que maneja una solicitud GET en el dominio raíz del servidor. Notarás que en esta función, podemos tener acceso a nuestro contexto de solicitud a través de la función getRequestContext(). Esta función devuelve un objeto que representa los encabezados de la solicitud y el ID de la solicitud generado y almacenado en el contexto de la solicitud.

Luego creamos un punto final simple y adjuntamos nuestro controlador de solicitudes como una devolución de llamada.

Finalmente, hacemos que nuestro servidor escuche las conexiones en el puerto 3000 llamando al método listen() de nuestra instancia de aplicación.

Antes de ejecutar el código, abra el archivo package.json en la raíz del directorio y reemplace la sección test del script con esto:

1
"start": "node server.js"

Hecho esto, podemos ejecutar nuestra aplicación con el siguiente comando:

1
npm start

Debería recibir una respuesta en su terminal indicando que la aplicación se está ejecutando en el puerto 3000, como se muestra:

1
2
3
4
5
> [correo electrónico protegido] start /Users/allanmogusu/wikihtp/async-hooks-demo
> node server.js

(node:88410) ExperimentalWarning: Conditional exports is an experimental feature. This feature could change at any time
server is listening on 3000

Con nuestra aplicación en ejecución, abra una instancia de terminal separada y ejecute el siguiente comando curl para probar nuestra ruta predeterminada:

1
curl http://localhost:3000

Este comando curl realiza una solicitud GET a nuestra ruta predeterminada. Debería obtener una respuesta similar a esta:

1
2
$ curl http://localhost:3000
{"requestId":"3aad88a6-07bb-41e0-ab5a-fa9d5c0269a7","data":{"headers":{"host":"localhost:3000","user-agent":"curl/7.64.1","accept":"*/*"}}}%

Observe que se devuelven el requestId generado y nuestros encabezados de solicitud. Repetir el comando debería generar una nueva ID de solicitud ya que haremos una nueva solicitud:

1
2
$ curl http://localhost:3000
{"requestId":"38da84792-e782-47dc-92b4-691f4285b172","data":{"headers":{"host":"localhost:3000","user-agent":"curl/7.64.1","accept":"*/*"}}}%

La respuesta contiene el ID que generamos para la solicitud y los encabezados que capturamos en la función de middleware. Con Async Hooks, podríamos pasar fácilmente datos de un middleware a otro para la misma solicitud.

Conclusión

Async Hooks proporciona una API para rastrear la vida útil de los recursos asíncronos en una aplicación Node.js.

En este artículo, analizamos brevemente la API de Async Hooks, la funcionalidad que proporciona y cómo podemos aprovecharla. Hemos cubierto específicamente un ejemplo básico de cómo podemos usar Async Hooks para manejar y rastrear el contexto de la solicitud web de manera eficiente y limpia.

Sin embargo, desde la versión 14 de Node.js, la API Async Hooks se envía con almacenamiento local asíncrono, una API que facilita el manejo del contexto de las solicitudes en Node.js. Puede leer más al respecto aquí. Además, puede acceder al código de este tutorial [aquí.](https://github.com/ Allan690/async-hooks-demo)