Skip to content

Domain

[Verlauf]

VersionÄnderungen
v8.8.0In VM-Kontexten erstellte Promises haben keine .domain-Eigenschaft mehr. Ihre Handler werden jedoch weiterhin im richtigen Domain ausgeführt, und Promises, die im Hauptkontext erstellt wurden, besitzen weiterhin eine .domain-Eigenschaft.
v8.0.0Handler für Promises werden jetzt in dem Domain aufgerufen, in dem der erste Promise einer Kette erstellt wurde.
v1.4.2Seit v1.4.2 veraltet

[Stabil: 0 - Veraltet]

Stabil: 0 Stabilität: 0 - Veraltet

Quellcode: lib/domain.js

Dieses Modul steht kurz vor der Veralterung. Sobald eine Ersatz-API finalisiert wurde, wird dieses Modul vollständig veraltet sein. Die meisten Entwickler sollten keinen Grund haben, dieses Modul zu verwenden. Benutzer, die unbedingt die Funktionalität benötigen, die Domains bieten, können sich vorerst darauf verlassen, sollten aber damit rechnen, in Zukunft auf eine andere Lösung migrieren zu müssen.

Domains bieten eine Möglichkeit, mehrere verschiedene IO-Operationen als einzelne Gruppe zu behandeln. Wenn einer der zu einem Domain registrierten EventEmitter oder Callbacks ein 'error'-Event auslöst oder einen Fehler auswirft, wird das Domain-Objekt benachrichtigt, anstatt den Kontext des Fehlers im process.on('uncaughtException')-Handler zu verlieren oder das Programm sofort mit einem Fehlercode zu beenden.

Warnung: Ignorieren Sie keine Fehler!

Domain-Fehlerhandler sind kein Ersatz für das Herunterfahren eines Prozesses, wenn ein Fehler auftritt.

Aufgrund der Funktionsweise von throw in JavaScript gibt es fast nie eine Möglichkeit, sicher "dort weiterzumachen, wo man aufgehört hat", ohne Referenzen zu verlieren oder einen anderen undefinierten, brüchigen Zustand zu erzeugen.

Die sicherste Methode, auf einen ausgeworfenen Fehler zu reagieren, ist das Herunterfahren des Prozesses. Natürlich kann es in einem normalen Webserver viele offene Verbindungen geben, und es ist nicht sinnvoll, diese abrupt zu schließen, weil ein Fehler von jemand anderem ausgelöst wurde.

Der bessere Ansatz ist, eine Fehlerantwort an die Anfrage zu senden, die den Fehler ausgelöst hat, während die anderen in ihrer normalen Zeit beendet werden, und das Abhören neuer Anfragen in diesem Worker zu stoppen.

Auf diese Weise geht die Verwendung von domain Hand in Hand mit dem Cluster-Modul, da der primäre Prozess einen neuen Worker forken kann, wenn ein Worker auf einen Fehler stößt. Für Node.js-Programme, die auf mehrere Maschinen skalieren, kann der terminierende Proxy oder die Service-Registrierung den Ausfall bemerken und entsprechend reagieren.

Beispielsweise ist dies keine gute Idee:

js
// XXX WARNUNG! SCHLECHTE IDEE!

const d = require('node:domain').create()
d.on('error', er => {
  // Der Fehler stürzt den Prozess nicht ab, aber was er tut, ist schlimmer!
  // Obwohl wir ein abruptes Neustarten des Prozesses verhindert haben, verlieren wir
  // viele Ressourcen, wenn dies jemals passiert.
  // Das ist nicht besser als process.on('uncaughtException')!
  console.log(`Fehler, aber na ja ${er.message}`)
})
d.run(() => {
  require('node:http')
    .createServer((req, res) => {
      handleRequest(req, res)
    })
    .listen(PORT)
})

Durch die Verwendung des Kontexts eines Domains und der Widerstandsfähigkeit, unser Programm in mehrere Worker-Prozesse zu trennen, können wir angemessener reagieren und Fehler mit viel größerer Sicherheit behandeln.

js
// Viel besser!

const cluster = require('node:cluster')
const PORT = +process.env.PORT || 1337

if (cluster.isPrimary) {
  // Ein realistischeres Szenario hätte mehr als 2 Worker,
  // und würde den primären und den Worker vielleicht nicht in dieselbe Datei setzen.
  //
  // Es ist auch möglich, etwas ausgefeilter zu protokollieren und
  // jede benutzerdefinierte Logik zu implementieren, die benötigt wird, um DoS-
  // Angriffe und anderes schlechtes Verhalten zu verhindern.
  //
  // Siehe die Optionen in der Cluster-Dokumentation.
  //
  // Wichtig ist, dass der Primärprozess sehr wenig tut,
  // wodurch unsere Widerstandsfähigkeit gegen unerwartete Fehler erhöht wird.

  cluster.fork()
  cluster.fork()

  cluster.on('disconnect', worker => {
    console.error('Verbindung getrennt!')
    cluster.fork()
  })
} else {
  // der Worker
  //
  // Hier setzen wir unsere Fehler!

  const domain = require('node:domain')

  // Weitere Details zur Verwendung von
  // Worker-Prozessen zum Bearbeiten von Anforderungen finden Sie in der Cluster-Dokumentation. Funktionsweise, Einschränkungen usw.

  const server = require('node:http').createServer((req, res) => {
    const d = domain.create()
    d.on('error', er => {
      console.error(`Fehler ${er.stack}`)

      // Wir befinden uns in gefährlichem Gebiet!
      // Per Definition ist etwas Unerwartetes passiert,
      // was wir wahrscheinlich nicht wollten.
      // Jetzt kann alles passieren! Seien Sie sehr vorsichtig!

      try {
        // Stellen Sie sicher, dass wir innerhalb von 30 Sekunden herunterfahren
        const killtimer = setTimeout(() => {
          process.exit(1)
        }, 30000)
        // Aber halten Sie den Prozess nicht nur dafür offen!
        killtimer.unref()

        // Keine neuen Anfragen mehr annehmen.
        server.close()

        // Lassen Sie den Primärprozess wissen, dass wir tot sind. Dies löst eine
        // 'disconnect'-Verbindung im Cluster-Primärprozess aus, und dann wird ein
        // neuer Worker geforkt.
        cluster.worker.disconnect()

        // Versuchen Sie, einen Fehler an die Anfrage zu senden, die das Problem ausgelöst hat
        res.statusCode = 500
        res.setHeader('content-type', 'text/plain')
        res.end('Hoppla, da gab es ein Problem!\n')
      } catch (er2) {
        // Na ja, da können wir an dieser Stelle nicht viel machen.
        console.error(`Fehler beim Senden von 500! ${er2.stack}`)
      }
    })

    // Da req und res erstellt wurden, bevor dieser Domain existierte,
    // müssen wir sie explizit hinzufügen.
    // Siehe die Erklärung der impliziten vs. expliziten Bindung unten.
    d.add(req)
    d.add(res)

    // Führen Sie nun die Handler-Funktion im Domain aus.
    d.run(() => {
      handleRequest(req, res)
    })
  })
  server.listen(PORT)
}

// Dieser Teil ist nicht wichtig. Nur ein Beispiel für Routing.
// Hier wird die ausgefeilte Anwendungslogik eingefügt.
function handleRequest(req, res) {
  switch (req.url) {
    case '/error':
      // Wir machen einige asynchrone Dinge und dann...
      setTimeout(() => {
        // Hoppla!
        flerb.bark()
      }, timeout)
      break
    default:
      res.end('ok')
  }
}

Ergänzungen zu Error-Objekten

Jedes Mal, wenn ein Error-Objekt durch eine Domain geleitet wird, werden einige zusätzliche Felder hinzugefügt.

  • error.domain Die Domain, die den Fehler zuerst behandelt hat.
  • error.domainEmitter Der Event-Emitter, der ein 'error'-Ereignis mit dem Fehlerobjekt emittiert hat.
  • error.domainBound Die Callback-Funktion, die an die Domain gebunden war und einen Fehler als erstes Argument erhalten hat.
  • error.domainThrown Ein boolescher Wert, der angibt, ob der Fehler ausgelöst, emittiert oder an eine gebundene Callback-Funktion übergeben wurde.

Implizite Bindung

Wenn Domains verwendet werden, werden alle neuen EventEmitter-Objekte (einschließlich Stream-Objekte, Anfragen, Antworten usw.) implizit an die aktive Domain zum Zeitpunkt ihrer Erstellung gebunden.

Zusätzlich werden Callbacks, die an Low-Level-Event-Loop-Anfragen übergeben werden (z. B. an fs.open() oder andere Callback-akzeptierende Methoden), automatisch an die aktive Domain gebunden. Wenn sie einen Fehler auslösen, fängt die Domain den Fehler ab.

Um übermäßigen Speicherverbrauch zu vermeiden, werden Domain-Objekte selbst nicht implizit als Kinder der aktiven Domain hinzugefügt. Wenn dies der Fall wäre, wäre es zu einfach, zu verhindern, dass Request- und Response-Objekte ordnungsgemäß garbage collected werden.

Um Domain-Objekte als Kinder einer übergeordneten Domain zu verschachteln, müssen sie explizit hinzugefügt werden.

Implizite Bindung leitet ausgelöste Fehler und 'error'-Ereignisse an das 'error'-Ereignis der Domain weiter, registriert den EventEmitter aber nicht in der Domain. Implizite Bindung kümmert sich nur um ausgelöste Fehler und 'error'-Ereignisse.

Explizite Bindung

Manchmal ist die verwendete Domain nicht diejenige, die für einen bestimmten Event-Emitter verwendet werden sollte. Oder der Event-Emitter könnte im Kontext einer Domain erstellt worden sein, sollte aber stattdessen an eine andere Domain gebunden werden.

Beispielsweise könnte eine Domain für einen HTTP-Server verwendet werden, aber vielleicht möchten wir für jede Anfrage eine separate Domain verwenden.

Dies ist durch explizite Bindung möglich.

js
// Erstellung einer Top-Level-Domain für den Server
const domain = require('node:domain')
const http = require('node:http')
const serverDomain = domain.create()

serverDomain.run(() => {
  // Server wird im Scope von serverDomain erstellt
  http
    .createServer((req, res) => {
      // Req und res werden auch im Scope von serverDomain erstellt
      // wir würden jedoch lieber eine separate Domain für jede Anfrage haben.
      // erstelle sie zuerst und füge req und res hinzu.
      const reqd = domain.create()
      reqd.add(req)
      reqd.add(res)
      reqd.on('error', er => {
        console.error('Error', er, req.url)
        try {
          res.writeHead(500)
          res.end('Error occurred, sorry.')
        } catch (er2) {
          console.error('Error sending 500', er2, req.url)
        }
      })
    })
    .listen(1337)
})

domain.create()

Klasse: Domain

Die Klasse Domain kapselt die Funktionalität des Routens von Fehlern und unaufgefangenen Ausnahmen an das aktive Domain-Objekt.

Um die abgefangenen Fehler zu behandeln, lauschen Sie auf sein 'error'-Ereignis.

domain.members

Ein Array von Timern und Event Emittern, die explizit dem Domain hinzugefügt wurden.

domain.add(emitter)

Fügt einen Emitter explizit dem Domain hinzu. Wenn ein vom Emitter aufgerufener Ereignis-Handler einen Fehler auslöst oder der Emitter ein 'error'-Ereignis auslöst, wird er an das 'error'-Ereignis des Domains weitergeleitet, genau wie bei impliziter Bindung.

Dies funktioniert auch mit Timern, die von setInterval() und setTimeout() zurückgegeben werden. Wenn ihre Callback-Funktion einen Fehler auslöst, wird dieser vom Domain 'error'-Handler abgefangen.

Wenn der Timer oder EventEmitter bereits an ein Domain gebunden war, wird er von diesem entfernt und stattdessen an dieses gebunden.

domain.bind(callback)

Die zurückgegebene Funktion ist ein Wrapper um die angegebene Callback-Funktion. Wenn die zurückgegebene Funktion aufgerufen wird, werden alle ausgelösten Fehler an das 'error'-Ereignis des Domains weitergeleitet.

js
const d = domain.create()

function readSomeFile(filename, cb) {
  fs.readFile(
    filename,
    'utf8',
    d.bind((er, data) => {
      // Wenn dies einen Fehler auslöst, wird dieser auch an das Domain weitergegeben.
      return cb(er, data ? JSON.parse(data) : null)
    })
  )
}

d.on('error', er => {
  // Irgendwo ist ein Fehler aufgetreten. Wenn wir ihn jetzt auslösen, stürzt das Programm
  // mit der normalen Zeilennummer und Stack-Meldung ab.
})

domain.enter()

Die Methode enter() ist eine interne Funktion, die von den Methoden run(), bind() und intercept() verwendet wird, um die aktive Domain festzulegen. Sie setzt domain.active und process.domain auf die Domain und pusht die Domain implizit auf den vom Domain-Modul verwalteten Domain-Stack (siehe domain.exit() für Details zum Domain-Stack). Der Aufruf von enter() begrenzt den Beginn einer Kette asynchroner Aufrufe und E/A-Operationen, die an eine Domain gebunden sind.

Der Aufruf von enter() ändert nur die aktive Domain und verändert die Domain selbst nicht. enter() und exit() können beliebig oft für eine einzelne Domain aufgerufen werden.

domain.exit()

Die Methode exit() verlässt die aktuelle Domain und entfernt sie vom Domain-Stack. Wann immer die Ausführung in den Kontext einer anderen Kette asynchroner Aufrufe wechselt, ist es wichtig sicherzustellen, dass die aktuelle Domain verlassen wird. Der Aufruf von exit() begrenzt entweder das Ende oder eine Unterbrechung der Kette asynchroner Aufrufe und E/A-Operationen, die an eine Domain gebunden sind.

Wenn mehrere, verschachtelte Domains an den aktuellen Ausführungskontext gebunden sind, verlässt exit() alle in dieser Domain verschachtelten Domains.

Der Aufruf von exit() ändert nur die aktive Domain und verändert die Domain selbst nicht. enter() und exit() können beliebig oft für eine einzelne Domain aufgerufen werden.

domain.intercept(callback)

Diese Methode ist fast identisch mit domain.bind(callback). Zusätzlich zum Abfangen ausgelöster Fehler fängt sie jedoch auch Error-Objekte ab, die als erstes Argument an die Funktion gesendet werden.

Auf diese Weise kann das gängige Muster if (err) return callback(err); durch einen einzigen Fehlerhandler an einer einzigen Stelle ersetzt werden.

js
const d = domain.create()

function readSomeFile(filename, cb) {
  fs.readFile(
    filename,
    'utf8',
    d.intercept(data => {
      // Hinweis: Das erste Argument wird niemals an den
      // Callback weitergegeben, da davon ausgegangen wird, dass es sich um das 'Error'-Argument handelt
      // und somit von der Domain abgefangen wird.

      // Wenn dies einen Fehler auslöst, wird dieser auch an die Domain weitergegeben
      // so dass die Fehlerbehandlungslogik in das 'error'-Ereignis der Domain verschoben werden kann, anstatt im gesamten Programm wiederholt zu werden.
      return cb(null, JSON.parse(data))
    })
  )
}

d.on('error', er => {
  // Irgendwo ist ein Fehler aufgetreten. Wenn wir ihn jetzt auslösen, stürzt das Programm ab
  // mit der normalen Zeilennummer und Stack-Meldung.
})

domain.remove(emitter)

Das Gegenteil von domain.add(emitter). Entfernt die Domain-Handhabung vom angegebenen Emitter.

domain.run(fn[, ...args])

Führt die angegebene Funktion im Kontext der Domain aus und bindet implizit alle Event-Emitter, Timer und Low-Level-Anfragen, die in diesem Kontext erstellt werden. Optional können Argumente an die Funktion übergeben werden.

Dies ist die grundlegendste Art, eine Domain zu verwenden.

js
const domain = require('node:domain')
const fs = require('node:fs')
const d = domain.create()
d.on('error', er => {
  console.error('Fehler gefangen!', er)
})
d.run(() => {
  process.nextTick(() => {
    setTimeout(() => {
      // Simulation verschiedener asynchroner Vorgänge
      fs.open('nicht existierende Datei', 'r', (er, fd) => {
        if (er) throw er
        // Fortfahren...
      })
    }, 100)
  })
})

In diesem Beispiel wird der d.on('error')-Handler ausgelöst, anstatt das Programm zum Absturz zu bringen.

Domains und Promises

Ab Node.js 8.0.0 werden die Handler von Promises innerhalb der Domain ausgeführt, in der der Aufruf von .then() oder .catch() selbst erfolgt ist:

js
const d1 = domain.create()
const d2 = domain.create()

let p
d1.run(() => {
  p = Promise.resolve(42)
})

d2.run(() => {
  p.then(v => {
    // wird in d2 ausgeführt
  })
})

Ein Callback kann mit domain.bind(callback) an eine bestimmte Domain gebunden werden:

js
const d1 = domain.create()
const d2 = domain.create()

let p
d1.run(() => {
  p = Promise.resolve(42)
})

d2.run(() => {
  p.then(
    p.domain.bind(v => {
      // wird in d1 ausgeführt
    })
  )
})

Domains beeinflussen die Fehlerbehandlungsmechanismen für Promises nicht. Mit anderen Worten, für nicht behandelte Promise-Ablehnungen wird kein 'error'-Ereignis emittiert.