Пуленепробиваемый Node.js

    Проблемы индейцев
    Одним из остававшихся до недавнего времени недостатком Node.js, который отпугивал бекенд-разработчиков от использования Node.js, был тот факт, что непойманное исключение в потоке рушило воркера, и все клиенты, которые ждали от него ответа, не получали ничего.

    tl;dr Eсть новое API для асинхронного try\catch. Из статьи можно скопипастить пример и поиграться.

    Рассмотрим ситуацию:
    У нас есть запрос к веб серверу ноды, в процессе обработки данных, полученных из базе данных происходит какая-то проблема (неверные параметры, которые нельзя валидировать, отвалилась стойка с базой данных, или просто невнимательность) — возникает ошибка. До начала нашего запроса было 5 запросов от других клиентов и у них начала грузиться страница. После запроса поступало еще 10 запросов от других пользователей. И вот она ошибка, в недрах коллбеков одного из параллельно выполняющихся запросов. Отваливаются все 16 запросов. Страдают невинные.

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

    Не канонично.

    Да приидет Domain
    Итак, в версии 0.8 появилась серебряная пуля, мастер отлова ошибок, непревзойденный Domain. На данный момент API находиться в статусе unstable, но уже им можно пользоваться.

    Простой пример:
    var domain = require('domain'),
        server = require('http').createServer(function(req, res) {
            
            // Создаем домен
            var d = domain.create();
    
            // Вешаем обработчик ошибки, который вернет 500й статус и текст проблемы
            d.on('error', function(err) {
                console.error('error', err.stack);
                res.statusCode = 500;
                res.setHeader('content-type', 'text/plain');
                res.end('Houston, we have a problem!\n');
            });
    
            // Добавляем наши переменные, которые тоже могут сгенерировать ошибки самостоятельно
            d.add(req);
            d.add(res);
            
            // Запускаем потенциально опасный код внутри домена
            d.run(function () {
              switch(req.url) {
              case '/error':
                  setTimeout(function() {
                       // Вызываем ошибку - несуществующий метод необъявленной переменной
                       uncaught.error();
                  });
                  break;
              default:
                  res.end('ok');
              }
            });
       });
    server.listen(8080);
    
    


    Запустив этот простой скрипт, при переходе на корневой адрес у нас все ОК, а вот если перейти по адресу /error, вы увидите в консоли лог ошибки, а в браузере — корректное сообщение, что что-то пошло не так, и статус 500.

    P.S. Простите за орфографию и пунктуцию, буду рад указанию на ошибки.
    Share post

    Comments 21

      +9
      Для express`а (connect) есть модуль connect-domain.
      var connectDomain = require('connect-domain');
       ...
      var app = express();
       ...
      // Подключаем middleware
      app.use(connectDomain());  
      // Обработка исключений.
      app.use(function(err, req, res, next) {
            logger.error(err);
            res.send(500, 'Houston, we have a problem!\n');
        });
      
        +7
        В ноде всегда было это:
         process.on('uncaughtException', function (err) { ... }); 
        

        Что не давала падать приложению из за ошибки. Проблема в том что этот обработчик не дает возможности обработать ошибку там где она случилась, поэтому и были изобретены domain'ы
          +9
          Это плохая практика. Мы теряем контекст при такой обработке. Можно использовать только в очень небольшом количестве сценариев.
            +8
            Все правильно, но это было единственное средство не дать приложению упасть полностью.

            Домены дали возможность обрабатывать исключения более управляемо, разделяя ошибки по уровням, то есть не просто не упасть на одном запросе, а еще и отследить и обработать эту ошибку.
          +13
          Надеюсь, я выучу Scala быстрее, чем весь мир начнет писать на node.js
            –1
            Будете компилировать программы на scala дольше, чем люди пишут код на js.
              +2
              долго запрягает, да быстро едет.
            +8
            Классная штука домены, но плюс к ним для отказоустойчивости не помешает:

            root@server:/usr/local/server# cat ./server.js
            
            #!/usr/bin/node
            process.on('uncaughtException', function (err)
              console.log(err);
            });
            
            ...
            
            root@server:/usr/local/server# chmod +x /usr/local/server/server.js
            
            # например под upstart:
            
            root@server:/usr/local/server# cat /etc/init/server.conf
            
            description     "My super server"
            
            start on runlevel [2345]
            stop on runlevel [016]
            
            respawn
            
            exec NODE_ENV="production" /usr/local/server/server.js 2>&1 >> /var/log/server/server.log
            
            # for production:
            root@server:/usr/local/server# service server start 
            root@server:/usr/local/server# tail -f /var/log/server/server.log
            
            # for development:
            root@server:/usr/local/server# ./server.js
            
              +1
              Спасибо, очень лаконичный конфиг, удобно.
              +5
              Обрабатывать ошибки как в вашем примере не рекомендуется самими разработчиками Node. О чем явно написано в той же документации по ссылке.

              В двух словах: после перехвата исключения, процесс находится в непредсказуемом состоянии, поэтому рекомендуется завершить обработку текущих запросов на этом воркере, завершить процесс и запустить новый воркер. Пример там есть.
                +2
                #5114 — тут есть эпичное обсуждение проблем на эту тему. Топикстартер — автор модуля trycatch, о котором я писал на хабре.

                Также вот тут был интересный эксперимент на тему того, как заставить работать вложенные домены в версии 0.8. Печальное зрелище :)
                  +1
                  Кстати, в каких типичных случаях приложение может кинуть исключение в асинхронном вызове не по вине программиста? На сколько я знаю, все популярные библиотеки пробрасывают ошибки первым аргументом в callback, разве что JSON.parse() в try/catch приходится оборачивать. Есть еще какие-нибудь юзкейсы?
                    0
                    Кидают исключения, например синхронные функции в модулях fs, path, и т.д. Кто даст гарантию, что они не вызываются из асинхронного кода?

                    А чуть менее популярные библиотеки? К сожалению, мы не можем поручиться за всех, и это становится принципиальной проблемой — как обезопасить себя от библиотеки, которая, вероятно, ведет себя не так?
                –1
                Это похоже на обертку try{}catch{} только с красивым синтаксисом.
                  0
                  Тоже сначала так показалось. Но фишка в том, что оно из callback-ов ловит исключения. Я правда по исходнику так и не понял каким образом они ловятся. А вообще, почему в таком коде pastebin.com/cix7GvjF, 'free exception' попадает то в один domain, то в другой, то вообще в uncachedException вываливается? Это же полный атас. Вы уж простите, но проще Erlang выучить.
                    0
                    Как заметил выше товарищ Nailgun: «после перехвата исключения, процесс находится в непредсказуемом состоянии». Соответственно, лучше не использовать такой юзкейс.
                    • UFO just landed and posted this here
                    0
                    Есть хороший мини модуль node-domain-middleware для express, написанный моим коллегой. Пример кода с исользованием его и okay

                    var ok = require('okay');
                    app.use(require('express-domain-middleware'));
                    app.use(app.router);
                    app.use(function errorHandler(err, req, res, next) {
                      console.log('error on request %d %s %s: %j', process.domain.id, req.method, req.url, err);
                      res.send(500, "Something bad happened. :(");
                    });
                    app.get('/error', function(req, res, next) {
                      db.query('SELECT happiness()', ok(next, function(rows) {
                        fs.readFile('asldkfjasdf', ok(next, function(contents) {
                          process.nextTick(ok(next, function() {
                            throw new Error("The individual request will be passed to the express error handler, and your application will keep running.");
                          }));
                        }));
                      }));
                    });
                    


                      +2
                      я хотел промолчать… честно.
                      0
                      Хм… знатоки ES6 расскажите, я правильно понимаю, что с появлением генераторов можно будет писать так:

                      try {
                        var res = yield asyncCall();
                      } catch (e) {
                        logger.log(e);
                      }
                      


                      и это решит все проблемы с обработкой асинхронных ошибок?

                    Only users with full accounts can post comments. Log in, please.