Spec-Zone.ru › Node.js

Домен

Исходный код: lib/domain.jsУстарело в: v1.4.2
История
Версия Изменения
v8.8.0

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

v8.0.0

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

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

Этот модуль ожидает признания устаревшим. После окончательной доработки 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);
});
jscopy

Используя контекст домена и устойчивость, которую обеспечивает разделение программы на несколько рабочих процессов, мы можем реагировать более подходящим образом и обрабатывать ошибки гораздо безопаснее.

// 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');
  }
}
jscopy

Дополнения к объектам 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);
});
jscopy

domain.create()

  • Возвращает: <Domain>

Класс: Domain

  • Расширяет: <EventEmitter>

Класс Domain инкапсулирует функциональность перенаправления ошибок и необработанных исключений в активный объект Domain.

Чтобы обрабатывать перехваченные ошибки, подпишитесь на его событие 'error'.

domain.members

  • Тип: <Array>

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

domain.add(emitter)

История
Версия Изменения
v9.3.0

Больше не принимает объекты таймеров.

  • emitter Эмиттер <EventEmitter>, который нужно добавить в домен

Явно добавляет эмиттер в домен. Если любой обработчик событий, вызванный эмиттером, выбрасывает ошибку или эмиттер генерирует событие 'error', оно будет перенаправлено в событие 'error' домена, как и при неявной привязке.

Если 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.
});
jscopy

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.
});
jscopy

domain.remove(emitter)

  • emitter Эмиттер <EventEmitter>, который нужно удалить из домена

Противоположность методу 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);
  });
});
jscopy

В этом примере будет вызван обработчик 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
  });
});
jscopy

Обратный вызов можно привязать к определённому домену с помощью метода 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
  }));
});
jscopy

Домены не вмешиваются в механизмы обработки ошибок промисов. Иными словами, событие 'error' не будет сгенерировано для необработанных отклонений Promise.

© 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/api/domain.html

Spec-Zone.ru

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