Domaine
[Historique]
Version | Changements |
---|---|
v8.8.0 | Les Promise créées dans les contextes VM n'ont plus de propriété .domain . Cependant, leurs gestionnaires sont toujours exécutés dans le domaine approprié, et les Promise créées dans le contexte principal possèdent toujours une propriété .domain . |
v8.0.0 | Les gestionnaires pour les Promise sont désormais invoqués dans le domaine dans lequel la première promesse d'une chaîne a été créée. |
v1.4.2 | Déprécié depuis : v1.4.2 |
[Stable : 0 - Déprécié]
Stable : 0 Stabilité : 0 - Déprécié
Code source : lib/domain.js
Ce module est en attente de dépréciation. Une fois qu'une API de remplacement aura été finalisée, ce module sera entièrement déprécié. La plupart des développeurs ne devraient pas avoir à utiliser ce module. Les utilisateurs qui doivent absolument disposer de la fonctionnalité fournie par les domaines peuvent s'y fier pour le moment, mais doivent s'attendre à devoir migrer vers une solution différente à l'avenir.
Les domaines offrent un moyen de gérer plusieurs opérations d'E/S différentes en un seul groupe. Si l'un des émetteurs d'événements ou des rappels enregistrés dans un domaine émet un événement 'error'
ou lève une erreur, l'objet de domaine sera notifié, au lieu de perdre le contexte de l'erreur dans le gestionnaire process.on('uncaughtException')
, ou de provoquer la fermeture immédiate du programme avec un code d'erreur.
Avertissement : n'ignorez pas les erreurs !
Les gestionnaires d'erreurs de domaine ne se substituent pas à l'arrêt d'un processus lorsqu'une erreur se produit.
De par la nature même du fonctionnement de throw
en JavaScript, il n'y a presque jamais de moyen sûr de « reprendre là où cela s'est arrêté », sans fuites de références ni création d'un autre type d'état fragile indéfini.
La manière la plus sûre de répondre à une erreur levée est d'arrêter le processus. Bien sûr, dans un serveur Web normal, il peut y avoir de nombreuses connexions ouvertes, et il n'est pas raisonnable de les arrêter brutalement parce qu'une erreur a été déclenchée par quelqu'un d'autre.
La meilleure approche consiste à envoyer une réponse d'erreur à la requête qui a déclenché l'erreur, tout en laissant les autres se terminer dans leur délai normal, et à arrêter d'écouter les nouvelles requêtes dans ce processus de travail.
De cette manière, l'utilisation de domain
va de pair avec le module de cluster, car le processus principal peut forker un nouveau processus de travail lorsqu'un processus de travail rencontre une erreur. Pour les programmes Node.js qui s'étendent à plusieurs machines, le proxy de terminaison ou le registre de service peuvent prendre note de l'échec et réagir en conséquence.
Par exemple, voici une mauvaise idée :
// XXX AVERTISSEMENT ! MAUVAISE IDÉE !
const d = require('node:domain').create()
d.on('error', er => {
// L'erreur ne plantera pas le processus, mais ce qu'elle fait est pire !
// Bien que nous ayons empêché le redémarrage brutal du processus, nous laissons
// fuir beaucoup de ressources si cela se produit.
// Ce n'est pas mieux que process.on('uncaughtException') !
console.log(`erreur, mais tant pis ${er.message}`)
})
d.run(() => {
require('node:http')
.createServer((req, res) => {
handleRequest(req, res)
})
.listen(PORT)
})
En utilisant le contexte d'un domaine et la résilience de la séparation de notre programme en plusieurs processus de travail, nous pouvons réagir de manière plus appropriée et gérer les erreurs avec une plus grande sécurité.
// Bien mieux !
const cluster = require('node:cluster')
const PORT = +process.env.PORT || 1337
if (cluster.isPrimary) {
// Un scénario plus réaliste aurait plus de 2 processus de travail,
// et peut-être ne pas mettre le principal et le processus de travail dans le même fichier.
//
// Il est également possible de se montrer un peu plus fantaisiste en matière de journalisation, et
// de mettre en œuvre toute logique personnalisée nécessaire pour prévenir les attaques par déni de service
// et autres mauvais comportements.
//
// Voir les options dans la documentation du cluster.
//
// L'important est que le principal fasse très peu de choses,
// ce qui augmente notre résilience face aux erreurs inattendues.
cluster.fork()
cluster.fork()
cluster.on('disconnect', worker => {
console.error('déconnexion !')
cluster.fork()
})
} else {
// Le processus de travail
//
// C'est là que nous mettons nos bogues !
const domain = require('node:domain')
// Voir la documentation du cluster pour plus de détails sur l'utilisation des
// processus de travail pour traiter les requêtes. Comment cela fonctionne, les mises en garde, etc.
const server = require('node:http').createServer((req, res) => {
const d = domain.create()
d.on('error', er => {
console.error(`erreur ${er.stack}`)
// Nous sommes en territoire dangereux !
// Par définition, quelque chose d'inattendu s'est produit,
// ce que nous ne voulions probablement pas.
// Tout peut arriver maintenant ! Soyez très prudent !
try {
// Assurez-vous que nous fermons dans les 30 secondes
const killtimer = setTimeout(() => {
process.exit(1)
}, 30000)
// Mais ne gardez pas le processus ouvert juste pour ça !
killtimer.unref()
// Arrêtez de traiter les nouvelles requêtes.
server.close()
// Faites savoir au principal que nous sommes morts. Cela déclenchera un
// 'disconnect' dans le principal du cluster, puis il forkera
// un nouveau processus de travail.
cluster.worker.disconnect()
// Essayez d'envoyer une erreur à la requête qui a déclenché le problème
res.statusCode = 500
res.setHeader('content-type', 'text/plain')
res.end('Oups, il y a eu un problème !\n')
} catch (er2) {
// Tant pis, on ne peut pas faire grand-chose à ce stade.
console.error(`Erreur lors de l'envoi de 500 ! ${er2.stack}`)
}
})
// Étant donné que req et res ont été créés avant l'existence de ce domaine,
// nous devons les ajouter explicitement.
// Voir l'explication de la liaison implicite par rapport à la liaison explicite ci-dessous.
d.add(req)
d.add(res)
// Maintenant, exécutez la fonction de gestion dans le domaine.
d.run(() => {
handleRequest(req, res)
})
})
server.listen(PORT)
}
// Cette partie n'est pas importante. Juste un exemple de routage.
// Mettez la logique d'application fantaisiste ici.
function handleRequest(req, res) {
switch (req.url) {
case '/error':
// Nous faisons des choses asynchrones, puis...
setTimeout(() => {
// Oups !
flerb.bark()
}, timeout)
break
default:
res.end('ok')
}
}
Ajouts aux objets Error
Chaque fois qu'un objet Error
est acheminé via un domaine, quelques champs supplémentaires lui sont ajoutés.
error.domain
Le domaine qui a géré l'erreur en premier.error.domainEmitter
L'émetteur d'événements qui a émis un événement'error'
avec l'objet d'erreur.error.domainBound
La fonction de rappel qui était liée au domaine et qui a reçu une erreur comme premier argument.error.domainThrown
Un booléen indiquant si l'erreur a été levée, émise ou transmise à une fonction de rappel liée.
Liaison implicite
Si des domaines sont utilisés, tous les nouveaux objets EventEmitter
(y compris les objets Stream, les requêtes, les réponses, etc.) seront implicitement liés au domaine actif au moment de leur création.
De plus, les rappels passés aux requêtes de la boucle d'événements de bas niveau (comme à fs.open()
ou d'autres méthodes prenant des rappels) seront automatiquement liés au domaine actif. S'ils lèvent une erreur, le domaine attrapera l'erreur.
Afin d'éviter une utilisation excessive de la mémoire, les objets Domain
eux-mêmes ne sont pas implicitement ajoutés en tant qu'enfants du domaine actif. Si c'était le cas, il serait trop facile d'empêcher les objets de requête et de réponse d'être correctement récupérés par le ramasse-miettes.
Pour imbriquer des objets Domain
en tant qu'enfants d'un Domain
parent, ils doivent être ajoutés explicitement.
La liaison implicite achemine les erreurs levées et les événements 'error'
vers l'événement 'error'
du Domain
, mais n'enregistre pas l'EventEmitter
sur le Domain
. La liaison implicite ne s'occupe que des erreurs levées et des événements 'error'
.
Liaison explicite
Parfois, le domaine utilisé n'est pas celui qui devrait être utilisé pour un émetteur d'événements spécifique. Ou, l'émetteur d'événements pourrait avoir été créé dans le contexte d'un domaine, mais devrait plutôt être lié à un autre domaine.
Par exemple, il pourrait y avoir un domaine en cours d'utilisation pour un serveur HTTP, mais nous pourrions préférer avoir un domaine séparé à utiliser pour chaque requête.
Cela est possible via la liaison explicite.
// Créer un domaine de niveau supérieur pour le serveur
const domain = require('node:domain');
const http = require('node:http');
const serverDomain = domain.create();
serverDomain.run(() => {
// Le serveur est créé dans le cadre de serverDomain
http.createServer((req, res) => {
// Req et res sont également créés dans le cadre de serverDomain
// cependant, nous préférerions avoir un domaine séparé pour chaque requête.
// créez-le en premier et ajoutez-y req et res.
const reqd = domain.create();
reqd.add(req);
reqd.add(res);
reqd.on('error', (er) => {
console.error('Erreur', er, req.url);
try {
res.writeHead(500);
res.end('Une erreur est survenue, désolé.');
} catch (er2) {
console.error('Erreur lors de l'envoi de 500', er2, req.url);
}
});
}).listen(1337);
});
domain.create()
- Retourne : <Domain>
Classe : Domain
- Hérite de : <EventEmitter>
La classe Domain
encapsule la fonctionnalité de routage des erreurs et des exceptions non interceptées vers l'objet Domain
actif.
Pour gérer les erreurs qu'elle intercepte, écoutez son événement 'error'
.
domain.members
Un tableau de minuteurs et d'émetteurs d'événements qui ont été explicitement ajoutés au domaine.
domain.add(emitter)
emitter
<EventEmitter> | <Timer> émetteur ou minuteur à ajouter au domaine
Ajoute explicitement un émetteur au domaine. Si des gestionnaires d'événements appelés par l'émetteur lèvent une erreur, ou si l'émetteur émet un événement 'error'
, il sera routé vers l'événement 'error'
du domaine, comme avec la liaison implicite.
Cela fonctionne également avec les minuteurs qui sont retournés par setInterval()
et setTimeout()
. Si leur fonction de rappel lève une exception, elle sera interceptée par le gestionnaire 'error'
du domaine.
Si le minuteur ou EventEmitter
était déjà lié à un domaine, il est supprimé de celui-ci et lié à celui-ci à la place.
domain.bind(callback)
callback
<Function> La fonction de rappel- Retourne : <Function> La fonction liée
La fonction retournée sera un wrapper autour de la fonction de rappel fournie. Lorsque la fonction retournée est appelée, toutes les erreurs levées seront routées vers l'événement 'error'
du domaine.
const d = domain.create()
function readSomeFile(filename, cb) {
fs.readFile(
filename,
'utf8',
d.bind((er, data) => {
// Si cela lève une exception, elle sera également transmise au domaine.
return cb(er, data ? JSON.parse(data) : null)
})
)
}
d.on('error', er => {
// Une erreur s'est produite quelque part. Si nous la levons maintenant, elle plantera le programme
// avec le numéro de ligne et le message de pile normaux.
})
domain.enter()
La méthode enter()
est un mécanisme utilisé par les méthodes run()
, bind()
et intercept()
pour définir le domaine actif. Elle définit domain.active
et process.domain
sur le domaine, et pousse implicitement le domaine sur la pile de domaines gérée par le module de domaine (voir domain.exit()
pour plus de détails sur la pile de domaines). L'appel à enter()
délimite le début d'une chaîne d'appels asynchrones et d'opérations d'E/S liées à un domaine.
L'appel à enter()
ne modifie que le domaine actif, et n'altère pas le domaine lui-même. enter()
et exit()
peuvent être appelés un nombre arbitraire de fois sur un seul domaine.
domain.exit()
La méthode exit()
quitte le domaine actuel, en le retirant de la pile de domaines. Chaque fois que l'exécution va passer au contexte d'une chaîne différente d'appels asynchrones, il est important de s'assurer que le domaine actuel est quitté. L'appel à exit()
délimite soit la fin, soit une interruption de la chaîne d'appels asynchrones et d'opérations d'E/S liées à un domaine.
S'il existe plusieurs domaines imbriqués liés au contexte d'exécution actuel, exit()
quittera tous les domaines imbriqués dans ce domaine.
L'appel à exit()
ne modifie que le domaine actif, et n'altère pas le domaine lui-même. enter()
et exit()
peuvent être appelés un nombre arbitraire de fois sur un seul domaine.
domain.intercept(callback)
callback
<Function> La fonction de rappel- Retourne : <Function> La fonction interceptée
Cette méthode est presque identique à domain.bind(callback)
. Cependant, en plus d'attraper les erreurs lancées, elle interceptera également les objets Error
envoyés comme premier argument à la fonction.
De cette façon, le schéma courant if (err) return callback(err);
peut être remplacé par un seul gestionnaire d'erreurs à un seul endroit.
const d = domain.create()
function readSomeFile(filename, cb) {
fs.readFile(
filename,
'utf8',
d.intercept(data => {
// Notez que le premier argument n'est jamais passé au
// rappel car il est supposé être l'argument 'Error'
// et donc intercepté par le domaine.
// Si cela lève une erreur, elle sera également transmise au domaine
// ainsi, la logique de gestion des erreurs peut être déplacée vers
// l'événement 'error' sur le domaine au lieu d'être répétée dans
// tout le programme.
return cb(null, JSON.parse(data))
})
)
}
d.on('error', er => {
// Une erreur s'est produite quelque part. Si nous la levons maintenant, cela
// fera planter le programme avec le numéro de ligne normal et le message de la pile.
})
domain.remove(emitter)
emitter
<EventEmitter> | <Timer> Émetteur ou minuteur à supprimer du domaine
L'opposé de domain.add(emitter)
. Supprime la gestion du domaine de l'émetteur spécifié.
domain.run(fn[, ...args])
fn
<Function>...args
<any>
Exécute la fonction fournie dans le contexte du domaine, liant implicitement tous les émetteurs d'événements, minuteurs et requêtes de bas niveau qui sont créés dans ce contexte. En option, des arguments peuvent être passés à la fonction.
C'est la façon la plus basique d'utiliser un domaine.
const domain = require('node:domain')
const fs = require('node:fs')
const d = domain.create()
d.on('error', er => {
console.error('Erreur interceptée !', er)
})
d.run(() => {
process.nextTick(() => {
setTimeout(() => {
// Simuler diverses actions asynchrones
fs.open('fichier inexistant', 'r', (er, fd) => {
if (er) throw er
// continuer...
})
}, 100)
})
})
Dans cet exemple, le gestionnaire d.on('error')
sera déclenché au lieu de faire planter le programme.
Domaines et promesses
Depuis Node.js 8.0.0, les gestionnaires de promesses sont exécutés dans le domaine dans lequel l'appel à .then()
ou .catch()
a été effectué :
const d1 = domain.create()
const d2 = domain.create()
let p
d1.run(() => {
p = Promise.resolve(42)
})
d2.run(() => {
p.then(v => {
// en cours d'exécution dans d2
})
})
Un rappel peut être lié à un domaine spécifique en utilisant domain.bind(callback)
:
const d1 = domain.create()
const d2 = domain.create()
let p
d1.run(() => {
p = Promise.resolve(42)
})
d2.run(() => {
p.then(
p.domain.bind(v => {
// en cours d'exécution dans d1
})
)
})
Les domaines n'interféreront pas avec les mécanismes de gestion des erreurs pour les promesses. En d'autres termes, aucun événement 'error'
ne sera émis pour les rejets de Promise
non gérés.