Домен
[История]
Версия | Изменения |
---|---|
v8.8.0 | Все Promise , созданные в контекстах VM, больше не имеют свойства .domain . Однако их обработчики по-прежнему выполняются в правильном домене, а Promise , созданные в основном контексте, по-прежнему имеют свойство .domain . |
v8.0.0 | Обработчики для Promise теперь вызываются в том домене, в котором был создан первый промис цепочки. |
v1.4.2 | Устарело с версии: v1.4.2 |
[Стабильность: 0 - Устарело]
Стабильность: 0 Стабильность: 0 - Устарело
Исходный код: lib/domain.js
Этот модуль ожидает устаревания. После того, как будет доработан API для замены, этот модуль будет полностью объявлен устаревшим. Большинству разработчиков не придется использовать этот модуль. Пользователи, которым абсолютно необходим функционал, предоставляемый доменами, могут полагаться на него в настоящее время, но должны быть готовы к миграции на другое решение в будущем.
Домены предоставляют способ обрабатывать несколько различных операций ввода-вывода как одну группу. Если какой-либо из генераторов событий или обратных вызовов, зарегистрированных в домене, генерирует событие 'error'
или выбрасывает ошибку, то объект домена будет уведомлен, а не потеряет контекст ошибки в обработчике process.on('uncaughtException')
или вызовет немедленное завершение программы с кодом ошибки.
Предупреждение: не игнорируйте ошибки!
Обработчики ошибок домена не являются заменой закрытию процесса при возникновении ошибки.
По самой сути того, как работает throw
в JavaScript, почти никогда нет возможности безопасно "продолжить с того места, где остановились", без утечки ссылок или создания какого-либо другого неопределенного хрупкого состояния.
Самый безопасный способ отреагировать на выброшенную ошибку — это закрыть процесс. Конечно, в обычном веб-сервере может быть много открытых соединений, и неразумно резко закрывать их из-за того, что ошибка была вызвана кем-то другим.
Лучший подход — отправить ответ об ошибке на запрос, который вызвал ошибку, позволив остальным завершиться в обычное время, и прекратить прослушивание новых запросов в этом рабочем процессе.
Таким образом, использование domain
идет рука об руку с модулем cluster, поскольку основной процесс может разветвить новый рабочий процесс, когда рабочий процесс сталкивается с ошибкой. Для программ Node.js, которые масштабируются до нескольких машин, завершающий прокси или реестр сервисов может принять к сведению сбой и отреагировать соответственно.
Например, это не очень хорошая идея:
// XXX ПРЕДУПРЕЖДЕНИЕ! ПЛОХАЯ ИДЕЯ!
const d = require('node:domain').create()
d.on('error', er => {
// Ошибка не приведет к сбою процесса, но то, что она делает, еще хуже!
// Хотя мы предотвратили резкий перезапуск процесса, у нас утекает
// много ресурсов, если это когда-нибудь произойдет.
// Это не лучше, чем process.on('uncaughtException')!
console.log(`ошибка, ну и ладно ${er.message}`)
})
d.run(() => {
require('node:http')
.createServer((req, res) => {
handleRequest(req, res)
})
.listen(PORT)
})
Используя контекст домена и отказоустойчивость разделения нашей программы на несколько рабочих процессов, мы можем реагировать более адекватно и обрабатывать ошибки с гораздо большей безопасностью.
// Гораздо лучше!
const cluster = require('node:cluster')
const PORT = +process.env.PORT || 1337
if (cluster.isPrimary) {
// Более реалистичный сценарий имел бы более 2 рабочих процессов,
// и, возможно, не помещал бы основной и рабочий процесс в один и тот же файл.
//
// Также можно немного изощриться с ведением журнала и
// реализовать любую пользовательскую логику, необходимую для предотвращения DoS
// атак и других плохих действий.
//
// См. параметры в документации по кластеру.
//
// Важно то, что основной делает очень мало,
// повышая нашу устойчивость к неожиданным ошибкам.
cluster.fork()
cluster.fork()
cluster.on('disconnect', worker => {
console.error('отключено!')
cluster.fork()
})
} else {
// рабочий процесс
//
// Вот куда мы помещаем наши ошибки!
const domain = require('node:domain')
// См. документацию по кластеру для получения более подробной информации об использовании
// рабочих процессов для обслуживания запросов. Как это работает, предостережения и т. д.
const server = require('node:http').createServer((req, res) => {
const d = domain.create()
d.on('error', er => {
console.error(`ошибка ${er.stack}`)
// Мы находимся на опасной территории!
// По определению произошло что-то неожиданное,
// чего мы, вероятно, не хотели.
// Теперь может случиться что угодно! Будьте очень осторожны!
try {
// Убедитесь, что мы закрываемся в течение 30 секунд
const killtimer = setTimeout(() => {
process.exit(1)
}, 30000)
// Но не держите процесс открытым только для этого!
killtimer.unref()
// Прекратите принимать новые запросы.
server.close()
// Сообщите основному процессу, что мы мертвы. Это вызовет
// 'отключение' в основном кластере, а затем он разветвит
// новый рабочий процесс.
cluster.worker.disconnect()
// Попробуйте отправить ошибку на запрос, который вызвал проблему
res.statusCode = 500
res.setHeader('content-type', 'text/plain')
res.end('Ой, возникла проблема!\n')
} catch (er2) {
// Ну что ж, на этом этапе мы мало что можем сделать.
console.error(`Ошибка отправки 500! ${er2.stack}`)
}
})
// Поскольку req и res были созданы до того, как этот домен существовал,
// нам нужно явно добавить их.
// См. объяснение неявной и явной привязки ниже.
d.add(req)
d.add(res)
// Теперь запустите функцию-обработчик в домене.
d.run(() => {
handleRequest(req, res)
})
})
server.listen(PORT)
}
// Эта часть не важна. Просто пример маршрутизации.
// Поместите сюда сложную логику приложения.
function handleRequest(req, res) {
switch (req.url) {
case '/error':
// Мы делаем некоторые асинхронные вещи, а затем...
setTimeout(() => {
// Ой!
flerb.bark()
}, timeout)
break
default:
res.end('ok')
}
}
Дополнения к объектам Error
Каждый раз, когда объект Error
проходит через домен, к нему добавляется несколько дополнительных полей.
error.domain
Домен, который первым обработал ошибку.error.domainEmitter
Генератор событий, который сгенерировал событие'error'
с объектом ошибки.error.domainBound
Функция обратного вызова, которая была привязана к домену и получила ошибку в качестве первого аргумента.error.domainThrown
Логическое значение, указывающее, была ли ошибка выброшена, сгенерирована или передана в связанную функцию обратного вызова.
Неявная привязка
Если домены используются, то все новые объекты EventEmitter
(включая объекты Stream
, запросы, ответы и т. д.) будут неявно привязаны к активному домену во время их создания.
Кроме того, обратные вызовы, переданные в низкоуровневые запросы цикла событий (например, в fs.open()
или другие методы, принимающие обратные вызовы), будут автоматически привязаны к активному домену. Если они выбрасывают исключение, то домен перехватит ошибку.
Чтобы предотвратить чрезмерное использование памяти, сами объекты Domain
не добавляются неявно в качестве дочерних элементов активного домена. Если бы это было так, то было бы слишком легко предотвратить правильный сбор мусора объектов запросов и ответов.
Чтобы вложить объекты Domain
как дочерние объекты родительского Domain
, они должны быть добавлены явно.
Неявная привязка направляет выброшенные ошибки и события 'error'
в событие 'error'
Domain
, но не регистрирует EventEmitter
в Domain
. Неявная привязка заботится только о выброшенных ошибках и событиях 'error'
.
Явная привязка
Иногда используемый домен не является тем, который следует использовать для конкретного генератора событий. Или же генератор событий мог быть создан в контексте одного домена, но вместо этого должен быть привязан к какому-либо другому домену.
Например, может существовать один домен, используемый для HTTP-сервера, но, возможно, мы хотели бы иметь отдельный домен для каждого запроса.
Это возможно посредством явной привязки.
// Создаем домен верхнего уровня для сервера
const domain = require('node:domain')
const http = require('node:http')
const serverDomain = domain.create()
serverDomain.run(() => {
// Сервер создается в области действия serverDomain
http
.createServer((req, res) => {
// Req и res также создаются в области действия serverDomain
// однако, мы предпочли бы иметь отдельный домен для каждого запроса.
// создадим его в первую очередь и добавим к нему req и res.
const reqd = domain.create()
reqd.add(req)
reqd.add(res)
reqd.on('error', er => {
console.error('Ошибка', er, req.url)
try {
res.writeHead(500)
res.end('Произошла ошибка, извините.')
} catch (er2) {
console.error('Ошибка при отправке 500', er2, req.url)
}
})
})
.listen(1337)
})
domain.create()
- Возвращает: <Domain>
Класс: Domain
- Расширяет: <EventEmitter>
Класс Domain
инкапсулирует функциональность маршрутизации ошибок и необработанных исключений в активный объект Domain
.
Для обработки перехваченных ошибок, прослушайте его событие 'error'
.
domain.members
Массив таймеров и излучателей событий, которые были явно добавлены в домен.
domain.add(emitter)
emitter
<EventEmitter> | <Timer> излучатель или таймер для добавления в домен
Явно добавляет излучатель в домен. Если какие-либо обработчики событий, вызываемые излучателем, выбрасывают ошибку, или если излучатель испускает событие 'error'
, оно будет направлено на событие 'error'
домена, как и при неявном связывании.
Это также работает с таймерами, возвращаемыми из setInterval()
и setTimeout()
. Если их функция обратного вызова выбрасывает исключение, оно будет перехвачено обработчиком 'error'
домена.
Если Timer
или EventEmitter
уже были привязаны к домену, он удаляется из него и привязывается к этому вместо этого.
domain.bind(callback)
callback
<Function> Функция обратного вызова- Возвращает: <Function> Связанная функция
Возвращаемая функция будет оберткой вокруг предоставленной функции обратного вызова. Когда вызывается возвращаемая функция, любые ошибки, которые выбрасываются, будут направлены на событие 'error'
домена.
const d = domain.create()
function readSomeFile(filename, cb) {
fs.readFile(
filename,
'utf8',
d.bind((er, data) => {
// Если это выбрасывает, это также будет передано в домен.
return cb(er, data ? JSON.parse(data) : null)
})
)
}
d.on('error', er => {
// Где-то произошла ошибка. Если мы выбросим ее сейчас, программа аварийно завершится
// с обычным номером строки и сообщением стека.
})
domain.enter()
Метод enter()
является служебным методом, используемым методами run()
, bind()
и intercept()
для установки активного домена. Он устанавливает domain.active
и process.domain
в домен и неявно помещает домен в стек доменов, управляемый модулем доменов (подробности о стеке доменов см. в domain.exit()
). Вызов enter()
определяет начало цепочки асинхронных вызовов и операций ввода-вывода, привязанных к домену.
Вызов enter()
изменяет только активный домен и не изменяет сам домен. enter()
и exit()
можно вызывать произвольное количество раз для одного домена.
domain.exit()
Метод exit()
выходит из текущего домена, удаляя его из стека доменов. Всякий раз, когда выполнение переключается в контекст другой цепочки асинхронных вызовов, важно убедиться, что текущий домен был завершен. Вызов exit()
определяет конец или прерывание цепочки асинхронных вызовов и операций ввода-вывода, привязанных к домену.
Если к текущему контексту выполнения привязано несколько вложенных доменов, exit()
завершит все домены, вложенные в этот домен.
Вызов exit()
изменяет только активный домен и не изменяет сам домен. enter()
и exit()
можно вызывать произвольное количество раз для одного домена.
domain.intercept(callback)
callback
<Function> Функция обратного вызова- Возвращает: <Function> Перехваченная функция
Этот метод почти идентичен domain.bind(callback)
. Однако, в дополнение к перехвату выброшенных ошибок, он также будет перехватывать объекты Error
, отправленные в качестве первого аргумента функции.
Таким образом, распространенный шаблон if (err) return callback(err);
можно заменить на единый обработчик ошибок в одном месте.
const d = domain.create()
function readSomeFile(filename, cb) {
fs.readFile(
filename,
'utf8',
d.intercept(data => {
// Обратите внимание, что первый аргумент никогда не передается в
// обратный вызов, поскольку он считается аргументом 'Error'
// и, следовательно, перехватывается доменом.
// Если это вызывает ошибку, она также будет передана домену,
// поэтому логику обработки ошибок можно переместить в событие 'error'
// в домене, вместо того, чтобы повторять ее во всей программе.
return cb(null, JSON.parse(data))
})
)
}
d.on('error', er => {
// Где-то произошла ошибка. Если мы ее выбросим сейчас, программа
// аварийно завершится с обычным номером строки и сообщением стека.
})
domain.remove(emitter)
emitter
<EventEmitter> | <Timer> emitter или таймер, который нужно удалить из домена
Противоположность domain.add(emitter)
. Удаляет обработку домена из указанного emitter.
domain.run(fn[, ...args])
fn
<Function>...args
<any>
Запускает предоставленную функцию в контексте домена, неявно привязывая все event emitter, таймеры и низкоуровневые запросы, которые создаются в этом контексте. Необязательно, аргументы могут быть переданы в функцию.
Это самый базовый способ использования домена.
const domain = require('node:domain')
const fs = require('node:fs')
const d = domain.create()
d.on('error', er => {
console.error('Caught error!', er)
})
d.run(() => {
process.nextTick(() => {
setTimeout(() => {
// Имитация некоторых различных асинхронных вещей
fs.open('non-existent file', 'r', (er, fd) => {
if (er) throw er
// продолжить...
})
}, 100)
})
})
В этом примере будет вызван обработчик d.on('error')
, а не произойдет сбой программы.
Домены и промисы
Начиная с Node.js 8.0.0, обработчики промисов выполняются внутри домена, в котором был сделан сам вызов .then()
или .catch()
:
const d1 = domain.create()
const d2 = domain.create()
let p
d1.run(() => {
p = Promise.resolve(42)
})
d2.run(() => {
p.then(v => {
// выполняется в d2
})
})
Обратный вызов может быть привязан к определенному домену с помощью 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 => {
// выполняется в d1
})
)
})
Домены не будут вмешиваться в механизмы обработки ошибок для промисов. Другими словами, событие 'error'
не будет испущено для необработанных отклонений Promise
.