├── .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 | ![PHP-Resque Logo](https://github.com/resque/php-resque/raw/develop/extras/php-resque.png) 7 | 8 | [![License (MIT)](https://img.shields.io/packagist/l/resque/php-resque.svg?style=flat-square)](https://github.com/resque/php-resque) 9 | [![PHP Version](https://img.shields.io/packagist/php-v/resque/php-resque.svg?style=flat-square&logo=php&logoColor=white)](https://packagist.org/packages/resque/php-resque) 10 | [![Latest Version](https://img.shields.io/packagist/v/resque/php-resque.svg?style=flat-square)](https://packagist.org/packages/resque/php-resque) 11 | [![Latest Unstable Version](https://img.shields.io/packagist/vpre/resque/php-resque.svg?style=flat-square)](https://packagist.org/packages/resque/php-resque) 12 | [![Downloads](https://img.shields.io/packagist/dt/resque/php-resque.svg?style=flat-square)](https://packagist.org/packages/resque/php-resque) 13 | 14 | [![Build Status](https://img.shields.io/github/checks-status/resque/php-resque/develop)]() 15 | [![Dependency Status](https://img.shields.io/librariesio/github/resque/php-resque.svg?style=flat-square)](https://libraries.io/github/resque/php-resque) 16 | 17 | [![Latest Release](https://img.shields.io/github/release/resque/php-resque.svg?style=flat-square&logo=github&logoColor=white)](https://github.com/resque/php-resque) 18 | [![Latest Release Date](https://img.shields.io/github/release-date/resque/php-resque.svg?style=flat-square&logo=github&logoColor=white)](https://github.com/resque/php-resque) 19 | [![Commits Since Latest Release](https://img.shields.io/github/commits-since/resque/php-resque/latest.svg?style=flat-square&logo=github&logoColor=white)](https://github.com/resque/php-resque) 20 | [![Maintenance Status](https://img.shields.io/maintenance/yes/2025.svg?style=flat-square&logo=github&logoColor=white)](https://github.com/resque/php-resque) 21 | 22 | [![Contributors](https://img.shields.io/github/contributors/resque/php-resque.svg?style=flat-square&logo=github&logoColor=white)](https://github.com/resque/php-resque) 23 | [![Chat on Slack](https://img.shields.io/badge/chat-Slack-blue.svg?style=flat-square&logo=slack&logoColor=white)](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 | --------------------------------------------------------------------------------