Skip to content

Dominio

[Historia]

VersiónCambios
v8.8.0Las Promise creadas en contextos de VM ya no tienen una propiedad .domain. Sin embargo, sus manejadores todavía se ejecutan en el dominio apropiado, y las Promise creadas en el contexto principal todavía poseen una propiedad .domain.
v8.0.0Los manejadores de Promise ahora se invocan en el dominio en el que se creó la primera promesa de una cadena.
v1.4.2Obsoleto desde: v1.4.2

[Estable: 0 - Obsoleto]

Estable: 0 Estabilidad: 0 - Obsoleto

Código fuente: lib/domain.js

Este módulo está pendiente de deprecación. Una vez que se haya finalizado una API de reemplazo, este módulo quedará completamente obsoleto. La mayoría de los desarrolladores no deberían tener motivos para usar este módulo. Los usuarios que absolutamente necesiten la funcionalidad que proporcionan los dominios pueden depender de él por el momento, pero deben esperar tener que migrar a una solución diferente en el futuro.

Los dominios proporcionan una forma de manejar múltiples operaciones de E/S diferentes como un solo grupo. Si alguno de los emisores de eventos o las funciones de devolución de llamada registradas en un dominio emiten un evento 'error', o lanzan un error, entonces se notificará al objeto de dominio, en lugar de perder el contexto del error en el manejador process.on('uncaughtException'), o causar que el programa se cierre inmediatamente con un código de error.

Advertencia: ¡No ignores los errores!

Los controladores de errores de dominio no son un sustituto para cerrar un proceso cuando ocurre un error.

Por la naturaleza misma de cómo funciona throw en JavaScript, casi nunca hay una forma segura de "retomar donde se dejó", sin filtrar referencias o crear algún tipo de estado frágil indefinido.

La forma más segura de responder a un error lanzado es cerrar el proceso. Por supuesto, en un servidor web normal, puede haber muchas conexiones abiertas, y no es razonable cerrarlas abruptamente porque un error fue provocado por otra persona.

El mejor enfoque es enviar una respuesta de error a la solicitud que desencadenó el error, mientras se permite que las demás finalicen en su tiempo normal, y dejar de escuchar nuevas solicitudes en ese trabajador.

De esta manera, el uso de domain va de la mano con el módulo cluster, ya que el proceso primario puede bifurcar un nuevo trabajador cuando un trabajador encuentra un error. Para los programas de Node.js que escalan a múltiples máquinas, el proxy de terminación o el registro de servicios pueden tomar nota del fallo y reaccionar en consecuencia.

Por ejemplo, esta no es una buena idea:

js
// XXX ¡ADVERTENCIA! ¡MALA IDEA!

const d = require('node:domain').create()
d.on('error', er => {
  // El error no bloqueará el proceso, ¡pero lo que hace es peor!
  // Aunque hemos evitado el reinicio abrupto del proceso, estamos filtrando
  // muchos recursos si esto sucede alguna vez.
  // ¡Esto no es mejor que process.on('uncaughtException')!
  console.log(`error, pero bueno ${er.message}`)
})
d.run(() => {
  require('node:http')
    .createServer((req, res) => {
      handleRequest(req, res)
    })
    .listen(PORT)
})

Al usar el contexto de un dominio y la resistencia de separar nuestro programa en múltiples procesos de trabajo, podemos reaccionar de manera más apropiada y manejar los errores con mucha mayor seguridad.

js
// ¡Mucho mejor!

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

if (cluster.isPrimary) {
  // Un escenario más realista tendría más de 2 trabajadores,
  // y quizás no pondría el primario y el trabajador en el mismo archivo.
  //
  // También es posible ser un poco más elegante con el registro, e
  // implementar cualquier lógica personalizada que se necesite para evitar ataques DoS
  // y otros malos comportamientos.
  //
  // Consulta las opciones en la documentación del cluster.
  //
  // Lo importante es que el primario haga muy poco,
  // aumentando nuestra resistencia a errores inesperados.

  cluster.fork()
  cluster.fork()

  cluster.on('disconnect', worker => {
    console.error('¡desconexión!')
    cluster.fork()
  })
} else {
  // el trabajador
  //
  // ¡Aquí es donde ponemos nuestros errores!

  const domain = require('node:domain')

  // Consulta la documentación del cluster para obtener más detalles sobre el uso de
  // procesos de trabajo para atender solicitudes. Cómo funciona, advertencias, etc.

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

      // ¡Estamos en territorio peligroso!
      // Por definición, ocurrió algo inesperado,
      // que probablemente no queríamos.
      // ¡Cualquier cosa puede pasar ahora! ¡Ten mucho cuidado!

      try {
        // Asegúrate de que cerremos en 30 segundos
        const killtimer = setTimeout(() => {
          process.exit(1)
        }, 30000)
        // ¡Pero no mantengas el proceso abierto solo por eso!
        killtimer.unref()

        // Deja de tomar nuevas solicitudes.
        server.close()

        // Hazle saber al primario que estamos muertos. Esto activará una
        // 'desconexión' en el primario del cluster y luego bifurcará
        // un nuevo trabajador.
        cluster.worker.disconnect()

        // Intenta enviar un error a la solicitud que provocó el problema
        res.statusCode = 500
        res.setHeader('content-type', 'text/plain')
        res.end('¡Ups, hubo un problema!\n')
      } catch (er2) {
        // Bueno, no hay mucho que podamos hacer en este punto.
        console.error(`¡Error al enviar 500! ${er2.stack}`)
      }
    })

    // Debido a que req y res se crearon antes de que existiera este dominio,
    // necesitamos agregarlos explícitamente.
    // Consulta la explicación del enlace implícito frente al explícito a continuación.
    d.add(req)
    d.add(res)

    // Ahora ejecuta la función de controlador en el dominio.
    d.run(() => {
      handleRequest(req, res)
    })
  })
  server.listen(PORT)
}

// Esta parte no es importante. Solo un ejemplo de enrutamiento.
// Pon lógica de aplicación compleja aquí.
function handleRequest(req, res) {
  switch (req.url) {
    case '/error':
      // Hacemos algunas cosas asíncronas y luego...
      setTimeout(() => {
        // ¡Ups!
        flerb.bark()
      }, timeout)
      break
    default:
      res.end('ok')
  }
}

Adiciones a los objetos Error

Cada vez que un objeto Error se enruta a través de un dominio, se le agregan algunos campos adicionales.

  • error.domain El dominio que manejó primero el error.
  • error.domainEmitter El emisor de eventos que emitió un evento 'error' con el objeto de error.
  • error.domainBound La función de retorno de llamada que se vinculó al dominio y se le pasó un error como su primer argumento.
  • error.domainThrown Un booleano que indica si el error fue lanzado, emitido o pasado a una función de retorno de llamada vinculada.

Enlace implícito

Si los dominios están en uso, entonces todos los objetos nuevos EventEmitter (incluidos los objetos Stream, solicitudes, respuestas, etc.) se enlazarán implícitamente al dominio activo en el momento de su creación.

Además, las funciones de retorno de llamada que se pasan a las solicitudes de bucle de eventos de bajo nivel (como a fs.open() u otros métodos que toman funciones de retorno de llamada) se enlazarán automáticamente al dominio activo. Si lanzan una excepción, el dominio atrapará el error.

Para evitar el uso excesivo de memoria, los propios objetos Domain no se agregan implícitamente como hijos del dominio activo. Si lo fueran, sería demasiado fácil evitar que los objetos de solicitud y respuesta se recolecten correctamente como basura.

Para anidar objetos Domain como hijos de un Domain padre, deben agregarse explícitamente.

El enlace implícito enruta los errores lanzados y los eventos 'error' al evento 'error' del Domain, pero no registra el EventEmitter en el Domain. El enlace implícito solo se encarga de los errores lanzados y los eventos 'error'.

Vinculación explícita

A veces, el dominio en uso no es el que debería usarse para un emisor de eventos específico. O bien, el emisor de eventos podría haberse creado en el contexto de un dominio, pero debería estar vinculado a algún otro dominio.

Por ejemplo, podría haber un dominio en uso para un servidor HTTP, pero tal vez nos gustaría tener un dominio separado para cada solicitud.

Eso es posible mediante la vinculación explícita.

js
// Crear un dominio de nivel superior para el servidor
const domain = require('node:domain')
const http = require('node:http')
const serverDomain = domain.create()

serverDomain.run(() => {
  // El servidor se crea en el ámbito de serverDomain
  http
    .createServer((req, res) => {
      // Req y res también se crean en el ámbito de serverDomain
      // sin embargo, preferiríamos tener un dominio separado para cada solicitud.
      // crearlo primero, y agregar req y res a él.
      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('Ocurrió un error, lo sentimos.')
        } catch (er2) {
          console.error('Error al enviar 500', er2, req.url)
        }
      })
    })
    .listen(1337)
})

domain.create()

Clase: Domain

La clase Domain encapsula la funcionalidad de enrutar errores y excepciones no capturadas al objeto Domain activo.

Para manejar los errores que captura, escucha su evento 'error'.

domain.members

Un array de temporizadores y emisores de eventos que se han añadido explícitamente al dominio.

domain.add(emitter)

Añade explícitamente un emisor al dominio. Si algún manejador de eventos llamado por el emisor lanza un error, o si el emisor emite un evento 'error', se enrutará al evento 'error' del dominio, al igual que con el enlace implícito.

Esto también funciona con los temporizadores que se devuelven de setInterval() y setTimeout(). Si su función de retrollamada lanza una excepción, la capturará el manejador 'error' del dominio.

Si el temporizador o EventEmitter ya estaba vinculado a un dominio, se elimina de ese y se vincula a este en su lugar.

domain.bind(callback)

La función devuelta será un envoltorio alrededor de la función de callback suministrada. Cuando se llama a la función devuelta, cualquier error que se produzca se enrutará al evento 'error' del dominio.

js
const d = domain.create()

function readSomeFile(filename, cb) {
  fs.readFile(
    filename,
    'utf8',
    d.bind((er, data) => {
      // Si esto lanza un error, también se pasará al dominio.
      return cb(er, data ? JSON.parse(data) : null)
    })
  )
}

d.on('error', er => {
  // Se produjo un error en algún lugar. Si lo lanzamos ahora, el programa fallará
  // con el número de línea normal y el mensaje de pila.
})

domain.enter()

El método enter() es una herramienta utilizada por los métodos run(), bind() e intercept() para establecer el dominio activo. Establece domain.active y process.domain al dominio, e implícitamente inserta el dominio en la pila de dominios administrada por el módulo de dominio (consulta domain.exit() para obtener detalles sobre la pila de dominios). La llamada a enter() delimita el comienzo de una cadena de llamadas asíncronas y operaciones de E/S vinculadas a un dominio.

Llamar a enter() solo cambia el dominio activo y no altera el dominio en sí. enter() y exit() se pueden llamar un número arbitrario de veces en un solo dominio.

domain.exit()

El método exit() sale del dominio actual, sacándolo de la pila de dominios. Cada vez que la ejecución vaya a cambiar al contexto de una cadena diferente de llamadas asíncronas, es importante asegurarse de que se sale del dominio actual. La llamada a exit() delimita el final o una interrupción de la cadena de llamadas asíncronas y operaciones de E/S vinculadas a un dominio.

Si hay varios dominios anidados vinculados al contexto de ejecución actual, exit() saldrá de cualquier dominio anidado dentro de este dominio.

Llamar a exit() solo cambia el dominio activo, y no altera el dominio en sí mismo. enter() y exit() se pueden llamar un número arbitrario de veces en un solo dominio.

domain.intercept(callback)

Este método es casi idéntico a domain.bind(callback). Sin embargo, además de capturar errores lanzados, también interceptará objetos Error enviados como el primer argumento a la función.

De esta manera, el patrón común if (err) return callback(err); se puede reemplazar con un único manejador de errores en un solo lugar.

js
const d = domain.create()

function readSomeFile(filename, cb) {
  fs.readFile(
    filename,
    'utf8',
    d.intercept(data => {
      // Nótese que el primer argumento nunca se pasa a la
      // callback ya que se asume que es el argumento 'Error'
      // y por lo tanto es interceptado por el dominio.

      // Si esto lanza un error, también se pasará al dominio
      // para que la lógica de manejo de errores se pueda mover al evento
      // 'error' del dominio en lugar de repetirse por todo
      // el programa.
      return cb(null, JSON.parse(data))
    })
  )
}

d.on('error', er => {
  // Ocurrió un error en alguna parte. Si lo lanzamos ahora, el programa fallará
  // con el número de línea normal y el mensaje de pila.
})

domain.remove(emitter)

Lo contrario de domain.add(emitter). Elimina el manejo del dominio del emisor especificado.

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

Ejecuta la función proporcionada en el contexto del dominio, vinculando implícitamente todos los emisores de eventos, temporizadores y solicitudes de bajo nivel que se crean en ese contexto. Opcionalmente, se pueden pasar argumentos a la función.

Esta es la forma más básica de usar un dominio.

js
const domain = require('node:domain')
const fs = require('node:fs')
const d = domain.create()
d.on('error', er => {
  console.error('¡Error capturado!', er)
})
d.run(() => {
  process.nextTick(() => {
    setTimeout(() => {
      // Simulación de varias cosas asíncronas
      fs.open('archivo-inexistente', 'r', (er, fd) => {
        if (er) throw er
        // continuar...
      })
    }, 100)
  })
})

En este ejemplo, se activará el controlador d.on('error'), en lugar de bloquear el programa.

Dominios y promesas

A partir de Node.js 8.0.0, los controladores de promesas se ejecutan dentro del dominio en el que se realizó la llamada a .then() o .catch():

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

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

d2.run(() => {
  p.then(v => {
    // ejecutándose en d2
  })
})

Una retrollamada se puede vincular a un dominio específico 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 => {
      // ejecutándose en d1
    })
  )
})

Los dominios no interferirán con los mecanismos de manejo de errores para las promesas. En otras palabras, no se emitirá ningún evento 'error' para los rechazos de Promise no manejados.