Домен
Исходный код: lib/domain.js
Для этого модуля планируется прекращение поддержки. После окончательной доработки API-замены поддержка этого модуля будет полностью прекращена. Большинству разработчиков не потребуется использовать этот модуль. Тем, кому совершенно необходимо использовать функциональность, предоставляемую доменами, пока можно на неё полагаться, но в будущем следует ожидать необходимости перейти на другое решение.
Домены позволяют обрабатывать несколько различных операций ввода-вывода как одну группу. Если какой-либо из эмиттеров событий или обратных вызовов, зарегистрированных в домене, генерирует событие 'error' или выбрасывает ошибку, объект домена будет уведомлён об этом. Так ошибка не потеряет свой контекст в обработчике process.on('uncaughtException'), и программа не завершится немедленно с кодом ошибки.
Предупреждение: не игнорируйте ошибки!
Обработчики ошибок домена не заменяют завершение процесса при возникновении ошибки.
В силу особенностей работы throw в JavaScript почти никогда не существует безопасного способа «продолжить с того места, где выполнение было прервано», не допуская утечки ссылок или создания другого нестабильного состояния с неопределённым поведением.
Самый безопасный способ отреагировать на выброшенную ошибку — завершить процесс. Разумеется, у обычного веб-сервера может быть много открытых соединений, и резко закрывать их из-за ошибки, вызванной кем-то другим, неразумно.
Лучше отправить ответ с ошибкой на запрос, вызвавший её, позволив остальным запросам завершиться в обычном режиме, и прекратить принимать новые запросы в этом рабочем процессе.
Таким образом, использование domain тесно связано с модулем cluster: основной процесс может запустить новый рабочий процесс, если в одном из них возникла ошибка. В программах Node.js, работающих на нескольких машинах, завершающий прокси-сервер или реестр служб может зафиксировать сбой и соответствующим образом отреагировать.
Например, так делать не следует:
// XXX WARNING! BAD IDEA!
const d = require('node:domain').create();
d.on('error', (er) => {
// The error won't crash the process, but what it does is worse!
// Though we've prevented abrupt process restarting, we are leaking
// a lot of resources if this ever happens.
// This is no better than process.on('uncaughtException')!
console.log(`error, but oh well ${er.message}`);
});
d.run(() => {
require('node:http').createServer((req, res) => {
handleRequest(req, res);
}).listen(PORT);
}); copy Используя контекст домена и повышая отказоустойчивость за счёт разделения программы на несколько рабочих процессов, мы можем реагировать более уместно и обрабатывать ошибки гораздо безопаснее.
// Much better!
const cluster = require('node:cluster');
const PORT = +process.env.PORT || 1337;
if (cluster.isPrimary) {
// A more realistic scenario would have more than 2 workers,
// and perhaps not put the primary and worker in the same file.
//
// It is also possible to get a bit fancier about logging, and
// implement whatever custom logic is needed to prevent DoS
// attacks and other bad behavior.
//
// See the options in the cluster documentation.
//
// The important thing is that the primary does very little,
// increasing our resilience to unexpected errors.
cluster.fork();
cluster.fork();
cluster.on('disconnect', (worker) => {
console.error('disconnect!');
cluster.fork();
});
} else {
// the worker
//
// This is where we put our bugs!
const domain = require('node:domain');
// See the cluster documentation for more details about using
// worker processes to serve requests. How it works, caveats, etc.
const server = require('node:http').createServer((req, res) => {
const d = domain.create();
d.on('error', (er) => {
console.error(`error ${er.stack}`);
// We're in dangerous territory!
// By definition, something unexpected occurred,
// which we probably didn't want.
// Anything can happen now! Be very careful!
try {
// Make sure we close down within 30 seconds
const killtimer = setTimeout(() => {
process.exit(1);
}, 30000);
// But don't keep the process open just for that!
killtimer.unref();
// Stop taking new requests.
server.close();
// Let the primary know we're dead. This will trigger a
// 'disconnect' in the cluster primary, and then it will fork
// a new worker.
cluster.worker.disconnect();
// Try to send an error to the request that triggered the problem
res.statusCode = 500;
res.setHeader('content-type', 'text/plain');
res.end('Oops, there was a problem!\n');
} catch (er2) {
// Oh well, not much we can do at this point.
console.error(`Error sending 500! ${er2.stack}`);
}
});
// Because req and res were created before this domain existed,
// we need to explicitly add them.
// See the explanation of implicit vs explicit binding below.
d.add(req);
d.add(res);
// Now run the handler function in the domain.
d.run(() => {
handleRequest(req, res);
});
});
server.listen(PORT);
}
// This part is not important. Just an example routing thing.
// Put fancy application logic here.
function handleRequest(req, res) {
switch (req.url) {
case '/error':
// We do some async stuff, and then...
setTimeout(() => {
// Whoops!
flerb.bark();
}, timeout);
break;
default:
res.end('ok');
}
} copy Дополнения к объектам 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-сервера может использоваться один домен, но для каждого запроса может понадобиться отдельный домен.
Это можно сделать с помощью явной привязки.
// Create a top-level domain for the server
const domain = require('node:domain');
const http = require('node:http');
const serverDomain = domain.create();
serverDomain.run(() => {
// Server is created in the scope of serverDomain
http.createServer((req, res) => {
// Req and res are also created in the scope of serverDomain
// however, we'd prefer to have a separate domain for each request.
// create it first thing, and add req and res to it.
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);
}); copy
domain.create()
- Возвращает: <Domain>
Класс: Domain
- Расширяет: <EventEmitter>
Класс Domain инкапсулирует функциональность перенаправления ошибок и неперехваченных исключений активному объекту Domain.
Чтобы обрабатывать перехваченные ошибки, подпишитесь на событие 'error'.
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) => {
// If this throws, it will also be passed to the domain.
return cb(er, data ? JSON.parse(data) : null);
}));
}
d.on('error', (er) => {
// An error occurred somewhere. If we throw it now, it will crash the program
// with the normal line number and stack message.
}); copy
domain.enter()
Метод enter() — это служебный метод, используемый методами run(), bind() и intercept() для установки активного домена. Он задаёт domain.active и process.domain в качестве домена и неявно помещает домен в стек доменов, управляемый модулем 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) => {
// Note, the first argument is never passed to the
// callback since it is assumed to be the 'Error' argument
// and thus intercepted by the domain.
// If this throws, it will also be passed to the domain
// so the error-handling logic can be moved to the 'error'
// event on the domain instead of being repeated throughout
// the program.
return cb(null, JSON.parse(data));
}));
}
d.on('error', (er) => {
// An error occurred somewhere. If we throw it now, it will crash the program
// with the normal line number and stack message.
}); copy
domain.remove(emitter)
-
emitter<EventEmitter> | <Timer> Эмиттер или таймер, удаляемый из домена
Противоположность методу domain.add(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);
});
}); copy В этом примере будет вызван обработчик 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
});
}); copy Функцию обратного вызова можно привязать к определённому домену с помощью метода 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
}));
}); copy Домены не влияют на механизмы обработки ошибок для обещаний. Иными словами, для необработанных отклонений Promise событие 'error' не генерируется.
© Joyent, Inc. and other Node contributors
Licensed under the MIT License.
Node.js is a trademark of Joyent, Inc. and is used with its permission.
We are not endorsed by or affiliated with Joyent.
https://nodejs.org/dist/latest-v22.x/docs/api/domain.html