Hooks asynchrones
[Stable : 1 - Expérimental]
Stable : 1 Stabilité : 1 - Expérimental. Veuillez migrer depuis cette API si possible. Nous ne recommandons pas l’utilisation des API createHook
, AsyncHook
et executionAsyncResource
car elles présentent des problèmes d’utilisabilité, des risques de sécurité et des implications sur les performances. L’API stable AsyncLocalStorage
est mieux adaptée aux cas d’utilisation du suivi de contexte asynchrone. Si vous avez un cas d’utilisation pour createHook
, AsyncHook
ou executionAsyncResource
au-delà du suivi de contexte résolu par AsyncLocalStorage
ou des données de diagnostic actuellement fournies par Canal de diagnostic, veuillez ouvrir un problème à l’adresse https://github.com/nodejs/node/issues en décrivant votre cas d’utilisation afin que nous puissions créer une API plus axée sur un objectif précis.
Code source : lib/async_hooks.js
Nous déconseillons fortement l’utilisation de l’API async_hooks
. D’autres API peuvent couvrir la plupart de ses cas d’utilisation, notamment :
AsyncLocalStorage
assure le suivi du contexte asynchroneprocess.getActiveResourcesInfo()
assure le suivi des ressources actives
Le module node:async_hooks
fournit une API pour suivre les ressources asynchrones. On y accède ainsi :
import async_hooks from 'node:async_hooks'
const async_hooks = require('node:async_hooks')
Terminologie
Une ressource asynchrone représente un objet avec une fonction de rappel associée. Cette fonction de rappel peut être appelée plusieurs fois, comme l’événement 'connection'
dans net.createServer()
, ou une seule fois comme dans fs.open()
. Une ressource peut également être fermée avant que la fonction de rappel ne soit appelée. AsyncHook
ne fait pas explicitement la distinction entre ces différents cas, mais les représentera comme le concept abstrait qu’est une ressource.
Si des Worker
sont utilisés, chaque thread possède une interface async_hooks
indépendante, et chaque thread utilisera un nouvel ensemble d’ID asynchrones.
Vue d'ensemble
Voici un bref aperçu de l'API publique.
import async_hooks from 'node:async_hooks'
// Retourne l'ID du contexte d'exécution actuel.
const eid = async_hooks.executionAsyncId()
// Retourne l'ID du gestionnaire responsable du déclenchement du rappel de
// la portée d'exécution actuelle à appeler.
const tid = async_hooks.triggerAsyncId()
// Crée une nouvelle instance AsyncHook. Tous ces rappels sont optionnels.
const asyncHook = async_hooks.createHook({ init, before, after, destroy, promiseResolve })
// Autorise les rappels de cette instance AsyncHook à s'exécuter. Ce n'est pas
// une action implicite après l'exécution du constructeur, et doit être
// explicitement exécutée pour commencer l'exécution des rappels.
asyncHook.enable()
// Désactive l'écoute des nouveaux événements asynchrones.
asyncHook.disable()
//
// Ce qui suit sont les rappels qui peuvent être passés à createHook().
//
// init() est appelé pendant la construction de l'objet. La ressource peut ne
// pas avoir terminé sa construction lorsque ce rappel s'exécute. Par
// conséquent, tous les champs de la ressource référencés par "asyncId" peuvent
// ne pas avoir été remplis.
function init(asyncId, type, triggerAsyncId, resource) {}
// before() est appelé juste avant que le rappel de la ressource ne soit
// appelé. Il peut être appelé 0 à N fois pour les gestionnaires (tels que
// TCPWrap), et sera appelé exactement 1 fois pour les requêtes (telles que
// FSReqCallback).
function before(asyncId) {}
// after() est appelé juste après que le rappel de la ressource a terminé.
function after(asyncId) {}
// destroy() est appelé lorsque la ressource est détruite.
function destroy(asyncId) {}
// promiseResolve() est appelé uniquement pour les ressources promise, lorsque la
// fonction resolve() passée au constructeur Promise est invoquée (soit
// directement, soit par d'autres moyens de résolution d'une promesse).
function promiseResolve(asyncId) {}
const async_hooks = require('node:async_hooks')
// Retourne l'ID du contexte d'exécution actuel.
const eid = async_hooks.executionAsyncId()
// Retourne l'ID du gestionnaire responsable du déclenchement du rappel de
// la portée d'exécution actuelle à appeler.
const tid = async_hooks.triggerAsyncId()
// Crée une nouvelle instance AsyncHook. Tous ces rappels sont optionnels.
const asyncHook = async_hooks.createHook({ init, before, after, destroy, promiseResolve })
// Autorise les rappels de cette instance AsyncHook à s'exécuter. Ce n'est pas
// une action implicite après l'exécution du constructeur, et doit être
// explicitement exécutée pour commencer l'exécution des rappels.
asyncHook.enable()
// Désactive l'écoute des nouveaux événements asynchrones.
asyncHook.disable()
//
// Ce qui suit sont les rappels qui peuvent être passés à createHook().
//
// init() est appelé pendant la construction de l'objet. La ressource peut ne
// pas avoir terminé sa construction lorsque ce rappel s'exécute. Par
// conséquent, tous les champs de la ressource référencés par "asyncId" peuvent
// ne pas avoir été remplis.
function init(asyncId, type, triggerAsyncId, resource) {}
// before() est appelé juste avant que le rappel de la ressource ne soit
// appelé. Il peut être appelé 0 à N fois pour les gestionnaires (tels que
// TCPWrap), et sera appelé exactement 1 fois pour les requêtes (telles que
// FSReqCallback).
function before(asyncId) {}
// after() est appelé juste après que le rappel de la ressource a terminé.
function after(asyncId) {}
// destroy() est appelé lorsque la ressource est détruite.
function destroy(asyncId) {}
// promiseResolve() est appelé uniquement pour les ressources promise, lorsque la
// fonction resolve() passée au constructeur Promise est invoquée (soit
// directement, soit par d'autres moyens de résolution d'une promesse).
function promiseResolve(asyncId) {}
async_hooks.createHook(callbacks)
Ajouté dans : v8.1.0
callbacks
<Objet> Les callbacks de Hook à enregistrerinit
<Fonction> Le callbackinit
.before
<Fonction> Le callbackbefore
.after
<Fonction> Le callbackafter
.destroy
<Fonction> Le callbackdestroy
.promiseResolve
<Fonction> Le callbackpromiseResolve
.
Retourne : <AsyncHook> Instance utilisée pour désactiver et activer les hooks
Enregistre les fonctions à appeler pour les différents événements de durée de vie de chaque opération asynchrone.
Les callbacks init()
/before()
/after()
/destroy()
sont appelés pour l'événement asynchrone respectif pendant la durée de vie d'une ressource.
Tous les callbacks sont facultatifs. Par exemple, si seul le nettoyage des ressources doit être suivi, seul le callback destroy
doit être passé. Les spécificités de toutes les fonctions pouvant être passées à callbacks
se trouvent dans la section Callbacks de Hook.
import { createHook } from 'node:async_hooks'
const asyncHook = createHook({
init(asyncId, type, triggerAsyncId, resource) {},
destroy(asyncId) {},
})
const async_hooks = require('node:async_hooks')
const asyncHook = async_hooks.createHook({
init(asyncId, type, triggerAsyncId, resource) {},
destroy(asyncId) {},
})
Les callbacks seront hérités via la chaîne de prototypes :
class MyAsyncCallbacks {
init(asyncId, type, triggerAsyncId, resource) {}
destroy(asyncId) {}
}
class MyAddedCallbacks extends MyAsyncCallbacks {
before(asyncId) {}
after(asyncId) {}
}
const asyncHook = async_hooks.createHook(new MyAddedCallbacks())
Parce que les promises sont des ressources asynchrones dont le cycle de vie est suivi via le mécanisme des hooks asynchrones, les callbacks init()
, before()
, after()
et destroy()
ne doivent pas être des fonctions asynchrones qui retournent des promises.
Gestion des erreurs
Si des callbacks AsyncHook
lèvent une exception, l'application affichera la trace de la pile et se terminera. Le chemin de sortie suit celui d'une exception non interceptée, mais tous les écouteurs 'uncaughtException'
sont supprimés, forçant ainsi le processus à se terminer. Les callbacks 'exit'
seront toujours appelés, sauf si l'application est exécutée avec --abort-on-uncaught-exception
, auquel cas une trace de la pile sera affichée et l'application se terminera, laissant un fichier core.
La raison de ce comportement de gestion des erreurs est que ces callbacks s'exécutent à des points potentiellement volatils du cycle de vie d'un objet, par exemple lors de la construction et de la destruction d'une classe. Pour cette raison, il est jugé nécessaire d'arrêter rapidement le processus afin d'éviter un arrêt inopiné ultérieur. Ceci est susceptible de changer à l'avenir si une analyse complète est effectuée pour garantir qu'une exception puisse suivre le flux de contrôle normal sans effets secondaires involontaires.
Impression dans les callbacks AsyncHook
Étant donné que l'impression sur la console est une opération asynchrone, console.log()
entraînera l'appel de callbacks AsyncHook
. L'utilisation de console.log()
ou d'opérations asynchrones similaires à l'intérieur d'une fonction de callback AsyncHook
provoquera une récursion infinie. Une solution simple pour le débogage consiste à utiliser une opération de journalisation synchrone telle que fs.writeFileSync(file, msg, flag)
. Cela imprimera dans le fichier et n'invoquera pas AsyncHook
récursivement car il est synchrone.
import { writeFileSync } from 'node:fs'
import { format } from 'node:util'
function debug(...args) {
// Utilisez une fonction comme celle-ci lors du débogage à l'intérieur d'un callback AsyncHook
writeFileSync('log.out', `${format(...args)}\n`, { flag: 'a' })
}
const fs = require('node:fs')
const util = require('node:util')
function debug(...args) {
// Utilisez une fonction comme celle-ci lors du débogage à l'intérieur d'un callback AsyncHook
fs.writeFileSync('log.out', `${util.format(...args)}\n`, { flag: 'a' })
}
Si une opération asynchrone est nécessaire pour la journalisation, il est possible de suivre ce qui a causé l'opération asynchrone en utilisant les informations fournies par AsyncHook
lui-même. La journalisation doit alors être ignorée lorsqu'elle est elle-même à l'origine de l'appel du callback AsyncHook
. Ce faisant, la récursion infinie est évitée.
Classe : AsyncHook
La classe AsyncHook
expose une interface pour le suivi des événements de durée de vie des opérations asynchrones.
asyncHook.enable()
- Retourne : <AsyncHook> Une référence à
asyncHook
.
Active les callbacks pour une instance AsyncHook
donnée. Si aucun callback n'est fourni, l'activation n'a aucun effet.
L'instance AsyncHook
est désactivée par défaut. Si l'instance AsyncHook
doit être activée immédiatement après sa création, le modèle suivant peut être utilisé.
import { createHook } from 'node:async_hooks'
const hook = createHook(callbacks).enable()
const async_hooks = require('node:async_hooks')
const hook = async_hooks.createHook(callbacks).enable()
asyncHook.disable()
- Retourne : <AsyncHook> Une référence à
asyncHook
.
Désactive les callbacks pour une instance AsyncHook
donnée du pool global de callbacks AsyncHook
à exécuter. Une fois qu'un hook a été désactivé, il ne sera plus appelé avant d'être réactivé.
Pour des raisons de cohérence de l'API, disable()
retourne également l'instance AsyncHook
.
Callbacks de hook
Les événements clés de la durée de vie des événements asynchrones ont été classés en quatre catégories : l'instanciation, avant/après l'appel du callback, et lors de la destruction de l'instance.
init(asyncId, type, triggerAsyncId, resource)
asyncId
<number> Un ID unique pour la ressource asynchrone.type
<string> Le type de la ressource asynchrone.triggerAsyncId
<number> L'ID unique de la ressource asynchrone dans le contexte d'exécution de laquelle cette ressource asynchrone a été créée.resource
<Object> Référence à la ressource représentant l'opération asynchrone, doit être libérée pendant destroy.
Appelée lorsqu'une classe est construite et qu'elle a la possibilité d'émettre un événement asynchrone. Cela ne signifie pas que l'instance doit appeler before
/after
avant que destroy
ne soit appelé, seulement que la possibilité existe.
Ce comportement peut être observé en effectuant une opération comme l'ouverture d'une ressource puis sa fermeture avant que la ressource puisse être utilisée. L'extrait suivant illustre ceci.
import { createServer } from 'node:net'
createServer().listen(function () {
this.close()
})
// OU
clearTimeout(setTimeout(() => {}, 10))
require('node:net')
.createServer()
.listen(function () {
this.close()
})
// OU
clearTimeout(setTimeout(() => {}, 10))
Chaque nouvelle ressource se voit attribuer un ID unique dans la portée de l'instance Node.js actuelle.
type
Le champ type
est une chaîne de caractères identifiant le type de ressource ayant provoqué l'appel de init
. Généralement, il correspondra au nom du constructeur de la ressource.
Le type
des ressources créées par Node.js lui-même peut changer dans n'importe quelle version de Node.js. Les valeurs valides incluent TLSWRAP
, TCPWRAP
, TCPSERVERWRAP
, GETADDRINFOREQWRAP
, FSREQCALLBACK
, Microtask
, et Timeout
. Inspectez le code source de la version de Node.js utilisée pour obtenir la liste complète.
De plus, les utilisateurs de AsyncResource
créent des ressources asynchrones indépendamment de Node.js lui-même.
Il existe également le type de ressource PROMISE
, utilisé pour suivre les instances de Promise
et le travail asynchrone planifié par celles-ci.
Les utilisateurs peuvent définir leur propre type
lorsqu'ils utilisent l'API d'intégration publique.
Il est possible d'avoir des collisions de noms de type. Les intégrateurs sont encouragés à utiliser des préfixes uniques, tels que le nom du package npm, pour éviter les collisions lors de l'écoute des hooks.
triggerAsyncId
triggerAsyncId
est l'asyncId
de la ressource qui a causé (ou "déclenché") l'initialisation de la nouvelle ressource et qui a provoqué l'appel de init
. Ceci est différent de async_hooks.executionAsyncId()
qui montre seulement quand une ressource a été créée, tandis que triggerAsyncId
montre pourquoi une ressource a été créée.
Voici une simple démonstration de triggerAsyncId
:
import { createHook, executionAsyncId } from 'node:async_hooks'
import { stdout } from 'node:process'
import net from 'node:net'
import fs from 'node:fs'
createHook({
init(asyncId, type, triggerAsyncId) {
const eid = executionAsyncId()
fs.writeSync(stdout.fd, `${type}(${asyncId}): trigger: ${triggerAsyncId} execution: ${eid}\n`)
},
}).enable()
net.createServer(conn => {}).listen(8080)
const { createHook, executionAsyncId } = require('node:async_hooks')
const { stdout } = require('node:process')
const net = require('node:net')
const fs = require('node:fs')
createHook({
init(asyncId, type, triggerAsyncId) {
const eid = executionAsyncId()
fs.writeSync(stdout.fd, `${type}(${asyncId}): trigger: ${triggerAsyncId} execution: ${eid}\n`)
},
}).enable()
net.createServer(conn => {}).listen(8080)
Sortie lorsqu'on accède au serveur avec nc localhost 8080
:
TCPSERVERWRAP(5): trigger: 1 execution: 1
TCPWRAP(7): trigger: 5 execution: 0
TCPSERVERWRAP
est le serveur qui reçoit les connexions.
TCPWRAP
est la nouvelle connexion du client. Lorsqu'une nouvelle connexion est établie, l'instance TCPWrap
est immédiatement construite. Cela se produit en dehors de toute pile JavaScript. (Un executionAsyncId()
de 0
signifie qu'il est exécuté depuis C++ sans pile JavaScript au-dessus.) Avec seulement cette information, il serait impossible de lier les ressources ensemble en termes de ce qui a causé leur création, donc triggerAsyncId
a pour tâche de propager quelle ressource est responsable de l'existence de la nouvelle ressource.
resource
resource
est un objet représentant la ressource asynchrone effective qui a été initialisée. L'API pour accéder à l'objet peut être spécifiée par le créateur de la ressource. Les ressources créées par Node.js lui-même sont internes et peuvent changer à tout moment. Par conséquent, aucune API n'est spécifiée pour celles-ci.
Dans certains cas, l'objet ressource est réutilisé pour des raisons de performance, il n'est donc pas sûr de l'utiliser comme clé dans une WeakMap
ou d'y ajouter des propriétés.
Exemple de contexte asynchrone
Le cas d'utilisation du suivi du contexte est couvert par l'API stable AsyncLocalStorage
. Cet exemple illustre uniquement le fonctionnement des hooks asynchrones, mais AsyncLocalStorage
convient mieux à ce cas d'utilisation.
Voici un exemple avec des informations supplémentaires sur les appels à init
entre les appels before
et after
, spécifiquement à quoi ressemblera la fonction de rappel à listen()
. La mise en forme de la sortie est légèrement plus élaborée pour faciliter la visualisation du contexte d'appel.
import async_hooks from 'node:async_hooks'
import fs from 'node:fs'
import net from 'node:net'
import { stdout } from 'node:process'
const { fd } = stdout
let indent = 0
async_hooks
.createHook({
init(asyncId, type, triggerAsyncId) {
const eid = async_hooks.executionAsyncId()
const indentStr = ' '.repeat(indent)
fs.writeSync(fd, `${indentStr}${type}(${asyncId}):` + ` trigger: ${triggerAsyncId} execution: ${eid}\n`)
},
before(asyncId) {
const indentStr = ' '.repeat(indent)
fs.writeSync(fd, `${indentStr}before: ${asyncId}\n`)
indent += 2
},
after(asyncId) {
indent -= 2
const indentStr = ' '.repeat(indent)
fs.writeSync(fd, `${indentStr}after: ${asyncId}\n`)
},
destroy(asyncId) {
const indentStr = ' '.repeat(indent)
fs.writeSync(fd, `${indentStr}destroy: ${asyncId}\n`)
},
})
.enable()
net
.createServer(() => {})
.listen(8080, () => {
// Attendons 10ms avant d'enregistrer le démarrage du serveur.
setTimeout(() => {
console.log('>>>', async_hooks.executionAsyncId())
}, 10)
})
const async_hooks = require('node:async_hooks')
const fs = require('node:fs')
const net = require('node:net')
const { fd } = process.stdout
let indent = 0
async_hooks
.createHook({
init(asyncId, type, triggerAsyncId) {
const eid = async_hooks.executionAsyncId()
const indentStr = ' '.repeat(indent)
fs.writeSync(fd, `${indentStr}${type}(${asyncId}):` + ` trigger: ${triggerAsyncId} execution: ${eid}\n`)
},
before(asyncId) {
const indentStr = ' '.repeat(indent)
fs.writeSync(fd, `${indentStr}before: ${asyncId}\n`)
indent += 2
},
after(asyncId) {
indent -= 2
const indentStr = ' '.repeat(indent)
fs.writeSync(fd, `${indentStr}after: ${asyncId}\n`)
},
destroy(asyncId) {
const indentStr = ' '.repeat(indent)
fs.writeSync(fd, `${indentStr}destroy: ${asyncId}\n`)
},
})
.enable()
net
.createServer(() => {})
.listen(8080, () => {
// Attendons 10ms avant d'enregistrer le démarrage du serveur.
setTimeout(() => {
console.log('>>>', async_hooks.executionAsyncId())
}, 10)
})
Sortie du seul démarrage du serveur :
TCPSERVERWRAP(5): trigger: 1 execution: 1
TickObject(6): trigger: 5 execution: 1
before: 6
Timeout(7): trigger: 6 execution: 6
after: 6
destroy: 6
before: 7
>>> 7
TickObject(8): trigger: 7 execution: 7
after: 7
before: 8
after: 8
Comme illustré dans l'exemple, executionAsyncId()
et execution
spécifient chacun la valeur du contexte d'exécution courant ; qui est délimité par les appels à before
et after
.
L'utilisation uniquement de execution
pour représenter graphiquement l'allocation des ressources donne le résultat suivant :
root(1)
^
|
TickObject(6)
^
|
Timeout(7)
TCPSERVERWRAP
ne fait pas partie de ce graphique, même si c'est la raison pour laquelle console.log()
a été appelé. C'est parce que la liaison à un port sans nom d'hôte est une opération synchrone, mais pour maintenir une API entièrement asynchrone, la fonction de rappel de l'utilisateur est placée dans un process.nextTick()
. C'est pourquoi TickObject
est présent dans la sortie et est un « parent » pour la fonction de rappel .listen()
.
Le graphique montre uniquement quand une ressource a été créée, pas pourquoi, donc pour suivre le pourquoi, utilisez triggerAsyncId
. Ce qui peut être représenté par le graphique suivant :
bootstrap(1)
|
˅
TCPSERVERWRAP(5)
|
˅
TickObject(6)
|
˅
Timeout(7)
before(asyncId)
asyncId
<number>
Lorsqu'une opération asynchrone est initiée (telle que la réception d'une nouvelle connexion par un serveur TCP) ou terminée (telle que l'écriture de données sur le disque), un rappel est appelé pour en informer l'utilisateur. Le rappel before
est appelé juste avant l'exécution de ce rappel. asyncId
est l'identifiant unique attribué à la ressource sur le point d'exécuter le rappel.
Le rappel before
sera appelé de 0 à N fois. Le rappel before
sera généralement appelé 0 fois si l'opération asynchrone a été annulée ou, par exemple, si aucune connexion n'est reçue par un serveur TCP. Les ressources asynchrones persistantes comme un serveur TCP appelleront généralement le rappel before
plusieurs fois, tandis que d'autres opérations comme fs.open()
ne l'appelleront qu'une seule fois.
after(asyncId)
asyncId
<number>
Appelée immédiatement après l'achèvement du rappel spécifié dans before
.
Si une exception non interceptée se produit pendant l'exécution du rappel, alors after
s'exécutera après l'émission de l'événement 'uncaughtException'
ou l'exécution du gestionnaire d'un domain
.
destroy(asyncId)
asyncId
<number>
Appelée après la destruction de la ressource correspondant à asyncId
. Elle est également appelée de manière asynchrone à partir de l'API d'intégration emitDestroy()
.
Certaines ressources dépendent du ramasse-miettes pour le nettoyage, donc si une référence est faite à l'objet resource
passé à init
, il est possible que destroy
ne soit jamais appelé, causant une fuite de mémoire dans l'application. Si la ressource ne dépend pas du ramasse-miettes, alors ce ne sera pas un problème.
L'utilisation du crochet destroy
entraîne des frais généraux supplémentaires car il permet le suivi des instances Promise
via le ramasse-miettes.
promiseResolve(asyncId)
Ajouté dans : v8.6.0
asyncId
<number>
Appelée lorsque la fonction resolve
passée au constructeur Promise
est invoquée (directement ou par d'autres moyens de résolution d'une promesse).
resolve()
n'effectue aucun travail synchrone observable.
La Promise
n'est pas nécessairement remplie ou rejetée à ce stade si la Promise
a été résolue en assumant l'état d'une autre Promise
.
new Promise(resolve => resolve(true)).then(a => {})
appelle les rappels suivants :
init for PROMISE with id 5, trigger id: 1
promise resolve 5 # corresponds to resolve(true)
init for PROMISE with id 6, trigger id: 5 # the Promise returned by then()
before 6 # the then() callback is entered
promise resolve 6 # the then() callback resolves the promise by returning
after 6
async_hooks.executionAsyncResource()
Ajouté dans : v13.9.0, v12.17.0
- Retourne : <Objet> La ressource représentant l'exécution actuelle. Utile pour stocker des données dans la ressource.
Les objets ressource retournés par executionAsyncResource()
sont le plus souvent des objets de gestion interne de Node.js avec des API non documentées. L'utilisation de fonctions ou de propriétés sur l'objet est susceptible de planter votre application et doit être évitée.
L'utilisation de executionAsyncResource()
dans le contexte d'exécution de premier niveau renverra un objet vide car il n'y a pas d'objet de gestion ou de requête à utiliser, mais avoir un objet représentant le niveau supérieur peut être utile.
import { open } from 'node:fs'
import { executionAsyncId, executionAsyncResource } from 'node:async_hooks'
console.log(executionAsyncId(), executionAsyncResource()) // 1 {}
open(new URL(import.meta.url), 'r', (err, fd) => {
console.log(executionAsyncId(), executionAsyncResource()) // 7 FSReqWrap
})
const { open } = require('node:fs')
const { executionAsyncId, executionAsyncResource } = require('node:async_hooks')
console.log(executionAsyncId(), executionAsyncResource()) // 1 {}
open(__filename, 'r', (err, fd) => {
console.log(executionAsyncId(), executionAsyncResource()) // 7 FSReqWrap
})
Cela peut être utilisé pour implémenter un stockage local de continuation sans utiliser une Map
de suivi pour stocker les métadonnées :
import { createServer } from 'node:http'
import { executionAsyncId, executionAsyncResource, createHook } from 'node:async_hooks'
const sym = Symbol('state') // Symbole privé pour éviter la pollution
createHook({
init(asyncId, type, triggerAsyncId, resource) {
const cr = executionAsyncResource()
if (cr) {
resource[sym] = cr[sym]
}
},
}).enable()
const server = createServer((req, res) => {
executionAsyncResource()[sym] = { state: req.url }
setTimeout(function () {
res.end(JSON.stringify(executionAsyncResource()[sym]))
}, 100)
}).listen(3000)
const { createServer } = require('node:http')
const { executionAsyncId, executionAsyncResource, createHook } = require('node:async_hooks')
const sym = Symbol('state') // Symbole privé pour éviter la pollution
createHook({
init(asyncId, type, triggerAsyncId, resource) {
const cr = executionAsyncResource()
if (cr) {
resource[sym] = cr[sym]
}
},
}).enable()
const server = createServer((req, res) => {
executionAsyncResource()[sym] = { state: req.url }
setTimeout(function () {
res.end(JSON.stringify(executionAsyncResource()[sym]))
}, 100)
}).listen(3000)
async_hooks.executionAsyncId()
[Historique]
Version | Modifications |
---|---|
v8.2.0 | Renommé de currentId . |
v8.1.0 | Ajouté dans : v8.1.0 |
- Retourne : <nombre> L’
asyncId
du contexte d’exécution actuel. Utile pour suivre quand quelque chose appelle.
import { executionAsyncId } from 'node:async_hooks'
import fs from 'node:fs'
console.log(executionAsyncId()) // 1 - bootstrap
const path = '.'
fs.open(path, 'r', (err, fd) => {
console.log(executionAsyncId()) // 6 - open()
})
const async_hooks = require('node:async_hooks')
const fs = require('node:fs')
console.log(async_hooks.executionAsyncId()) // 1 - bootstrap
const path = '.'
fs.open(path, 'r', (err, fd) => {
console.log(async_hooks.executionAsyncId()) // 6 - open()
})
L’ID retourné par executionAsyncId()
est lié au timing d’exécution, et non à la causalité (qui est couverte par triggerAsyncId()
) :
const server = net
.createServer(conn => {
// Retourne l’ID du serveur, et non de la nouvelle connexion, car le
// callback s’exécute dans la portée d’exécution de MakeCallback() du serveur.
async_hooks.executionAsyncId()
})
.listen(port, () => {
// Retourne l’ID d’un TickObject (process.nextTick()) car tous les
// callbacks passés à .listen() sont encapsulés dans un nextTick().
async_hooks.executionAsyncId()
})
Les contextes Promise peuvent ne pas obtenir d’executionAsyncIds
précis par défaut. Voir la section sur le suivi de l’exécution des promises.
async_hooks.triggerAsyncId()
- Retourne : <nombre> L’ID de la ressource responsable de l’appel du callback qui est actuellement en cours d’exécution.
const server = net
.createServer(conn => {
// La ressource qui a causé (ou déclenché) l’appel de ce callback
// était celle de la nouvelle connexion. Ainsi, la valeur de retour de triggerAsyncId()
// est l’asyncId de "conn".
async_hooks.triggerAsyncId()
})
.listen(port, () => {
// Même si tous les callbacks passés à .listen() sont encapsulés dans un nextTick()
// le callback lui-même existe car l’appel à .listen() du serveur
// a été effectué. Ainsi, la valeur de retour serait l’ID du serveur.
async_hooks.triggerAsyncId()
})
Les contextes Promise peuvent ne pas obtenir de triggerAsyncId
valides par défaut. Voir la section sur le suivi de l’exécution des promises.
async_hooks.asyncWrapProviders
Ajouté dans : v17.2.0, v16.14.0
- Retourne : Une map des types de fournisseur à l'ID numérique correspondant. Cette map contient tous les types d'événements qui pourraient être émis par l'événement
async_hooks.init()
.
Cette fonctionnalité supprime l'utilisation obsolète de process.binding('async_wrap').Providers
. Voir : DEP0111
Suivi de l'exécution des promesses
Par défaut, les exécutions de promesses ne se voient pas attribuer d'ID asyncId
en raison de la nature relativement coûteuse de l'API d'introspection des promesses promise introspection API fournie par V8. Cela signifie que les programmes utilisant des promesses ou async
/await
n'obtiendront pas les ID d'exécution et de déclenchement corrects pour les contextes de rappel de promesse par défaut.
import { executionAsyncId, triggerAsyncId } from 'node:async_hooks'
Promise.resolve(1729).then(() => {
console.log(`eid ${executionAsyncId()} tid ${triggerAsyncId()}`)
})
// produit :
// eid 1 tid 0
const { executionAsyncId, triggerAsyncId } = require('node:async_hooks')
Promise.resolve(1729).then(() => {
console.log(`eid ${executionAsyncId()} tid ${triggerAsyncId()}`)
})
// produit :
// eid 1 tid 0
On observe que le rappel then()
prétend s'être exécuté dans le contexte de la portée externe même s'il y a eu un saut asynchrone. De plus, la valeur triggerAsyncId
est 0
, ce qui signifie que nous manquons de contexte sur la ressource qui a causé (déclenché) l'exécution du rappel then()
.
L'installation de hooks asynchrones via async_hooks.createHook
permet le suivi de l'exécution des promesses :
import { createHook, executionAsyncId, triggerAsyncId } from 'node:async_hooks'
createHook({ init() {} }).enable() // force l'activation de PromiseHooks.
Promise.resolve(1729).then(() => {
console.log(`eid ${executionAsyncId()} tid ${triggerAsyncId()}`)
})
// produit :
// eid 7 tid 6
const { createHook, executionAsyncId, triggerAsyncId } = require('node:async_hooks')
createHook({ init() {} }).enable() // force l'activation de PromiseHooks.
Promise.resolve(1729).then(() => {
console.log(`eid ${executionAsyncId()} tid ${triggerAsyncId()}`)
})
// produit :
// eid 7 tid 6
Dans cet exemple, l'ajout d'une fonction hook réelle a activé le suivi des promesses. Il y a deux promesses dans l'exemple ci-dessus ; la promesse créée par Promise.resolve()
et la promesse renvoyée par l'appel à then()
. Dans l'exemple ci-dessus, la première promesse a obtenu l'asyncId
6
et la seconde a obtenu l'asyncId
7
. Lors de l'exécution du rappel then()
, nous exécutons dans le contexte de la promesse avec l'asyncId
7
. Cette promesse a été déclenchée par la ressource asynchrone 6
.
Une autre subtilité avec les promesses est que les rappels before
et after
ne sont exécutés que sur les promesses chaînées. Cela signifie que les promesses non créées par then()
/catch()
n'auront pas les rappels before
et after
déclenchés sur elles. Pour plus de détails, voir les détails de l'API V8 PromiseHooks.
API d'intégration JavaScript
Les développeurs de bibliothèques qui gèrent leurs propres ressources asynchrones effectuant des tâches telles que les E/S, la mise en pool de connexions ou la gestion des files d'attente de rappels peuvent utiliser l'API JavaScript AsyncResource
afin que tous les rappels appropriés soient appelés.
Classe : AsyncResource
La documentation de cette classe a été déplacée vers AsyncResource
.
Classe : AsyncLocalStorage
La documentation de cette classe a été déplacée vers AsyncLocalStorage
.