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 "";
27 | foreach ($particles as $particle) {
28 | echo "- {$particle}
";
29 | }
30 | 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 [](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 |
--------------------------------------------------------------------------------