├── .gitignore ├── .travis.yml ├── demo ├── long_job.php ├── php_error_job.php ├── bad_job.php ├── job.php ├── resque.php ├── queue.php └── check_status.php ├── test ├── misc │ └── redis.conf └── Resque │ └── Tests │ ├── TestCase.php │ ├── StatTest.php │ ├── JobStatusTest.php │ ├── bootstrap.php │ ├── EventTest.php │ ├── JobTest.php │ └── WorkerTest.php ├── extras ├── resque.logrotate ├── resque.monit └── sample-plugin.php ├── lib ├── Resque │ ├── Exception.php │ ├── Job │ │ ├── DontPerform.php │ │ ├── DirtyExitException.php │ │ └── Status.php │ ├── Failure │ │ ├── Interface.php │ │ └── Redis.php │ ├── Stat.php │ ├── Failure.php │ ├── Event.php │ ├── RedisCluster.php │ ├── Redis.php │ ├── Job.php │ └── Worker.php ├── Redisent │ ├── LICENSE │ ├── README.markdown │ ├── RedisentCluster.php │ └── Redisent.php └── Resque.php ├── phpunit.xml ├── LICENSE ├── composer.json ├── CHANGELOG.md ├── bin └── resque └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /composer.phar 2 | /vendor/ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | php: 3 | - "5.5" 4 | - "5.6" 5 | - "7.0" 6 | before_script: 7 | - composer install -------------------------------------------------------------------------------- /demo/long_job.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /demo/php_error_job.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /demo/bad_job.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /demo/job.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/misc/redis.conf: -------------------------------------------------------------------------------- 1 | daemonize yes 2 | pidfile ./redis.pid 3 | port 6379 4 | bind 127.0.0.1 5 | timeout 300 6 | dbfilename dump.rdb 7 | dir ./ 8 | loglevel debug -------------------------------------------------------------------------------- /demo/resque.php: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /extras/resque.logrotate: -------------------------------------------------------------------------------- 1 | /var/log/resque/*.log { 2 | daily 3 | missingok 4 | rotate 7 5 | compress 6 | compressoptions -4 7 | notifempty 8 | create 640 root adm 9 | copytruncate 10 | } -------------------------------------------------------------------------------- /lib/Resque/Exception.php: -------------------------------------------------------------------------------- 1 | 7 | * @license http://www.opensource.org/licenses/mit-license.php 8 | */ 9 | class Resque_Exception extends Exception 10 | { 11 | } 12 | ?> -------------------------------------------------------------------------------- /lib/Resque/Job/DontPerform.php: -------------------------------------------------------------------------------- 1 | 7 | * @license http://www.opensource.org/licenses/mit-license.php 8 | */ 9 | class Resque_Job_DontPerform extends Exception 10 | { 11 | 12 | } -------------------------------------------------------------------------------- /lib/Resque/Job/DirtyExitException.php: -------------------------------------------------------------------------------- 1 | 7 | * @license http://www.opensource.org/licenses/mit-license.php 8 | */ 9 | class Resque_Job_DirtyExitException extends RuntimeException 10 | { 11 | 12 | } -------------------------------------------------------------------------------- /demo/queue.php: -------------------------------------------------------------------------------- 1 | time(), 12 | 'array' => array( 13 | 'test' => 'test', 14 | ), 15 | ); 16 | 17 | $jobId = Resque::enqueue('default', $argv[1], $args, true); 18 | echo "Queued job ".$jobId."\n\n"; 19 | ?> -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 13 | ./test/Resque/ 14 | 15 | 16 | -------------------------------------------------------------------------------- /demo/check_status.php: -------------------------------------------------------------------------------- 1 | isTracking()) { 13 | die("Resque is not tracking the status of this job.\n"); 14 | } 15 | 16 | echo "Tracking status of ".$argv[1].". Press [break] to stop.\n\n"; 17 | while(true) { 18 | fwrite(STDOUT, "Status of ".$argv[1]." is: ".$status->get()."\n"); 19 | sleep(1); 20 | } 21 | ?> 22 | -------------------------------------------------------------------------------- /test/Resque/Tests/TestCase.php: -------------------------------------------------------------------------------- 1 | 10 | * @license http://www.opensource.org/licenses/mit-license.php 11 | */ 12 | class Resque_Tests_TestCase extends TestCase 13 | { 14 | protected $resque; 15 | protected $redis; 16 | 17 | public function setUp(): void { 18 | $config = file_get_contents(REDIS_CONF); 19 | preg_match('#^\s*port\s+([0-9]+)#m', $config, $matches); 20 | $this->redis = new RedisApi('localhost', $matches[1]); 21 | $this->redis->prefix(REDIS_NAMESPACE); 22 | $this->redis->select(REDIS_DATABASE); 23 | 24 | 25 | // Flush redis 26 | $this->redis->flushAll(); 27 | } 28 | } -------------------------------------------------------------------------------- /extras/resque.monit: -------------------------------------------------------------------------------- 1 | # Borrowed with modifications from 2 | # https://github.com/defunkt/resque/blob/master/examples/monit/resque.monit 3 | # Replace these with your own: 4 | # [QUEUE] 5 | # [PATH/TO/RESQUE] 6 | # [UID] 7 | # [GID] 8 | # [APP_INCLUDE] 9 | 10 | check process resque_worker_[QUEUE] 11 | with pidfile /var/run/resque/worker_[QUEUE].pid 12 | start program = "/bin/sh -c 'APP_INCLUDE=[APP_INCLUDE] QUEUE=[QUEUE] VERBOSE=1 PIDFILE=/var/run/resque/worker_[QUEUE].pid nohup php -f [PATH/TO/RESQUE]/resque.php > /var/log/resque/worker_[QUEUE].log &'" as uid [UID] and gid [GID] 13 | stop program = "/bin/sh -c 'kill -s QUIT `cat /var/run/resque/worker_[QUEUE].pid` && rm -f /var/run/resque/worker_[QUEUE].pid; exit 0;'" 14 | if totalmem is greater than 300 MB for 10 cycles then restart # eating up memory? 15 | group resque_workers -------------------------------------------------------------------------------- /lib/Resque/Failure/Interface.php: -------------------------------------------------------------------------------- 1 | 7 | * @license http://www.opensource.org/licenses/mit-license.php 8 | */ 9 | interface Resque_Failure_Interface 10 | { 11 | /** 12 | * Initialize a failed job class and save it (where appropriate). 13 | * 14 | * @param object $payload Object containing details of the failed job. 15 | * @param object $exception Instance of the exception that was thrown by the failed job. 16 | * @param object $worker Instance of Resque_Worker that received the job. 17 | * @param string $queue The name of the queue the job was fetched from. 18 | */ 19 | public function __construct($payload, $exception, $worker, $queue); 20 | 21 | /** 22 | * Return details about a failed jobs 23 | * @param string job Id 24 | * @return object Object containing details of the failed job. 25 | */ 26 | static public function get($jobId); 27 | } 28 | ?> -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (c) Chris Boulton 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /lib/Redisent/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009 Justin Poliey 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kamisama/php-resque-ex", 3 | "type": "library", 4 | "description": "Redis backed library for creating background jobs and processing them later. PHP port based on resque for Ruby.", 5 | "keywords": ["job", "background", "redis", "resque", "queue", "php"], 6 | "homepage": "http://www.github.com/kamisama/php-resque-ex/", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "Wan Chen", 11 | "email": "kami@kamisama.me", 12 | "homepage": "http://www.kamisama.me" 13 | }, 14 | { 15 | "name": "Chris Boulton", 16 | "email": "chris@bigcommerce.com" 17 | } 18 | ], 19 | "require": { 20 | "php": ">=8.1", 21 | "ext-pcntl": "*", 22 | "monolog/monolog": ">=1.2.0", 23 | "kamisama/monolog-init": ">=0.1.1" 24 | }, 25 | "require-dev": { 26 | "phpunit/phpunit": "9.5.27" 27 | }, 28 | "autoload": { 29 | "psr-0": { 30 | "Resque": "lib" 31 | } 32 | }, 33 | "suggest": { 34 | "fresque/fresque": "A command line tool to manage your workers" 35 | }, 36 | "support": { 37 | "email": "kami@kamisama.me", 38 | "issues": "https://github.com/kamisama/php-resque-ex/issues", 39 | "source": "https://github.com/kamisama/php-resque-ex" 40 | }, 41 | "bin": [ 42 | "bin/resque" 43 | ], 44 | "conflict": { 45 | "chrisboulton/php-resque": "*" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /lib/Resque/Failure/Redis.php: -------------------------------------------------------------------------------- 1 | 7 | * @license http://www.opensource.org/licenses/mit-license.php 8 | */ 9 | 10 | class Resque_Failure_Redis implements Resque_Failure_Interface 11 | { 12 | /** 13 | * Initialize a failed job class and save it (where appropriate). 14 | * 15 | * @param object $payload Object containing details of the failed job. 16 | * @param object $exception Instance of the exception that was thrown by the failed job. 17 | * @param object $worker Instance of Resque_Worker that received the job. 18 | * @param string $queue The name of the queue the job was fetched from. 19 | */ 20 | public function __construct($payload, $exception, $worker, $queue) 21 | { 22 | $data = array(); 23 | $data['failed_at'] = (new DateTimeImmutable())->format('D M d H:i:s e Y'); 24 | $data['payload'] = $payload; 25 | $data['exception'] = get_class($exception); 26 | $data['error'] = $exception->getMessage(); 27 | $data['backtrace'] = explode("\n", $exception->getTraceAsString()); 28 | $data['worker'] = (string)$worker; 29 | $data['queue'] = $queue; 30 | Resque::Redis()->setex(Resque::FAILED_PREFIX . $payload['id'], 3600*14, serialize($data)); 31 | } 32 | 33 | static public function get($jobId) 34 | { 35 | $data = Resque::Redis()->get(Resque::FAILED_PREFIX . $jobId); 36 | return unserialize($data); 37 | } 38 | } 39 | ?> -------------------------------------------------------------------------------- /test/Resque/Tests/StatTest.php: -------------------------------------------------------------------------------- 1 | 9 | * @license http://www.opensource.org/licenses/mit-license.php 10 | */ 11 | class Resque_Tests_StatTest extends Resque_Tests_TestCase 12 | { 13 | public function testStatCanBeIncremented() 14 | { 15 | Resque_Stat::incr('test_incr'); 16 | Resque_Stat::incr('test_incr'); 17 | $this->assertEquals(2, $this->redis->get('stat:test_incr')); 18 | } 19 | 20 | public function testStatCanBeIncrementedByX() 21 | { 22 | Resque_Stat::incr('test_incrX', 10); 23 | Resque_Stat::incr('test_incrX', 11); 24 | $this->assertEquals(21, $this->redis->get('stat:test_incrX')); 25 | } 26 | 27 | public function testStatCanBeDecremented() 28 | { 29 | Resque_Stat::incr('test_decr', 22); 30 | Resque_Stat::decr('test_decr'); 31 | $this->assertEquals(21, $this->redis->get('stat:test_decr')); 32 | } 33 | 34 | public function testStatCanBeDecrementedByX() 35 | { 36 | Resque_Stat::incr('test_decrX', 22); 37 | Resque_Stat::decr('test_decrX', 11); 38 | $this->assertEquals(11, $this->redis->get('stat:test_decrX')); 39 | } 40 | 41 | public function testGetStatByName() 42 | { 43 | Resque_Stat::incr('test_get', 100); 44 | $this->assertEquals(100, Resque_Stat::get('test_get')); 45 | } 46 | 47 | public function testGetUnknownStatReturns0() 48 | { 49 | $this->assertEquals(0, Resque_Stat::get('test_get_unknown')); 50 | } 51 | } -------------------------------------------------------------------------------- /extras/sample-plugin.php: -------------------------------------------------------------------------------- 1 | queues(false)) . "\n"; 22 | } 23 | 24 | public static function beforeFork($job) 25 | { 26 | echo "Just about to fork to run " . $job; 27 | } 28 | 29 | public static function afterFork($job) 30 | { 31 | echo "Forked to run " . $job . ". This is the child process.\n"; 32 | } 33 | 34 | public static function beforePerform($job) 35 | { 36 | echo "Cancelling " . $job . "\n"; 37 | // throw new Resque_Job_DontPerform; 38 | } 39 | 40 | public static function afterPerform($job) 41 | { 42 | echo "Just performed " . $job . "\n"; 43 | } 44 | 45 | public static function onFailure($exception, $job) 46 | { 47 | echo $job . " threw an exception:\n" . $exception; 48 | } 49 | } -------------------------------------------------------------------------------- /lib/Resque/Stat.php: -------------------------------------------------------------------------------- 1 | 7 | * @license http://www.opensource.org/licenses/mit-license.php 8 | */ 9 | class Resque_Stat 10 | { 11 | /** 12 | * Get the value of the supplied statistic counter for the specified statistic. 13 | * 14 | * @param string $stat The name of the statistic to get the stats for. 15 | * @return mixed Value of the statistic. 16 | */ 17 | public static function get($stat) 18 | { 19 | return (int)Resque::redis()->get('stat:' . $stat); 20 | } 21 | 22 | /** 23 | * Increment the value of the specified statistic by a certain amount (default is 1) 24 | * 25 | * @param string $stat The name of the statistic to increment. 26 | * @param int $by The amount to increment the statistic by. 27 | * @return boolean True if successful, false if not. 28 | */ 29 | public static function incr($stat, $by = 1) 30 | { 31 | return (bool)Resque::redis()->incrby('stat:' . $stat, $by); 32 | } 33 | 34 | /** 35 | * Decrement the value of the specified statistic by a certain amount (default is 1) 36 | * 37 | * @param string $stat The name of the statistic to decrement. 38 | * @param int $by The amount to decrement the statistic by. 39 | * @return boolean True if successful, false if not. 40 | */ 41 | public static function decr($stat, $by = 1) 42 | { 43 | return (bool)Resque::redis()->decrby('stat:' . $stat, $by); 44 | } 45 | 46 | /** 47 | * Delete a statistic with the given name. 48 | * 49 | * @param string $stat The name of the statistic to delete. 50 | * @return boolean True if successful, false if not. 51 | */ 52 | public static function clear($stat) 53 | { 54 | return (bool)Resque::redis()->del('stat:' . $stat); 55 | } 56 | } -------------------------------------------------------------------------------- /lib/Resque/Failure.php: -------------------------------------------------------------------------------- 1 | 9 | * @license http://www.opensource.org/licenses/mit-license.php 10 | */ 11 | class Resque_Failure 12 | { 13 | /** 14 | * @var string Class name representing the backend to pass failed jobs off to. 15 | */ 16 | private static $backend; 17 | 18 | /** 19 | * Create a new failed job on the backend. 20 | * 21 | * @param object $payload The contents of the job that has just failed. 22 | * @param \Exception $exception The exception generated when the job failed to run. 23 | * @param \Resque_Worker $worker Instance of Resque_Worker that was running this job when it failed. 24 | * @param string $queue The name of the queue that this job was fetched from. 25 | */ 26 | public static function create($payload, Exception $exception, Resque_Worker $worker, $queue) 27 | { 28 | $backend = self::getBackend(); 29 | new $backend($payload, $exception, $worker, $queue); 30 | } 31 | 32 | /** 33 | * Return an instance of the backend for saving job failures. 34 | * 35 | * @return object Instance of backend object. 36 | */ 37 | public static function getBackend() 38 | { 39 | if(self::$backend === null) { 40 | require dirname(__FILE__) . '/Failure/Redis.php'; 41 | self::$backend = 'Resque_Failure_Redis'; 42 | } 43 | 44 | return self::$backend; 45 | } 46 | 47 | /** 48 | * Set the backend to use for raised job failures. The supplied backend 49 | * should be the name of a class to be instantiated when a job fails. 50 | * It is your responsibility to have the backend class loaded (or autoloaded) 51 | * 52 | * @param string $backend The class name of the backend to pipe failures to. 53 | */ 54 | public static function setBackend($backend) 55 | { 56 | self::$backend = $backend; 57 | } 58 | } -------------------------------------------------------------------------------- /lib/Resque/Event.php: -------------------------------------------------------------------------------- 1 | 7 | * @license http://www.opensource.org/licenses/mit-license.php 8 | */ 9 | class Resque_Event 10 | { 11 | /** 12 | * @var array Array containing all registered callbacks, indexked by event name. 13 | */ 14 | private static $events = array(); 15 | 16 | /** 17 | * Raise a given event with the supplied data. 18 | * 19 | * @param string $event Name of event to be raised. 20 | * @param mixed $data Optional, any data that should be passed to each callback. 21 | * @return true 22 | */ 23 | public static function trigger($event, $data = null) 24 | { 25 | if (!is_array($data)) { 26 | $data = array($data); 27 | } 28 | 29 | if (empty(self::$events[$event])) { 30 | return true; 31 | } 32 | 33 | foreach (self::$events[$event] as $callback) { 34 | if (!is_callable($callback)) { 35 | continue; 36 | } 37 | call_user_func_array($callback, $data); 38 | } 39 | 40 | return true; 41 | } 42 | 43 | /** 44 | * Listen in on a given event to have a specified callback fired. 45 | * 46 | * @param string $event Name of event to listen on. 47 | * @param mixed $callback Any callback callable by call_user_func_array. 48 | * @return true 49 | */ 50 | public static function listen($event, $callback) 51 | { 52 | if (!isset(self::$events[$event])) { 53 | self::$events[$event] = array(); 54 | } 55 | 56 | self::$events[$event][] = $callback; 57 | return true; 58 | } 59 | 60 | /** 61 | * Stop a given callback from listening on a specific event. 62 | * 63 | * @param string $event Name of event. 64 | * @param mixed $callback The callback as defined when listen() was called. 65 | * @return true 66 | */ 67 | public static function stopListening($event, $callback) 68 | { 69 | if (!isset(self::$events[$event])) { 70 | return true; 71 | } 72 | 73 | $key = array_search($callback, self::$events[$event]); 74 | if ($key !== false) { 75 | unset(self::$events[$event][$key]); 76 | } 77 | 78 | return true; 79 | } 80 | 81 | /** 82 | * Call all registered listeners. 83 | */ 84 | public static function clearListeners() 85 | { 86 | self::$events = array(); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /lib/Resque/RedisCluster.php: -------------------------------------------------------------------------------- 1 | 14 | * @license http://www.opensource.org/licenses/mit-license.php 15 | */ 16 | class Resque_RedisCluster extends RedisentCluster 17 | { 18 | /** 19 | * Redis namespace 20 | * @var string 21 | */ 22 | private static $defaultNamespace = 'resque:'; 23 | /** 24 | * @var array List of all commands in Redis that supply a key as their 25 | * first argument. Used to prefix keys with the Resque namespace. 26 | */ 27 | private $keyCommands = array( 28 | 'exists', 29 | 'del', 30 | 'type', 31 | 'keys', 32 | 'expire', 33 | 'ttl', 34 | 'move', 35 | 'set', 36 | 'get', 37 | 'getset', 38 | 'setnx', 39 | 'incr', 40 | 'incrby', 41 | 'decrby', 42 | 'decrby', 43 | 'rpush', 44 | 'lpush', 45 | 'llen', 46 | 'lrange', 47 | 'ltrim', 48 | 'lindex', 49 | 'lset', 50 | 'lrem', 51 | 'lpop', 52 | 'rpop', 53 | 'sadd', 54 | 'srem', 55 | 'spop', 56 | 'scard', 57 | 'sismember', 58 | 'smembers', 59 | 'srandmember', 60 | 'zadd', 61 | 'zrem', 62 | 'zrange', 63 | 'zrevrange', 64 | 'zrangebyscore', 65 | 'zcard', 66 | 'zscore', 67 | 'zremrangebyscore', 68 | 'sort' 69 | ); 70 | // sinterstore 71 | // sunion 72 | // sunionstore 73 | // sdiff 74 | // sdiffstore 75 | // sinter 76 | // smove 77 | // rename 78 | // rpoplpush 79 | // mget 80 | // msetnx 81 | // mset 82 | // renamenx 83 | 84 | /** 85 | * Set Redis namespace (prefix) default: resque 86 | * @param string $namespace 87 | */ 88 | public static function prefix($namespace) 89 | { 90 | if (strpos($namespace, ':') === false) { 91 | $namespace .= ':'; 92 | } 93 | self::$defaultNamespace = $namespace; 94 | } 95 | 96 | /** 97 | * Magic method to handle all function requests and prefix key based 98 | * operations with the '{self::$defaultNamespace}' key prefix. 99 | * 100 | * @param string $name The name of the method called. 101 | * @param array $args Array of supplied arguments to the method. 102 | * @return mixed Return value from Resident::call() based on the command. 103 | */ 104 | public function __call($name, $args) { 105 | $args = func_get_args(); 106 | if(in_array($name, $this->keyCommands)) { 107 | $args[1][0] = self::$defaultNamespace . $args[1][0]; 108 | } 109 | try { 110 | return parent::__call($name, $args[1]); 111 | } 112 | catch(RedisException $e) { 113 | return false; 114 | } 115 | } 116 | } 117 | ?> 118 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.3.0 (2014-01-28) 2 | 3 | * Fix #8: Performing a DB select when the DB is set to default '0' is not necessary and breaks Twemproxy 4 | * Fix #13: Added PIDFILE writing when child COUNT > 1 5 | * Fix #14: Add bin/resque to composer 6 | * Fix #17: Catch redis connection issue 7 | * Fix #24: Use getmypid to specify a persistent connection unique identifier 8 | * Add redis authentication support 9 | 10 | 11 | ## 1.2.7 (2013-10-15) 12 | 13 | * Include the file given by APP_INCLUDE as soon as possible in bin/resque 14 | 15 | ## 1.2.6 (2013-06-20) 16 | 17 | * Update composer dependencies 18 | 19 | ## 1.2.5 (2013-05-22) 20 | 21 | * Drop support of igbinary serializer in failed job trace 22 | * Use ISO-8601 formatted date in log 23 | * Drop .php extension in resque bin filename 24 | 25 | > If you're starting your workers manually, use `php bin/resque` instead of `php bin/resque.php` 26 | 27 | 28 | ## 1.2.4 (2013-04-141) ## 29 | 30 | * Fix #3 : Logging now honour verbose level 31 | 32 | ## 1.2.3 (2012-01-31) ## 33 | 34 | * Fix fatal error when updating job status 35 | 36 | ## 1.2.2 (2012-01-30) ## 37 | 38 | * Add missing autoloader path 39 | 40 | ## 1.2.1 (2012-01-30) ## 41 | 42 | * Moved top-level resque.php to bin folder 43 | * Detect composer autoloader up to 3 directory level, and fail gracefully if not found 44 | * Change some functions scope to allow inheritance 45 | 46 | 47 | ## 1.0.15 (2012-01-23) ## 48 | 49 | * Record job processing time 50 | 51 | ## 1.0.14 (2012-10-23) ## 52 | 53 | * Add method to get failed jobs details 54 | * Merge v1.2 from parent 55 | 56 | ## 1.0.13 (2012-10-17) ## 57 | 58 | * Pause and unpause events go into their own log category 59 | 60 | ## 1.0.12 (2012-10-14) ## 61 | 62 | * Check that `$logger` is not null before using 63 | 64 | ## 1.0.11 (2012-10-01) ## 65 | 66 | * Update Composer.json 67 | 68 | ## 1.0.10 (2012-09-27) ## 69 | 70 | * Update Composer.json 71 | 72 | 73 | ## 1.0.9 (2012-09-20) ## 74 | 75 | * Delegate all the MonologHandler creation to MonologInit. (requires a composer update). 76 | * Fix stop event that was not logged 77 | 78 | ## 1.0.8 (2012-09-19) ## 79 | 80 | * In start log, add a new fields for recording queues names 81 | 82 | ## 1.0.7 (2012-09-10) ## 83 | 84 | * Fix tests 85 | 86 | ## 1.0.6 (2012-09-10) ## 87 | 88 | * Merge latest commits from php-resque 89 | 90 | 91 | ## 1.0.5 (2012-08-29) ## 92 | 93 | * Add custom redis database and namespace support 94 | 95 | ## 1.0.4 (2012-08-29) ## 96 | 97 | * Job creation will be delegated to Resque_Job_Creator class if found 98 | * Use persistent connection to Redis 99 | 100 | ## 1.0.3 (2012-08-26) ## 101 | 102 | * Fix unknown self reference 103 | 104 | ## 1.0.2 (2012-08-22) ## 105 | 106 | * Don't use persistent connection to redis, because of segfault bug 107 | 108 | ## 1.0.1 (2012-08-21) ## 109 | 110 | * Output to STDOUT if no log Handler is defined 111 | 112 | ## 1.0.0 (2012-08-21) ## 113 | 114 | * Initial release -------------------------------------------------------------------------------- /lib/Redisent/README.markdown: -------------------------------------------------------------------------------- 1 | # Redisent 2 | 3 | Redisent is a simple, no-nonsense interface to the [Redis](http://code.google.com/p/redis/) key-value store for modest developers. 4 | Due to the way it is implemented, it is flexible and tolerant of changes to the Redis protocol. 5 | 6 | ## Getting to work 7 | 8 | If you're at all familiar with the Redis protocol and PHP objects, you've already mastered Redisent. 9 | All Redisent does is map the Redis protocol to a PHP object, abstract away the nitty-gritty, and make the return values PHP compatible. 10 | 11 | require 'redisent.php'; 12 | $redis = new Redisent('localhost'); 13 | $redis->set('awesome', 'absolutely'); 14 | echo sprintf('Is Redisent awesome? %s.\n', $redis->get('awesome')); 15 | 16 | You use the exact same command names, and the exact same argument order. **How wonderful.** How about a more complex example? 17 | 18 | require 'redisent.php'; 19 | $redis = new Redisent('localhost'); 20 | $redis->rpush('particles', 'proton'); 21 | $redis->rpush('particles', 'electron'); 22 | $redis->rpush('particles', 'neutron'); 23 | $particles = $redis->lrange('particles', 0, -1); 24 | $particle_count = $redis->llen('particles'); 25 | echo "

The {$particle_count} particles that make up atoms are:

"; 26 | echo ""; 31 | 32 | Be aware that Redis error responses will be wrapped in a RedisException class and thrown, so do be sure to use proper coding techniques. 33 | 34 | ## Clustering your servers 35 | 36 | Redisent also includes a way for developers to fully utilize the scalability of Redis with multiple servers and [consistent hashing](http://en.wikipedia.org/wiki/Consistent_hashing). 37 | Using the RedisentCluster class, you can use Redisent the same way, except that keys will be hashed across multiple servers. 38 | Here is how to set up a cluster: 39 | 40 | include 'redisent_cluster.php'; 41 | 42 | $cluster = new RedisentCluster(array( 43 | array('host' => '127.0.0.1', 'port' => 6379), 44 | array('host' => '127.0.0.1', 'port' => 6380) 45 | )); 46 | 47 | You can then use Redisent the way you normally would, i.e., `$cluster->set('key', 'value')` or `$cluster->lrange('particles', 0, -1)`. 48 | But what about when you need to use commands that are server specific and do not operate on keys? You can use routing, with the `RedisentCluster::to` method. 49 | To use routing, you need to assign a server an alias in the constructor of the Redis cluster. Aliases are not required on all servers, just the ones you want to be able to access directly. 50 | 51 | include 'redisent_cluster.php'; 52 | 53 | $cluster = new RedisentCluster(array( 54 | 'alpha' => array('host' => '127.0.0.1', 'port' => 6379), 55 | array('host' => '127.0.0.1', 'port' => 6380) 56 | )); 57 | 58 | Now there is an alias of the server running on 127.0.0.1:6379 called **alpha**, and can be interacted with like this: 59 | 60 | // get server info 61 | $cluster->to('alpha')->info(); 62 | 63 | Now you have complete programatic control over your Redis servers. 64 | 65 | ## About 66 | 67 | © 2009 [Justin Poliey](http://justinpoliey.com) -------------------------------------------------------------------------------- /test/Resque/Tests/JobStatusTest.php: -------------------------------------------------------------------------------- 1 | 9 | * @license http://www.opensource.org/licenses/mit-license.php 10 | */ 11 | class Resque_Tests_JobStatusTest extends Resque_Tests_TestCase 12 | { 13 | public function setUp(): void 14 | { 15 | parent::setUp(); 16 | 17 | // Register a worker to test with 18 | $this->worker = new Resque_Worker('jobs'); 19 | } 20 | 21 | public function testJobStatusCanBeTracked() 22 | { 23 | $token = Resque::enqueue('jobs', 'Test_Job', null, true); 24 | $status = new Resque_Job_Status($token); 25 | $this->assertTrue($status->isTracking()); 26 | } 27 | 28 | public function testJobStatusIsReturnedViaJobInstance() 29 | { 30 | $token = Resque::enqueue('jobs', 'Test_Job', null, true); 31 | $job = Resque_Job::reserve('jobs'); 32 | $this->assertEquals(Resque_Job_Status::STATUS_WAITING, $job->getStatus()); 33 | } 34 | 35 | public function testQueuedJobReturnsQueuedStatus() 36 | { 37 | $token = Resque::enqueue('jobs', 'Test_Job', null, true); 38 | $status = new Resque_Job_Status($token); 39 | $this->assertEquals(Resque_Job_Status::STATUS_WAITING, $status->get()); 40 | } 41 | public function testRunningJobReturnsRunningStatus() 42 | { 43 | $token = Resque::enqueue('jobs', 'Failing_Job', null, true); 44 | $job = $this->worker->reserve(); 45 | $this->worker->workingOn($job); 46 | $status = new Resque_Job_Status($token); 47 | $this->assertEquals(Resque_Job_Status::STATUS_RUNNING, $status->get()); 48 | } 49 | 50 | public function testFailedJobReturnsFailedStatus() 51 | { 52 | $token = Resque::enqueue('jobs', 'Failing_Job', null, true); 53 | $this->worker->work(0); 54 | $status = new Resque_Job_Status($token); 55 | $this->assertEquals(Resque_Job_Status::STATUS_FAILED, $status->get()); 56 | } 57 | 58 | public function testCompletedJobReturnsCompletedStatus() 59 | { 60 | $token = Resque::enqueue('jobs', 'Test_Job', null, true); 61 | $this->worker->work(0); 62 | $status = new Resque_Job_Status($token); 63 | $this->assertEquals(Resque_Job_Status::STATUS_COMPLETE, $status->get()); 64 | } 65 | 66 | public function testStatusIsNotTrackedWhenToldNotTo() 67 | { 68 | $token = Resque::enqueue('jobs', 'Test_Job', null, false); 69 | $status = new Resque_Job_Status($token); 70 | $this->assertFalse($status->isTracking()); 71 | } 72 | 73 | public function testStatusTrackingCanBeStopped() 74 | { 75 | Resque_Job_Status::create('test'); 76 | $status = new Resque_Job_Status('test'); 77 | $this->assertEquals(Resque_Job_Status::STATUS_WAITING, $status->get()); 78 | $status->stop(); 79 | $this->assertFalse($status->get()); 80 | } 81 | 82 | public function testRecreatedJobWithTrackingStillTracksStatus() 83 | { 84 | $originalToken = Resque::enqueue('jobs', 'Test_Job', null, true); 85 | $job = $this->worker->reserve(); 86 | 87 | // Mark this job as being worked on to ensure that the new status is still 88 | // waiting. 89 | $this->worker->workingOn($job); 90 | 91 | // Now recreate it 92 | $newToken = $job->recreate(); 93 | 94 | // Make sure we've got a new job returned 95 | $this->assertNotEquals($originalToken, $newToken); 96 | 97 | // Now check the status of the new job 98 | $newJob = Resque_Job::reserve('jobs'); 99 | $this->assertEquals(Resque_Job_Status::STATUS_WAITING, $newJob->getStatus()); 100 | } 101 | } -------------------------------------------------------------------------------- /bin/resque: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | 1) { 75 | $count = $COUNT; 76 | } 77 | 78 | if ($count > 1) { 79 | for ($i = 0; $i < $count; ++$i) { 80 | $pid = pcntl_fork(); 81 | if ($pid == -1) { 82 | die("Could not fork worker " . $i . "\n"); 83 | } elseif (!$pid) { // Child, start the worker 84 | if ($PIDFILE) { 85 | if ($i == 0) { 86 | writePidFile($PIDFILE); 87 | } 88 | writePidFile($PIDFILE . '.child.' . ($i + 1)); 89 | } 90 | startWorker($QUEUE, $logLevel, $logger, $interval); 91 | break; 92 | } 93 | } 94 | } else { // Start a single worker 95 | 96 | if ($PIDFILE) { 97 | writePidFile($PIDFILE); 98 | } 99 | 100 | startWorker($QUEUE, $logLevel, $logger, $interval); 101 | } 102 | 103 | function startWorker($QUEUE, $logLevel, $logger, $interval) 104 | { 105 | $queues = explode(',', $QUEUE); 106 | $worker = new Resque_Worker($queues); 107 | $worker->registerLogger($logger); 108 | $worker->logLevel = $logLevel; 109 | $worker->work($interval); 110 | } 111 | 112 | function writePidFile($fullPath, $pid = null) 113 | { 114 | if (is_null($pid)) { 115 | $pid = getmypid(); 116 | } 117 | file_put_contents($fullPath, $pid) or die('Could not write PID information to ' . $fullPath); 118 | } -------------------------------------------------------------------------------- /test/Resque/Tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | 7 | * @license http://www.opensource.org/licenses/mit-license.php 8 | */ 9 | define('CWD', dirname(__FILE__)); 10 | define('RESQUE_LIB', CWD . '/../../../lib/'); 11 | 12 | define('TEST_MISC', realpath(CWD . '/../../misc/')); 13 | define('REDIS_CONF', TEST_MISC . '/redis.conf'); 14 | 15 | 16 | 17 | // Change to the directory this file lives in. This is important, due to 18 | // how we'll be running redis. 19 | 20 | require_once CWD . '/TestCase.php'; 21 | 22 | // Include Resque 23 | require_once RESQUE_LIB . 'Resque.php'; 24 | require_once RESQUE_LIB . 'Resque/Worker.php'; 25 | require_once RESQUE_LIB . 'Resque/Redis.php'; 26 | 27 | // Attempt to start our own redis instance for tesitng. 28 | exec('which redis-server', $output, $returnVar); 29 | if($returnVar != 0) { 30 | echo "Cannot find redis-server in path. Please make sure redis is installed.\n"; 31 | exit(1); 32 | } 33 | 34 | exec('cd ' . TEST_MISC . '; redis-server ' . REDIS_CONF, $output, $returnVar); 35 | usleep(500000); 36 | if($returnVar != 0) { 37 | echo "Cannot start redis-server.\n"; 38 | exit(1); 39 | 40 | } 41 | 42 | // Get redis port from conf 43 | $config = file_get_contents(REDIS_CONF); 44 | if(!preg_match('#^\s*port\s+([0-9]+)#m', $config, $matches)) { 45 | echo "Could not determine redis port from redis.conf"; 46 | exit(1); 47 | } 48 | 49 | define('REDIS_HOST', 'localhost:' . $matches[1]); 50 | define('REDIS_DATABASE', 7); 51 | define('REDIS_NAMESPACE', 'testResque'); 52 | 53 | Resque::setBackend(REDIS_HOST, REDIS_DATABASE, REDIS_NAMESPACE); 54 | 55 | // Shutdown 56 | function killRedis($pid) 57 | { 58 | if (getmypid() !== $pid) { 59 | return; // don't kill from a forked worker 60 | } 61 | $config = file_get_contents(REDIS_CONF); 62 | if(!preg_match('#^\s*pidfile\s+([^\s]+)#m', $config, $matches)) { 63 | return; 64 | } 65 | 66 | $pidFile = TEST_MISC . '/' . $matches[1]; 67 | if (file_exists($pidFile)) { 68 | $pid = trim(file_get_contents($pidFile)); 69 | posix_kill((int) $pid, 9); 70 | 71 | if(is_file($pidFile)) { 72 | unlink($pidFile); 73 | } 74 | } 75 | 76 | // Remove the redis database 77 | if(!preg_match('#^\s*dir\s+([^\s]+)#m', $config, $matches)) { 78 | return; 79 | } 80 | $dir = $matches[1]; 81 | 82 | if(!preg_match('#^\s*dbfilename\s+([^\s]+)#m', $config, $matches)) { 83 | return; 84 | } 85 | 86 | $filename = TEST_MISC . '/' . $dir . '/' . $matches[1]; 87 | if(is_file($filename)) { 88 | unlink($filename); 89 | } 90 | } 91 | register_shutdown_function('killRedis', getmypid()); 92 | 93 | if(function_exists('pcntl_signal')) { 94 | // Override INT and TERM signals, so they do a clean shutdown and also 95 | // clean up redis-server as well. 96 | function sigint() 97 | { 98 | exit; 99 | } 100 | pcntl_signal(SIGINT, 'sigint'); 101 | pcntl_signal(SIGTERM, 'sigint'); 102 | } 103 | 104 | class Test_Job 105 | { 106 | public static $called = false; 107 | 108 | public function perform() 109 | { 110 | self::$called = true; 111 | } 112 | } 113 | 114 | class Failing_Job_Exception extends Exception 115 | { 116 | 117 | } 118 | 119 | class Failing_Job 120 | { 121 | public function perform() 122 | { 123 | throw new Failing_Job_Exception('Message!'); 124 | } 125 | } 126 | 127 | class Test_Job_Without_Perform_Method 128 | { 129 | 130 | } 131 | 132 | class Test_Job_With_SetUp 133 | { 134 | public static $called = false; 135 | public $args = false; 136 | 137 | public function setUp() 138 | { 139 | self::$called = true; 140 | } 141 | 142 | public function perform() 143 | { 144 | 145 | } 146 | } 147 | 148 | 149 | class Test_Job_With_TearDown 150 | { 151 | public static $called = false; 152 | public $args = false; 153 | 154 | public function perform() 155 | { 156 | 157 | } 158 | 159 | public function tearDown() 160 | { 161 | self::$called = true; 162 | } 163 | } -------------------------------------------------------------------------------- /lib/Resque/Job/Status.php: -------------------------------------------------------------------------------- 1 | 7 | * @license http://www.opensource.org/licenses/mit-license.php 8 | */ 9 | class Resque_Job_Status 10 | { 11 | const STATUS_WAITING = 1; 12 | const STATUS_RUNNING = 2; 13 | const STATUS_FAILED = 3; 14 | const STATUS_COMPLETE = 4; 15 | 16 | const SECONDS_IN_1_DAY = 86400; 17 | const SECONDS_IN_1_WEEK = 604800; 18 | 19 | /** 20 | * @var string The ID of the job this status class refers back to. 21 | */ 22 | private $id; 23 | 24 | /** 25 | * @var mixed Cache variable if the status of this job is being monitored or not. 26 | * True/false when checked at least once or null if not checked yet. 27 | */ 28 | private $isTracking = null; 29 | 30 | /** 31 | * @var array Array of statuses that are considered final/complete. 32 | */ 33 | private static $completeStatuses = array( 34 | self::STATUS_FAILED, 35 | self::STATUS_COMPLETE 36 | ); 37 | 38 | /** 39 | * Setup a new instance of the job monitor class for the supplied job ID. 40 | * 41 | * @param string $id The ID of the job to manage the status for. 42 | */ 43 | public function __construct($id) 44 | { 45 | $this->id = $id; 46 | } 47 | 48 | /** 49 | * Create a new status monitor item for the supplied job ID. Will create 50 | * all necessary keys in Redis to monitor the status of a job. 51 | * 52 | * @param string $id The ID of the job to monitor the status of. 53 | */ 54 | public static function create($id, $status = self::STATUS_WAITING) 55 | { 56 | $statusPacket = array( 57 | 'status' => $status, 58 | 'updated' => time(), 59 | 'started' => time(), 60 | ); 61 | Resque::redis()->setex('job:' . $id . ':status', self::SECONDS_IN_1_WEEK, json_encode($statusPacket)); 62 | } 63 | 64 | /** 65 | * Check if we're actually checking the status of the loaded job status 66 | * instance. 67 | * 68 | * @return boolean True if the status is being monitored, false if not. 69 | */ 70 | public function isTracking() 71 | { 72 | if($this->isTracking === false) { 73 | return false; 74 | } 75 | 76 | if(!Resque::redis()->exists((string)$this)) { 77 | $this->isTracking = false; 78 | return false; 79 | } 80 | 81 | $this->isTracking = true; 82 | return true; 83 | } 84 | 85 | /** 86 | * Update the status indicator for the current job with a new status. 87 | * 88 | * @param int The status of the job (see constants in Resque_Job_Status) 89 | */ 90 | public function update($status) 91 | { 92 | if(!$this->isTracking()) { 93 | return; 94 | } 95 | 96 | $statusPacket = array( 97 | 'status' => $status, 98 | 'updated' => time(), 99 | ); 100 | Resque::redis()->setex((string)$this, self::SECONDS_IN_1_WEEK, json_encode($statusPacket)); 101 | 102 | // Expire the status for completed jobs after 24 hours 103 | if(in_array($status, self::$completeStatuses)) { 104 | Resque::redis()->expire((string)$this, self::SECONDS_IN_1_DAY); 105 | } 106 | } 107 | 108 | /** 109 | * Fetch the status for the job being monitored. 110 | * 111 | * @return mixed False if the status is not being monitored, otherwise the status as 112 | * as an integer, based on the Resque_Job_Status constants. 113 | */ 114 | public function get() 115 | { 116 | if(!$this->isTracking()) { 117 | return false; 118 | } 119 | 120 | $statusPacket = json_decode(Resque::redis()->get((string)$this), true); 121 | if(!$statusPacket) { 122 | return false; 123 | } 124 | 125 | return $statusPacket['status']; 126 | } 127 | 128 | /** 129 | * Stop tracking the status of a job. 130 | */ 131 | public function stop() 132 | { 133 | Resque::redis()->del((string)$this); 134 | } 135 | 136 | /** 137 | * Generate a string representation of this object. 138 | * 139 | * @return string String representation of the current job status class. 140 | */ 141 | public function __toString() 142 | { 143 | return 'job:' . $this->id . ':status'; 144 | } 145 | } 146 | ?> -------------------------------------------------------------------------------- /lib/Redisent/RedisentCluster.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright 2009 Justin Poliey 6 | * @license http://www.opensource.org/licenses/mit-license.php The MIT License 7 | * @package Redisent 8 | */ 9 | 10 | require_once dirname(__FILE__) . '/Redisent.php'; 11 | 12 | /** 13 | * A generalized Redisent interface for a cluster of Redis servers 14 | */ 15 | class RedisentCluster { 16 | 17 | /** 18 | * Collection of Redisent objects attached to Redis servers 19 | * @var array 20 | * @access private 21 | */ 22 | private $redisents; 23 | 24 | /** 25 | * Aliases of Redisent objects attached to Redis servers, used to route commands to specific servers 26 | * @see RedisentCluster::to 27 | * @var array 28 | * @access private 29 | */ 30 | private $aliases; 31 | 32 | /** 33 | * Hash ring of Redis server nodes 34 | * @var array 35 | * @access private 36 | */ 37 | private $ring; 38 | 39 | /** 40 | * Individual nodes of pointers to Redis servers on the hash ring 41 | * @var array 42 | * @access private 43 | */ 44 | private $nodes; 45 | 46 | /** 47 | * Number of replicas of each node to make around the hash ring 48 | * @var integer 49 | * @access private 50 | */ 51 | private $replicas = 128; 52 | 53 | /** 54 | * The commands that are not subject to hashing 55 | * @var array 56 | * @access private 57 | */ 58 | private $dont_hash = array( 59 | 'RANDOMKEY', 'DBSIZE', 60 | 'SELECT', 'MOVE', 'FLUSHDB', 'FLUSHALL', 61 | 'SAVE', 'BGSAVE', 'LASTSAVE', 'SHUTDOWN', 62 | 'INFO', 'MONITOR', 'SLAVEOF' 63 | ); 64 | 65 | /** 66 | * Creates a Redisent interface to a cluster of Redis servers 67 | * @param array $servers The Redis servers in the cluster. Each server should be in the format array('host' => hostname, 'port' => port) 68 | */ 69 | function __construct($servers) { 70 | $this->ring = array(); 71 | $this->aliases = array(); 72 | foreach ($servers as $alias => $server) { 73 | $this->redisents[] = new Redisent($server['host'], $server['port']); 74 | if (is_string($alias)) { 75 | $this->aliases[$alias] = $this->redisents[count($this->redisents)-1]; 76 | } 77 | for ($replica = 1; $replica <= $this->replicas; $replica++) { 78 | $this->ring[crc32($server['host'].':'.$server['port'].'-'.$replica)] = $this->redisents[count($this->redisents)-1]; 79 | } 80 | } 81 | ksort($this->ring, SORT_NUMERIC); 82 | $this->nodes = array_keys($this->ring); 83 | } 84 | 85 | /** 86 | * Routes a command to a specific Redis server aliased by {$alias}. 87 | * @param string $alias The alias of the Redis server 88 | * @return Redisent The Redisent object attached to the Redis server 89 | */ 90 | function to($alias) { 91 | if (isset($this->aliases[$alias])) { 92 | return $this->aliases[$alias]; 93 | } 94 | else { 95 | throw new Exception("That Redisent alias does not exist"); 96 | } 97 | } 98 | 99 | /* Execute a Redis command on the cluster */ 100 | function __call($name, $args) { 101 | 102 | /* Pick a server node to send the command to */ 103 | $name = strtoupper($name); 104 | if (!in_array($name, $this->dont_hash)) { 105 | $node = $this->nextNode(crc32($args[0])); 106 | $redisent = $this->ring[$node]; 107 | } 108 | else { 109 | $redisent = $this->redisents[0]; 110 | } 111 | 112 | /* Execute the command on the server */ 113 | return call_user_func_array(array($redisent, $name), $args); 114 | } 115 | 116 | /** 117 | * Routes to the proper server node 118 | * @param integer $needle The hash value of the Redis command 119 | * @return Redisent The Redisent object associated with the hash 120 | */ 121 | private function nextNode($needle) { 122 | $haystack = $this->nodes; 123 | while (count($haystack) > 2) { 124 | $try = floor(count($haystack) / 2); 125 | if ($haystack[$try] == $needle) { 126 | return $needle; 127 | } 128 | if ($needle < $haystack[$try]) { 129 | $haystack = array_slice($haystack, 0, $try + 1); 130 | } 131 | if ($needle > $haystack[$try]) { 132 | $haystack = array_slice($haystack, $try + 1); 133 | } 134 | } 135 | return $haystack[count($haystack)-1]; 136 | } 137 | 138 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Php-Resque-ex: Resque for PHP [![Build Status](https://secure.travis-ci.org/wa0x6e/php-resque-ex.png)](http://travis-ci.org/wa0x6e/php-resque-ex) 2 | =========================================== 3 | 4 | Resque is a Redis-backed library for creating background jobs, placing 5 | those jobs on multiple queues, and processing them later. 6 | 7 | ## Background ## 8 | 9 | Php-Resque-Ex is a fork of [php-resque](https://github.com/chrisboulton/php-resque) by chrisboulton. See the [original README](https://github.com/chrisboulton/php-resque/blob/master/README.md) for more informations. 10 | 11 | ## Additional features ## 12 | 13 | This fork provides some additional features : 14 | 15 | ### Support of php-redis 16 | 17 | Autodetect and use [phpredis](https://github.com/nicolasff/phpredis) to connect to Redis if available. Redisent is used as fallback. 18 | 19 | ### Powerfull logging 20 | 21 | Instead of piping STDOUT output to a file, you can log directly to a database, or send them elsewhere via a socket. We use [Monolog](https://github.com/Seldaek/monolog) to manage all the logging. See their documentation to see all the available handlers. 22 | 23 | Log infos are augmented with more informations, and associated with a workers, a queue, and a job ID if any. 24 | 25 | ### Job creation delegation 26 | 27 | If Resque_Job_Creator class exists and is found by Resque, all jobs creation will be delegated to this class. 28 | 29 | The best way to inject this class is to include it in you `APP_INCLUDE` file. 30 | 31 | Class content is : 32 | 33 | ```php 34 | class Resque_Job_Creator 35 | { 36 | public static function createJob($className, $args) { 37 | 38 | // $className is you job class name, the second arguments when enqueuing a job 39 | // $args are the arguments passed to your jobs 40 | 41 | // Instanciate your class, and return the instance 42 | 43 | return new $className(); 44 | } 45 | } 46 | ``` 47 | 48 | This is pretty useful when your autoloader can not load the class, like when classname doesn't match its filename. Some framework, like CakePHP, uses `PluginName.ClassName` convention for classname, and require special handling before loading. 49 | 50 | ### Failed jobs logs 51 | 52 | You can easily retrieve logs for a failed jobs in the redis database, their keys are named after their job ID. Each failed log will expire after 2 weeks to save space. 53 | 54 | ### Command Line tool 55 | 56 | Fresque is shipped by default to manage your workers. See [Fresque Documentation](https://github.com/wa0x6e/Fresque) for usage. 57 | 58 | ## Installation 59 | 60 | Clone the git repo 61 | 62 | $ git clone git://github.com/kamisama/php-resque-ex.git 63 | 64 | `cd` into the folder you just cloned 65 | 66 | $ cd ./php-resque-ex 67 | 68 | Download Composer 69 | 70 | $ curl -s https://getcomposer.org/installer | php 71 | 72 | Install dependencies 73 | 74 | $ php composer.phar install 75 | 76 | #### Warning 77 | 78 | php-resque requires the pcntl php extension, not available on Windows platform. Composer installation will fail if you're trying to install this package on Windows machine. If you still want to continue with the installation at your own risk, execute the composer install command with the `--ignore-platform-reqs` option. 79 | 80 | ## Usage 81 | 82 | ### Logging 83 | 84 | Use the same way as the original port, with additional ENV : 85 | 86 | * `LOGHANDLER` : Specify the handler to use for logging (File, MongoDB, Socket, etc …). 87 | See [Monolog](https://github.com/Seldaek/monolog#handlers) doc for all available handlers. 88 | `LOGHANDLER` is the name of the handler, without the "Handler" part. To use CubeHandler, just type "Cube". 89 | * `LOGHANDLERTARGET` : Information used by the handler to connect to the database. 90 | Depends on the type of loghandler. If it's the *RotatingFileHandler*, the target will be the filename. If it's CubeHandler, target will be a udp address. Refer to each Handler to see what type of argument their `__construct()` method requires. 91 | * `LOGGING` : This environment variable must be set in order to enable logging via Monolog. i.e `LOGGING=1` 92 | 93 | If one of these two environement variable is missing, it will default to *RotatingFile* Handler. 94 | 95 | ### Redis backend 96 | 97 | * `REDIS_BACKEND` : hostname of your Redis database 98 | * `REDIS_DATABASE` : To select another redis database (default 0) 99 | * `REDIS_NAMESPACE` : To set a different namespace for the keys (default to *resque*) 100 | * `REDIS_PASSWORD` : If your Redis backend needs authentication 101 | 102 | ## Requirements ## 103 | 104 | * PHP 8.1+ 105 | * Redis 2.2+ 106 | 107 | ## Contributors ## 108 | 109 | * [chrisboulton](https://github.com/chrisboulton/php-resque) for the original port 110 | * wa0x6e 111 | -------------------------------------------------------------------------------- /test/Resque/Tests/EventTest.php: -------------------------------------------------------------------------------- 1 | 9 | * @license http://www.opensource.org/licenses/mit-license.php 10 | */ 11 | class Resque_Tests_EventTest extends Resque_Tests_TestCase 12 | { 13 | private $callbacksHit = array(); 14 | 15 | public function setUp(): void 16 | { 17 | Test_Job::$called = false; 18 | 19 | // Register a worker to test with 20 | $this->worker = new Resque_Worker('jobs'); 21 | $this->worker->registerWorker(); 22 | } 23 | 24 | public function tearDown(): void 25 | { 26 | Resque_Event::clearListeners(); 27 | $this->callbacksHit = array(); 28 | } 29 | 30 | public function getEventTestJob() 31 | { 32 | $payload = array( 33 | 'class' => 'Test_Job', 34 | 'id' => 'randomId', 35 | 'args' => array( 36 | 'somevar', 37 | ), 38 | ); 39 | $job = new Resque_Job('jobs', $payload); 40 | $job->worker = $this->worker; 41 | return $job; 42 | } 43 | 44 | public function eventCallbackProvider() 45 | { 46 | return array( 47 | array('beforePerform', 'beforePerformEventCallback'), 48 | array('afterPerform', 'afterPerformEventCallback'), 49 | array('afterFork', 'afterForkEventCallback'), 50 | ); 51 | } 52 | 53 | /** 54 | * @dataProvider eventCallbackProvider 55 | */ 56 | public function testEventCallbacksFire($event, $callback) 57 | { 58 | Resque_Event::listen($event, array($this, $callback)); 59 | 60 | $job = $this->getEventTestJob(); 61 | $this->worker->perform($job); 62 | $this->worker->work(0); 63 | 64 | $this->assertContains($callback, $this->callbacksHit, $event . ' callback (' . $callback .') was not called'); 65 | } 66 | 67 | public function testBeforeForkEventCallbackFires() 68 | { 69 | $event = 'beforeFork'; 70 | $callback = 'beforeForkEventCallback'; 71 | 72 | Resque_Event::listen($event, array($this, $callback)); 73 | Resque::enqueue('jobs', 'Test_Job', array( 74 | 'somevar' 75 | )); 76 | $job = $this->getEventTestJob(); 77 | $this->worker->work(0); 78 | $this->assertContains($callback, $this->callbacksHit, $event . ' callback (' . $callback .') was not called'); 79 | } 80 | 81 | public function testBeforePerformEventCanStopWork() 82 | { 83 | $callback = 'beforePerformEventDontPerformCallback'; 84 | Resque_Event::listen('beforePerform', array($this, $callback)); 85 | 86 | $job = $this->getEventTestJob(); 87 | 88 | $this->assertFalse($job->perform()); 89 | $this->assertContains($callback, $this->callbacksHit, $callback . ' callback was not called'); 90 | $this->assertFalse(Test_Job::$called, 'Job was still performed though Resque_Job_DontPerform was thrown'); 91 | } 92 | 93 | public function testAfterEnqueueEventCallbackFires() 94 | { 95 | $callback = 'afterEnqueueEventCallback'; 96 | $event = 'afterEnqueue'; 97 | 98 | Resque_Event::listen($event, array($this, $callback)); 99 | Resque::enqueue('jobs', 'Test_Job', array( 100 | 'somevar' 101 | )); 102 | $this->assertContains($callback, $this->callbacksHit, $event . ' callback (' . $callback .') was not called'); 103 | } 104 | 105 | public function testStopListeningRemovesListener() 106 | { 107 | $callback = 'beforePerformEventCallback'; 108 | $event = 'beforePerform'; 109 | 110 | Resque_Event::listen($event, array($this, $callback)); 111 | Resque_Event::stopListening($event, array($this, $callback)); 112 | 113 | $job = $this->getEventTestJob(); 114 | $this->worker->perform($job); 115 | $this->worker->work(0); 116 | 117 | $this->assertNotContains($callback, $this->callbacksHit, 118 | $event . ' callback (' . $callback .') was called though Resque_Event::stopListening was called' 119 | ); 120 | } 121 | 122 | 123 | public function beforePerformEventDontPerformCallback($instance) 124 | { 125 | $this->callbacksHit[] = __FUNCTION__; 126 | throw new Resque_Job_DontPerform; 127 | } 128 | 129 | public function assertValidEventCallback($function, $job) 130 | { 131 | $this->callbacksHit[] = $function; 132 | if (!$job instanceof Resque_Job) { 133 | $this->fail('Callback job argument is not an instance of Resque_Job'); 134 | } 135 | $args = $job->getArguments(); 136 | $this->assertEquals($args[0], 'somevar'); 137 | } 138 | 139 | public function afterEnqueueEventCallback($class, $args) 140 | { 141 | $this->callbacksHit[] = __FUNCTION__; 142 | $this->assertEquals('Test_Job', $class); 143 | $this->assertEquals(array( 144 | 'somevar', 145 | ), $args); 146 | } 147 | 148 | public function beforePerformEventCallback($job) 149 | { 150 | $this->assertValidEventCallback(__FUNCTION__, $job); 151 | } 152 | 153 | public function afterPerformEventCallback($job) 154 | { 155 | $this->assertValidEventCallback(__FUNCTION__, $job); 156 | } 157 | 158 | public function beforeForkEventCallback($job) 159 | { 160 | $this->assertValidEventCallback(__FUNCTION__, $job); 161 | } 162 | 163 | public function afterForkEventCallback($job) 164 | { 165 | $this->assertValidEventCallback(__FUNCTION__, $job); 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /lib/Redisent/Redisent.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright 2009 Justin Poliey 6 | * @license http://www.opensource.org/licenses/mit-license.php The MIT License 7 | * @package Redisent 8 | */ 9 | 10 | define('CRLF', sprintf('%s%s', chr(13), chr(10))); 11 | 12 | /** 13 | * Wraps native Redis errors in friendlier PHP exceptions 14 | * Only declared if class doesn't already exist to ensure compatibility with php-redis 15 | */ 16 | if (! class_exists('RedisException', false)) { 17 | class RedisException extends Exception { 18 | } 19 | } 20 | 21 | /** 22 | * Redisent, a Redis interface for the modest among us 23 | */ 24 | class Redisent { 25 | 26 | /** 27 | * Socket connection to the Redis server 28 | * @var resource 29 | * @access private 30 | */ 31 | private $__sock; 32 | 33 | /** 34 | * Host of the Redis server 35 | * @var string 36 | * @access public 37 | */ 38 | public $host; 39 | 40 | /** 41 | * Port on which the Redis server is running 42 | * @var integer 43 | * @access public 44 | */ 45 | public $port; 46 | 47 | /** 48 | * Creates a Redisent connection to the Redis server on host {@link $host} and port {@link $port}. 49 | * @param string $host The hostname of the Redis server 50 | * @param integer $port The port number of the Redis server 51 | */ 52 | function __construct($host, $port = 6379) { 53 | $this->host = $host; 54 | $this->port = $port; 55 | $this->establishConnection(); 56 | } 57 | 58 | function establishConnection() { 59 | $this->__sock = fsockopen($this->host, $this->port, $errno, $errstr); 60 | if (!$this->__sock) { 61 | throw new Exception("{$errno} - {$errstr}"); 62 | } 63 | } 64 | 65 | function __destruct() { 66 | fclose($this->__sock); 67 | } 68 | 69 | function __call($name, $args) { 70 | 71 | /* Build the Redis unified protocol command */ 72 | array_unshift($args, strtoupper($name)); 73 | $command = sprintf('*%d%s%s%s', count($args), CRLF, implode(array_map(array($this, 'formatArgument'), $args), CRLF), CRLF); 74 | 75 | /* Open a Redis connection and execute the command */ 76 | for ($written = 0; $written < strlen($command); $written += $fwrite) { 77 | $fwrite = fwrite($this->__sock, substr($command, $written)); 78 | if ($fwrite === FALSE) { 79 | throw new Exception('Failed to write entire command to stream'); 80 | } 81 | } 82 | 83 | /* Parse the response based on the reply identifier */ 84 | $reply = trim(fgets($this->__sock, 512)); 85 | switch (substr($reply, 0, 1)) { 86 | /* Error reply */ 87 | case '-': 88 | throw new RedisException(substr(trim($reply), 4)); 89 | break; 90 | /* Inline reply */ 91 | case '+': 92 | $response = substr(trim($reply), 1); 93 | break; 94 | /* Bulk reply */ 95 | case '$': 96 | $response = null; 97 | if ($reply == '$-1') { 98 | break; 99 | } 100 | $read = 0; 101 | $size = substr($reply, 1); 102 | do { 103 | $block_size = ($size - $read) > 1024 ? 1024 : ($size - $read); 104 | $response .= fread($this->__sock, $block_size); 105 | $read += $block_size; 106 | } while ($read < $size); 107 | fread($this->__sock, 2); /* discard crlf */ 108 | break; 109 | /* Multi-bulk reply */ 110 | case '*': 111 | $count = substr($reply, 1); 112 | if ($count == '-1') { 113 | return null; 114 | } 115 | $response = array(); 116 | for ($i = 0; $i < $count; $i++) { 117 | $bulk_head = trim(fgets($this->__sock, 512)); 118 | $size = substr($bulk_head, 1); 119 | if ($size == '-1') { 120 | $response[] = null; 121 | } 122 | else { 123 | $read = 0; 124 | $block = ""; 125 | do { 126 | $block_size = ($size - $read) > 1024 ? 1024 : ($size - $read); 127 | $block .= fread($this->__sock, $block_size); 128 | $read += $block_size; 129 | } while ($read < $size); 130 | fread($this->__sock, 2); /* discard crlf */ 131 | $response[] = $block; 132 | } 133 | } 134 | break; 135 | /* Integer reply */ 136 | case ':': 137 | $response = intval(substr(trim($reply), 1)); 138 | break; 139 | default: 140 | throw new RedisException("invalid server response: {$reply}"); 141 | break; 142 | } 143 | /* Party on */ 144 | return $response; 145 | } 146 | 147 | private function formatArgument($arg) { 148 | if (is_array($arg)) { 149 | $arg = implode(' ', $arg); 150 | } 151 | return sprintf('$%d%s%s', strlen($arg), CRLF, $arg); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /lib/Resque/Redis.php: -------------------------------------------------------------------------------- 1 | host = $host; 13 | $this->port = $port; 14 | $this->timeout = $timeout; 15 | $this->password = $password; 16 | 17 | $this->establishConnection(); 18 | } 19 | 20 | function establishConnection() 21 | { 22 | $this->pconnect($this->host, (int)$this->port, (int)$this->timeout, getmypid()); 23 | if ($this->password !== null) { 24 | $this->auth($this->password); 25 | } 26 | 27 | $this->setOption(Redis::OPT_PREFIX, self::$defaultNamespace); 28 | } 29 | 30 | public function prefix($namespace) 31 | { 32 | if (empty($namespace)) $namespace = self::$defaultNamespace; 33 | if (strpos($namespace, ':') === false) { 34 | $namespace .= ':'; 35 | } 36 | self::$defaultNamespace = $namespace; 37 | 38 | $this->setOption(Redis::OPT_PREFIX, self::$defaultNamespace); 39 | } 40 | 41 | public static function getPrefix() 42 | { 43 | return ''; 44 | } 45 | } 46 | } else { 47 | // Third- party apps may have already loaded Resident from elsewhere 48 | // so lets be careful. 49 | if (!class_exists('Redisent', false)) { 50 | require_once dirname(__FILE__) . '/../Redisent/Redisent.php'; 51 | } 52 | 53 | /** 54 | * Extended Redisent class used by Resque for all communication with 55 | * redis. Essentially adds namespace support to Redisent. 56 | * 57 | * @package Resque/Redis 58 | * @author Chris Boulton 59 | * @copyright (c) 2010 Chris Boulton 60 | * @license http://www.opensource.org/licenses/mit-license.php 61 | */ 62 | class RedisApi extends Redisent 63 | { 64 | /** 65 | * Redis namespace 66 | * @var string 67 | */ 68 | private static $defaultNamespace = 'resque:'; 69 | /** 70 | * @var array List of all commands in Redis that supply a key as their 71 | * first argument. Used to prefix keys with the Resque namespace. 72 | */ 73 | private $keyCommands = [ 74 | 'exists', 75 | 'del', 76 | 'type', 77 | 'keys', 78 | 'expire', 79 | 'ttl', 80 | 'move', 81 | 'set', 82 | 'setex', 83 | 'get', 84 | 'getset', 85 | 'setnx', 86 | 'incr', 87 | 'incrby', 88 | 'decr', 89 | 'decrby', 90 | 'rpush', 91 | 'lpush', 92 | 'llen', 93 | 'lrange', 94 | 'ltrim', 95 | 'lindex', 96 | 'lset', 97 | 'lrem', 98 | 'lpop', 99 | 'rpop', 100 | 'sadd', 101 | 'srem', 102 | 'spop', 103 | 'scard', 104 | 'sismember', 105 | 'smembers', 106 | 'srandmember', 107 | 'zadd', 108 | 'zrem', 109 | 'zrange', 110 | 'zrevrange', 111 | 'zrangebyscore', 112 | 'zcard', 113 | 'zscore', 114 | 'zremrangebyscore', 115 | 'sort', 116 | 'rpoplpush', 117 | ]; 118 | // sinterstore 119 | // sunion 120 | // sunionstore 121 | // sdiff 122 | // sdiffstore 123 | // sinter 124 | // smove 125 | // rename 126 | // mget 127 | // msetnx 128 | // mset 129 | // renamenx 130 | 131 | /** 132 | * Set Redis namespace (prefix) default: resque 133 | * @param string $namespace 134 | */ 135 | public function prefix($namespace) 136 | { 137 | if (strpos($namespace, ':') === false) { 138 | $namespace .= ':'; 139 | } 140 | self::$defaultNamespace = $namespace; 141 | } 142 | 143 | /** 144 | * Magic method to handle all function requests and prefix key based 145 | * operations with the {self::$defaultNamespace} key prefix. 146 | * 147 | * @param string $name The name of the method called. 148 | * @param array $args Array of supplied arguments to the method. 149 | * @return mixed Return value from Resident::call() based on the command. 150 | */ 151 | public function __call($name, $args) 152 | { 153 | $args = func_get_args(); 154 | if (in_array(strtolower($name), $this->keyCommands)) { 155 | $args[1][0] = self::$defaultNamespace . $args[1][0]; 156 | } 157 | try { 158 | return parent::__call($name, $args[1]); 159 | } catch (RedisException $e) { 160 | return false; 161 | } 162 | } 163 | 164 | public static function getPrefix() 165 | { 166 | return self::$defaultNamespace; 167 | } 168 | } 169 | } 170 | 171 | class Resque_Redis extends redisApi 172 | { 173 | 174 | public function __construct($host, $port, $password = null) 175 | { 176 | if (is_subclass_of($this, 'Redis')) { 177 | parent::__construct($host, $port, 5, $password); 178 | } else { 179 | parent::__construct($host, $port); 180 | } 181 | 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /lib/Resque/Job.php: -------------------------------------------------------------------------------- 1 | 11 | * @license http://www.opensource.org/licenses/mit-license.php 12 | */ 13 | class Resque_Job 14 | { 15 | /** 16 | * @var string The name of the queue that this job belongs to. 17 | */ 18 | public $queue; 19 | 20 | /** 21 | * @var Resque_Worker Instance of the Resque worker running this job. 22 | */ 23 | public $worker; 24 | 25 | /** 26 | * @var object Object containing details of the job. 27 | */ 28 | public $payload; 29 | 30 | /** 31 | * @var object Instance of the class performing work for this job. 32 | */ 33 | private $instance; 34 | 35 | /** 36 | * Instantiate a new instance of a job. 37 | * 38 | * @param string $queue The queue that the job belongs to. 39 | * @param array $payload array containing details of the job. 40 | */ 41 | public function __construct($queue, $payload) 42 | { 43 | $this->queue = $queue; 44 | $this->payload = $payload; 45 | } 46 | 47 | /** 48 | * Create a new job and save it to the specified queue. 49 | * 50 | * @param string $queue The name of the queue to place the job in. 51 | * @param string $class The name of the class that contains the code to execute the job. 52 | * @param array $args Any optional arguments that should be passed when the job is executed. 53 | * @param boolean $monitor Set to true to be able to monitor the status of a job. 54 | * 55 | * @return string 56 | */ 57 | public static function create($queue, $class, $args = null, $monitor = false) 58 | { 59 | if ($args !== null && !is_array($args)) { 60 | throw new InvalidArgumentException( 61 | 'Supplied $args must be an array.' 62 | ); 63 | } 64 | 65 | $new = true; 66 | if(isset($args['id'])) { 67 | $id = $args['id']; 68 | unset($args['id']); 69 | $new = false; 70 | } else { 71 | $id = md5(uniqid('', true)); 72 | } 73 | Resque::push($queue, array( 74 | 'class' => $class, 75 | 'args' => array($args), 76 | 'id' => $id, 77 | )); 78 | 79 | if ($monitor) { 80 | if ($new) { 81 | Resque_Job_Status::create($id); 82 | } else { 83 | $statusInstance = new Resque_Job_Status($id); 84 | $statusInstance->update($id, Resque_Job_Status::STATUS_WAITING); 85 | } 86 | } 87 | 88 | return $id; 89 | } 90 | 91 | /** 92 | * Find the next available job from the specified queue and return an 93 | * instance of Resque_Job for it. 94 | * 95 | * @param string $queue The name of the queue to check for a job in. 96 | * @return null|object Null when there aren't any waiting jobs, instance of Resque_Job when a job was found. 97 | */ 98 | public static function reserve($queue) 99 | { 100 | $payload = Resque::pop($queue); 101 | if(!is_array($payload)) { 102 | return false; 103 | } 104 | 105 | return new Resque_Job($queue, $payload); 106 | } 107 | 108 | /** 109 | * Update the status of the current job. 110 | * 111 | * @param int $status Status constant from Resque_Job_Status indicating the current status of a job. 112 | */ 113 | public function updateStatus($status) 114 | { 115 | if(empty($this->payload['id'])) { 116 | return; 117 | } 118 | 119 | $statusInstance = new Resque_Job_Status($this->payload['id']); 120 | $statusInstance->update($status); 121 | } 122 | 123 | /** 124 | * Return the status of the current job. 125 | * 126 | * @return int The status of the job as one of the Resque_Job_Status constants. 127 | */ 128 | public function getStatus() 129 | { 130 | $status = new Resque_Job_Status($this->payload['id']); 131 | return $status->get(); 132 | } 133 | 134 | /** 135 | * Get the arguments supplied to this job. 136 | * 137 | * @return array Array of arguments. 138 | */ 139 | public function getArguments() 140 | { 141 | if (!isset($this->payload['args'])) { 142 | return array(); 143 | } 144 | 145 | return $this->payload['args'][0]; 146 | } 147 | 148 | /** 149 | * Get the instantiated object for this job that will be performing work. 150 | * 151 | * @return object Instance of the object that this job belongs to. 152 | */ 153 | public function getInstance() 154 | { 155 | if (!is_null($this->instance)) { 156 | return $this->instance; 157 | } 158 | 159 | if (class_exists('Resque_Job_Creator')) { 160 | $this->instance = Resque_Job_Creator::createJob($this->payload['class'], $this->getArguments()); 161 | } else { 162 | if(!class_exists($this->payload['class'])) { 163 | throw new Resque_Exception( 164 | 'Could not find job class ' . $this->payload['class'] . '.' 165 | ); 166 | } 167 | 168 | if(!method_exists($this->payload['class'], 'perform')) { 169 | throw new Resque_Exception( 170 | 'Job class ' . $this->payload['class'] . ' does not contain a perform method.' 171 | ); 172 | } 173 | $this->instance = new $this->payload['class'](); 174 | } 175 | 176 | $this->instance->job = $this; 177 | $this->instance->args = $this->getArguments(); 178 | $this->instance->queue = $this->queue; 179 | return $this->instance; 180 | } 181 | 182 | /** 183 | * Actually execute a job by calling the perform method on the class 184 | * associated with the job with the supplied arguments. 185 | * 186 | * @return bool 187 | * @throws Resque_Exception When the job's class could not be found or it does not contain a perform method. 188 | */ 189 | public function perform() 190 | { 191 | $instance = $this->getInstance(); 192 | try { 193 | Resque_Event::trigger('beforePerform', $this); 194 | 195 | if(method_exists($instance, 'setUp')) { 196 | $instance->setUp(); 197 | } 198 | 199 | $instance->perform(); 200 | 201 | if(method_exists($instance, 'tearDown')) { 202 | $instance->tearDown(); 203 | } 204 | 205 | Resque_Event::trigger('afterPerform', $this); 206 | } 207 | // beforePerform/setUp have said don't perform this job. Return. 208 | catch(Resque_Job_DontPerform $e) { 209 | return false; 210 | } 211 | 212 | return true; 213 | } 214 | 215 | /** 216 | * Mark the current job as having failed. 217 | * 218 | * @param $exception 219 | */ 220 | public function fail($exception) 221 | { 222 | Resque_Event::trigger('onFailure', array( 223 | 'exception' => $exception, 224 | 'job' => $this, 225 | )); 226 | 227 | $this->updateStatus(Resque_Job_Status::STATUS_FAILED); 228 | require_once dirname(__FILE__) . '/Failure.php'; 229 | Resque_Failure::create( 230 | $this->payload, 231 | $exception, 232 | $this->worker, 233 | $this->queue 234 | ); 235 | Resque_Stat::incr(Resque::FAILED); 236 | Resque_Stat::incr(Resque::FAILED_PREFIX . $this->worker); 237 | } 238 | 239 | /** 240 | * Re-queue the current job. 241 | * @return string 242 | */ 243 | public function recreate() 244 | { 245 | $status = new Resque_Job_Status($this->payload['id']); 246 | $monitor = false; 247 | if($status->isTracking()) { 248 | $monitor = true; 249 | } 250 | 251 | return self::create($this->queue, $this->payload['class'], $this->payload['args'], $monitor); 252 | } 253 | 254 | /** 255 | * Generate a string representation used to describe the current job. 256 | * 257 | * @return string The string representation of the job. 258 | */ 259 | public function __toString() 260 | { 261 | return json_encode(array( 262 | 'queue' => $this->queue, 263 | 'id' => !empty($this->payload['id']) ? $this->payload['id'] : '', 264 | 'class' => $this->payload['class'], 265 | 'args' => !empty($this->payload['args']) ? $this->payload['args'] : '' 266 | )); 267 | } 268 | } 269 | 270 | -------------------------------------------------------------------------------- /test/Resque/Tests/JobTest.php: -------------------------------------------------------------------------------- 1 | 9 | * @license http://www.opensource.org/licenses/mit-license.php 10 | */ 11 | class Resque_Tests_JobTest extends Resque_Tests_TestCase 12 | { 13 | protected $worker; 14 | 15 | public function setUp(): void 16 | { 17 | parent::setUp(); 18 | 19 | // Register a worker to test with 20 | $this->worker = new Resque_Worker('jobs'); 21 | $this->worker->registerWorker(); 22 | } 23 | 24 | public function testJobCanBeQueued() 25 | { 26 | $this->assertTrue((bool)Resque::enqueue('jobs', 'Test_Job')); 27 | } 28 | 29 | public function testQeueuedJobCanBeReserved() 30 | { 31 | Resque::enqueue('jobs', 'Test_Job'); 32 | 33 | $job = Resque_Job::reserve('jobs'); 34 | if ($job == false) { 35 | $this->fail('Job could not be reserved.'); 36 | } 37 | $this->assertEquals('jobs', $job->queue); 38 | $this->assertEquals('Test_Job', $job->payload['class']); 39 | } 40 | 41 | public function testObjectArgumentsCannotBePassedToJob() 42 | { 43 | $this->expectException(InvalidArgumentException::class); 44 | $args = new stdClass; 45 | $args->test = 'somevalue'; 46 | Resque::enqueue('jobs', 'Test_Job', $args); 47 | } 48 | 49 | public function testQueuedJobReturnsExactSamePassedInArguments() 50 | { 51 | $args = [ 52 | 'int' => 123, 53 | 'numArray' => [ 54 | 1, 55 | 2, 56 | ], 57 | 'assocArray' => [ 58 | 'key1' => 'value1', 59 | 'key2' => 'value2', 60 | ], 61 | ]; 62 | Resque::enqueue('jobs', 'Test_Job', $args); 63 | $job = Resque_Job::reserve('jobs'); 64 | 65 | $this->assertEquals($args, $job->getArguments()); 66 | } 67 | 68 | public function testAfterJobIsReservedItIsRemoved() 69 | { 70 | Resque::enqueue('jobs', 'Test_Job'); 71 | Resque_Job::reserve('jobs'); 72 | $this->assertFalse(Resque_Job::reserve('jobs')); 73 | } 74 | 75 | public function testRecreatedJobMatchesExistingJob() 76 | { 77 | $args = [ 78 | 'int' => 123, 79 | 'numArray' => [ 80 | 1, 81 | 2, 82 | ], 83 | 'assocArray' => [ 84 | 'key1' => 'value1', 85 | 'key2' => 'value2', 86 | ], 87 | ]; 88 | 89 | Resque::enqueue('jobs', 'Test_Job', $args); 90 | $job = Resque_Job::reserve('jobs'); 91 | 92 | // Now recreate it 93 | $job->recreate(); 94 | 95 | $newJob = Resque_Job::reserve('jobs'); 96 | $this->assertEquals($job->payload['class'], $newJob->payload['class']); 97 | $this->assertEquals($job->payload['args'], $newJob->getArguments()); 98 | } 99 | 100 | 101 | public function testFailedJobExceptionsAreCaught() 102 | { 103 | $payload = [ 104 | 'class' => 'Failing_Job', 105 | 'id' => 'randomId', 106 | 'args' => null, 107 | ]; 108 | $job = new Resque_Job('jobs', $payload); 109 | $job->worker = $this->worker; 110 | 111 | $this->worker->perform($job); 112 | 113 | $this->assertEquals(1, Resque_Stat::get(Resque::FAILED)); 114 | $this->assertEquals(1, Resque_Stat::get(Resque::FAILED_PREFIX . $this->worker)); 115 | } 116 | 117 | public function testJobWithoutPerformMethodThrowsException() 118 | { 119 | $this->expectException(Resque_Exception::class); 120 | Resque::enqueue('jobs', 'Test_Job_Without_Perform_Method'); 121 | $job = $this->worker->reserve(); 122 | $job->worker = $this->worker; 123 | $job->perform(); 124 | } 125 | 126 | public function testInvalidJobThrowsException() 127 | { 128 | $this->expectException(Resque_Exception::class); 129 | Resque::enqueue('jobs', 'Invalid_Job'); 130 | $job = $this->worker->reserve(); 131 | $job->worker = $this->worker; 132 | $job->perform(); 133 | } 134 | 135 | public function testJobWithSetUpCallbackFiresSetUp() 136 | { 137 | $payload = [ 138 | 'class' => 'Test_Job_With_SetUp', 139 | 'args' => [ 140 | 'somevar', 141 | 'somevar2', 142 | ], 143 | ]; 144 | $job = new Resque_Job('jobs', $payload); 145 | $job->perform(); 146 | 147 | $this->assertTrue(Test_Job_With_SetUp::$called); 148 | } 149 | 150 | public function testJobWithTearDownCallbackFiresTearDown() 151 | { 152 | $payload = [ 153 | 'class' => 'Test_Job_With_TearDown', 154 | 'args' => [ 155 | 'somevar', 156 | 'somevar2', 157 | ], 158 | ]; 159 | $job = new Resque_Job('jobs', $payload); 160 | $job->perform(); 161 | 162 | $this->assertTrue(Test_Job_With_TearDown::$called); 163 | } 164 | 165 | public function testJobWithNamespace() 166 | { 167 | Resque::setBackend(REDIS_HOST, REDIS_DATABASE, 'php'); 168 | $queue = 'jobs'; 169 | $payload = ['another_value']; 170 | Resque::enqueue($queue, 'Test_Job_With_TearDown', $payload); 171 | 172 | $this->assertEquals(Resque::queues(), ['jobs']); 173 | $this->assertEquals(Resque::size($queue), 1); 174 | 175 | Resque::setBackend(REDIS_HOST, REDIS_DATABASE, REDIS_NAMESPACE); 176 | $this->assertEquals(Resque::size($queue), 0); 177 | } 178 | 179 | public function testDequeueAll() 180 | { 181 | $queue = 'jobs'; 182 | Resque::enqueue($queue, 'Test_Job_Dequeue'); 183 | Resque::enqueue($queue, 'Test_Job_Dequeue'); 184 | $this->assertEquals(Resque::size($queue), 2); 185 | $this->assertEquals(Resque::dequeue($queue), 2); 186 | $this->assertEquals(Resque::size($queue), 0); 187 | } 188 | 189 | public function testDequeueMakeSureNotDeleteOthers() 190 | { 191 | $queue = 'jobs'; 192 | Resque::enqueue($queue, 'Test_Job_Dequeue'); 193 | Resque::enqueue($queue, 'Test_Job_Dequeue'); 194 | $other_queue = 'other_jobs'; 195 | Resque::enqueue($other_queue, 'Test_Job_Dequeue'); 196 | Resque::enqueue($other_queue, 'Test_Job_Dequeue'); 197 | $this->assertEquals(Resque::size($queue), 2); 198 | $this->assertEquals(Resque::size($other_queue), 2); 199 | $this->assertEquals(Resque::dequeue($queue), 2); 200 | $this->assertEquals(Resque::size($queue), 0); 201 | $this->assertEquals(Resque::size($other_queue), 2); 202 | } 203 | 204 | public function testDequeueSpecificItem() 205 | { 206 | $queue = 'jobs'; 207 | Resque::enqueue($queue, 'Test_Job_Dequeue1'); 208 | Resque::enqueue($queue, 'Test_Job_Dequeue2'); 209 | $this->assertEquals(Resque::size($queue), 2); 210 | $test = ['Test_Job_Dequeue2']; 211 | $this->assertEquals(Resque::dequeue($queue, $test), 1); 212 | $this->assertEquals(Resque::size($queue), 1); 213 | } 214 | 215 | public function testDequeueSpecificMultipleItems() 216 | { 217 | $queue = 'jobs'; 218 | Resque::enqueue($queue, 'Test_Job_Dequeue1'); 219 | Resque::enqueue($queue, 'Test_Job_Dequeue2'); 220 | Resque::enqueue($queue, 'Test_Job_Dequeue3'); 221 | $this->assertEquals(Resque::size($queue), 3); 222 | $test = ['Test_Job_Dequeue2', 'Test_Job_Dequeue3']; 223 | $this->assertEquals(Resque::dequeue($queue, $test), 2); 224 | $this->assertEquals(Resque::size($queue), 1); 225 | } 226 | 227 | public function testDequeueNonExistingItem() 228 | { 229 | $queue = 'jobs'; 230 | Resque::enqueue($queue, 'Test_Job_Dequeue1'); 231 | Resque::enqueue($queue, 'Test_Job_Dequeue2'); 232 | Resque::enqueue($queue, 'Test_Job_Dequeue3'); 233 | $this->assertEquals(Resque::size($queue), 3); 234 | $test = ['Test_Job_Dequeue4']; 235 | $this->assertEquals(Resque::dequeue($queue, $test), 0); 236 | $this->assertEquals(Resque::size($queue), 3); 237 | } 238 | 239 | public function testDequeueNonExistingItem2() 240 | { 241 | $queue = 'jobs'; 242 | Resque::enqueue($queue, 'Test_Job_Dequeue1'); 243 | Resque::enqueue($queue, 'Test_Job_Dequeue2'); 244 | Resque::enqueue($queue, 'Test_Job_Dequeue3'); 245 | $this->assertEquals(Resque::size($queue), 3); 246 | $test = ['Test_Job_Dequeue4', 'Test_Job_Dequeue1']; 247 | $this->assertEquals(Resque::dequeue($queue, $test), 1); 248 | $this->assertEquals(Resque::size($queue), 2); 249 | } 250 | 251 | } -------------------------------------------------------------------------------- /lib/Resque.php: -------------------------------------------------------------------------------- 1 | 10 | * @license http://www.opensource.org/licenses/mit-license.php 11 | */ 12 | class Resque 13 | { 14 | const VERSION = '1.2.5'; 15 | const WORKERS = 'workers'; 16 | const CURRENT_JOBS = 'current_jobs'; 17 | const WORKER_LOGGER = 'workerLogger'; 18 | const WORKER_PREFIX = 'worker:'; 19 | const PROCESSED = 'processed'; 20 | const PROCESSED_PREFIX = self::PROCESSED . ':'; 21 | const FAILED = 'failed'; 22 | const FAILED_PREFIX = self::FAILED . ':'; 23 | const STARTED_SUFFIX = ':started'; 24 | const PING_SUFFIX = ':ping'; 25 | const SCHEDULER_IDENTIFIER = '-scheduler-'; 26 | 27 | /** 28 | * @var Resque_Redis Instance of Resque_Redis that talks to redis. 29 | */ 30 | public static $redis = null; 31 | 32 | /** 33 | * @var mixed Host/port conbination separated by a colon, or a nested 34 | * array of server swith host/port pairs 35 | */ 36 | protected static $redisServer = null; 37 | 38 | /** 39 | * @var int ID of Redis database to select. 40 | */ 41 | protected static $redisDatabase = 0; 42 | 43 | /** 44 | * @var string namespace of the redis keys 45 | */ 46 | protected static $namespace = ''; 47 | 48 | /** 49 | * @var string password for the redis server 50 | */ 51 | protected static $password = null; 52 | 53 | /** 54 | * @var int PID of current process. Used to detect changes when forking 55 | * and implement "thread" safety to avoid race conditions. 56 | */ 57 | protected static $pid = null; 58 | 59 | /** 60 | * Given a host/port combination separated by a colon, set it as 61 | * the redis server that Resque will talk to. 62 | * 63 | * @param mixed $server Host/port combination separated by a colon, or 64 | * a nested array of servers with host/port pairs. 65 | * @param int $database 66 | */ 67 | public static function setBackend($server, $database = 0, $namespace = 'resque', $password = null) 68 | { 69 | self::$redisServer = $server; 70 | self::$redisDatabase = $database; 71 | self::$redis = null; 72 | self::$namespace = $namespace; 73 | self::$password = $password; 74 | } 75 | 76 | /** 77 | * Return an instance of the Resque_Redis class instantiated for Resque. 78 | * 79 | * @return Resque_Redis Instance of Resque_Redis. 80 | */ 81 | public static function redis() 82 | { 83 | // Detect when the PID of the current process has changed (from a fork, etc) 84 | // and force a reconnect to redis. 85 | $pid = getmypid(); 86 | if (self::$pid !== $pid) { 87 | self::$redis = null; 88 | self::$pid = $pid; 89 | } 90 | 91 | if (!is_null(self::$redis)) { 92 | return self::$redis; 93 | } 94 | 95 | $server = self::$redisServer; 96 | if (empty($server)) { 97 | $server = 'localhost:6379'; 98 | } 99 | 100 | if (is_array($server)) { 101 | require_once dirname(__FILE__) . '/Resque/RedisCluster.php'; 102 | self::$redis = new Resque_RedisCluster($server); 103 | } else { 104 | if (strpos($server, 'unix:') === false) { 105 | list($host, $port) = explode(':', $server); 106 | } else { 107 | $host = $server; 108 | $port = null; 109 | } 110 | require_once dirname(__FILE__) . '/Resque/Redis.php'; 111 | $redisInstance = new Resque_Redis($host, $port, self::$password); 112 | $redisInstance->prefix(self::$namespace); 113 | self::$redis = $redisInstance; 114 | } 115 | 116 | if (!empty(self::$redisDatabase)) { 117 | self::$redis->select(self::$redisDatabase); 118 | } 119 | 120 | return self::$redis; 121 | } 122 | 123 | /** 124 | * Push a job to the end of a specific queue. If the queue does not 125 | * exist, then create it as well. 126 | * 127 | * @param string $queue The name of the queue to add the job to. 128 | * @param array $item Job description as an array to be JSON encoded. 129 | */ 130 | public static function push($queue, $item) 131 | { 132 | self::redis()->sadd('queues', $queue); 133 | self::redis()->rpush('queue:' . $queue, json_encode($item)); 134 | } 135 | 136 | /** 137 | * Pop an item off the end of the specified queue, decode it and 138 | * return it. 139 | * 140 | * @param string $queue The name of the queue to fetch an item from. 141 | * @return array Decoded item from the queue. 142 | */ 143 | public static function pop($queue) 144 | { 145 | $item = self::redis()->lpop('queue:' . $queue); 146 | if (!$item) { 147 | return; 148 | } 149 | 150 | return json_decode($item, true); 151 | } 152 | 153 | /** 154 | * Remove items of the specified queue 155 | * 156 | * @param string $queue The name of the queue to fetch an item from. 157 | * @param array $items 158 | * @return integer number of deleted items 159 | */ 160 | public static function dequeue($queue, $items = []) 161 | { 162 | if (count($items) > 0) { 163 | return self::removeItems($queue, $items); 164 | } else { 165 | return self::removeList($queue); 166 | } 167 | } 168 | 169 | /** 170 | * Return the size (number of pending jobs) of the specified queue. 171 | * 172 | * @param $queue name of the queue to be checked for pending jobs 173 | * 174 | * @return int The size of the queue. 175 | */ 176 | public static function size($queue) 177 | { 178 | return self::redis()->llen('queue:' . $queue); 179 | } 180 | 181 | /** 182 | * Create a new job and save it to the specified queue. 183 | * 184 | * @param string $queue The name of the queue to place the job in. 185 | * @param string $class The name of the class that contains the code to execute the job. 186 | * @param array $args Any optional arguments that should be passed when the job is executed. 187 | * @param boolean $trackStatus Set to true to be able to monitor the status of a job. 188 | * 189 | * @return string 190 | */ 191 | public static function enqueue($queue, $class, $args = null, $trackStatus = false) 192 | { 193 | require_once dirname(__FILE__) . '/Resque/Job.php'; 194 | $result = Resque_Job::create($queue, $class, $args, $trackStatus); 195 | if ($result) { 196 | Resque_Event::trigger('afterEnqueue', [ 197 | 'class' => $class, 198 | 'args' => $args, 199 | ]); 200 | } 201 | 202 | return $result; 203 | } 204 | 205 | /** 206 | * Reserve and return the next available job in the specified queue. 207 | * 208 | * @param string $queue Queue to fetch next available job from. 209 | * @return Resque_Job Instance of Resque_Job to be processed, false if none or error. 210 | */ 211 | public static function reserve($queue) 212 | { 213 | require_once dirname(__FILE__) . '/Resque/Job.php'; 214 | 215 | return Resque_Job::reserve($queue); 216 | } 217 | 218 | /** 219 | * Get an array of all known queues. 220 | * 221 | * @return array Array of queues. 222 | */ 223 | public static function queues() 224 | { 225 | $queues = self::redis()->smembers('queues'); 226 | if (!is_array($queues)) { 227 | $queues = []; 228 | } 229 | 230 | return $queues; 231 | } 232 | 233 | /** 234 | * Remove Items from the queue 235 | * Safely moving each item to a temporary queue before processing it 236 | * If the Job matches, counts otherwise puts it in a requeue_queue 237 | * which at the end eventually be copied back into the original queue 238 | * 239 | * @private 240 | * 241 | * @param string $queue The name of the queue 242 | * @param array $items 243 | * @return integer number of deleted items 244 | */ 245 | private static function removeItems($queue, $items = []) 246 | { 247 | $counter = 0; 248 | $originalQueue = 'queue:' . $queue; 249 | $tempQueue = $originalQueue . ':temp:' . time(); 250 | $requeueQueue = $tempQueue . ':requeue'; 251 | 252 | // move each item from original queue to temp queue and process it 253 | $finished = false; 254 | while (!$finished) { 255 | $string = self::redis()->rpoplpush($originalQueue, self::redis()->getPrefix() . $tempQueue); 256 | 257 | if (!empty($string)) { 258 | if (self::matchItem($string, $items)) { 259 | self::redis()->rpop($tempQueue); 260 | $counter++; 261 | } else { 262 | self::redis()->rpoplpush($tempQueue, self::redis()->getPrefix() . $requeueQueue); 263 | } 264 | } else { 265 | $finished = true; 266 | } 267 | } 268 | 269 | // move back from temp queue to original queue 270 | $finished = false; 271 | while (!$finished) { 272 | $string = self::redis()->rpoplpush($requeueQueue, self::redis()->getPrefix() . $originalQueue); 273 | if (empty($string)) { 274 | $finished = true; 275 | } 276 | } 277 | 278 | // remove temp queue and requeue queue 279 | self::redis()->del($requeueQueue); 280 | self::redis()->del($tempQueue); 281 | 282 | return $counter; 283 | } 284 | 285 | /** 286 | * matching item 287 | * item can be ['class'] or ['class' => 'id'] or ['class' => {:foo => 1, :bar => 2}] 288 | * @private 289 | * 290 | * @params string $string redis result in json 291 | * @params $items 292 | * 293 | * @return (bool) 294 | */ 295 | private static function matchItem($string, $items) 296 | { 297 | $decoded = json_decode($string, true); 298 | 299 | foreach ($items as $key => $val) { 300 | # class name only ex: item[0] = ['class'] 301 | if (is_numeric($key)) { 302 | if ($decoded['class'] == $val) { 303 | return true; 304 | } 305 | # class name with args , example: item[0] = ['class' => {'foo' => 1, 'bar' => 2}] 306 | } elseif (is_array($val)) { 307 | $decodedArgs = (array)$decoded['args'][0]; 308 | if ($decoded['class'] == $key && 309 | count($decodedArgs) > 0 && count(array_diff($decodedArgs, $val)) == 0 310 | ) { 311 | return true; 312 | } 313 | # class name with ID, example: item[0] = ['class' => 'id'] 314 | } else { 315 | if ($decoded['class'] == $key && $decoded['id'] == $val) { 316 | return true; 317 | } 318 | } 319 | } 320 | 321 | return false; 322 | } 323 | 324 | /** 325 | * Remove List 326 | * 327 | * @private 328 | * 329 | * @params string $queue the name of the queue 330 | * @return integer number of deleted items belongs to this list 331 | */ 332 | private static function removeList($queue) 333 | { 334 | $counter = self::size($queue); 335 | $result = self::redis()->del('queue:' . $queue); 336 | 337 | return ($result == 1) ? $counter : 0; 338 | } 339 | 340 | /* 341 | * Generate an identifier to attach to a job for status tracking. 342 | * 343 | * @return string 344 | */ 345 | public static function generateJobId() 346 | { 347 | return md5(uniqid('', true)); 348 | } 349 | 350 | public static function getInProgressJobsCount(string $workersPrefix = null): int { 351 | if (empty($workersPrefix)) { 352 | return self::redis()->hlen(self::CURRENT_JOBS); 353 | } 354 | 355 | $keys = self::redis()->hKeys(self::CURRENT_JOBS); 356 | return count(array_filter($keys, function ($key) use($workersPrefix) { 357 | return self::isEnvWorker($key, $workersPrefix); 358 | })); 359 | } 360 | 361 | /** 362 | * Clean workers that were terminated after timeout 363 | * 364 | * @return array of unfinished jobs 365 | */ 366 | public static function cleanWorkers(string $workersPrefix = null): array { 367 | $notFinishedJobs = []; 368 | $workers = self::redis()->sMembers(self::WORKERS); 369 | foreach ($workers as $workerId) { 370 | if (self::isEnvWorker($workerId, $workersPrefix) && !self::isWorkerAliveByPing($workerId)) { 371 | $notFinishedJob = self::redis()->hget(self::CURRENT_JOBS, $workerId); 372 | if ($notFinishedJob) { 373 | $notFinishedJobs[] = $notFinishedJob; 374 | } 375 | self::workerCleanup($workerId); 376 | } 377 | } 378 | 379 | return $notFinishedJobs; 380 | } 381 | 382 | public static function getJobsToRerun(string $workersPrefix = null, int $jobTimeout = 60, array $workersToRerun = []) { 383 | $jobsToRerun = []; 384 | $workers = self::redis()->hKeys(self::CURRENT_JOBS); 385 | $now = date_timestamp_get(date_create()); 386 | foreach ($workers as $workerId) { 387 | if (self::isEnvWorker($workerId, $workersPrefix)) { 388 | $notFinishedJob = self::redis()->hget(self::CURRENT_JOBS, $workerId); 389 | if ($notFinishedJob) { 390 | $notFinishedJob = json_decode($notFinishedJob, true); 391 | $runAt = date_timestamp_get(date_create_from_format('D M d H:i:s e Y', $notFinishedJob['run_at'])); 392 | if (in_array($notFinishedJob['payload']['class'], $workersToRerun) && ($now - $runAt) > $jobTimeout 393 | && !array_key_exists("rerun", $notFinishedJob['payload']['args'][0])) { 394 | $jobsToRerun[] = $notFinishedJob; 395 | self::redis()->hdel(self::CURRENT_JOBS, $workerId); 396 | } 397 | } 398 | } 399 | } 400 | return $jobsToRerun; 401 | } 402 | 403 | public static function workerCleanup(string $workerId) { 404 | try { 405 | self::redis()->multi(); 406 | self::redis()->srem(self::WORKERS, $workerId); 407 | self::redis()->hdel(self::CURRENT_JOBS, $workerId); 408 | self::redis()->del(self::WORKER_PREFIX . $workerId . self:: STARTED_SUFFIX); 409 | Resque_Stat::clear(self::PROCESSED_PREFIX . $workerId); 410 | Resque_Stat::clear(self::FAILED_PREFIX . $workerId); 411 | self::redis()->hdel(self::WORKER_LOGGER, $workerId); 412 | self::redis()->del(self::WORKER_PREFIX . $workerId . self::PING_SUFFIX); 413 | self::redis()->exec(); 414 | } catch (Throwable $t) { 415 | error_log("Worker cleanup error: " . $workerId); 416 | } 417 | } 418 | 419 | private static function isWorkerAliveByPing(string $workerId): bool { 420 | return self::redis()->get(self::WORKER_PREFIX . $workerId . self::PING_SUFFIX) !== false; 421 | } 422 | 423 | private static function isEnvWorker(string $workerId, string $workersPrefix = null): bool { 424 | return (empty($workersPrefix) || strpos($workerId, $workersPrefix) === 0) 425 | && strpos($workerId, self::SCHEDULER_IDENTIFIER) === false; 426 | } 427 | } 428 | -------------------------------------------------------------------------------- /test/Resque/Tests/WorkerTest.php: -------------------------------------------------------------------------------- 1 | 12 | * @license http://www.opensource.org/licenses/mit-license.php 13 | */ 14 | class Resque_Tests_WorkerTest extends Resque_Tests_TestCase 15 | { 16 | public function testWorkerRegistersInList() 17 | { 18 | $worker = new Resque_Worker('*'); 19 | $worker->registerWorker(); 20 | 21 | // Make sure the worker is in the list 22 | $this->assertTrue((bool)$this->redis->sismember(Resque::WORKERS, (string)$worker)); 23 | } 24 | 25 | public function testGetAllWorkers() 26 | { 27 | $num = 3; 28 | // Register a few workers 29 | for($i = 0; $i < $num; ++$i) { 30 | $worker = new Resque_Worker('queue_' . $i); 31 | $worker->registerWorker(); 32 | $worker->registerLogger(new MonologInit('', '')); 33 | } 34 | 35 | // Now try to get them 36 | $this->assertEquals($num, count(Resque_Worker::all())); 37 | } 38 | 39 | public function testGetWorkerById() 40 | { 41 | $worker = new Resque_Worker('*'); 42 | $worker->registerWorker(); 43 | $worker->registerLogger(new MonologInit('', '')); 44 | 45 | $newWorker = Resque_Worker::find((string)$worker); 46 | $this->assertEquals((string)$worker, (string)$newWorker); 47 | } 48 | 49 | public function testInvalidWorkerDoesNotExist() 50 | { 51 | $this->assertFalse(Resque_Worker::exists('blah')); 52 | } 53 | 54 | public function testWorkerCanUnregister() 55 | { 56 | $worker = new Resque_Worker('*'); 57 | $worker->registerWorker(); 58 | $worker->unregisterWorker(); 59 | 60 | $this->assertFalse(Resque_Worker::exists((string)$worker)); 61 | $this->assertEquals(array(), Resque_Worker::all()); 62 | $this->assertEquals(array(), $this->redis->smembers('resque:workers')); 63 | } 64 | 65 | public function testPausedWorkerDoesNotPickUpJobs() 66 | { 67 | $worker = new Resque_Worker('*'); 68 | $worker->pauseProcessing(); 69 | Resque::enqueue('jobs', 'Test_Job'); 70 | $worker->work(0); 71 | $worker->work(0); 72 | $this->assertEquals(0, Resque_Stat::get(Resque::PROCESSED)); 73 | } 74 | 75 | public function testResumedWorkerPicksUpJobs() 76 | { 77 | $worker = new Resque_Worker('*'); 78 | $worker->pauseProcessing(); 79 | Resque::enqueue('jobs', 'Test_Job'); 80 | $worker->work(0); 81 | $this->assertEquals(0, Resque_Stat::get(Resque::PROCESSED)); 82 | $worker->unPauseProcessing(); 83 | $worker->work(0); 84 | $this->assertEquals(1, Resque_Stat::get(Resque::PROCESSED)); 85 | } 86 | 87 | public function testWorkerCanWorkOverMultipleQueues() 88 | { 89 | $worker = new Resque_Worker(array( 90 | 'queue1', 91 | 'queue2' 92 | )); 93 | $worker->registerWorker(); 94 | Resque::enqueue('queue1', 'Test_Job_1'); 95 | Resque::enqueue('queue2', 'Test_Job_2'); 96 | 97 | $job = $worker->reserve(); 98 | $this->assertEquals('queue1', $job->queue); 99 | 100 | $job = $worker->reserve(); 101 | $this->assertEquals('queue2', $job->queue); 102 | } 103 | 104 | public function testWorkerWorksQueuesInSpecifiedOrder() 105 | { 106 | $worker = new Resque_Worker(array( 107 | 'high', 108 | 'medium', 109 | 'low' 110 | )); 111 | $worker->registerWorker(); 112 | 113 | // Queue the jobs in a different order 114 | Resque::enqueue('low', 'Test_Job_1'); 115 | Resque::enqueue('high', 'Test_Job_2'); 116 | Resque::enqueue('medium', 'Test_Job_3'); 117 | 118 | // Now check we get the jobs back in the right order 119 | $job = $worker->reserve(); 120 | $this->assertEquals('high', $job->queue); 121 | 122 | $job = $worker->reserve(); 123 | $this->assertEquals('medium', $job->queue); 124 | 125 | $job = $worker->reserve(); 126 | $this->assertEquals('low', $job->queue); 127 | } 128 | 129 | public function testWildcardQueueWorkerWorksAllQueues() 130 | { 131 | $worker = new Resque_Worker('*'); 132 | $worker->registerWorker(); 133 | 134 | Resque::enqueue('queue1', 'Test_Job_1'); 135 | Resque::enqueue('queue2', 'Test_Job_2'); 136 | 137 | $job = $worker->reserve(); 138 | $this->assertEquals('queue1', $job->queue); 139 | 140 | $job = $worker->reserve(); 141 | $this->assertEquals('queue2', $job->queue); 142 | } 143 | 144 | public function testWorkerDoesNotWorkOnUnknownQueues() 145 | { 146 | $worker = new Resque_Worker('queue1'); 147 | $worker->registerWorker(); 148 | Resque::enqueue('queue2', 'Test_Job'); 149 | 150 | $this->assertFalse($worker->reserve()); 151 | } 152 | 153 | public function testWorkerClearsItsStatusWhenNotWorking() 154 | { 155 | Resque::enqueue('jobs', 'Test_Job'); 156 | $worker = new Resque_Worker('jobs'); 157 | $job = $worker->reserve(); 158 | $worker->workingOn($job); 159 | $worker->doneWorking(); 160 | $this->assertEquals(array(), $worker->job()); 161 | } 162 | 163 | public function testWorkerRecordsWhatItIsWorkingOn() 164 | { 165 | $worker = new Resque_Worker('jobs'); 166 | $worker->registerWorker(); 167 | 168 | $payload = array( 169 | 'class' => 'Test_Job' 170 | ); 171 | $job = new Resque_Job('jobs', $payload); 172 | $worker->workingOn($job); 173 | 174 | $job = $worker->job(); 175 | $this->assertEquals('jobs', $job['queue']); 176 | if(!isset($job['run_at'])) { 177 | $this->fail('Job does not have run_at time'); 178 | } 179 | $this->assertEquals($payload, $job['payload']); 180 | } 181 | 182 | public function testWorkerErasesItsStatsWhenShutdown() 183 | { 184 | Resque::enqueue('jobs', 'Test_Job'); 185 | Resque::enqueue('jobs', 'Invalid_Job'); 186 | 187 | $worker = new Resque_Worker('jobs'); 188 | $worker->work(0); 189 | $worker->work(0); 190 | 191 | $this->assertEquals(0, $worker->getStat(Resque::PROCESSED)); 192 | $this->assertEquals(0, $worker->getStat(Resque::FAILED)); 193 | } 194 | 195 | public function testWorkerCleansUpDeadWorkersOnStartup() 196 | { 197 | // Register a good worker 198 | $goodWorker = new Resque_Worker('jobs'); 199 | $goodWorker->registerWorker(); 200 | $workerId = explode(':', $goodWorker); 201 | $goodWorker->registerLogger(new MonologInit('', '')); 202 | 203 | // Register some bad workers 204 | $worker = new Resque_Worker('jobs'); 205 | $worker->setId($workerId[0].':1:jobs'); 206 | $worker->registerWorker(); 207 | $worker->registerLogger(new MonologInit('', '')); 208 | 209 | $worker = new Resque_Worker(array('high', 'low')); 210 | $worker->setId($workerId[0].':2:high,low'); 211 | $worker->registerWorker(); 212 | $worker->registerLogger(new MonologInit('', '')); 213 | 214 | $this->assertEquals(3, count(Resque_Worker::all())); 215 | 216 | $goodWorker->pruneDeadWorkers(); 217 | 218 | // There should only be $goodWorker left now 219 | $this->assertEquals(1, count(Resque_Worker::all())); 220 | } 221 | 222 | public function testDeadWorkerCleanUpDoesNotCleanUnknownWorkers() 223 | { 224 | // Register a bad worker on this machine 225 | $worker = new Resque_Worker('jobs'); 226 | $workerId = explode(':', $worker); 227 | $worker->setId($workerId[0].':1:jobs'); 228 | $worker->registerWorker(); 229 | $worker->registerLogger(new MonologInit('', '')); 230 | 231 | // Register some other false workers 232 | $worker = new Resque_Worker('jobs'); 233 | $worker->setId('my.other.host:1:jobs'); 234 | $worker->registerWorker(); 235 | $worker->registerLogger(new MonologInit('', '')); 236 | 237 | $this->assertEquals(2, count(Resque_Worker::all())); 238 | 239 | $worker->pruneDeadWorkers(); 240 | 241 | // my.other.host should be left 242 | $workers = Resque_Worker::all(); 243 | $this->assertEquals(1, count($workers)); 244 | $this->assertEquals((string)$worker, (string)$workers[0]); 245 | } 246 | 247 | public function testWorkerFailsUncompletedJobsOnExit() 248 | { 249 | $worker = new Resque_Worker('jobs'); 250 | $worker->registerWorker(); 251 | 252 | $payload = array( 253 | 'class' => 'Test_Job', 254 | 'id' => 'randomId' 255 | ); 256 | $job = new Resque_Job('jobs', $payload); 257 | 258 | $worker->workingOn($job); 259 | $worker->unregisterWorker(); 260 | 261 | $this->assertEquals(1, Resque_Stat::get(Resque::FAILED)); 262 | } 263 | 264 | public function testWorkerLogAllMessageOnVerbose() 265 | { 266 | $worker = new Resque_Worker('jobs'); 267 | $worker->logLevel = Resque_Worker::LOG_VERBOSE; 268 | $worker->logOutput = fopen('php://memory', 'r+'); 269 | 270 | $message = array('message' => 'x', 'data' => []); 271 | 272 | $this->assertEquals(true, $worker->log($message, Resque_Worker::LOG_TYPE_DEBUG)); 273 | $this->assertEquals(true, $worker->log($message, Resque_Worker::LOG_TYPE_INFO)); 274 | $this->assertEquals(true, $worker->log($message, Resque_Worker::LOG_TYPE_WARNING)); 275 | $this->assertEquals(true, $worker->log($message, Resque_Worker::LOG_TYPE_CRITICAL)); 276 | $this->assertEquals(true, $worker->log($message, Resque_Worker::LOG_TYPE_ERROR)); 277 | $this->assertEquals(true, $worker->log($message, Resque_Worker::LOG_TYPE_ALERT)); 278 | 279 | rewind($worker->logOutput); 280 | $output = stream_get_contents($worker->logOutput); 281 | 282 | $lines = explode("\n", $output); 283 | $this->assertEquals(6, count($lines) -1); 284 | } 285 | 286 | public function testWorkerLogOnlyInfoMessageOnNonVerbose() 287 | { 288 | $worker = new Resque_Worker('jobs'); 289 | $worker->logLevel = Resque_Worker::LOG_NORMAL; 290 | $worker->logOutput = fopen('php://memory', 'r+'); 291 | 292 | $message = array('message' => 'x', 'data' => []); 293 | 294 | $this->assertEquals(false, $worker->log($message, Resque_Worker::LOG_TYPE_DEBUG)); 295 | $this->assertEquals(true, $worker->log($message, Resque_Worker::LOG_TYPE_INFO)); 296 | $this->assertEquals(true, $worker->log($message, Resque_Worker::LOG_TYPE_WARNING)); 297 | $this->assertEquals(true, $worker->log($message, Resque_Worker::LOG_TYPE_CRITICAL)); 298 | $this->assertEquals(true, $worker->log($message, Resque_Worker::LOG_TYPE_ERROR)); 299 | $this->assertEquals(true, $worker->log($message, Resque_Worker::LOG_TYPE_ALERT)); 300 | 301 | rewind($worker->logOutput); 302 | $output = stream_get_contents($worker->logOutput); 303 | 304 | $lines = explode("\n", $output); 305 | $this->assertEquals(5, count($lines) -1); 306 | } 307 | 308 | public function testWorkerLogNothingWhenLogNone() 309 | { 310 | $worker = new Resque_Worker('jobs'); 311 | $worker->logLevel = Resque_Worker::LOG_NONE; 312 | $worker->logOutput = fopen('php://memory', 'r+'); 313 | 314 | $message = array('message' => 'x', 'data' => ''); 315 | 316 | $this->assertEquals(false, $worker->log($message, Resque_Worker::LOG_TYPE_DEBUG)); 317 | $this->assertEquals(false, $worker->log($message, Resque_Worker::LOG_TYPE_INFO)); 318 | $this->assertEquals(false, $worker->log($message, Resque_Worker::LOG_TYPE_WARNING)); 319 | $this->assertEquals(false, $worker->log($message, Resque_Worker::LOG_TYPE_CRITICAL)); 320 | $this->assertEquals(false, $worker->log($message, Resque_Worker::LOG_TYPE_ERROR)); 321 | $this->assertEquals(false, $worker->log($message, Resque_Worker::LOG_TYPE_ALERT)); 322 | 323 | rewind($worker->logOutput); 324 | $output = stream_get_contents($worker->logOutput); 325 | 326 | $lines = explode("\n", $output); 327 | $this->assertEquals(0, count($lines) -1); 328 | } 329 | 330 | public function testWorkerLogWithISOTime() 331 | { 332 | $worker = new Resque_Worker('jobs'); 333 | $worker->logLevel = Resque_Worker::LOG_NORMAL; 334 | $worker->logOutput = fopen('php://memory', 'r+'); 335 | 336 | $message = array('message' => 'x', 'data' => []); 337 | 338 | $now = date('c'); 339 | $this->assertEquals(true, $worker->log($message, Resque_Worker::LOG_TYPE_INFO)); 340 | 341 | rewind($worker->logOutput); 342 | $output = stream_get_contents($worker->logOutput); 343 | 344 | $lines = explode("\n", $output); 345 | $this->assertEquals(1, count($lines) -1); 346 | $this->assertEquals('[' . $now . '] x', $lines[0]); 347 | } 348 | 349 | public function testWorkersCleanupWithPrefix() { 350 | $payload = array( 351 | 'class' => 'Test_Job' 352 | ); 353 | 354 | $worker1 = $this->createWorker("prod-worker-12345:1:jobs", $payload); 355 | $worker2 = $this->createWorker("prod-worker-12346:1:jobs", $payload); 356 | $worker3 = $this->createWorker("prod-worker-12347:1:jobs", $payload); 357 | $worker4 = $this->createWorker("prod-rc-worker-12346:1:jobs", $payload); 358 | $worker5 = $this->createWorker("prod-rc-worker-12347:1:jobs", $payload); 359 | $worker6 = $this->createWorker("prod-worker-scheduler-12347:1:jobs", $payload); 360 | 361 | Resque::redis()->del(Resque::WORKER_PREFIX . $worker1 . Resque::PING_SUFFIX); 362 | Resque::redis()->del(Resque::WORKER_PREFIX . $worker3 . Resque::PING_SUFFIX); 363 | Resque::redis()->del(Resque::WORKER_PREFIX . $worker5 . Resque::PING_SUFFIX); 364 | Resque::redis()->del(Resque::WORKER_PREFIX . $worker6 . Resque::PING_SUFFIX); 365 | 366 | $this->assertCount(2, Resque::cleanWorkers('prod-worker-')); 367 | $this->assertCount(4, Resque_Worker::all()); 368 | } 369 | 370 | public function testWorkersCleanupNoPrefix() { 371 | $payload = array( 372 | 'class' => 'Test_Job' 373 | ); 374 | 375 | $worker1 = $this->createWorker("prod-worker-12345:1:jobs", $payload); 376 | $worker2 = $this->createWorker("prod-worker-12346:1:jobs", $payload); 377 | $worker3 = $this->createWorker("prod-worker-12347:1:jobs", $payload); 378 | $worker4 = $this->createWorker("prod-rc-worker-12346:1:jobs", $payload); 379 | $worker5 = $this->createWorker("prod-rc-worker-12347:1:jobs", $payload); 380 | $worker6 = $this->createWorker("prod-worker-scheduler-12347:1:jobs", $payload); 381 | 382 | Resque::redis()->del(Resque::WORKER_PREFIX . $worker1 . Resque::PING_SUFFIX); 383 | Resque::redis()->del(Resque::WORKER_PREFIX . $worker3 . Resque::PING_SUFFIX); 384 | Resque::redis()->del(Resque::WORKER_PREFIX . $worker5 . Resque::PING_SUFFIX); 385 | Resque::redis()->del(Resque::WORKER_PREFIX . $worker6 . Resque::PING_SUFFIX); 386 | 387 | $this->assertCount(3, Resque::cleanWorkers()); 388 | $this->assertCount(3, Resque_Worker::all()); 389 | } 390 | 391 | public function testGetInProgressJobsCountWithPrefix() { 392 | $payload = array( 393 | 'id' => 'id', 394 | 'class' => 'Test_Job' 395 | ); 396 | 397 | $this->assertEquals(0, Resque::getInProgressJobsCount('prod-worker-')); 398 | 399 | $worker1 = $this->createWorker("prod-worker-12345:1:jobs", $payload); 400 | $worker2 = $this->createWorker("prod-worker-12346:1:jobs", $payload); 401 | $worker3 = $this->createWorker("prod-worker-12347:1:jobs", $payload); 402 | $worker4 = $this->createWorker("prod-rc-worker-12346:1:jobs", $payload); 403 | $worker5 = $this->createWorker("prod-rc-worker-12347:1:jobs", $payload); 404 | 405 | $this->assertEquals(3, Resque::getInProgressJobsCount('prod-worker-')); 406 | 407 | $worker1->unregisterWorker(); 408 | $worker2->unregisterWorker(); 409 | $worker3->unregisterWorker(); 410 | 411 | $this->assertEquals(0, Resque::getInProgressJobsCount('prod-worker-')); 412 | } 413 | 414 | public function testGetInProgressJobsCountWithoutPrefix() { 415 | $payload = array( 416 | 'class' => 'Test_Job' 417 | ); 418 | 419 | $this->assertEquals(0, Resque::getInProgressJobsCount()); 420 | 421 | $worker1 = $this->createWorker("prod-worker-12345:1:jobs", $payload); 422 | $worker2 = $this->createWorker("prod-worker-12346:1:jobs", $payload); 423 | $worker3 = $this->createWorker("prod-worker-12347:1:jobs", $payload); 424 | $worker4 = $this->createWorker("prod-rc-worker-12346:1:jobs", $payload); 425 | $worker5 = $this->createWorker("prod-rc-worker-12347:1:jobs", $payload); 426 | 427 | $this->assertEquals(5, Resque::getInProgressJobsCount()); 428 | 429 | Resque::redis()->del(Resque::CURRENT_JOBS); 430 | 431 | $this->assertEquals(0, Resque::getInProgressJobsCount()); 432 | } 433 | 434 | public function testGetJobsToRerunWithPrefix() { 435 | $payload1 = array( 436 | 'class' => 'Test_Job1', 437 | 'args' => [[]] 438 | ); 439 | 440 | $payload2 = array( 441 | 'class' => 'Test_Job2', 442 | 'args' => [[]] 443 | ); 444 | 445 | $payload3 = array( 446 | 'class' => 'Test_Job1', 447 | 'args' => [['rerun' => true]] 448 | ); 449 | 450 | $worker1 = $this->createWorker("prod-worker-12345:1:jobs", $payload1); 451 | $worker2 = $this->createWorker("prod-worker-12346:1:jobs", $payload3); 452 | $worker3 = $this->createWorker("prod-worker-12347:1:jobs", $payload2); 453 | $worker4 = $this->createWorker("prod-rc-worker-12346:1:jobs", $payload1); 454 | $worker5 = $this->createWorker("prod-rc-worker-12347:1:jobs", $payload2); 455 | sleep(3); 456 | $worker6 = $this->createWorker("prod-worker-12348:1:jobs", $payload1); 457 | sleep(2); 458 | 459 | $this->assertCount(1, Resque::getJobsToRerun('prod-worker-', 4, ['Test_Job1'])); 460 | $this->assertEquals(3, Resque::getInProgressJobsCount('prod-worker-')); 461 | } 462 | 463 | public function testGetJobsToRerunWithoutPrefix() { 464 | $payload1 = array( 465 | 'class' => 'Test_Job1', 466 | 'args' => [[]] 467 | ); 468 | 469 | $payload2 = array( 470 | 'class' => 'Test_Job2', 471 | 'args' => [[]] 472 | ); 473 | 474 | $payload3 = array( 475 | 'class' => 'Test_Job1', 476 | 'args' => [['rerun' => true]] 477 | ); 478 | 479 | $worker1 = $this->createWorker("prod-worker-12345:1:jobs", $payload1); 480 | $worker2 = $this->createWorker("prod-worker-12346:1:jobs", $payload3); 481 | $worker3 = $this->createWorker("prod-worker-12347:1:jobs", $payload2); 482 | $worker4 = $this->createWorker("prod-rc-worker-12346:1:jobs", $payload1); 483 | $worker5 = $this->createWorker("prod-rc-worker-12347:1:jobs", $payload2); 484 | sleep(3); 485 | $worker6 = $this->createWorker("prod-worker-12348:1:jobs", $payload1); 486 | sleep(2); 487 | 488 | $this->assertCount(2, Resque::getJobsToRerun(null, 4, ['Test_Job1'])); 489 | $this->assertEquals(4, Resque::getInProgressJobsCount()); 490 | } 491 | 492 | private function createWorker($workerId, $payload): Resque_Worker { 493 | $worker = new Resque_Worker('jobs'); 494 | $worker->setId($workerId); 495 | $worker->registerWorker(); 496 | $worker->registerLogger(new MonologInit('', '')); 497 | $job = new Resque_Job('jobs', $payload); 498 | $worker->workingOn($job); 499 | return $worker; 500 | } 501 | } 502 | -------------------------------------------------------------------------------- /lib/Resque/Worker.php: -------------------------------------------------------------------------------- 1 | 36 | * @license http://www.opensource.org/licenses/mit-license.php 37 | */ 38 | class Resque_Worker 39 | { 40 | const LOG_NONE = 0; 41 | const LOG_NORMAL = 1; 42 | const LOG_VERBOSE = 2; 43 | 44 | 45 | const LOG_TYPE_DEBUG = 100; 46 | const LOG_TYPE_INFO = 200; 47 | const LOG_TYPE_WARNING = 300; 48 | const LOG_TYPE_ERROR = 400; 49 | const LOG_TYPE_CRITICAL = 500; 50 | const LOG_TYPE_ALERT = 550; 51 | 52 | public $logOutput = STDOUT; 53 | 54 | /** 55 | * @var int Current log level of this worker. 56 | */ 57 | public $logLevel = self::LOG_NONE; 58 | 59 | /** 60 | * @var array Array of all associated queues for this worker. 61 | */ 62 | protected $queues = array(); 63 | 64 | /** 65 | * @var string The hostname of this worker. 66 | */ 67 | protected $hostname; 68 | 69 | /** 70 | * @var boolean True if on the next iteration, the worker should shutdown. 71 | */ 72 | protected $shutdown = false; 73 | 74 | /** 75 | * @var boolean True if this worker is paused. 76 | */ 77 | protected $paused = false; 78 | 79 | /** 80 | * @var string String identifying this worker. 81 | */ 82 | protected $id; 83 | 84 | /** 85 | * @var Resque_Job Current job, if any, being processed by this worker. 86 | * 87 | */ 88 | protected $currentJob = null; 89 | 90 | /** 91 | * @var int Process ID of child worker processes. 92 | */ 93 | protected $child = null; 94 | 95 | protected $logger = null; 96 | 97 | /** 98 | * Return all workers known to Resque as instantiated instances. 99 | * @return array 100 | */ 101 | public static function all() 102 | { 103 | $workers = Resque::redis()->smembers(Resque::WORKERS); 104 | if (!is_array($workers)) { 105 | $workers = array(); 106 | } 107 | 108 | $instances = array(); 109 | foreach ($workers as $workerId) { 110 | $instances[] = self::find($workerId); 111 | } 112 | return $instances; 113 | } 114 | 115 | /** 116 | * Given a worker ID, check if it is registered/valid. 117 | * 118 | * @param string $workerId ID of the worker. 119 | * @return boolean True if the worker exists, false if not. 120 | */ 121 | public static function exists($workerId) 122 | { 123 | return (bool)Resque::redis()->sismember(Resque::WORKERS, $workerId); 124 | } 125 | 126 | /** 127 | * Given a worker ID, find it and return an instantiated worker class for it. 128 | * 129 | * @param string $workerId The ID of the worker. 130 | * @return Resque_Worker Instance of the worker. False if the worker does not exist. 131 | */ 132 | public static function find($workerId) 133 | { 134 | if (!self::exists($workerId) || false === strpos($workerId, ":")) { 135 | return false; 136 | } 137 | 138 | list($hostname, $pid, $queues) = explode(':', $workerId, 3); 139 | $queues = explode(',', $queues); 140 | $worker = new self($queues); 141 | $worker->setId($workerId); 142 | $worker->logger = $worker->getLogger($workerId); 143 | return $worker; 144 | } 145 | 146 | /** 147 | * Set the ID of this worker to a given ID string. 148 | * 149 | * @param string $workerId ID for the worker. 150 | */ 151 | public function setId($workerId) 152 | { 153 | $this->id = $workerId; 154 | } 155 | 156 | /** 157 | * Instantiate a new worker, given a list of queues that it should be working 158 | * on. The list of queues should be supplied in the priority that they should 159 | * be checked for jobs (first come, first served) 160 | * 161 | * Passing a single '*' allows the worker to work on all queues in alphabetical 162 | * order. You can easily add new queues dynamically and have them worked on using 163 | * this method. 164 | * 165 | * @param string|array $queues String with a single queue name, array with multiple. 166 | */ 167 | public function __construct($queues) 168 | { 169 | if (!is_array($queues)) { 170 | $queues = array($queues); 171 | } 172 | 173 | $this->queues = $queues; 174 | if (function_exists('gethostname')) { 175 | $hostname = gethostname(); 176 | } else { 177 | $hostname = php_uname('n'); 178 | } 179 | $this->hostname = $hostname; 180 | $this->id = $this->hostname . ':'.getmypid() . ':' . implode(',', $this->queues); 181 | } 182 | 183 | /** 184 | * The primary loop for a worker which when called on an instance starts 185 | * the worker's life cycle. 186 | * 187 | * Queues are checked every $interval (seconds) for new jobs. 188 | * 189 | * @param int $interval How often to check for new jobs across the queues. 190 | */ 191 | public function work($interval = 5) 192 | { 193 | $this->updateProcLine('Starting'); 194 | $this->startup(); 195 | 196 | while (true) { 197 | if ($this->shutdown) { 198 | break; 199 | } 200 | 201 | $this->workerPing(); 202 | 203 | // Attempt to find and reserve a job 204 | $job = false; 205 | if (!$this->paused) { 206 | try { 207 | $job = $this->reserve(); 208 | } 209 | catch (\RedisException $e) { 210 | $this->log(array('message' => 'Redis exception caught: ' . $e->getMessage(), 'data' => array('type' => 'fail', 'log' => $e->getMessage(), 'time' => time())), self::LOG_TYPE_ALERT); 211 | } 212 | } 213 | 214 | if (!$job) { 215 | // For an interval of 0, break now - helps with unit testing etc 216 | if ($interval == 0) { 217 | break; 218 | } 219 | // If no job was found, we sleep for $interval before continuing and checking again 220 | $this->log(array('message' => 'Sleeping for ' . $interval, 'data' => array('type' => 'sleep', 'second' => $interval)), self::LOG_TYPE_DEBUG); 221 | if ($this->paused) { 222 | $this->updateProcLine('Paused'); 223 | } else { 224 | $this->updateProcLine('Waiting for ' . implode(',', $this->queues)); 225 | } 226 | usleep($interval * 1000000); 227 | continue; 228 | } 229 | 230 | $this->log(array('message' => 'got ' . $job, 'data' => array('type' => 'got', 'args' => $job)), self::LOG_TYPE_INFO); 231 | Resque_Event::trigger('beforeFork', $job); 232 | $this->workingOn($job); 233 | 234 | $workerName = $this->hostname . ':'.getmypid(); 235 | 236 | $this->child = $this->fork(); 237 | 238 | // Forked and we're the child. Run the job. 239 | if ($this->child === 0 || $this->child === false) { 240 | $status = 'Processing ID:' . $job->payload['id'] . ' in ' . $job->queue; 241 | $this->updateProcLine($status); 242 | $this->log(array('message' => $status, 'data' => array('type' => 'process', 'worker' => $workerName, 'job_id' => $job->payload['id'])), self::LOG_TYPE_INFO); 243 | $this->perform($job); 244 | if ($this->child === 0) { 245 | exit(0); 246 | } 247 | } 248 | 249 | if ($this->child > 0) { 250 | // Parent process, sit and wait 251 | $status = 'Forked ' . $this->child . ' for ID:' . $job->payload['id']; 252 | $this->updateProcLine($status); 253 | $this->log(array('message' => $status, 'data' => array('type' => 'fork', 'worker' => $workerName, 'job_id' => $job->payload['id'])), self::LOG_TYPE_DEBUG); 254 | 255 | // Wait until the child process finishes before continuing 256 | pcntl_wait($status); 257 | $exitStatus = pcntl_wexitstatus($status); 258 | if ($exitStatus !== 0) { 259 | $job->fail(new Resque_Job_DirtyExitException('Job exited with exit code ' . $exitStatus)); 260 | } 261 | } 262 | 263 | $this->child = null; 264 | $this->doneWorking(); 265 | } 266 | 267 | $this->unregisterWorker(); 268 | } 269 | 270 | /** 271 | * Process a single job. 272 | * 273 | * @param Resque_Job $job The job to be processed. 274 | */ 275 | public function perform(Resque_Job $job) 276 | { 277 | $startTime = microtime(true); 278 | try { 279 | Resque_Event::trigger('afterFork', $job); 280 | $job->perform(); 281 | $this->log(array('message' => 'done ID:' . $job->payload['id'], 'data' => array('type' => 'done', 'job_id' => $job->payload['id'], 'time' => round(microtime(true) - $startTime, 3) * 1000)), self::LOG_TYPE_INFO); 282 | } catch (Exception $e) { 283 | $this->log(array('message' => $job . ' failed: ' . $e->getMessage(), 'data' => array('type' => 'fail', 'log' => $e->getMessage(), 'job_id' => $job->payload['id'], 'time' => round(microtime(true) - $startTime, 3) * 1000)), self::LOG_TYPE_ERROR); 284 | $job->fail($e); 285 | return; 286 | } 287 | 288 | $job->updateStatus(Resque_Job_Status::STATUS_COMPLETE); 289 | } 290 | 291 | /** 292 | * Attempt to find a job from the top of one of the queues for this worker. 293 | * 294 | * @return object|boolean Instance of Resque_Job if a job is found, false if not. 295 | */ 296 | public function reserve() 297 | { 298 | $queues = $this->queues(); 299 | if (!is_array($queues)) { 300 | return; 301 | } 302 | foreach ($queues as $queue) { 303 | $this->log(array('message' => 'Checking ' . $queue, 'data' => array('type' => 'check', 'queue' => $queue)), self::LOG_TYPE_DEBUG); 304 | $job = Resque_Job::reserve($queue); 305 | if ($job) { 306 | $this->log(array('message' => 'Found job on ' . $queue, 'data' => array('type' => 'found', 'queue' => $queue)), self::LOG_TYPE_DEBUG); 307 | return $job; 308 | } 309 | } 310 | 311 | return false; 312 | } 313 | 314 | /** 315 | * Return an array containing all of the queues that this worker should use 316 | * when searching for jobs. 317 | * 318 | * If * is found in the list of queues, every queue will be searched in 319 | * alphabetic order. (@see $fetch) 320 | * 321 | * @param boolean $fetch If true, and the queue is set to *, will fetch 322 | * all queue names from redis. 323 | * @return array Array of associated queues. 324 | */ 325 | public function queues($fetch = true) 326 | { 327 | if (!in_array('*', $this->queues) || $fetch == false) { 328 | return $this->queues; 329 | } 330 | 331 | $queues = Resque::queues(); 332 | sort($queues); 333 | return $queues; 334 | } 335 | 336 | /** 337 | * Attempt to fork a child process from the parent to run a job in. 338 | * 339 | * Return values are those of pcntl_fork(). 340 | * 341 | * @return int -1 if the fork failed, 0 for the forked child, the PID of the child for the parent. 342 | */ 343 | protected function fork() 344 | { 345 | if (!function_exists('pcntl_fork')) { 346 | return false; 347 | } 348 | 349 | $pid = pcntl_fork(); 350 | if ($pid === -1) { 351 | throw new RuntimeException('Unable to fork child worker.'); 352 | } 353 | 354 | return $pid; 355 | } 356 | 357 | /** 358 | * Perform necessary actions to start a worker. 359 | */ 360 | protected function startup() 361 | { 362 | $this->log(array('message' => 'Starting worker ' . $this, 'data' => array('type' => 'start', 'worker' => (string) $this)), self::LOG_TYPE_INFO); 363 | 364 | $this->registerSigHandlers(); 365 | 366 | if(!getenv('WORKER_NO_PRUNE_DEAD_WORKERS') === "TRUE") { 367 | $this->log(array('message' => 'Not pruning dead workers ' . $this, 'data' => array('type' => 'start', 'worker' => (string) $this)), self::LOG_TYPE_INFO); 368 | 369 | $this->pruneDeadWorkers(); 370 | } 371 | 372 | if(!getenv('WORKER_NO_TRIGGER') === "TRUE") { 373 | $this->log(array('message' => 'Not triggering event ' . $this, 'data' => array('type' => 'start', 'worker' => (string) $this)), self::LOG_TYPE_INFO); 374 | Resque_Event::trigger('beforeFirstFork', $this); 375 | } 376 | 377 | $this->registerWorker(); 378 | } 379 | 380 | /** 381 | * On supported systems (with the PECL proctitle module installed), update 382 | * the name of the currently running process to indicate the current state 383 | * of a worker. 384 | * 385 | * @param string $status The updated process title. 386 | */ 387 | protected function updateProcLine($status) 388 | { 389 | if (function_exists('setproctitle')) { 390 | setproctitle('resque-' . Resque::VERSION . ': ' . $status); 391 | } 392 | } 393 | 394 | /** 395 | * Register signal handlers that a worker should respond to. 396 | * 397 | * TERM: Shutdown immediately and stop processing jobs. 398 | * INT: Shutdown immediately and stop processing jobs. 399 | * QUIT: Shutdown after the current job finishes processing. 400 | * USR1: Kill the forked child immediately and continue processing jobs. 401 | */ 402 | protected function registerSigHandlers() 403 | { 404 | if (!function_exists('pcntl_signal')) { 405 | $this->log(array('message' => 'Signals handling is unsupported', 'data' => array('type' => 'signal')), self::LOG_TYPE_WARNING); 406 | return; 407 | } 408 | 409 | declare(ticks = 1); 410 | pcntl_signal(SIGTERM, array($this, 'shutDownNow')); 411 | pcntl_signal(SIGINT, array($this, 'shutDownNow')); 412 | pcntl_signal(SIGQUIT, array($this, 'shutdown')); 413 | pcntl_signal(SIGUSR1, array($this, 'killChild')); 414 | pcntl_signal(SIGUSR2, array($this, 'pauseProcessing')); 415 | pcntl_signal(SIGCONT, array($this, 'unPauseProcessing')); 416 | pcntl_signal(SIGPIPE, array($this, 'reestablishRedisConnection')); 417 | $this->log(array('message' => 'Registered signals', 'data' => array('type' => 'signal')), self::LOG_TYPE_DEBUG); 418 | } 419 | 420 | /** 421 | * Signal handler callback for USR2, pauses processing of new jobs. 422 | */ 423 | public function pauseProcessing() 424 | { 425 | $this->log(array('message' => 'USR2 received; pausing job processing', 'data' => array('type' => 'pause')), self::LOG_TYPE_INFO); 426 | $this->paused = true; 427 | } 428 | 429 | /** 430 | * Signal handler callback for CONT, resumes worker allowing it to pick 431 | * up new jobs. 432 | */ 433 | public function unPauseProcessing() 434 | { 435 | $this->log(array('message' => 'CONT received; resuming job processing', 'data' => array('type' => 'resume')), self::LOG_TYPE_INFO); 436 | $this->paused = false; 437 | } 438 | 439 | /** 440 | * Signal handler for SIGPIPE, in the event the redis connection has gone away. 441 | * Attempts to reconnect to redis, or raises an Exception. 442 | */ 443 | public function reestablishRedisConnection() 444 | { 445 | $this->log(array('message' => 'SIGPIPE received; attempting to reconnect', 'data' => array('type' => 'reconnect')), self::LOG_TYPE_INFO); 446 | Resque::redis()->establishConnection(); 447 | } 448 | 449 | /** 450 | * Schedule a worker for shutdown. Will finish processing the current job 451 | * and when the timeout interval is reached, the worker will shut down. 452 | */ 453 | public function shutdown() 454 | { 455 | $this->shutdown = true; 456 | $this->log(array('message' => 'Exiting...', 'data' => array('type' => 'shutdown')), self::LOG_TYPE_INFO); 457 | } 458 | 459 | /** 460 | * Force an immediate shutdown of the worker, killing any child jobs 461 | * currently running. 462 | */ 463 | public function shutdownNow() 464 | { 465 | $this->shutdown(); 466 | $this->killChild(); 467 | } 468 | 469 | /** 470 | * Kill a forked child job immediately. The job it is processing will not 471 | * be completed. 472 | */ 473 | public function killChild() 474 | { 475 | if (!$this->child) { 476 | $this->log(array('message' => 'No child to kill.', 'data' => array('type' => 'kill', 'child' => null)), self::LOG_TYPE_DEBUG); 477 | return; 478 | } 479 | 480 | $this->log(array('message' => 'Killing child at ' . $this->child, 'data' => array('type' => 'kill', 'child' => $this->child)), self::LOG_TYPE_DEBUG); 481 | if (exec('ps -o pid,state -p ' . $this->child, $output, $returnCode) && $returnCode != 1) { 482 | $this->log(array('message' => 'Killing child at ' . $this->child, 'data' => array('type' => 'kill', 'child' => $this->child)), self::LOG_TYPE_DEBUG); 483 | posix_kill($this->child, SIGKILL); 484 | $this->child = null; 485 | } else { 486 | $this->log(array('message' => 'Child ' . $this->child . ' not found, restarting.', 'data' => array('type' => 'kill', 'child' => $this->child)), self::LOG_TYPE_ERROR); 487 | $this->shutdown(); 488 | } 489 | } 490 | 491 | /** 492 | * Look for any workers which should be running on this server and if 493 | * they're not, remove them from Redis. 494 | * 495 | * This is a form of garbage collection to handle cases where the 496 | * server may have been killed and the Resque workers did not die gracefully 497 | * and therefore leave state information in Redis. 498 | */ 499 | public function pruneDeadWorkers() 500 | { 501 | $workerPids = $this->workerPids(); 502 | $workers = self::all(); 503 | foreach ($workers as $worker) { 504 | if (is_object($worker)) { 505 | list($host, $pid, $queues) = explode(':', (string)$worker, 3); 506 | if ($host != $this->hostname || in_array($pid, $workerPids) || $pid == getmypid()) { 507 | continue; 508 | } 509 | $this->log(array('message' => 'Pruning dead worker: ' . (string)$worker, 'data' => array('type' => 'prune')), self::LOG_TYPE_DEBUG); 510 | $worker->unregisterWorker(); 511 | } 512 | } 513 | } 514 | 515 | /** 516 | * Return an array of process IDs for all of the Resque workers currently 517 | * running on this machine. 518 | * 519 | * @return array Array of Resque worker process IDs. 520 | */ 521 | public function workerPids() 522 | { 523 | $pids = array(); 524 | exec('ps -A -o pid,comm | grep [r]esque', $cmdOutput); 525 | foreach ($cmdOutput as $line) { 526 | list($pids[]) = explode(' ', trim($line), 2); 527 | } 528 | return $pids; 529 | } 530 | 531 | /** 532 | * Register this worker in Redis. 533 | */ 534 | public function registerWorker() 535 | { 536 | Resque::redis()->multi(); 537 | $this->workerPing(); 538 | Resque::redis()->sadd(Resque::WORKERS, (string)$this); 539 | Resque::redis()->set(Resque::WORKER_PREFIX . (string)$this . Resque::STARTED_SUFFIX, (new DateTimeImmutable())->format('D M d H:i:s e Y')); 540 | Resque::redis()->exec(); 541 | } 542 | 543 | /** 544 | * Unregister this worker in Redis. (shutdown etc) 545 | */ 546 | public function unregisterWorker() 547 | { 548 | if (is_object($this->currentJob)) { 549 | $this->currentJob->fail(new Resque_Job_DirtyExitException); 550 | } 551 | 552 | Resque::workerCleanup((string)$this); 553 | } 554 | 555 | /** 556 | * Tell Redis which job we're currently working on. 557 | * 558 | * @param object $job Resque_Job instance containing the job we're working on. 559 | */ 560 | public function workingOn(Resque_Job $job) 561 | { 562 | $job->worker = $this; 563 | $this->currentJob = $job; 564 | $job->updateStatus(Resque_Job_Status::STATUS_RUNNING); 565 | $data = json_encode( 566 | array( 567 | 'queue' => $job->queue, 568 | 'run_at' => (new DateTimeImmutable())->format('D M d H:i:s e Y'), 569 | 'payload' => $job->payload 570 | ) 571 | ); 572 | Resque::redis()->hset(Resque::CURRENT_JOBS, (string)$job->worker, $data); 573 | } 574 | 575 | /** 576 | * Notify Redis that we've finished working on a job, clearing the working 577 | * state and incrementing the job stats. 578 | */ 579 | public function doneWorking() 580 | { 581 | $this->currentJob = null; 582 | Resque::redis()->multi(); 583 | Resque_Stat::incr(Resque::PROCESSED); 584 | Resque_Stat::incr(Resque::PROCESSED_PREFIX . (string)$this); 585 | Resque::redis()->hdel(Resque::CURRENT_JOBS, (string)$this); 586 | Resque::redis()->exec(); 587 | } 588 | 589 | /** 590 | * Generate a string representation of this worker. 591 | * 592 | * @return string String identifier for this worker instance. 593 | */ 594 | public function __toString() 595 | { 596 | return $this->id; 597 | } 598 | 599 | /** 600 | * Output a given log message to STDOUT. 601 | * 602 | * @param string $message Message to output. 603 | * @return boolean True if the message is logged 604 | */ 605 | public function log($message, $code = self::LOG_TYPE_INFO) 606 | { 607 | if ($this->logLevel === self::LOG_NONE) { 608 | return false; 609 | } 610 | 611 | /*if ($this->logger === null) { 612 | if ($this->logLevel === self::LOG_NORMAL && $code !== self::LOG_TYPE_DEBUG) { 613 | fwrite($this->logOutput, "*** " . $message['message'] . "\n"); 614 | } else if ($this->logLevel === self::LOG_VERBOSE) { 615 | fwrite($this->logOutput, "** [" . strftime('%T %Y-%m-%d') . "] " . $message['message'] . "\n"); 616 | } else { 617 | return false; 618 | } 619 | return true; 620 | } else {*/ 621 | $extra = array(); 622 | 623 | if (is_array($message)) { 624 | $extra = $message['data']; 625 | $message = $message['message']; 626 | } 627 | 628 | if (!isset($extra['worker'])) { 629 | if ($this->child > 0) { 630 | $extra['worker'] = $this->hostname . ':' . getmypid(); 631 | } else { 632 | list($host, $pid, $queues) = explode(':', (string) $this, 3); 633 | $extra['worker'] = $host . ':' . $pid; 634 | } 635 | } 636 | 637 | if (($this->logLevel === self::LOG_NORMAL || $this->logLevel === self::LOG_VERBOSE) && $code !== self::LOG_TYPE_DEBUG) { 638 | 639 | if ($this->logger === null) { 640 | fwrite($this->logOutput, "[" . date('c') . "] " . $message . "\n"); 641 | } else { 642 | switch ($code) { 643 | case self::LOG_TYPE_INFO: 644 | $this->logger->addInfo($message, $extra); 645 | break; 646 | case self::LOG_TYPE_WARNING: 647 | $this->logger->addWarning($message, $extra); 648 | break; 649 | case self::LOG_TYPE_ERROR: 650 | $this->logger->addError($message, $extra); 651 | break; 652 | case self::LOG_TYPE_CRITICAL: 653 | $this->logger->addCritical($message, $extra); 654 | break; 655 | case self::LOG_TYPE_ALERT: 656 | $this->logger->addAlert($message, $extra); 657 | } 658 | } 659 | 660 | 661 | 662 | } else if ($code === self::LOG_TYPE_DEBUG && $this->logLevel === self::LOG_VERBOSE) { 663 | if ($this->logger === null) { 664 | fwrite($this->logOutput, "[" . date('c') . "] " . $message . "\n"); 665 | } else { 666 | $this->logger->addDebug($message, $extra); 667 | } 668 | } else { 669 | return false; 670 | } 671 | 672 | return true; 673 | //} 674 | } 675 | 676 | public function registerLogger($logger = null) 677 | { 678 | $this->logger = $logger->getInstance(); 679 | Resque::redis()->hset(Resque::WORKER_LOGGER, (string)$this, json_encode(array($logger->handler, $logger->target))); 680 | } 681 | 682 | public function getLogger($workerId) 683 | { 684 | $settings = json_decode(Resque::redis()->hget(Resque::WORKER_LOGGER, (string)$workerId)); 685 | $logger = new MonologInit\MonologInit($settings[0], $settings[1]); 686 | return $logger->getInstance(); 687 | } 688 | 689 | /** 690 | * Return an object describing the job this worker is currently working on. 691 | * 692 | * @return object Object with details of current job. 693 | */ 694 | public function job() 695 | { 696 | $job = Resque::redis()->hget(Resque::CURRENT_JOBS, (string)$this); 697 | if (!$job) { 698 | return array(); 699 | } else { 700 | return json_decode($job, true); 701 | } 702 | } 703 | 704 | /** 705 | * Get a statistic belonging to this worker. 706 | * 707 | * @param string $stat Statistic to fetch. 708 | * @return int Statistic value. 709 | */ 710 | public function getStat($stat) 711 | { 712 | return Resque_Stat::get($stat . ':' . $this); 713 | } 714 | 715 | public function workerPing() { 716 | $key = Resque::WORKER_PREFIX . (string)$this . Resque::PING_SUFFIX; 717 | Resque::redis()->set($key, (new DateTimeImmutable())->format('D M d H:i:s e Y')); 718 | Resque::redis()->expire($key, 3600); 719 | } 720 | } 721 | --------------------------------------------------------------------------------