Персональный cron для проекта
Думаю, все знакомы с таким замечательным инструментом как cron. С его помощью мы с достаточной гибкостью можем конфигурировать время запуска необходимых нам скриптов наших проектов. Однако с ростом числа и сложности проектов данный инструмент обнаруживает некоторые свои недостатки:
В обоих случаях используется локальный (по отношению к проекту) аналог crontab’a и скрипт, реализующий логику работы cron’а (т.е. разбор crontab’а и запуск соответствующих скриптов). Разница в основном заключается в способе запуска скрипта cron’а.
В данном методе подразумевается, что у Вас в принципе нет доступа к crontab’у на сервере. В этом случае запуск cron-скрипта инициализируется по средствам пользовательских запросов, т.е. происходит каждый раз при обращении пользователя к странице. Для того, что бы избежать дублирующих запусков к crontab’у добавляется время последнего запуска проекта. Реализацию этой (или схожей) технологии можно увидеть как в персональных проектах, так и в различных фреймворках. Этот метод безусловно хорош с точки зрения массового использования, так как лишён необходимости правки системного crontab’а. Однако, при его использовании возникает другая проблема: как запустить скрипт (возможно «тяжёлый») во время отсутствия посетителей на сайте? В ответ на данный вопрос мне на ум приходят лишь ректальные методики (типа имитации запроса пользователя с другой машины, где есть cron), что делает данный подход на мой взгляд не применимым.
Второй же метод подразумевает наличие доступа к crontab’у. В этом случае для избежания недостатков, описанных в начале статьи, нам достаточно выделить в каждом проекте на сервере «точку входа» (скрипт/контроллер, реализующий логику cron’a для проекта) и добавить его ежеминутный вызов в системный crontab.
К слову сказать «гибрид» этих двух методов должен мог быть включён в ZF в качестве класса Zend_Scheduler, но работы, к сожалению, прикрыли.
Напоследок хочу привести пример реализации cron-скрипта. Я постарался сделать код достаточно самодокументированным, так что надеюсь, что вопросов не возникнет. Вкратце, суть работы скрипта сводится к трём шагам:
Для использования класса достаточно создать скрипт, который будет скармливать классу содержимое crontab-файла Вашего проекта и добавить ежеминутный запуск этого скрипта в системный crontab.
_________
Текст подготовлен в ХабраРедакторе
- crontab нарушает целостность проекта, вбирая часть его логики (время запуска скриптов) в себя;
- редактирование crontab’a не возможно без выделения соответствующих прав.
В обоих случаях используется локальный (по отношению к проекту) аналог crontab’a и скрипт, реализующий логику работы cron’а (т.е. разбор crontab’а и запуск соответствующих скриптов). Разница в основном заключается в способе запуска скрипта cron’а.
Запуск на основе пользовательских запросов
В данном методе подразумевается, что у Вас в принципе нет доступа к crontab’у на сервере. В этом случае запуск cron-скрипта инициализируется по средствам пользовательских запросов, т.е. происходит каждый раз при обращении пользователя к странице. Для того, что бы избежать дублирующих запусков к crontab’у добавляется время последнего запуска проекта. Реализацию этой (или схожей) технологии можно увидеть как в персональных проектах, так и в различных фреймворках. Этот метод безусловно хорош с точки зрения массового использования, так как лишён необходимости правки системного crontab’а. Однако, при его использовании возникает другая проблема: как запустить скрипт (возможно «тяжёлый») во время отсутствия посетителей на сайте? В ответ на данный вопрос мне на ум приходят лишь ректальные методики (типа имитации запроса пользователя с другой машины, где есть cron), что делает данный подход на мой взгляд не применимым.
Запуск на основе крона
Второй же метод подразумевает наличие доступа к crontab’у. В этом случае для избежания недостатков, описанных в начале статьи, нам достаточно выделить в каждом проекте на сервере «точку входа» (скрипт/контроллер, реализующий логику cron’a для проекта) и добавить его ежеминутный вызов в системный crontab.
К слову сказать «гибрид» этих двух методов должен мог быть включён в ZF в качестве класса Zend_Scheduler, но работы, к сожалению, прикрыли.
Пример реализации
Напоследок хочу привести пример реализации cron-скрипта. Я постарался сделать код достаточно самодокументированным, так что надеюсь, что вопросов не возникнет. Вкратце, суть работы скрипта сводится к трём шагам:
- разбор строки crontab’а согласно регулярному выражению;
- преобразование шаблона времени запуска в список возможных значений;
- проверка совпадения текущего времени с критериями запуска скрипта.
<?php
/**
* Реализация крона
* @see unixhelp.ed.ac.uk/CGI/main-cgi?crontab+5
*/
class Cron {
/**
* CronTab
* @var array
*/
private $aCronTab;
/**
* Конструктор
* @param string $sCronTabFile
*/
public function __construct( $sCronTabFile ) {
$aCronTab = file( $sCronTabFile );
$this->aCronTab = $this->_parse( $aCronTab );
}
// Минимальные/максимальные значения для временных критериев
const MINUTE_MIN = 0;
const MINUTE_MAX = 59;
const HOUR_MIN = 0;
const HOUR_MAX = 23;
const DAY_MIN = 1;
const DAY_MAX = 31;
const MONTH_MIN = 1;
const MONTH_MAX = 12;
const DOW_MIN = 0;
const DOW_MAX = 7;
/**
* Разбор crontab'а
* @param array $aCronTab
* @return array
*/
private function _parse( array $aCronTab ) {
// Шаблон для разбора строки crontab'а
$sPattern = '~^(?<minutes>[-0-9,/*]+)\s+(?<hours>[-0-9,/*]+)\s+' .
'(?<days>[-0-9,/*]+)\s+' .
'(?<months>[-0-9,/*]+|(-|Jan|Feb|Mar|Apr|May|Jul|Jun|Aug|Sep|Oct|Nov|Dec)+)\s+' .
'(?<dows>[-0-9,/*]+|(-|Sun|Mon|Tue|Wen|Thu|Fri|Sat)+)\s*' .
'(?<command>[^#]+)$~i';
// Разбор строк crontab'а
$aParsedCronTab = array();
foreach( $aCronTab as $sJob ) {
if( '#' !== $sJob[0] && preg_match( $sPattern, $sJob, $aJob ) ) {
// Замена имён месяцов их номерами
$aJob['months'] = str_replace(
array('Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep',
'Oct','Nov','Dec'),
array(1,2,3,4,5,6,7,8,9,10,11,12),
$aJob['months']
);
// Замена названий дней недели их номерами
$aJob['dows'] = str_replace(
array('Sun','Mon','Tue','Wed','Thu','Fri','Sat'),
array(0,1,2,3,4,5,6),
$aJob['dows']
);
// Преобразование времени запуска заданий в диапозон возможных значений
$aTime['minutes'] = $this->_convertElement( $aJob['minutes'],
self::MINUTE_MIN, self::MINUTE_MAX );
$aTime['hours'] = $this->_convertElement( $aJob['hours'],
self::HOUR_MIN, self::HOUR_MAX );
$aTime['days'] = $this->_convertElement( $aJob['days'],
self::DAY_MIN, self::DAY_MAX );
$aTime['months'] = $this->_convertElement( $aJob['months'],
self::MONTH_MIN, self::MONTH_MAX );
$aTime['dows'] = $this->_convertElement( $aJob['dows'],
self::DOW_MIN, self::DOW_MAX );
$aParsedCronTab[] = array(
'time' => $aTime,
'command' => trim( $aJob['command'] )
);
}
}
return $aParsedCronTab;
}
/**
* Преобразование времени запуска задания из формата crontab'а
* в список возможных значений
*
* @param string $sElement
* @param int $iMinValue Минимальное значение для элемента
* @param int $iMaxValue Максимальное значение для элемента
* @return array Список возможных значений элемента
*/
private function _convertElement( $sElement, $iMinValue, $iMaxValue ) {
// Инициализация переменных
$aAvailableValues = array();
// Шаблон для разбора элемента
$sPattern = '~^((?<asterisk>\*)|((?<number>[0-9]{1,2})+(-(?<range>[0-9]{1,2}))?))' .
'(/(?<step>[0-9]{1,2}))?$~i';
// Разделение списка элементов (1,2,3)
$aElements = explode( ',', $sElement );
// Получение возможных значений для каждого элемента
foreach( $aElements as $sElements ) {
if( preg_match( $sPattern, $sElements, $aElement ) ) {
// Элемент равен "*"
if( !empty( $aElement['asterisk'] ) ) {
$aValues = range(
$iMinValue,
$iMaxValue,
( !empty( $aElement['step'] ) ) ? (int)$aElement['step'] : 1
);
$aAvailableValues = array_merge( $aAvailableValues, $aValues );
}
// Элемен - диапозон значений
elseif( !empty( $aElement['range'] ) ) {
$aValues = range(
$aElement['number'],
$aElement['range'],
( !empty( $aElement['step'] ) ) ? (int)$aElement['step'] : 1
);
$aAvailableValues = array_merge( $aAvailableValues, $aValues );
}
// Элемент - обычное число
else {
$aAvailableValues = array_merge( $aAvailableValues,
array( (int)$aElement['number'] ) );
}
}
}
return $aAvailableValues;
}
/**
* Запуск задач из crontab'а
*/
public function run() {
// Максимальное количество дней в месяце и дней недели
$iMaxDaysCount = self::DAY_MAX - self::DAY_MIN + 1;
$iMaxDOWsCount = self::DOW_MAX - self::DOW_MIN + 1;
// Получение текущего времени
$aCurrentDate = getdate();
// Перебор задач из crontab'а
foreach( $this->aCronTab as $aJob ) {
// Проверка времени запуска задачи
$bRun = false;
$aTaskDate = $aJob['time'];
if( in_array( $aCurrentDate['minutes'], $aTaskDate['minutes'] )
&& in_array( $aCurrentDate['hours'], $aTaskDate['hours'] )
&& in_array( $aCurrentDate['mon'], $aTaskDate['months'] )
) {
// Дни месяца и дни недели не заданы как '*'
if( ( $iMaxDOWsCount !== count( $aTaskDate['dows'] ) )
&& ( $iMaxDaysCount !== count( $aTaskDate['days'] ) )
) {
if( in_array( $aCurrentDate['mday'], $aTaskDate['days'] )
|| in_array( $aCurrentDate['wday'], $aTaskDate['dows'] )
)
$bRun = true;
// Дни месяца или дни недели заданы как '*'
} else {
if( in_array( $aCurrentDate['mday'], $aTaskDate['days'] )
&& in_array( $aCurrentDate['wday'], $aTaskDate['dows'] )
)
$bRun = true;
}
}
if( $bRun )
$this->_runJob( $aJob['command'] );
}
}
/**
* Запуск задачи
* @param string $sCommand Задача
*/
private function _runJob( $sCommand ) {
exec( "{$sCommand} &" );
}
}
* This source code was highlighted with Source Code Highlighter.Для использования класса достаточно создать скрипт, который будет скармливать классу содержимое crontab-файла Вашего проекта и добавить ежеминутный запуск этого скрипта в системный crontab.
_________
Текст подготовлен в ХабраРедакторе



комментарии (18)