Skip to content

Dominio

[Storia]

VersioneCambiamenti
v8.8.0Nessuna Promise creata nei contesti VM ha più una proprietà .domain. I loro gestori vengono comunque eseguiti nel dominio corretto, e le Promise create nel contesto principale conservano ancora una proprietà .domain.
v8.0.0I gestori per le Promise vengono ora invocati nel dominio in cui è stata creata la prima promise di una catena.
v1.4.2Deprecato dal: v1.4.2

[Stabile: 0 - Deprecato]

Stabile: 0 Stabilità: 0 - Deprecato

Codice Sorgente: lib/domain.js

Questo modulo è in attesa di essere deprecato. Una volta finalizzata un'API sostitutiva, questo modulo sarà completamente deprecato. La maggior parte degli sviluppatori non dovrebbe avere motivo di utilizzare questo modulo. Gli utenti che necessitano assolutamente della funzionalità fornita dai domini possono fare affidamento su di essa per il momento, ma dovrebbero aspettarsi di dover migrare a una soluzione diversa in futuro.

I domini forniscono un modo per gestire più operazioni di I/O diverse come un singolo gruppo. Se uno qualsiasi degli emettitori di eventi o dei callback registrati in un dominio emette un evento 'error', o lancia un errore, allora l'oggetto dominio verrà notificato, invece di perdere il contesto dell'errore nel gestore process.on('uncaughtException'), o causando l'uscita immediata del programma con un codice di errore.

Attenzione: Non ignorare gli errori!

I gestori di errori del dominio non sono un sostituto per la chiusura di un processo quando si verifica un errore.

Per la natura stessa di come funziona throw in JavaScript, non c'è quasi mai modo di "riprendere da dove si era interrotto" in modo sicuro, senza perdite di riferimenti o la creazione di qualche altro tipo di stato fragile indefinito.

Il modo più sicuro per rispondere a un errore generato è chiudere il processo. Naturalmente, in un normale web server, potrebbero esserci molte connessioni aperte e non è ragionevole chiuderle bruscamente perché un errore è stato innescato da qualcun altro.

L'approccio migliore è inviare una risposta di errore alla richiesta che ha innescato l'errore, lasciando che le altre terminino nel loro tempo normale e interrompendo l'ascolto di nuove richieste in quel worker.

In questo modo, l'utilizzo del domain va di pari passo con il modulo cluster, poiché il processo principale può forcare un nuovo worker quando un worker incontra un errore. Per i programmi Node.js che scalano a più macchine, il proxy di terminazione o il registro dei servizi possono prendere nota del fallimento e reagire di conseguenza.

Ad esempio, questa non è una buona idea:

js
// XXX ATTENZIONE! BRUTTA IDEA!

const d = require('node:domain').create()
d.on('error', er => {
  // L'errore non bloccherà il processo, ma ciò che fa è peggio!
  // Anche se abbiamo impedito il riavvio brusco del processo, stiamo perdendo
  // un sacco di risorse se questo accade.
  // Non è meglio di process.on('uncaughtException')!
  console.log(`errore, ma oh beh ${er.message}`)
})
d.run(() => {
  require('node:http')
    .createServer((req, res) => {
      handleRequest(req, res)
    })
    .listen(PORT)
})

Utilizzando il contesto di un dominio e la resilienza della separazione del nostro programma in più processi worker, possiamo reagire in modo più appropriato e gestire gli errori con molta maggiore sicurezza.

js
// Molto meglio!

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

if (cluster.isPrimary) {
  // Uno scenario più realistico avrebbe più di 2 worker,
  // e forse non metterebbe il primary e il worker nello stesso file.
  //
  // È anche possibile diventare un po' più fantasiosi riguardo al logging e
  // implementare qualsiasi logica personalizzata sia necessaria per prevenire DoS
  // attacchi e altri comportamenti dannosi.
  //
  // Vedi le opzioni nella documentazione del cluster.
  //
  // La cosa importante è che il primary faccia pochissimo,
  // aumentando la nostra resilienza a errori inattesi.

  cluster.fork()
  cluster.fork()

  cluster.on('disconnect', worker => {
    console.error('disconnesso!')
    cluster.fork()
  })
} else {
  // il worker
  //
  // Qui è dove mettiamo i nostri bug!

  const domain = require('node:domain')

  // Vedi la documentazione del cluster per maggiori dettagli sull'utilizzo
  // di processi worker per servire le richieste. Come funziona, avvertenze, ecc.

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

      // Siamo in un territorio pericoloso!
      // Per definizione, si è verificato qualcosa di inaspettato,
      // che probabilmente non volevamo.
      // Tutto può succedere ora! Fare molta attenzione!

      try {
        // Assicurati di chiudere entro 30 secondi
        const killtimer = setTimeout(() => {
          process.exit(1)
        }, 30000)
        // Ma non tenere aperto il processo solo per questo!
        killtimer.unref()

        // Interrompi la ricezione di nuove richieste.
        server.close()

        // Comunica al primary che siamo morti. Questo innescherà un
        // 'disconnect' nel cluster primary, e poi forcherà
        // un nuovo worker.
        cluster.worker.disconnect()

        // Prova a inviare un errore alla richiesta che ha innescato il problema
        res.statusCode = 500
        res.setHeader('content-type', 'text/plain')
        res.end("Oops, c'è stato un problema!\n")
      } catch (er2) {
        // Oh beh, non c'è molto che possiamo fare a questo punto.
        console.error(`Errore nell'invio del 500! ${er2.stack}`)
      }
    })

    // Poiché req e res sono stati creati prima che questo dominio esistesse,
    // dobbiamo aggiungerli esplicitamente.
    // Vedi la spiegazione del binding implicito vs. esplicito di seguito.
    d.add(req)
    d.add(res)

    // Ora esegui la funzione gestore nel dominio.
    d.run(() => {
      handleRequest(req, res)
    })
  })
  server.listen(PORT)
}

// Questa parte non è importante. Solo un esempio di routing.
// Inserisci qui la logica dell'applicazione complessa.
function handleRequest(req, res) {
  switch (req.url) {
    case '/error':
      // Facciamo alcune cose asincrone, e poi...
      setTimeout(() => {
        // Ops!
        flerb.bark()
      }, timeout)
      break
    default:
      res.end('ok')
  }
}

Aggiunte agli oggetti Error

Ogni volta che un oggetto Error viene instradato attraverso un dominio, gli vengono aggiunti alcuni campi extra.

  • error.domain Il dominio che ha gestito per primo l'errore.
  • error.domainEmitter L'emitter di eventi che ha emesso un evento 'error' con l'oggetto errore.
  • error.domainBound La funzione di callback che è stata associata al dominio e a cui è stato passato un errore come primo argomento.
  • error.domainThrown Un booleano che indica se l'errore è stato lanciato, emesso o passato a una funzione di callback associata.

Associazione implicita

Se i domini sono in uso, tutti i nuovi oggetti EventEmitter (inclusi gli oggetti Stream, le richieste, le risposte, ecc.) saranno implicitamente associati al dominio attivo al momento della loro creazione.

Inoltre, le callback passate alle richieste di basso livello del ciclo di eventi (come a fs.open() o ad altri metodi che accettano callback) saranno automaticamente associate al dominio attivo. Se lanciano un'eccezione, il dominio intercetterà l'errore.

Al fine di prevenire un eccessivo utilizzo della memoria, gli oggetti Domain stessi non vengono aggiunti implicitamente come figli del dominio attivo. Se lo fossero, sarebbe troppo facile impedire la corretta garbage collection degli oggetti di richiesta e risposta.

Per annidare gli oggetti Domain come figli di un Domain padre, devono essere aggiunti esplicitamente.

L'associazione implicita instrada gli errori lanciati e gli eventi 'error' all'evento 'error' del Domain, ma non registra l'EventEmitter sul Domain. L'associazione implicita si occupa solo degli errori lanciati e degli eventi 'error'.

Associazione esplicita

A volte, il dominio in uso non è quello che dovrebbe essere utilizzato per uno specifico emitter di eventi. Oppure, l'emitter di eventi potrebbe essere stato creato nel contesto di un dominio, ma dovrebbe invece essere associato a un altro dominio.

Ad esempio, potrebbe esserci un dominio in uso per un server HTTP, ma forse vorremmo avere un dominio separato da utilizzare per ogni richiesta.

Ciò è possibile tramite l'associazione esplicita.

js
// Crea un dominio di livello superiore per il server
const domain = require('node:domain')
const http = require('node:http')
const serverDomain = domain.create()

serverDomain.run(() => {
  // Il server viene creato nell'ambito di serverDomain
  http
    .createServer((req, res) => {
      // Anche req e res vengono creati nell'ambito di serverDomain
      // tuttavia, preferiremmo avere un dominio separato per ogni richiesta.
      // creiamolo subito e aggiungiamo req e res ad esso.
      const reqd = domain.create()
      reqd.add(req)
      reqd.add(res)
      reqd.on('error', er => {
        console.error('Errore', er, req.url)
        try {
          res.writeHead(500)
          res.end('Si è verificato un errore, ci scusiamo.')
        } catch (er2) {
          console.error("Errore durante l'invio di 500", er2, req.url)
        }
      })
    })
    .listen(1337)
})

domain.create()

Classe: Domain

La classe Domain incapsula la funzionalità di indirizzare errori ed eccezioni non gestite all'oggetto Domain attivo.

Per gestire gli errori che rileva, ascolta il suo evento 'error'.

domain.members

Un array di timer ed emettitori di eventi che sono stati aggiunti esplicitamente al dominio.

domain.add(emitter)

Aggiunge esplicitamente un emettitore al dominio. Se qualsiasi gestore di eventi chiamato dall'emettitore genera un errore, o se l'emettitore emette un evento 'error', verrà indirizzato all'evento 'error' del dominio, proprio come con il binding implicito.

Questo funziona anche con i timer restituiti da setInterval() e setTimeout(). Se la loro funzione di callback genera un'eccezione, verrà intercettata dal gestore 'error' del dominio.

Se il timer o EventEmitter era già associato a un dominio, viene rimosso da quello e associato invece a questo.

domain.bind(callback)

La funzione restituita sarà un wrapper attorno alla funzione di callback fornita. Quando la funzione restituita viene chiamata, tutti gli errori che vengono generati verranno indirizzati all'evento 'error' del dominio.

js
const d = domain.create()

function readSomeFile(filename, cb) {
  fs.readFile(
    filename,
    'utf8',
    d.bind((er, data) => {
      // Se questo genera un'eccezione, verrà anche passato al dominio.
      return cb(er, data ? JSON.parse(data) : null)
    })
  )
}

d.on('error', er => {
  // Si è verificato un errore da qualche parte. Se lo lanciamo ora, il programma si arresterà
  // con il normale numero di riga e messaggio di stack.
})

domain.enter()

Il metodo enter() è un meccanismo utilizzato dai metodi run(), bind() e intercept() per impostare il dominio attivo. Imposta domain.active e process.domain sul dominio e implicitamente inserisce il dominio nello stack di dominio gestito dal modulo dominio (vedi domain.exit() per i dettagli sullo stack di dominio). La chiamata a enter() delimita l'inizio di una catena di chiamate asincrone e operazioni di I/O associate a un dominio.

La chiamata a enter() modifica solo il dominio attivo e non altera il dominio stesso. enter() e exit() possono essere chiamati un numero arbitrario di volte su un singolo dominio.

domain.exit()

Il metodo exit() esce dal dominio corrente, rimuovendolo dallo stack di dominio. Ogni volta che l'esecuzione sta per passare al contesto di una diversa catena di chiamate asincrone, è importante assicurarsi che si esca dal dominio corrente. La chiamata a exit() delimita la fine o un'interruzione della catena di chiamate asincrone e operazioni di I/O associate a un dominio.

Se ci sono più domini nidificati associati al contesto di esecuzione corrente, exit() uscirà da tutti i domini nidificati all'interno di questo dominio.

La chiamata a exit() modifica solo il dominio attivo e non altera il dominio stesso. enter() e exit() possono essere chiamati un numero arbitrario di volte su un singolo dominio.

domain.intercept(callback)

Questo metodo è quasi identico a domain.bind(callback). Tuttavia, oltre a catturare gli errori generati, intercetterà anche gli oggetti Error inviati come primo argomento alla funzione.

In questo modo, il pattern comune if (err) return callback(err); può essere sostituito con un singolo gestore di errori in un unico posto.

js
const d = domain.create()

function readSomeFile(filename, cb) {
  fs.readFile(
    filename,
    'utf8',
    d.intercept(data => {
      // Si noti che il primo argomento non viene mai passato al
      // callback in quanto si presume che sia l'argomento 'Error'
      // e quindi intercettato dal dominio.

      // Se questo genera un errore, verrà anche passato al dominio
      // in modo che la logica di gestione degli errori possa essere spostata all'evento 'error'
      // sul dominio invece di essere ripetuta in tutto
      // il programma.
      return cb(null, JSON.parse(data))
    })
  )
}

d.on('error', er => {
  // Si è verificato un errore da qualche parte. Se lo lanciamo ora, il programma si bloccherà
  // con il normale numero di riga e il messaggio di stack.
})

domain.remove(emitter)

L'opposto di domain.add(emitter). Rimuove la gestione del dominio dall'emitter specificato.

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

Esegue la funzione fornita nel contesto del dominio, collegando implicitamente tutti gli emettitori di eventi, i timer e le richieste di basso livello creati in quel contesto. Facoltativamente, gli argomenti possono essere passati alla funzione.

Questo è il modo più basilare per usare un dominio.

js
const domain = require('node:domain')
const fs = require('node:fs')
const d = domain.create()
d.on('error', er => {
  console.error('Errore intercettato!', er)
})
d.run(() => {
  process.nextTick(() => {
    setTimeout(() => {
      // Simulazione di varie cose asincrone
      fs.open('file-non-esistente', 'r', (er, fd) => {
        if (er) throw er
        // procedi...
      })
    }, 100)
  })
})

In questo esempio, il gestore d.on('error') verrà attivato, invece di mandare in crash il programma.

Domini e promesse

A partire da Node.js 8.0.0, i gestori delle promesse vengono eseguiti all'interno del dominio in cui è stata effettuata la chiamata a .then() o .catch() stessa:

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

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

d2.run(() => {
  p.then(v => {
    // in esecuzione in d2
  })
})

Un callback può essere associato a un dominio specifico usando domain.bind(callback):

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 => {
      // in esecuzione in d1
    })
  )
})

I domini non interferiranno con i meccanismi di gestione degli errori per le promesse. In altre parole, non verrà emesso alcun evento 'error' per i Promise rifiutati non gestiti.