Доброго времени суток! Сегодня в сто первый раз сталкнулся с проблемой, что какая- то задача, вызываемая ночью по крону не работает, а я об этом узнаю спустя несколько дней. Эти кроновские скрипты отличаются от обычных. Я с ними намучился. Они, во- первых, не вызываются через апач, и,во- вторых, результат их работы не виден на экране. Так что вы не скоро узнаете, если он не сработал. Самый смешной случай был, когда я случайно открыл одну XML- выгрузку, еженочно генерируемую по крону и обнаружил, что это файл двухмесячной давности. Эта выгрузка потом загружалась сторонней статистической программой и на основании ее данных высокое руководство судит о работе всей нашей организации. Хорошо, что отчет они видят в виде двух статистических цифр и они думали, что раз цифры не изменяются, значит мы работаем очень стабильно. Давно собирался, но только сегодня в очередной раз кинутый занялся этой проблемой, и создал себе помощника - небольшую библиотечку. Класец Cron, который будет выполнять такие скрипты и вызывает обработчик ошибок, если они конечно там возникнут и сообщать мне. Работать будет так: PHP: <?php class FuckingBuggingCronTask implements Cron_Task{ function run() { //тут сам скрипт, который раньше запускался по крону include 'cron_tumbit.php'; } } $cron= new Cron(); $cron->setErrorHandler(new Cron_ErrorMailer()); $cron->runTask(new FuckingBuggingCronTask()); вот исходники PHP: <?php /** * Description of Cron * */ class Cron extends Model { /** * * @var Cron_ErrorHandler Перехватчик ошибок, возникших во время выполнения задачи */ private $errorHandler; function __construct($errorHandler) { $this->setErrorHandler($errorHandler); } /** * Перехватчик ошибок, возникших во время выполнения задачи * * @return Cron_ErrorHandler */ public function getErrorHandler() { return $this->errorHandler; } /** * @param Cron_ErrorHandler $errorHandler */ public function setErrorHandler($errorHandler) { $this->errorHandler = $errorHandler; } /** * запускает задачу * в случае, если во время ее выполнения происходит ошибка, * отдает ее обработчику ошибок * любой вывод считается варнингом и тоже обрабатывает обработчиком * * @var Cron_Task $task Задача, которую нужно выполнить крону */ function runTask(Cron_Task $task) { try { ob_start(); $task->run(); //во время выполнения таска произошел варнинг if ($output= ob_get_clean()) { $e= new Exception($output); $this->getErrorHandler()->OnError($task, $e); } //во время выполнения таска произошела ошибка } catch (Exception $e) { $this->getErrorHandler()->OnError($task, $e); } } } /** * Задача, запускаемая по крону */ Interface Cron_Task { function run(); } /** * Перехватчик ошибок возникших во время выполнения задачи крона * обрабатыввает ошибки возникшие во время выполнения задачи */ Interface Cron_ErrorHandler { /** * * @param Cron_Task $task * @param Exception $error */ function OnError($task, $error); } /** * Перехватчик ошибок внутри задачи, который отсылает ошибки админу на почту */ class Cron_ErrorMailer implements Cron_ErrorHandler { /** * * @param Cron_Task $task * @param Exception $error */ function OnError($task, $error) { $mailer= new AutoMailer(); $mailer->AddAddress(ADMINMAIL); $mailer->Subject= "Ошибка во время выполнения в задачи крона!"; $mailer->Body= " <p>Во время выполнения задачи крона произошла ошибка</p> <p> </p> <p>Класс задачи: ".get_class($task)."</p> <p>Класс ошибки: ".get_class($error)."</p> <p>Сообщение: {$e->getMessage()}</p> <p>Файл: {$e->getFile()}</p> <p>Строка: {$e->getLine()}</p> <p>Стэк:</p> <pre>$e->getTraceAsString()</pre> "; try{ $mailer->Send(); } catch(MailException $e) {} } } Исходники наверняка в багах, потому что я их еще ни разу не запускал. сегодня время остается только чтобы открыть тему. Смысл все равно понятен. Кто что думает? может я вообще зря все это делаю, потому что кто- то уже написал такую библиотечку или есть какое- то другое решение средствами крона и пхп?
по идее вот это должно сохранить вывод выполненного скрипта по крону в файл Код (Text): php /www/htdocs/file.php >> log.log А файл потом можно отправить потом по почте, опять таки по крону
А как вы их пробовали вызывать через апач? Или может быть вызываются не через апач?) Скрипт может в базу или файл записать Утром открыли страничку со статистикой и посмотрели, кто выполнился, а кто нет.
да, косяков у такого подхода много. во-первых, крон может вообще не вызвать задачу, потому что сисадмин снес его и забыл что он зачем- то был нужен. она не выполнится, но я об этом ничего не узнаю во- вторых, есть ошибки, которые не перехватываются try- catch, например нет файла или нет такого класса, такие ошибки админу тоже не уйдут. мне очень понравились логи в базе, потому что они решают обе эти проблемы. но и у логов есть один недостаток- их надо каждый день просматривать. Поэтому, совместив оба этих подхода, получил вот такого франкенштейна. использовать планирую все так же PHP: <?php class FuckingBuggingCronTask implements Cron_Task{ function run() { //тут сам скрипт, который раньше запускался по крону include 'cron_tumbit.php'; } } $cron= new Cron(); $cron->addEventHendler(new Cron_ErrorMailer()); $cron->addEventHendler(new Cron_DBLogger()); $cron->runTask(new FuckingBuggingCronTask()); исходники (в багах) PHP: <?php <?php /** * Description of Cron * */ class Cron extends Model { /** * * @var array ICron_EventHandler массив обработчиков крона. вызываются при запуске задачи, ошибке во время задачи и завершении задачи */ private $eventHandlers= array(); function __construct($eventHandlers= array()) { foreach($eventHandlers as $eventHandler) $this->addEventHandler($eventHandler); } /** * массив обработчиков крона. вызываются при запуске задачи, ошибке во время задачи и завершении задачи * * @return array Cron_EventHandler */ public function getEventHandlers() { return $this->eventHandlers; } /** * @param Cron_EventHandler $eventHandler */ function addEventHandler($eventHandler) { $this->eventHandlers[] = $eventHandler; } /** * запускает задачу * основные события передает обработчикам событий: * запуск, ошибки и варнинги, завершение задачи * любой вывод считается варнингом и тоже обрабатывает обработчикам * * @var Cron_Task $task Задача, которую нужно выполнить крону */ function runTask(Cron_Task $task) { foreach($this->getEventHandlers() as $eventHandler) $eventHandler->OnRun($task); try { ob_start(); $taskResult= $task->run(); //во время выполнения таска произошел варнинг if ($output= ob_get_clean()) { $e= new Exception($output); foreach($this->getEventHandlers() as $eventHandler) $eventHandler->OnError($task, $e); return; } //во время выполнения таска произошела ошибка } catch (Exception $e) { foreach($this->getEventHandlers() as $eventHandler) $eventHandler->OnError($task, $e); return; } //нормальное завершение foreach($this->getEventHandlers() as $eventHandler) $eventHandler->OnComplete($task, $taskResult?$taskResult:new Cron_TaskResult()); } } /** * Результат задачи, который будет передан логеру */ final class Cron_TaskResult{ public $code= 0; public $message= 'Выполнено'; } /** * Задача, запускаемая по крону */ abstract class Cron_Task { /** * * @var int идентификатор задачи */ private $id; function __construct() { $this->id= 'время в милисекундах'; } function getId() { return $this->id; } /** * @return Cron_TaskResult */ abstract function run(); } /** * Перехватчик ошибок возникших во время выполнения задачи крона * обрабатыввает ошибки возникшие во время выполнения задачи */ Interface ICron_EventHandler { /** * Вызывается при старте задачи * @param Cron_Task $task */ function OnRun(Cron_Task $task); /** * Вызывается при возникновении ошибки в задаче * * @param Cron_Task $task * @param Exception $error */ function OnError(Cron_Task $task, Exception $e); /** * Вызывается при нормальном завершении задачи * * @param Cron_Task $task * @param Cron_Result $taskResult */ function OnComplete(Cron_Task $task, Cron_TaskResult $taskResult); } /** * обработчик событий, который отсылает ошибки внутри задачи админу на почту */ class Cron_ErrorMailer implements ICron_EventHandler { function OnRun(Cron_Task $task){} function OnComplete(Cron_Task $task, Cron_TaskResult $taskResult){} function OnError($task, $error) { $mailer= new AutoMailer(); $mailer->AddAddress(ADMINMAIL); $mailer->Subject= "Ошибка во время выполнения в задачи крона!"; $mailer->Body= " <p>Во время выполнения задачи крона произошла ошибка</p> <p> </p> <p>Класс задачи: ".get_class($task)."</p> <p>Класс ошибки: ".get_class($error)."</p> <p>Сообщение: {$e->getMessage()}</p> <p>Файл: {$e->getFile()}</p> <p>Строка: {$e->getLine()}</p> <p>Стэк:</p> <pre>$e->getTraceAsString()</pre> "; try{ $mailer->Send(); } catch(MailException $e) {} } } /** * Логгер крона, который всю информацию сохраняет в базе * таблица cron_logs */ class Cron_DBLogger extends Model implements ICron_EventHandler { function OnRun(Cron_Task $task){ //бла бла бла задача и время старта уходят в базу } function OnError(Cron_Task $task, Exception $e){ //ошибка уходит в базу } function OnComplete(Cron_Task $task, Cron_TaskResult $taskResult){ //бла бла бла результат тоже в базу } /** * Очищает свои накопленные логи */ function clear(){ //delete from cron_logs } /** * возвращает все логи с $from по $to, включительно в виде плоского массива {task, run, errorcode, errormessage, errorfile, errorline, errortrace, resultcode, resultmessage} * * @param Date $from * @param Date $to * * @return array {task, run, errorcode, errormessage, errorfile, errorline, errortrace, resultcode, resultmessage} */ function get($from, $to){ //select from cron_logs where between $from $to } } Естественно, в уме предполагаю вьюшку для DBLoger а в виде таблицы с красными строками, у которых есть эррор или нет резалта, что означает крон оборвался где- то посередине на require_once или "нет такого класса". Какие есть мнения?
сразу видно, я не сисадмин ни в одном глазу, если не знал этого. а эта >> добавляет в файл или пересоздает его по новой? а вообще тоже ничего, главное просто, как колесо
Код не смотрел, но по поводу какая разница - почта, файл, смска или страничка, на которой список скриптов, которые не запускались? это всё логи и их надо смотреть)
Я обычно при запуске крона пишу в служебную таблицу запись, что такая-то задача запустилась тогда-то. Таким образом можно в WEB интерфейсе админки проверять когда последний раз запускался cron и сигнализировать пользователю если что-то не так. Насчёт нету файла - тут уж пользователь сам виноват - надо обязаловкой проверять file_exists, is_readable, is_writeable и.т.д. P.S. Можно записывать started в начале, а в конце менять на completed. Так мы узнаем об ошибках, даже если упадёт скрипт в неотлавливаемую ошибку.
пишешь из ПХП, правильно я понял? если так, это как раз то, что я и собираюсь делать при помощи обертки. а что ты думаешь по поводу php /www/htdocs/file.php >> log.log ?
тут я так понял человеку требуется знать выполнился скрипт вообще или нет и каков результат без просмотра логов и всего такого Самый простой и безотказный вариант: в конце скрипта поставить пару функций отсылки результата на почту и на смс, можно даже звонок на тел организовать для самых извращенных нелюбителей читать логи итого получаем - если выполнилось, то пришло, если не выполнилось - то не пришло. вот и вся кухня и никакие мега классы писать не надо...
да какие мега-классы? ровно это и написано, если использовать Cron + Cron_ErrorMailer просто чтобы каждый раз в каждом кроновском скрипте не писать эти отсылки и звонки и решился написать эту обертку. это реально удобнее, чем захламлять смысловой код кроновского скрипта всякими отсылками и звонками, которые по смыслу как бы за рамками этого скрипта. php /www/htdocs/file.php >> log.log очень нравится своей банальностью.
Крон может высылать весь вывод скрипта на почту. Таким образом любая ошибка или варнинг придет на почту. Если в скрипте делать вывод об успешном завершении задачи - можно получать успех на почту и таким образом проверять, что "работает". Мы делаем как Psih, пишем в базу, но это работает, ясно дело, когда у вас свой планировщик, а крон - это так, обертку с номером задачи дергать.
Понятно. Задача в том, чтобы оставить скрипты как есть прикладными скриптами без дополнительной нагрузки всякими служебными приблудами. И придумать способ как в 100% случаев сбоев получать Alarm! на свою почту. Получать сообщения в случаях успешного выполнения (это когда oputput пустой) не желательно. Потому что зачем оно мне надо да? Спам какой-то, ну выполнился и выполнился. Зачем мне эти письма каждое утро? А вот это вот не понял еще раз другими словами можно? Если как Psih, то Cron + Cron_DBLogger, и у меня тоже все будет ок! ну и Cron_ErrorMailer впридачу, чтобы с утра первым делом почту открыл и сразу увидел, что что-то не так. А почему никто не делает php /www/htdocs/file.php >> log.log ?
Ну когда у вас есть список задач в базе и вы дергаете в кроне cron.php id-задачи то можно писать в эту же таблицу по id задачи дату последнего выполнения. Если же у вас набор скриптов - тут уже сложно как-то унифицировать. Теперь, у каждого скрипта есть два "канала" вывода - STDOUT и STDERR. На первый посылаются все ваши print-ы из скрипта. На второй - сообщения об ошибках (PHP-ые ну или вы сами можете туда вывод делать) На 100% получить информацию, что задача вообще не запускалась - нельзя. Ибо, что бы что-то периодически проверять, нужно так же периодически запускать проверку.. которая тоже может не запуститься и т.д. И вообще, случаи "админ снес крон", это из серии кирипича на голову - можно одевать каску, но и уронить могут кувалду. Так что в рассмотрение не берется. Т.е. отсекать нужно варианты, когда крон дернул скрипт, но вышла ошибка. В этом случае или сам крон или PHP генерит ошибку и посылает ее на STDERR. ЕЕ можно получать на почту. Обычный вывод скрипта, если он вдруг есть, можно "занулить" через >/dev/null Код (Text): А почему никто не делает php /www/htdocs/file.php >> log.log ? А какой в этом смысл? Этот файл никто не будет читать. Ошибки нужно получать как можно быстрее, а следить что "работает" нужно по конечному результату скрипта (например, где-то у себя в админке проверять timestamp вашего XML-я и предупреждать, если файл давно не менялся).
если я правильно понял, то это что-то вроде моего abstract class Cron_Task с его уникальным айдишником это да, перебрал. К тому же такие случаи лежат в зоне ответственности сисадмина, поэтому правильнее бедет, если об этом будет ломать голову сам сисадмин. про то что крон "забыл" вызвать скрипт навсегда забываем.
Если не делать >> log.log, то результат приходит мне на почту результат выполнения своей последней задачи я направляю в /dev/null ибо результат мне не нужен И по хорошему лучше повесить дополнительное оповещение (почта, смс, аська) при выполнении задачи некорректно, и потом уже лезть читать логи.
Получить систему, которая гарантированно сообщает об ошибках сама можно только обрабатывая STDERR снаружи от ПХП, потому что в самом ПХП невозможно перехватить ошибки типа "банан тебе а не require_once" и "нет такого класса". Так что только PHP обертками тут не обойдешься. От Cron + Cron_DBLogger (Psih style) тоже не в восторге, потому что его надо открывать и просматривать каждый день, а я лентяй в хорошем смысле. С таким же успехом я могу STDERR отправлять в файл при помощи >> , сделать ссылку на рабочем столе на него и с утра проверять не появилось ли там чего нового.
alexey_baranov Ну почему-же, проверь существование файла через file_exists перед require и выдай ошибку, если такого нету
Допустим существование самого cron_tumbit.php можно проверить по file_exists, но он рекварит пяток других файлов, а каждый из них еще пяток и т.д. кроновский файл может подключать больше сотни файлов. Их ты как проверять будешь по file_exists? А про рекваре я беспокоюсь потому что на той неделе именно она оборвала ночной скрипт после того как я технично подредактировал include_path. Если вводить какую- то страховочную систему, которая сообщает о ночных багах, то только такую которая работает во всех 100% ситуациях. Если система обрабатывает все ошибки но не файл нот экзист и класс нот экзист, то это какая-то полумера. Ее саму придется страховать. А зачем она тогда нужна? Я пока что ума не приложу, как из PHP перехватить эти две ошибки. Если это сделать, можно запускать любой кроновский скрипт из под PHP-обертки.
autoload тут не поможет. Из __autoload(), насколько я знаю, запрещено выбрасывать эксепшины. И сам file not exists trycatch-ом не перехватывается.