This commit is contained in:
Deon George 2011-08-16 12:22:47 +10:00
parent c1b7196f41
commit 91c51fdb83
6 changed files with 852 additions and 0 deletions

View File

@ -0,0 +1,86 @@
# Kohana-Cron
This module provides a way to schedule tasks (jobs) within your Kohana application.
## Installation
Step 1: Download the module into your modules subdirectory.
Step 2: Enable the module in your bootstrap file:
/**
* Enable modules. Modules are referenced by a relative or absolute path.
*/
Kohana::modules(array(
'cron' => MODPATH.'cron',
// 'auth' => MODPATH.'auth', // Basic authentication
// 'codebench' => MODPATH.'codebench', // Benchmarking tool
// 'database' => MODPATH.'database', // Database access
// 'image' => MODPATH.'image', // Image manipulation
// 'orm' => MODPATH.'orm', // Object Relationship Mapping
// 'pagination' => MODPATH.'pagination', // Paging of results
// 'userguide' => MODPATH.'userguide', // User guide and API documentation
));
Step 3: Make sure the settings in `config/cron.php` are correct for your environment.
If not, copy the file to `application/config/cron.php` and change the values accordingly.
## Usage
In its simplest form, a task is a [PHP callback][1] and times at which it should run.
To configure a task call `Cron::set($name, array($frequency, $callback))` where
`$frequency` is a string of date and time fields identical to those found in [crontab][2].
For example,
Cron::set('reindex_catalog', array('@daily', 'Catalog::regenerate_index'));
Cron::set('calendar_notifications', array('*/5 * * * *', 'Calendar::send_emails'));
Configured tasks are run with their appropriate frequency by calling `Cron::run()`. Call
this method in your bootstrap file, and you're done!
## Advanced Usage
A task can also be an instance of `Cron` that extends `next()` and/or `execute()` as
needed. Such a task is configured by calling `Cron::set($name, $instance)`.
If you have access to the system crontab, you can run Cron less (or more) than once
every request. You will need to modify the lines where the request is handled in your
bootstrap file to prevent extraneous output. The default is:
/**
* Execute the main request. A source of the URI can be passed, eg: $_SERVER['PATH_INFO'].
* If no source is specified, the URI will be automatically detected.
*/
echo Request::instance()
->execute()
->send_headers()
->response;
Change it to:
if ( ! defined('SUPPRESS_REQUEST'))
{
/**
* Execute the main request. A source of the URI can be passed, eg: $_SERVER['PATH_INFO'].
* If no source is specified, the URI will be automatically detected.
*/
echo Request::instance()
->execute()
->send_headers()
->response;
}
Then set up a system cron job to run your application's Cron once a minute:
* * * * * /usr/bin/php -f /path/to/kohana/modules/cron/run.php
The included `run.php` should work for most cases, but you are free to call `Cron::run()`
in any way you see fit.
[1]: http://php.net/manual/language.pseudo-types.php#language.types.callback
[2]: http://linux.die.net/man/5/crontab

View File

@ -0,0 +1,10 @@
<?php defined('SYSPATH') or die('No direct script access.');
/**
* @package Cron
*
* @author Chris Bandy
* @copyright (c) 2010 Chris Bandy
* @license http://www.opensource.org/licenses/isc-license.txt
*/
class Cron extends Kohana_Cron {}

View File

@ -0,0 +1,621 @@
<?php defined('SYSPATH') or die('No direct script access.');
/**
* @package Cron
*
* @author Chris Bandy
* @copyright (c) 2010 Chris Bandy
* @license http://www.opensource.org/licenses/isc-license.txt
*/
class Kohana_Cron
{
protected static $_jobs = array();
protected static $_times = array();
/**
* Registers a job to be run
*
* @param string Unique name
* @param array|Cron Job to run
*/
public static function set($name, $job)
{
if (is_array($job))
{
$job = new Cron(reset($job), next($job));
}
Cron::$_jobs[$name] = $job;
}
/**
* Retrieve the timestamps of when jobs should run
*/
protected static function _load()
{
Cron::$_times = Kohana::cache("Cron::run()");
}
/**
* Acquire the Cron mutex
*
* @return boolean
*/
protected static function _lock()
{
$config = Kohana::config('cron');
$result = FALSE;
if (file_exists($config->lock) AND ($stat = @stat($config->lock)) AND time() - $config->window < $stat['mtime'])
{
// Lock exists and has not expired
return $result;
}
$fh = fopen($config->lock, 'a');
if (flock($fh, LOCK_EX))
{
fseek($fh, 0, SEEK_END);
if (ftell($fh) === (empty($stat) ? 0 : $stat['size']))
{
// Current size matches expected size
// Claim the file by changing the size
fwrite($fh, '.');
$result = TRUE;
}
// else, Another process acquired during flock()
}
fclose($fh);
return $result;
}
/**
* Store the timestamps of when jobs should run next
*/
protected static function _save()
{
Kohana::cache("Cron::run()", Cron::$_times, Kohana::config('cron')->window * 2);
}
/**
* Release the Cron mutex
*/
protected static function _unlock()
{
return @unlink(Kohana::config('cron')->lock);
}
/**
* @return boolean FALSE when another instance is running
*/
public static function run()
{
if (empty(Cron::$_jobs))
return TRUE;
if ( ! Cron::_lock())
return FALSE;
try
{
Cron::_load();
$now = time();
$threshold = $now - Kohana::config('cron')->window;
foreach (Cron::$_jobs as $name => $job)
{
if (empty(Cron::$_times[$name]) OR Cron::$_times[$name] < $threshold)
{
// Expired
Cron::$_times[$name] = $job->next($now);
if ($job->next($threshold) < $now)
{
// Within the window
$job->execute();
}
}
elseif (Cron::$_times[$name] < $now)
{
// Within the window
Cron::$_times[$name] = $job->next($now);
$job->execute();
}
}
}
catch (Exception $e) {}
Cron::_save();
Cron::_unlock();
if (isset($e))
throw $e;
return TRUE;
}
protected $_callback;
protected $_period;
public function __construct($period, $callback)
{
$this->_period = $period;
$this->_callback = $callback;
}
/**
* Execute this job
*/
public function execute()
{
call_user_func($this->_callback);
}
/**
* Calculates the next timestamp in this period
*
* @param integer Timestamp from which to calculate
* @return integer Next timestamp in this period
*/
public function next($from)
{
// PHP >= 5.3.0
//if ($this->_period instanceof DatePeriod) { return; }
//if (is_string($this->_period) AND preg_match('/^P[\dDHMSTWY]+$/', $period)) { $this->_period = new DateInterval($this->_period); }
//if ($this->_period instanceof DateInterval) { return; }
return $this->_next_crontab($from);
}
/**
* Calculates the next timestamp of this crontab period
*
* @param integer Timestamp from which to calculate
* @return integer Next timestamp in this period
*/
protected function _next_crontab($from)
{
if (is_string($this->_period))
{
// Convert string to lists of valid values
if ($this->_period[0] === '@')
{
switch (substr($this->_period, 1))
{
case 'annually':
case 'yearly':
// '0 0 1 1 *'
$this->_period = array('minutes' => array(0), 'hours' => array(0), 'monthdays' => array(1), 'months' => array(1), 'weekdays' => range(0,6));
break;
case 'daily':
case 'midnight':
// '0 0 * * *'
$this->_period = array('minutes' => array(0), 'hours' => array(0), 'monthdays' => range(1,31), 'months' => range(1,12), 'weekdays' => range(0,6));
break;
case 'hourly':
// '0 * * * *'
$this->_period = array('minutes' => array(0), 'hours' => range(0,23), 'monthdays' => range(1,31), 'months' => range(1,12), 'weekdays' => range(0,6));
break;
case 'monthly':
// '0 0 1 * *'
$this->_period = array('minutes' => array(0), 'hours' => array(0), 'monthdays' => array(1), 'months' => range(1,12), 'weekdays' => range(0,6));
break;
case 'weekly':
// '0 0 * * 0'
$this->_period = array('minutes' => array(0), 'hours' => array(0), 'monthdays' => range(1,31), 'months' => range(1,12), 'weekdays' => array(0));
break;
}
}
else
{
list($minutes, $hours, $monthdays, $months, $weekdays) = explode(' ', $this->_period);
$months = strtr(strtolower($months), array(
'jan' => 1,
'feb' => 2,
'mar' => 3,
'apr' => 4,
'may' => 5,
'jun' => 6,
'jul' => 7,
'aug' => 8,
'sep' => 9,
'oct' => 10,
'nov' => 11,
'dec' => 12,
));
$weekdays = strtr(strtolower($weekdays), array(
'sun' => 0,
'mon' => 1,
'tue' => 2,
'wed' => 3,
'thu' => 4,
'fri' => 5,
'sat' => 6,
));
$this->_period = array(
'minutes' => $this->_parse_crontab_field($minutes, 0, 59),
'hours' => $this->_parse_crontab_field($hours, 0, 23),
'monthdays' => $this->_parse_crontab_field($monthdays, 1, 31),
'months' => $this->_parse_crontab_field($months, 1, 12),
'weekdays' => $this->_parse_crontab_field($weekdays, 0, 7)
);
// Ensure Sunday is zero
if (end($this->_period['weekdays']) === 7)
{
array_pop($this->_period['weekdays']);
if (reset($this->_period['weekdays']) !== 0)
{
array_unshift($this->_period['weekdays'], 0);
}
}
}
}
$from = getdate($from);
if ( ! in_array($from['mon'], $this->_period['months']))
return $this->_next_crontab_month($from);
if (count($this->_period['weekdays']) === 7)
{
// Day of Week is unrestricted, defer to Day of Month
if ( ! in_array($from['mday'], $this->_period['monthdays']))
return $this->_next_crontab_monthday($from);
}
elseif (count($this->_period['monthdays']) === 31)
{
// Day of Month is unrestricted, use Day of Week
if ( ! in_array($from['wday'], $this->_period['weekdays']))
return $this->_next_crontab_weekday($from);
}
else
{
// Both Day of Week and Day of Month are restricted
if ( ! in_array($from['mday'], $this->_period['monthdays']) AND ! in_array($from['wday'], $this->_period['weekdays']))
return $this->_next_crontab_day($from);
}
if ( ! in_array($from['hours'], $this->_period['hours']))
return $this->_next_crontab_hour($from);
return $this->_next_crontab_minute($from);
}
/**
* Calculates the first timestamp in the next day of this period when both
* Day of Week and Day of Month are restricted
*
* @uses _next_crontab_month()
*
* @param array Date array from getdate()
* @return integer Timestamp of next restricted Day
*/
protected function _next_crontab_day(array $from)
{
// Calculate effective Day of Month for next Day of Week
if ($from['wday'] >= end($this->_period['weekdays']))
{
$next = reset($this->_period['weekdays']) + 7;
}
else
{
foreach ($this->_period['weekdays'] as $next)
{
if ($from['wday'] < $next)
break;
}
}
$monthday = $from['mday'] + $next - $from['wday'];
if ($monthday <= (int) date('t', mktime(0, 0, 0, $from['mon'], 1, $from['year'])))
{
// Next Day of Week is in this Month
if ($from['mday'] >= end($this->_period['monthdays']))
{
// No next Day of Month, use next Day of Week
$from['mday'] = $monthday;
}
else
{
// Calculate next Day of Month
foreach ($this->_period['monthdays'] as $next)
{
if ($from['mday'] < $next)
break;
}
// Use earliest day
$from['mday'] = min($monthday, $next);
}
}
else
{
if ($from['mday'] >= end($this->_period['monthdays']))
{
// No next Day of Month, use next Month
return $this->_next_crontab_month($from);
}
// Calculate next Day of Month
foreach ($this->_period['monthdays'] as $next)
{
if ($from['mday'] < $next)
break;
}
// Use next Day of Month
$from['mday'] = $next;
}
// Use first Hour and first Minute
return mktime(reset($this->_period['hours']), reset($this->_period['minutes']), 0, $from['mon'], $from['mday'], $from['year']);
}
/**
* Calculates the first timestamp in the next hour of this period
*
* @uses _next_crontab_day()
* @uses _next_crontab_monthday()
* @uses _next_crontab_weekday()
*
* @param array Date array from getdate()
* @return integer Timestamp of next Hour
*/
protected function _next_crontab_hour(array $from)
{
if ($from['hours'] >= end($this->_period['hours']))
{
// No next Hour
if (count($this->_period['weekdays']) === 7)
{
// Day of Week is unrestricted, defer to Day of Month
return $this->_next_crontab_monthday($from);
}
if (count($this->_period['monthdays']) === 31)
{
// Day of Month is unrestricted, use Day of Week
return $this->_next_crontab_weekday($from);
}
// Both Day of Week and Day of Month are restricted
return $this->_next_crontab_day($from);
}
// Calculate next Hour
foreach ($this->_period['hours'] as $next)
{
if ($from['hours'] < $next)
break;
}
// Use next Hour and first Minute
return mktime($next, reset($this->_period['minutes']), 0, $from['mon'], $from['mday'], $from['year']);
}
/**
* Calculates the timestamp of the next minute in this period
*
* @uses _next_crontab_hour()
*
* @param array Date array from getdate()
* @return integer Timestamp of next Minute
*/
protected function _next_crontab_minute(array $from)
{
if ($from['minutes'] >= end($this->_period['minutes']))
{
// No next Minute, use next Hour
return $this->_next_crontab_hour($from);
}
// Calculate next Minute
foreach ($this->_period['minutes'] as $next)
{
if ($from['minutes'] < $next)
break;
}
// Use next Minute
return mktime($from['hours'], $next, 0, $from['mon'], $from['mday'], $from['year']);
}
/**
* Calculates the first timestamp in the next month of this period
*
* @param array Date array from getdate()
* @return integer Timestamp of next Month
*/
protected function _next_crontab_month(array $from)
{
if ($from['mon'] >= end($this->_period['months']))
{
// No next Month, increment Year and use first Month
++$from['year'];
$from['mon'] = reset($this->_period['months']);
}
else
{
// Calculate next Month
foreach ($this->_period['months'] as $next)
{
if ($from['mon'] < $next)
break;
}
// Use next Month
$from['mon'] = $next;
}
if (count($this->_period['weekdays']) === 7)
{
// Day of Week is unrestricted, use first Day of Month
$from['mday'] = reset($this->_period['monthdays']);
}
else
{
// Calculate Day of Month for the first Day of Week
$indices = array_flip($this->_period['weekdays']);
$monthday = 1;
$weekday = (int) date('w', mktime(0, 0, 0, $from['mon'], 1, $from['year']));
while ( ! isset($indices[$weekday % 7]) AND $monthday < 7)
{
++$monthday;
++$weekday;
}
if (count($this->_period['monthdays']) === 31)
{
// Day of Month is unrestricted, use first Day of Week
$from['mday'] = $monthday;
}
else
{
// Both Day of Month and Day of Week are restricted, use earliest one
$from['mday'] = min($monthday, reset($this->_period['monthdays']));
}
}
// Use first Hour and first Minute
return mktime(reset($this->_period['hours']), reset($this->_period['minutes']), 0, $from['mon'], $from['mday'], $from['year']);
}
/**
* Calculates the first timestamp in the next day of this period when only
* Day of Month is restricted
*
* @uses _next_crontab_month()
*
* @param array Date array from getdate()
* @return integer Timestamp of next Day of Month
*/
protected function _next_crontab_monthday(array $from)
{
if ($from['mday'] >= end($this->_period['monthdays']))
{
// No next Day of Month, use next Month
return $this->_next_crontab_month($from);
}
// Calculate next Day of Month
foreach ($this->_period['monthdays'] as $next)
{
if ($from['mday'] < $next)
break;
}
// Use next Day of Month, first Hour, and first Minute
return mktime(reset($this->_period['hours']), reset($this->_period['minutes']), 0, $from['mon'], $next, $from['year']);
}
/**
* Calculates the first timestamp in the next day of this period when only
* Day of Week is restricted
*
* @uses _next_crontab_month()
*
* @param array Date array from getdate()
* @return integer Timestamp of next Day of Week
*/
protected function _next_crontab_weekday(array $from)
{
// Calculate effective Day of Month for next Day of Week
if ($from['wday'] >= end($this->_period['weekdays']))
{
$next = reset($this->_period['weekdays']) + 7;
}
else
{
foreach ($this->_period['weekdays'] as $next)
{
if ($from['wday'] < $next)
break;
}
}
$monthday = $from['mday'] + $next - $from['wday'];
if ($monthday > (int) date('t', mktime(0, 0, 0, $from['mon'], 1, $from['year'])))
{
// Next Day of Week is not in this Month, use next Month
return $this->_next_crontab_month($from);
}
// Use next Day of Week, first Hour, and first Minute
return mktime(reset($this->_period['hours']), reset($this->_period['minutes']), 0, $from['mon'], $monthday, $from['year']);
}
/**
* Returns a sorted array of all the values indicated in a Crontab field
* @link http://linux.die.net/man/5/crontab
*
* @param string Crontab field
* @param integer Minimum value for this field
* @param integer Maximum value for this field
* @return array
*/
protected function _parse_crontab_field($value, $min, $max)
{
$result = array();
foreach (explode(',', $value) as $value)
{
if ($slash = strrpos($value, '/'))
{
$step = (int) substr($value, $slash + 1);
$value = substr($value, 0, $slash);
}
if ($value === '*')
{
$result = array_merge($result, range($min, $max, $slash ? $step : 1));
}
elseif ($dash = strpos($value, '-'))
{
$result = array_merge($result, range(max($min, (int) substr($value, 0, $dash)), min($max, (int) substr($value, $dash + 1)), $slash ? $step : 1));
}
else
{
$value = (int) $value;
if ($min <= $value AND $value <= $max)
{
$result[] = $value;
}
}
}
sort($result);
return array_unique($result);
}
}

View File

@ -0,0 +1,28 @@
<?php defined('SYSPATH') OR die('No direct access allowed.');
/**
* @package Cron
*
* @author Chris Bandy
* @copyright (c) 2010 Chris Bandy
* @license http://www.opensource.org/licenses/isc-license.txt
*/
return array
(
// Path to a writable directory and lock file
'lock' => Kohana::$cache_dir.DIRECTORY_SEPARATOR.'cron.lck',
/**
* Cron does not run EXACTLY when tasks are scheduled.
* A task can be executed up to this many seconds AFTER its scheduled time.
*
* For example, Cron is run at 10:48 and a task was scheduled to execute at
* 10:45, 180 seconds ago. If window is greater than 180, the task will be
* executed.
*
* This value should always be larger than the time it takes to run all
* your tasks.
*/
'window' => 300,
);

View File

@ -0,0 +1,22 @@
<?php
/**
* @package Cron
*
* @author Chris Bandy
* @copyright (c) 2010 Chris Bandy
* @license http://www.opensource.org/licenses/isc-license.txt
*/
// Path to Kohana's index.php
$system = dirname(dirname(dirname(__FILE__))).DIRECTORY_SEPARATOR.'index.php';
if (file_exists($system))
{
defined('SUPPRESS_REQUEST') or define('SUPPRESS_REQUEST', TRUE);
include $system;
// If Cron has been run in APPPATH/bootstrap.php, this second call is harmless
Cron::run();
}

View File

@ -0,0 +1,85 @@
<?php
/**
* @package Cron
* @group kohana
* @group kohana.cron
*
* @author Chris Bandy
* @copyright (c) 2010 Chris Bandy
* @license http://www.opensource.org/licenses/isc-license.txt
*/
class Kohana_Cron_Test extends PHPUnit_Framework_TestCase
{
/**
* @test
* @dataProvider provider_next
*
* @param string Period
* @param integer Timestamp from which to calculate
* @param integer Next timestamp in period
*/
public function test_next($period, $from, $expected_result)
{
$cron = new Cron($period, NULL);
$result = $cron->next($from);
$this->assertSame($expected_result, $result);
}
public function provider_next()
{
return array
(
array('@annually', mktime(8, 45, 0, 11, 19, 2009), mktime(0, 0, 0, 1, 1, 2010)),
array('@monthly', mktime(8, 45, 0, 11, 19, 2009), mktime(0, 0, 0, 12, 1, 2009)),
array('@weekly', mktime(8, 45, 0, 11, 19, 2009), mktime(0, 0, 0, 11, 22, 2009)),
array('@daily', mktime(8, 45, 0, 11, 19, 2009), mktime(0, 0, 0, 11, 20, 2009)),
array('@hourly', mktime(8, 45, 0, 11, 19, 2009), mktime(9, 0, 0, 11, 19, 2009)),
array('* * * * *', mktime(8, 45, 0, 11, 19, 2009), mktime(8, 46, 0, 11, 19, 2009)),
array(
'* * * * 0', // Sundays
mktime(0, 0, 0, 11, 30, 2009), // Monday, Nov 30, 2009
mktime(0, 0, 0, 12, 6, 2009) // Sunday, Dec 6, 2009
),
array(
'* * 15 * 6', // 15th and Saturdays
mktime(0, 0, 0, 11, 29, 2009), // Sunday, Nov 29, 2009
mktime(0, 0, 0, 12, 5, 2009) // Saturday, Dec 5, 2009
),
array(
'* * * * 1,5', // Mondays and Fridays
mktime(0, 0, 0, 11, 24, 2009), // Tuesday, Nov 24, 2009
mktime(0, 0, 0, 11, 27, 2009) // Friday, Nov 27, 2009
),
array(
'* * 15 * 6-7', // 15th, Saturdays, and Sundays
mktime(0, 0, 0, 11, 23, 2009), // Monday, Nov 23, 2009
mktime(0, 0, 0, 11, 28, 2009) // Saturday, Nov 28, 2009
),
array(
'* * 15,30 * 2', // 15th, 30th, and Tuesdays
mktime(0, 0, 0, 11, 29, 2009), // Sunday, Nov 29, 2009
mktime(0, 0, 0, 11, 30, 2009) // Monday, Nov 30, 2009
),
array(
'0 0 * * 4', // Midnight on Thursdays
mktime(1, 0, 0, 11, 19, 2009), // 01:00 Thursday, Nov 19, 2009
mktime(0, 0, 0, 11, 26, 2009) // 00:00 Thursday, Nov 26, 2009
),
array(
'0 0 */2 * 4', // Midnight on odd days and Thursdays
mktime(1, 0, 0, 11, 19, 2009), // 01:00 Thursday, Nov 19, 2009
mktime(0, 0, 0, 11, 21, 2009) // 00:00 Saturday, Nov 21, 2009
),
);
}
}