Spec-Zone.ru › Node.js 22 LTS

Домен

История
Версия Изменения
v8.8.0

У любых Promises, созданных в контекстах VM, больше нет свойства .domain. Однако их обработчики по-прежнему выполняются в соответствующем домене, а Promises, созданные в основном контексте, по-прежнему имеют свойство .domain.

v8.0.0

Обработчики для Promises теперь вызываются в домене, в котором было создано первое обещание цепочки.

v1.4.2

Устарело начиная с: v1.4.2

Стабильность: 0 - Устарело

Исходный код: 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.members

  • Тип: <Array>

Массив таймеров и эмиттеров событий, явно добавленных в домен.

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

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API