Domain


Стабилность: 0 – Отказано

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

Домены предоставляют способ обработки множественных различных IO операций как единой группы. Если эмиттер события или функция обратного вызова регистрируют в домен событие 'error', или выдают ошибку, то будет уведомлен объект домена вместо потери контекста ошибки в обработчике process.on('uncaughtException') или принудительного завершения программы с кодом ошибки.

Внимание: не игнорируйте ошибки!

Обработчики ошибок в доменах не являются заменой закрытия процесса после возникновения ошибки.

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

Самый безопасный путь ответа на ошибку – закрытие процесса. Естественно, на обычном веб-сервере у вас может быть много открытых соединений, и иногда нет причин резко закрывать все процессы, так как ошибка может быть вызвана по другой причине.

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

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

К примеру, делать так – не самая лучшая идея:


  // XXX WARNING!  BAD IDEA!

var d = require('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
  // resources like crazy if this ever happens.
  // This is no better than process.on('uncaughtException')!
  console.log('error, but oh well', er.message);
});
d.run(() =< {
  require('http').createServer((req, res) =< {
    handleRequest(req, res);
  }).listen(PORT);
});

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


// Much better!

const cluster = require('cluster');
const PORT = +process.env.PORT || 1337;

if (cluster.isMaster) {
  // In real life, you'd probably use more than just 2 workers,
  // and perhaps not put the master and worker in the same file.
  //
  // You can also of course get a bit fancier about logging, and
  // implement whatever custom logic you need to prevent DoS
  // attacks and other bad behavior.
  //
  // See the options in the cluster documentation.
  //
  // The important thing is that the master 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('domain');

  // See the cluster documentation for more details about using
  // worker processes to serve requests.  How it works, caveats, etc.

  const server = require('http').createServer((req, res) => {
    var d = domain.create();
    d.on('error', (er) => {
      console.error('error', er.stack);

      // Note: 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
        var 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 master know we're dead.  This will trigger a
        // 'disconnect' in the cluster master, 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 isn't important.  Just an example routing thing.
// You'd put your fancy application logic here.
function handleRequest(req, res) {
  switch(req.url) {
    case '/error':
      // We do some async stuff, and then...
      setTimeout(() => {
        // Whoops!
        flerb.bark();
      });
      break;
    default:
      res.end('ok');
  }
}

Дополнения к объектам Error

В любое время, когда объект Error маршрутизируется через домен, добвляется несколько дополнительных полей:

  • error.domain – домен, который первым обработал ошибку
  • error.domainEmitter – эмитер события, который создал событие 'error' с объектом ошибки
  • error.domainBound – функция обратного вызова, которая была привязана к домену и передавала ошибку как первый аргумент
  • error.domainThrown – Boolean, показывающий, выпала ли ошибка, создана или передана в привязанную функцию обратного вызова

Неявные связи (Implicit Binding)

Если домен используется, то все новые объекты EventEmitter (включая объекты Stream, запросы, ответы и т.п.) могут быть неявно привязаны к активному домену во время своего создания.

Также функции обратного вызова, переданные в низкоуровневый цикл запросов событий (по типу fs.open или любого другого метода, основанного на callback) будут автоматически привязываться к активному домену. Если это не удается, то домен ловит ошибку.

Для того, чтобы предотвратить чрезмерное использование памяти, сами объекты домена не являются неявно добавленными "детьми" активного домена. В противном случае было бы слишком просто предотвратить помещение объектов запросов и ответов в так называемый "мусор".

Если вы хотите сохранить объекты домена как детей родительского домена, то нужно добавить их явно.

Неявно связанные маршруты выдают ошибки и события 'error' в событие 'error' домена, но не регистрируют EventEmitter домена, поэтому domain.dispose() не будет закрывать EventEmitter. Неявные связи отвечают только за ошибки и события 'error'.

Явные связи

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

Например, есть один используемый домен на HTTP сервере, но, возможно, нам нужен отдельный домен для каждого запроса.

Такое возможно при использовании явных связей.

К примеру:


// create a top-level domain for the server
const domain = require('domain');
const http = require('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.
    var 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 (er) {
        console.error('Error sending 500', er, req.url);
      }
    });
  }).listen(1337);
});

domain.create()

  • return >Домен<

Возвращает объект домена.

Class: Domain

Класс Domain инкапсулирует функционал ошибок маршрутизации и допускает исключения в объект активного домена.

Domain является дочерним классом EventEmitter. Для обработки пойманных им ошибок, следует обратиться к событию 'error'.

domain.run(fn[, arg][, ...])

    fn >Функция<

Запускает функцию в контексте домена, неяано связывая все эмитеры событий, таймеры и низкоуровневые запросы, которые были созданы в этом контексте. Опционально, функции могут передаваться аргументы.

Вот самый основной способ использования домена:


const domain = require('domain');
const fs = require('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').

domain.members

  • >Массив<

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

domain.add(emitter)

    emitter >EventEmmitter< | >Таймер< – эмитер или таймер, которые нужно добавить в домен.

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

Это также работает с таймерами, которые возвращаются из setInterval() и setTimeout(). Если их функции обратного вызова выдают ошибку, она будет "поймана" обработчиком 'error' домена.

Если Timer или EventEmitter уже были привязаны к домену, они удаляются из предыдущего и привязыываются к другому.

domain.remove(emitter)

    emitter >EventEmmitter< | >Таймер< – эмитер или таймер, которые нужно удалить из домена.

Противоположно domain.add(emitter). Удаляет обработчик домена из определенного эмитера.

domain.bind(callback)

  • callback >Функция< Функция обратного вызова
  • return:
  • >Функция< Связанная функция

Возвращаемая функция будет оборачивать указанную функцию обратного вызова. Когда вызывается возвращаемая функция, все ошибки направляются в событие домена '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.
});

domain.intercept(callback)

  • callback >Функция< Функция обратного вызова
  • return:
  • >Функция< Перехваченная функция

Этот метод практически идентичен 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.
});

domain.enter()

Метод enter является проводником, используемым методами run, bind и intercept для настройки активного домена. Он устанавливает domain.active и process.domain в домен и неявно помещает домен в стек доменов, который управляется модулем доменов (см. domain.exit()). Вызов enter разграничивает начало цепи асинхронных вызовов и I/O операции, связанные с доменом.

Вызов enter изменяет только активный домен, без изменения самого домена. enter и exit можно вызывать сколько угодно раз на одном домене.

Если домен, на котором вызывается enter был удален, то enter возвращается без настройки домена.

domain.exit()

Метод exit закрывает текущий домен, удаляя его из стека доменов. Каждый раз, когда выполнение переключается на другую цепь асинхронных вызовов, важно убедиться, что текущий домен закрыт. Вызов exit разграничивает либо окончание, либо приостановку цепи асинхронных вызовов и I/O операции, связанные с доменом.

Если есть множественные вложенные домены, связанные с текущим контекстом выполнения, exit все домены, вложенные в текущий.

Вызов exit изменяет только активный домен, без изменения самого домена. enter и exit можно вызывать сколько угодно раз на одном домене.

Если домен, на котором вызывается exit был удален, то exit возвращается без настройки домена.

domain.dispose()

Стабильность: 0 – Отказано.

Пожалуйста,восстанавливайтесь из завершившихся с ошибкой событий IO явно через обработчик события error, настроенный на домене.

После вызова dispose домен не будет больше использоваться функциями обратного вызова, связанными с доменом посредством run, bind или intercept и генерируется событие 'dispose'.