mirror of
https://github.com/idanoo/php-resque
synced 2025-07-01 05:32:20 +00:00
Initial commit
This commit is contained in:
commit
cb4205d508
37 changed files with 2808 additions and 0 deletions
13
lib/Resque/Exception.php
Normal file
13
lib/Resque/Exception.php
Normal file
|
@ -0,0 +1,13 @@
|
|||
<?php
|
||||
/**
|
||||
* Resque exception.
|
||||
*
|
||||
* @package Resque
|
||||
* @author Chris Boulton <chris.boulton@interspire.com>
|
||||
* @copyright (c) 2010 Chris Boulton
|
||||
* @license http://www.opensource.org/licenses/mit-license.php
|
||||
*/
|
||||
class Resque_Exception extends Exception
|
||||
{
|
||||
}
|
||||
?>
|
59
lib/Resque/Failure.php
Normal file
59
lib/Resque/Failure.php
Normal file
|
@ -0,0 +1,59 @@
|
|||
<?php
|
||||
require_once 'Failure/Interface.php';
|
||||
|
||||
/**
|
||||
* Failed Resque job.
|
||||
*
|
||||
* @package Resque/Failure
|
||||
* @author Chris Boulton <chris.boulton@interspire.com>
|
||||
* @copyright (c) 2010 Chris Boulton
|
||||
* @license http://www.opensource.org/licenses/mit-license.php
|
||||
*/
|
||||
class Resque_Failure
|
||||
{
|
||||
/**
|
||||
* @var string Class name representing the backend to pass failed jobs off to.
|
||||
*/
|
||||
private static $backend;
|
||||
|
||||
/**
|
||||
* Create a new failed job on the backend.
|
||||
*
|
||||
* @param object $payload The contents of the job that has just failed.
|
||||
* @param object $exception The exception generated when the job failed to run.
|
||||
* @param object $worker Instance of Resque_Worker that was running this job when it failed.
|
||||
* @param string $queue The name of the queue that this job was fetched from.
|
||||
*/
|
||||
public static function create($payload, Exception $exception, Resque_Worker $worker, $queue)
|
||||
{
|
||||
$backend = self::getBackend();
|
||||
new $backend($payload, $exception, $worker, $queue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an instance of the backend for saving job failures.
|
||||
*
|
||||
* @return object Instance of backend object.
|
||||
*/
|
||||
public function getBackend()
|
||||
{
|
||||
if(self::$backend === null) {
|
||||
require 'Failure/Redis.php';
|
||||
self::$backend = 'Resque_Failure_Redis';
|
||||
}
|
||||
|
||||
return self::$backend;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the backend to use for raised job failures. The supplied backend
|
||||
* should be the name of a class to be instantiated when a job fails.
|
||||
* It is your responsibility to have the backend class loaded (or autoloaded)
|
||||
*
|
||||
* @param string $backend The class name of the backend to pipe failures to.
|
||||
*/
|
||||
public function setBackend($backend)
|
||||
{
|
||||
self::$backend = $backend;
|
||||
}
|
||||
}
|
22
lib/Resque/Failure/Interface.php
Normal file
22
lib/Resque/Failure/Interface.php
Normal file
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
/**
|
||||
* Interface that all failure backends should implement.
|
||||
*
|
||||
* @package Resque/Failure
|
||||
* @author Chris Boulton <chris.boulton@interspire.com>
|
||||
* @copyright (c) 2010 Chris Boulton
|
||||
* @license http://www.opensource.org/licenses/mit-license.php
|
||||
*/
|
||||
interface Resque_Failure_Interface
|
||||
{
|
||||
/**
|
||||
* Initialize a failed job class and save it (where appropriate).
|
||||
*
|
||||
* @param object $payload Object containing details of the failed job.
|
||||
* @param object $exception Instance of the exception that was thrown by the failed job.
|
||||
* @param object $worker Instance of Resque_Worker that received the job.
|
||||
* @param string $queue The name of the queue the job was fetched from.
|
||||
*/
|
||||
public function __construct($payload, $exception, $worker, $queue);
|
||||
}
|
||||
?>
|
35
lib/Resque/Failure/Redis.php
Normal file
35
lib/Resque/Failure/Redis.php
Normal file
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
/**
|
||||
* Redis backend for storing failed Resque jobs.
|
||||
*
|
||||
* @package Resque/Failure
|
||||
* @author Chris Boulton <chris.boulton@interspire.com>
|
||||
* @copyright (c) 2010 Chris Boulton
|
||||
* @license http://www.opensource.org/licenses/mit-license.php
|
||||
*/
|
||||
|
||||
class Resque_Failure_Redis implements Resque_Failure_Interface
|
||||
{
|
||||
/**
|
||||
* Initialize a failed job class and save it (where appropriate).
|
||||
*
|
||||
* @param object $payload Object containing details of the failed job.
|
||||
* @param object $exception Instance of the exception that was thrown by the failed job.
|
||||
* @param object $worker Instance of Resque_Worker that received the job.
|
||||
* @param string $queue The name of the queue the job was fetched from.
|
||||
*/
|
||||
public function __construct($payload, $exception, $worker, $queue)
|
||||
{
|
||||
$data = new stdClass;
|
||||
$data->failed_at = strftime('%a %b %d %H:%M:%S %Z %Y');
|
||||
$data->payload = $payload;
|
||||
$data->exception = get_class($exception);
|
||||
$data->error = $exception->getMessage();
|
||||
$data->backtrace = explode("\n", $exception->getTraceAsString());
|
||||
$data->worker = (string)$worker;
|
||||
$data->queue = $queue;
|
||||
$data = json_encode($data);
|
||||
Resque::redis()->rpush('failed', $data);
|
||||
}
|
||||
}
|
||||
?>
|
195
lib/Resque/Job.php
Normal file
195
lib/Resque/Job.php
Normal file
|
@ -0,0 +1,195 @@
|
|||
<?php
|
||||
require_once 'Job/Status.php';
|
||||
|
||||
/**
|
||||
* Resque job.
|
||||
*
|
||||
* @package Resque/Job
|
||||
* @author Chris Boulton <chris.boulton@interspire.com>
|
||||
* @copyright (c) 2010 Chris Boulton
|
||||
* @license http://www.opensource.org/licenses/mit-license.php
|
||||
*/
|
||||
class Resque_Job
|
||||
{
|
||||
/**
|
||||
* @var string The name of the queue that this job belongs to.
|
||||
*/
|
||||
public $queue;
|
||||
|
||||
/**
|
||||
* @var Resque_Worker Instance of the Resque worker running this job.
|
||||
*/
|
||||
public $worker;
|
||||
|
||||
/**
|
||||
* @var object Object containing details of the job.
|
||||
*/
|
||||
public $payload;
|
||||
|
||||
/**
|
||||
* Instantiate a new instance of a job.
|
||||
*
|
||||
* @param string $queue The queue that the job belongs to.
|
||||
* @param object $payload Object containing details of the job.
|
||||
*/
|
||||
public function __construct($queue, $payload)
|
||||
{
|
||||
$this->queue = $queue;
|
||||
$this->payload = $payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new job and save it to the specified queue.
|
||||
*
|
||||
* @param string $queue The name of the queue to place the job in.
|
||||
* @param string $class The name of the class that contains the code to execute the job.
|
||||
* @param object $args Any optional arguments that should be passed when the job is executed. Pass as a class.
|
||||
* @param boolean $monitor Set to true to be able to monitor the status of a job.
|
||||
*/
|
||||
public static function create($queue, $class, $args = null, $monitor = false)
|
||||
{
|
||||
if($args !== null && !is_object($args)) {
|
||||
throw new InvalidArgumentException(
|
||||
'Supplied $args must be an object and an instance of stdClass.'
|
||||
);
|
||||
}
|
||||
$id = md5(uniqid('', true));
|
||||
Resque::push($queue, array(
|
||||
'class' => $class,
|
||||
'args' => $args,
|
||||
'id' => $id,
|
||||
));
|
||||
|
||||
if($monitor) {
|
||||
Resque_Job_Status::create($id);
|
||||
}
|
||||
|
||||
return $id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the next available job from the specified queue and return an
|
||||
* instance of Resque_Job for it.
|
||||
*
|
||||
* @param string $queue The name of the queue to check for a job in.
|
||||
* @return null|object Null when there aren't any waiting jobs, instance of Resque_Job when a job was found.
|
||||
*/
|
||||
public static function reserve($queue)
|
||||
{
|
||||
$payload = Resque::pop($queue);
|
||||
if(!$payload) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return new Resque_Job($queue, $payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the status of the current job.
|
||||
*
|
||||
* @param int $status Status constant from Resque_Job_Status indicating the current status of a job.
|
||||
*/
|
||||
public function updateStatus($status)
|
||||
{
|
||||
if(empty($this->payload->id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$statusInstance = new Resque_Job_Status($this->payload->id);
|
||||
$statusInstance->update($status);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the status of the current job.
|
||||
*
|
||||
* @return int The status of the job as one of the Resque_Job_Status constants.
|
||||
*/
|
||||
public function getStatus()
|
||||
{
|
||||
$status = new Resque_Job_Status($this->payload->id);
|
||||
return $status->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Actually execute a job by calling the perform method on the class
|
||||
* associated with the job with the supplied arguments.
|
||||
*
|
||||
* @throws Resque_Exception When the job's class could not be found or it does not contain a perform method.
|
||||
*/
|
||||
public function perform()
|
||||
{
|
||||
if(!class_exists($this->payload->class)) {
|
||||
throw new Resque_Exception(
|
||||
'Could not find job class ' . $this->payload->class . '.'
|
||||
);
|
||||
}
|
||||
|
||||
if(!method_exists($this->payload->class, 'perform')) {
|
||||
throw new Resque_Exception(
|
||||
'Job class ' . $this->payload->class . ' does not contain a perform method.'
|
||||
);
|
||||
}
|
||||
|
||||
call_user_func(array($this->payload->class, 'perform'), $this->payload->args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark the current job as having failed.
|
||||
*/
|
||||
public function fail($exception)
|
||||
{
|
||||
$this->updateStatus(Resque_Job_Status::STATUS_FAILED);
|
||||
require_once 'Failure.php';
|
||||
Resque_Failure::create(
|
||||
$this->payload,
|
||||
$exception,
|
||||
$this->worker,
|
||||
$this->queue
|
||||
);
|
||||
Resque_Stat::incr('failed');
|
||||
Resque_Stat::incr('failed:' . $this->worker);
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-queue the current job.
|
||||
*/
|
||||
public function recreate()
|
||||
{
|
||||
$status = new Resque_Job_Status($this->payload->id);
|
||||
$monitor = false;
|
||||
if($status->isTracking()) {
|
||||
$monitor = true;
|
||||
}
|
||||
|
||||
return self::create($this->queue, $this->payload->class, $this->payload->args, $monitor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a string representation used to describe the current job.
|
||||
*
|
||||
* @return string The string representation of the job.
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
$args = array();
|
||||
if(isset($this->payload->args)) {
|
||||
$args = $this->payload->args;
|
||||
foreach($args as $k => $v) {
|
||||
if(is_object($v)) {
|
||||
$args[$k] = '{' . get_class($v) . ' - '.implode(',', get_object_vars($v)) . '}';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$name = array(
|
||||
'Job{' . $this->queue .'}'
|
||||
);
|
||||
if(!empty($this->payload->id)) {
|
||||
$name[] = 'ID: ' . $this->payload->id;
|
||||
}
|
||||
$name[] = $this->payload->class;
|
||||
$name[] = implode(',', $args);
|
||||
return '(' . implode(' | ', $name) . ')';
|
||||
}
|
||||
}
|
||||
?>
|
13
lib/Resque/Job/DirtyExitException.php
Normal file
13
lib/Resque/Job/DirtyExitException.php
Normal file
|
@ -0,0 +1,13 @@
|
|||
<?php
|
||||
/**
|
||||
* Runtime exception class for a job that does not exit cleanly.
|
||||
*
|
||||
* @package Resque/Job
|
||||
* @author Chris Boulton <chris.boulton@interspire.com>
|
||||
* @copyright (c) 2010 Chris Boulton
|
||||
* @license http://www.opensource.org/licenses/mit-license.php
|
||||
*/
|
||||
class Resque_Job_DirtyExitException extends RuntimeException
|
||||
{
|
||||
|
||||
}
|
144
lib/Resque/Job/Status.php
Normal file
144
lib/Resque/Job/Status.php
Normal file
|
@ -0,0 +1,144 @@
|
|||
<?php
|
||||
/**
|
||||
* Status tracker/information for a job.
|
||||
*
|
||||
* @package Resque/Job
|
||||
* @author Chris Boulton <chris.boulton@interspire.com>
|
||||
* @copyright (c) 2010 Chris Boulton
|
||||
* @license http://www.opensource.org/licenses/mit-license.php
|
||||
*/
|
||||
class Resque_Job_Status
|
||||
{
|
||||
const STATUS_WAITING = 1;
|
||||
const STATUS_RUNNING = 2;
|
||||
const STATUS_FAILED = 3;
|
||||
const STATUS_COMPLETE = 4;
|
||||
|
||||
/**
|
||||
* @var string The ID of the job this status class refers back to.
|
||||
*/
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* @var mixed Cache variable if the status of this job is being monitored or not.
|
||||
* True/false when checked at least once or null if not checked yet.
|
||||
*/
|
||||
private $isTracking = null;
|
||||
|
||||
/**
|
||||
* @var array Array of statuses that are considered final/complete.
|
||||
*/
|
||||
private static $completeStatuses = array(
|
||||
self::STATUS_FAILED,
|
||||
self::STATUS_COMPLETE
|
||||
);
|
||||
|
||||
/**
|
||||
* Setup a new instance of the job monitor class for the supplied job ID.
|
||||
*
|
||||
* @param string $id The ID of the job to manage the status for.
|
||||
*/
|
||||
public function __construct($id)
|
||||
{
|
||||
$this->id = $id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new status monitor item for the supplied job ID. Will create
|
||||
* all necessary keys in Redis to monitor the status of a job.
|
||||
*
|
||||
* @param string $id The ID of the job to monitor the status of.
|
||||
*/
|
||||
public static function create($id)
|
||||
{
|
||||
$statusPacket = array(
|
||||
'status' => self::STATUS_WAITING,
|
||||
'updated' => time(),
|
||||
'started' => time(),
|
||||
);
|
||||
Resque::redis()->set('job:' . $id . ':status', json_encode($statusPacket));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if we're actually checking the status of the loaded job status
|
||||
* instance.
|
||||
*
|
||||
* @return boolean True if the status is being monitored, false if not.
|
||||
*/
|
||||
public function isTracking()
|
||||
{
|
||||
if($this->isTracking === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if(!Resque::redis()->exists((string)$this)) {
|
||||
$this->isTracking = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->isTracking = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the status indicator for the current job with a new status.
|
||||
*
|
||||
* @param int The status of the job (see constants in Resque_Job_Status)
|
||||
*/
|
||||
public function update($status)
|
||||
{
|
||||
if(!$this->isTracking()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$statusPacket = array(
|
||||
'status' => $status,
|
||||
'updated' => time(),
|
||||
);
|
||||
Resque::redis()->set((string)$this, json_encode($statusPacket));
|
||||
|
||||
// Expire the status for completed jobs after 24 hours
|
||||
if(in_array($status, self::$completeStatuses)) {
|
||||
Resque::redis()->expire((string)$this, 86400);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the status for the job being monitored.
|
||||
*
|
||||
* @return mixed False if the status is not being monitored, otherwise the status as
|
||||
* as an integer, based on the Resque_Job_Status constants.
|
||||
*/
|
||||
public function get()
|
||||
{
|
||||
if(!$this->isTracking()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$statusPacket = json_decode(Resque::redis()->get((string)$this));
|
||||
if(!$statusPacket) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $statusPacket->status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop tracking the status of a job.
|
||||
*/
|
||||
public function stop()
|
||||
{
|
||||
Resque::redis()->del((string)$this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a string representation of this object.
|
||||
*
|
||||
* @return string String representation of the current job status class.
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
return 'job:' . $this->id . ':status';
|
||||
}
|
||||
}
|
||||
?>
|
101
lib/Resque/Redis.php
Normal file
101
lib/Resque/Redis.php
Normal file
|
@ -0,0 +1,101 @@
|
|||
<?php
|
||||
// Third- party apps may have already loaded Resident from elsewhere
|
||||
// so lets be careful.
|
||||
if(!class_exists('Redisent')) {
|
||||
require_once dirname(__FILE__) . '/../Redisent/Redisent.php';
|
||||
}
|
||||
|
||||
/**
|
||||
* Extended Redisent class used by Resque for all communication with
|
||||
* redis. Essentially adds namespace support to Redisent.
|
||||
*
|
||||
* @package Resque/Redis
|
||||
* @author Chris Boulton <chris.boulton@interspire.com>
|
||||
* @copyright (c) 2010 Chris Boulton
|
||||
* @license http://www.opensource.org/licenses/mit-license.php
|
||||
*/
|
||||
class Resque_Redis extends Redisent
|
||||
{
|
||||
/**
|
||||
* @var array List of all commands in Redis that supply a key as their
|
||||
* first argument. Used to prefix keys with the Resque namespace.
|
||||
*/
|
||||
private $keyCommands = array(
|
||||
'exists',
|
||||
'del',
|
||||
'type',
|
||||
'keys',
|
||||
'expire',
|
||||
'ttl',
|
||||
'move',
|
||||
'set',
|
||||
'get',
|
||||
'getset',
|
||||
'setnx',
|
||||
'incr',
|
||||
'incrby',
|
||||
'decrby',
|
||||
'decrby',
|
||||
'rpush',
|
||||
'lpush',
|
||||
'llen',
|
||||
'lrange',
|
||||
'ltrim',
|
||||
'lindex',
|
||||
'lset',
|
||||
'lrem',
|
||||
'lpop',
|
||||
'rpop',
|
||||
'sadd',
|
||||
'srem',
|
||||
'spop',
|
||||
'scard',
|
||||
'sismember',
|
||||
'smembers',
|
||||
'srandmember',
|
||||
'zadd',
|
||||
'zrem',
|
||||
'zrange',
|
||||
'zrevrange',
|
||||
'zrangebyscore',
|
||||
'zcard',
|
||||
'zscore',
|
||||
'zremrangebyscore',
|
||||
'sort'
|
||||
);
|
||||
// sinterstore
|
||||
// sunion
|
||||
// sunionstore
|
||||
// sdiff
|
||||
// sdiffstore
|
||||
// sinter
|
||||
// smove
|
||||
// rename
|
||||
// rpoplpush
|
||||
// mget
|
||||
// msetnx
|
||||
// mset
|
||||
// renamenx
|
||||
|
||||
/**
|
||||
* Magic method to handle all function requests and prefix key based
|
||||
* operations with the 'resque:' key prefix.
|
||||
*
|
||||
* @param string $name The name of the method called.
|
||||
* @param array $args Array of supplied arguments to the method.
|
||||
* @return mixed Return value from Resident::call() based on the command.
|
||||
*/
|
||||
public function __call($name, $args) {
|
||||
$args = func_get_args();
|
||||
if(in_array($name, $this->keyCommands)) {
|
||||
$args[1][0] = 'resque:' . $args[1][0];
|
||||
}
|
||||
try {
|
||||
return parent::__call($name, $args[1]);
|
||||
}
|
||||
catch(RedisException $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
?>
|
57
lib/Resque/Stat.php
Normal file
57
lib/Resque/Stat.php
Normal file
|
@ -0,0 +1,57 @@
|
|||
<?php
|
||||
/**
|
||||
* Resque statistic management (jobs processed, failed, etc)
|
||||
*
|
||||
* @package Resque/Stat
|
||||
* @author Chris Boulton <chris.boulton@interspire.com>
|
||||
* @copyright (c) 2010 Chris Boulton
|
||||
* @license http://www.opensource.org/licenses/mit-license.php
|
||||
*/
|
||||
class Resque_Stat
|
||||
{
|
||||
/**
|
||||
* Get the value of the supplied statistic counter for the specified statistic.
|
||||
*
|
||||
* @param string $stat The name of the statistic to get the stats for.
|
||||
* @return mixed Value of the statistic.
|
||||
*/
|
||||
public function get($stat)
|
||||
{
|
||||
return (int)Resque::redis()->get('stat:' . $stat);
|
||||
}
|
||||
|
||||
/**
|
||||
* Increment the value of the specified statistic by a certain amount (default is 1)
|
||||
*
|
||||
* @param string $stat The name of the statistic to increment.
|
||||
* @param int $by The amount to increment the statistic by.
|
||||
* @return boolean True if successful, false if not.
|
||||
*/
|
||||
public function incr($stat, $by = 1)
|
||||
{
|
||||
return (bool)Resque::redis()->incrby('stat:' . $stat, $by);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrement the value of the specified statistic by a certain amount (default is 1)
|
||||
*
|
||||
* @param string $stat The name of the statistic to decrement.
|
||||
* @param int $by The amount to decrement the statistic by.
|
||||
* @return boolean True if successful, false if not.
|
||||
*/
|
||||
public function decr($stat, $by = 1)
|
||||
{
|
||||
return (bool)Resque::redis()->decrby('stat:' . $stat, $by);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a statistic with the given name.
|
||||
*
|
||||
* @param string $stat The name of the statistic to delete.
|
||||
* @return boolean True if successful, false if not.
|
||||
*/
|
||||
public function clear($stat)
|
||||
{
|
||||
return (bool)Resque::redis()->del('stat:' . $stat);
|
||||
}
|
||||
}
|
564
lib/Resque/Worker.php
Normal file
564
lib/Resque/Worker.php
Normal file
|
@ -0,0 +1,564 @@
|
|||
<?php
|
||||
require_once 'Stat.php';
|
||||
require_once 'Job.php';
|
||||
require_once 'Job/DirtyExitException.php';
|
||||
|
||||
/**
|
||||
* Resque worker that handles checking queues for jobs, fetching them
|
||||
* off the queues, running them and handling the result.
|
||||
*
|
||||
* @package Resque/Worker
|
||||
* @author Chris Boulton <chris.boulton@interspire.com>
|
||||
* @copyright (c) 2010 Chris Boulton
|
||||
* @license http://www.opensource.org/licenses/mit-license.php
|
||||
*/
|
||||
class Resque_Worker
|
||||
{
|
||||
const LOG_NONE = 0;
|
||||
const LOG_NORMAL = 1;
|
||||
const LOG_VERBOSE = 2;
|
||||
|
||||
/**
|
||||
* @var int Current log level of this worker.
|
||||
*/
|
||||
public $logLevel = 0;
|
||||
|
||||
/**
|
||||
* @var array Array of all associated queues for this worker.
|
||||
*/
|
||||
private $queues = array();
|
||||
|
||||
/**
|
||||
* @var string The hostname of this worker.
|
||||
*/
|
||||
private $hostname;
|
||||
|
||||
/**
|
||||
* @var boolean True if on the next iteration, the worker should shutdown.
|
||||
*/
|
||||
private $shutdown = false;
|
||||
|
||||
/**
|
||||
* @var boolean True if this worker is paused.
|
||||
*/
|
||||
private $paused = false;
|
||||
|
||||
/**
|
||||
* @var string String identifying this worker.
|
||||
*/
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* @var Resque_Job Current job, if any, being processed by this worker.
|
||||
*/
|
||||
private $currentJob = null;
|
||||
|
||||
/**
|
||||
* Return all workers known to Resque as instantiated instances.
|
||||
*/
|
||||
public static function all()
|
||||
{
|
||||
$workers = Resque::redis()->smembers('workers');
|
||||
if(!is_array($workers)) {
|
||||
$workers = array();
|
||||
}
|
||||
|
||||
$instances = array();
|
||||
foreach($workers as $workerId) {
|
||||
$instances[] = self::find($workerId);
|
||||
}
|
||||
return $instances;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a worker ID, check if it is registered/valid.
|
||||
*
|
||||
* @param string $workerId ID of the worker.
|
||||
* @return boolean True if the worker exists, false if not.
|
||||
*/
|
||||
public static function exists($workerId)
|
||||
{
|
||||
return (bool)Resque::redis()->sismember('workers', $workerId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a worker ID, find it and return an instantiated worker class for it.
|
||||
*
|
||||
* @param string $workerId The ID of the worker.
|
||||
* @return Resque_Worker Instance of the worker. False if the worker does not exist.
|
||||
*/
|
||||
public static function find($workerId)
|
||||
{
|
||||
if(!self::exists($workerId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
list($hostname, $pid, $queues) = explode(':', $workerId, 3);
|
||||
$queues = explode(',', $queues);
|
||||
$worker = new self($queues);
|
||||
$worker->setId($workerId);
|
||||
return $worker;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the ID of this worker to a given ID string.
|
||||
*
|
||||
* @param string $workerId ID for the worker.
|
||||
*/
|
||||
public function setId($workerId)
|
||||
{
|
||||
$this->id = $workerId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiate a new worker, given a list of queues that it should be working
|
||||
* on. The list of queues should be supplied in the priority that they should
|
||||
* be checked for jobs (first come, first served)
|
||||
*
|
||||
* Passing a single '*' allows the worker to work on all queues in alphabetical
|
||||
* order. You can easily add new queues dynamically and have them worked on using
|
||||
* this method.
|
||||
*
|
||||
* @param string|array $queues String with a single queue name, array with multiple.
|
||||
*/
|
||||
public function __construct($queues)
|
||||
{
|
||||
if(!is_array($queues)) {
|
||||
$queues = array($queues);
|
||||
}
|
||||
|
||||
$this->queues = $queues;
|
||||
if(function_exists('gethostname')) {
|
||||
$hostname = gethostname();
|
||||
}
|
||||
else {
|
||||
$hostname = php_uname('n');
|
||||
}
|
||||
$this->hostname = $hostname;
|
||||
$this->id = $this->hostname . ':'.getmypid() . ':' . implode(',', $this->queues);
|
||||
}
|
||||
|
||||
/**
|
||||
* The primary loop for a worker which when called on an instance starts
|
||||
* the worker's life cycle.
|
||||
*
|
||||
* Queues are checked every $interval (seconds) for new jobs.
|
||||
*
|
||||
* @param int $interval How often to check for new jobs across the queues.
|
||||
*/
|
||||
public function work($interval = 5)
|
||||
{
|
||||
$this->updateProcLine('Starting');
|
||||
$this->startup();
|
||||
|
||||
while(true) {
|
||||
if($this->shutdown) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Attempt to find and reserve a job
|
||||
$job = false;
|
||||
if(!$this->paused) {
|
||||
$job = $this->reserve();
|
||||
}
|
||||
|
||||
if(!$job) {
|
||||
// For an interval of 0, break now - helps with unit testing etc
|
||||
if($interval == 0) {
|
||||
break;
|
||||
}
|
||||
// If no job was found, we sleep for $interval before continuing and checking again
|
||||
$this->log('Sleeping for ' . $interval, true);
|
||||
if($this->paused) {
|
||||
$this->updateProcLine('Paused');
|
||||
}
|
||||
else {
|
||||
$this->updateProcLine('Waiting for ' . implode(',', $this->queues));
|
||||
}
|
||||
sleep($interval);
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->log('got ' . $job);
|
||||
$this->workingOn($job);
|
||||
|
||||
$this->child = $this->fork();
|
||||
|
||||
// Forked and we're the child. Run the job.
|
||||
if($this->child === 0 || $this->child === false) {
|
||||
$status = 'Processing ' . $job->queue . ' since ' . strftime('%F %T');
|
||||
$this->updateProcLine($status);
|
||||
$this->log($status, self::LOG_VERBOSE);
|
||||
$this->perform($job);
|
||||
if($this->child === 0) {
|
||||
exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
if($this->child > 0) {
|
||||
// Parent process, sit and wait
|
||||
$status = 'Forked ' . $this->child . ' at ' . strftime('%F %T');
|
||||
$this->updateProcLine($status);
|
||||
$this->log($status, self::LOG_VERBOSE);
|
||||
|
||||
// Wait until the child process finishes before continuing
|
||||
pcntl_wait($status);
|
||||
$exitStatus = pcntl_wexitstatus($status);
|
||||
if($exitStatus !== 0) {
|
||||
$job->fail(new Resque_Job_DirtyExitException(
|
||||
'Job exited with exit code ' . $exitStatus
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
$this->child = null;
|
||||
$this->doneWorking();
|
||||
}
|
||||
|
||||
$this->unregisterWorker();
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a single job.
|
||||
*
|
||||
* @param object|null $job The job to be processed.
|
||||
*/
|
||||
public function perform(Resque_Job $job)
|
||||
{
|
||||
try {
|
||||
$job->perform();
|
||||
}
|
||||
catch(Exception $e) {
|
||||
$this->log($job . ' failed: ' . $e->getMessage());
|
||||
$job->fail($e);
|
||||
return;
|
||||
}
|
||||
|
||||
$job->updateStatus(Resque_Job_Status::STATUS_COMPLETE);
|
||||
$this->log('done ' . $job);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to find a job from the top of one of the queues for this worker.
|
||||
*
|
||||
* @return object|boolean Instance of Resque_Job if a job is found, false if not.
|
||||
*/
|
||||
public function reserve()
|
||||
{
|
||||
$queues = $this->queues();
|
||||
if(!is_array($queues)) {
|
||||
return;
|
||||
}
|
||||
foreach($queues as $queue) {
|
||||
$this->log('Checking ' . $queue, self::LOG_VERBOSE);
|
||||
$job = Resque_Job::reserve($queue);
|
||||
if($job) {
|
||||
$this->log('Found job on ' . $queue, self::LOG_VERBOSE);
|
||||
return $job;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an array containing all of the queues that this worker should use
|
||||
* when searching for jobs.
|
||||
*
|
||||
* If * is found in the list of queues, every queue will be searched in
|
||||
* alphabetic order.
|
||||
*
|
||||
* @return array Array of associated queues.
|
||||
*/
|
||||
public function queues()
|
||||
{
|
||||
if(!in_array('*', $this->queues)) {
|
||||
return $this->queues;
|
||||
}
|
||||
|
||||
$queues = Resque::queues();
|
||||
sort($queues);
|
||||
return $queues;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to fork a child process from the parent to run a job in.
|
||||
*
|
||||
* Return values are those of pcntl_fork().
|
||||
*
|
||||
* @return int -1 if the fork failed, 0 for the forked child, the PID of the child for the parent.
|
||||
*/
|
||||
private function fork()
|
||||
{
|
||||
if(!function_exists('pcntl_fork')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$pid = pcntl_fork();
|
||||
if($pid === -1) {
|
||||
throw new RuntimeException('Unable to fork child worker.');
|
||||
}
|
||||
|
||||
return $pid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform necessary actions to start a worker.
|
||||
*/
|
||||
private function startup()
|
||||
{
|
||||
$this->registerSigHandlers();
|
||||
$this->pruneDeadWorkers();
|
||||
$this->registerWorker();
|
||||
}
|
||||
|
||||
/**
|
||||
* On supported systems (with the PECL proctitle module installed), update
|
||||
* the name of the currently running process to indicate the current state
|
||||
* of a worker.
|
||||
*
|
||||
* @param string $status The updated process title.
|
||||
*/
|
||||
private function updateProcLine($status)
|
||||
{
|
||||
if(function_exists('setproctitle')) {
|
||||
setproctitle('resque-' . Resque::VERSION . ': ' . $status);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register signal handlers that a worker should respond to.
|
||||
*
|
||||
* TERM: Shutdown immediately and stop processing jobs.
|
||||
* INT: Shutdown immediately and stop processing jobs.
|
||||
* QUIT: Shutdown after the current job finishes processing.
|
||||
* USR1: Kill the forked child immediately and continue processing jobs.
|
||||
*/
|
||||
private function registerSigHandlers()
|
||||
{
|
||||
if(!function_exists('pcntl_signal')) {
|
||||
return;
|
||||
}
|
||||
|
||||
declare(ticks = 1);
|
||||
pcntl_signal(SIGTERM, array($this, 'shutDownNow'));
|
||||
pcntl_signal(SIGINT, array($this, 'shutDownNow'));
|
||||
pcntl_signal(SIGQUIT, array($this, 'shutdown'));
|
||||
pcntl_signal(SIGUSR1, array($this, 'killChild'));
|
||||
pcntl_signal(SIGUSR2, array($this, 'pauseProcessing'));
|
||||
pcntl_signal(SIGCONT, array($this, 'unPauseProcessing'));
|
||||
$this->log('Registered signals', self::LOG_VERBOSE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Signal handler callback for USR2, pauses processing of new jobs.
|
||||
*/
|
||||
public function pauseProcessing()
|
||||
{
|
||||
$this->log('USR2 received; pausing job processing');
|
||||
$this->paused = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Signal handler callback for CONT, resumes worker allowing it to pick
|
||||
* up new jobs.
|
||||
*/
|
||||
public function unPauseProcessing()
|
||||
{
|
||||
$this->log('CONT received; resuming job processing');
|
||||
$this->paused = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule a worker for shutdown. Will finish processing the current job
|
||||
* and when the timeout interval is reached, the worker will shut down.
|
||||
*/
|
||||
public function shutdown()
|
||||
{
|
||||
$this->shutdown = true;
|
||||
$this->log('Exiting...');
|
||||
}
|
||||
|
||||
/**
|
||||
* Force an immediate shutdown of the worker, killing any child jobs
|
||||
* currently running.
|
||||
*/
|
||||
public function shutdownNow()
|
||||
{
|
||||
$this->shutdown();
|
||||
$this->killChild();
|
||||
}
|
||||
|
||||
/**
|
||||
* Kill a forked child job immediately. The job it is processing will not
|
||||
* be completed.
|
||||
*/
|
||||
public function killChild()
|
||||
{
|
||||
if(!$this->child) {
|
||||
$this->log('No child to kill.', self::LOG_VERBOSE);
|
||||
return;
|
||||
}
|
||||
|
||||
$this->log('Killing child at ' . $this->child, self::LOG_VERBOSE);
|
||||
if(exec('ps -o pid,state -p ' . $this->child, $output, $returnCode) && $returnCode != 1) {
|
||||
$this->log('Killing child at ' . $this->child, self::LOG_VERBOSE);
|
||||
posix_kill($this->child, SIGKILL);
|
||||
$this->child = null;
|
||||
}
|
||||
else {
|
||||
$this->log('Child ' . $this->child . ' not found, restarting.', self::LOG_VERBOSE);
|
||||
$this->shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Look for any workers which should be running on this server and if
|
||||
* they're not, remove them from Redis.
|
||||
*
|
||||
* This is a form of garbage collection to handle cases where the
|
||||
* server may have been killed and the Resque workers did not die gracefully
|
||||
* and therefore leave state information in Redis.
|
||||
*/
|
||||
public function pruneDeadWorkers()
|
||||
{
|
||||
$workerPids = $this->workerPids();
|
||||
if(empty($workerPids)) {
|
||||
continue;
|
||||
}
|
||||
$workers = self::all();
|
||||
foreach($workers as $worker) {
|
||||
list($host, $pid, $queues) = explode(':', (string)$worker, 3);
|
||||
if($host != $this->hostname || in_array($pid, $workerPids) || $pid == getmypid()) {
|
||||
continue;
|
||||
}
|
||||
$this->log('Pruning dead worker: ' . (string)$worker, self::LOG_VERBOSE);
|
||||
$worker->unregisterWorker();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an array of process IDs for all of the Resque workers currently
|
||||
* running on this machine.
|
||||
*
|
||||
* @return array Array of Resque worker process IDs.
|
||||
*/
|
||||
public function workerPids()
|
||||
{
|
||||
$pids = array();
|
||||
exec('ps -A -o pid,command | grep [r]esque', $cmdOutput);
|
||||
foreach($cmdOutput as $line) {
|
||||
list($pids[],) = explode(' ', $line, 2);
|
||||
}
|
||||
return $pids;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register this worker in Redis.
|
||||
*/
|
||||
public function registerWorker()
|
||||
{
|
||||
Resque::redis()->sadd('workers', $this);
|
||||
Resque::redis()->set('worker:' . (string)$this . ':started', strftime('%a %b %d %H:%M:%S %Z %Y'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister this worker in Redis. (shutdown etc)
|
||||
*/
|
||||
public function unregisterWorker()
|
||||
{
|
||||
if(is_object($this->currentJob)) {
|
||||
$this->currentJob->fail(new Resque_Job_DirtyExitException);
|
||||
}
|
||||
|
||||
$id = (string)$this;
|
||||
Resque::redis()->srem('workers', $id);
|
||||
Resque::redis()->del('worker:' . $id);
|
||||
Resque::redis()->del('worker:' . $id . ':started');
|
||||
Resque_Stat::clear('processed:' . $id);
|
||||
Resque_Stat::clear('failed:' . $id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tell Redis which job we're currently working on.
|
||||
*
|
||||
* @param object $job Resque_Job instance containing the job we're working on.
|
||||
*/
|
||||
public function workingOn(Resque_Job $job)
|
||||
{
|
||||
$job->worker = $this;
|
||||
$this->currentJob = $job;
|
||||
$job->updateStatus(Resque_Job_Status::STATUS_RUNNING);
|
||||
$data = json_encode(array(
|
||||
'queue' => $job->queue,
|
||||
'run_at' => strftime('%a %b %d %H:%M:%S %Z %Y'),
|
||||
'payload' => $job->payload
|
||||
));
|
||||
Resque::redis()->set('worker:' . $job->worker, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify Redis that we've finished working on a job, clearing the working
|
||||
* state and incrementing the job stats.
|
||||
*/
|
||||
public function doneWorking()
|
||||
{
|
||||
$this->currentJob = null;
|
||||
Resque_Stat::incr('processed');
|
||||
Resque_Stat::incr('processed:' . (string)$this);
|
||||
Resque::redis()->del('worker:' . (string)$this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a string representation of this worker.
|
||||
*
|
||||
* @return string String identifier for this worker instance.
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Output a given log message to STDOUT.
|
||||
*
|
||||
* @param string $message Message to output.
|
||||
*/
|
||||
public function log($message)
|
||||
{
|
||||
if($this->logLevel == self::LOG_NORMAL) {
|
||||
fwrite(STDOUT, "*** " . $message . "\n");
|
||||
}
|
||||
else if($this->logLevel == self::LOG_VERBOSE) {
|
||||
fwrite(STDOUT, "** [" . strftime('%T %Y-%m-%d') . "] " . $message . "\n");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an object describing the job this worker is currently working on.
|
||||
*
|
||||
* @return object Object with details of current job.
|
||||
*/
|
||||
public function job()
|
||||
{
|
||||
$job = Resque::redis()->get('worker:' . $this);
|
||||
if(!$job) {
|
||||
return new stdClass;
|
||||
}
|
||||
else {
|
||||
return json_decode($job);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a statistic belonging to this worker.
|
||||
*
|
||||
* @param string $stat Statistic to fetch.
|
||||
* @return int Statistic value.
|
||||
*/
|
||||
public function getStat($stat)
|
||||
{
|
||||
return Resque_Stat::get($stat . ':' . $this);
|
||||
}
|
||||
}
|
||||
?>
|
Loading…
Add table
Add a link
Reference in a new issue