├── .gitignore
├── extras
├── php-resque.png
├── resque.logrotate
├── resque-scheduler.monit
├── resque.monit
└── sample-plugin.php
├── demo
├── long_job.php
├── php_error_job.php
├── bad_job.php
├── resque.php
├── job.php
├── check_status.php
├── queue.php
└── init.php
├── test
├── misc
│ └── redis.conf
├── Resque
│ └── Tests
│ │ ├── ResqueTestCase.php
│ │ ├── StatTest.php
│ │ ├── JobPIDTest.php
│ │ ├── JobStatusTest.php
│ │ ├── RedisTest.php
│ │ ├── EventTest.php
│ │ ├── WorkerTest.php
│ │ └── JobHandlerTest.php
└── bootstrap.php
├── .github
├── dependabot.yml
├── ISSUE_TEMPLATE
│ ├── 3_Support_question.md
│ ├── 2_Feature_request.md
│ └── 1_Bug_report.md
├── PULL_REQUEST_TEMPLATE
│ ├── 4_Documentation.md
│ ├── 5_Tests.md
│ ├── 1_Bug_fix.md
│ ├── 2_New_feature.md
│ └── 3_Breaking_change.md
└── workflows
│ └── php-test.yml
├── .editorconfig
├── lib
├── Job
│ ├── FactoryInterface.php
│ ├── Factory.php
│ ├── Job.php
│ ├── PID.php
│ └── Status.php
├── Exceptions
│ ├── RedisException.php
│ ├── ResqueException.php
│ ├── DoNotPerformException.php
│ ├── DirtyExitException.php
│ ├── DoNotCreateException.php
│ └── InvalidTimestampException.php
├── Failure
│ ├── FailureInterface.php
│ └── RedisFailure.php
├── Stat.php
├── Event.php
├── FailureHandler.php
├── Worker
│ ├── SchedulerWorker.php
│ └── ResqueWorker.php
├── Scheduler.php
├── JobHandler.php
├── Redis.php
└── Resque.php
├── phpcs.xml.dist
├── phpunit.xml.dist
├── LICENSE
├── composer.json
├── bin
├── resque-scheduler
└── resque
├── CONTRIBUTING.md
├── CODE-OF-CONDUCT.md
├── CHANGELOG.md
├── HOWITWORKS.md
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | vendor/
2 | *.swp
3 | phpunit.xml
4 | composer.lock
5 |
--------------------------------------------------------------------------------
/extras/php-resque.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/resque/php-resque/HEAD/extras/php-resque.png
--------------------------------------------------------------------------------
/demo/long_job.php:
--------------------------------------------------------------------------------
1 | ');
7 | sleep(1);
8 | fwrite(STDOUT, 'Job ended!' . PHP_EOL);
9 | }
10 | }
--------------------------------------------------------------------------------
/extras/resque.logrotate:
--------------------------------------------------------------------------------
1 | /var/log/resque/*.log {
2 | daily
3 | missingok
4 | rotate 7
5 | compress
6 | compressoptions -4
7 | notifempty
8 | create 640 root adm
9 | copytruncate
10 | }
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "composer"
4 | directory: "/"
5 | schedule:
6 | interval: "daily"
7 |
8 | - package-ecosystem: "github-actions"
9 | directory: "/"
10 | schedule:
11 | interval: "weekly"
12 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | end_of_line = lf
6 | insert_final_newline = true
7 | trim_trailing_whitespace = true
8 | indent_style = tab
9 |
10 | [*.yml]
11 | indent_style = space
12 | indent_size = 2
13 |
14 | [*.md]
15 | trim_trailing_whitespace = false
16 |
--------------------------------------------------------------------------------
/lib/Job/FactoryInterface.php:
--------------------------------------------------------------------------------
1 |
10 | * @license http://www.opensource.org/licenses/mit-license.php
11 | */
12 | class RedisException extends ResqueException
13 | {
14 | }
15 |
--------------------------------------------------------------------------------
/lib/Exceptions/ResqueException.php:
--------------------------------------------------------------------------------
1 |
12 | * @license http://www.opensource.org/licenses/mit-license.php
13 | */
14 | class ResqueException extends Exception
15 | {
16 | }
17 |
--------------------------------------------------------------------------------
/lib/Exceptions/DoNotPerformException.php:
--------------------------------------------------------------------------------
1 |
12 | * @license http://www.opensource.org/licenses/mit-license.php
13 | */
14 | class DoNotPerformException extends Exception
15 | {
16 | }
17 |
--------------------------------------------------------------------------------
/lib/Exceptions/DirtyExitException.php:
--------------------------------------------------------------------------------
1 |
12 | * @license http://www.opensource.org/licenses/mit-license.php
13 | */
14 | class DirtyExitException extends RuntimeException
15 | {
16 | }
17 |
--------------------------------------------------------------------------------
/lib/Exceptions/DoNotCreateException.php:
--------------------------------------------------------------------------------
1 |
12 | * @license http://www.opensource.org/licenses/mit-license.php
13 | */
14 | class DoNotCreateException extends Exception
15 | {
16 | }
17 |
--------------------------------------------------------------------------------
/lib/Exceptions/InvalidTimestampException.php:
--------------------------------------------------------------------------------
1 |
10 | * @copyright (c) 2012 Chris Boulton
11 | * @license http://www.opensource.org/licenses/mit-license.php
12 | */
13 | class InvalidTimestampException extends ResqueException
14 | {
15 | }
16 |
--------------------------------------------------------------------------------
/phpcs.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
3 | PSR2 with tabs instead of spaces.
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/3_Support_question.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: "🧐 Support Question"
3 | about: I need assistance with php-resque
4 | labels: question
5 | ---
6 |
7 |
8 | ### My question:
9 |
10 |
11 | ## My Environment
12 |
13 | * PHP-Resque version:
14 | * PHP version:
15 | * Redis version:
16 | * Server type and version:
17 | * Operating System and version:
18 |
--------------------------------------------------------------------------------
/phpunit.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
14 |
15 | ./test/Resque/
16 |
17 |
18 |
19 |
20 |
21 | ./lib
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/lib/Job/Factory.php:
--------------------------------------------------------------------------------
1 | args = $args;
26 | $instance->queue = $queue;
27 | return $instance;
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/2_Feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: "🚀 Feature Request"
3 | about: I have a suggestion (and may want to implement it 🙂)!
4 | labels: "feature request"
5 | ---
6 |
7 |
8 | ## Expected Behavior
9 |
10 |
11 | ## Current Behavior
12 |
13 |
14 | ## Possible Solution
15 |
16 |
17 | ## Context
18 |
19 |
20 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE/4_Documentation.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: "📑 Documentation Only"
3 | about: A change that only affects the documentation, not the code itself!
4 | labels: docs
5 | ---
6 |
7 |
8 | ## Description
9 |
10 |
11 | ## Motivation and Context
12 |
13 |
14 |
15 |
16 | ## Checklist:
17 |
18 |
19 | - [ ] I have read the **CONTRIBUTING** document.
20 |
--------------------------------------------------------------------------------
/demo/check_status.php:
--------------------------------------------------------------------------------
1 | isTracking()) {
16 | die("Resque is not tracking the status of this job.\n");
17 | }
18 |
19 | echo "Tracking status of ".$argv[1].". Press [break] to stop.\n\n";
20 | while(true) {
21 | fwrite(STDOUT, "Status of ".$argv[1]." is: ".$status->get()."\n");
22 | sleep(1);
23 | }
24 |
--------------------------------------------------------------------------------
/demo/queue.php:
--------------------------------------------------------------------------------
1 | time(),
16 | 'array' => array(
17 | 'test' => 'test',
18 | ),
19 | );
20 | if (empty($argv[2])) {
21 | $jobId = \Resque\Resque::enqueue('default', $argv[1], $args, true);
22 | } else {
23 | $jobId = \Resque\Resque::enqueue($argv[1], $argv[2], $args, true);
24 | }
25 |
26 | echo "Queued job ".$jobId."\n\n";
27 |
--------------------------------------------------------------------------------
/extras/resque-scheduler.monit:
--------------------------------------------------------------------------------
1 | # Replace these with your own:
2 | # [PATH/TO/RESQUE]
3 | # [PATH/TO/RESQUE-SCHEDULER]
4 | # [UID]
5 | # [GID]
6 | # [APP_INCLUDE]
7 |
8 | check process resque-scheduler_worker
9 | with pidfile /var/run/resque/scheduler-worker.pid
10 | start program = "/bin/sh -c 'APP_INCLUDE=[APP_INCLUDE] RESQUE_PHP=[PATH/TO/RESQUE] PIDFILE=/var/run/resque/scheduler-worker.pid nohup php -f [PATH/TO/RESQUE-SCHEDULER]/resque-scheduler.php > /var/log/resque/scheduler-worker.log &'" as uid [UID] and gid [GID]
11 | stop program = "/bin/sh -c 'kill -s QUIT `cat /var/run/resque/scheduler-worker.pid` && rm -f /var/run/resque/scheduler-worker.pid; exit 0;'"
12 | if totalmem is greater than 300 MB for 10 cycles then restart # eating up memory?
13 | group resque-scheduler_workers
--------------------------------------------------------------------------------
/demo/init.php:
--------------------------------------------------------------------------------
1 | /var/log/resque/worker_[QUEUE].log &'" as uid [UID] and gid [GID]
13 | stop program = "/bin/sh -c 'kill -s QUIT `cat /var/run/resque/worker_[QUEUE].pid` && rm -f /var/run/resque/worker_[QUEUE].pid; exit 0;'"
14 | if totalmem is greater than 300 MB for 10 cycles then restart # eating up memory?
15 | group resque_workers
--------------------------------------------------------------------------------
/lib/Failure/FailureInterface.php:
--------------------------------------------------------------------------------
1 |
10 | * @license http://www.opensource.org/licenses/mit-license.php
11 | */
12 | interface FailureInterface
13 | {
14 | /**
15 | * Initialize a failed job class and save it (where appropriate).
16 | *
17 | * @param object $payload Object containing details of the failed job.
18 | * @param object $exception Instance of the exception that was thrown by the failed job.
19 | * @param object $worker Instance of \Resque\Worker\ResqueWorker that received the job.
20 | * @param string $queue The name of the queue the job was fetched from.
21 | */
22 | public function __construct($payload, $exception, $worker, $queue);
23 | }
24 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/1_Bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: "🐛 Bug Report"
3 | about: Report a general bug
4 | labels: bug
5 | ---
6 |
7 |
8 | ## Expected Behavior
9 |
10 |
11 | ## Current Behavior
12 |
13 |
14 | ## Possible Solution
15 |
16 |
17 | ## Steps to Reproduce
18 |
19 |
20 |
21 | ## Context
22 |
23 |
24 |
25 | ## My Environment
26 |
27 | * PHP-Resque version:
28 | * PHP version:
29 | * Redis version:
30 | * Server type and version:
31 | * Operating System and version:
32 |
--------------------------------------------------------------------------------
/test/Resque/Tests/ResqueTestCase.php:
--------------------------------------------------------------------------------
1 |
14 | * @license http://www.opensource.org/licenses/mit-license.php
15 | */
16 | class ResqueTestCase extends TestCase
17 | {
18 | protected $resque;
19 | protected $redis;
20 | protected $logger;
21 |
22 | public static function setUpBeforeClass(): void
23 | {
24 | date_default_timezone_set('UTC');
25 | }
26 |
27 | public function setUp(): void
28 | {
29 | $config = file_get_contents(REDIS_CONF);
30 | preg_match('#^\s*port\s+([0-9]+)#m', $config, $matches);
31 | $this->redis = new Credis_Client('localhost', $matches[1]);
32 |
33 | $this->logger = $this->getMockBuilder('Psr\Log\LoggerInterface')
34 | ->getMock();
35 |
36 | Resque::setBackend('redis://localhost:' . $matches[1]);
37 |
38 | // Flush redis
39 | $this->redis->flushAll();
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE/5_Tests.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: "🧪 Tests Only"
3 | about: A change that only affects the tests, not the code itself!
4 | labels: tests
5 | ---
6 |
7 |
8 | ## Description
9 |
10 |
11 | ## Motivation and Context
12 |
13 |
14 |
15 |
16 | ## How Has This Been Tested?
17 |
18 |
19 |
20 |
21 |
22 | ## Checklist:
23 |
24 |
25 | - [ ] My code follows the code style of this project.
26 | - [ ] I have read the **CONTRIBUTING** document.
27 | - [ ] All new and existing tests passed.
28 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | (c) PHP Resque Team
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining
4 | a copy of this software and associated documentation files (the
5 | "Software"), to deal in the Software without restriction, including
6 | without limitation the rights to use, copy, modify, merge, publish,
7 | distribute, sublicense, and/or sell copies of the Software, and to
8 | permit persons to whom the Software is furnished to do so, subject to
9 | the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be
12 | included in all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/lib/Job/Job.php:
--------------------------------------------------------------------------------
1 |
12 | * @license http://www.opensource.org/licenses/mit-license.php
13 | */
14 | abstract class Job
15 | {
16 | /**
17 | * Job arguments
18 | * @var array
19 | */
20 | public $args;
21 |
22 | /**
23 | * Associated JobHandler instance
24 | * @var JobHandler
25 | */
26 | public $job;
27 |
28 | /**
29 | * Name of the queue the job was in
30 | * @var string
31 | */
32 | public $queue;
33 |
34 | /**
35 | * Unique job ID
36 | * @var string
37 | */
38 | public $jobID;
39 |
40 | /**
41 | * (Optional) Job setup
42 | *
43 | * @return void
44 | */
45 | public function setUp(): void
46 | {
47 | // no-op
48 | }
49 |
50 | /**
51 | * (Optional) Job teardown
52 | *
53 | * @return void
54 | */
55 | public function tearDown(): void
56 | {
57 | // no-op
58 | }
59 |
60 | /**
61 | * Main method of the Job
62 | *
63 | * @return mixed|void
64 | */
65 | abstract public function perform();
66 | }
67 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE/1_Bug_fix.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: "🐛 Bug Fix"
3 | about: Fixes a bug!
4 | labels: bug
5 | ---
6 |
7 |
8 | ## Description
9 |
10 |
11 | ## Motivation and Context
12 |
13 |
14 |
15 |
16 | ## How Has This Been Tested?
17 |
18 |
19 |
20 |
21 |
22 | ## Checklist:
23 |
24 |
25 | - [ ] My code follows the code style of this project.
26 | - [ ] My change requires a change to the documentation.
27 | - [ ] I have updated the documentation accordingly.
28 | - [ ] I have read the **CONTRIBUTING** document.
29 | - [ ] I have added tests to cover my changes.
30 | - [ ] All new and existing tests passed.
31 |
--------------------------------------------------------------------------------
/lib/Job/PID.php:
--------------------------------------------------------------------------------
1 |
12 | * @license http://www.opensource.org/licenses/mit-license.php
13 | */
14 | class PID
15 | {
16 | /**
17 | * Create a new PID tracker item for the supplied job ID.
18 | *
19 | * @param string $id The ID of the job to track the PID of.
20 | */
21 | public static function create($id)
22 | {
23 | Resque::redis()->set('job:' . $id . ':pid', (string)getmypid());
24 | }
25 |
26 | /**
27 | * Fetch the PID for the process actually executing the job.
28 | *
29 | * @param string $id The ID of the job to get the PID of.
30 | *
31 | * @return int PID of the process doing the job (on non-forking OS, PID of the worker, otherwise forked PID).
32 | */
33 | public static function get($id)
34 | {
35 | return (int)Resque::redis()->get('job:' . $id . ':pid');
36 | }
37 |
38 | /**
39 | * Remove the PID tracker for the job.
40 | *
41 | * @param string $id The ID of the job to remove the tracker from.
42 | */
43 | public static function del($id)
44 | {
45 | Resque::redis()->del('job:' . $id . ':pid');
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE/2_New_feature.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: "🚀 New Feature"
3 | about: Adds a new feature, or enhances an existing one!
4 | labels: "feature request"
5 | ---
6 |
7 |
8 | ## Description
9 |
10 |
11 | ## Motivation and Context
12 |
13 |
14 |
15 |
16 | ## How Has This Been Tested?
17 |
18 |
19 |
20 |
21 |
22 | ## Checklist:
23 |
24 |
25 | - [ ] My code follows the code style of this project.
26 | - [ ] My change requires a change to the documentation.
27 | - [ ] I have updated the documentation accordingly.
28 | - [ ] I have read the **CONTRIBUTING** document.
29 | - [ ] I have added tests to cover my changes.
30 | - [ ] All new and existing tests passed.
31 |
--------------------------------------------------------------------------------
/lib/Failure/RedisFailure.php:
--------------------------------------------------------------------------------
1 |
13 | * @license http://www.opensource.org/licenses/mit-license.php
14 | */
15 |
16 | class RedisFailure implements FailureInterface
17 | {
18 | /**
19 | * Initialize a failed job class and save it (where appropriate).
20 | *
21 | * @param object $payload Object containing details of the failed job.
22 | * @param object $exception Instance of the exception that was thrown by the failed job.
23 | * @param object $worker Instance of \Resque\Worker\ResqueWorker that received the job.
24 | * @param string $queue The name of the queue the job was fetched from.
25 | */
26 | public function __construct($payload, $exception, $worker, $queue)
27 | {
28 | $data = new stdClass();
29 | $data->failed_at = date('c');
30 | $data->payload = $payload;
31 | $data->exception = get_class($exception);
32 | $data->error = $exception->getMessage();
33 | $data->backtrace = explode("\n", $exception->getTraceAsString());
34 | $data->worker = (string)$worker;
35 | $data->queue = $queue;
36 | $data = json_encode($data);
37 | Resque::redis()->rpush('failed', $data);
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE/3_Breaking_change.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: "💥 Breaking Change"
3 | about: A change that might BREAK THINGS in existing code!
4 | labels: break
5 | ---
6 |
7 |
8 | ## Description
9 |
10 |
11 | ## Motivation and Context
12 |
13 |
14 |
15 |
16 |
17 |
18 | ## How Has This Been Tested?
19 |
20 |
21 |
22 |
23 |
24 | ## Checklist:
25 |
26 |
27 | - [ ] My code follows the code style of this project.
28 | - [ ] My change requires a change to the documentation.
29 | - [ ] I have updated the documentation accordingly.
30 | - [ ] I have read the **CONTRIBUTING** document.
31 | - [ ] I have added tests to cover my changes.
32 | - [ ] All new and existing tests passed.
33 |
--------------------------------------------------------------------------------
/test/Resque/Tests/StatTest.php:
--------------------------------------------------------------------------------
1 |
12 | * @license http://www.opensource.org/licenses/mit-license.php
13 | */
14 | class StatTest extends ResqueTestCase
15 | {
16 | public function testStatCanBeIncremented()
17 | {
18 | Stat::incr('test_incr');
19 | Stat::incr('test_incr');
20 | $this->assertEquals(2, $this->redis->get('resque:stat:test_incr'));
21 | }
22 |
23 | public function testStatCanBeIncrementedByX()
24 | {
25 | Stat::incr('test_incrX', 10);
26 | Stat::incr('test_incrX', 11);
27 | $this->assertEquals(21, $this->redis->get('resque:stat:test_incrX'));
28 | }
29 |
30 | public function testStatCanBeDecremented()
31 | {
32 | Stat::incr('test_decr', 22);
33 | Stat::decr('test_decr');
34 | $this->assertEquals(21, $this->redis->get('resque:stat:test_decr'));
35 | }
36 |
37 | public function testStatCanBeDecrementedByX()
38 | {
39 | Stat::incr('test_decrX', 22);
40 | Stat::decr('test_decrX', 11);
41 | $this->assertEquals(11, $this->redis->get('resque:stat:test_decrX'));
42 | }
43 |
44 | public function testGetStatByName()
45 | {
46 | Stat::incr('test_get', 100);
47 | $this->assertEquals(100, Stat::get('test_get'));
48 | }
49 |
50 | public function testGetUnknownStatReturns0()
51 | {
52 | $this->assertEquals(0, Stat::get('test_get_unknown'));
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/test/Resque/Tests/JobPIDTest.php:
--------------------------------------------------------------------------------
1 |
14 | * @license http://www.opensource.org/licenses/mit-license.php
15 | */
16 | class JobPIDTest extends ResqueTestCase
17 | {
18 | /**
19 | * @var \Resque\Worker\ResqueWorker
20 | */
21 | protected $worker;
22 |
23 | public function setUp(): void
24 | {
25 | parent::setUp();
26 |
27 | // Register a worker to test with
28 | $this->worker = new ResqueWorker('jobs');
29 | $this->worker->setLogger($this->logger);
30 | }
31 |
32 | public function testQueuedJobDoesNotReturnPID()
33 | {
34 | $this->logger->expects($this->never())
35 | ->method('log');
36 |
37 | $token = Resque::enqueue('jobs', 'Test_Job', [], true);
38 | $this->assertEquals(0, PID::get($token));
39 | }
40 |
41 | public function testRunningJobReturnsPID()
42 | {
43 | // Cannot use InProgress_Job on non-forking OS.
44 | if(!function_exists('pcntl_fork')) return;
45 |
46 | $token = Resque::enqueue('jobs', 'InProgress_Job', [], true);
47 | $this->worker->work(0);
48 | $this->assertNotEquals(0, PID::get($token));
49 | }
50 |
51 | public function testFinishedJobDoesNotReturnPID()
52 | {
53 | $token = Resque::enqueue('jobs', 'Test_Job', [], true);
54 | $this->worker->work(0);
55 | $this->assertEquals(0, PID::get($token));
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/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\Exceptions\DoNotPerformException;
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 | }
50 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "resque/php-resque",
3 | "type": "library",
4 | "description": "Redis backed library for creating background jobs and processing them later. Based on resque for Ruby.",
5 | "keywords": ["job", "background", "redis", "resque"],
6 | "homepage": "http://www.github.com/resque/php-resque/",
7 | "license": "MIT",
8 | "authors": [
9 | {
10 | "name": "Dan Hunsaker",
11 | "email": "danhunsaker+resque@gmail.com",
12 | "role": "Maintainer"
13 | },
14 | {
15 | "name": "Rajib Ahmed",
16 | "homepage": "https://github.com/rajibahmed",
17 | "role": "Maintainer"
18 | },
19 | {
20 | "name": "Steve Klabnik",
21 | "email": "steve@steveklabnik.com",
22 | "role": "Maintainer"
23 | },
24 | {
25 | "name": "Chris Boulton",
26 | "email": "chris@bigcommerce.com",
27 | "role": "Creator"
28 | }
29 | ],
30 | "require": {
31 | "php": ">=7.3.0",
32 | "colinmollenhour/credis": "~1.7",
33 | "psr/log": ">=1.1.0"
34 | },
35 | "suggest": {
36 | "ext-pcntl": "REQUIRED for forking processes on platforms that support it (so anything but Windows).",
37 | "ext-proctitle": "Allows php-resque to rename the title of UNIX processes to show the status of a worker.",
38 | "ext-redis": "Native PHP extension for Redis connectivity. Credis will automatically utilize when available."
39 | },
40 | "require-dev": {
41 | "phpunit/phpunit": ">=9.0 <9.6",
42 | "squizlabs/php_codesniffer": "^3.10",
43 | "phpstan/phpstan": "^1.12"
44 | },
45 | "bin": [
46 | "bin/resque",
47 | "bin/resque-scheduler"
48 | ],
49 | "autoload": {
50 | "psr-4": {
51 | "Resque\\": "lib"
52 | }
53 | },
54 | "autoload-dev": {
55 | "psr-4": {
56 | "Resque\\Tests\\": "test/Resque/Tests"
57 | }
58 | },
59 | "extra": {
60 | "branch-alias": {
61 | "dev-master": "1.0-dev"
62 | }
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/lib/Stat.php:
--------------------------------------------------------------------------------
1 |
10 | * @license http://www.opensource.org/licenses/mit-license.php
11 | */
12 | class Stat
13 | {
14 | /**
15 | * Get the value of the supplied statistic counter for the specified statistic.
16 | *
17 | * @param string $stat The name of the statistic to get the stats for.
18 | * @return mixed Value of the statistic.
19 | */
20 | public static function get($stat)
21 | {
22 | return (int)Resque::redis()->get('stat:' . $stat);
23 | }
24 |
25 | /**
26 | * Increment the value of the specified statistic by a certain amount (default is 1)
27 | *
28 | * @param string $stat The name of the statistic to increment.
29 | * @param int $by The amount to increment the statistic by.
30 | * @return boolean True if successful, false if not.
31 | */
32 | public static function incr($stat, $by = 1)
33 | {
34 | return (bool)Resque::redis()->incrby('stat:' . $stat, $by);
35 | }
36 |
37 | /**
38 | * Decrement the value of the specified statistic by a certain amount (default is 1)
39 | *
40 | * @param string $stat The name of the statistic to decrement.
41 | * @param int $by The amount to decrement the statistic by.
42 | * @return boolean True if successful, false if not.
43 | */
44 | public static function decr($stat, $by = 1)
45 | {
46 | return (bool)Resque::redis()->decrby('stat:' . $stat, $by);
47 | }
48 |
49 | /**
50 | * Delete a statistic with the given name.
51 | *
52 | * @param string $stat The name of the statistic to delete.
53 | * @return boolean True if successful, false if not.
54 | */
55 | public static function clear($stat)
56 | {
57 | return (bool)Resque::redis()->del('stat:' . $stat);
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/lib/Event.php:
--------------------------------------------------------------------------------
1 |
10 | * @license http://www.opensource.org/licenses/mit-license.php
11 | */
12 | class Event
13 | {
14 | /**
15 | * @var array Array containing all registered callbacks, indexked by event name.
16 | */
17 | private static $events = array();
18 |
19 | /**
20 | * Raise a given event with the supplied data.
21 | *
22 | * @param string $event Name of event to be raised.
23 | * @param mixed $data Optional, any data that should be passed to each callback.
24 | * @return true
25 | */
26 | public static function trigger($event, $data = null)
27 | {
28 | if (!is_array($data)) {
29 | $data = array($data);
30 | }
31 |
32 | if (empty(self::$events[$event])) {
33 | return true;
34 | }
35 |
36 | foreach (self::$events[$event] as $callback) {
37 | if (!is_callable($callback)) {
38 | continue;
39 | }
40 | call_user_func_array($callback, $data);
41 | }
42 |
43 | return true;
44 | }
45 |
46 | /**
47 | * Listen in on a given event to have a specified callback fired.
48 | *
49 | * @param string $event Name of event to listen on.
50 | * @param mixed $callback Any callback callable by call_user_func_array.
51 | * @return true
52 | */
53 | public static function listen($event, $callback)
54 | {
55 | if (!isset(self::$events[$event])) {
56 | self::$events[$event] = array();
57 | }
58 |
59 | self::$events[$event][] = $callback;
60 | return true;
61 | }
62 |
63 | /**
64 | * Stop a given callback from listening on a specific event.
65 | *
66 | * @param string $event Name of event.
67 | * @param mixed $callback The callback as defined when listen() was called.
68 | * @return true
69 | */
70 | public static function stopListening($event, $callback)
71 | {
72 | if (!isset(self::$events[$event])) {
73 | return true;
74 | }
75 |
76 | $key = array_search($callback, self::$events[$event]);
77 | if ($key !== false) {
78 | unset(self::$events[$event][$key]);
79 | }
80 |
81 | return true;
82 | }
83 |
84 | /**
85 | * Call all registered listeners.
86 | */
87 | public static function clearListeners()
88 | {
89 | self::$events = array();
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/bin/resque-scheduler:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env php
2 | logLevel = $logLevel;
74 |
75 | $PIDFILE = getenv('PIDFILE');
76 | if ($PIDFILE) {
77 | file_put_contents($PIDFILE, getmypid()) or
78 | die('Could not write PID information to ' . $PIDFILE);
79 | }
80 |
81 | fwrite(STDOUT, "*** Starting scheduler worker\n");
82 | $worker->work($interval);
83 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to PHP-Resque
2 |
3 | First of all: thank you! We appreciate any help you can give PHP-Resque.
4 |
5 | Second: before you participate in PHP-Resque, be sure to read our [Code of
6 | Conduct](CODE-OF-CONDUCT.md). Participation indicates you accept and agree to
7 | abide by the guidelines there.
8 |
9 | The main way to contribute to PHP-Resque is to write some code! Here's how:
10 |
11 | 1. [Fork](https://help.github.com/articles/fork-a-repo) PHP-Resque
12 | 2. Clone your fork - `git clone git@github.com/your-username/php-resque`
13 | 3. Be sure to start from the `develop` branch! - `git checkout develop`
14 | 4. Create a topic branch - `git checkout -b my_branch`
15 | 5. Push to your branch - `git push origin my_branch`
16 | 6. Create a [Pull Request](http://help.github.com/pull-requests/) from your
17 | branch
18 | 7. That's it!
19 |
20 | If you're not just doing some sort of refactoring, a CHANGELOG entry is
21 | appropriate. Please include them in pull requests adding features or fixing
22 | bugs.
23 |
24 | Oh, and 80 character columns, please!
25 |
26 | ## Tests
27 |
28 | We use PHPUnit for testing. A simple `vendor/bin/phpunit` will run all the
29 | tests. Make sure they pass when you submit a pull request.
30 |
31 | Please include tests with your pull request.
32 |
33 | ## Documentation
34 |
35 | Writing docs is really important. Please include docs in your pull requests.
36 |
37 | ## Bugs & Feature Requests
38 |
39 | You can file bugs on the [issues
40 | tracker](https://github.com/resque/php-resque/issues), and tag them with 'bug'.
41 |
42 | When filing a bug, please follow these tips to help us help you:
43 |
44 | ### Fill In - Don't Replace! - The Template
45 |
46 | The sections in the issue template are there to ensure we get all the
47 | information we need to reproduce and fix your bug. Follow the prompts in there,
48 | and everything should be great!
49 |
50 | ### Reproduction
51 |
52 | If possible, please provide some sort of executable reproduction of the issue.
53 | Your application has a lot of things in it, and it might be a complex
54 | interaction between components that causes the issue.
55 |
56 | To reproduce the issue, please make a simple Job that demonstrates the essence
57 | of the issue. If the basic job doesn't demonstrate the issue, the issue may be
58 | in another package pulled in by Composer.
59 |
60 | ### Version information
61 |
62 | If you can't provide a reproduction, a copy of your `composer.lock` would be
63 | helpful.
64 |
--------------------------------------------------------------------------------
/lib/FailureHandler.php:
--------------------------------------------------------------------------------
1 |
14 | * @license http://www.opensource.org/licenses/mit-license.php
15 | */
16 | class FailureHandler
17 | {
18 | /**
19 | * @var string Class name representing the backend to pass failed jobs off to.
20 | */
21 | private static $backend;
22 |
23 | /**
24 | * Create a new failed job on the backend.
25 | *
26 | * @param object $payload The contents of the job that has just failed.
27 | * @param \Exception $exception The exception generated when the job failed to run.
28 | * @param \Resque\Worker\ResqueWorker $worker Instance of Resque\Worker\ResqueWorker
29 | * that was running this job when it failed.
30 | * @param string $queue The name of the queue that this job was fetched from.
31 | */
32 | public static function create($payload, Exception $exception, ResqueWorker $worker, $queue)
33 | {
34 | $backend = self::getBackend();
35 | new $backend($payload, $exception, $worker, $queue);
36 | }
37 |
38 | /**
39 | * Create a new failed job on the backend from PHP 7 errors.
40 | *
41 | * @param object $payload The contents of the job that has just failed.
42 | * @param \Error $exception The PHP 7 error generated when the job failed to run.
43 | * @param \Resque\Worker\ResqueWorker $worker Instance of Resque\Worker\ResqueWorker
44 | * that was running this job when it failed.
45 | * @param string $queue The name of the queue that this job was fetched from.
46 | */
47 | public static function createFromError($payload, Error $exception, ResqueWorker $worker, $queue)
48 | {
49 | $backend = self::getBackend();
50 | new $backend($payload, $exception, $worker, $queue);
51 | }
52 |
53 | /**
54 | * Return an instance of the backend for saving job failures.
55 | *
56 | * @return object Instance of backend object.
57 | */
58 | public static function getBackend()
59 | {
60 | if (self::$backend === null) {
61 | self::$backend = 'Resque\Failure\RedisFailure';
62 | }
63 |
64 | return self::$backend;
65 | }
66 |
67 | /**
68 | * Set the backend to use for raised job failures. The supplied backend
69 | * should be the name of a class to be instantiated when a job fails.
70 | * It is your responsibility to have the backend class loaded (or autoloaded)
71 | *
72 | * @param string $backend The class name of the backend to pipe failures to.
73 | */
74 | public static function setBackend($backend)
75 | {
76 | self::$backend = $backend;
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/.github/workflows/php-test.yml:
--------------------------------------------------------------------------------
1 | name: PHP Tests
2 | on: [push, pull_request]
3 |
4 | jobs:
5 | phpunit:
6 | runs-on: ubuntu-latest
7 | continue-on-error: ${{ matrix.experimental }}
8 | name: "PHP-${{ matrix.php-versions }}: PHPUnit"
9 | strategy:
10 | matrix:
11 | php-versions: ['7.3', '7.4']
12 | experimental: [false]
13 | include:
14 | - php-versions: '8.0'
15 | experimental: true
16 | - php-versions: '8.1'
17 | experimental: true
18 | - php-versions: '8.2'
19 | experimental: true
20 | - php-versions: '8.3'
21 | experimental: true
22 | - php-versions: '8.4'
23 | experimental: true
24 | steps:
25 | - name: Checkout
26 | uses: actions/checkout@v6
27 |
28 | - name: Setup PHP
29 | uses: shivammathur/setup-php@v2
30 | with:
31 | php-version: ${{ matrix.php-versions }}
32 | extensions: redis
33 |
34 | - name: Setup problem matchers for PHP
35 | run: echo "::add-matcher::${{ runner.tool_cache }}/php.json"
36 |
37 | - name: Install dependencies
38 | run: composer install
39 |
40 | - name: Install redis
41 | run: sudo apt-get install -y redis-server
42 |
43 | - name: Setup problem matchers for PHPUnit
44 | run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json"
45 |
46 | - name: Run PHPunit
47 | run: ./vendor/bin/phpunit --configuration phpunit.xml.dist
48 |
49 | phpcs:
50 | runs-on: ubuntu-latest
51 | continue-on-error: false
52 | name: "PHPCS"
53 | steps:
54 | - name: Checkout
55 | uses: actions/checkout@v6
56 |
57 | - name: Setup PHP
58 | uses: shivammathur/setup-php@v2
59 | with:
60 | php-version: '7.4'
61 | tools: cs2pr
62 |
63 | - name: Install dependencies
64 | run: composer install
65 |
66 | - name: Setup problem matchers for PHP
67 | run: echo "::add-matcher::${{ runner.tool_cache }}/php.json"
68 |
69 | - name: Run PHPCS
70 | run: ./vendor/bin/phpcs -q --report=checkstyle lib | cs2pr
71 |
72 | phpstan:
73 | runs-on: ubuntu-latest
74 | continue-on-error: false
75 | name: "PHPStan"
76 | steps:
77 | - name: Checkout
78 | uses: actions/checkout@v6
79 |
80 | - name: Setup PHPStan
81 | uses: shivammathur/setup-php@v2
82 | with:
83 | php-version: '7.4'
84 |
85 | - name: Setup problem matchers for PHP
86 | run: echo "::add-matcher::${{ runner.tool_cache }}/php.json"
87 |
88 | - name: Install dependencies
89 | run: composer install
90 |
91 | - name: Run PHPStan
92 | run: ./vendor/bin/phpstan analyse lib -l1
93 |
94 |
--------------------------------------------------------------------------------
/CODE-OF-CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, sex characteristics, gender identity and expression,
9 | level of experience, education, socio-economic status, nationality, personal
10 | appearance, race, religion, or sexual identity and orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | * Using welcoming and inclusive language
18 | * Being respectful of differing viewpoints and experiences
19 | * Gracefully accepting constructive criticism
20 | * Focusing on what is best for the community
21 | * Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | * The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | * Trolling, insulting/derogatory comments, and personal or political attacks
28 | * Public or private harassment
29 | * Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | * Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at . All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at
72 |
73 | [homepage]: https://www.contributor-covenant.org
74 |
75 | For answers to common questions about this code of conduct, see
76 |
77 |
--------------------------------------------------------------------------------
/test/bootstrap.php:
--------------------------------------------------------------------------------
1 |
7 | * @license http://www.opensource.org/licenses/mit-license.php
8 | */
9 |
10 | $loader = require __DIR__ . '/../vendor/autoload.php';
11 |
12 | define('TEST_MISC', realpath(__DIR__ . '/misc/'));
13 | define('REDIS_CONF', TEST_MISC . '/redis.conf');
14 |
15 | // Attempt to start our own redis instance for tesitng.
16 | exec('which redis-server', $output, $returnVar);
17 | if($returnVar != 0) {
18 | echo "Cannot find redis-server in path. Please make sure redis is installed.\n";
19 | exit(1);
20 | }
21 |
22 | exec('cd ' . TEST_MISC . '; redis-server ' . REDIS_CONF, $output, $returnVar);
23 | usleep(500000);
24 | if($returnVar != 0) {
25 | echo "Cannot start redis-server.\n";
26 | exit(1);
27 |
28 | }
29 |
30 | // Get redis port from conf
31 | $config = file_get_contents(REDIS_CONF);
32 | if(!preg_match('#^\s*port\s+([0-9]+)#m', $config, $matches)) {
33 | echo "Could not determine redis port from redis.conf";
34 | exit(1);
35 | }
36 |
37 | \Resque\Resque::setBackend('localhost:' . $matches[1]);
38 |
39 | // Shutdown
40 | function killRedis($pid)
41 | {
42 | if (getmypid() !== $pid) {
43 | return; // don't kill from a forked worker
44 | }
45 | $config = file_get_contents(REDIS_CONF);
46 | if(!preg_match('#^\s*pidfile\s+([^\s]+)#m', $config, $matches)) {
47 | return;
48 | }
49 |
50 | $pidFile = TEST_MISC . '/' . $matches[1];
51 | if (file_exists($pidFile)) {
52 | $pid = trim(file_get_contents($pidFile));
53 | posix_kill((int) $pid, 9);
54 |
55 | if(is_file($pidFile)) {
56 | unlink($pidFile);
57 | }
58 | }
59 |
60 | // Remove the redis database
61 | if(!preg_match('#^\s*dir\s+([^\s]+)#m', $config, $matches)) {
62 | return;
63 | }
64 | $dir = $matches[1];
65 |
66 | if(!preg_match('#^\s*dbfilename\s+([^\s]+)#m', $config, $matches)) {
67 | return;
68 | }
69 |
70 | $filename = TEST_MISC . '/' . $dir . '/' . $matches[1];
71 | if(is_file($filename)) {
72 | unlink($filename);
73 | }
74 | }
75 | register_shutdown_function('killRedis', getmypid());
76 |
77 | if(function_exists('pcntl_signal')) {
78 | // Override INT and TERM signals, so they do a clean shutdown and also
79 | // clean up redis-server as well.
80 | function sigint()
81 | {
82 | exit;
83 | }
84 | pcntl_signal(SIGINT, 'sigint');
85 | pcntl_signal(SIGTERM, 'sigint');
86 | }
87 |
88 | class Test_Job extends \Resque\Job\Job
89 | {
90 | public static $called = false;
91 |
92 | public function perform()
93 | {
94 | self::$called = true;
95 | }
96 | }
97 |
98 | class Returning_Job extends \Resque\Job\Job
99 | {
100 | public function perform()
101 | {
102 | return $this->args['return'] ?? NULL;
103 | }
104 | }
105 |
106 | class Failing_Job_Exception extends \Exception
107 | {
108 |
109 | }
110 |
111 | class Failing_Job extends \Resque\Job\Job
112 | {
113 | public function perform()
114 | {
115 | throw new Failing_Job_Exception('Message!');
116 | }
117 | }
118 |
119 | /**
120 | * This job exits the forked worker process, which simulates the job being (forever) in progress,
121 | * so that we can verify the state of the system for "running jobs". Does not work on a non-forking OS.
122 | *
123 | * CAUTION Use this test job only with Worker::work, i.e. only when you actually trigger the fork in tests.
124 | */
125 | class InProgress_Job extends \Resque\Job\Job
126 | {
127 | public function perform()
128 | {
129 | if(!function_exists('pcntl_fork')) {
130 | // We can't lose the worker on a non-forking OS.
131 | throw new Failing_Job_Exception('Do not use InProgress_Job for tests on non-forking OS!');
132 | }
133 | exit(0);
134 | }
135 | }
136 |
137 | class Test_Job_With_SetUp extends \Resque\Job\Job
138 | {
139 | public static $called = false;
140 | public $args = [];
141 |
142 | public function setUp(): void
143 | {
144 | self::$called = true;
145 | }
146 |
147 | public function perform()
148 | {
149 |
150 | }
151 | }
152 |
153 |
154 | class Test_Job_With_TearDown extends \Resque\Job\Job
155 | {
156 | public static $called = false;
157 | public $args = [];
158 |
159 | public function perform()
160 | {
161 |
162 | }
163 |
164 | public function tearDown(): void
165 | {
166 | self::$called = true;
167 | }
168 | }
169 |
170 | class Test_Infinite_Recursion_Job extends \Resque\Job\Job
171 | {
172 | public function perform()
173 | {
174 | $this->perform();
175 | }
176 | }
177 |
--------------------------------------------------------------------------------
/test/Resque/Tests/JobStatusTest.php:
--------------------------------------------------------------------------------
1 |
16 | * @license http://www.opensource.org/licenses/mit-license.php
17 | */
18 | class JobStatusTest extends ResqueTestCase
19 | {
20 | /**
21 | * @var \Resque\Worker\ResqueWorker
22 | */
23 | protected $worker;
24 |
25 | public function setUp(): void
26 | {
27 | parent::setUp();
28 |
29 | // Register a worker to test with
30 | $this->worker = new ResqueWorker('jobs');
31 | $this->worker->setLogger($this->logger);
32 | }
33 |
34 | /**
35 | * Unit test data provider for potential return values from perform().
36 | *
37 | * @return array
38 | */
39 | public static function performResultProvider(): array
40 | {
41 | $data = [];
42 |
43 | $data['boolean'] = [ true ];
44 | $data['float'] = [ 1.0 ];
45 | $data['integer'] = [ 100 ];
46 | $data['string'] = [ 'string' ];
47 | $data['null'] = [ null ];
48 | $data['array'] = [[ 'key' => 'value' ]];
49 |
50 | return $data;
51 | }
52 |
53 | public function testJobStatusCanBeTracked()
54 | {
55 | $token = Resque::enqueue('jobs', 'Test_Job', [], true);
56 | $status = new Status($token);
57 | $this->assertTrue($status->isTracking());
58 | }
59 |
60 | public function testJobStatusIsReturnedViaJobInstance()
61 | {
62 | $token = Resque::enqueue('jobs', 'Test_Job', [], true);
63 | $job = JobHandler::reserve('jobs');
64 | $this->assertEquals(Status::STATUS_WAITING, $job->getStatus());
65 | }
66 |
67 | public function testQueuedJobReturnsQueuedStatus()
68 | {
69 | $token = Resque::enqueue('jobs', 'Test_Job', [], true);
70 | $status = new Status($token);
71 | $this->assertEquals(Status::STATUS_WAITING, $status->get());
72 | }
73 |
74 | public function testRunningJobReturnsRunningStatus()
75 | {
76 | $token = Resque::enqueue('jobs', 'Failing_Job', [], true);
77 | $job = $this->worker->reserve();
78 | $this->worker->workingOn($job);
79 | $status = new Status($token);
80 | $this->assertEquals(Status::STATUS_RUNNING, $status->get());
81 | }
82 |
83 | public function testFailedJobReturnsFailedStatus()
84 | {
85 | $token = Resque::enqueue('jobs', 'Failing_Job', [], true);
86 | $this->worker->work(0);
87 | $status = new Status($token);
88 | $this->assertEquals(Status::STATUS_FAILED, $status->get());
89 | }
90 |
91 | public function testCompletedJobReturnsCompletedStatus()
92 | {
93 | $token = Resque::enqueue('jobs', 'Test_Job', [], true);
94 | $this->worker->work(0);
95 | $status = new Status($token);
96 | $this->assertEquals(Status::STATUS_COMPLETE, $status->get());
97 | }
98 |
99 | /**
100 | * @param mixed $value Potential return value from perform()
101 | *
102 | * @dataProvider performResultProvider
103 | */
104 | public function testCompletedJobReturnsResult($value)
105 | {
106 | $token = Resque::enqueue('jobs', 'Returning_Job', [ 'return' => $value ], true);
107 | $this->worker->work(0);
108 | $status = new Status($token);
109 | $this->assertEquals($value, $status->result());
110 | }
111 |
112 | public function testCompletedJobReturnsObjectResultAsArray()
113 | {
114 | $value = new stdClass();
115 | $token = Resque::enqueue('jobs', 'Returning_Job', [ 'return' => $value ], true);
116 | $this->worker->work(0);
117 | $status = new Status($token);
118 | $this->assertEquals([], $status->result());
119 | }
120 |
121 | public function testStatusIsNotTrackedWhenToldNotTo()
122 | {
123 | $token = Resque::enqueue('jobs', 'Test_Job', [], false);
124 | $status = new Status($token);
125 | $this->assertFalse($status->isTracking());
126 | }
127 |
128 | public function testStatusTrackingCanBeStopped()
129 | {
130 | Status::create('test');
131 | $status = new Status('test');
132 | $this->assertEquals(Status::STATUS_WAITING, $status->get());
133 | $status->stop();
134 | $this->assertFalse($status->get());
135 | }
136 |
137 | public function testRecreatedJobWithTrackingStillTracksStatus()
138 | {
139 | $originalToken = Resque::enqueue('jobs', 'Test_Job', [], true);
140 | $job = $this->worker->reserve();
141 |
142 | // Mark this job as being worked on to ensure that the new status is still
143 | // waiting.
144 | $this->worker->workingOn($job);
145 |
146 | // Now recreate it
147 | $newToken = $job->recreate();
148 |
149 | // Make sure we've got a new job returned
150 | $this->assertNotEquals($originalToken, $newToken);
151 |
152 | // Now check the status of the new job
153 | $newJob = JobHandler::reserve('jobs');
154 | $this->assertEquals(Status::STATUS_WAITING, $newJob->getStatus());
155 | }
156 | }
157 |
--------------------------------------------------------------------------------
/test/Resque/Tests/RedisTest.php:
--------------------------------------------------------------------------------
1 |
14 | * @license http://www.opensource.org/licenses/mit-license.php
15 | */
16 | class RedisTest extends ResqueTestCase
17 | {
18 | public function testRedisExceptionsAreSurfaced()
19 | {
20 | $this->expectException('\Resque\Exceptions\RedisException');
21 |
22 | $mockCredis = $this->getMockBuilder('Credis_Client')
23 | ->setMethods(['connect', '__call'])
24 | ->getMock();
25 | $mockCredis->expects($this->any())->method('__call')
26 | ->will($this->throwException(new CredisException('failure')));
27 |
28 | Resque::setBackend(function($database) use ($mockCredis) {
29 | return new Redis('localhost:6379', $database, $mockCredis);
30 | });
31 | Resque::redis()->ping();
32 | }
33 |
34 | /**
35 | * These DNS strings are considered valid.
36 | *
37 | * @return array
38 | */
39 | public function validDsnStringProvider()
40 | {
41 | return array(
42 | // Input , Expected output
43 | array('', array(
44 | 'localhost',
45 | 6379,
46 | false,
47 | false, false,
48 | array(),
49 | )),
50 | array('localhost', array(
51 | 'localhost',
52 | 6379,
53 | false,
54 | false, false,
55 | array(),
56 | )),
57 | array('localhost:1234', array(
58 | 'localhost',
59 | 1234,
60 | false,
61 | false, false,
62 | array(),
63 | )),
64 | array('localhost:1234/2', array(
65 | 'localhost',
66 | 1234,
67 | 2,
68 | false, false,
69 | array(),
70 | )),
71 | array('redis://foobar', array(
72 | 'foobar',
73 | 6379,
74 | false,
75 | false, false,
76 | array(),
77 | )),
78 | array('redis://foobar/', array(
79 | 'foobar',
80 | 6379,
81 | false,
82 | false, false,
83 | array(),
84 | )),
85 | array('redis://foobar:1234', array(
86 | 'foobar',
87 | 1234,
88 | false,
89 | false, false,
90 | array(),
91 | )),
92 | array('redis://foobar:1234/15', array(
93 | 'foobar',
94 | 1234,
95 | 15,
96 | false, false,
97 | array(),
98 | )),
99 | array('redis://foobar:1234/0', array(
100 | 'foobar',
101 | 1234,
102 | 0,
103 | false, false,
104 | array(),
105 | )),
106 | array('redis://user@foobar:1234', array(
107 | 'foobar',
108 | 1234,
109 | false,
110 | 'user', false,
111 | array(),
112 | )),
113 | array('redis://user@foobar:1234/15', array(
114 | 'foobar',
115 | 1234,
116 | 15,
117 | 'user', false,
118 | array(),
119 | )),
120 | array('redis://user:pass@foobar:1234', array(
121 | 'foobar',
122 | 1234,
123 | false,
124 | 'user', 'pass',
125 | array(),
126 | )),
127 | array('redis://user:pass@foobar:1234?x=y&a=b', array(
128 | 'foobar',
129 | 1234,
130 | false,
131 | 'user', 'pass',
132 | array('x' => 'y', 'a' => 'b'),
133 | )),
134 | array('redis://:pass@foobar:1234?x=y&a=b', array(
135 | 'foobar',
136 | 1234,
137 | false,
138 | false, 'pass',
139 | array('x' => 'y', 'a' => 'b'),
140 | )),
141 | array('redis://user@foobar:1234?x=y&a=b', array(
142 | 'foobar',
143 | 1234,
144 | false,
145 | 'user', false,
146 | array('x' => 'y', 'a' => 'b'),
147 | )),
148 | array('redis://foobar:1234?x=y&a=b', array(
149 | 'foobar',
150 | 1234,
151 | false,
152 | false, false,
153 | array('x' => 'y', 'a' => 'b'),
154 | )),
155 | array('redis://user@foobar:1234/12?x=y&a=b', array(
156 | 'foobar',
157 | 1234,
158 | 12,
159 | 'user', false,
160 | array('x' => 'y', 'a' => 'b'),
161 | )),
162 | array('tcp://user@foobar:1234/12?x=y&a=b', array(
163 | 'foobar',
164 | 1234,
165 | 12,
166 | 'user', false,
167 | array('x' => 'y', 'a' => 'b'),
168 | )),
169 | );
170 | }
171 |
172 | /**
173 | * These DSN values should throw exceptions
174 | * @return array
175 | */
176 | public function bogusDsnStringProvider()
177 | {
178 | return array(
179 | array('http://foo.bar/'),
180 | array('user:@foobar:1234?x=y&a=b'),
181 | array('foobar:1234?x=y&a=b'),
182 | );
183 | }
184 |
185 | /**
186 | * @dataProvider validDsnStringProvider
187 | */
188 | public function testParsingValidDsnString($dsn, $expected)
189 | {
190 | $result = Redis::parseDsn($dsn);
191 | $this->assertEquals($expected, $result);
192 | }
193 |
194 | /**
195 | * @dataProvider bogusDsnStringProvider
196 | */
197 | public function testParsingBogusDsnStringThrowsException($dsn)
198 | {
199 | $this->expectException('InvalidArgumentException');
200 |
201 | // The next line should throw an InvalidArgumentException
202 | $result = Redis::parseDsn($dsn);
203 | }
204 | }
205 |
--------------------------------------------------------------------------------
/lib/Worker/SchedulerWorker.php:
--------------------------------------------------------------------------------
1 |
15 | * @copyright (c) 2012 Chris Boulton
16 | * @license http://www.opensource.org/licenses/mit-license.php
17 | */
18 | class SchedulerWorker
19 | {
20 | public const LOG_NONE = 0;
21 | public const LOG_NORMAL = 1;
22 | public const LOG_VERBOSE = 2;
23 |
24 | /**
25 | * @var int Current log level of this worker.
26 | */
27 | public $logLevel = 0;
28 |
29 | /**
30 | * @var int Interval to sleep for between checking schedules.
31 | */
32 | protected $interval = 5;
33 |
34 | /**
35 | * @var boolean True if on the next iteration, the worker should shutdown.
36 | */
37 | private $shutdown = false;
38 |
39 | /**
40 | * @var boolean True if this worker is paused.
41 | */
42 | private $paused = false;
43 |
44 | /**
45 | * The primary loop for a worker.
46 | *
47 | * Every $interval (seconds), the scheduled queue will be checked for jobs
48 | * that should be pushed to Resque.
49 | *
50 | * @param int $interval How often to check schedules.
51 | */
52 | public function work($interval = null)
53 | {
54 | if ($interval !== null) {
55 | $this->interval = $interval;
56 | }
57 |
58 | $this->updateProcLine('Starting');
59 | $this->registerSigHandlers();
60 |
61 | while (true) {
62 | if ($this->shutdown) {
63 | break;
64 | }
65 | if (!$this->paused) {
66 | $this->handleDelayedItems();
67 | }
68 | $this->sleep();
69 | }
70 | }
71 |
72 | /**
73 | * Handle delayed items for the next scheduled timestamp.
74 | *
75 | * Searches for any items that are due to be scheduled in Resque
76 | * and adds them to the appropriate job queue in Resque.
77 | *
78 | * @param \DateTime|int $timestamp Search for any items up to this timestamp to schedule.
79 | */
80 | public function handleDelayedItems($timestamp = null)
81 | {
82 | while (($oldestJobTimestamp = Scheduler::nextDelayedTimestamp($timestamp)) !== false) {
83 | $this->updateProcLine('Processing Delayed Items');
84 | $this->enqueueDelayedItemsForTimestamp($oldestJobTimestamp);
85 | }
86 | }
87 |
88 | /**
89 | * Schedule all of the delayed jobs for a given timestamp.
90 | *
91 | * Searches for all items for a given timestamp, pulls them off the list of
92 | * delayed jobs and pushes them across to Resque.
93 | *
94 | * @param \DateTime|int $timestamp Search for any items up to this timestamp to schedule.
95 | */
96 | public function enqueueDelayedItemsForTimestamp($timestamp)
97 | {
98 | $item = null;
99 | while ($item = Scheduler::nextItemForTimestamp($timestamp)) {
100 | $this->log('queueing ' . $item['class'] . ' in ' . $item['queue'] . ' [delayed]');
101 |
102 | Event::trigger('beforeDelayedEnqueue', array(
103 | 'queue' => $item['queue'],
104 | 'class' => $item['class'],
105 | 'args' => $item['args'],
106 | ));
107 |
108 | $payload = array_merge(array($item['queue'], $item['class']), $item['args']);
109 | call_user_func_array('Resque\Resque::enqueue', $payload);
110 | }
111 | }
112 |
113 | /**
114 | * Sleep for the defined interval.
115 | */
116 | protected function sleep()
117 | {
118 | sleep($this->interval);
119 | }
120 |
121 | /**
122 | * Update the status of the current worker process.
123 | *
124 | * On supported systems (with the PECL proctitle module installed), update
125 | * the name of the currently running process to indicate the current state
126 | * of a worker.
127 | *
128 | * @param string $status The updated process title.
129 | */
130 | private function updateProcLine($status)
131 | {
132 | if (function_exists('setproctitle')) {
133 | setproctitle('resque-scheduler-' . Scheduler::VERSION . ': ' . $status);
134 | }
135 | }
136 |
137 | /**
138 | * Output a given log message to STDOUT.
139 | *
140 | * @param string $message Message to output.
141 | */
142 | public function log($message)
143 | {
144 | if ($this->logLevel == self::LOG_NORMAL) {
145 | fwrite(STDOUT, "*** " . $message . "\n");
146 | } elseif ($this->logLevel == self::LOG_VERBOSE) {
147 | fwrite(STDOUT, "** [" . date('H:i:s Y-m-d') . "] " . $message . "\n");
148 | }
149 | }
150 |
151 | /**
152 | * Register signal handlers that a worker should respond to.
153 | *
154 | * TERM: Shutdown after the current timestamp was processed.
155 | * INT: Shutdown after the current timestamp was processed.
156 | * QUIT: Shutdown after the current timestamp was processed.
157 | */
158 | private function registerSigHandlers()
159 | {
160 | if (!function_exists('pcntl_signal')) {
161 | return;
162 | }
163 |
164 | pcntl_signal(SIGTERM, array($this, 'shutdown'));
165 | pcntl_signal(SIGINT, array($this, 'shutdown'));
166 | pcntl_signal(SIGQUIT, array($this, 'shutdown'));
167 | pcntl_signal(SIGUSR2, array($this, 'pauseProcessing'));
168 | pcntl_signal(SIGCONT, array($this, 'unPauseProcessing'));
169 |
170 | $this->log('Registered signals');
171 | }
172 |
173 | public function shutdown()
174 | {
175 | $this->log('Shutting down');
176 | $this->shutdown = true;
177 | }
178 |
179 | /**
180 | * Signal handler callback for USR2, pauses processing.
181 | */
182 | public function pauseProcessing()
183 | {
184 | $this->log('USR2 received; pausing processing');
185 | $this->paused = true;
186 | }
187 |
188 | /**
189 | * Signal handler callback for CONT, resume processing.
190 | */
191 | public function unPauseProcessing()
192 | {
193 | $this->log('CONT received; resuming processing');
194 | $this->paused = false;
195 | }
196 | }
197 |
--------------------------------------------------------------------------------
/bin/resque:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env php
2 | 1) {
88 | $count = $COUNT;
89 | }
90 |
91 | $PREFIX = getenv('PREFIX');
92 | if(!empty($PREFIX)) {
93 | $logger->log(\Psr\Log\LogLevel::INFO, 'Prefix set to {prefix}', array('prefix' => $PREFIX));
94 | \Resque\Redis::prefix($PREFIX);
95 | }
96 |
97 | function cleanup_children($signal){
98 | $GLOBALS['send_signal'] = $signal;
99 | }
100 |
101 | if($count > 1) {
102 | $children = array();
103 | $GLOBALS['send_signal'] = FALSE;
104 |
105 | $die_signals = array(SIGTERM, SIGINT, SIGQUIT);
106 | $all_signals = array_merge($die_signals, array(SIGUSR1, SIGUSR2, SIGCONT, SIGPIPE));
107 |
108 | for($i = 0; $i < $count; ++$i) {
109 | $pid = \Resque\Resque::fork();
110 | if($pid == -1) {
111 | die("Could not fork worker ".$i."\n");
112 | }
113 | // Child, start the worker
114 | elseif(!$pid) {
115 | $queues = explode(',', $QUEUE);
116 | $worker = new \Resque\Worker\ResqueWorker($queues);
117 | $worker->setLogger($logger);
118 | $worker->hasParent = TRUE;
119 | fwrite(STDOUT, '*** Starting worker '.$worker."\n");
120 | $worker->work($interval);
121 | break;
122 | }
123 | else {
124 | $children[$pid] = 1;
125 | while (count($children) == $count){
126 | if (!isset($registered)) {
127 | declare(ticks = 1);
128 | foreach ($all_signals as $signal) {
129 | pcntl_signal($signal, "cleanup_children");
130 | }
131 |
132 | $PIDFILE = getenv('PIDFILE');
133 | if ($PIDFILE) {
134 | if(file_put_contents($PIDFILE, getmypid()) === false){
135 | $logger->log(\Psr\Log\LogLevel::NOTICE, 'Could not write PID information to {pidfile}', array('pidfile' => $PIDFILE));
136 | die(2);
137 | }
138 | }
139 |
140 | $registered = TRUE;
141 | }
142 |
143 | if(function_exists('setproctitle')) {
144 | setproctitle('resque-' . \Resque\Resque::VERSION . ": Monitoring {$count} children: [".implode(',', array_keys($children))."]");
145 | }
146 |
147 | $childPID = pcntl_waitpid(-1, $childStatus, WNOHANG);
148 | if ($childPID != 0) {
149 | fwrite(STDOUT, "*** A child worker died: {$childPID}\n");
150 | unset($children[$childPID]);
151 | $i--;
152 | }
153 | usleep(250000);
154 | if ($GLOBALS['send_signal'] !== FALSE){
155 | foreach ($children as $k => $v){
156 | posix_kill($k, $GLOBALS['send_signal']);
157 | if (in_array($GLOBALS['send_signal'], $die_signals)) {
158 | pcntl_waitpid($k, $childStatus);
159 | }
160 | }
161 | if (in_array($GLOBALS['send_signal'], $die_signals)) {
162 | exit;
163 | }
164 | $GLOBALS['send_signal'] = FALSE;
165 | }
166 | }
167 | }
168 | }
169 | }
170 | // Start a single worker
171 | else {
172 | $queues = explode(',', $QUEUE);
173 | $worker = new \Resque\Worker\ResqueWorker($queues);
174 | $worker->setLogger($logger);
175 | $worker->hasParent = FALSE;
176 |
177 | $PIDFILE = getenv('PIDFILE');
178 | if ($PIDFILE) {
179 | if(file_put_contents($PIDFILE, getmypid()) === false) {
180 | $logger->log(\Psr\Log\LogLevel::NOTICE, 'Could not write PID information to {pidfile}', array('pidfile' => $PIDFILE));
181 | die(2);
182 | }
183 | }
184 |
185 | $logger->log(\Psr\Log\LogLevel::NOTICE, 'Starting worker {worker}', array('worker' => $worker));
186 | $worker->work($interval, $BLOCKING);
187 | }
188 | ?>
189 |
--------------------------------------------------------------------------------
/lib/Job/Status.php:
--------------------------------------------------------------------------------
1 |
12 | * @license http://www.opensource.org/licenses/mit-license.php
13 | */
14 | class Status
15 | {
16 | public const STATUS_WAITING = 1;
17 | public const STATUS_RUNNING = 2;
18 | public const STATUS_FAILED = 3;
19 | public const STATUS_COMPLETE = 4;
20 |
21 | /**
22 | * @var string The prefix of the job status id.
23 | */
24 | private $prefix;
25 |
26 | /**
27 | * @var string The ID of the job this status class refers back to.
28 | */
29 | private $id;
30 |
31 | /**
32 | * @var mixed Cache variable if the status of this job is being monitored or not.
33 | * True/false when checked at least once or null if not checked yet.
34 | */
35 | private $isTracking = null;
36 |
37 | /**
38 | * @var array Array of statuses that are considered final/complete.
39 | */
40 | private static $completeStatuses = array(
41 | self::STATUS_FAILED,
42 | self::STATUS_COMPLETE
43 | );
44 |
45 | /**
46 | * Setup a new instance of the job monitor class for the supplied job ID.
47 | *
48 | * @param string $id The ID of the job to manage the status for.
49 | */
50 | public function __construct($id, $prefix = '')
51 | {
52 | $this->id = $id;
53 | $this->prefix = empty($prefix) ? '' : "{$prefix}_";
54 | }
55 |
56 | /**
57 | * Create a new status monitor item for the supplied job ID. Will create
58 | * all necessary keys in Redis to monitor the status of a job.
59 | *
60 | * @param string $id The ID of the job to monitor the status of.
61 | */
62 | public static function create($id, $prefix = "")
63 | {
64 | $status = new self($id, $prefix);
65 | $statusPacket = array(
66 | 'status' => self::STATUS_WAITING,
67 | 'updated' => time(),
68 | 'started' => time(),
69 | 'result' => null,
70 | );
71 | Resque::redis()->set((string) $status, json_encode($statusPacket));
72 |
73 | return $status;
74 | }
75 |
76 | /**
77 | * Check if we're actually checking the status of the loaded job status
78 | * instance.
79 | *
80 | * @return boolean True if the status is being monitored, false if not.
81 | */
82 | public function isTracking()
83 | {
84 | if ($this->isTracking === false) {
85 | return false;
86 | }
87 |
88 | if (!Resque::redis()->exists((string)$this)) {
89 | $this->isTracking = false;
90 | return false;
91 | }
92 |
93 | $this->isTracking = true;
94 | return true;
95 | }
96 |
97 | /**
98 | * Update the status indicator for the current job with a new status.
99 | *
100 | * @param int The status of the job (see constants in Resque\Job\Status)
101 | */
102 | public function update($status, $result = null)
103 | {
104 | $status = (int) $status;
105 |
106 | if (!$this->isTracking()) {
107 | return;
108 | }
109 |
110 | if ($status < self::STATUS_WAITING || $status > self::STATUS_COMPLETE) {
111 | return;
112 | }
113 |
114 | $statusPacket = array(
115 | 'status' => $status,
116 | 'updated' => time(),
117 | 'started' => $this->fetch('started'),
118 | 'result' => $result,
119 | );
120 | Resque::redis()->set((string)$this, json_encode($statusPacket));
121 |
122 | // Expire the status for completed jobs after 24 hours
123 | if (in_array($status, self::$completeStatuses)) {
124 | Resque::redis()->expire((string)$this, 86400);
125 | }
126 | }
127 |
128 | /**
129 | * Fetch the status for the job being monitored.
130 | *
131 | * @return mixed False if the status is not being monitored, otherwise the status
132 | * as an integer, based on the Resque\Job\Status constants.
133 | */
134 | public function get()
135 | {
136 | return $this->status();
137 | }
138 |
139 | /**
140 | * Fetch the status for the job being monitored.
141 | *
142 | * @return mixed False if the status is not being monitored, otherwise the status
143 | * as an integer, based on the Resque\Job\Status constants.
144 | */
145 | public function status()
146 | {
147 | return $this->fetch('status');
148 | }
149 |
150 | /**
151 | * Fetch the last update timestamp of the job being monitored.
152 | *
153 | * @return mixed False if the job is not being monitored, otherwise the
154 | * update timestamp.
155 | */
156 | public function updated()
157 | {
158 | return $this->fetch('updated');
159 | }
160 |
161 | /**
162 | * Fetch the start timestamp of the job being monitored.
163 | *
164 | * @return mixed False if the job is not being monitored, otherwise the
165 | * start timestamp.
166 | */
167 | public function started()
168 | {
169 | return $this->fetch('started');
170 | }
171 |
172 | /**
173 | * Fetch the result of the job being monitored.
174 | *
175 | * @return mixed False if the job is not being monitored, otherwise the result
176 | * as mixed
177 | */
178 | public function result()
179 | {
180 | return $this->fetch('result');
181 | }
182 |
183 | /**
184 | * Stop tracking the status of a job.
185 | */
186 | public function stop()
187 | {
188 | Resque::redis()->del((string)$this);
189 | }
190 |
191 | /**
192 | * Generate a string representation of this object.
193 | *
194 | * @return string String representation of the current job status class.
195 | */
196 | public function __toString()
197 | {
198 | return 'job:' . $this->prefix . $this->id . ':status';
199 | }
200 |
201 | /**
202 | * Fetch a value from the status packet for the job being monitored.
203 | *
204 | * @return mixed False if the status is not being monitored, otherwise the
205 | * requested value from the status packet.
206 | */
207 | protected function fetch($value = null)
208 | {
209 | if (!$this->isTracking()) {
210 | return false;
211 | }
212 |
213 | $statusPacket = json_decode(Resque::redis()->get((string)$this), true);
214 | if (!$statusPacket) {
215 | return false;
216 | }
217 |
218 | if (empty($value)) {
219 | return $statusPacket;
220 | } else {
221 | if (isset($statusPacket[$value])) {
222 | return $statusPacket[$value];
223 | } else {
224 | return null;
225 | }
226 | }
227 | }
228 | }
229 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## 1.3 (2013-??-??) - Current Master
2 |
3 | **Note:** This release introduces backwards incompatible changes with all
4 | previous versions of php-resque. Please see below for details.
5 |
6 | ### Redisent (Redis Library) Replaced with Credis
7 |
8 | Redisent has always been the Redis backend for php-resque because of its
9 | lightweight nature. Unfortunately, Redisent is largely unmaintained.
10 |
11 | [Credis](https://github.com/colinmollenhour/credis) is a fork of Redisent, which
12 | among other improvements will automatically use the
13 | [phpredis](https://github.com/nicolasff/phpredis) native PHP extension if it is
14 | available. (you want this for speed, trust me)
15 |
16 | php-resque now utilizes Credis for all Redis based operations. Credis
17 | automatically required and installed as a Composer dependency.
18 |
19 | ### Composer Support
20 |
21 | Composer support has been improved and is now the recommended method for
22 | including php-resque in your project. Details on Composer support can be found
23 | in the Getting Started section of the readme.
24 |
25 | ### Improved DSN Support
26 |
27 | Changes by iskandar introduce improved support for using DSNs to connect to
28 | Redis. You can now utilize the following formatted strings for the REDIS_BACKEND
29 | environment variable to connect:
30 |
31 | - `host`
32 | - `host:port`
33 | - `redis://host:port`
34 | - `redis://host:port/db`
35 | - `redis://user:pass@host:port/` (username is required but will be ignored)
36 | - `tcp://user:pass@host:port/` (username is required but will be ignored)
37 |
38 | ### Other Improvements/Changes
39 |
40 | - **COMPATIBILITY BREAKING**: The bundled worker manager `resque.php` has been
41 | moved to `bin/resque`, and is available as `vendor/bin/resque` when
42 | php-resque is installed as a Composer package.
43 | - Restructure tests and test bootstrapping. Autoload tests via Composer
44 | (install test dependencies with `composer install --dev`)
45 | - Add `SETEX` to list of commands which supply a key as the first argument in
46 | Redisent (danhunsaker)
47 | - Fix an issue where a lost connection to Redis could cause an infinite loop
48 | (atorres757)
49 | - Add a helper method to `Resque_Redis` to remove the namespace applied to
50 | Redis keys (tonypiper)
51 | - Call beforePerform hook before retrieivng an instance of the job class
52 | (allows beforePerform to cancel a job with DontPerform before initialising
53 | your application)
54 | - Add `beforeEnqueue` hook, called before a job is placed on a queue
55 |
56 | ## 1.2 (2012-10-13)
57 |
58 | **Note:** This release is largely backwards compatible with php-resque 1.1. The
59 | next release will introduce backwards incompatible changes (moving from
60 | Redisent to Credis), and will drop compatibility with PHP 5.2.
61 |
62 | - Allow alternate redis database to be selected when calling setBackend by
63 | supplying a second argument (patrickbajao)
64 | - Use `require_once` when including php-resque after the app has been included
65 | in the sample resque.php to prevent include conflicts (andrewjshults)
66 | - Wrap job arguments in an array to improve compatibility with ruby resque
67 | (warezthebeef)
68 | - Fix a bug where the worker would spin out of control taking the server with
69 | it, if the redis connection was interrupted even briefly. Use SIGPIPE to
70 | trap this scenario cleanly. (d11wtq)
71 | - Added support of Redis prefix (namespaces) (hlegius)
72 | - When reserving jobs, check if the payload received from popping a queue is a
73 | valid object (fix bug whereby jobs are reserved based on an erroneous
74 | payload) (salimane)
75 | - Re-enable autoload for class_exists in Job.php (humancopy)
76 | - Fix lost jobs when there is more than one worker process started by the same
77 | parent process (salimane)
78 | - Move include for resque before APP_INCLUDE is loaded in, so that way resque
79 | is available for the app
80 | - Avoid working with dirty worker IDs (salimane)
81 | - Allow UNIX socket to be passed to Resque when connecting to Redis
82 | (pedroarnal)
83 | - Fix typographical errors in PHP docblocks (chaitanyakuber)
84 | - Set the queue name on job instances when jobs are executed (chaitanyakuber)
85 | - Fix and add tests for Resque_Event::stopListening (ebernhardson)
86 | - Documentation cleanup (maetl)
87 | - Pass queue name to afterEvent callback
88 | - Only declare RedisException if it doesn't already exist (Matt Heath)
89 | - Add support for Composer
90 | - Fix missing and incorrect paths for Resque and Resque_Job_Status classes in
91 | demo (jjfrey)
92 | - Disable autoload for the RedisException class_exists call (scragg0x)
93 | - General tidy up of comments and files/folders
94 |
95 | ## 1.1 (2011-03-27)
96 |
97 | - Update Redisent library for Redis 2.2 compatibility. Redis 2.2 is now
98 | required. (thedotedge)
99 | - Trim output of `ps` to remove any prepended whitespace (KevBurnsJr)
100 | - Use `getenv` instead of `$_ENV` for better portability across PHP
101 | configurations (hobodave)
102 | - Add support for sub-second queue check intervals (KevBurnsJr)
103 | - Ability to specify a cluster/multiple redis servers and consistent hash
104 | between them (dceballos)
105 | - Change arguments for jobs to be an array as they're easier to work with in
106 | PHP.
107 | - Implement ability to have setUp and tearDown methods for jobs, called before
108 | and after every single run.
109 | - Fix `APP_INCLUDE` environment variable not loading correctly.
110 | - Jobs are no longer defined as static methods, and classes are instantiated
111 | first. This change is NOT backwards compatible and requires job classes are
112 | updated.
113 | - Job arguments are passed to the job class when it is instantiated, and are
114 | accessible by $this->args. This change will break existing job classes that
115 | rely on arguments that have not been updated.
116 | - Bundle sample script for managing php-resque instances using monit
117 | - Fix undefined variable `$child` when exiting on non-forking operating
118 | systems
119 | - Add `PIDFILE` environment variable to write out a PID for single running
120 | workers
121 |
122 | ## 1.0 (2010-04-18)
123 |
124 | - Initial release
125 |
--------------------------------------------------------------------------------
/test/Resque/Tests/EventTest.php:
--------------------------------------------------------------------------------
1 |
18 | * @license http://www.opensource.org/licenses/mit-license.php
19 | */
20 | class EventTest extends ResqueTestCase
21 | {
22 | private $callbacksHit = array();
23 |
24 | private $worker;
25 |
26 | public function setUp(): void
27 | {
28 | Test_Job::$called = false;
29 |
30 | $this->logger = $this->getMockBuilder('Psr\Log\LoggerInterface')
31 | ->getMock();
32 |
33 | // Register a worker to test with
34 | $this->worker = new ResqueWorker('jobs');
35 | $this->worker->setLogger($this->logger);
36 | $this->worker->registerWorker();
37 | }
38 |
39 | public function tearDown(): void
40 | {
41 | Event::clearListeners();
42 | $this->callbacksHit = array();
43 | }
44 |
45 | public function getEventTestJob()
46 | {
47 | $payload = array(
48 | 'class' => 'Test_Job',
49 | 'args' => array(
50 | array('somevar'),
51 | ),
52 | 'id' => Resque::generateJobId()
53 | );
54 | $job = new JobHandler('jobs', $payload);
55 | $job->worker = $this->worker;
56 | return $job;
57 | }
58 |
59 | public function eventCallbackProvider()
60 | {
61 | return array(
62 | array('beforePerform', 'beforePerformEventCallback'),
63 | array('afterPerform', 'afterPerformEventCallback'),
64 | array('afterFork', 'afterForkEventCallback'),
65 | );
66 | }
67 |
68 | /**
69 | * @dataProvider eventCallbackProvider
70 | */
71 | public function testEventCallbacksFire($event, $callback)
72 | {
73 | Event::listen($event, array($this, $callback));
74 |
75 | $job = $this->getEventTestJob();
76 |
77 | $this->logger->expects($this->exactly(3))
78 | ->method('log')
79 | ->withConsecutive(
80 | [ 'notice', '{job} has finished', [ 'job' => $job ] ],
81 | [ 'debug', 'Registered signals', [] ],
82 | [ 'info', 'Checking {queue} for jobs', [ 'queue' => 'jobs' ] ]
83 | );
84 |
85 | $this->worker->perform($job);
86 | $this->worker->work(0);
87 |
88 | $this->assertContains($callback, $this->callbacksHit, $event . ' callback (' . $callback .') was not called');
89 | }
90 |
91 | public function testBeforeForkEventCallbackFires()
92 | {
93 | $event = 'beforeFork';
94 | $callback = 'beforeForkEventCallback';
95 |
96 | Event::listen($event, array($this, $callback));
97 | Resque::enqueue('jobs', 'Test_Job', array(
98 | 'somevar'
99 | ));
100 | $job = $this->getEventTestJob();
101 |
102 | $this->worker->work(0);
103 | $this->assertContains($callback, $this->callbacksHit, $event . ' callback (' . $callback .') was not called');
104 | }
105 |
106 | public function testBeforeEnqueueEventCallbackFires()
107 | {
108 | $event = 'beforeEnqueue';
109 | $callback = 'beforeEnqueueEventCallback';
110 |
111 | Event::listen($event, array($this, $callback));
112 | Resque::enqueue('jobs', 'Test_Job', array(
113 | 'somevar'
114 | ));
115 | $this->assertContains($callback, $this->callbacksHit, $event . ' callback (' . $callback .') was not called');
116 | }
117 |
118 | public function testBeforePerformEventCanStopWork()
119 | {
120 | $callback = 'beforePerformEventDontPerformCallback';
121 | Event::listen('beforePerform', array($this, $callback));
122 |
123 | $job = $this->getEventTestJob();
124 |
125 | $this->assertFalse($job->perform());
126 | $this->assertContains($callback, $this->callbacksHit, $callback . ' callback was not called');
127 | $this->assertFalse(Test_Job::$called, 'Job was still performed though DoNotPerformException was thrown');
128 | }
129 |
130 | public function testBeforeEnqueueEventStopsJobCreation()
131 | {
132 | $callback = 'beforeEnqueueEventDontCreateCallback';
133 | Event::listen('beforeEnqueue', array($this, $callback));
134 | Event::listen('afterEnqueue', array($this, 'afterEnqueueEventCallback'));
135 |
136 | $result = Resque::enqueue('test_job', 'TestClass');
137 | $this->assertContains($callback, $this->callbacksHit, $callback . ' callback was not called');
138 | $this->assertNotContains('afterEnqueueEventCallback', $this->callbacksHit, 'afterEnqueue was still called, even though it should not have been');
139 | $this->assertFalse($result);
140 | }
141 |
142 | public function testAfterEnqueueEventCallbackFires()
143 | {
144 | $callback = 'afterEnqueueEventCallback';
145 | $event = 'afterEnqueue';
146 |
147 | Event::listen($event, array($this, $callback));
148 | Resque::enqueue('jobs', 'Test_Job', array(
149 | 'somevar'
150 | ));
151 | $this->assertContains($callback, $this->callbacksHit, $event . ' callback (' . $callback .') was not called');
152 | }
153 |
154 | public function testStopListeningRemovesListener()
155 | {
156 | $callback = 'beforePerformEventCallback';
157 | $event = 'beforePerform';
158 |
159 | Event::listen($event, array($this, $callback));
160 | Event::stopListening($event, array($this, $callback));
161 |
162 | $job = $this->getEventTestJob();
163 | $this->worker->perform($job);
164 | $this->worker->work(0);
165 |
166 | $this->assertNotContains($callback, $this->callbacksHit,
167 | $event . ' callback (' . $callback .') was called though Event::stopListening was called'
168 | );
169 | }
170 |
171 | public function beforePerformEventDontPerformCallback($instance)
172 | {
173 | $this->callbacksHit[] = __FUNCTION__;
174 | throw new DoNotPerformException;
175 | }
176 |
177 | public function beforeEnqueueEventDontCreateCallback($queue, $class, $args, $id, $track = false)
178 | {
179 | $this->callbacksHit[] = __FUNCTION__;
180 | throw new DoNotCreateException;
181 | }
182 |
183 | public function assertValidEventCallback($function, $job)
184 | {
185 | $this->callbacksHit[] = $function;
186 | if (!$job instanceof JobHandler) {
187 | $this->fail('Callback job argument is not an instance of JobHandler');
188 | }
189 | $args = $job->getArguments();
190 | $this->assertEquals($args[0], 'somevar');
191 | }
192 |
193 | public function afterEnqueueEventCallback($class, $args, $queue, $id)
194 | {
195 | $this->callbacksHit[] = __FUNCTION__;
196 | $this->assertEquals('Test_Job', $class);
197 | $this->assertEquals(array(
198 | 'somevar',
199 | ), $args);
200 | }
201 |
202 | public function beforeEnqueueEventCallback($class, $args, $queue, $id)
203 | {
204 | $this->callbacksHit[] = __FUNCTION__;
205 | }
206 |
207 | public function beforePerformEventCallback($job)
208 | {
209 | $this->assertValidEventCallback(__FUNCTION__, $job);
210 | }
211 |
212 | public function afterPerformEventCallback($job)
213 | {
214 | $this->assertValidEventCallback(__FUNCTION__, $job);
215 | }
216 |
217 | public function beforeForkEventCallback($job)
218 | {
219 | $this->assertValidEventCallback(__FUNCTION__, $job);
220 | }
221 |
222 | public function afterForkEventCallback($job)
223 | {
224 | $this->assertValidEventCallback(__FUNCTION__, $job);
225 | }
226 | }
227 |
--------------------------------------------------------------------------------
/lib/Scheduler.php:
--------------------------------------------------------------------------------
1 |
14 | * @copyright (c) 2012 Chris Boulton
15 | * @license http://www.opensource.org/licenses/mit-license.php
16 | */
17 | class Scheduler
18 | {
19 | public const VERSION = "0.1";
20 |
21 | /**
22 | * Enqueue a job in a given number of seconds from now.
23 | *
24 | * Identical to Resque::enqueue, however the first argument is the number
25 | * of seconds before the job should be executed.
26 | *
27 | * @param int $in Number of seconds from now when the job should be executed.
28 | * @param string $queue The name of the queue to place the job in.
29 | * @param string $class The name of the class that contains the code to execute the job.
30 | * @param array $args Any optional arguments that should be passed when the job is executed.
31 | */
32 | public static function enqueueIn($in, $queue, $class, array $args = array())
33 | {
34 | self::enqueueAt(time() + $in, $queue, $class, $args);
35 | }
36 |
37 | /**
38 | * Enqueue a job for execution at a given timestamp.
39 | *
40 | * Identical to Resque::enqueue, however the first argument is a timestamp
41 | * (either UNIX timestamp in integer format or an instance of the DateTime
42 | * class in PHP).
43 | *
44 | * @param \DateTime|int $at Instance of PHP DateTime object or int of UNIX timestamp.
45 | * @param string $queue The name of the queue to place the job in.
46 | * @param string $class The name of the class that contains the code to execute the job.
47 | * @param array $args Any optional arguments that should be passed when the job is executed.
48 | */
49 | public static function enqueueAt($at, $queue, $class, $args = array())
50 | {
51 | self::validateJob($class, $queue);
52 |
53 | $job = self::jobToHash($queue, $class, $args);
54 | self::delayedPush($at, $job);
55 |
56 | Event::trigger('afterSchedule', array(
57 | 'at' => $at,
58 | 'queue' => $queue,
59 | 'class' => $class,
60 | 'args' => $args,
61 | ));
62 | }
63 |
64 | /**
65 | * Directly append an item to the delayed queue schedule.
66 | *
67 | * @param \DateTime|int $timestamp Timestamp job is scheduled to be run at.
68 | * @param array $item Hash of item to be pushed to schedule.
69 | */
70 | public static function delayedPush($timestamp, $item)
71 | {
72 | $timestamp = self::getTimestamp($timestamp);
73 | $redis = Resque::redis();
74 | $redis->rpush('delayed:' . $timestamp, json_encode($item));
75 |
76 | $redis->zadd('delayed_queue_schedule', $timestamp, $timestamp);
77 | }
78 |
79 | /**
80 | * Get the total number of jobs in the delayed schedule.
81 | *
82 | * @return int Number of scheduled jobs.
83 | */
84 | public static function getDelayedQueueScheduleSize()
85 | {
86 | return (int)Resque::redis()->zcard('delayed_queue_schedule');
87 | }
88 |
89 | /**
90 | * Get the number of jobs for a given timestamp in the delayed schedule.
91 | *
92 | * @param \DateTime|int $timestamp Timestamp
93 | * @return int Number of scheduled jobs.
94 | */
95 | public static function getDelayedTimestampSize($timestamp)
96 | {
97 | $timestamp = self::getTimestamp($timestamp);
98 | return Resque::redis()->llen('delayed:' . $timestamp, $timestamp);
99 | }
100 |
101 | /**
102 | * Remove a delayed job from the queue
103 | *
104 | * note: you must specify exactly the same
105 | * queue, class and arguments that you used when you added
106 | * to the delayed queue
107 | *
108 | * also, this is an expensive operation because all delayed keys have tobe
109 | * searched
110 | *
111 | * @param $queue
112 | * @param $class
113 | * @param $args
114 | * @return int number of jobs that were removed
115 | */
116 | public static function removeDelayed($queue, $class, $args)
117 | {
118 | $destroyed = 0;
119 | $item = json_encode(self::jobToHash($queue, $class, $args));
120 | $redis = Resque::redis();
121 |
122 | foreach ($redis->keys('delayed:*') as $key) {
123 | $key = $redis->removePrefix($key);
124 | $destroyed += $redis->lrem($key, 0, $item);
125 | }
126 |
127 | return $destroyed;
128 | }
129 |
130 | /**
131 | * removed a delayed job queued for a specific timestamp
132 | *
133 | * note: you must specify exactly the same
134 | * queue, class and arguments that you used when you added
135 | * to the delayed queue
136 | *
137 | * @param $timestamp
138 | * @param $queue
139 | * @param $class
140 | * @param $args
141 | * @return mixed
142 | */
143 | public static function removeDelayedJobFromTimestamp($timestamp, $queue, $class, $args)
144 | {
145 | $key = 'delayed:' . self::getTimestamp($timestamp);
146 | $item = json_encode(self::jobToHash($queue, $class, $args));
147 | $redis = Resque::redis();
148 | $count = $redis->lrem($key, 0, $item);
149 | self::cleanupTimestamp($key, $timestamp);
150 |
151 | return $count;
152 | }
153 |
154 | /**
155 | * Generate hash of all job properties to be saved in the scheduled queue.
156 | *
157 | * @param string $queue Name of the queue the job will be placed on.
158 | * @param string $class Name of the job class.
159 | * @param array $args Array of job arguments.
160 | */
161 |
162 | private static function jobToHash($queue, $class, $args)
163 | {
164 | return array(
165 | 'class' => $class,
166 | 'args' => array($args),
167 | 'queue' => $queue,
168 | );
169 | }
170 |
171 | /**
172 | * If there are no jobs for a given key/timestamp, delete references to it.
173 | *
174 | * Used internally to remove empty delayed: items in Redis when there are
175 | * no more jobs left to run at that timestamp.
176 | *
177 | * @param string $key Key to count number of items at.
178 | * @param int $timestamp Matching timestamp for $key.
179 | */
180 | private static function cleanupTimestamp($key, $timestamp)
181 | {
182 | $timestamp = self::getTimestamp($timestamp);
183 | $redis = Resque::redis();
184 |
185 | if ($redis->llen($key) == 0) {
186 | $redis->del($key);
187 | $redis->zrem('delayed_queue_schedule', $timestamp);
188 | }
189 | }
190 |
191 | /**
192 | * Convert a timestamp in some format in to a unix timestamp as an integer.
193 | *
194 | * @param \DateTime|int $timestamp Instance of DateTime or UNIX timestamp.
195 | * @return int Timestamp
196 | * @throws Scheduler_InvalidTimestampException
197 | */
198 | private static function getTimestamp($timestamp)
199 | {
200 | if ($timestamp instanceof DateTime) {
201 | $timestamp = $timestamp->getTimestamp();
202 | }
203 |
204 | if ((int)$timestamp != $timestamp) {
205 | throw new InvalidTimestampException(
206 | 'The supplied timestamp value could not be converted to an integer.'
207 | );
208 | }
209 |
210 | return (int)$timestamp;
211 | }
212 |
213 | /**
214 | * Find the first timestamp in the delayed schedule before/including the timestamp.
215 | *
216 | * Will find and return the first timestamp upto and including the given
217 | * timestamp. This is the heart of the Scheduler that will make sure
218 | * that any jobs scheduled for the past when the worker wasn't running are
219 | * also queued up.
220 | *
221 | * @param \DateTime|int $timestamp Instance of DateTime or UNIX timestamp.
222 | * Defaults to now.
223 | * @return int|false UNIX timestamp, or false if nothing to run.
224 | */
225 | public static function nextDelayedTimestamp($at = null)
226 | {
227 | if ($at === null) {
228 | $at = time();
229 | } else {
230 | $at = self::getTimestamp($at);
231 | }
232 |
233 | $items = Resque::redis()->zrangebyscore('delayed_queue_schedule', '-inf', $at, array('limit' => array(0, 1)));
234 | if (!empty($items)) {
235 | return $items[0];
236 | }
237 |
238 | return false;
239 | }
240 |
241 | /**
242 | * Pop a job off the delayed queue for a given timestamp.
243 | *
244 | * @param \DateTime|int $timestamp Instance of DateTime or UNIX timestamp.
245 | * @return array Matching job at timestamp.
246 | */
247 | public static function nextItemForTimestamp($timestamp)
248 | {
249 | $timestamp = self::getTimestamp($timestamp);
250 | $key = 'delayed:' . $timestamp;
251 |
252 | $item = json_decode(Resque::redis()->lpop($key), true);
253 |
254 | self::cleanupTimestamp($key, $timestamp);
255 | return $item;
256 | }
257 |
258 | /**
259 | * Ensure that supplied job class/queue is valid.
260 | *
261 | * @param string $class Name of job class.
262 | * @param string $queue Name of queue.
263 | * @throws \Resque\Exceptions\ResqueException
264 | */
265 | private static function validateJob($class, $queue)
266 | {
267 | if (empty($class)) {
268 | throw new ResqueException('Jobs must be given a class.');
269 | } elseif (empty($queue)) {
270 | throw new ResqueException('Jobs must be put in a queue.');
271 | }
272 |
273 | return true;
274 | }
275 | }
276 |
--------------------------------------------------------------------------------
/lib/JobHandler.php:
--------------------------------------------------------------------------------
1 |
18 | * @license http://www.opensource.org/licenses/mit-license.php
19 | */
20 | class JobHandler
21 | {
22 | /**
23 | * @var string The name of the queue that this job belongs to.
24 | */
25 | public $queue;
26 |
27 | /**
28 | * @var \Resque\Worker\Resque Instance of the Resque worker running this job.
29 | */
30 | public $worker;
31 |
32 | /**
33 | * @var array Array containing details of the job.
34 | */
35 | public $payload;
36 |
37 | /**
38 | * @var float Timestamp of when the data was popped from redis.
39 | */
40 | public $popTime;
41 |
42 | /**
43 | * @var float Timestamp of when the job started processing.
44 | */
45 | public $startTime;
46 |
47 | /**
48 | * @var float Timestamp of when the job finished processing.
49 | */
50 | public $endTime;
51 |
52 | /**
53 | * @var Job Instance of the class performing work for this job.
54 | */
55 | private $instance;
56 |
57 | /**
58 | * @var \Resque\Job\FactoryInterface
59 | */
60 | private $jobFactory;
61 |
62 | /**
63 | * Instantiate a new instance of a job.
64 | *
65 | * @param string $queue The queue that the job belongs to.
66 | * @param array $payload array containing details of the job.
67 | */
68 | public function __construct($queue, $payload)
69 | {
70 | $this->queue = $queue;
71 | $this->payload = $payload;
72 | $this->popTime = microtime(true);
73 |
74 | if (!isset($this->payload['id'])) {
75 | $this->payload['id'] = Resque::generateJobId();
76 | }
77 | }
78 |
79 | /**
80 | * Create a new job and save it to the specified queue.
81 | *
82 | * @param string $queue The name of the queue to place the job in.
83 | * @param string $class The name of the class that contains the code to execute the job.
84 | * @param array $args Any optional arguments that should be passed when the job is executed.
85 | * @param boolean $monitor Set to true to be able to monitor the status of a job.
86 | * @param string $id Unique identifier for tracking the job. Generated if not supplied.
87 | * @param string $prefix The prefix needs to be set for the status key
88 | *
89 | * @return string
90 | */
91 | public static function create($queue, $class, array $args = [], $monitor = false, $id = null, $prefix = "")
92 | {
93 | if (is_null($id)) {
94 | $id = Resque::generateJobId();
95 | }
96 |
97 | Resque::push($queue, array(
98 | 'class' => $class,
99 | 'args' => array($args),
100 | 'id' => $id,
101 | 'prefix' => $prefix,
102 | 'queue_time' => microtime(true),
103 | ));
104 |
105 | if ($monitor) {
106 | Status::create($id, $prefix);
107 | }
108 |
109 | return $id;
110 | }
111 |
112 | /**
113 | * Find the next available job from the specified queue and return an
114 | * instance of JobHandler for it.
115 | *
116 | * @param string $queue The name of the queue to check for a job in.
117 | * @return false|object Null when there aren't any waiting jobs, instance of Resque\JobHandler when a job was found.
118 | */
119 | public static function reserve($queue)
120 | {
121 | $payload = Resque::pop($queue);
122 | if (!is_array($payload)) {
123 | return false;
124 | }
125 |
126 | return new JobHandler($queue, $payload);
127 | }
128 |
129 | /**
130 | * Find the next available job from the specified queues using blocking list pop
131 | * and return an instance of JobHandler for it.
132 | *
133 | * @param array $queues
134 | * @param int $timeout
135 | * @return false|object Null when there aren't any waiting jobs, instance of Resque\JobHandler when a job was found.
136 | */
137 | public static function reserveBlocking(array $queues, $timeout = null)
138 | {
139 | $item = Resque::blpop($queues, $timeout);
140 |
141 | if (!is_array($item)) {
142 | return false;
143 | }
144 |
145 | return new JobHandler($item['queue'], $item['payload']);
146 | }
147 |
148 | /**
149 | * Update the status of the current job.
150 | *
151 | * @param int $status Status constant from Resque\Job\Status indicating the current status of a job.
152 | */
153 | public function updateStatus($status, $result = null)
154 | {
155 | if (empty($this->payload['id'])) {
156 | return;
157 | }
158 |
159 | $statusInstance = new Status($this->payload['id'], $this->getPrefix());
160 | $statusInstance->update($status, $result);
161 | }
162 |
163 | /**
164 | * Return the status of the current job.
165 | *
166 | * @return int|null The status of the job as one of the Resque\Job\Status constants
167 | * or null if job is not being tracked.
168 | */
169 | public function getStatus()
170 | {
171 | if (empty($this->payload['id'])) {
172 | return null;
173 | }
174 |
175 | $status = new Status($this->payload['id'], $this->getPrefix());
176 | return $status->get();
177 | }
178 |
179 | /**
180 | * Get the arguments supplied to this job.
181 | *
182 | * @return array Array of arguments.
183 | */
184 | public function getArguments(): array
185 | {
186 | if (!isset($this->payload['args'])) {
187 | return array();
188 | }
189 |
190 | return $this->payload['args'][0];
191 | }
192 |
193 | /**
194 | * Get the instantiated object for this job that will be performing work.
195 | * @return \Resque\Job\Job Instance of the object that this job belongs to.
196 | * @throws \Resque\Exceptions\ResqueException
197 | */
198 | public function getInstance(): Job
199 | {
200 | if (!is_null($this->instance)) {
201 | return $this->instance;
202 | }
203 |
204 | $this->instance = $this->getJobFactory()->create($this->payload['class'], $this->getArguments(), $this->queue);
205 | $this->instance->job = $this;
206 | $this->instance->jobID = $this->payload['id'];
207 | return $this->instance;
208 | }
209 |
210 | /**
211 | * Actually execute a job by calling the perform method on the class
212 | * associated with the job with the supplied arguments.
213 | *
214 | * @return mixed
215 | * @throws Resque\Exceptions\ResqueException When the job's class could not be found.
216 | */
217 | public function perform()
218 | {
219 | $result = true;
220 | try {
221 | Event::trigger('beforePerform', $this);
222 |
223 | $this->startTime = microtime(true);
224 |
225 | $instance = $this->getInstance();
226 |
227 | $instance->setUp();
228 |
229 | $result = $instance->perform();
230 |
231 | $instance->tearDown();
232 |
233 | $this->endTime = microtime(true);
234 |
235 | Event::trigger('afterPerform', $this);
236 | } catch (DoNotPerformException $e) {
237 | // beforePerform/setUp have said don't perform this job. Return.
238 | $result = false;
239 | }
240 |
241 | return $result;
242 | }
243 |
244 | /**
245 | * Mark the current job as having failed.
246 | *
247 | * @param $exception
248 | */
249 | public function fail($exception)
250 | {
251 | $this->endTime = microtime(true);
252 |
253 | Event::trigger('onFailure', array(
254 | 'exception' => $exception,
255 | 'job' => $this,
256 | ));
257 |
258 | $this->updateStatus(Status::STATUS_FAILED);
259 | if ($exception instanceof Error) {
260 | FailureHandler::createFromError(
261 | $this->payload,
262 | $exception,
263 | $this->worker,
264 | $this->queue
265 | );
266 | } else {
267 | FailureHandler::create(
268 | $this->payload,
269 | $exception,
270 | $this->worker,
271 | $this->queue
272 | );
273 | }
274 |
275 | if (!empty($this->payload['id'])) {
276 | PID::del($this->payload['id']);
277 | }
278 |
279 | Stat::incr('failed');
280 | Stat::incr('failed:' . $this->worker);
281 | }
282 |
283 | /**
284 | * Re-queue the current job.
285 | * @return string
286 | */
287 | public function recreate()
288 | {
289 | $monitor = false;
290 | if (!empty($this->payload['id'])) {
291 | $status = new Status($this->payload['id'], $this->getPrefix());
292 | if ($status->isTracking()) {
293 | $monitor = true;
294 | }
295 | }
296 |
297 | return self::create(
298 | $this->queue,
299 | $this->payload['class'],
300 | $this->getArguments(),
301 | $monitor,
302 | null,
303 | $this->getPrefix()
304 | );
305 | }
306 |
307 | /**
308 | * Generate a string representation used to describe the current job.
309 | *
310 | * @return string The string representation of the job.
311 | */
312 | public function __toString()
313 | {
314 | $name = array(
315 | 'Job{' . $this->queue . '}'
316 | );
317 | if (!empty($this->payload['id'])) {
318 | $name[] = 'ID: ' . $this->payload['id'];
319 | }
320 | $name[] = $this->payload['class'];
321 | if (!empty($this->payload['args'])) {
322 | $name[] = json_encode($this->payload['args']);
323 | }
324 | return '(' . implode(' | ', $name) . ')';
325 | }
326 |
327 | /**
328 | * @param Resque\Job\FactoryInterface $jobFactory
329 | * @return Resque\JobHandler
330 | */
331 | public function setJobFactory(FactoryInterface $jobFactory)
332 | {
333 | $this->jobFactory = $jobFactory;
334 |
335 | return $this;
336 | }
337 |
338 | /**
339 | * @return Resque\Job\FactoryInterface
340 | */
341 | public function getJobFactory(): FactoryInterface
342 | {
343 | if ($this->jobFactory === null) {
344 | $this->jobFactory = new Factory();
345 | }
346 | return $this->jobFactory;
347 | }
348 |
349 | /**
350 | * @return string
351 | */
352 | private function getPrefix()
353 | {
354 | if (isset($this->payload['prefix'])) {
355 | return $this->payload['prefix'];
356 | }
357 |
358 | return '';
359 | }
360 | }
361 |
--------------------------------------------------------------------------------
/test/Resque/Tests/WorkerTest.php:
--------------------------------------------------------------------------------
1 |
15 | * @license http://www.opensource.org/licenses/mit-license.php
16 | */
17 | class WorkerTest extends ResqueTestCase
18 | {
19 | public function testWorkerRegistersInList()
20 | {
21 | $worker = new ResqueWorker('*');
22 | $worker->setLogger($this->logger);
23 | $worker->registerWorker();
24 |
25 | // Make sure the worker is in the list
26 | $this->assertTrue((bool)$this->redis->sismember('resque:workers', (string)$worker));
27 | }
28 |
29 | public function testGetAllWorkers()
30 | {
31 | $num = 3;
32 | // Register a few workers
33 | for($i = 0; $i < $num; ++$i) {
34 | $worker = new ResqueWorker('queue_' . $i);
35 | $worker->setLogger($this->logger);
36 | $worker->registerWorker();
37 | }
38 |
39 | // Now try to get them
40 | $this->assertEquals($num, count(ResqueWorker::all()));
41 | }
42 |
43 | public function testGetWorkerById()
44 | {
45 | $worker = new ResqueWorker('*');
46 | $worker->setLogger($this->logger);
47 | $worker->registerWorker();
48 |
49 | $newWorker = ResqueWorker::find((string)$worker);
50 | $this->assertEquals((string)$worker, (string)$newWorker);
51 | }
52 |
53 | public function testInvalidWorkerDoesNotExist()
54 | {
55 | $this->assertFalse(ResqueWorker::exists('blah'));
56 | }
57 |
58 | public function testWorkerCanUnregister()
59 | {
60 | $worker = new ResqueWorker('*');
61 | $worker->setLogger($this->logger);
62 | $worker->registerWorker();
63 | $worker->unregisterWorker();
64 |
65 | $this->assertFalse(ResqueWorker::exists((string)$worker));
66 | $this->assertEquals(array(), ResqueWorker::all());
67 | $this->assertEquals(array(), $this->redis->smembers('resque:workers'));
68 | }
69 |
70 | public function testPausedWorkerDoesNotPickUpJobs()
71 | {
72 | $worker = new ResqueWorker('*');
73 | $worker->setLogger($this->logger);
74 | $worker->pauseProcessing();
75 | Resque::enqueue('jobs', 'Test_Job');
76 | $worker->work(0);
77 | $worker->work(0);
78 | $this->assertEquals(0, Stat::get('processed'));
79 | }
80 |
81 | public function testResumedWorkerPicksUpJobs()
82 | {
83 | $worker = new ResqueWorker('*');
84 | $worker->setLogger($this->logger);
85 | $worker->pauseProcessing();
86 | Resque::enqueue('jobs', 'Test_Job');
87 | $worker->work(0);
88 | $this->assertEquals(0, Stat::get('processed'));
89 | $worker->unPauseProcessing();
90 | $worker->work(0);
91 | $this->assertEquals(1, Stat::get('processed'));
92 | }
93 |
94 | public function testWorkerCanWorkOverMultipleQueues()
95 | {
96 | $worker = new ResqueWorker(array(
97 | 'queue1',
98 | 'queue2'
99 | ));
100 | $worker->setLogger($this->logger);
101 | $worker->registerWorker();
102 | Resque::enqueue('queue1', 'Test_Job_1');
103 | Resque::enqueue('queue2', 'Test_Job_2');
104 |
105 | $job = $worker->reserve();
106 | $this->assertEquals('queue1', $job->queue);
107 |
108 | $job = $worker->reserve();
109 | $this->assertEquals('queue2', $job->queue);
110 | }
111 |
112 | public function testWorkerWorksQueuesInSpecifiedOrder()
113 | {
114 | $worker = new ResqueWorker(array(
115 | 'high',
116 | 'medium',
117 | 'low'
118 | ));
119 | $worker->setLogger($this->logger);
120 | $worker->registerWorker();
121 |
122 | // Queue the jobs in a different order
123 | Resque::enqueue('low', 'Test_Job_1');
124 | Resque::enqueue('high', 'Test_Job_2');
125 | Resque::enqueue('medium', 'Test_Job_3');
126 |
127 | // Now check we get the jobs back in the right order
128 | $job = $worker->reserve();
129 | $this->assertEquals('high', $job->queue);
130 |
131 | $job = $worker->reserve();
132 | $this->assertEquals('medium', $job->queue);
133 |
134 | $job = $worker->reserve();
135 | $this->assertEquals('low', $job->queue);
136 | }
137 |
138 | public function testWildcardQueueWorkerWorksAllQueues()
139 | {
140 | $worker = new ResqueWorker('*');
141 | $worker->setLogger($this->logger);
142 | $worker->registerWorker();
143 |
144 | Resque::enqueue('queue1', 'Test_Job_1');
145 | Resque::enqueue('queue2', 'Test_Job_2');
146 |
147 | $job = $worker->reserve();
148 | $this->assertEquals('queue1', $job->queue);
149 |
150 | $job = $worker->reserve();
151 | $this->assertEquals('queue2', $job->queue);
152 | }
153 |
154 | public function testWorkerDoesNotWorkOnUnknownQueues()
155 | {
156 | $worker = new ResqueWorker('queue1');
157 | $worker->setLogger($this->logger);
158 | $worker->registerWorker();
159 | Resque::enqueue('queue2', 'Test_Job');
160 |
161 | $this->assertFalse($worker->reserve());
162 | }
163 |
164 | public function testWorkerClearsItsStatusWhenNotWorking()
165 | {
166 | Resque::enqueue('jobs', 'Test_Job');
167 | $worker = new ResqueWorker('jobs');
168 | $worker->setLogger($this->logger);
169 | $job = $worker->reserve();
170 | $worker->workingOn($job);
171 | $worker->doneWorking();
172 | $this->assertEquals(array(), $worker->job());
173 | }
174 |
175 | public function testWorkerRecordsWhatItIsWorkingOn()
176 | {
177 | $worker = new ResqueWorker('jobs');
178 | $worker->setLogger($this->logger);
179 | $worker->registerWorker();
180 |
181 | $payload = array(
182 | 'class' => 'Test_Job',
183 | 'id' => '87993253a68c47e697fc03a515154339'
184 | );
185 | $job = new JobHandler('jobs', $payload);
186 | $worker->workingOn($job);
187 |
188 | $job = $worker->job();
189 | $this->assertEquals('jobs', $job['queue']);
190 | if(!isset($job['run_at'])) {
191 | $this->fail('Job does not have run_at time');
192 | }
193 | $this->assertEquals($payload, $job['payload']);
194 | }
195 |
196 | public function testWorkerRecordsWhatItIsWorkingOnWithAutogeneratedId()
197 | {
198 | $worker = new ResqueWorker('jobs');
199 | $worker->setLogger($this->logger);
200 | $worker->registerWorker();
201 |
202 | $payload = array(
203 | 'class' => 'Test_Job',
204 | );
205 | $job = new JobHandler('jobs', $payload);
206 | $worker->workingOn($job);
207 |
208 | $job = $worker->job();
209 | $this->assertEquals('jobs', $job['queue']);
210 | if(!isset($job['run_at'])) {
211 | $this->fail('Job does not have run_at time');
212 | }
213 | $this->assertArrayHasKey('id', $job['payload']);
214 | }
215 |
216 | public function testWorkerErasesItsStatsWhenShutdown()
217 | {
218 | Resque::enqueue('jobs', 'Test_Job');
219 | Resque::enqueue('jobs', 'Invalid_Job');
220 |
221 | $worker = new ResqueWorker('jobs');
222 | $worker->setLogger($this->logger);
223 | $worker->work(0);
224 | $worker->work(0);
225 |
226 | $this->assertEquals(0, $worker->getStat('processed'));
227 | $this->assertEquals(0, $worker->getStat('failed'));
228 | }
229 |
230 | public function testWorkerCleansUpDeadWorkersOnStartup()
231 | {
232 | // Register a good worker
233 | $goodWorker = new ResqueWorker('jobs');
234 | $goodWorker->setLogger($this->logger);
235 | $goodWorker->registerWorker();
236 | $workerId = explode(':', $goodWorker);
237 |
238 | // Register some bad workers
239 | $worker = new ResqueWorker('jobs');
240 | $worker->setLogger($this->logger);
241 | $worker->setId($workerId[0].':1:jobs');
242 | $worker->registerWorker();
243 |
244 | $worker = new ResqueWorker(array('high', 'low'));
245 | $worker->setLogger($this->logger);
246 | $worker->setId($workerId[0].':2:high,low');
247 | $worker->registerWorker();
248 |
249 | $this->assertEquals(3, count(ResqueWorker::all()));
250 |
251 | $goodWorker->pruneDeadWorkers();
252 |
253 | // There should only be $goodWorker left now
254 | $this->assertEquals(1, count(ResqueWorker::all()));
255 | }
256 |
257 | public function testDeadWorkerCleanUpDoesNotCleanUnknownWorkers()
258 | {
259 | // Register a bad worker on this machine
260 | $worker = new ResqueWorker('jobs');
261 | $worker->setLogger($this->logger);
262 | $workerId = explode(':', $worker);
263 | $worker->setId($workerId[0].':1:jobs');
264 | $worker->registerWorker();
265 |
266 | // Register some other false workers
267 | $worker = new ResqueWorker('jobs');
268 | $worker->setLogger($this->logger);
269 | $worker->setId('my.other.host:1:jobs');
270 | $worker->registerWorker();
271 |
272 | $this->assertEquals(2, count(ResqueWorker::all()));
273 |
274 | $worker->pruneDeadWorkers();
275 |
276 | // my.other.host should be left
277 | $workers = ResqueWorker::all();
278 | $this->assertEquals(1, count($workers));
279 | $this->assertEquals((string)$worker, (string)$workers[0]);
280 | }
281 |
282 | public function testWorkerFailsUncompletedJobsOnExit()
283 | {
284 | $worker = new ResqueWorker('jobs');
285 | $worker->setLogger($this->logger);
286 | $worker->registerWorker();
287 |
288 | $payload = array(
289 | 'class' => 'Test_Job'
290 | );
291 | $job = new JobHandler('jobs', $payload);
292 |
293 | $worker->workingOn($job);
294 | $worker->unregisterWorker();
295 |
296 | $this->assertEquals(1, Stat::get('failed'));
297 | }
298 |
299 | public function testBlockingListPop()
300 | {
301 | $worker = new ResqueWorker('jobs');
302 | $worker->setLogger($this->logger);
303 | $worker->registerWorker();
304 |
305 | Resque::enqueue('jobs', 'Test_Job_1');
306 | Resque::enqueue('jobs', 'Test_Job_2');
307 |
308 | $i = 1;
309 | while($job = $worker->reserve(true, 1))
310 | {
311 | $this->assertEquals('Test_Job_' . $i, $job->payload['class']);
312 |
313 | if($i == 2) {
314 | break;
315 | }
316 |
317 | $i++;
318 | }
319 |
320 | $this->assertEquals(2, $i);
321 | }
322 |
323 | public function testWorkerFailsSegmentationFaultJob()
324 | {
325 | Resque::enqueue('jobs', 'Test_Infinite_Recursion_Job');
326 |
327 | $worker = new ResqueWorker('jobs');
328 | $worker->setLogger($this->logger);
329 | $worker->work(0);
330 |
331 | $this->assertEquals(1, Stat::get('failed'));
332 | }
333 | }
334 |
--------------------------------------------------------------------------------
/lib/Redis.php:
--------------------------------------------------------------------------------
1 |
16 | * @license http://www.opensource.org/licenses/mit-license.php
17 | *
18 | * @method array|null blpop(string $keyN, int $timeout)
19 | * @method int decrby(string $key, int $decrement)
20 | * @method int del(string|array ...$keys)
21 | * @method int exists(string $key)
22 | * @method int expire(string $key, int $seconds)
23 | * @method false|string get(string $key)
24 | * @method int incrby(string $key, int $decrement)
25 | * @method array keys(string $key)
26 | * @method int llen(string $key)
27 | * @method string|null lpop(string $key)
28 | * @method array lrange(string $key, int $start, int $stop)
29 | * @method int lrem(string $key, int $count, mixed $value)
30 | * @method string ping(string|null $name = null)
31 | * @method string|null rpop(string $key)
32 | * @method string|null rpoplpush(string $source, string $destination)
33 | * @method int rpush(string $key, mixed $value, mixed $valueN = null)
34 | * @method int sadd(string $key, mixed $value, string $valueN = null)
35 | * @method bool set(string $key, string $value, int | array $options = null)
36 | * @method int sismember(string $key, string $member)
37 | * @method array smembers(string $key)
38 | * @method int srem(string $key, mixed $value, string $valueN = null)
39 | * @method int zadd(string $key, double $score, string $value)
40 | * @method int zcard(string $key)
41 | * @method array zrangebyscore(string $key, mixed $start, mixed $stop, array $args = null)
42 | * @method int zrem(string $key, string $member)
43 | */
44 | class Redis
45 | {
46 | /**
47 | * Redis namespace
48 | * @var string
49 | */
50 | private static $defaultNamespace = 'resque:';
51 |
52 | /**
53 | * A default host to connect to
54 | */
55 | protected const DEFAULT_HOST = 'localhost';
56 |
57 | /**
58 | * The default Redis port
59 | */
60 | protected const DEFAULT_PORT = 6379;
61 |
62 | /**
63 | * The default Redis Database number
64 | */
65 | protected const DEFAULT_DATABASE = 0;
66 |
67 | /**
68 | * Connection driver
69 | * @var mixed
70 | */
71 | private $driver;
72 |
73 | /**
74 | * @var array List of all commands in Redis that supply a key as their
75 | * first argument. Used to prefix keys with the Resque namespace.
76 | */
77 | private $keyCommands = array(
78 | 'exists',
79 | 'del',
80 | 'type',
81 | 'keys',
82 | 'expire',
83 | 'ttl',
84 | 'move',
85 | 'set',
86 | 'setex',
87 | 'get',
88 | 'getset',
89 | 'setnx',
90 | 'incr',
91 | 'incrby',
92 | 'decr',
93 | 'decrby',
94 | 'rpush',
95 | 'lpush',
96 | 'llen',
97 | 'lrange',
98 | 'ltrim',
99 | 'lindex',
100 | 'lset',
101 | 'lrem',
102 | 'lpop',
103 | 'blpop',
104 | 'rpop',
105 | 'sadd',
106 | 'srem',
107 | 'spop',
108 | 'scard',
109 | 'sismember',
110 | 'smembers',
111 | 'srandmember',
112 | 'zadd',
113 | 'zrem',
114 | 'zrange',
115 | 'zrevrange',
116 | 'zrangebyscore',
117 | 'zcard',
118 | 'zscore',
119 | 'zremrangebyscore',
120 | 'sort',
121 | 'rename',
122 | 'rpoplpush'
123 | );
124 | // sinterstore
125 | // sunion
126 | // sunionstore
127 | // sdiff
128 | // sdiffstore
129 | // sinter
130 | // smove
131 | // mget
132 | // msetnx
133 | // mset
134 | // renamenx
135 |
136 | /**
137 | * Set Redis namespace (prefix) default: resque
138 | * @param string $namespace
139 | */
140 | public static function prefix($namespace)
141 | {
142 | if (substr($namespace, -1) !== ':' && $namespace != '') {
143 | $namespace .= ':';
144 | }
145 | self::$defaultNamespace = $namespace;
146 | }
147 |
148 | /**
149 | * @param string|array $server A DSN or array
150 | * @param int $database A database number to select. However, if we find a valid database number in the DSN the
151 | * DSN-supplied value will be used instead and this parameter is ignored.
152 | * @param object $client Optional Credis_Cluster or Credis_Client instance instantiated by you
153 | */
154 | public function __construct($server, $database = null, $client = null)
155 | {
156 | try {
157 | if (is_object($client)) {
158 | $this->driver = $client;
159 | } elseif (is_object($server)) {
160 | $this->driver = $server;
161 | } elseif (is_array($server)) {
162 | $this->driver = new Credis_Cluster($server);
163 | } else {
164 | list($host, $port, $dsnDatabase, $user, $password, $options) = self::parseDsn($server);
165 | // $user is not used, only $password
166 |
167 | // Look for known Credis_Client options
168 | $timeout = isset($options['timeout']) ? intval($options['timeout']) : null;
169 | $persistent = isset($options['persistent']) ? $options['persistent'] : '';
170 | $maxRetries = isset($options['max_connect_retries']) ? $options['max_connect_retries'] : 0;
171 |
172 | $this->driver = new Credis_Client($host, $port, $timeout, $persistent);
173 | $this->driver->setMaxConnectRetries($maxRetries);
174 | if ($password) {
175 | $this->driver->auth($password);
176 | }
177 |
178 | // If we have found a database in our DSN, use it instead of the `$database`
179 | // value passed into the constructor.
180 | if ($dsnDatabase !== false) {
181 | $database = $dsnDatabase;
182 | }
183 | }
184 |
185 | if ($database !== null) {
186 | $this->driver->select($database);
187 | }
188 | } catch (CredisException $e) {
189 | throw new RedisException('Error communicating with Redis: ' . $e->getMessage(), 0, $e);
190 | }
191 | }
192 |
193 | /**
194 | * Parse a DSN string, which can have one of the following formats:
195 | *
196 | * - host:port
197 | * - redis://user:pass@host:port/db?option1=val1&option2=val2
198 | * - tcp://user:pass@host:port/db?option1=val1&option2=val2
199 | * - unix:///path/to/redis.sock
200 | *
201 | * Note: the 'user' part of the DSN is not used.
202 | *
203 | * @param string $dsn A DSN string
204 | * @return array An array of DSN compotnents, with 'false' values for any unknown components. e.g.
205 | * [host, port, db, user, pass, options]
206 | */
207 | public static function parseDsn($dsn)
208 | {
209 | if ($dsn == '') {
210 | // Use a sensible default for an empty DNS string
211 | $dsn = 'redis://' . self::DEFAULT_HOST;
212 | }
213 | if (substr($dsn, 0, 7) === 'unix://') {
214 | return array(
215 | $dsn,
216 | null,
217 | false,
218 | null,
219 | null,
220 | null,
221 | );
222 | }
223 | $parts = parse_url($dsn);
224 |
225 | // Check the URI scheme
226 | $validSchemes = array('redis', 'rediss', 'tcp');
227 | if (isset($parts['scheme']) && ! in_array($parts['scheme'], $validSchemes)) {
228 | throw new InvalidArgumentException("Invalid DSN. Supported schemes are " . implode(', ', $validSchemes));
229 | }
230 |
231 | // Allow simple 'hostname' format, which `parse_url` treats as a path, not host.
232 | if (! isset($parts['host']) && isset($parts['path'])) {
233 | $parts['host'] = $parts['path'];
234 | unset($parts['path']);
235 | }
236 |
237 | // Extract the port number as an integer
238 | $port = isset($parts['port']) ? intval($parts['port']) : self::DEFAULT_PORT;
239 |
240 | // Get the database from the 'path' part of the URI
241 | $database = false;
242 | if (isset($parts['path'])) {
243 | // Strip non-digit chars from path
244 | $database = intval(preg_replace('/[^0-9]/', '', $parts['path']));
245 | }
246 |
247 | // Extract any 'user' values
248 | $user = isset($parts['user']) ? $parts['user'] : false;
249 |
250 | // Convert the query string into an associative array
251 | $options = array();
252 | if (isset($parts['query'])) {
253 | // Parse the query string into an array
254 | parse_str($parts['query'], $options);
255 | }
256 |
257 | //check 'password-encoding' parameter and extracting password based on encoding
258 | if ($options && isset($options['password-encoding']) && $options['password-encoding'] === 'u') {
259 | //extracting urlencoded password
260 | $pass = isset($parts['pass']) ? urldecode($parts['pass']) : false;
261 | } elseif ($options && isset($options['password-encoding']) && $options['password-encoding'] === 'b') {
262 | //extracting base64 encoded password
263 | $pass = isset($parts['pass']) ? base64_decode($parts['pass']) : false;
264 | } else {
265 | //extracting pass directly since 'password-encoding' parameter is not present
266 | $pass = isset($parts['pass']) ? $parts['pass'] : false;
267 | }
268 |
269 | return array(
270 | $parts['host'],
271 | $port,
272 | $database,
273 | $user,
274 | $pass,
275 | $options,
276 | );
277 | }
278 |
279 | /**
280 | * Magic method to handle all function requests and prefix key based
281 | * operations with the {self::$defaultNamespace} key prefix.
282 | *
283 | * @param string $name The name of the method called.
284 | * @param array $args Array of supplied arguments to the method.
285 | * @return mixed Return value from Resident::call() based on the command.
286 | */
287 | public function __call($name, $args)
288 | {
289 | if (in_array($name, $this->keyCommands)) {
290 | if (is_array($args[0])) {
291 | foreach ($args[0] as $i => $v) {
292 | $args[0][$i] = self::$defaultNamespace . $v;
293 | }
294 | } else {
295 | $args[0] = self::$defaultNamespace . $args[0];
296 | }
297 | }
298 | try {
299 | return $this->driver->__call($name, $args);
300 | } catch (CredisException $e) {
301 | throw new RedisException('Error communicating with Redis: ' . $e->getMessage(), 0, $e);
302 | }
303 | }
304 |
305 | public static function getPrefix()
306 | {
307 | return self::$defaultNamespace;
308 | }
309 |
310 | public static function removePrefix($string)
311 | {
312 | $prefix = self::getPrefix();
313 |
314 | if (substr($string, 0, strlen($prefix)) == $prefix) {
315 | $string = substr($string, strlen($prefix), strlen($string));
316 | }
317 | return $string;
318 | }
319 | }
320 |
--------------------------------------------------------------------------------
/HOWITWORKS.md:
--------------------------------------------------------------------------------
1 | _For an overview of how to **use** php-resque, see `README.md`._
2 |
3 | The following is a step-by-step breakdown of how php-resque operates.
4 |
5 | ## Enqueue Job
6 |
7 | What happens when you call `Resque\Resque::enqueue()`?
8 |
9 | 1. `Resque\Resque::enqueue()` calls `Resque\JobHandler::create()` with the same arguments it
10 | received.
11 | 2. `Resque\JobHandler::create()` checks that your `$args` (the third argument) are
12 | either `null` or in an array
13 | 3. `Resque\JobHandler::create()` generates a job ID (a "token" in most of the docs)
14 | 4. `Resque\JobHandler::create()` pushes the job to the requested queue (first
15 | argument)
16 | 5. `Resque\JobHandler::create()`, if status monitoring is enabled for the job (fourth
17 | argument), calls `Resque\Job\Status::create()` with the job ID as its only
18 | argument
19 | 6. `Resque\Job\Status::create()` creates a key in Redis with the job ID in its
20 | name, and the current status (as well as a couple of timestamps) as its
21 | value, then returns control to `Resque\JobHandler::create()`
22 | 7. `Resque\JobHandler::create()` returns control to `Resque\Resque::enqueue()`, with the job
23 | ID as a return value
24 | 8. `Resque\Resque::enqueue()` triggers the `afterEnqueue` event, then returns control
25 | to your application, again with the job ID as its return value
26 |
27 | ## Workers At Work
28 |
29 | How do the workers process the queues?
30 |
31 | 1. `Resque\Worker\ResqueWorker::work()`, the main loop of the worker process, calls
32 | `Resque\Worker\ResqueWorker->reserve()` to check for a job
33 | 2. `Resque\Worker\ResqueWorker->reserve()` checks whether to use blocking pops or not (from
34 | `BLOCKING`), then acts accordingly:
35 |
36 | - Blocking Pop
37 | 1. `Resque\Worker\ResqueWorker->reserve()` calls `Resque\JobHandler::reserveBlocking()` with
38 | the entire queue list and the timeout (from `INTERVAL`) as arguments
39 | 2. `Resque\JobHandler::reserveBlocking()` calls `Resque\Resque::blpop()` (which in turn
40 | calls Redis' `blpop`, after prepping the queue list for the call, then
41 | processes the response for consistency with other aspects of the
42 | library, before finally returning control [and the queue/content of the
43 | retrieved job, if any] to `Resque\JobHandler::reserveBlocking()`)
44 | 3. `Resque\JobHandler::reserveBlocking()` checks whether the job content is an
45 | array (it should contain the job's type [class], payload [args], and
46 | ID), and aborts processing if not
47 | 4. `Resque\JobHandler::reserveBlocking()` creates a new `Resque\JobHandler` object with
48 | the queue and content as constructor arguments to initialize the job
49 | itself, and returns it, along with control of the process, to
50 | `Resque\Worker\ResqueWorker->reserve()`
51 | - Queue Polling
52 | 1. `Resque\Worker\ResqueWorker->reserve()` iterates through the queue list, calling
53 | `Resque\JobHandler::reserve()` with the current queue's name as the sole
54 | argument on each pass
55 | 2. `Resque\JobHandler::reserve()` passes the queue name on to `Resque\Resque::pop()`,
56 | which in turn calls Redis' `lpop` with the same argument, then returns
57 | control (and the job content, if any) to `Resque\JobHandler::reserve()`
58 | 3. `Resque\JobHandler::reserve()` checks whether the job content is an array (as
59 | before, it should contain the job's type [class], payload [args], and
60 | ID), and aborts processing if not
61 | 4. `Resque\JobHandler::reserve()` creates a new `Resque\JobHandler` object in the same
62 | manner as above, and also returns this object (along with control of
63 | the process) to `Resque\Worker\ResqueWorker->reserve()`
64 |
65 | 3. In either case, `Resque\Worker\ResqueWorker->reserve()` returns the new `Resque\JobHandler`
66 | object, along with control, up to `Resque\Worker\ResqueWorker::work()`; if no job is
67 | found, it simply returns `FALSE`
68 |
69 | - No Jobs
70 | 1. If blocking mode is not enabled, `Resque\Worker\ResqueWorker::work()` sleeps for
71 | `INTERVAL` seconds; it calls `usleep()` for this, so fractional seconds
72 | _are_ supported
73 | - Job Reserved
74 | 1. `Resque\Worker\ResqueWorker::work()` triggers a `beforeFork` event
75 | 2. `Resque\Worker\ResqueWorker::work()` calls `Resque\Worker\ResqueWorker->workingOn()` with the new
76 | `Resque\JobHandler` object as its argument
77 | 3. `Resque\Worker\ResqueWorker->workingOn()` does some reference assignments to help
78 | keep track of the worker/job relationship, then updates the job status
79 | from `WAITING` to `RUNNING`
80 | 4. `Resque\Worker\ResqueWorker->workingOn()` stores the new `Resque\JobHandler` object's
81 | payload in a Redis key associated to the worker itself (this is to
82 | prevent the job from being lost indefinitely, but does rely on that PID
83 | never being allocated on that host to a different worker process), then
84 | returns control to `Resque\Worker\ResqueWorker::work()`
85 | 5. `Resque\Worker\ResqueWorker::work()` forks a child process to run the actual
86 | `perform()`
87 | 6. The next steps differ between the worker and the child, now running in
88 | separate processes:
89 | - Worker
90 | 1. The worker waits for the job process to complete
91 | 2. If the exit status is not 0, the worker calls `Resque\JobHandler->fail()`
92 | with a `Resque\Exceptions\DirtyExitException` as its only argument.
93 | 3. `Resque\JobHandler->fail()` triggers an `onFailure` event
94 | 4. `Resque\JobHandler->fail()` updates the job status from `RUNNING` to
95 | `FAILED`
96 | 5. `Resque\JobHandler->fail()` calls `Resque\FailureHandler::create()` with the job
97 | payload, the `Resque\Exceptions\DirtyExitException`, the internal ID of the
98 | worker, and the queue name as arguments
99 | 6. `Resque\FailureHandler::create()` creates a new object of whatever type has
100 | been set as the `Resque\FailureHandler` "backend" handler; by default, this
101 | is a `Resque\FailureHandler_Redis` object, whose constructor simply
102 | collects the data passed into `Resque\FailureHandler::create()` and pushes
103 | it into Redis in the `failed` queue
104 | 7. `Resque\JobHandler->fail()` increments two failure counters in Redis: one
105 | for a total count, and one for the worker
106 | 8. `Resque\JobHandler->fail()` returns control to the worker (still in
107 | `Resque\Worker\ResqueWorker::work()`) without a value
108 | - Job
109 | 1. `Resque\Job\PID` is created, registering the PID of the actual process
110 | doing the job.
111 | 2. The job calls `Resque\Worker\ResqueWorker->perform()` with the `Resque\JobHandler` as
112 | its only argument.
113 | 3. `Resque\Worker\ResqueWorker->perform()` sets up a `try...catch` block so it can
114 | properly handle exceptions by marking jobs as failed (by calling
115 | `Resque\JobHandler->fail()`, as above)
116 | 4. Inside the `try...catch`, `Resque\Worker\ResqueWorker->perform()` triggers an
117 | `afterFork` event
118 | 5. Still inside the `try...catch`, `Resque\Worker\ResqueWorker->perform()` calls
119 | `Resque\JobHandler->perform()` with no arguments
120 | 6. `Resque\JobHandler->perform()` calls `Resque\JobHandler->getInstance()` with no
121 | arguments
122 | 7. If `Resque\JobHandler->getInstance()` has already been called, it returns
123 | the existing instance; otherwise:
124 | 8. `Resque\JobHandler->getInstance()` checks that the job's class (type)
125 | exists and has a `perform()` method; if not, in either case, it
126 | throws an exception which will be caught by
127 | `Resque\Worker\ResqueWorker->perform()`
128 | 9. `Resque\JobHandler->getInstance()` creates an instance of the job's class,
129 | and initializes it with a reference to the `Resque\JobHandler` itself, the
130 | job's arguments (which it gets by calling
131 | `Resque\JobHandler->getArguments()`, which in turn simply returns the value
132 | of `args[0]`, or an empty array if no arguments were passed), and
133 | the queue name
134 | 10. `Resque\JobHandler->getInstance()` returns control, along with the job
135 | class instance, to `Resque\JobHandler->perform()`
136 | 11. `Resque\JobHandler->perform()` sets up its own `try...catch` block to
137 | handle `Resque\Exceptions\DoNotPerformException` exceptions; any other exceptions are
138 | passed up to `Resque\Worker\ResqueWorker->perform()`
139 | 12. `Resque\JobHandler->perform()` triggers a `beforePerform` event
140 | 13. `Resque\JobHandler->perform()` calls `setUp()` on the instance, if it
141 | exists
142 | 14. `Resque\JobHandler->perform()` calls `perform()` on the instance
143 | 15. `Resque\JobHandler->perform()` calls `tearDown()` on the instance, if it
144 | exists
145 | 16. `Resque\JobHandler->perform()` triggers an `afterPerform` event
146 | 17. The `try...catch` block ends, suppressing `Resque\Exceptions\DoNotPerformException`
147 | exceptions by returning control, and the value `FALSE`, to
148 | `Resque\Worker\ResqueWorker->perform()`; any other situation returns the value
149 | `TRUE` along with control, instead
150 | 18. The `try...catch` block in `Resque\Worker\ResqueWorker->perform()` ends
151 | 19. `Resque\Worker\ResqueWorker->perform()` updates the job status from `RUNNING` to
152 | `COMPLETE`, then returns control, with no value, to the worker
153 | (again still in `Resque\Worker\ResqueWorker::work()`)
154 | 20. `Resque\Job\PID()` is removed, the forked process will terminate soon
155 | cleanly
156 | 21. `Resque\Worker\ResqueWorker::work()` calls `exit(0)` to terminate the job process
157 | - SPECIAL CASE: Non-forking OS (Windows)
158 | 1. Same as the job above, except it doesn't call `exit(0)` when done
159 | 7. `Resque\Worker\ResqueWorker::work()` calls `Resque\Worker\ResqueWorker->doneWorking()` with no
160 | arguments
161 | 8. `Resque\Worker\ResqueWorker->doneWorking()` increments two processed counters in
162 | Redis: one for a total count, and one for the worker
163 | 9. `Resque\Worker\ResqueWorker->doneWorking()` deletes the Redis key set in
164 | `Resque\Worker\ResqueWorker->workingOn()`, then returns control, with no value, to
165 | `Resque\Worker\ResqueWorker::work()`
166 |
167 | 4. `Resque\Worker\ResqueWorker::work()` returns control to the beginning of the main loop,
168 | where it will wait for the next job to become available, and start this
169 | process all over again
170 |
--------------------------------------------------------------------------------
/lib/Resque.php:
--------------------------------------------------------------------------------
1 |
13 | * @license http://www.opensource.org/licenses/mit-license.php
14 | */
15 | class Resque
16 | {
17 | public const VERSION = '1.2';
18 |
19 | public const DEFAULT_INTERVAL = 5;
20 |
21 | /**
22 | * @var Redis Instance of Resque\Redis that talks to redis.
23 | */
24 | public static $redis = null;
25 |
26 | /**
27 | * @var mixed Host/port conbination separated by a colon, or a nested
28 | * array of server swith host/port pairs
29 | */
30 | protected static $redisServer = null;
31 |
32 | /**
33 | * @var int ID of Redis database to select.
34 | */
35 | protected static $redisDatabase = 0;
36 |
37 | /**
38 | * @var string auth of Redis database
39 | */
40 | protected static $auth;
41 |
42 | /**
43 | * Given a host/port combination separated by a colon, set it as
44 | * the redis server that Resque will talk to.
45 | *
46 | * @param mixed $server Host/port combination separated by a colon, DSN-formatted URI, or
47 | * a callable that receives the configured database ID
48 | * and returns a Resque\Redis instance, or
49 | * a nested array of servers with host/port pairs.
50 | * @param int $database
51 | * @param string $auth
52 | */
53 | public static function setBackend($server, $database = 0, $auth = null)
54 | {
55 | self::$redisServer = $server;
56 | self::$redisDatabase = $database;
57 | self::$auth = $auth;
58 | self::$redis = null;
59 | }
60 |
61 | /**
62 | * Return an instance of the Resque\Redis class instantiated for Resque.
63 | *
64 | * @return \Resque\Redis Instance of Resque\Redis.
65 | */
66 | public static function redis()
67 | {
68 | if (self::$redis !== null) {
69 | return self::$redis;
70 | }
71 |
72 | if (is_callable(self::$redisServer)) {
73 | self::$redis = call_user_func(self::$redisServer, self::$redisDatabase);
74 | } else {
75 | self::$redis = new Redis(self::$redisServer, self::$redisDatabase);
76 | }
77 |
78 | if (!empty(self::$auth)) {
79 | self::$redis->auth(self::$auth);
80 | }
81 |
82 | return self::$redis;
83 | }
84 |
85 | /**
86 | * fork() helper method for php-resque that handles issues PHP socket
87 | * and phpredis have with passing around sockets between child/parent
88 | * processes.
89 | *
90 | * Will close connection to Redis before forking.
91 | *
92 | * @return int Return vars as per pcntl_fork(). False if pcntl_fork is unavailable
93 | */
94 | public static function fork()
95 | {
96 | if (!function_exists('pcntl_fork')) {
97 | return false;
98 | }
99 |
100 | // Close the connection to Redis before forking.
101 | // This is a workaround for issues phpredis has.
102 | self::$redis = null;
103 |
104 | $pid = pcntl_fork();
105 | if ($pid === -1) {
106 | throw new RuntimeException('Unable to fork child worker.');
107 | }
108 |
109 | return $pid;
110 | }
111 |
112 | /**
113 | * Push a job to the end of a specific queue. If the queue does not
114 | * exist, then create it as well.
115 | *
116 | * @param string $queue The name of the queue to add the job to.
117 | * @param array $item Job description as an array to be JSON encoded.
118 | */
119 | public static function push($queue, $item)
120 | {
121 | $encodedItem = json_encode($item);
122 | if ($encodedItem === false) {
123 | return false;
124 | }
125 | self::redis()->sadd('queues', $queue);
126 | $length = self::redis()->rpush('queue:' . $queue, $encodedItem);
127 | if ($length < 1) {
128 | return false;
129 | }
130 | return true;
131 | }
132 |
133 | /**
134 | * Pop an item off the end of the specified queue, decode it and
135 | * return it.
136 | *
137 | * @param string $queue The name of the queue to fetch an item from.
138 | * @return array Decoded item from the queue.
139 | */
140 | public static function pop($queue)
141 | {
142 | $item = self::redis()->lpop('queue:' . $queue);
143 |
144 | if (!$item) {
145 | return;
146 | }
147 |
148 | return json_decode($item, true);
149 | }
150 |
151 | /**
152 | * Remove items of the specified queue
153 | *
154 | * @param string $queue The name of the queue to fetch an item from.
155 | * @param array $items
156 | * @return integer number of deleted items
157 | */
158 | public static function dequeue($queue, $items = array())
159 | {
160 | if (count($items) > 0) {
161 | return self::removeItems($queue, $items);
162 | } else {
163 | return self::removeList($queue);
164 | }
165 | }
166 |
167 | /**
168 | * Remove specified queue
169 | *
170 | * @param string $queue The name of the queue to remove.
171 | * @return integer Number of deleted items
172 | */
173 | public static function removeQueue($queue)
174 | {
175 | $num = self::removeList($queue);
176 | self::redis()->srem('queues', $queue);
177 | return $num;
178 | }
179 |
180 | /**
181 | * Pop an item off the end of the specified queues, using blocking list pop,
182 | * decode it and return it.
183 | *
184 | * @param array $queues
185 | * @param int $timeout
186 | * @return null|array Decoded item from the queue.
187 | */
188 | public static function blpop(array $queues, $timeout)
189 | {
190 | $list = array();
191 | foreach ($queues as $queue) {
192 | $list[] = 'queue:' . $queue;
193 | }
194 |
195 | $item = self::redis()->blpop($list, (int)$timeout);
196 |
197 | if (!$item) {
198 | return;
199 | }
200 |
201 | /**
202 | * Normally the Resque\Redis class returns queue names without the prefix
203 | * But the blpop is a bit different. It returns the name as prefix:queue:name
204 | * So we need to strip off the prefix:queue: part
205 | */
206 | $queue = substr($item[0], strlen(self::redis()->getPrefix() . 'queue:'));
207 |
208 | return array(
209 | 'queue' => $queue,
210 | 'payload' => json_decode($item[1], true)
211 | );
212 | }
213 |
214 | /**
215 | * Return the size (number of pending jobs) of the specified queue.
216 | *
217 | * @param string $queue name of the queue to be checked for pending jobs
218 | *
219 | * @return int The size of the queue.
220 | */
221 | public static function size($queue)
222 | {
223 | return self::redis()->llen('queue:' . $queue);
224 | }
225 |
226 | /**
227 | * Create a new job and save it to the specified queue.
228 | *
229 | * @param string $queue The name of the queue to place the job in.
230 | * @param string $class The name of the class that contains the code to execute the job.
231 | * @param array $args Any optional arguments that should be passed when the job is executed.
232 | * @param boolean $trackStatus Set to true to be able to monitor the status of a job.
233 | * @param string $prefix The prefix needs to be set for the status key
234 | *
235 | * @return string|boolean Job ID when the job was created, false if creation was cancelled due to beforeEnqueue
236 | */
237 | public static function enqueue($queue, $class, array $args = [], $trackStatus = false, $prefix = "")
238 | {
239 | $id = Resque::generateJobId();
240 | $hookParams = array(
241 | 'class' => $class,
242 | 'args' => $args,
243 | 'queue' => $queue,
244 | 'id' => $id,
245 | );
246 | try {
247 | Event::trigger('beforeEnqueue', $hookParams);
248 | } catch (DoNotCreateException $e) {
249 | return false;
250 | }
251 |
252 | JobHandler::create($queue, $class, $args, $trackStatus, $id, $prefix);
253 | Event::trigger('afterEnqueue', $hookParams);
254 |
255 | return $id;
256 | }
257 |
258 | /**
259 | * Reserve and return the next available job in the specified queue.
260 | *
261 | * @param string $queue Queue to fetch next available job from.
262 | * @return \Resque\JobHandler Instance of Resque\JobHandler to be processed, false if none or error.
263 | */
264 | public static function reserve($queue)
265 | {
266 | return JobHandler::reserve($queue);
267 | }
268 |
269 | /**
270 | * Get an array of all known queues.
271 | *
272 | * @return array Array of queues.
273 | */
274 | public static function queues()
275 | {
276 | $queues = self::redis()->smembers('queues');
277 | if (!is_array($queues)) {
278 | $queues = array();
279 | }
280 | return $queues;
281 | }
282 |
283 | /**
284 | * Retrieve all the items of a queue with Redis
285 | *
286 | * @return array Array of items.
287 | */
288 | public static function items($queue, $start = 0, $stop = -1)
289 | {
290 | $list = self::redis()->lrange('queue:' . $queue, $start, $stop);
291 | if (!is_array($list)) {
292 | $list = array();
293 | }
294 | return $list;
295 | }
296 |
297 | /**
298 | * Remove Items from the queue
299 | * Safely moving each item to a temporary queue before processing it
300 | * If the Job matches, counts otherwise puts it in a requeue_queue
301 | * which at the end eventually be copied back into the original queue
302 | *
303 | * @private
304 | *
305 | * @param string $queue The name of the queue
306 | * @param array $items
307 | * @return integer number of deleted items
308 | */
309 | private static function removeItems($queue, $items = array())
310 | {
311 | $counter = 0;
312 | $originalQueue = 'queue:' . $queue;
313 | $tempQueue = $originalQueue . ':temp:' . time();
314 | $requeueQueue = $tempQueue . ':requeue';
315 |
316 | // move each item from original queue to temp queue and process it
317 | $finished = false;
318 | while (!$finished) {
319 | $string = self::redis()->rpoplpush($originalQueue, self::redis()->getPrefix() . $tempQueue);
320 |
321 | if (!empty($string)) {
322 | if (self::matchItem($string, $items)) {
323 | self::redis()->rpop($tempQueue);
324 | $counter++;
325 | } else {
326 | self::redis()->rpoplpush($tempQueue, self::redis()->getPrefix() . $requeueQueue);
327 | }
328 | } else {
329 | $finished = true;
330 | }
331 | }
332 |
333 | // move back from temp queue to original queue
334 | $finished = false;
335 | while (!$finished) {
336 | $string = self::redis()->rpoplpush($requeueQueue, self::redis()->getPrefix() . $originalQueue);
337 | if (empty($string)) {
338 | $finished = true;
339 | }
340 | }
341 |
342 | // remove temp queue and requeue queue
343 | self::redis()->del($requeueQueue);
344 | self::redis()->del($tempQueue);
345 |
346 | return $counter;
347 | }
348 |
349 | /**
350 | * matching item
351 | * item can be ['class'] or ['class' => 'id'] or ['class' => {'foo' => 1, 'bar' => 2}]
352 | * @private
353 | *
354 | * @params string $string redis result in json
355 | * @params $items
356 | *
357 | * @return (bool)
358 | */
359 | private static function matchItem($string, $items)
360 | {
361 | $decoded = json_decode($string, true);
362 |
363 | foreach ($items as $key => $val) {
364 | # class name only ex: item[0] = ['class']
365 | if (is_numeric($key)) {
366 | if ($decoded['class'] == $val) {
367 | return true;
368 | }
369 | # class name with args , example: item[0] = ['class' => {'foo' => 1, 'bar' => 2}]
370 | } elseif (is_array($val)) {
371 | $decodedArgs = (array)$decoded['args'][0];
372 | if (
373 | $decoded['class'] == $key &&
374 | count($decodedArgs) > 0 && count(array_diff($decodedArgs, $val)) == 0
375 | ) {
376 | return true;
377 | }
378 | # class name with ID, example: item[0] = ['class' => 'id']
379 | } else {
380 | if ($decoded['class'] == $key && $decoded['id'] == $val) {
381 | return true;
382 | }
383 | }
384 | }
385 | return false;
386 | }
387 |
388 | /**
389 | * Remove List
390 | *
391 | * @private
392 | *
393 | * @params string $queue the name of the queue
394 | * @return integer number of deleted items belongs to this list
395 | */
396 | private static function removeList($queue)
397 | {
398 | $counter = self::size($queue);
399 | $result = self::redis()->del('queue:' . $queue);
400 | return ($result == 1) ? $counter : 0;
401 | }
402 |
403 | /*
404 | * Generate an identifier to attach to a job for status tracking.
405 | *
406 | * @return string
407 | */
408 | public static function generateJobId()
409 | {
410 | return md5(uniqid('', true));
411 | }
412 | }
413 |
--------------------------------------------------------------------------------
/test/Resque/Tests/JobHandlerTest.php:
--------------------------------------------------------------------------------
1 |
22 | * @license http://www.opensource.org/licenses/mit-license.php
23 | */
24 | class JobHandlerTest extends ResqueTestCase
25 | {
26 | protected $worker;
27 |
28 | public function setUp(): void
29 | {
30 | parent::setUp();
31 |
32 | // Register a worker to test with
33 | $this->worker = new ResqueWorker('jobs');
34 | $this->worker->setLogger($this->logger);
35 | $this->worker->registerWorker();
36 | }
37 |
38 | public function testJobCanBeQueued()
39 | {
40 | $this->assertTrue((bool)Resque::enqueue('jobs', 'Test_Job'));
41 | }
42 |
43 | public function testRedisErrorThrowsExceptionOnJobCreation()
44 | {
45 | $this->expectException('\Resque\Exceptions\RedisException');
46 |
47 | $mockCredis = $this->getMockBuilder('Credis_Client')
48 | ->setMethods(['connect', '__call'])
49 | ->getMock();
50 | $mockCredis->expects($this->any())->method('__call')
51 | ->will($this->throwException(new CredisException('failure')));
52 |
53 | Resque::setBackend(function($database) use ($mockCredis) {
54 | return new Redis('localhost:6379', $database, $mockCredis);
55 | });
56 | Resque::enqueue('jobs', 'This is a test');
57 | }
58 |
59 | public function testQeueuedJobCanBeReserved()
60 | {
61 | Resque::enqueue('jobs', 'Test_Job');
62 |
63 | $job = JobHandler::reserve('jobs');
64 | if($job == false) {
65 | $this->fail('Job could not be reserved.');
66 | }
67 | $this->assertEquals('jobs', $job->queue);
68 | $this->assertEquals('Test_Job', $job->payload['class']);
69 | }
70 |
71 | public function testQueuedJobReturnsExactSamePassedInArguments()
72 | {
73 | $args = array(
74 | 'int' => 123,
75 | 'numArray' => array(
76 | 1,
77 | 2,
78 | ),
79 | 'assocArray' => array(
80 | 'key1' => 'value1',
81 | 'key2' => 'value2'
82 | ),
83 | );
84 | Resque::enqueue('jobs', 'Test_Job', $args);
85 | $job = JobHandler::reserve('jobs');
86 |
87 | $this->assertEquals($args, $job->getArguments());
88 | }
89 |
90 | public function testAfterJobIsReservedItIsRemoved()
91 | {
92 | Resque::enqueue('jobs', 'Test_Job');
93 | JobHandler::reserve('jobs');
94 | $this->assertFalse(JobHandler::reserve('jobs'));
95 | }
96 |
97 | public function testRecreatedJobMatchesExistingJob()
98 | {
99 | $args = array(
100 | 'int' => 123,
101 | 'numArray' => array(
102 | 1,
103 | 2,
104 | ),
105 | 'assocArray' => array(
106 | 'key1' => 'value1',
107 | 'key2' => 'value2'
108 | ),
109 | );
110 |
111 | Resque::enqueue('jobs', 'Test_Job', $args);
112 | $job = JobHandler::reserve('jobs');
113 |
114 | // Now recreate it
115 | $job->recreate();
116 |
117 | $newJob = JobHandler::reserve('jobs');
118 | $this->assertEquals($job->payload['class'], $newJob->payload['class']);
119 | $this->assertEquals($job->getArguments(), $newJob->getArguments());
120 | }
121 |
122 |
123 | public function testFailedJobExceptionsAreCaught()
124 | {
125 | $payload = array(
126 | 'class' => 'Failing_Job',
127 | 'args' => null
128 | );
129 | $job = new JobHandler('jobs', $payload);
130 | $job->worker = $this->worker;
131 |
132 | $this->worker->perform($job);
133 |
134 | $this->assertEquals(1, Stat::get('failed'));
135 | $this->assertEquals(1, Stat::get('failed:'.$this->worker));
136 | }
137 |
138 | public function testJobWithoutPerformMethodThrowsException()
139 | {
140 | $this->expectException('\Resque\Exceptions\ResqueException');
141 |
142 | Resque::enqueue('jobs', 'Test_Job_Without_Perform_Method');
143 | $job = $this->worker->reserve();
144 | $job->worker = $this->worker;
145 | $job->perform();
146 | }
147 |
148 | public function testInvalidJobThrowsException()
149 | {
150 | $this->expectException('Resque\Exceptions\ResqueException');
151 |
152 | Resque::enqueue('jobs', 'Invalid_Job');
153 | $job = $this->worker->reserve();
154 | $job->worker = $this->worker;
155 | $job->perform();
156 | }
157 |
158 | public function testJobWithSetUpCallbackFiresSetUp()
159 | {
160 | $payload = array(
161 | 'class' => 'Test_Job_With_SetUp',
162 | 'args' => array(array(
163 | 'somevar',
164 | 'somevar2',
165 | )),
166 | 'id' => Resque::generateJobId(),
167 | );
168 | $job = new JobHandler('jobs', $payload);
169 | $job->perform();
170 |
171 | $this->assertTrue(Test_Job_With_SetUp::$called);
172 | }
173 |
174 | public function testJobWithTearDownCallbackFiresTearDown()
175 | {
176 | $payload = array(
177 | 'class' => 'Test_Job_With_TearDown',
178 | 'args' => array(array(
179 | 'somevar',
180 | 'somevar2',
181 | )),
182 | 'id' => Resque::generateJobId(),
183 | );
184 | $job = new JobHandler('jobs', $payload);
185 | $job->perform();
186 |
187 | $this->assertTrue(Test_Job_With_TearDown::$called);
188 | }
189 |
190 | public function testNamespaceNaming() {
191 | $fixture = array(
192 | array('test' => 'more:than:one:with:', 'assertValue' => 'more:than:one:with:'),
193 | array('test' => 'more:than:one:without', 'assertValue' => 'more:than:one:without:'),
194 | array('test' => 'resque', 'assertValue' => 'resque:'),
195 | array('test' => 'resque:', 'assertValue' => 'resque:'),
196 | );
197 |
198 | foreach($fixture as $item) {
199 | Redis::prefix($item['test']);
200 | $this->assertEquals(Redis::getPrefix(), $item['assertValue']);
201 | }
202 | }
203 |
204 | public function testJobWithNamespace()
205 | {
206 | Redis::prefix('php');
207 | $queue = 'jobs';
208 | $payload = array('another_value');
209 | Resque::enqueue($queue, 'Test_Job_With_TearDown', $payload);
210 |
211 | $this->assertEquals(Resque::queues(), array('jobs'));
212 | $this->assertEquals(Resque::size($queue), 1);
213 |
214 | Redis::prefix('resque');
215 | $this->assertEquals(Resque::size($queue), 0);
216 | }
217 |
218 | public function testDequeueAll()
219 | {
220 | $queue = 'jobs';
221 | Resque::enqueue($queue, 'Test_Job_Dequeue');
222 | Resque::enqueue($queue, 'Test_Job_Dequeue');
223 | $this->assertEquals(Resque::size($queue), 2);
224 | $this->assertEquals(Resque::dequeue($queue), 2);
225 | $this->assertEquals(Resque::size($queue), 0);
226 | }
227 |
228 | public function testDequeueMakeSureNotDeleteOthers()
229 | {
230 | $queue = 'jobs';
231 | Resque::enqueue($queue, 'Test_Job_Dequeue');
232 | Resque::enqueue($queue, 'Test_Job_Dequeue');
233 | $other_queue = 'other_jobs';
234 | Resque::enqueue($other_queue, 'Test_Job_Dequeue');
235 | Resque::enqueue($other_queue, 'Test_Job_Dequeue');
236 | $this->assertEquals(Resque::size($queue), 2);
237 | $this->assertEquals(Resque::size($other_queue), 2);
238 | $this->assertEquals(Resque::dequeue($queue), 2);
239 | $this->assertEquals(Resque::size($queue), 0);
240 | $this->assertEquals(Resque::size($other_queue), 2);
241 | }
242 |
243 | public function testDequeueSpecificItem()
244 | {
245 | $queue = 'jobs';
246 | Resque::enqueue($queue, 'Test_Job_Dequeue1');
247 | Resque::enqueue($queue, 'Test_Job_Dequeue2');
248 | $this->assertEquals(Resque::size($queue), 2);
249 | $test = array('Test_Job_Dequeue2');
250 | $this->assertEquals(Resque::dequeue($queue, $test), 1);
251 | $this->assertEquals(Resque::size($queue), 1);
252 | }
253 |
254 | public function testDequeueSpecificMultipleItems()
255 | {
256 | $queue = 'jobs';
257 | Resque::enqueue($queue, 'Test_Job_Dequeue1');
258 | Resque::enqueue($queue, 'Test_Job_Dequeue2');
259 | Resque::enqueue($queue, 'Test_Job_Dequeue3');
260 | $this->assertEquals(Resque::size($queue), 3);
261 | $test = array('Test_Job_Dequeue2', 'Test_Job_Dequeue3');
262 | $this->assertEquals(Resque::dequeue($queue, $test), 2);
263 | $this->assertEquals(Resque::size($queue), 1);
264 | }
265 |
266 | public function testDequeueNonExistingItem()
267 | {
268 | $queue = 'jobs';
269 | Resque::enqueue($queue, 'Test_Job_Dequeue1');
270 | Resque::enqueue($queue, 'Test_Job_Dequeue2');
271 | Resque::enqueue($queue, 'Test_Job_Dequeue3');
272 | $this->assertEquals(Resque::size($queue), 3);
273 | $test = array('Test_Job_Dequeue4');
274 | $this->assertEquals(Resque::dequeue($queue, $test), 0);
275 | $this->assertEquals(Resque::size($queue), 3);
276 | }
277 |
278 | public function testDequeueNonExistingItem2()
279 | {
280 | $queue = 'jobs';
281 | Resque::enqueue($queue, 'Test_Job_Dequeue1');
282 | Resque::enqueue($queue, 'Test_Job_Dequeue2');
283 | Resque::enqueue($queue, 'Test_Job_Dequeue3');
284 | $this->assertEquals(Resque::size($queue), 3);
285 | $test = array('Test_Job_Dequeue4', 'Test_Job_Dequeue1');
286 | $this->assertEquals(Resque::dequeue($queue, $test), 1);
287 | $this->assertEquals(Resque::size($queue), 2);
288 | }
289 |
290 | public function testDequeueItemID()
291 | {
292 | $queue = 'jobs';
293 | Resque::enqueue($queue, 'Test_Job_Dequeue');
294 | $qid = Resque::enqueue($queue, 'Test_Job_Dequeue');
295 | $this->assertEquals(Resque::size($queue), 2);
296 | $test = array('Test_Job_Dequeue' => $qid);
297 | $this->assertEquals(Resque::dequeue($queue, $test), 1);
298 | $this->assertEquals(Resque::size($queue), 1);
299 | }
300 |
301 | public function testDequeueWrongItemID()
302 | {
303 | $queue = 'jobs';
304 | Resque::enqueue($queue, 'Test_Job_Dequeue');
305 | $qid = Resque::enqueue($queue, 'Test_Job_Dequeue');
306 | $this->assertEquals(Resque::size($queue), 2);
307 | #qid right but class name is wrong
308 | $test = array('Test_Job_Dequeue1' => $qid);
309 | $this->assertEquals(Resque::dequeue($queue, $test), 0);
310 | $this->assertEquals(Resque::size($queue), 2);
311 | }
312 |
313 | public function testDequeueWrongItemID2()
314 | {
315 | $queue = 'jobs';
316 | Resque::enqueue($queue, 'Test_Job_Dequeue');
317 | $qid = Resque::enqueue($queue, 'Test_Job_Dequeue');
318 | $this->assertEquals(Resque::size($queue), 2);
319 | $test = array('Test_Job_Dequeue' => 'r4nD0mH4sh3dId');
320 | $this->assertEquals(Resque::dequeue($queue, $test), 0);
321 | $this->assertEquals(Resque::size($queue), 2);
322 | }
323 |
324 | public function testDequeueItemWithArg()
325 | {
326 | $queue = 'jobs';
327 | $arg = array('foo' => 1, 'bar' => 2);
328 | Resque::enqueue($queue, 'Test_Job_Dequeue9');
329 | Resque::enqueue($queue, 'Test_Job_Dequeue9', $arg);
330 | $this->assertEquals(Resque::size($queue), 2);
331 | $test = array('Test_Job_Dequeue9' => $arg);
332 | $this->assertEquals(Resque::dequeue($queue, $test), 1);
333 | #$this->assertEquals(Resque::size($queue), 1);
334 | }
335 |
336 | public function testDequeueSeveralItemsWithArgs()
337 | {
338 | // GIVEN
339 | $queue = 'jobs';
340 | $args = array('foo' => 1, 'bar' => 10);
341 | $removeArgs = array('foo' => 1, 'bar' => 2);
342 | Resque::enqueue($queue, 'Test_Job_Dequeue9', $args);
343 | Resque::enqueue($queue, 'Test_Job_Dequeue9', $removeArgs);
344 | Resque::enqueue($queue, 'Test_Job_Dequeue9', $removeArgs);
345 | $this->assertEquals(Resque::size($queue), 3);
346 |
347 | // WHEN
348 | $test = array('Test_Job_Dequeue9' => $removeArgs);
349 | $removedItems = Resque::dequeue($queue, $test);
350 |
351 | // THEN
352 | $this->assertEquals($removedItems, 2);
353 | $this->assertEquals(Resque::size($queue), 1);
354 | $item = Resque::pop($queue);
355 | $this->assertIsArray($item['args']);
356 | $this->assertEquals(10, $item['args'][0]['bar'], 'Wrong items were dequeued from queue!');
357 | }
358 |
359 | public function testDequeueItemWithUnorderedArg()
360 | {
361 | $queue = 'jobs';
362 | $arg = array('foo' => 1, 'bar' => 2);
363 | $arg2 = array('bar' => 2, 'foo' => 1);
364 | Resque::enqueue($queue, 'Test_Job_Dequeue');
365 | Resque::enqueue($queue, 'Test_Job_Dequeue', $arg);
366 | $this->assertEquals(Resque::size($queue), 2);
367 | $test = array('Test_Job_Dequeue' => $arg2);
368 | $this->assertEquals(Resque::dequeue($queue, $test), 1);
369 | $this->assertEquals(Resque::size($queue), 1);
370 | }
371 |
372 | public function testDequeueItemWithiWrongArg()
373 | {
374 | $queue = 'jobs';
375 | $arg = array('foo' => 1, 'bar' => 2);
376 | $arg2 = array('foo' => 2, 'bar' => 3);
377 | Resque::enqueue($queue, 'Test_Job_Dequeue');
378 | Resque::enqueue($queue, 'Test_Job_Dequeue', $arg);
379 | $this->assertEquals(Resque::size($queue), 2);
380 | $test = array('Test_Job_Dequeue' => $arg2);
381 | $this->assertEquals(Resque::dequeue($queue, $test), 0);
382 | $this->assertEquals(Resque::size($queue), 2);
383 | }
384 |
385 | public function testUseDefaultFactoryToGetJobInstance()
386 | {
387 | $payload = array(
388 | 'class' => 'Resque\Tests\Some_Job_Class',
389 | 'args' => null,
390 | 'id' => Resque::generateJobId()
391 | );
392 | $job = new JobHandler('jobs', $payload);
393 | $instance = $job->getInstance();
394 | $this->assertInstanceOf('Resque\Tests\Some_Job_Class', $instance);
395 | }
396 |
397 | public function testUseFactoryToGetJobInstance()
398 | {
399 | $payload = array(
400 | 'class' => 'Resque\Tests\Some_Job_Class',
401 | 'args' => array(array()),
402 | 'id' => Resque::generateJobId()
403 | );
404 | $job = new JobHandler('jobs', $payload);
405 | $factory = new Some_Stub_Factory();
406 | $job->setJobFactory($factory);
407 | $instance = $job->getInstance();
408 | $this->assertInstanceOf('Resque\Job\Job', $instance);
409 | }
410 |
411 | public function testDoNotUseFactoryToGetInstance()
412 | {
413 | $payload = array(
414 | 'class' => 'Resque\Tests\Some_Job_Class',
415 | 'args' => array(array()),
416 | 'id' => Resque::generateJobId()
417 | );
418 | $job = new JobHandler('jobs', $payload);
419 | $factory = $this->getMockBuilder('Resque\Job\FactoryInterface')
420 | ->getMock();
421 | $testJob = $this->getMockBuilder('Resque\Job\Job')
422 | ->getMock();
423 | $factory->expects(self::never())->method('create')->will(self::returnValue($testJob));
424 | $instance = $job->getInstance();
425 | $this->assertInstanceOf('Resque\Job\Job', $instance);
426 | }
427 |
428 | public function testJobStatusIsNullIfIdMissingFromPayload()
429 | {
430 | $payload = array(
431 | 'class' => 'Resque\Tests\Some_Job_Class',
432 | 'args' => null,
433 | 'id' => Resque::generateJobId()
434 | );
435 | $job = new JobHandler('jobs', $payload);
436 | $this->assertEquals(null, $job->getStatus());
437 | }
438 |
439 | public function testJobCanBeRecreatedFromLegacyPayload()
440 | {
441 | $payload = array(
442 | 'class' => 'Resque\Tests\Some_Job_Class',
443 | 'args' => null,
444 | 'id' => Resque::generateJobId()
445 | );
446 | $job = new JobHandler('jobs', $payload);
447 | $job->recreate();
448 | $newJob = JobHandler::reserve('jobs');
449 | $this->assertEquals('jobs', $newJob->queue);
450 | $this->assertEquals('Resque\Tests\Some_Job_Class', $newJob->payload['class']);
451 | $this->assertNotNull($newJob->payload['id']);
452 | }
453 |
454 | public function testJobHandlerSetsPopTime()
455 | {
456 | $payload = array(
457 | 'class' => 'Resque\Tests\Some_Job_Class',
458 | 'args' => null
459 | );
460 |
461 | $now = microtime(true);
462 |
463 | $job = new JobHandler('jobs', $payload);
464 | $instance = $job->getInstance();
465 |
466 | $this->assertIsFloat($job->popTime);
467 | $this->assertTrue($job->popTime >= $now);
468 | }
469 |
470 | public function testJobHandlerSetsStartAndEndTimeForSuccessfulJob()
471 | {
472 | $payload = array(
473 | 'class' => 'Resque\Tests\Some_Job_Class',
474 | 'args' => null
475 | );
476 |
477 | $job = new JobHandler('jobs', $payload);
478 | $job->perform();
479 |
480 | $this->assertIsFloat($job->startTime);
481 | $this->assertTrue($job->startTime >= $job->popTime);
482 |
483 | $this->assertIsFloat($job->endTime);
484 | $this->assertTrue($job->endTime >= $job->startTime);
485 | }
486 |
487 | public function testJobHandlerSetsStartAndEndTimeForFailedJob()
488 | {
489 | $payload = array(
490 | 'class' => 'Failing_Job',
491 | 'args' => null
492 | );
493 | $job = new JobHandler('jobs', $payload);
494 | $job->worker = $this->worker;
495 |
496 | $this->worker->perform($job);
497 |
498 | $this->assertIsFloat($job->startTime);
499 | $this->assertTrue($job->startTime >= $job->popTime);
500 |
501 | $this->assertIsFloat($job->endTime);
502 | $this->assertTrue($job->endTime >= $job->startTime);
503 | }
504 | }
505 |
506 | class Some_Job_Class extends Job
507 | {
508 |
509 | /**
510 | * @return bool
511 | */
512 | public function perform()
513 | {
514 | return true;
515 | }
516 | }
517 |
518 | class Some_Stub_Factory implements FactoryInterface
519 | {
520 |
521 | /**
522 | * @param $className
523 | * @param array $args
524 | * @param $queue
525 | * @return Resque\Job\Job
526 | */
527 | public function create($className, $args, $queue): Job
528 | {
529 | return new Some_Job_Class();
530 | }
531 | }
532 |
--------------------------------------------------------------------------------
/lib/Worker/ResqueWorker.php:
--------------------------------------------------------------------------------
1 |
27 | * @license http://www.opensource.org/licenses/mit-license.php
28 | */
29 | class ResqueWorker
30 | {
31 | /**
32 | * @var string Prefix for the process name
33 | */
34 | private static $processPrefix = 'resque';
35 |
36 | /**
37 | * @var \Psr\Log\LoggerInterface Logging object that impliments the PSR-3 LoggerInterface
38 | */
39 | public $logger;
40 |
41 | /**
42 | * @var bool Whether this worker is running in a forked child process.
43 | */
44 | public $hasParent = false;
45 |
46 | /**
47 | * @var array Array of all associated queues for this worker.
48 | */
49 | private $queues = array();
50 |
51 | /**
52 | * @var string The hostname of this worker.
53 | */
54 | private $hostname;
55 |
56 | /**
57 | * @var boolean True if on the next iteration, the worker should shutdown.
58 | */
59 | private $shutdown = false;
60 |
61 | /**
62 | * @var boolean True if this worker is paused.
63 | */
64 | private $paused = false;
65 |
66 | /**
67 | * @var string String identifying this worker.
68 | */
69 | private $id;
70 |
71 | /**
72 | * @var \Resque\JobHandler Current job, if any, being processed by this worker.
73 | */
74 | private $currentJob = null;
75 |
76 | /**
77 | * @var int Process ID of child worker processes.
78 | */
79 | private $child = null;
80 |
81 | /**
82 | * Instantiate a new worker, given a list of queues that it should be working
83 | * on. The list of queues should be supplied in the priority that they should
84 | * be checked for jobs (first come, first served)
85 | *
86 | * Passing a single '*' allows the worker to work on all queues in alphabetical
87 | * order. You can easily add new queues dynamically and have them worked on using
88 | * this method.
89 | *
90 | * @param string|array $queues String with a single queue name, array with multiple.
91 | */
92 | public function __construct($queues)
93 | {
94 | $this->logger = new NullLogger();
95 |
96 | if (!is_array($queues)) {
97 | $queues = array($queues);
98 | }
99 |
100 | $this->queues = $queues;
101 | $this->hostname = php_uname('n');
102 |
103 | $this->id = $this->hostname . ':' . getmypid() . ':' . implode(',', $this->queues);
104 | }
105 |
106 | /**
107 | * Set the process prefix of the workers to the given prefix string.
108 | * @param string $prefix The new process prefix
109 | */
110 | public static function setProcessPrefix($prefix)
111 | {
112 | self::$processPrefix = $prefix;
113 | }
114 |
115 | /**
116 | * Return all workers known to Resque as instantiated instances.
117 | * @return array
118 | */
119 | public static function all()
120 | {
121 | $workers = Resque::redis()->smembers('workers');
122 | if (!is_array($workers)) {
123 | $workers = array();
124 | }
125 |
126 | $instances = array();
127 | foreach ($workers as $workerId) {
128 | $instances[] = self::find($workerId);
129 | }
130 | return $instances;
131 | }
132 |
133 | /**
134 | * Given a worker ID, check if it is registered/valid.
135 | *
136 | * @param string $workerId ID of the worker.
137 | * @return boolean True if the worker exists, false if not.
138 | */
139 | public static function exists($workerId)
140 | {
141 | return (bool)Resque::redis()->sismember('workers', $workerId);
142 | }
143 |
144 | /**
145 | * Given a worker ID, find it and return an instantiated worker class for it.
146 | *
147 | * @param string $workerId The ID of the worker.
148 | * @return \Resque\Worker\ResqueWorker Instance of the worker. False if the worker does not exist.
149 | */
150 | public static function find($workerId)
151 | {
152 | if (!self::exists($workerId) || false === strpos($workerId, ":")) {
153 | return false;
154 | }
155 |
156 | list($hostname, $pid, $queues) = explode(':', $workerId, 3);
157 | $queues = explode(',', $queues);
158 | $worker = new self($queues);
159 | $worker->setId($workerId);
160 | return $worker;
161 | }
162 |
163 | /**
164 | * Set the ID of this worker to a given ID string.
165 | *
166 | * @param string $workerId ID for the worker.
167 | */
168 | public function setId($workerId)
169 | {
170 | $this->id = $workerId;
171 | }
172 |
173 | /**
174 | * The primary loop for a worker which when called on an instance starts
175 | * the worker's life cycle.
176 | *
177 | * Queues are checked every $interval (seconds) for new jobs.
178 | *
179 | * @param int $interval How often to check for new jobs across the queues.
180 | */
181 | public function work($interval = Resque::DEFAULT_INTERVAL, $blocking = false)
182 | {
183 | $this->updateProcLine('Starting');
184 | $this->startup();
185 |
186 | if (function_exists('pcntl_signal_dispatch')) {
187 | pcntl_signal_dispatch();
188 | }
189 |
190 | $ready_statuses = array(Status::STATUS_WAITING, Status::STATUS_RUNNING);
191 |
192 | while (true) {
193 | if ($this->shutdown) {
194 | break;
195 | }
196 |
197 | // is redis still alive?
198 | try {
199 | if (!$this->paused && Resque::redis()->ping() === false) {
200 | throw new CredisException('redis ping() failed');
201 | }
202 | } catch (CredisException $e) {
203 | $this->logger->log(LogLevel::ERROR, 'redis went away. trying to reconnect');
204 | Resque::$redis = null;
205 | usleep($interval * 1000000);
206 | continue;
207 | }
208 |
209 | // Attempt to find and reserve a job
210 | $job = false;
211 | if (!$this->paused) {
212 | if ($blocking === true) {
213 | $context = array('interval' => $interval);
214 | $message = 'Starting blocking with timeout of {interval}';
215 | $this->logger->log(LogLevel::INFO, $message, $context);
216 | $this->updateProcLine('Waiting with blocking timeout ' . $interval);
217 | } else {
218 | $this->updateProcLine('Waiting with interval ' . $interval);
219 | }
220 |
221 | $job = $this->reserve($blocking, $interval);
222 | }
223 |
224 | if (!$job) {
225 | // For an interval of 0, break now - helps with unit testing etc
226 | if ($interval == 0) {
227 | break;
228 | }
229 |
230 | if ($blocking === false) {
231 | // If no job was found, we sleep for $interval before continuing and checking again
232 | $context = array('interval' => $interval);
233 | $this->logger->log(LogLevel::INFO, 'Sleeping for {interval}', $context);
234 | if ($this->paused) {
235 | $this->updateProcLine('Paused');
236 | } else {
237 | $this->updateProcLine('Waiting');
238 | }
239 |
240 | usleep($interval * 1000000);
241 | }
242 |
243 | continue;
244 | }
245 |
246 | $context = array('job' => $job);
247 | $this->logger->log(LogLevel::NOTICE, 'Starting work on {job}', $context);
248 | Event::trigger('beforeFork', $job);
249 | $this->workingOn($job);
250 |
251 | $this->child = Resque::fork();
252 |
253 | // Forked and we're the child. Or PCNTL is not installed. Run the job.
254 | if ($this->child === 0 || $this->child === false || $this->child === -1) {
255 | $status = 'Processing ' . $job->queue . ' since ' . date('Y-m-d H:i:s');
256 | $this->updateProcLine($status);
257 | $this->logger->log(LogLevel::INFO, $status);
258 |
259 | if (!empty($job->payload['id'])) {
260 | PID::create($job->payload['id']);
261 | }
262 |
263 | $this->perform($job);
264 |
265 | if (!empty($job->payload['id'])) {
266 | PID::del($job->payload['id']);
267 | }
268 |
269 | if ($this->child === 0) {
270 | exit(0);
271 | }
272 | }
273 |
274 | if ($this->child > 0) {
275 | // Parent process, sit and wait
276 | $status = 'Forked ' . $this->child . ' at ' . date('Y-m-d H:i:s');
277 | $this->updateProcLine($status);
278 | $this->logger->log(LogLevel::INFO, $status);
279 |
280 | // Wait until the child process finishes before continuing
281 | while (pcntl_wait($status, WNOHANG) === 0) {
282 | if (function_exists('pcntl_signal_dispatch')) {
283 | pcntl_signal_dispatch();
284 | }
285 |
286 | // Pause for a half a second to conserve system resources
287 | usleep(500000);
288 | }
289 |
290 | if (pcntl_wifexited($status) !== true) {
291 | $job->fail(new DirtyExitException('Job exited abnormally'));
292 | } elseif (($exitStatus = pcntl_wexitstatus($status)) !== 0) {
293 | $job->fail(new DirtyExitException(
294 | 'Job exited with exit code ' . $exitStatus
295 | ));
296 | } else {
297 | if (in_array($job->getStatus(), $ready_statuses)) {
298 | $job->updateStatus(Status::STATUS_COMPLETE);
299 | $this->logger->log(LogLevel::INFO, 'done ' . $job);
300 | }
301 | }
302 | }
303 |
304 | $this->child = null;
305 | $this->doneWorking();
306 | }
307 |
308 | $this->unregisterWorker();
309 | }
310 |
311 | /**
312 | * Process a single job.
313 | *
314 | * @param \Resque\JobHandler $job The job to be processed.
315 | */
316 | public function perform(JobHandler $job)
317 | {
318 | $result = null;
319 | try {
320 | Event::trigger('afterFork', $job);
321 | $result = $job->perform();
322 | } catch (Exception $e) {
323 | $context = array('job' => $job, 'exception' => $e);
324 | $this->logger->log(LogLevel::CRITICAL, '{job} has failed {exception}', $context);
325 | $job->fail($e);
326 | return;
327 | } catch (Error $e) {
328 | $context = array('job' => $job, 'exception' => $e);
329 | $this->logger->log(LogLevel::CRITICAL, '{job} has failed {exception}', $context);
330 | $job->fail($e);
331 | return;
332 | }
333 |
334 | $job->updateStatus(Status::STATUS_COMPLETE, $result);
335 | $this->logger->log(LogLevel::NOTICE, '{job} has finished', array('job' => $job));
336 | }
337 |
338 | /**
339 | * @param bool $blocking
340 | * @param int $timeout
341 | * @return object|boolean Instance of Resque\JobHandler if a job is found, false if not.
342 | */
343 | public function reserve($blocking = false, $timeout = null)
344 | {
345 | if ($this->hasParent && !posix_kill(posix_getppid(), 0)) {
346 | $this->shutdown();
347 | return false;
348 | }
349 |
350 | $queues = $this->queues();
351 | if (!is_array($queues)) {
352 | return;
353 | }
354 |
355 | if ($blocking === true) {
356 | if (empty($queues)) {
357 | $context = array('interval' => $timeout);
358 | $this->logger->log(LogLevel::INFO, 'No queue was found, sleeping for {interval}', $context);
359 | usleep($timeout * 1000000);
360 | return false;
361 | }
362 | $job = JobHandler::reserveBlocking($queues, $timeout);
363 | if ($job) {
364 | $context = array('queue' => $job->queue);
365 | $this->logger->log(LogLevel::INFO, 'Found job on {queue}', $context);
366 | return $job;
367 | }
368 | } else {
369 | foreach ($queues as $queue) {
370 | $context = array('queue' => $queue);
371 | $this->logger->log(LogLevel::INFO, 'Checking {queue} for jobs', $context);
372 | $job = JobHandler::reserve($queue);
373 | if ($job) {
374 | $context = array('queue' => $job->queue);
375 | $this->logger->log(LogLevel::INFO, 'Found job on {queue}', $context);
376 | return $job;
377 | }
378 | }
379 | }
380 |
381 | return false;
382 | }
383 |
384 | /**
385 | * Return an array containing all of the queues that this worker should use
386 | * when searching for jobs.
387 | *
388 | * If * is found in the list of queues, every queue will be searched in
389 | * alphabetic order. (@see $fetch)
390 | *
391 | * @param boolean $fetch If true, and the queue is set to *, will fetch
392 | * all queue names from redis.
393 | * @return array Array of associated queues.
394 | */
395 | public function queues($fetch = true)
396 | {
397 | if (!in_array('*', $this->queues) || $fetch == false) {
398 | return $this->queues;
399 | }
400 |
401 | $queues = Resque::queues();
402 | sort($queues);
403 | return $queues;
404 | }
405 |
406 | /**
407 | * Perform necessary actions to start a worker.
408 | */
409 | private function startup()
410 | {
411 | $this->registerSigHandlers();
412 | $this->pruneDeadWorkers();
413 | Event::trigger('beforeFirstFork', $this);
414 | $this->registerWorker();
415 | }
416 |
417 | /**
418 | * On supported systems (with the PECL proctitle module installed), update
419 | * the name of the currently running process to indicate the current state
420 | * of a worker.
421 | *
422 | * @param string $status The updated process title.
423 | */
424 | private function updateProcLine($status)
425 | {
426 | $processTitle = static::$processPrefix . '-' . Resque::VERSION;
427 | $processTitle .= ' (' . implode(',', $this->queues) . '): ' . $status;
428 | if (function_exists('cli_set_process_title') && PHP_OS !== 'Darwin') {
429 | cli_set_process_title($processTitle);
430 | } elseif (function_exists('setproctitle')) {
431 | setproctitle($processTitle);
432 | }
433 | }
434 |
435 | /**
436 | * Register signal handlers that a worker should respond to.
437 | *
438 | * TERM: Shutdown immediately and stop processing jobs.
439 | * INT: Shutdown immediately and stop processing jobs.
440 | * QUIT: Shutdown after the current job finishes processing.
441 | * USR1: Kill the forked child immediately and continue processing jobs.
442 | */
443 | private function registerSigHandlers()
444 | {
445 | if (!function_exists('pcntl_signal')) {
446 | return;
447 | }
448 |
449 | pcntl_signal(SIGTERM, array($this, 'shutDownNow'));
450 | pcntl_signal(SIGINT, array($this, 'shutDownNow'));
451 | pcntl_signal(SIGQUIT, array($this, 'shutdown'));
452 | pcntl_signal(SIGUSR1, array($this, 'killChild'));
453 | pcntl_signal(SIGUSR2, array($this, 'pauseProcessing'));
454 | pcntl_signal(SIGCONT, array($this, 'unPauseProcessing'));
455 | $this->logger->log(LogLevel::DEBUG, 'Registered signals');
456 | }
457 |
458 | /**
459 | * Signal handler callback for USR2, pauses processing of new jobs.
460 | */
461 | public function pauseProcessing()
462 | {
463 | $this->logger->log(LogLevel::NOTICE, 'USR2 received; pausing job processing');
464 | $this->paused = true;
465 | }
466 |
467 | /**
468 | * Signal handler callback for CONT, resumes worker allowing it to pick
469 | * up new jobs.
470 | */
471 | public function unPauseProcessing()
472 | {
473 | $this->logger->log(LogLevel::NOTICE, 'CONT received; resuming job processing');
474 | $this->paused = false;
475 | }
476 |
477 | /**
478 | * Schedule a worker for shutdown. Will finish processing the current job
479 | * and when the timeout interval is reached, the worker will shut down.
480 | */
481 | public function shutdown()
482 | {
483 | $this->shutdown = true;
484 | $this->logger->log(LogLevel::NOTICE, 'Shutting down');
485 | }
486 |
487 | /**
488 | * Force an immediate shutdown of the worker, killing any child jobs
489 | * currently running.
490 | */
491 | public function shutdownNow()
492 | {
493 | $this->shutdown();
494 | $this->killChild();
495 | }
496 |
497 | /**
498 | * @return int Child process PID.
499 | */
500 | public function getChildPID()
501 | {
502 | return $this->child;
503 | }
504 |
505 | /**
506 | * Kill a forked child job immediately. The job it is processing will not
507 | * be completed.
508 | */
509 | public function killChild()
510 | {
511 | if (!$this->child) {
512 | $this->logger->log(LogLevel::DEBUG, 'No child to kill.');
513 | return;
514 | }
515 |
516 | $context = array('child' => $this->child);
517 | $this->logger->log(LogLevel::INFO, 'Killing child at {child}', $context);
518 | if (exec('ps -o pid,s -p ' . $this->child, $output, $returnCode) && $returnCode != 1) {
519 | $context = array('child' => $this->child);
520 | $this->logger->log(LogLevel::DEBUG, 'Child {child} found, killing.', $context);
521 | posix_kill($this->child, SIGKILL);
522 | $this->child = null;
523 | } else {
524 | $context = array('child' => $this->child);
525 | $this->logger->log(LogLevel::INFO, 'Child {child} not found, restarting.', $context);
526 | $this->shutdown();
527 | }
528 | }
529 |
530 | /**
531 | * Look for any workers which should be running on this server and if
532 | * they're not, remove them from Redis.
533 | *
534 | * This is a form of garbage collection to handle cases where the
535 | * server may have been killed and the Resque workers did not die gracefully
536 | * and therefore leave state information in Redis.
537 | */
538 | public function pruneDeadWorkers()
539 | {
540 | $workerPids = $this->workerPids();
541 | $workers = self::all();
542 | foreach ($workers as $worker) {
543 | if (is_object($worker)) {
544 | list($host, $pid, $queues) = explode(':', (string)$worker, 3);
545 | if ($host != $this->hostname || in_array($pid, $workerPids) || $pid == getmypid()) {
546 | continue;
547 | }
548 | $context = array('worker' => (string)$worker);
549 | $this->logger->log(LogLevel::INFO, 'Pruning dead worker: {worker}', $context);
550 | $worker->unregisterWorker();
551 | }
552 | }
553 | }
554 |
555 | /**
556 | * Return an array of process IDs for all of the Resque workers currently
557 | * running on this machine.
558 | *
559 | * @return array Array of Resque worker process IDs.
560 | */
561 | public function workerPids()
562 | {
563 | $pids = array();
564 | if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
565 | exec('WMIC path win32_process get Processid,Commandline | findstr resque | findstr /V findstr', $cmdOutput);
566 | foreach ($cmdOutput as $line) {
567 | $line = preg_replace('/\s+/m', ' ', $line);
568 | list(,,$pids[]) = explode(' ', trim($line), 3);
569 | }
570 | } else {
571 | exec('ps -A -o pid,args | grep [r]esque', $cmdOutput);
572 | foreach ($cmdOutput as $line) {
573 | list($pids[],) = explode(' ', trim($line), 2);
574 | }
575 | }
576 | return $pids;
577 | }
578 |
579 | /**
580 | * Register this worker in Redis.
581 | */
582 | public function registerWorker()
583 | {
584 | Resque::redis()->sadd('workers', (string)$this);
585 | Resque::redis()->set('worker:' . (string)$this . ':started', date('c'));
586 | }
587 |
588 | /**
589 | * Unregister this worker in Redis. (shutdown etc)
590 | */
591 | public function unregisterWorker()
592 | {
593 | if (is_object($this->currentJob)) {
594 | $this->currentJob->fail(new DirtyExitException());
595 | }
596 |
597 | $id = (string)$this;
598 | Resque::redis()->srem('workers', $id);
599 | Resque::redis()->del('worker:' . $id);
600 | Resque::redis()->del('worker:' . $id . ':started');
601 | Stat::clear('processed:' . $id);
602 | Stat::clear('failed:' . $id);
603 | }
604 |
605 | /**
606 | * Tell Redis which job we're currently working on.
607 | *
608 | * @param object $job \Resque\JobHandler instance containing the job we're working on.
609 | */
610 | public function workingOn(JobHandler $job)
611 | {
612 | $job->worker = $this;
613 | $this->currentJob = $job;
614 | $job->updateStatus(Status::STATUS_RUNNING);
615 | $data = json_encode(array(
616 | 'queue' => $job->queue,
617 | 'run_at' => date('c'),
618 | 'payload' => $job->payload
619 | ));
620 | Resque::redis()->set('worker:' . $job->worker, $data);
621 | }
622 |
623 | /**
624 | * Notify Redis that we've finished working on a job, clearing the working
625 | * state and incrementing the job stats.
626 | */
627 | public function doneWorking()
628 | {
629 | $this->currentJob = null;
630 | Stat::incr('processed');
631 | Stat::incr('processed:' . (string)$this);
632 | Resque::redis()->del('worker:' . (string)$this);
633 | }
634 |
635 | /**
636 | * Generate a string representation of this worker.
637 | *
638 | * @return string String identifier for this worker instance.
639 | */
640 | public function __toString()
641 | {
642 | return $this->id;
643 | }
644 |
645 | /**
646 | * Return an object describing the job this worker is currently working on.
647 | *
648 | * @return object Object with details of current job.
649 | */
650 | public function job()
651 | {
652 | $job = Resque::redis()->get('worker:' . $this);
653 | if (!$job) {
654 | return array();
655 | } else {
656 | return json_decode($job, true);
657 | }
658 | }
659 |
660 | /**
661 | * Get a statistic belonging to this worker.
662 | *
663 | * @param string $stat Statistic to fetch.
664 | * @return int Statistic value.
665 | */
666 | public function getStat($stat)
667 | {
668 | return Stat::get($stat . ':' . $this);
669 | }
670 |
671 | /**
672 | * Inject the logging object into the worker
673 | *
674 | * @param \Psr\Log\LoggerInterface $logger
675 | */
676 | public function setLogger(LoggerInterface $logger)
677 | {
678 | $this->logger = $logger;
679 | }
680 | }
681 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # PHP Resque Worker (and Enqueue)
2 |
3 | PHP Resque is a Redis-backed library for creating background jobs, placing those
4 | jobs on one or more queues, and processing them later.
5 |
6 | 
7 |
8 | [](https://github.com/resque/php-resque)
9 | [](https://packagist.org/packages/resque/php-resque)
10 | [](https://packagist.org/packages/resque/php-resque)
11 | [](https://packagist.org/packages/resque/php-resque)
12 | [](https://packagist.org/packages/resque/php-resque)
13 |
14 | []()
15 | [](https://libraries.io/github/resque/php-resque)
16 |
17 | [](https://github.com/resque/php-resque)
18 | [](https://github.com/resque/php-resque)
19 | [](https://github.com/resque/php-resque)
20 | [](https://github.com/resque/php-resque)
21 |
22 | [](https://github.com/resque/php-resque)
23 | [](https://join.slack.com/t/php-resque/shared_invite/enQtNTIwODk0OTc1Njg3LWYyODczMTZjMzI2N2JkYWUzM2FlNDk5ZjY2ZGM4Njc4YjFiMzU2ZWFjOGQxMDIyNmE5MTBlNWEzODBiMmVmOTI)
24 |
25 | ## Background
26 |
27 | Resque was pioneered by GitHub, and written in Ruby. What you're seeing here
28 | started life as an almost direct port of the Resque worker and enqueue system to
29 | PHP.
30 |
31 | For more information on Resque, visit the official GitHub project:
32 |
33 |
34 | For further information, see the launch post on the GitHub blog:
35 |
36 |
37 | > The PHP port does NOT include its own web interface for viewing queue stats,
38 | > as the data is stored in the exact same expected format as the Ruby version of
39 | > Resque.
40 |
41 | The PHP port provides much the same features as the Ruby version:
42 |
43 | - Workers can be distributed between multiple machines
44 | - Includes support for priorities (queues)
45 | - Resilient to memory leaks (forking)
46 | - Expects failure
47 |
48 | It also supports the following additional features:
49 |
50 | - Has the ability to track the status of jobs
51 | - Will mark a job as failed, if a forked child running a job does not exit
52 | with a status code as `0`
53 | - Has built in support for `setUp` and `tearDown` methods, called pre and post
54 | jobs
55 |
56 | Additionally it includes php-resque-scheduler, a PHP port of [resque-scheduler](http://github.com/resque/resque),
57 | which adds support for scheduling items in the future to Resque. It has been
58 | designed to be an almost direct-copy of the Ruby plugin
59 |
60 | At the moment, php-resque-scheduler only supports delayed jobs, which is the
61 | ability to push a job to the queue and have it run at a certain timestamp, or
62 | in a number of seconds. Support for recurring jobs (similar to CRON) is planned
63 | for a future release.
64 |
65 | This port was originally made by [Chris
66 | Boulton](https://github.com/chrisboulton), with maintenance by the community.
67 | See for more on that history.
68 |
69 | ## Requirements
70 |
71 | - PHP 7.3+
72 | - Redis 2.2+
73 | - Optional but Recommended: Composer
74 |
75 | ## Getting Started
76 |
77 | The easiest way to work with php-resque is when it's installed as a Composer
78 | package inside your project. Composer isn't strictly required, but makes life a
79 | lot easier.
80 |
81 | If you're not familiar with Composer, please see .
82 |
83 | 1. Run `composer require resque/php-resque`.
84 |
85 | 2. If you haven't already, add the Composer autoload to your project's
86 | initialization file. (example)
87 |
88 | ```php
89 | require 'vendor/autoload.php';
90 | ```
91 |
92 | ## Jobs
93 |
94 | ### Queueing Jobs
95 |
96 | Jobs are queued as follows:
97 |
98 | ```php
99 | // Required if redis is located elsewhere
100 | Resque\Resque::setBackend('localhost:6379');
101 |
102 | $args = array(
103 | 'name' => 'Chris'
104 | );
105 | Resque\Resque::enqueue('default', 'My_Job', $args);
106 | ```
107 |
108 | ### Defining Jobs
109 |
110 | Each job should be in its own class, and include a `perform` method.
111 |
112 | ```php
113 | class My_Job extends \Resque\Job\Job
114 | {
115 | public function perform()
116 | {
117 | // Work work work
118 | echo $this->args['name'];
119 | }
120 | }
121 | ```
122 |
123 | When the job is run, the class will be instantiated and any arguments will be
124 | set as an array on the instantiated object, and are accessible via
125 | `$this->args`.
126 |
127 | Any exception thrown by a job will result in the job failing - be careful here
128 | and make sure you handle the exceptions that shouldn't result in a job failing.
129 |
130 | Jobs can also have `setUp` and `tearDown` methods. If a `setUp` method is
131 | defined, it will be called before the `perform` method is run. The `tearDown`
132 | method, if defined, will be called after the job finishes.
133 |
134 | ```php
135 | class My_Job extends \Resque\Job\Job
136 | {
137 | public function setUp(): void
138 | {
139 | // ... Set up environment for this job
140 | }
141 |
142 | public function perform()
143 | {
144 | // .. Run job
145 | }
146 |
147 | public function tearDown(): void
148 | {
149 | // ... Remove environment for this job
150 | }
151 | }
152 | ```
153 |
154 | ### Dequeueing Jobs
155 |
156 | This method can be used to conveniently remove a job from a queue.
157 |
158 | ```php
159 | // Removes job class 'My_Job' of queue 'default'
160 | Resque\Resque::dequeue('default', ['My_Job']);
161 |
162 | // Removes job class 'My_Job' with Job ID '087df5819a790ac666c9608e2234b21e' of queue 'default'
163 | Resque\Resque::dequeue('default', ['My_Job' => '087df5819a790ac666c9608e2234b21e']);
164 |
165 | // Removes job class 'My_Job' with arguments of queue 'default'
166 | Resque\Resque::dequeue('default', ['My_Job' => array('foo' => 1, 'bar' => 2)]);
167 |
168 | // Removes multiple jobs
169 | Resque\Resque::dequeue('default', ['My_Job', 'My_Job2']);
170 | ```
171 |
172 | If no jobs are given, this method will dequeue all jobs matching the provided
173 | queue.
174 |
175 | ```php
176 | // Removes all jobs of queue 'default'
177 | Resque\Resque::dequeue('default');
178 | ```
179 |
180 | ### Tracking Job Statuses
181 |
182 | php-resque has the ability to perform basic status tracking of a queued job. The
183 | status information will allow you to check if a job is in the queue, is
184 | currently being run, has finished, or has failed.
185 |
186 | To track the status of a job, pass `true` as the fourth argument to
187 | `Resque\Resque::enqueue`. A token used for tracking the job status will be returned:
188 |
189 | ```php
190 | $token = Resque\Resque::enqueue('default', 'My_Job', $args, true);
191 | echo $token;
192 | ```
193 |
194 | To fetch the status of a job:
195 |
196 | ```php
197 | $status = new Resque\Job\Status($token);
198 | echo $status->get(); // Outputs the status
199 | ```
200 |
201 | Job statuses are defined as constants in the `Resque\Job\Status` class. Valid
202 | statuses include:
203 |
204 | - `Resque\Job\Status::STATUS_WAITING` - Job is still queued
205 | - `Resque\Job\Status::STATUS_RUNNING` - Job is currently running
206 | - `Resque\Job\Status::STATUS_FAILED` - Job has failed
207 | - `Resque\Job\Status::STATUS_COMPLETE` - Job is complete
208 | - `false` - Failed to fetch the status; is the token valid?
209 |
210 | Statuses are available for up to 24 hours after a job has completed or failed,
211 | and are then automatically expired. A status can also forcefully be expired by
212 | calling the `stop()` method on a status class.
213 |
214 | ### Obtaining job PID ###
215 |
216 | You can obtain the PID of the actual process doing the work through `Resque\Job\PID`. On a forking OS this will be the
217 | PID of the forked process.
218 |
219 | CAUTION: on a non-forking OS, the PID returned will be of the worker itself.
220 |
221 | ```php
222 | echo Resque\Job\PID::get($token);
223 | ```
224 |
225 | Function returns `0` if the `perform` hasn't started yet, or if it has already ended.
226 |
227 | ## Delayed Jobs
228 |
229 | To quote the documentation for the Ruby resque-scheduler:
230 |
231 | > Delayed jobs are one-off jobs that you want to be put into a queue at some
232 | point in the future. The classic example is sending an email:
233 |
234 | require 'Resque.php';
235 | require 'Scheduler.php';
236 |
237 | $in = 3600;
238 | $args = array('id' => $user->id);
239 | Resque\Scheduler::enqueueIn($in, 'email', 'SendFollowUpEmail', $args);
240 |
241 | The above will store the job for 1 hour in the delayed queue, and then pull the
242 | job off and submit it to the `email` queue in Resque for processing as soon as
243 | a worker is available.
244 |
245 | Instead of passing a relative time in seconds, you can also supply a timestamp
246 | as either a DateTime object or integer containing a UNIX timestamp to the
247 | `enqueueAt` method:
248 |
249 | require 'Resque.php';
250 | require 'Scheduler.php';
251 |
252 | $time = 1332067214;
253 | Resque\Scheduler::enqueueAt($time, 'email', 'SendFollowUpEmail', $args);
254 |
255 | $datetime = new DateTime('2012-03-18 13:21:49');
256 | Resque\Scheduler::enqueueAt($datetime, 'email', 'SendFollowUpEmail', $args);
257 |
258 | NOTE: resque-scheduler does not guarantee a job will fire at the time supplied.
259 | At the time supplied, resque-scheduler will take the job out of the delayed
260 | queue and push it to the appropriate queue in Resque. Your next available Resque
261 | worker will pick the job up. To keep processing as quick as possible, keep your
262 | queues as empty as possible.
263 |
264 | ## Workers
265 |
266 | Workers work in the exact same way as the Ruby workers. For complete
267 | documentation on workers, see the original documentation.
268 |
269 | A basic "up-and-running" `bin/resque` file is included that sets up a running
270 | worker environment. (`vendor/bin/resque` when installed via Composer)
271 |
272 | The exception to the similarities with the Ruby version of resque is how a
273 | worker is initially setup. To work under all environments, not having a single
274 | environment such as with Ruby, the PHP port makes _no_ assumptions about your
275 | setup.
276 |
277 | To start a worker, it's very similar to the Ruby version:
278 |
279 | ```sh
280 | $ QUEUE=file_serve php bin/resque
281 | ```
282 |
283 | It's your responsibility to tell the worker which file to include to get your
284 | application underway. You do so by setting the `APP_INCLUDE` environment
285 | variable:
286 |
287 | ```sh
288 | $ QUEUE=file_serve APP_INCLUDE=../application/init.php php bin/resque
289 | ```
290 |
291 | _Pro tip: Using Composer? More than likely, you don't need to worry about
292 | `APP_INCLUDE`, because hopefully Composer is responsible for autoloading your
293 | application too!_
294 |
295 | Getting your application underway also includes telling the worker your job
296 | classes, by means of either an autoloader or including them.
297 |
298 | Alternately, you can always `include('bin/resque')` from your application and
299 | skip setting `APP_INCLUDE` altogether. Just be sure the various environment
300 | variables are set (`setenv`) before you do.
301 |
302 | ### Logging
303 |
304 | The port supports the same environment variables for logging to STDOUT. Setting
305 | `VERBOSE` will print basic debugging information and `VVERBOSE` will print
306 | detailed information.
307 |
308 | ```sh
309 | $ VERBOSE=1 QUEUE=file_serve bin/resque
310 | $ VVERBOSE=1 QUEUE=file_serve bin/resque
311 | ```
312 |
313 | ### Priorities and Queue Lists
314 |
315 | Similarly, priority and queue list functionality works exactly the same as the
316 | Ruby workers. Multiple queues should be separated with a comma, and the order
317 | that they're supplied in is the order that they're checked in.
318 |
319 | As per the original example:
320 |
321 | ```sh
322 | $ QUEUE=file_serve,warm_cache bin/resque
323 | ```
324 |
325 | The `file_serve` queue will always be checked for new jobs on each iteration
326 | before the `warm_cache` queue is checked.
327 |
328 | ### Running All Queues
329 |
330 | All queues are supported in the same manner and processed in alphabetical order:
331 |
332 | ```sh
333 | $ QUEUE='*' bin/resque
334 | ```
335 |
336 | ### Running Multiple Workers
337 |
338 | Multiple workers can be launched simultaneously by supplying the `COUNT`
339 | environment variable:
340 |
341 | ```sh
342 | $ COUNT=5 bin/resque
343 | ```
344 |
345 | Be aware, however, that each worker is its own fork, and the original process
346 | will shut down as soon as it has spawned `COUNT` forks. If you need to keep
347 | track of your workers using an external application such as `monit`, you'll need
348 | to work around this limitation.
349 |
350 | ### Custom prefix
351 |
352 | When you have multiple apps using the same Redis database it is better to use a
353 | custom prefix to separate the Resque data:
354 |
355 | ```sh
356 | $ PREFIX=my-app-name bin/resque
357 | ```
358 |
359 | ### Setting Redis backend ###
360 |
361 | When you have the Redis database on a different host than the one the workers
362 | are running, you must set the `REDIS_BACKEND` environment variable:
363 |
364 | ```sh
365 | $ REDIS_BACKEND=my-redis-ip:my-redis-port bin/resque
366 | ```
367 |
368 | ### Forking
369 |
370 | Similarly to the Ruby versions, supported platforms will immediately fork after
371 | picking up a job. The forked child will exit as soon as the job finishes.
372 |
373 | The difference with php-resque is that if a forked child does not exit nicely
374 | (PHP error or such), php-resque will automatically fail the job.
375 |
376 | ### Signals
377 |
378 | Signals also work on supported platforms exactly as in the Ruby version of
379 | Resque:
380 |
381 | - `QUIT` - Wait for job to finish processing then exit
382 | - `TERM` / `INT` - Immediately kill job then exit
383 | - `USR1` - Immediately kill job but don't exit
384 | - `USR2` - Pause worker, no new jobs will be processed
385 | - `CONT` - Resume worker.
386 |
387 | ### Process Titles/Statuses
388 |
389 | The Ruby version of Resque has a nifty feature whereby the process title of the
390 | worker is updated to indicate what the worker is doing, and any forked children
391 | also set their process title with the job being run. This helps identify running
392 | processes on the server and their resque status.
393 |
394 | **PHP does not have this functionality by default until 5.5.**
395 |
396 | A PECL module () exists that adds this
397 | functionality to PHP before 5.5, so if you'd like process titles updated,
398 | install the PECL module as well. php-resque will automatically detect and use
399 | it.
400 |
401 | ### Resque Scheduler
402 |
403 | resque-scheduler requires a special worker that runs in the background. This
404 | worker is responsible for pulling items off the schedule/delayed queue and adding
405 | them to the queue for resque. This means that for delayed or scheduled jobs to be
406 | executed, that worker needs to be running.
407 |
408 | A basic "up-and-running" `bin/resque-scheduler` file that sets up a
409 | running worker environment is included (`vendor/bin/resque-scheduler` when
410 | installed via composer). It accepts many of the same environment variables as
411 | the main workers for php-resque:
412 |
413 | * `REDIS_BACKEND` - Redis server to connect to
414 | * `LOGGING` - Enable logging to STDOUT
415 | * `VERBOSE` - Enable verbose logging
416 | * `VVERBOSE` - Enable very verbose logging
417 | * `INTERVAL` - Sleep for this long before checking scheduled/delayed queues
418 | * `APP_INCLUDE` - Include this file when starting (to launch your app)
419 | * `PIDFILE` - Write the PID of the worker out to this file
420 |
421 | It's easy to start the resque-scheduler worker using `bin/resque-scheduler`:
422 | $ php bin/resque-scheduler
423 |
424 | ## Event/Hook System
425 |
426 | php-resque has a basic event system that can be used by your application to
427 | customize how some of the php-resque internals behave.
428 |
429 | You listen in on events (as listed below) by registering with `Resque\Event` and
430 | supplying a callback that you would like triggered when the event is raised:
431 |
432 | ```php
433 | Resque\Event::listen('eventName', [callback]);
434 | ```
435 |
436 | `[callback]` may be anything in PHP that is callable by `call_user_func_array`:
437 |
438 | - A string with the name of a function
439 | - An array containing an object and method to call
440 | - An array containing an object and a static method to call
441 | - A closure (PHP 5.3+)
442 |
443 | Events may pass arguments (documented below), so your callback should accept
444 | these arguments.
445 |
446 | You can stop listening to an event by calling `Resque\Event::stopListening` with
447 | the same arguments supplied to `Resque\Event::listen`.
448 |
449 | It is up to your application to register event listeners. When enqueuing events
450 | in your application, it should be as easy as making sure php-resque is loaded
451 | and calling `Resque\Event::listen`.
452 |
453 | When running workers, if you run workers via the default `bin/resque` script,
454 | your `APP_INCLUDE` script should initialize and register any listeners required
455 | for operation. If you have rolled your own worker manager, then it is again your
456 | responsibility to register listeners.
457 |
458 | A sample plugin is included in the `extras` directory.
459 |
460 | ### Events
461 |
462 | #### beforeFirstFork
463 |
464 | Called once, as a worker initializes. Argument passed is the instance of
465 | `Resque\Worker\ResqueWorker` that was just initialized.
466 |
467 | #### beforeFork
468 |
469 | Called before php-resque forks to run a job. Argument passed contains the
470 | instance of `Resque\JobHandler` for the job about to be run.
471 |
472 | `beforeFork` is triggered in the **parent** process. Any changes made will be
473 | permanent for as long as the **worker** lives.
474 |
475 | #### afterFork
476 |
477 | Called after php-resque forks to run a job (but before the job is run). Argument
478 | passed contains the instance of `Resque\JobHandler` for the job about to be run.
479 |
480 | `afterFork` is triggered in the **child** process after forking out to complete
481 | a job. Any changes made will only live as long as the **job** is being
482 | processed.
483 |
484 | #### beforePerform
485 |
486 | Called before the `setUp` and `perform` methods on a job are run. Argument
487 | passed contains the instance of `Resque\JobHandler` for the job about to be run.
488 |
489 | You can prevent execution of the job by throwing an exception of
490 | `Resque\Exceptions\DoNotPerformException`. Any other exceptions thrown will be treated as if they
491 | were thrown in a job, causing the job to fail.
492 |
493 | #### afterPerform
494 |
495 | Called after the `perform` and `tearDown` methods on a job are run. Argument
496 | passed contains the instance of `Resque\JobHandler` that was just run.
497 |
498 | Any exceptions thrown will be treated as if they were thrown in a job, causing
499 | the job to be marked as having failed.
500 |
501 | #### onFailure
502 |
503 | Called whenever a job fails. Arguments passed (in this order) include:
504 |
505 | - Exception - The exception that was thrown when the job failed
506 | - Resque\JobHandler - The job that failed
507 |
508 | #### beforeEnqueue
509 |
510 | Called immediately before a job is enqueued using the `Resque\Resque::enqueue` method.
511 | Arguments passed (in this order) include:
512 |
513 | - Class - string containing the name of the job to be enqueued
514 | - Arguments - array of arguments for the job
515 | - Queue - string containing the name of the queue the job is to be enqueued in
516 | - ID - string containing the token of the job to be enqueued
517 |
518 | You can prevent enqueing of the job by throwing an exception of
519 | `Resque\Exceptions\DoNotCreateException`.
520 |
521 | #### afterEnqueue
522 |
523 | Called after a job has been queued using the `Resque\Resque::enqueue` method. Arguments
524 | passed (in this order) include:
525 |
526 | - Class - string containing the name of scheduled job
527 | - Arguments - array of arguments supplied to the job
528 | - Queue - string containing the name of the queue the job was added to
529 | - ID - string containing the new token of the enqueued job
530 |
531 | ### afterSchedule
532 |
533 | Called after a job has been added to the schedule. Arguments passed are the
534 | timestamp, queue of the job, the class name of the job, and the job's arguments.
535 |
536 | ### beforeDelayedEnqueue
537 |
538 | Called immediately after a job has been pulled off the delayed queue and right
539 | before the job is added to the queue in resque. Arguments passed are the queue
540 | of the job, the class name of the job, and the job's arguments.
541 |
542 | ## Step-By-Step
543 |
544 | For a more in-depth look at what php-resque does under the hood (without needing
545 | to directly examine the code), have a look at `HOWITWORKS.md`.
546 |
547 | ## Contributors
548 |
549 | ### Project Creator
550 |
551 | - @chrisboulton
552 |
553 | ### Project Maintainers
554 |
555 | - @danhunsaker
556 | - @rajibahmed
557 | - @steveklabnik
558 |
559 | ### Others
560 |
561 | - @acinader
562 | - @ajbonner
563 | - @andrewjshults
564 | - @atorres757
565 | - @benjisg
566 | - @biinari
567 | - @cballou
568 | - @chaitanyakuber
569 | - @charly22
570 | - @CyrilMazur
571 | - @d11wtq
572 | - @dceballos
573 | - @ebernhardson
574 | - @hlegius
575 | - @hobodave
576 | - @humancopy
577 | - @iskandar
578 | - @JesseObrien
579 | - @jjfrey
580 | - @jmathai
581 | - @joshhawthorne
582 | - @KevBurnsJr
583 | - @lboynton
584 | - @maetl
585 | - @matteosister
586 | - @MattHeath
587 | - @mickhrmweb
588 | - @Olden
589 | - @patrickbajao
590 | - @pedroarnal
591 | - @ptrofimov
592 | - @rayward
593 | - @richardkmiller
594 | - @Rockstar04
595 | - @ruudk
596 | - @salimane
597 | - @scragg0x
598 | - @scraton
599 | - @thedotedge
600 | - @tonypiper
601 | - @trimbletodd
602 | - @warezthebeef
603 |
--------------------------------------------------------------------------------