Domain
[История]
Версия | Изменения |
---|---|
v8.8.0 | Любые Promise , созданные в контекстах VM, больше не имеют свойства .domain . Однако их обработчики по-прежнему выполняются в соответствующем домене, а Promise , созданные в основном контексте, по-прежнему имеют свойство .domain . |
v8.0.0 | Обработчики для Promise теперь вызываются в домене, в котором был создан первый промис цепочки. |
v1.4.2 | Устарело с версии: v1.4.2 |
[Stable: 0 - Deprecated]
Stable: 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('disconnect!');
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();
// Сообщите основному процессу, что мы мертвы. Это вызовет
// 'disconnect' в основном процессе кластера, а затем он форкнет
// новый рабочий процесс.
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('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()
- Возвращает: <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>
Выполняет предоставленную функцию в контексте домена, неявно привязывая все эмиттеры событий, таймеры и низкоуровневые запросы, которые создаются в этом контексте. Дополнительно, функции могут быть переданы аргументы.
Это самый простой способ использования домена.
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(() => { // Simulating some various async stuff
fs.open('non-existent file', 'r', (er, fd) => {
if (er) throw er;
// proceed...
});
}, 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) => {
// running in 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) => {
// running in d1
}));
});
Домены не будут вмешиваться в механизмы обработки ошибок для промисов. Другими словами, событие 'error'
не будет генерироваться для необработанных отклонений Promise
.