Pull to refresh

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

Reading time 4 min
Views 19K
В одном из наших проектов (социальная генеалогическая сеть), о котором я писал в данном топике, мы используем очередь отложенных событий, реализованную на мемкеше. Ее архитектура такова: приложение записывает в эту очередь различные события и данные, относящиеся к ним (тип события, входящие параметры, и функция обработчик этого события). После чего менеджер(-ы) очереди разбирают эту очередь и выполняют отложенные события. В частности такая очередь используется для сбора статистики, но также и для других более критичных к выполнению задач.
Поэтому очень важно обеспечить 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, а он как извесно не вернется, ведь мы же создаем демона, в связи с этим вы получите кучу процессов, висящих в памяти, которые в конце концов забьют ее полностью.
Tags:
Hubs:
+1
Comments 33
Comments Comments 33

Articles