Как обрабатывать Fatal Error в PHP

    В одном из наших проектов (социальная генеалогическая сеть), о котором я писал в данном топике, мы используем очередь отложенных событий, реализованную на мемкеше. Ее архитектура такова: приложение записывает в эту очередь различные события и данные, относящиеся к ним (тип события, входящие параметры, и функция обработчик этого события). После чего менеджер(-ы) очереди разбирают эту очередь и выполняют отложенные события. В частности такая очередь используется для сбора статистики, но также и для других более критичных к выполнению задач.
    Поэтому очень важно обеспечить high availability для менеджера(-ов) очереди.

    Но т.к. ф-ия обработчик очереди к нам приходит из вне, то за качество этого обработчика события мы не отвечаем, т.е. если обработчик вдруг выбросит ошибку, то нам ее нужно обработать и продолжить работу менеджера очереди. Но иногда случается, что обработчики выбрасывают фатальные ошибки (Fatal Error), и это может стать проблемой…



    Для треккинга процессов (демонов), очень удобно пользоваться наблюдателями за процессами, такими как monit, мы используем monit для мониторинга сисстемных демонов. Кстати, на хабре недавно была статья о моните.
    Но речь не о нем :-)

    Я попросил одного из разработчиков моей команды сделать нормальный обработчик фатальной ошибки в коде менеджера очереди, а именно форк нового инстанса обработчика и логирование ошибки по типу события. На это я получил ответ, что в php фатальные ошибки обрабатывать в принципе невозможно и позорно об этом не знать и что: «компьютерные науки на текущем этапе своего эволюционного развития еще не располагают алгоритмами способными решить поставленную задачу опираясь на возможности php коректно...»

    После этого я написал такой код, который обрабатывает фатальные, а также все другие ошибки в php приложении. Если кому-то еще он поможет, то я буду только рад.

    <?php

    ini_set("display_errors", "on");
    error_reporting(E_ALL);
    ini_set('html_errors', 'on');

    function fatal_error_handler($buffer) {
      if (preg_match("|(Fatal error</b>:)(.+)(<br)|", $buffer, $regs) ) {
       //Форкаем новый инстанс демона и готовимся к заавершению выполнения текущего скрипта
       file_put_contents("php://stderr", "before fork (pid: " . getmypid() . ")\n");
       system("php tester.php " . getmypid() . " &" );
       return "ERROR CAUGHT, check log file" ;
      }
      return $buffer;
    }

    function handle_error ($errno, $errstr, $errfile, $errline)
    {
     if($errno & E_ALL){
    // Логирование ошибки как в ф-ии выше
    //switch в котором, собственно обрабатываем ошибку
     switch ($errno) {
     case E_USER_ERROR:
     case E_USER_WARNING:
     case E_USER_NOTICE:
     default:
     //do something
     break;
     }
      ob_end_clean();
      echo "CAUGHT OTHER THAN FATAL ERRORS!!! " . $errstr;
      exit;
     }
    }

    //code between ob_start and ob_end_flush is included by MQ Handler, so we know nothing about it, and this code could fire a Fatal Error
    if(isset($_SERVER["argv"][1])){
      file_put_contents("php://stderr", "kill {$_SERVER['argv'][1]}:   ".var_export(posix_kill($_SERVER['argv'][1], 15), true)."\n");
    }
    ob_start("fatal_error_handler");
    set_error_handler("handle_error");

    while(true) {
    //Just a Warning
    //$a = 9/0;
    sleep(10);
    file_put_contents("php://stderr", "live\n");
    //Fatal error - вызов необъявленной ф-ии
    if(rand(1,10) % 2 == 1) {
     ololo(123);
    }
    }

    /*
    Код без ошибок
    */
    $a = rand(1,10);
    echo $a."<br/>";

    ob_end_flush();

    echo "Program still executing....";

    ?>


    * This source code was highlighted with Source Code Highlighter.

    Небольшое объяснение по текущему коду.
    Fatal Error — мы ловим через буферизацию вывода в ф-ии fatal_error_handler
    Остальные ошибки (все кроме фатальных) обрабатываются ф-ией handle_error
    Если ошибок нет — код выполняется нормально :-)

    Да, это также не является единственным средством high availability и отказоустойчивости.
    Мы пытаемся запустить демон каждую минуту по крону, а код демона начинается ф-ией

    <?php

    if (!checkSingleProcess()) {
      exit;
    }

    function checkSingleProcess() {
      $res = exec('ps aux | grep mq_manager.php | grep -v grep | grep -v '.getmypid(), $output, $return);
      return $output == array();
    }


    * This source code was highlighted with Source Code Highlighter.


    т.е. если демон запущен, то мы прекращаем выполнение.

    Открыт ко всем мнения и по возможности буду отвечать на все комментарии.

    UPD: обратите внимание на ini_set('html_errors', 'on'); я потратил с пол часа времени не понимая почему обработчик не работает из-под CLI. Дело было как раз в HTML-ных ошибках. Т.к. из-под CLI они давались без HTML тегов, и условие preg_match("|(Fatal error:)(.+)(<br)|", $buffer, $regs) просто не выполнялось. // Вот так вот.

    UUPD: Немного обновил код, дело в том, что форкая новый процесс через ф-ию system нужно позаботиться о том, чтобы убить процесс который форкал текущий, т.к. функция обработчик будет ждать результата выполнении ф-ии system, а он как извесно не вернется, ведь мы же создаем демона, в связи с этим вы получите кучу процессов, висящих в памяти, которые в конце концов забьют ее полностью.
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 33

      +6
      Может, пригодиться: Про перехват и обработку фатальных ошибок (Fatal Error) в PHP ©dkLab

      Насколько универсален данный способ проверки того, что демон уже запущен? О винде, конечно, не говорю. Нельзя ли блокировками файлов воспользоваться?
        +1
        Если вы про monit, то у него есть куча разных тестов: это порты (причём поддерживается тонна протоколов), использование ресурсов, проверка пидов, проверка файлов и пр. Подробнее смотрите в мануале.

        На практике мне обычно хватало раз в N минут проверить, открыт ли сетевой порт YYYY. Если нет — перезапуск демона на этом порту. Если Z попыток обломалось — емыло мне. Такой схемы хватает в большинстве случаев
          0
          Мы пользуемся такой же схемой как и Вы :-) относительно проверки демона по порту.
          0
          Можно через блокировки файлов. Это будет универсальней и кросс-платформенней, просто открывать файл c эксклюзивным локом.

          <?php

          $fp = fopen("/tmp/lock.txt", «w»);

          if (flock($fp, LOCK_EX)) { // do an exclusive lock
          //some logic here

          } else {
          die;
          }

          fclose($fp);

          +3
          Использую такой код, еще не встречал случаи когда ошибка не обрабатывалась:

          register_shutdown_function(«dbg_last_error»);

          function dbg_last_error(){
          $e=error_get_last();
          if(($e[«type»] & E_COMPILE_ERROR) || ($e[«type»] & E_ERROR) ||
          ($e[«type»] & E_CORE_ERROR) || ($e[«type»] & E_RECOVERABLE_ERROR))
          trace("[".$e[«type»]."] ".$e[«file»].":".$e[«line»]."\n".
          $e[«message»]."\n", array(), «error»);}

            0
            Такое, как мне кажется ловит только runtime фаталы. А если будет фатал на этапе парсинга кода, то это не спасет
              0
              Пишите красивым кодом, пожалейте остальных разработчиков:
              switch ($e['type'])
              {
                case E_COMPILE_ERROR:
                case E_ERROR:
                case E_CORE_ERROR:
                case E_RECOVERABLE_ERROR:
                  trace(.....);
              }
                –1
                А теперь посмотрите в чем разница между вашим кодом и тем что выше.
                  0
                  Лучшей читаемостью?
                    0
                    А ничего что там операторы не &&, а &?
                      –1
                      Смотрим маны. Все константы ошибок — степени двойки. Поэтому битовое умножение тут вообще не нужно.
              0
              первым же делом проверил приведенный пример через браузер.
              Строчки «Program still executing....» я так и не увидел, зато "(! ) Fatal error: Call to undefined function ololo() in /home/www/test/fatal.php on line 59" светилась во весь экран
                0
                нужно смотреть какие у вас настройки php

                в частности html_errors, т.к. это рабочий вариант.
                  0
                  html_errors On
                  output_buffering 4096

                  какие еще нужны настроки?
                    0
                    могу выслать вам php.ini с моей локальной машины, под котороый работает.

                    пхп, кстати, 5.2.6.
                      0
                      у меня 5.2.10

                      Я думаю, что стоило описать полность при каких условиях работает сей код.
                      Указать каие именно настроки нужно включить/выключить в php.ini, чтобы получить желаемый результат
                        +1
                        Как оказалось, нужно было отключить xdebug. Он меняет html фатала =)
                          0
                          Да, главного мы и не заметили :-)
                  0
                  мы все фатальные ошибки заворачиваем через эксепшены.
                  про статью на ©dkLab уже упоминали.

                  пару слов о мониторинге:
                  у нас свой собственный мониторинг (к сожалению про монит я узнал поздно)
                  есть класс скриптов, которые должны жить «вечно»

                  алгоритм приблизительно следующий:
                  — скрит запускается по крону каждую минуту.
                  — скрипт проверяет кол-во запущенных копий, кстати метод тот же через ps ax -u | grep $scriptName
                  — если копий больше чем нужно, то завершаемся

                  — далее идет цикл на длительный интервал напрмер час
                  в цикле выполняем метод run() & sleep(1)
                  — далее выполняем ps ax -u | grep getmypid() и вычисляем занимаемую память
                  — если в конце цикла кол-во памяти превысит допустимый — цикл заканчиваем (так избегаем утечки памяти)

                  — перезапускаем процесс используя php-forker
                  это позволяет нам запустить процесс не ровно в 00сек а например в 05сек (и не ждем 55 сек)

                  крон — для подтсраховки, хотя существует собственный мониторинг работы процессов.
                    0
                    Не понимаю, почему все пытаются ссылаться на статью с ©dkLab, там такой же метод как у меня описан. Логика таже.

                    Только у меня обработка более сложная, как раз создается вечный процесс, за счет того, что форкается этот же процесс в обработчике ф-ии ob_start.

                    system(«php tester.php ». getmypid(). " &" );

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

                    if(isset($_SERVER[«argv»][1])){
                    file_put_contents(«php://stderr», «kill {$_SERVER['argv'][1]}: ». var_export(posix_kill($_SERVER['argv'][1], 15), true)."\n");
                    }
                      0
                      Ссылаются потому что там, в частности, предусмотрена ошибка «нехватка памяти». Во-вторых, как просто дополнение в вашей статье.

                      За работу с процессами, безусловно, спасибо. Только вот это не относится к тематике статьи:)
                        0
                        :-)

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

                        Поэтому и был описан механизм как это делалось. Ну а статью назвал так, потому что просил программиста из команды ловить фатал ероры, а он мне утверждал, что этого в php сделать невозможно в принципе. Поэтому разозлился и написал код, а потом и жту статью. :-)

                        Ну, кстати, благодаря статье бклаб узнал, что register_shutdown_function сейчас вызывается когда фаерится фатал ерор, раньше такого не было. :-)
                          0
                          а в целом мне статья понравилась
                            0
                            в ней я узнал про monit
                            наверно тогда бы «Мы пошли другим путем»
                              0
                              Спасибо
                          0
                          system(«php tester.php ». getmypid(). " &" );
                          форкая таким образом ты рождаешь зомби.
                          необходимо обрабатывать SIGCHL

                          именно чтоб не было зомби и был придуман php-forker :)
                            0
                              0
                              нет, зомби не рождается. я же передаю ПИД текущего процесса (в котором произошла фатальная ошибка) во вновь созданный, и тот киляет этот старый процесс (который мог бы стать зомби)

                              iminyaylo@md-1:~$ ps -aux | grep php | grep index.php | grep -v grep
                              40449 26548 0.1 0.3 54628 7480 ttyp1 S 12:21 0:00 php index.php 26494
                              iminyaylo@md-1:~$ ps -aux | grep php | grep index.php | grep -v grep
                              40449 26548 0.1 0.3 54628 7480 ttyp1 S 12:21 0:00 php index.php 26494
                              iminyaylo@md-1:~$ ps -aux | grep php | grep index.php | grep -v grep
                              40449 26548 0.1 0.3 54628 7480 ttyp1 S 12:21 0:00 php index.php 26494
                              iminyaylo@md-1:~$ ps -aux | grep php | grep index.php | grep -v grep
                              40449 26548 0.0 0.3 54628 7480 ttyp1 S 12:21 0:00 php index.php 26494
                              iminyaylo@md-1:~$ ps -aux | grep php | grep index.php | grep -v grep
                              40449 26548 0.0 0.3 54628 7480 ttyp1 S 12:21 0:00 php index.php 26494
                              iminyaylo@md-1:~$ ps -aux | grep php | grep index.php | grep -v grep
                              40449 27468 0.5 0.3 54632 7472 ttyp1 S 12:22 0:00 php index.php 27460
                              iminyaylo@md-1:~$ ps -aux | grep php | grep index.php | grep -v grep
                              40449 27468 0.2 0.3 54632 7472 ttyp1 S 12:22 0:00 php index.php 27460
                              iminyaylo@md-1:~$ ps -aux | grep php | grep index.php | grep -v grep
                              40449 27468 0.2 0.3 54632 7472 ttyp1 S 12:22 0:00 php index.php 27460
                              iminyaylo@md-1:~$ ps -aux | grep php | grep index.php | grep -v grep
                              40449 27941 1.0 0.3 54628 7472 ttyp1 S 12:23 0:00 php index.php 27468
                              iminyaylo@md-1:~$ ps -aux | grep php | grep index.php | grep -v grep
                              40449 27941 0.2 0.3 54628 7472 ttyp1 S 12:23 0:00 php index.php 27468
                              iminyaylo@md-1:~$ ps -aux | grep php | grep index.php | grep -v grep
                              40449 27941 0.0 0.3 54628 7484 ttyp1 S 12:23 0:00 php index.php 27468
                              iminyaylo@md-1:~$ ps -aux | grep php | grep index.php | grep -v grep
                              40449 29381 0.0 0.3 54628 7468 ttyp1 S 12:24 0:00 php index.php 29375

                              Видишь, кол-во процессов не растет, а их PID-ы меняются. Здесь нет зомби-процессов.
                                0
                                если килл, то зомби не рождается.
                                я на тот случай, если мы не обрабатываем сигналы, то рождаются зомби.
                                0
                                Ну а это мой лог куда это пишется

                                kill 11162: true

                                live

                                before fork (pid: 11170)

                                kill 11170: true

                                live

                                live

                                before fork (pid: 11202)

                                kill 11202: true

                                live

                                before fork (pid: 11174)

                                kill 11174: true

                                live

                                before fork (pid: 11580)

                                kill 11580: true

                                live

                                before fork (pid: 11584)

                                kill 11584: true
                                  0
                                  Ну и я не создаю фактически дочерний процесс, дочерний процесс создается форком (pcntl_fork), здесь же я запускаю независимый процес, с другим адресным пространством посредством системного вызова. Так что тут не будет зомби.
                                  0
                                  PS мне сегодня тоже минусов за статью напихали
                                  видно день такой…
                                    0
                                    да, и погода никчерту…

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