18Nov

23_3
Итак, из чего состоит app.js?

Он состоит из того, что мы с самого начала делаем  домен, и запускаем в нем все наше приложение. Здесь подключаются все модули, создается сервер и т.д. Почему  мы подключаем модули здесь? Потому что во время подключения модулей могут подключаться какие-то другие модули, а те могут подключать – третьи, и т.д.

Многие из этих модулей я вообще не знаю. Они используются в каких-то библиотеках. При подключении модулей и их инициализации могут создавать какие-то объекты – EventEmitters. Я хочу быть уверенным, что эти EventEmitter подключаться именно к домену server.domain. Но основная задача этого кода – это запустить http.server:

server = http.createServer (function(req, res) {

       var reqDomain = domain.create();
        reqDomain.add(req);
        reqDomain.add(res);

        reqDomain.on('error', function(err) {
          res.statusCode = 500;
          res.end("Sorry, " + err);
           // ...
          serverDomain.emit('error', err);
        });

        reqDomain.run(function() {
           handler(req, res);
         });
       });

     server.listen(3000);

В обработчике запроса мы тоже создаем новый домен:

var reqDomain = domain.create();
        reqDomain.add(req);
        reqDomain.add(res);

Этот домен будет заниматься обработкой ошибок, которые появились во время данного конкретного запроса. Его преимущество – это то, что он знает про запрос. У него есть обработчик on.error, который, если какая-то ошибка, может ответить: 500, извините, ошибка. И не только ответить, он может  записать вlog именно то, что такой запрос пришел и такая-то ошибка. Таким образом, вы, как разработчик, будете видеть, что у Вас был такой запрос, и мне будет гораздо проще эту ошибку воспроизвести и исправить.

reqDomain.on('error', function(err) {
  res.statusCode = 500;
  res.end("Sorry, " + err);
  console.error('Error for req = ', req);

Мы привязываем к домену объекты req и res, поскольку они, во-первых, являются EventEmitters, во-вторых, они были созданы во внешнем коде Node.js. Поэтому мы вынуждены привязать их явно.

Допустим, выполняется handler. Произошла ошибка. Мы ответили человеку: “до свидания, извини, тут проблема” . Записали все, что надо в log. У нас один сервер поддерживает множество клиентов. Если те клиенты общались как-то между собой, то в произвольном месте выскочившая ошибка, которая может быть достаточно серьезной  – создаст кучу проблем. Обратите внимание, домены какие попало ошибки здесь у нас не ловят,

Обычно ошибки мы перехватываем в callback, try catch, а это именно какая-то программная ошибка, скорее всего. Соответственно, ее появление означает, что сервер сейчас находится в непонятном состоянии, и оставлять его в нем достаточно опасно. Поэтому хорошо бы этот сервер прибить и поднять новый чистый процесс Node.js.  Есть, конечно, ситуации, в которых можно оставить сервер работать дальше, но давайте делать безопаснее. Для этого  вызываем

     serverDomain.emit('error', err);

таким образом  передаем работу с ошибкой on.error. Обращаем ваше внимание еще на такой нюанс, домены, конечно, похожи на try catch, но это все-таки не они. Если в этом on.error сделать throw, то управление в serverDomain.on не попадет.  Вместо этого такой throw отовсюду выпадет и повалит процесс.

 reqDomain.on('error', function(err) {
          res.statusCode = 500;
          res.end("Sorry, " + err);
          console.error('Error for req = ', req);
            
          throw err;
          serverDomain.emit('error', err);
        });

Поэтому вызываем  внешний домен именно через Emit . Дальше обработчик на внешнем домене будет заниматься тем, что завершит обработку ошибки и, по возможности, мягко загасит текущий процесс сервера. На код ниже внимание можете не обращать:

 if (server) server.close();

   setTimeout(function () {
       process.exit(1);
   }, 1000).unref();

Он схематичный.  Нам здесь важны именно домены. Мы получили рабочую схему для вложенной обработки ошибок. По возможности мы стараемся обработать ошибки в контексте запроса:

  reqDomain.on('error', function(err) {
          res.statusCode = 500;
          res.end("Sorry, " + err);
          console.error('Error for req = ', req);

          serverDomain.emit('error', err);
        });

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

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

Возможно ли такое, что во время обработки запроса ниже, ошибка, которая произойдет, выскочит за пределы reqDomainв app.js?

if (req.url == '/') {  
  
    fs.readFile('no-such-file', function(err, content) {  
  
      if (err) throw err; // JSON.parse("invalid!")  
  
      res.end(content);  
    });  
  
  } else {  
    res.statusCode = 404;  
    res.end("Not Found");  
  }  
  
}; 

Ответ: да, возможно. Чтобы это продемонстрировать,  немного модифицируем пример, этот пример справедлив для старых версий Node.js, в сегодняшних версиях (а это 7.1) таких проблем нет, также как и самого модуля Domain, т.к. он устарел (пример может понадобиться для понимания Legacy Code):

var domain = require('domain');
var serverDomain = domain.create();

var server;

serverDomain.on('error', function(err) {
    console.error("Server error", err);
    if (server) server.close();

    setTimeout(function () {
        process.exit(1);
    }, 1000).unref();
});

serverDomain.run(function() {
    var http = require('http');
    var handler = require('./handler');


    server = http.createServer (function(req, res) {

        var reqDomain = domain.create();
        reqDomain.add(req);
        reqDomain.add(res);

        reqDomain.on('error', function(err) {
            res.statusCode = 500;
            res.end("Sorry, " + err);

            console.error('Error for req = ', req);

            serverDomain.emit('error', err);
        });

        reqDomain.run(function() {
            handler(req, res);
        });
    });

    server.listen(3000);
});

В handler.js у нас есть база данных, это может быть MongoDB, MySQL, Redis (необходимо иметь установленный Redis у себя на компьютере и клиент для него, установите последний с помощью:

npm install redis 

сама база устанавливается отдельно).

у них у всех одна и та же проблема с  при работе с доменами.

//var database = require ('mongodb');
//var mysql = require ('mysql');
var redis = require('redis').createClient();

module.exports = function handler(req, res) {
    if (req.url == '/') {

        redis.get('data', function(err, data) {

            throw new Error("redis callback"); //

        });

    } else {
        res.statusCode = 404;
        res.end("Not Found");
    }

};

Для того, чтобы ответить посетителю, нужно сделать запрос к базе. Дальше мы должны что-то сделать с данными, возможно, обработать или могут понадобиться другие запросы.  Так или иначе в процессе произошла какая-то ошибка. Куда же она попадет? Запускаем и переходим на страницу:

127.0.0.1:3000

Видим, что соединение было разорвано. Server error – это то, что выводит домен ниже:

serverDomain.on('error', function(err) {
    console.error("Server error", err);
    if (server) server.close();

    setTimeout(function () {
        process.exit(1);
    }, 1000).unref();
});

То есть, ошибка throw new Error ("redis callback") вовсе не была перехвачена обработчиком:

        reqDomain.on('error', function(err) {
                res.statusCode = 500;
                res.end("Sorry, " + err);

                console.error('Error for req = ', req);

она пошла сразу в верхнюю часть. Почему? Ведь ошибка возникла в процессе работы обработчика function handler(req, res) , как так получилось, что она вышла из requestDomain ?

Для того, чтобы ответить на этот вопрос необходимо вспомнить, что существуют всего 2 способа, как домен переходит в callback асинхронных вызовов. Первый – это внутренний метод Node.js: встроенный setTimeout, fs.readFile и т.д. Они интегрированы с доменами и передают их дальше в callback. Второй – это всякие  EventEmitter. Если бы EventEmitter  был создан в контексте домена, то он к нему привязывается.

Метод redis.get, который мы здесь вызываем, использует объект redis, который был создан вне контекста запроса, а при подключении модуля. В Node.js это стандартная практика, одно соединение используется для множества запросов. Соответственно, если мы хотим получить какие-то данные, то мы даем объекту redis то, что мы хотим получить (data), и какой-то callback. Дальше этот объект что-то делает, что-то отсылает в базу данных, и у него есть внутренние EventEmitter, которые были созданы вместе с ним, и которые будут генерировать события, когда база что-то ответит. И эти внутренние EventEmitter при генерации события передадут домен. Какой? Конечно же, тот, с которым они были созданы, то внешний домен из app.js :

serverDomain.run(function() {.....});

Получится, что этот callback будет вызван в домене serverDomain и не сохранит контекста запроса. Дальше, если этот callback вызвал какие-то другие функции, то, соответственно, мы получим тот же домен, и requestDomain для нас будет потерян.

Что делать? Есть 2 варианта. Первый – это поставить специальный вызов process.domain.bind.

 redis.get('data', process.domain.bind(function(err, data) {

            throw new Error("redis callback"); 

        }));

Этот вызов возвращает обертку вокруг функции, которая привязывает его к текущему активному домену, то есть, домену запроса. Если мы сейчас запустим наш сервер, то все будет хорошо. (В текущей версии Node.js – 7.1 все сработает и без этой записи, но для старых версий можно встретить и такую запись).

Все, что надо, сработало. Вы должны увидеть в браузере:
Sorry, Error: redis callback;

Код урока вы сможете найти здесь|

23_end

Материалы для статьи взяты из следующего скринкаста

We are looking forward to meeting you on our website soshace.com

Уроки React. Урок 4. Домашнее Задание.

Поговорим о нашем домашнем задании. Стоит отметить что при разработке decorators/mixins вся логика в большинстве случаев работает прекрасно. Она была реализована нами в классе, для выполнения домашнего задания оставалось вынести ее в decorator и соответствующий mixin. Так будет выглядеть наш decorator:

Leave a Reply