├── .gitignore ├── src ├── ResqueException.php ├── Version.php ├── Exception │ ├── JobLogicException.php │ ├── JobClassNotFoundException.php │ ├── JobIdException.php │ ├── JobInvalidException.php │ └── DirtyExitException.php ├── Client │ ├── PredisClient.php │ ├── CredisClient.php │ └── ClientInterface.php ├── Console │ ├── Helper │ │ ├── RedisHelper.php │ │ └── LoggerHelper.php │ ├── Command.php │ ├── QueueListCommand.php │ ├── QueueClearCommand.php │ ├── WorkerCommand.php │ ├── ConsoleRunner.php │ └── EnqueueCommand.php ├── JobInterface.php ├── Failure │ ├── BackendInterface.php │ └── RedisBackend.php ├── Job │ ├── StatusFactory.php │ └── Status.php ├── Statistic.php ├── AbstractJob.php ├── Resque.php └── Worker.php ├── test ├── Test │ ├── NoPerformJob.php │ ├── MinimalJob.php │ ├── FailingJob.php │ ├── Job.php │ └── Settings.php ├── ResqueTest.php ├── bootstrap.php ├── ClientTest.php ├── Test.php ├── StatisticTest.php ├── AbstractJobTest.php ├── Job │ └── StatusTest.php └── WorkerTest.php ├── phpunit.xml ├── .scrutinizer.yml ├── resque ├── LICENSE ├── cli-config.php ├── .travis.yml ├── composer.json ├── CHANGELOG.md ├── README.md └── composer.lock /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | vendor/ 3 | composer.phar 4 | -------------------------------------------------------------------------------- /src/ResqueException.php: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | test 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/Exception/JobIdException.php: -------------------------------------------------------------------------------- 1 | performed = true; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /test/Test/FailingJob.php: -------------------------------------------------------------------------------- 1 | 12 | * @license http://www.opensource.org/licenses/mit-license.php 13 | */ 14 | class DirtyExitException extends ResqueException 15 | { 16 | } 17 | -------------------------------------------------------------------------------- /test/ResqueTest.php: -------------------------------------------------------------------------------- 1 | assertFalse($this->resque->workerExists('blah')); 10 | } 11 | 12 | public function testEmptyWorkerIds() 13 | { 14 | $this->assertInternalType('array', $this->resque->getWorkerIds()); 15 | } 16 | 17 | public function testEmptyWorkerPids() 18 | { 19 | $this->assertInternalType('array', $this->resque->getWorkerPids()); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /test/bootstrap.php: -------------------------------------------------------------------------------- 1 | addPsr4('Resque\\', __DIR__); 5 | 6 | $build = __DIR__ . '/../build'; 7 | 8 | $logger = new Monolog\Logger('test'); 9 | $logger->pushHandler(new Monolog\Handler\StreamHandler($build . '/test.log')); 10 | 11 | $settings = new Resque\Test\Settings(); 12 | $settings->setLogger($logger); 13 | $settings->fromEnvironment(); 14 | $settings->setBuildDir($build); 15 | $settings->checkBuildDir(); 16 | $settings->dumpConfig(); 17 | $settings->catchSignals(); 18 | $settings->startRedis(); // registers shutdown function 19 | 20 | Resque\Test::setSettings($settings); 21 | -------------------------------------------------------------------------------- /test/ClientTest.php: -------------------------------------------------------------------------------- 1 | 'abc', 11 | 'two' => 'def', 12 | 'three' => 123, 13 | 'four' => 1.0 / 3 14 | ); 15 | 16 | $success = $this->redis->hmset('some_other_key', $values); 17 | $hash = $this->redis->hgetall('some_other_key'); 18 | 19 | $this->assertTrue((boolean)$success, 'HMSET command returns success'); 20 | $this->assertEquals($values, $hash, 'HGETALL command returns array'); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /resque: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | performed = true; 14 | } 15 | 16 | /** 17 | * @return int See Resque\Job\Status::STATUS_* 18 | */ 19 | public function getStatusCode() 20 | { 21 | return $this->getStatus()->get(); 22 | } 23 | 24 | /** 25 | * @return string ID of the recreated job 26 | */ 27 | public function recreate() 28 | { 29 | return parent::recreate(); 30 | } 31 | 32 | /** 33 | * @return \Resque\Job\Status 34 | */ 35 | public function getStatus() 36 | { 37 | return parent::getStatus(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Console/Helper/RedisHelper.php: -------------------------------------------------------------------------------- 1 | client = $client; 24 | } 25 | 26 | /** 27 | * @return ClientInterface 28 | */ 29 | public function getClient() 30 | { 31 | return $this->client; 32 | } 33 | 34 | /** 35 | * @return string 36 | */ 37 | public function getName() 38 | { 39 | return 'redis'; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Console/Helper/LoggerHelper.php: -------------------------------------------------------------------------------- 1 | logger = $logger; 24 | } 25 | 26 | /** 27 | * @return LoggerInterface 28 | */ 29 | public function getLogger() 30 | { 31 | return $this->logger; 32 | } 33 | 34 | /** 35 | * @return string 36 | */ 37 | public function getName() 38 | { 39 | return 'logger'; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/JobInterface.php: -------------------------------------------------------------------------------- 1 | connected; 24 | } 25 | 26 | /** 27 | * Disconnects the client 28 | */ 29 | public function disconnect() 30 | { 31 | $this->close(); 32 | } 33 | 34 | /** 35 | * Alias to exec() for pipeline compatibility with Predis 36 | */ 37 | public function execute() 38 | { 39 | $this->__call('exec', array()); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Failure/BackendInterface.php: -------------------------------------------------------------------------------- 1 | 13 | * @license http://www.opensource.org/licenses/mit-license.php 14 | */ 15 | interface BackendInterface 16 | { 17 | /** 18 | * Initialize a failed job class and save it (where appropriate). 19 | * 20 | * @param array $payload Object containing details of the failed job. 21 | * @param \Exception $exception Instance of the exception that was thrown by the failed job. 22 | * @param Worker $worker Instance of Worker that received the job. 23 | * @param string $queue The name of the queue the job was fetched from. 24 | * @return void 25 | */ 26 | public function receiveFailure($payload, Exception $exception, Worker $worker, $queue); 27 | } 28 | -------------------------------------------------------------------------------- /src/Job/StatusFactory.php: -------------------------------------------------------------------------------- 1 | resque = $resque; 19 | } 20 | 21 | /** 22 | * @param String $id 23 | * @return \Resque\Job\Status 24 | */ 25 | public function forId($id) 26 | { 27 | return new Status($id, $this->resque); 28 | } 29 | 30 | /** 31 | * @param JobInterface $job 32 | * @return Status 33 | * @throws \Resque\Exception\JobIdException 34 | */ 35 | public function forJob(JobInterface $job) 36 | { 37 | $payload = $job->getPayload(); 38 | 39 | if (empty($payload['id'])) { 40 | throw new JobIdException('Job has no ID in payload, cannot get Status object'); 41 | } 42 | 43 | return $this->forId($payload['id']); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Console/Command.php: -------------------------------------------------------------------------------- 1 | getHelper('redis')->getClient(); 19 | } 20 | 21 | /** 22 | * @param OutputInterface $output 23 | * @return \Resque\Resque 24 | */ 25 | public function getResque(OutputInterface $output) 26 | { 27 | $resque = new Resque($this->getRedis()); 28 | 29 | if (($helper = $this->getHelper('logger'))) { 30 | /* @var LoggerHelper $helper */ 31 | $resque->setLogger($helper->getLogger()); 32 | } else { 33 | $resque->setLogger(new ConsoleLogger($output)); 34 | } 35 | 36 | return $resque; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (c) Chris Boulton 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /src/Console/QueueListCommand.php: -------------------------------------------------------------------------------- 1 | setName('queue:list') 19 | ->setDescription('Outputs information about queues'); 20 | } 21 | 22 | /** 23 | * @param InputInterface $input 24 | * @param OutputInterface $output 25 | * @return int 26 | */ 27 | protected function execute(InputInterface $input, OutputInterface $output) 28 | { 29 | $resque = $this->getResque($output); 30 | $queues = $resque->queues(); 31 | 32 | foreach ($queues as $queue) { 33 | $size = $resque->size($queue); 34 | $output->writeln($queue . " " . $size); 35 | } 36 | 37 | if (!count($queues)) { 38 | $output->writeln('No queues'); 39 | return 1; // If no queues, return error exit code 40 | } 41 | 42 | return 0; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Console/QueueClearCommand.php: -------------------------------------------------------------------------------- 1 | setName('queue:clear') 20 | ->setDescription('Clears a specified queue') 21 | ->addArgument( 22 | 'queue', 23 | InputArgument::REQUIRED, 24 | 'The name of the queue to clear' 25 | ); 26 | } 27 | 28 | /** 29 | * @param InputInterface $input 30 | * @param OutputInterface $output 31 | * @return int 32 | */ 33 | protected function execute(InputInterface $input, OutputInterface $output) 34 | { 35 | $resque = $this->getResque($output); 36 | $queue = $input->getArgument('queue'); 37 | 38 | $cleared = $resque->size($queue); 39 | $resque->clearQueue($queue); 40 | 41 | $output->writeln('Cleared ' . $cleared . ' jobs on queue ' . $queue); 42 | return 0; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /cli-config.php: -------------------------------------------------------------------------------- 1 | 'tcp', 27 | 'host' => '127.0.0.1', 28 | 'port' => 6379 29 | )); 30 | 31 | /* 32 | * You can optionally customize the PSR3 logger used on the CLI. The 33 | * default is the Console component's ConsoleLogger 34 | * 35 | * $logger = new Monolog\Logger('resque'); 36 | */ 37 | 38 | return \Resque\Console\ConsoleRunner::createHelperSet($predis/*, $logger*/); 39 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 5.3 5 | - 5.4 6 | - 5.5 7 | 8 | env: 9 | - RESQUE_CLIENT_TYPE=predis 10 | - RESQUE_CLIENT_TYPE=credis 11 | # - RESQUE_CLIENT_TYPE=phpredis (broken by https://github.com/nicolasff/phpredis/issues/474) 12 | - RESQUE_CLIENT_TYPE=phpiredis 13 | 14 | before_script: 15 | - sh -c "if [ \"$RESQUE_CLIENT_TYPE\" = 'phpredis' ]; then ant phpredis; fi" 16 | - sh -c "if [ \"$RESQUE_CLIENT_TYPE\" = 'phpredis' ]; then echo 'extension=redis.so' >> `php --ini | grep 'Loaded Configuration' | sed -e \"s|.*:\s*||\"`; fi" 17 | - sh -c "if [ \"$RESQUE_CLIENT_TYPE\" = 'phpiredis' ]; then ant phpiredis; fi" 18 | - sh -c "if [ \"$RESQUE_CLIENT_TYPE\" = 'phpiredis' ]; then echo 'extension=phpiredis.so' >> `php --ini | grep 'Loaded Configuration' | sed -e \"s|.*:\s*||\"`; fi" 19 | - composer install 20 | 21 | script: sh -c "if [ \"$RESQUE_CLIENT_TYPE\" = 'predis' -a \"$TRAVIS_PHP_VERSION\" = '5.5' ]; then phpunit --coverage-clover=coverage.clover; else phpunit; fi" 22 | 23 | after_script: 24 | - sh -c "if [ \"$RESQUE_CLIENT_TYPE\" = 'predis' -a \"$TRAVIS_PHP_VERSION\" = '5.5' ]; then wget https://scrutinizer-ci.com/ocular.phar; fi" 25 | - sh -c "if [ \"$RESQUE_CLIENT_TYPE\" = 'predis' -a \"$TRAVIS_PHP_VERSION\" = '5.5' ]; then php ocular.phar code-coverage:upload --format=php-clover coverage.clover; fi" 26 | - cat build/redis.log 27 | - cat build/test.log 28 | -------------------------------------------------------------------------------- /src/Console/WorkerCommand.php: -------------------------------------------------------------------------------- 1 | setName('worker') 21 | ->setDescription('Runs a Resque worker') 22 | ->addOption( 23 | 'queue', 24 | 'Q', 25 | InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 26 | 'A queue to listen to and run jobs from' 27 | ) 28 | ->addOption( 29 | 'interval', 30 | 'i', 31 | InputOption::VALUE_REQUIRED, 32 | 'Interval in seconds to wait for between reserving jobs', 33 | 5 34 | ); 35 | } 36 | 37 | /** 38 | * @param InputInterface $input 39 | * @param OutputInterface $output 40 | * @return int 41 | */ 42 | protected function execute(InputInterface $input, OutputInterface $output) 43 | { 44 | $resque = $this->getResque($output); 45 | $queues = $input->getOption('queue'); 46 | 47 | $worker = new Worker($resque, $queues); 48 | $worker->work($input->getOption('interval')); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "_-x-vim-formatting": " vim: set ft=javascript ts=4 sw=4 et : ", 3 | 4 | "name": "vend/resque", 5 | "description": "Namespaced port of chrisbolton/php-resque, supports Predis, more DI", 6 | "license": "MIT", 7 | 8 | "authors": [ 9 | { 10 | "name": "Chris Boulton", 11 | "email": "chris@bigcommerce.com" 12 | }, 13 | { 14 | "name": "Vend Devteam", 15 | "email": "devteam@vendhq.com" 16 | } 17 | ], 18 | 19 | "autoload": { 20 | "psr-4": { 21 | "Resque\\": "src" 22 | } 23 | }, 24 | 25 | "bin": [ 26 | "resque" 27 | ], 28 | 29 | "require": { 30 | "php": ">=5.3.7", 31 | "ext-pcntl": "*", 32 | "psr/log": ">=1.0.0", 33 | "symfony/console": ">=2.5.6" 34 | }, 35 | 36 | "require-dev": { 37 | "colinmollenhour/credis": "1.3.*", 38 | "predis/predis": ">=0.8.6", 39 | "phpunit/phpunit": "4.3.*", 40 | "monolog/monolog": "1.7.*" 41 | }, 42 | 43 | "extra": { 44 | "branch-alias": { 45 | "dev-master": "2.1.x-dev", 46 | "dev-2.0": "2.0.x-dev" 47 | } 48 | }, 49 | 50 | "suggest": { 51 | "ext-proctitle": "Allows php-resque to rename the title of UNIX processes to show the status of a worker.", 52 | "predis/predis": "Suggested Redis client", 53 | "ext-phpiredis": "Better performance for Predis", 54 | "colinmoullenhour/credis": "Alternative Redis client", 55 | "ext-phpredis": "Better performance for Credis" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /test/Test.php: -------------------------------------------------------------------------------- 1 | redis = self::$settings->getClient(); 45 | $this->redis->flushdb(); 46 | 47 | $this->logger = self::$settings->getLogger(); 48 | 49 | $this->resque = new Resque($this->redis); 50 | $this->resque->setLogger($this->logger); 51 | } 52 | 53 | public function tearDown() 54 | { 55 | if ($this->redis) { 56 | $this->redis->flushdb(); 57 | 58 | if ($this->redis->isConnected()) { 59 | $this->logger->notice('Shutting down connected Redis instance in tearDown()'); 60 | $this->redis->disconnect(); 61 | } 62 | } 63 | } 64 | 65 | protected function getWorker($queues) 66 | { 67 | $worker = new Worker($this->resque, $queues); 68 | $worker->setLogger($this->logger); 69 | $worker->register(); 70 | 71 | return $worker; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Console/ConsoleRunner.php: -------------------------------------------------------------------------------- 1 | set(new RedisHelper($client)); 28 | 29 | if ($logger) { 30 | $helper->set(new LoggerHelper($logger)); 31 | } 32 | 33 | return $helper; 34 | } 35 | 36 | /** 37 | * Runs the application using the given HelperSet 38 | * 39 | * This method is responsible for creating the console application, adding 40 | * relevant commands, and running it. Other code is responsible for producing 41 | * the HelperSet itself (your cli-config.php or bootstrap code), and for 42 | * calling this method (the actual bin command file). 43 | * 44 | * @param HelperSet $helperSet 45 | * @return integer 0 if everything went fine, or an error code 46 | */ 47 | public static function run(HelperSet $helperSet) 48 | { 49 | $application = new Application('Resque Console Tool', Version::VERSION); 50 | $application->setCatchExceptions(true); 51 | $application->setHelperSet($helperSet); 52 | 53 | $application->add(new QueueListCommand()); 54 | $application->add(new QueueClearCommand()); 55 | $application->add(new EnqueueCommand()); 56 | $application->add(new WorkerCommand()); 57 | 58 | return $application->run(); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /test/StatisticTest.php: -------------------------------------------------------------------------------- 1 | 10 | * @license http://www.opensource.org/licenses/mit-license.php 11 | */ 12 | class StatisticTest extends Test 13 | { 14 | /** 15 | * @var Statistic 16 | */ 17 | protected $statistic; 18 | 19 | public function setUp() 20 | { 21 | parent::setUp(); 22 | 23 | $this->statistic = new Statistic($this->resque, __CLASS__); 24 | } 25 | 26 | public function tearDown() 27 | { 28 | if ($this->statistic) { 29 | $this->statistic->clear(); 30 | $this->statistic = null; 31 | } 32 | 33 | parent::tearDown(); 34 | } 35 | 36 | protected function assertStatisticValueByClient($value, $message = '') 37 | { 38 | $this->assertEquals($value, $this->redis->get('resque:stat:' . __CLASS__), $message); 39 | } 40 | 41 | public function testStatCanBeIncremented() 42 | { 43 | $this->statistic->incr(); 44 | $this->statistic->incr(); 45 | $this->assertStatisticValueByClient(2); 46 | } 47 | 48 | public function testStatCanBeIncrementedByX() 49 | { 50 | $this->statistic->incr(10); 51 | $this->statistic->incr(11); 52 | $this->assertStatisticValueByClient(21); 53 | } 54 | 55 | public function testStatCanBeDecremented() 56 | { 57 | $this->statistic->incr(22); 58 | $this->statistic->decr(); 59 | $this->assertStatisticValueByClient(21); 60 | } 61 | 62 | public function testStatCanBeDecrementedByX() 63 | { 64 | $this->statistic->incr(22); 65 | $this->statistic->decr(11); 66 | $this->assertStatisticValueByClient(11); 67 | } 68 | 69 | public function testGetStatByName() 70 | { 71 | $this->statistic->incr(100); 72 | $this->assertEquals(100, $this->statistic->get()); 73 | } 74 | 75 | public function testGetUnknownStatReturns0() 76 | { 77 | $statistic = new Statistic($this->resque, 'some_unknown_statistic'); 78 | $this->assertEquals(0, $statistic->get()); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/Statistic.php: -------------------------------------------------------------------------------- 1 | 10 | * @license http://www.opensource.org/licenses/mit-license.php 11 | */ 12 | class Statistic 13 | { 14 | const KEY = 'stat:'; 15 | 16 | protected $resque; 17 | protected $statistic; 18 | 19 | /** 20 | * Constructor 21 | * 22 | * @param Resque $resque 23 | * @param string $statistic 24 | */ 25 | public function __construct(Resque $resque, $statistic) 26 | { 27 | $this->resque = $resque; 28 | $this->statistic = $statistic; 29 | } 30 | 31 | /** 32 | * Gets the key for a statistic 33 | * 34 | * @return string 35 | */ 36 | public function getKey() 37 | { 38 | return $this->resque->getKey(self::KEY . $this->statistic); 39 | } 40 | 41 | /** 42 | * Get the value of the supplied statistic counter for the specified statistic. 43 | * 44 | * @return integer Value of the statistic. 45 | */ 46 | public function get() 47 | { 48 | return (int)$this->resque->getClient()->get($this->getKey()); 49 | } 50 | 51 | /** 52 | * Increment the value of the specified statistic by a certain amount (default is 1) 53 | * 54 | * @param int $by The amount to increment the statistic by. 55 | * @return boolean True if successful, false if not. 56 | */ 57 | public function incr($by = 1) 58 | { 59 | return (bool)$this->resque->getClient()->incrby($this->getKey(), $by); 60 | } 61 | 62 | /** 63 | * Decrement the value of the specified statistic by a certain amount (default is 1) 64 | * 65 | * @param int $by The amount to decrement the statistic by. 66 | * @return boolean True if successful, false if not. 67 | */ 68 | public function decr($by = 1) 69 | { 70 | return (bool)$this->resque->getClient()->decrby($this->getKey(), $by); 71 | } 72 | 73 | /** 74 | * Delete a statistic with the given name. 75 | * 76 | * @return boolean True if successful, false if not. 77 | */ 78 | public function clear() 79 | { 80 | return (bool)$this->resque->getClient()->del($this->getKey()); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /test/AbstractJobTest.php: -------------------------------------------------------------------------------- 1 | 13 | * @license http://www.opensource.org/licenses/mit-license.php 14 | */ 15 | class AbstractJobTest extends Test 16 | { 17 | /** 18 | * @var Worker 19 | */ 20 | protected $worker; 21 | 22 | public function setUp() 23 | { 24 | parent::setUp(); 25 | 26 | // Register a worker to test with 27 | $this->worker = new Worker($this->resque, 'jobs'); 28 | $this->worker->setLogger($this->logger); 29 | $this->worker->register(); 30 | } 31 | 32 | public function testJobCanBeQueued() 33 | { 34 | $this->assertTrue((bool)$this->resque->enqueue('jobs', 'Resque\Test\Job')); 35 | } 36 | 37 | public function testQueuedJobCanBeReserved() 38 | { 39 | $this->resque->enqueue('jobs', 'Resque\Test\Job'); 40 | 41 | $worker = $this->getWorker('jobs'); 42 | 43 | $job = $worker->reserve('jobs'); 44 | 45 | if ($job == false) { 46 | $this->fail('Job could not be reserved.'); 47 | } 48 | 49 | $this->assertEquals('jobs', $job->getQueue()); 50 | $this->assertEquals('Resque\Test\Job', $job['class']); 51 | } 52 | 53 | /** 54 | * @expectedException \InvalidArgumentException 55 | */ 56 | public function testObjectArgumentsCannotBePassedToJob() 57 | { 58 | $args = new \stdClass; 59 | $args->test = 'somevalue'; 60 | $this->resque->enqueue('jobs', 'Resque\Test\Job', $args); 61 | } 62 | 63 | public function testFailedJobExceptionsAreCaught() 64 | { 65 | $this->resque->clearQueue('jobs'); 66 | 67 | $job = new FailingJob('jobs', array( 68 | 'class' => 'Resque\Test\FailingJob', 69 | 'args' => null, 70 | 'id' => 'failing_test_job' 71 | )); 72 | $job->setResque($this->resque); 73 | 74 | $this->worker->perform($job); 75 | 76 | $failed = new Statistic($this->resque, 'failed'); 77 | $workerFailed = new Statistic($this->resque, 'failed:' . (string)$this->worker); 78 | 79 | $this->assertEquals(1, $failed->get()); 80 | $this->assertEquals(1, $workerFailed->get()); 81 | } 82 | 83 | public function testInvalidJobReservesNull() 84 | { 85 | $this->resque->enqueue('jobs', 'Resque\Test\NoPerformJob'); 86 | $job = $this->worker->reserve(); 87 | $this->assertNull($job); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/Console/EnqueueCommand.php: -------------------------------------------------------------------------------- 1 | setName('enqueue') 21 | ->setDescription('Enqueues a job into a queue') 22 | ->addArgument( 23 | 'queue', 24 | InputArgument::REQUIRED, 25 | 'The name of the queue where the job should be enqueued' 26 | ) 27 | ->addArgument( 28 | 'class', 29 | InputArgument::REQUIRED, 30 | 'The class name of the job to enqueue' 31 | ) 32 | ->addOption( 33 | 'payload', 34 | 'p', 35 | InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 36 | 'A payload to send with the job', 37 | null 38 | ) 39 | ->addOption( 40 | 'json', 41 | 'j', 42 | InputOption::VALUE_REQUIRED, 43 | 'A JSON payload to send with the job (specify as a JSON encoded string)', 44 | null 45 | ) 46 | ->addOption( 47 | 'track', 48 | 't', 49 | InputOption::VALUE_NONE, 50 | 'If present, the job will be tracked' 51 | ); 52 | } 53 | 54 | /** 55 | * @param InputInterface $input 56 | * @param OutputInterface $output 57 | * @return int 58 | */ 59 | protected function execute(InputInterface $input, OutputInterface $output) 60 | { 61 | $resque = $this->getResque($output); 62 | $queue = $input->getArgument('queue'); 63 | 64 | $id = $resque->enqueue( 65 | $queue, 66 | $input->getArgument('class'), 67 | $this->getPayload($input), 68 | $input->getOption('track') 69 | ); 70 | 71 | if ($id) { 72 | $message = sprintf('Enqueued job as %s to %s', $id, $queue); 73 | } else { 74 | $message = sprintf('Enqueued job to %s', $queue); 75 | } 76 | 77 | $output->writeln($message); 78 | return 0; 79 | } 80 | 81 | /** 82 | * @param InputInterface $input 83 | * @return array|mixed 84 | */ 85 | protected function getPayload(InputInterface $input) 86 | { 87 | if ($input->hasOption('payload') && 88 | 0 < count($input->getOption('payload'))) { 89 | return $input->getOption('payload'); 90 | } 91 | 92 | if ($input->hasOption('json')) { 93 | return json_decode($input->getOption('json'), true); 94 | } 95 | 96 | return array(); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/Client/ClientInterface.php: -------------------------------------------------------------------------------- 1 | 137 | */ 138 | public function hgetall($key); 139 | 140 | /** 141 | * @param string $key 142 | * @param array $hash 143 | * @return boolean|\Predis\Response\ResponseInterface 144 | */ 145 | public function hmset($key, array $hash); 146 | 147 | /** 148 | * @param string $key 149 | * @param string $field 150 | * @param integer $increment 151 | * @return integer The value at field after the increment operation 152 | */ 153 | public function hincrby($key, $field, $increment); 154 | } 155 | -------------------------------------------------------------------------------- /src/Failure/RedisBackend.php: -------------------------------------------------------------------------------- 1 | 14 | * @license http://www.opensource.org/licenses/mit-license.php 15 | */ 16 | class RedisBackend implements BackendInterface 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 Exception $exception Instance of the exception that was thrown by the failed job. 23 | * @param Worker $worker Instance of Worker that received the job. 24 | * @param string $queue The name of the queue the job was fetched from. 25 | */ 26 | public function receiveFailure($payload, Exception $exception, Worker $worker, $queue) 27 | { 28 | $data = new stdClass; 29 | $data->failed_at = strftime('%a %b %d %H:%M:%S %Z %Y'); 30 | $data->payload = $payload; 31 | $data->exception = $this->getClass($exception); 32 | $data->error = $this->getErrorMessage($this->getDistalCause($exception)); 33 | $data->worker = (string)$worker; 34 | $data->queue = $queue; 35 | $data->backtrace = $this->getBacktrace($exception); 36 | 37 | $data = json_encode($data); 38 | $worker->getResque()->getClient()->rpush($worker->getResque()->getKey('failed'), $data); 39 | } 40 | 41 | /** 42 | * Gets the backtrace for the exception 43 | * 44 | * The backtrace area is the only part of the failure that's shown on 45 | * multiple lines by resque-web. So, we'll also use it to mention the 46 | * wrapping exceptions. 47 | * 48 | * @param \Exception $exception 49 | * @return array 50 | */ 51 | protected function getBacktrace(\Exception $exception) 52 | { 53 | $backtrace = array(); 54 | 55 | $backtrace[] = '---'; 56 | $backtrace[] = $this->getErrorMessage($exception); 57 | $backtrace[] = '---'; 58 | 59 | // Allow marshalling of the trace: PHP marks getTraceAsString as final :-( 60 | if (method_exists($exception, 'getPreviousTraceAsString')) { 61 | $backtrace = array_merge($backtrace, explode("\n", $exception->getPreviousTraceAsString())); 62 | } else { 63 | $backtrace = array_merge($backtrace, explode("\n", $exception->getTraceAsString())); 64 | } 65 | 66 | if (($previous = $exception->getPrevious())) { 67 | $backtrace = array_merge($backtrace, $this->getBacktrace($previous)); // Recurse 68 | } 69 | 70 | return $backtrace; 71 | } 72 | 73 | /** 74 | * Find the ultimate cause exception, by following previous members right back 75 | * 76 | * @param Exception $exception 77 | * @return Exception 78 | */ 79 | protected function getDistalCause(\Exception $exception) 80 | { 81 | if (($previous = $exception->getPrevious())) { 82 | return $this->getDistalCause($previous); 83 | } 84 | return $exception; 85 | } 86 | 87 | /** 88 | * Find the class names of the exceptions 89 | * 90 | * @param Exception $exception 91 | * @return string 92 | */ 93 | protected function getClass(\Exception $exception) 94 | { 95 | $message = ''; 96 | 97 | if (($previous = $exception->getPrevious())) { 98 | $message = $this->getClass($previous) . ' < '; // Recurse 99 | } 100 | 101 | // Let the exception lie about its class: to support marshalling exceptions 102 | if (method_exists($exception, 'getClass')) { 103 | $message .= $exception->getClass(); 104 | } else { 105 | $message .= get_class($exception); 106 | } 107 | 108 | return $message; 109 | } 110 | 111 | /** 112 | * Gets a single string error message from the exception 113 | * 114 | * @param \Exception $exception 115 | * @return string 116 | */ 117 | protected function getErrorMessage(\Exception $exception) 118 | { 119 | return $exception->getMessage() . ' at ' . $exception->getFile() . ':' . $exception->getLine(); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/AbstractJob.php: -------------------------------------------------------------------------------- 1 | 18 | * @license http://www.opensource.org/licenses/mit-license.php 19 | */ 20 | abstract class AbstractJob implements ArrayAccess, IteratorAggregate, JobInterface 21 | { 22 | /** 23 | * @var string The ID of the job 24 | */ 25 | protected $id; 26 | 27 | /** 28 | * @var string The name of the queue that this job belongs to. 29 | */ 30 | protected $queue; 31 | 32 | /** 33 | * @var Resque The instance of Resque this job belongs to. 34 | */ 35 | protected $resque; 36 | 37 | /** 38 | * @var array Containing details of the job. 39 | */ 40 | protected $payload; 41 | 42 | /** 43 | * Instantiate a new instance of a job. 44 | * 45 | * @param string $queue The queue that the job belongs to. 46 | * @param array $payload array containing details of the job. 47 | */ 48 | public function __construct($queue, array $payload) 49 | { 50 | $this->queue = $queue; 51 | $this->payload = $payload; 52 | $this->id = isset($payload['id']) ? $payload['id'] : null; 53 | } 54 | 55 | /** 56 | * @param Resque $resque 57 | */ 58 | public function setResque(Resque $resque) 59 | { 60 | $this->resque = $resque; 61 | } 62 | 63 | /** 64 | * @return string 65 | */ 66 | public function getQueue() 67 | { 68 | return $this->queue; 69 | } 70 | 71 | /** 72 | * Gets a status instance for this job 73 | * 74 | * @throws InvalidArgumentException 75 | * @throws JobLogicException 76 | * @return \Resque\Job\Status 77 | */ 78 | protected function getStatus() 79 | { 80 | if (!$this->resque) { 81 | throw new JobLogicException('Job has no Resque instance: cannot get status'); 82 | } 83 | 84 | return $this->resque->getStatusFactory()->forJob($this); 85 | } 86 | 87 | public function getId() 88 | { 89 | return $this->id; 90 | } 91 | 92 | public function getPayload() 93 | { 94 | return $this->payload; 95 | } 96 | 97 | /** 98 | * Execute the job 99 | * 100 | * @return bool 101 | */ 102 | abstract public function perform(); 103 | 104 | /** 105 | * Re-queue the current job. 106 | * 107 | * @return string ID of the recreated job 108 | */ 109 | protected function recreate() 110 | { 111 | $status = $this->getStatus(); 112 | $tracking = $status->isTracking(); 113 | 114 | $new = $this->resque->enqueue( 115 | $this->queue, 116 | $this->payload['class'], 117 | $this->payload['args'], 118 | $tracking 119 | ); 120 | 121 | if ($tracking) { 122 | $status->update(Status::STATUS_RECREATED); 123 | $status->setAttribute('recreated_as', $new); 124 | } 125 | 126 | return $new; 127 | } 128 | 129 | /** 130 | * Generate a string representation used to describe the current job. 131 | * 132 | * @return string The string representation of the job. 133 | */ 134 | public function __toString() 135 | { 136 | $name = array( 137 | 'Job{' . $this->queue .'}' 138 | ); 139 | 140 | if(!empty($this->id)) { 141 | $name[] = 'ID: ' . $this->id; 142 | } 143 | 144 | $name[] = $this->payload['class']; 145 | 146 | if(!empty($this->payload['args'])) { 147 | $name[] = json_encode($this->payload['args']); 148 | } 149 | 150 | return '(' . implode(' | ', $name) . ')'; 151 | } 152 | 153 | /** 154 | * @see ArrayAccess::offsetSet() 155 | */ 156 | public function offsetSet($offset, $value) 157 | { 158 | if (is_null($offset)) { 159 | $this->payload[] = $value; 160 | } else { 161 | $this->payload[$offset] = $value; 162 | } 163 | } 164 | 165 | /** 166 | * @see ArrayAccess::offsetExists() 167 | */ 168 | public function offsetExists($offset) 169 | { 170 | return isset($this->payload[$offset]); 171 | } 172 | 173 | /** 174 | * @see ArrayAccess::offsetUnset() 175 | */ 176 | public function offsetUnset($offset) 177 | { 178 | unset($this->payload[$offset]); 179 | } 180 | 181 | /** 182 | * @see ArrayAccess::offsetGet() 183 | */ 184 | public function offsetGet($offset) 185 | { 186 | return isset($this->payload[$offset]) ? $this->payload[$offset] : null; 187 | } 188 | 189 | /** 190 | * @see IteratorAggregate::getIterator() 191 | */ 192 | public function getIterator() 193 | { 194 | return new \ArrayIterator($this->payload); 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /test/Job/StatusTest.php: -------------------------------------------------------------------------------- 1 | 17 | * @license http://www.opensource.org/licenses/mit-license.php 18 | */ 19 | class StatusTest extends Test 20 | { 21 | public function tearDown() 22 | { 23 | parent::tearDown(); 24 | 25 | $this->resque->clearQueue('jobs'); 26 | } 27 | 28 | public function testConstructor() 29 | { 30 | $status = new Status(uniqid(__CLASS__, true), $this->resque); 31 | } 32 | 33 | public function testJobStatusCanBeTracked() 34 | { 35 | $this->resque->clearQueue('jobs'); 36 | $token = $this->resque->enqueue('jobs', 'Resque\Test\Job', null, true); 37 | 38 | $status = new Status($token, $this->resque); 39 | $this->assertTrue($status->isTracking()); 40 | } 41 | 42 | public function testJobStatusIsReturnedViaJobInstance() 43 | { 44 | $this->resque->clearQueue('jobs'); 45 | 46 | $token = $this->resque->enqueue('jobs', 'Resque\Test\Job', null, true); 47 | 48 | $worker = $this->getWorker('jobs'); 49 | $job = $worker->reserve(); 50 | 51 | if (!$job) { 52 | $this->fail('Could not get job'); 53 | } 54 | 55 | $status = $this->resque->getStatusFactory()->forJob($job); 56 | $status = $status->get(); 57 | 58 | $this->assertEquals(Status::STATUS_WAITING, $status); 59 | } 60 | 61 | public function testQueuedJobReturnsQueuedStatus() 62 | { 63 | $token = $this->resque->enqueue('jobs', 'Resque\Test\Job', null, true); 64 | $status = new Status($token, $this->resque); 65 | $this->assertEquals(Status::STATUS_WAITING, $status->get()); 66 | } 67 | 68 | public function testQueuedJobReturnsCreatedAndUpdatedKeys() 69 | { 70 | $token = $this->resque->enqueue('jobs', 'Resque\Test\Job', null, true); 71 | $status = new Status($token, $this->resque); 72 | $this->assertGreaterThan(0, $status->getCreated()); 73 | $this->assertGreaterThan(0, $status->getUpdated()); 74 | } 75 | 76 | public function testStartingQueuedJobUpdatesUpdatedAtStatus() 77 | { 78 | $this->resque->clearQueue('jobs'); 79 | 80 | $token = $this->resque->enqueue('jobs', 'Resque\Test\Job', null, true); 81 | 82 | $status = new Status($token, $this->resque); 83 | $old_created = $status->getCreated(); 84 | $old_updated = $status->getUpdated(); 85 | 86 | sleep(1); 87 | 88 | $worker = $this->getWorker('jobs'); 89 | 90 | $job = $worker->reserve(); 91 | 92 | if (!$job) { 93 | $this->fail('Cannot get job'); 94 | } 95 | 96 | $worker->workingOn($job); 97 | 98 | $status = new Status($token, $this->resque); 99 | $this->assertEquals(Status::STATUS_RUNNING, $status->get()); 100 | $this->assertEquals($old_created, $status->getCreated()); 101 | $this->assertGreaterThan($old_updated, $status->getUpdated()); 102 | } 103 | 104 | public function testRunningJobReturnsRunningStatus() 105 | { 106 | $this->resque->clearQueue('jobs'); 107 | 108 | $token = $this->resque->enqueue('jobs', 'Resque\Test\Job', null, true); 109 | 110 | $worker = $this->getWorker('jobs'); 111 | 112 | $job = $worker->reserve(); 113 | 114 | if (!$job) { 115 | $this->fail('Cannot get job'); 116 | } 117 | 118 | $worker->workingOn($job); 119 | 120 | $status = new Status($token, $this->resque); 121 | $this->assertEquals(Status::STATUS_RUNNING, $status->get()); 122 | } 123 | 124 | public function testFailedJobReturnsFailedStatus() 125 | { 126 | $pid = getmypid(); 127 | 128 | $this->resque->clearQueue('jobs'); 129 | 130 | $token = $this->resque->enqueue('jobs', 'Resque\Test\FailingJob', null, true); 131 | 132 | $worker = $this->getWorker('jobs'); 133 | 134 | $status = new Status($token, $this->resque); 135 | $before = $status->get(); 136 | 137 | $pid2 = getmypid(); 138 | 139 | $worker->work(0); 140 | 141 | $pid3 = getmypid(); 142 | $status = new Status($token, $this->resque); 143 | $after = $status->get(); 144 | 145 | $this->assertEquals(Status::STATUS_FAILED, $after); 146 | } 147 | 148 | public function testCompletedJobReturnsCompletedStatus() 149 | { 150 | $this->resque->clearQueue('jobs'); 151 | 152 | $token = $this->resque->enqueue('jobs', 'Resque\Test\Job', null, true); 153 | 154 | $worker = $this->getWorker('jobs'); 155 | $worker->work(0); 156 | 157 | $status = new Status($token, $this->resque); 158 | $this->assertEquals(Status::STATUS_COMPLETE, $status->get()); 159 | } 160 | 161 | public function testStatusIsNotTrackedWhenToldNotTo() 162 | { 163 | $token = $this->resque->enqueue('jobs', 'Resque\Test\Job', null, false); 164 | $status = new Status($token, $this->resque); 165 | $this->assertFalse($status->isTracking()); 166 | } 167 | 168 | public function testStatusTrackingCanBeStopped() 169 | { 170 | $status = new Status('test', $this->resque); 171 | $status->create(); 172 | $this->assertEquals(Status::STATUS_WAITING, $status->get()); 173 | 174 | $status->stop(); 175 | $this->assertNull($status->get()); 176 | } 177 | 178 | /* 179 | public function testRecreatedJobWithTrackingStillTracksStatus() 180 | { 181 | $worker = $this->getWorker('jobs'); 182 | $originalToken = $this->resque->enqueue('jobs', 'Resque\Test\Job', null, true); 183 | 184 | $job = $worker->reserve(); 185 | 186 | if (!$job) { 187 | $this->fail('Could not reserve job'); 188 | } 189 | 190 | // Mark this job as being worked on to ensure that the new status is still 191 | // waiting. 192 | $worker->workingOn($job); 193 | 194 | // Now recreate it 195 | $newToken = $job->recreate(); 196 | 197 | // Make sure we've got a new job returned 198 | $this->assertNotEquals($originalToken, $newToken); 199 | 200 | // Now check the status of the new job 201 | $newJob = $worker->reserve(); 202 | 203 | if (!$newJob) { 204 | $this->fail('Could not get newJob'); 205 | } 206 | 207 | $this->assertEquals(Status::STATUS_WAITING, $newJob->getStatus()->get()); 208 | } 209 | */ 210 | } 211 | -------------------------------------------------------------------------------- /test/Test/Settings.php: -------------------------------------------------------------------------------- 1 | testPid = getmypid(); 28 | $this->fromDefaults(); 29 | } 30 | 31 | protected function fromDefaults() 32 | { 33 | $this->buildDir = __DIR__ . '/../../build'; 34 | $this->port = '6379'; 35 | $this->host = 'localhost'; 36 | $this->bind = '127.0.0.1'; 37 | $this->db = 0; 38 | $this->prefix = ''; 39 | $this->clientType = 'predis'; 40 | } 41 | 42 | public function fromEnvironment() 43 | { 44 | $env = array( 45 | 'client_type' => 'clientType', 46 | 'host' => 'host', 47 | 'port' => 'port', 48 | 'bind' => 'bind', 49 | 'build_dir' => 'buildDir', 50 | 'run' => 'run', 51 | 'db' => 'db', 52 | 'prefix' => 'prefix' 53 | ); 54 | 55 | foreach ($env as $var => $setting) { 56 | $name = 'RESQUE_' . strtoupper($var); 57 | 58 | if (isset($_SERVER[$name])) { 59 | $this->$setting = $_SERVER[$name]; 60 | } 61 | } 62 | } 63 | 64 | public function setBuildDir($dir) 65 | { 66 | $this->buildDir = $dir; 67 | } 68 | 69 | public function startRedis() 70 | { 71 | $this->dumpRedisConfig(); 72 | $this->registerShutdown(); 73 | 74 | $this->logger->notice('Starting redis server in {buildDir}', array('buildDir' => $this->buildDir)); 75 | exec('cd ' . $this->buildDir . '; redis-server ' . $this->buildDir . '/redis.conf', $output, $return); 76 | usleep(500000); 77 | 78 | if ($return != 0) { 79 | throw new \RuntimeException('Cannot start redis-server'); 80 | } 81 | } 82 | 83 | protected function getRedisConfig() 84 | { 85 | return array( 86 | 'daemonize' => 'yes', 87 | 'pidfile' => './redis.pid', 88 | 'port' => $this->port, 89 | 'bind' => $this->bind, 90 | 'timeout' => 0, 91 | 'dbfilename' => 'dump.rdb', 92 | 'dir' => $this->buildDir, 93 | 'loglevel' => 'debug', 94 | 'logfile' => './redis.log' 95 | ); 96 | } 97 | 98 | /** 99 | * @return ClientInterface 100 | * @throws \InvalidArgumentException 101 | */ 102 | public function getClient() 103 | { 104 | switch ($this->clientType) { 105 | case 'predis': 106 | return new \Predis\Client(array( 107 | 'host' => $this->host, 108 | 'port' => $this->port, 109 | 'db' => $this->db, 110 | 'prefix' => $this->prefix 111 | )); 112 | case 'phpiredis': 113 | return new \Predis\Client(array( 114 | 'host' => $this->host, 115 | 'port' => $this->port, 116 | 'db' => $this->db, 117 | 'prefix' => $this->prefix 118 | ), array( 119 | 'tcp' => 'Predis\Connection\PhpiredisStreamConnection', 120 | 'unix' => 'Predis\Connection\PhpiredisSocketConnection' 121 | )); 122 | case 'credis': 123 | case 'phpredis': 124 | $client = new \Resque\Client\CredisClient($this->host, $this->port); 125 | $client->setCloseOnDestruct(false); 126 | return $client; 127 | default: 128 | throw new \InvalidArgumentException('Invalid or unknown client type: ' . $this->clientType); 129 | } 130 | } 131 | 132 | public function checkBuildDir() 133 | { 134 | if (!is_dir($this->buildDir)) { 135 | mkdir($this->buildDir); 136 | } 137 | 138 | if (!is_dir($this->buildDir)) { 139 | throw new RuntimeException('Could not create build dir: ' . $this->buildDir); 140 | } 141 | } 142 | 143 | protected function dumpRedisConfig() 144 | { 145 | $file = $this->buildDir . '/redis.conf'; 146 | $conf = ''; 147 | 148 | foreach ($this->getRedisConfig() as $name => $value) { 149 | $conf .= "$name $value\n"; 150 | } 151 | 152 | $this->logger->info('Dumping redis config {config} to {file}', array('file' => $file, 'config' => $conf)); 153 | file_put_contents($file, $conf); 154 | } 155 | 156 | // Override INT and TERM signals, so they do a clean shutdown and also 157 | // clean up redis-server as well. 158 | public function catchSignals() 159 | { 160 | if (function_exists('pcntl_signal')) { 161 | pcntl_signal(SIGINT, function () { 162 | $this->logger->debug('SIGINT received'); 163 | exit; 164 | }); 165 | 166 | pcntl_signal(SIGTERM, function () { 167 | $this->logger->debug('SIGTERM received'); 168 | exit; 169 | }); 170 | } 171 | } 172 | 173 | public function killRedis() 174 | { 175 | $pid = getmypid(); 176 | 177 | $this->logger->notice('Attempting to kill redis from {pid}', array('pid' => $pid)); 178 | 179 | if ($pid === null || $this->testPid !== $pid) { 180 | $this->logger->warning('Refusing to kill redis from forked worker'); 181 | return; // don't kill from a forked worker 182 | } 183 | 184 | $pidFile = $this->buildDir . '/redis.pid'; 185 | 186 | if (file_exists($pidFile)) { 187 | $pid = trim(file_get_contents($pidFile)); 188 | posix_kill((int) $pid, 9); 189 | 190 | if(is_file($pidFile)) { 191 | unlink($pidFile); 192 | } 193 | } 194 | 195 | $filename = $this->buildDir . '/dump.rdb'; 196 | 197 | if (is_file($filename)) { 198 | unlink($filename); 199 | } 200 | } 201 | 202 | protected function registerShutdown() 203 | { 204 | $this->logger->info('Registered shutdown function'); 205 | register_shutdown_function(array($this, 'killRedis')); 206 | } 207 | 208 | /** 209 | * Sets a logger instance on the object 210 | * 211 | * @param LoggerInterface $logger 212 | * @return null 213 | */ 214 | public function setLogger(LoggerInterface $logger) 215 | { 216 | $this->logger = $logger; 217 | } 218 | 219 | /** 220 | * @return LoggerInterface 221 | */ 222 | public function getLogger() 223 | { 224 | return $this->logger; 225 | } 226 | 227 | /** 228 | * Dumps configuration to a file 229 | * 230 | * @return void 231 | */ 232 | public function dumpConfig() 233 | { 234 | $file = $this->buildDir . \DIRECTORY_SEPARATOR . 'settings.json'; 235 | $config = json_encode(get_object_vars($this), JSON_PRETTY_PRINT); 236 | 237 | $this->logger->info('Dumping test config {config} to {file}', array('file' => $file, 'config' => $config)); 238 | file_put_contents($file, $config); 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Forked version to add namespacing 2 | 3 | ### 2.0.0 ## 4 | 5 | **Note:**: vend/php-resque introduces namespacing and other backward incompatible changes to the API. 6 | 7 | The code is still released under the same license. With vend/php-resque, we've 8 | chosen to increment the major version number to reflect the changes, but this 9 | is a fork that we use at Vend, where we use Predis, and using the original 10 | chrisboulton/php-resque version wasn't an option. 11 | 12 | #### Namespacing 13 | 14 | All resque code is loaded from the `Resque` namespace. This means a slight 15 | change in layout to support PSR-0 (given, for instance, `Resque` is now 16 | `Resque\Resque`) 17 | 18 | #### Backend coupling removed 19 | 20 | Direct Credis coupling has been removed: Resque is now free of coupling to any Redis 21 | client library, meaning you can use it with Predis, Credis, redisent, etc. 22 | 23 | Instead of this library creating a Redis client instance for you, you'll be 24 | expected to provide a Redis client object to the Resque instance 25 | as a dependency. This helps eliminate global state, and means the library can 26 | be readily integrated into, for example, your existing Depedency Injection 27 | container or factory methods. 28 | 29 | For Credis clients, a lightweight wrapper is provided (this is chiefly used to 30 | implement a reconnect method). Predis clients can be 31 | used directly with no wrapping class. With these two main client types, 32 | support for phpiredis, phpredis 33 | and webdis is provided indirectly. 34 | 35 | You can use any client that implements the *methods* in Resque\Client\ClientInterface 36 | - your client need not *actually* implement the interface (many Redis clients 37 | use `__call()`). 38 | 39 | #### Protected API improvements and general SOLIDness 40 | 41 | Hidden global state removed: connection state is no longer hidden behind 42 | Resque::redis() static call. The Resque object is now instantiated like any 43 | normal object, and uses dependency injection in its constructor. 44 | 45 | The mostly private API of the previous versions has been opened up 46 | considerably, meaning more opportunity for extending classes. At Vend, we use 47 | this to send jobs to an FPM pool of workers, rather than forking for every job. 48 | 49 | #### Worker shuffles queues 50 | 51 | The worker shuffles the queues it will dequeue from on every pass of the main loop. 52 | This means each queue a worker runs has an equal chance to be dequeued from. (The 53 | previous behaviour was to work the queues in the order specified). 54 | 55 | #### Statistic object wraps main value 56 | 57 | Gone is the static `Statistic::get/set` API. A Statistic object now wraps the counter 58 | value, which can be retrieved via `$statistic->get()`. 59 | 60 | #### Job implementation made more flexible 61 | 62 | Jobs now only need implement JobInterface. They can optionally do this by extending 63 | AbstractJob. Job instances are directly created by the Worker, rather than being attached 64 | to a tightly-coupled concrete Job implementation. 65 | 66 | ## chrisboulton/php-resque 67 | 68 | ### 1.3 (2013-??-??) ## 69 | 70 | #### Redisent (Redis Library) Replaced with Credis 71 | 72 | Redisent has always been the Redis backend for php-resque because of its lightweight nature. Unfortunately, Redisent is largely unmaintained. 73 | 74 | [Credis](https://github.com/colinmollenhour/credis) is a fork of Redisent, which among other improvements will automatically use the [phpredis](https://github.com/nicolasff/phpredis) native PHP extension if it is available. (you want this for speed, trust me) 75 | 76 | php-resque now utilizes Credis for all Redis based operations. Credis automatically required and installed as a Composer dependency. 77 | 78 | #### Composer Support 79 | 80 | Composer support has been improved and is now the recommended method for including php-resque in your project. Details on Composer support can be found in the Getting Started section of the readme. 81 | 82 | #### Other Improvements/Changes 83 | 84 | * **COMPATIBILITY BREAKING**: The bundled worker manager `resque.php` has been moved to `bin/resque`, and is available as `vendor/bin/resque` when php-resque is installed as a Composer package. 85 | * Restructure tests and test bootstrapping. Autoload tests via Composer (install test dependencies with `composer install --dev`) 86 | * Add `SETEX` to list of commands which supply a key as the first argument in Redisent (danhunsaker) 87 | * Fix an issue where a lost connection to Redis could cause an infinite loop (atorres757) 88 | * Add a helper method to `Resque_Redis` to remove the namespace applied to Redis keys (tonypiper) 89 | 90 | ### 1.2 (2012-10-13) 91 | 92 | **Note:** This release is largely backwards compatible with php-resque 1.1. The next release will introduce backwards incompatible changes (moving from Redisent to Credis), and will drop compatibility with PHP 5.2. 93 | 94 | * Allow alternate redis database to be selected when calling setBackend by supplying a second argument (patrickbajao) 95 | * Use `require_once` when including php-resque after the app has been included in the sample resque.php to prevent include conflicts (andrewjshults) 96 | * Wrap job arguments in an array to improve compatibility with ruby resque (warezthebeef) 97 | * Fix a bug where the worker would spin out of control taking the server with it, if the redis connection was interrupted even briefly. Use SIGPIPE to trap this scenario cleanly. (d11wtq) 98 | * Added support of Redis prefix (namespaces) (hlegius) 99 | * When reserving jobs, check if the payload received from popping a queue is a valid object (fix bug whereby jobs are reserved based on an erroneous payload) (salimane) 100 | * Re-enable autoload for class_exists in Job.php (humancopy) 101 | * Fix lost jobs when there is more than one worker process started by the same parent process (salimane) 102 | * Move include for resque before APP_INCLUDE is loaded in, so that way resque is available for the app 103 | * Avoid working with dirty worker IDs (salimane) 104 | * Allow UNIX socket to be passed to Resque when connecting to Redis (pedroarnal) 105 | * Fix typographical errors in PHP docblocks (chaitanyakuber) 106 | * Set the queue name on job instances when jobs are executed (chaitanyakuber) 107 | * Fix and add tests for Resque_Event::stopListening (ebernhardson) 108 | * Documentation cleanup (maetl) 109 | * Pass queue name to afterEvent callback 110 | * Only declare RedisException if it doesn't already exist (Matt Heath) 111 | * Add support for Composer 112 | * Fix missing and incorrect paths for Resque and Resque_Job_Status classes in demo (jjfrey) 113 | * Disable autoload for the RedisException class_exists call (scragg0x) 114 | * General tidy up of comments and files/folders 115 | 116 | ### 1.1 (2011-03-27) 117 | 118 | * Update Redisent library for Redis 2.2 compatibility. Redis 2.2 is now required. (thedotedge) 119 | * Trim output of `ps` to remove any prepended whitespace (KevBurnsJr) 120 | * Use `getenv` instead of `$_ENV` for better portability across PHP configurations (hobodave) 121 | * Add support for sub-second queue check intervals (KevBurnsJr) 122 | * Ability to specify a cluster/multiple redis servers and consistent hash between them (dceballos) 123 | * Change arguments for jobs to be an array as they're easier to work with in PHP. 124 | * Implement ability to have setUp and tearDown methods for jobs, called before and after every single run. 125 | * Fix `APP_INCLUDE` environment variable not loading correctly. 126 | * Jobs are no longer defined as static methods, and classes are instantiated first. This change is NOT backwards compatible and requires job classes are updated. 127 | * Job arguments are passed to the job class when it is instantiated, and are accessible by $this->args. This change will break existing job classes that rely on arguments that have not been updated. 128 | * Bundle sample script for managing php-resque instances using monit 129 | * Fix undefined variable `$child` when exiting on non-forking operating systems 130 | * Add `PIDFILE` environment variable to write out a PID for single running workers 131 | 132 | ### 1.0 (2010-04-18) ## 133 | 134 | * Initial release 135 | -------------------------------------------------------------------------------- /test/WorkerTest.php: -------------------------------------------------------------------------------- 1 | 11 | * @license http://www.opensource.org/licenses/mit-license.php 12 | */ 13 | class WorkerTest extends Test 14 | { 15 | public function testWorkerRegistersInList() 16 | { 17 | $worker = $this->getWorker('*'); 18 | 19 | // Make sure the worker is in the list 20 | $this->assertTrue((bool)$this->redis->sismember('resque:workers', (string)$worker)); 21 | } 22 | 23 | public function testGetAllWorkers() 24 | { 25 | $num = 3; 26 | // Register a few workers 27 | for($i = 0; $i < $num; ++$i) { 28 | $worker = $this->getWorker('queue_' . $i); 29 | } 30 | 31 | // Now try to get them 32 | $this->assertEquals($num, count($this->resque->getWorkerIds())); 33 | } 34 | 35 | public function testGetWorkerById() 36 | { 37 | $worker = $this->getWorker('*'); 38 | 39 | $newWorker = new Worker($this->resque, '*'); 40 | $newWorker->setId((string)$worker); 41 | 42 | $this->assertEquals((string)$worker, (string)$newWorker); 43 | } 44 | 45 | 46 | public function testWorkerCanUnregister() 47 | { 48 | $worker = $this->getWorker('*'); 49 | $worker->unregister(); 50 | 51 | $this->assertFalse($this->resque->workerExists((string)$worker)); 52 | $this->assertEquals(array(), $this->resque->getWorkerIds()); 53 | $this->assertEquals(array(), $this->redis->smembers('resque:workers')); 54 | } 55 | 56 | public function testPausedWorkerDoesNotPickUpJobs() 57 | { 58 | $this->resque->clearQueue('jobs'); 59 | 60 | $worker = $this->getWorker('*'); 61 | $worker->pauseProcessing(); 62 | 63 | $this->resque->enqueue('jobs', 'Resque\Test\Job'); 64 | 65 | $worker->work(0); 66 | $worker->work(0); 67 | 68 | $this->assertEquals(0, $worker->getStatistic('processed')->get()); 69 | } 70 | 71 | /* 72 | public function testResumedWorkerPicksUpJobs() 73 | { 74 | $this->resque->clearQueue('jobs'); 75 | 76 | $worker = $this->getWorker('*'); 77 | $worker->pauseProcessing(); 78 | 79 | $this->resque->enqueue('jobs', 'Resque\Test\Job'); 80 | $worker->work(0); 81 | 82 | $this->assertEquals(0, $worker->getStatistic('processed')->get()); 83 | 84 | $worker->unPauseProcessing(); 85 | $worker->work(0); 86 | 87 | $this->assertEquals(1, $worker->getStatistic('processed')->get()); 88 | }*/ 89 | 90 | protected function clearQueues(array $queues) 91 | { 92 | foreach ($queues as $queue) { 93 | $this->resque->clearQueue($queue); 94 | } 95 | } 96 | 97 | public function testWorkerCanWorkOverMultipleQueues() 98 | { 99 | $queues = array( 100 | 'queue1', 101 | 'queue2' 102 | ); 103 | 104 | $this->clearQueues($queues); 105 | 106 | $worker = $this->getWorker($queues); 107 | 108 | $this->resque->enqueue($queues[0], 'Resque\Test\Job'); 109 | $this->resque->enqueue($queues[1], 'Resque\Test\Job'); 110 | 111 | $job = $worker->reserve(); 112 | 113 | if (!$job) { 114 | $this->fail('Cannot reserve job'); 115 | } 116 | 117 | $this->assertTrue(in_array($job->getQueue(), $queues), 'Job from valid queues'); 118 | 119 | $job = $worker->reserve(); 120 | 121 | if (!$job) { 122 | $this->fail('Cannot reserve job'); 123 | } 124 | 125 | $this->assertTrue(in_array($job->getQueue(), $queues), 'Job from valid queues'); 126 | } 127 | 128 | public function testWildcardQueueWorkerWorksAllQueues() 129 | { 130 | $queues = array( 131 | 'queue1', 132 | 'queue2' 133 | ); 134 | 135 | $this->clearQueues($queues); 136 | 137 | $worker = $this->getWorker('*'); 138 | 139 | $this->resque->enqueue($queues[0], 'Resque\Test\Job'); 140 | $this->resque->enqueue($queues[1], 'Resque\Test\Job'); 141 | 142 | $job = $worker->reserve(); 143 | 144 | if (!$job) { 145 | $this->fail('Cannot reserve job'); 146 | } 147 | 148 | $this->assertTrue(in_array($job->getQueue(), $queues), 'Job from valid queues'); 149 | 150 | $job = $worker->reserve(); 151 | 152 | if (!$job) { 153 | $this->fail('Cannot reserve job'); 154 | } 155 | 156 | $this->assertTrue(in_array($job->getQueue(), $queues), 'Job from valid queues'); 157 | } 158 | 159 | public function testWorkerDoesNotWorkOnUnknownQueues() 160 | { 161 | $worker = $this->getWorker('queue1'); 162 | 163 | $this->resque->enqueue('queue2', 'Resque\Test\Job'); 164 | 165 | $this->assertNull($worker->reserve()); 166 | } 167 | 168 | public function testWorkerClearsItsStatusWhenNotWorking() 169 | { 170 | $this->resque->enqueue('jobs', 'Resque\Test\Job'); 171 | $worker = $this->getWorker('jobs'); 172 | $job = $worker->reserve(); 173 | 174 | if (!$job) { 175 | $this->fail('Could not reserve job'); 176 | } 177 | 178 | $worker->workingOn($job); 179 | $worker->doneWorking(); 180 | $this->assertEquals(array(), $worker->job()); 181 | } 182 | 183 | public function testWorkerRecordsWhatItIsWorkingOn() 184 | { 185 | $worker = $this->getWorker('jobs'); 186 | 187 | $payload = array( 188 | 'class' => 'Resque\Test\Job', 189 | 'id' => 'test' 190 | ); 191 | 192 | $job = new Job('jobs', $payload); 193 | $worker->workingOn($job); 194 | 195 | $job = $worker->job(); 196 | 197 | if (!is_array($job)) { 198 | $this->fail('Could not get job being worked on'); 199 | } 200 | 201 | $this->assertEquals('jobs', $job['queue']); 202 | 203 | if(!isset($job['run_at'])) { 204 | $this->fail('Job does not have run_at time'); 205 | } 206 | 207 | $this->assertEquals($payload, $job['payload']); 208 | } 209 | 210 | /* 211 | public function testWorkerErasesItsStatsWhenShutdown() 212 | { 213 | $this->resque->enqueue('jobs', 'Resque\Test\Job'); 214 | $this->resque->enqueue('jobs', 'Resque\Test\FailingJob'); 215 | 216 | $worker = $this->getWorker('jobs'); 217 | 218 | $worker->work(0); 219 | $worker->shutdown(); 220 | $worker->work(0); 221 | 222 | $this->resque->clearQueue('jobs'); 223 | 224 | $this->assertEquals(0, $worker->getStatistic('processed')->get()); 225 | $this->assertEquals(0, $worker->getStatistic('failed')->get()); 226 | }*/ 227 | 228 | public function testWorkerCleansUpDeadWorkersOnStartup() 229 | { 230 | // Register a good worker 231 | $goodWorker = new Worker($this->resque, 'jobs'); 232 | $goodWorker->setLogger($this->logger); 233 | $goodWorker->register(); 234 | $goodWorker = $this->getWorker('jobs'); 235 | 236 | $workerId = explode(':', $goodWorker); 237 | 238 | // Register some bad workers 239 | $worker = new Worker($this->resque, 'jobs'); 240 | $worker->setLogger($this->logger); 241 | $worker->setId($workerId[0].':1:jobs'); 242 | $worker->register(); 243 | 244 | $worker = new Worker($this->resque, array('high', 'low')); 245 | $worker->setLogger($this->logger); 246 | $worker->setId($workerId[0].':2:high,low'); 247 | $worker->register(); 248 | 249 | $this->assertEquals(3, count($this->resque->getWorkerIds())); 250 | 251 | $goodWorker->pruneDeadWorkers(); 252 | 253 | // There should only be $goodWorker left now 254 | $this->assertEquals(1, count($this->resque->getWorkerIds())); 255 | } 256 | 257 | public function testDeadWorkerCleanUpDoesNotCleanUnknownWorkers() 258 | { 259 | // Register a bad worker on this machine 260 | $worker = new Worker($this->resque, 'jobs'); 261 | $worker->setLogger($this->logger); 262 | $workerId = explode(':', $worker); 263 | $worker->setId($workerId[0].':1:jobs'); 264 | $worker->register(); 265 | 266 | // Register some other false workers 267 | $worker = new Worker($this->resque, 'jobs'); 268 | $worker->setLogger($this->logger); 269 | $worker->setId('my.other.host:1:jobs'); 270 | $worker->register(); 271 | 272 | $this->assertEquals(2, count($this->resque->getWorkerIds())); 273 | 274 | $worker->pruneDeadWorkers(); 275 | 276 | // my.other.host should be left 277 | $workers = $this->resque->getWorkerIds(); 278 | $this->assertEquals(1, count($workers)); 279 | $this->assertEquals((string)$worker, (string)$workers[0]); 280 | } 281 | 282 | public function testWorkerFailsUncompletedJobsOnExit() 283 | { 284 | $backend = $this->getMockForAbstractClass('Resque\Failure\BackendInterface'); 285 | 286 | $backend->expects($this->once()) 287 | ->method('receiveFailure'); 288 | 289 | $this->resque->setFailureBackend($backend); 290 | 291 | $worker = $this->getWorker('jobs'); 292 | 293 | $job = new Job('jobs', array( 294 | 'class' => 'Resque\Test\Job', 295 | 'id' => __METHOD__ 296 | )); 297 | $job->setResque($this->resque); 298 | 299 | $worker->workingOn($job); 300 | $worker->unregister(); 301 | } 302 | 303 | public function testBlockingListPop() 304 | { 305 | $worker = $this->getWorker('jobs'); 306 | 307 | $this->resque->enqueue('jobs', 'Resque\Test\Job'); 308 | $this->resque->enqueue('jobs', 'Resque\Test\Job'); 309 | 310 | $i = 1; 311 | while($job = $worker->reserve(true, 1)) 312 | { 313 | $this->assertEquals('Resque\Test\Job', $job['class']); 314 | 315 | if($i == 2) { 316 | break; 317 | } 318 | 319 | $i++; 320 | } 321 | 322 | $this->assertEquals(2, $i); 323 | } 324 | 325 | public function testReestablishRedisConnection() 326 | { 327 | $worker = $this->getWorker('jobs'); 328 | $worker->reestablishRedisConnection(); 329 | } 330 | } 331 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Resque for PHP 2 | ## Namespaced Fork 3 | 4 | [![Build Status](https://travis-ci.org/vend/php-resque.svg?branch=master)](https://travis-ci.org/vend/php-resque) 5 | [![Latest Stable Version](https://poser.pugx.org/vend/resque/v/stable.svg)](https://packagist.org/packages/vend/resque) 6 | [![Latest Unstable Version](https://poser.pugx.org/vend/resque/v/unstable.svg)](https://packagist.org/packages/vend/resque) 7 | [![License](https://poser.pugx.org/vend/resque/license.svg)](https://packagist.org/packages/vend/resque) 8 | 9 | [Resque](https://github.com/resque/resque) is a Redis-backed library for creating background jobs, placing 10 | those jobs on one or more queues, and processing them later. 11 | 12 | This is a PHP fork of the Resque worker and job classes. This makes it compatible with the 13 | resque-web interface, and with other Resque libraries. (You could enqueue jobs from Ruby and dequeue 14 | them in PHP, for instance). 15 | 16 | This library (`vend/resque`) is a fork of [chrisboulton/php-resque](https://github.com/chrisboulton/php-resque) at around 17 | version 1.3, that has been refactored to remove global state, add namespacing, and improve 18 | decoupling. This makes it easier to use and extend. 19 | 20 | ## Getting Started 21 | 22 | Add `vend/resque` to your application's composer.json. 23 | 24 | ```json 25 | { 26 | "require": { 27 | "vend/resque": "~2.1.0" 28 | } 29 | } 30 | ``` 31 | 32 | ## Requirements 33 | 34 | * PHP 5.3+ 35 | * A Redis client library (for instance, [Predis](https://github.com/nrk/predis) or [Credis](https://github.com/colinmollenhour/credis)) 36 | 37 | ## Jobs 38 | 39 | ### Queueing Jobs 40 | 41 | Jobs are queued as follows: 42 | 43 | ```php 44 | use Resque\Resque; 45 | use Predis\Client; 46 | 47 | $resque = new Resque(new Client()); 48 | $resque->enqueue('default_queue', 'App\Job', array('foo' => 'bar'), true); 49 | ``` 50 | 51 | In order the arguments are: queue, job class, job payload, whether to enable tracking. 52 | 53 | ### Defining Jobs 54 | 55 | Each job should be in its own class, and implement the `Resque\JobInterface`. (This is pretty easy, 56 | and only really requires a single custom method: `perform()`.) Most of the time, you'll want to 57 | use the default implementation of a Job, and extend the `Resque\AbstractJob` instead of implementing 58 | the interface yourself: 59 | 60 | ```php 61 | namespace App; 62 | 63 | use Resque\AbstractJob; 64 | 65 | class Job extends AbstractJob 66 | { 67 | public function perform() 68 | { 69 | // work work work 70 | $this->doSomething($this->payload['foo']); 71 | } 72 | } 73 | ``` 74 | 75 | Any exception thrown by a job will result in the job failing - be 76 | careful here and make sure you handle the exceptions that shouldn't 77 | result in a job failing. 78 | 79 | ### Job Status Tracking 80 | 81 | vend/resque has the ability to perform basic status tracking of a queued 82 | job. The status information will allow you to check if a job is in the 83 | queue, is currently being run, has finished, or has failed. 84 | 85 | To track the status of a job, pass `true` as the fourth argument to 86 | `Resque\Resque::enqueue`. An ID used for tracking the job status will be 87 | returned: 88 | 89 | ```php 90 | $id = $resque->enqueue('default_queue', 'App\Job', $payload, true); 91 | echo $id; // [0-9a-f]{32} 92 | ``` 93 | 94 | To fetch the status of a job: 95 | 96 | ```php 97 | $factory = new Resque\Job\StatusFactory($resque); 98 | 99 | // Pass the ID returned from enqueue 100 | $status = $factory->forId($id); 101 | 102 | // Alternatively, to get the status for a Job instance: 103 | $status = $factory->forJob($job); 104 | 105 | // Outputs the status as a string: 'waiting', 'running', 'complete', etc. 106 | echo $status->getStatus(); 107 | ``` 108 | 109 | The Status object contains methods for adding other attributes to the 110 | tracked status. (For instance, you might use the status object to track 111 | errors, completion information, iterations, etc.) 112 | 113 | #### Statuses 114 | 115 | Job statuses are defined as constants in the `Resque\Job\Status` class. 116 | Valid statuses include: 117 | 118 | * `Resque\Job\Status::STATUS_WAITING` - Job is still queued 119 | * `Resque\Job\Status::STATUS_RUNNING` - Job is currently running 120 | * `Resque\Job\Status::STATUS_FAILED` - Job has failed 121 | * `Resque\Job\Status::STATUS_COMPLETE` - Job is complete 122 | 123 | Statuses are available for up to 24 hours after a job has completed 124 | or failed, and are then automatically expired. A status can also 125 | forcefully be expired by calling the `stop()` method on a status 126 | class. 127 | 128 | ## Console 129 | 130 | You are free you implement your own daemonization/worker pool strategy by 131 | subclassing the `Worker` class. For playing around, and low throughput 132 | applications, you might like to use the built-in console commands. 133 | 134 | This version of the library uses the Symfony2 Console component. But it 135 | must be configured with the details of your Redis connection. We do this 136 | in much the same way that the Doctrine2 Console component gets details 137 | of your database connection: via a `cli-config.php` file. 138 | 139 | There is an example `cli-config.php` in this repository. 140 | 141 | If you're running a full-stack web application, you'd generally use your 142 | locator/service container to fill in the Redis client connection in this 143 | file. (The default is to use Predis, and to connect to 127.0.0.1:6379). 144 | 145 | ### Basic Usage 146 | 147 | ``` 148 | resque 149 | 150 | Available commands: 151 | enqueue Enqueues a job into a queue 152 | help Displays help for a command 153 | list Lists commands 154 | worker Runs a Resque worker 155 | queue 156 | queue:clear Clears a specified queue 157 | queue:list Outputs information about queues 158 | ``` 159 | 160 | ### Enqueueing 161 | 162 | This command will enqueue a `Some\Test\Job` job onto the `default` queue. 163 | Watch out for the single quotes around the class name: when specifying backslashes 164 | on the command line, you'll probably have to avoid your shell escaping them. 165 | 166 | ``` 167 | resque enqueue default 'Some\Test\Job' -t 168 | ``` 169 | 170 | ### Worker 171 | 172 | This command will run a simple pre-forking worker on two queues: 173 | 174 | ``` 175 | resque worker -Q default -Q some_other_queue 176 | ``` 177 | 178 | (`-q` means quiet, `-Q` specifies queues). You can also specify no queues, or 179 | the special queue `'*'` (watch for shell expansion). 180 | 181 | 182 | ### Queue Information 183 | 184 | There are a couple of useful commands for getting information about the queues. This 185 | will show a list of queues and how many jobs are waiting on each: 186 | 187 | ``` 188 | resque queue:list 189 | ``` 190 | 191 | This command will clear a specified queue: 192 | 193 | ``` 194 | resque queue:clear default 195 | ``` 196 | 197 | ### Logging 198 | 199 | The library now uses PSR3. When running as a console component, you can customise 200 | the logger to use in `cli-config.php`. (For instance, you might like to send your 201 | worker logs to Monolog.) 202 | 203 | ## Why Fork? 204 | 205 | Unfortunately, several things about the existing versions of php-resque made it 206 | a candidate for refactoring: 207 | 208 | * php-resque supported the Credis connection library and had hard-coded this 209 | support by using static accessors. This meant more advanced features such as 210 | replication and pipelining were unavailable. 211 | * Now, Resque for PHP supports any client object that implements a suitable 212 | subset of Redis commands. No type-checking is done on the passed in connection, 213 | meaning you're free to use Predis, or whatever you like. 214 | * While the public API of `php-resque` was alright, the protected API was pretty 215 | much useless. This made it hard to extend worker classes to dispatch jobs differently. 216 | * Important state (the underlying connection) was thrown into the global scope 217 | by hiding it behind static accessors (`Resque::redis()`). This makes things 218 | easier for the library author (because he/she need not think about dependencies) 219 | but also statically ties together the classes in the library: it makes 220 | testing and extending the library hard. 221 | * There's no reason to do this: `Resque` instances should simply use DI and 222 | take a client connection as a required constructor argument. 223 | * This improvement also allows the connection to be mocked without extending 224 | the `Resque` class. 225 | * And it lets you reuse your existing connection to Redis, if you have one. 226 | No need to open a new connection just to enqueue a job. 227 | * Statistic classes were static for no good reason. 228 | * To work, statistics need a connection to Redis, a name, and several methods 229 | (get, set). State and methods to manipulate it? Sounds like a task for 230 | objects! *Not* just static methods, that don't encapsulate any of the state. 231 | * Because these were static calls, the `Resque_Stat` class was hard-coded into 232 | several other classes, and could not be easily extended. 233 | * The library is now fully namespaced and compatible with PSR-0. The top level 234 | namespace is `Resque`. 235 | * The events system has been removed. There is now little need for it. 236 | It seems like the events system was just a workaround due to the poor 237 | extensibility of the Worker class. The library should allow you to extend any 238 | class you like, and no method should be too long or arduous to move into a 239 | subclass. 240 | 241 | ## Contributors ## 242 | 243 | Here's the contributor list from earlier versions, at [chrisboulton/php-resque](https://github.com/chrisboulton/php-resque): 244 | 245 | * @chrisboulton 246 | * @acinader 247 | * @ajbonner 248 | * @andrewjshults 249 | * @atorres757 250 | * @benjisg 251 | * @cballou 252 | * @chaitanyakuber 253 | * @charly22 254 | * @CyrilMazur 255 | * @d11wtq 256 | * @danhunsaker 257 | * @dceballos 258 | * @ebernhardson 259 | * @hlegius 260 | * @hobodave 261 | * @humancopy 262 | * @JesseObrien 263 | * @jjfrey 264 | * @jmathai 265 | * @joshhawthorne 266 | * @KevBurnsJr 267 | * @lboynton 268 | * @maetl 269 | * @matteosister 270 | * @MattHeath 271 | * @mickhrmweb 272 | * @Olden 273 | * @patrickbajao 274 | * @pedroarnal 275 | * @ptrofimov 276 | * @rajibahmed 277 | * @richardkmiller 278 | * @Rockstar04 279 | * @ruudk 280 | * @salimane 281 | * @scragg0x 282 | * @scraton 283 | * @thedotedge 284 | * @tonypiper 285 | * @trimbletodd 286 | * @warezthebeef 287 | -------------------------------------------------------------------------------- /src/Resque.php: -------------------------------------------------------------------------------- 1 | 41 | */ 42 | protected $options; 43 | 44 | /** 45 | * @var LoggerInterface 46 | */ 47 | protected $logger; 48 | 49 | /** 50 | * @var BackendInterface 51 | */ 52 | protected $failures; 53 | 54 | /** 55 | * @var StatusFactory 56 | */ 57 | protected $statuses; 58 | 59 | /** 60 | * Constructor 61 | * @param ClientInterface $client 62 | */ 63 | public function __construct($client, array $options = array()) 64 | { 65 | $this->client = $client; 66 | $this->logger = new NullLogger(); 67 | 68 | $this->configure($options); 69 | } 70 | 71 | /** 72 | * Configures the options of the resque background queue system 73 | * 74 | * @param array $options 75 | * @return void 76 | */ 77 | public function configure(array $options) 78 | { 79 | $this->options = array_merge(array( 80 | 'pgrep' => 'pgrep -f', 81 | 'pgrep_pattern' => '[r]esque[^-]', 82 | 'prefix' => 'resque:', 83 | 'statistic_class' => 'Resque\Statistic' 84 | ), $options); 85 | } 86 | 87 | /** 88 | * Gets the underlying Redis client 89 | * 90 | * The Redis client can be any object that implements a suitable subset 91 | * of Redis commands. 92 | * 93 | * @return ClientInterface 94 | */ 95 | public function getClient() 96 | { 97 | return $this->client; 98 | } 99 | 100 | /** 101 | * @param \Resque\Failure\BackendInterface $backend 102 | */ 103 | public function setFailureBackend(BackendInterface $backend) 104 | { 105 | $this->failures = $backend; 106 | } 107 | 108 | /** 109 | * @return BackendInterface 110 | */ 111 | public function getFailureBackend() 112 | { 113 | if (!isset($this->failures)) { 114 | $this->failures = new RedisBackend(); 115 | } 116 | 117 | return $this->failures; 118 | } 119 | 120 | /** 121 | * @param StatusFactory $factory 122 | */ 123 | public function setStatusFactory(StatusFactory $factory) 124 | { 125 | $this->statuses = $factory; 126 | } 127 | 128 | /** 129 | * @return StatusFactory 130 | */ 131 | public function getStatusFactory() 132 | { 133 | if (!isset($this->statuses)) { 134 | $this->statuses = new StatusFactory($this); 135 | } 136 | 137 | return $this->statuses; 138 | } 139 | 140 | /** 141 | * Causes the client to reconnect to the Redis server 142 | * 143 | * @return void 144 | */ 145 | public function reconnect() 146 | { 147 | if ($this->client->isConnected()) { 148 | $this->client->disconnect(); 149 | } 150 | $this->client->connect(); 151 | } 152 | 153 | /** 154 | * Causes the client to connect to the Redis server 155 | */ 156 | public function connect() 157 | { 158 | $this->client->connect(); 159 | } 160 | 161 | /** 162 | * Disconnects the client from the Redis server 163 | */ 164 | public function disconnect() 165 | { 166 | $this->client->disconnect(); 167 | } 168 | 169 | /** 170 | * Logs a message 171 | * 172 | * @param string $message 173 | * @param string $priority 174 | * @return void 175 | */ 176 | public function log($message, $priority = LogLevel::INFO) 177 | { 178 | $this->logger->log($message, $priority); 179 | } 180 | 181 | /** 182 | * Gets a namespaced/prefixed key for the given key suffix 183 | * 184 | * @param string $key 185 | * @return string 186 | */ 187 | public function getKey($key) 188 | { 189 | return $this->options['prefix'] . $key; 190 | } 191 | 192 | /** 193 | * Push a job to the end of a specific queue. If the queue does not 194 | * exist, then create it as well. 195 | * 196 | * @param string $queue The name of the queue to add the job to. 197 | * @param array $item Job description as an array to be JSON encoded. 198 | */ 199 | public function push($queue, $item) 200 | { 201 | // Add the queue to the list of queues 202 | $this->getClient()->sadd($this->getKey(self::QUEUES_KEY), $queue); 203 | 204 | // Add the job to the specified queue 205 | $this->getClient()->rpush($this->getKey(self::QUEUE_KEY . $queue), json_encode($item)); 206 | } 207 | 208 | /** 209 | * Pop an item off the end of the specified queue, decode it and 210 | * return it. 211 | * 212 | * @param string $queue The name of the queue to fetch an item from. 213 | * @return array Decoded item from the queue. 214 | */ 215 | public function pop($queue) 216 | { 217 | $item = $this->getClient()->lpop($this->getKey(self::QUEUE_KEY . $queue)); 218 | 219 | if (!$item) { 220 | return null; 221 | } 222 | 223 | return json_decode($item, true); 224 | } 225 | 226 | /** 227 | * Clears the whole of a queue 228 | * 229 | * @param string $queue 230 | */ 231 | public function clearQueue($queue) 232 | { 233 | $this->getClient()->del($this->getKey(self::QUEUE_KEY . $queue)); 234 | } 235 | 236 | /** 237 | * Return the size (number of pending jobs) of the specified queue. 238 | * 239 | * @param $queue name of the queue to be checked for pending jobs 240 | * 241 | * @return int The size of the queue. 242 | */ 243 | public function size($queue) 244 | { 245 | return $this->getClient()->llen($this->getKey(self::QUEUE_KEY . $queue)); 246 | } 247 | 248 | /** 249 | * Create a new job and save it to the specified queue. 250 | * 251 | * @param string $queue The name of the queue to place the job in. 252 | * @param string $class The name of the class that contains the code to execute the job. 253 | * @param array $args Any optional arguments that should be passed when the job is executed. 254 | * @param boolean $trackStatus Set to true to be able to monitor the status of a job. 255 | * @throws \InvalidArgumentException 256 | * @return string 257 | */ 258 | public function enqueue($queue, $class, $args = null, $trackStatus = false) 259 | { 260 | if ($args !== null && !is_array($args)) { 261 | throw new InvalidArgumentException( 262 | 'Supplied $args must be an array.' 263 | ); 264 | } 265 | 266 | $id = md5(uniqid('', true)); 267 | 268 | $this->push($queue, array( 269 | 'class' => $class, 270 | 'args' => $args, 271 | 'id' => $id, 272 | )); 273 | 274 | if ($trackStatus) { 275 | $status = new Status($id, $this); 276 | $status->create(); 277 | } 278 | 279 | return $id; 280 | } 281 | 282 | /** 283 | * Get an array of all known queues. 284 | * 285 | * @return array 286 | */ 287 | public function queues() 288 | { 289 | return $this->getSetMembers(self::QUEUES_KEY); 290 | } 291 | 292 | /** 293 | * Gets an array of all known worker IDs 294 | * 295 | * @return array 296 | */ 297 | public function getWorkerIds() 298 | { 299 | return $this->getSetMembers(self::WORKERS_KEY); 300 | } 301 | 302 | /** 303 | * @param string $suffix Partial key (don't pass to getKey() - let this method do it for you) 304 | * @return array 305 | */ 306 | protected function getSetMembers($suffix) 307 | { 308 | $members = $this->getClient()->smembers($this->getKey($suffix)); 309 | 310 | if (!is_array($members)) { 311 | $members = array(); 312 | } 313 | 314 | return $members; 315 | } 316 | 317 | /** 318 | * @param string $id Worker ID 319 | * @return bool 320 | */ 321 | public function workerExists($id) 322 | { 323 | return in_array($id, $this->getWorkerIds()); 324 | } 325 | 326 | /** 327 | * Return an array of process IDs for all of the Resque workers currently 328 | * running on this machine. 329 | * 330 | * Expects pgrep to be in the path, and for it to inspect full argument 331 | * lists using -f 332 | * 333 | * @return array Array of Resque worker process IDs. 334 | */ 335 | public function getWorkerPids() 336 | { 337 | $command = $this->options['pgrep'] . ' ' . escapeshellarg($this->options['pgrep_pattern']); 338 | 339 | $pids = array(); 340 | $output = null; 341 | $return = null; 342 | 343 | exec($command, $output, $return); 344 | 345 | /* 346 | * Exit codes: 347 | * 0 One or more processes were matched 348 | * 1 No processes were matched 349 | * 2 Invalid options were specified on the command line 350 | * 3 An internal error occurred 351 | */ 352 | if (($return !== 0 && $return !== 1) || empty($output) || !is_array($output)) { 353 | $this->logger->warning('Unable to determine worker PIDs'); 354 | return array(); 355 | } 356 | 357 | foreach ($output as $line) { 358 | $line = explode(' ', trim($line), 2); 359 | 360 | if (!$line[0] || !is_numeric($line[0])) { 361 | continue; 362 | } 363 | 364 | $pids[] = (int)$line[0]; 365 | } 366 | 367 | return $pids; 368 | } 369 | 370 | /** 371 | * Gets a statistic 372 | * 373 | * @param string $name 374 | * @return \Resque\Statistic 375 | */ 376 | public function getStatistic($name) 377 | { 378 | return new Statistic($this, $name); 379 | } 380 | 381 | /** 382 | * @param LoggerInterface $logger 383 | * @return void 384 | */ 385 | public function setLogger(LoggerInterface $logger) 386 | { 387 | $this->logger = $logger; 388 | } 389 | 390 | /** 391 | * @return LoggerInterface 392 | */ 393 | public function getLogger() 394 | { 395 | return $this->logger; 396 | } 397 | } 398 | -------------------------------------------------------------------------------- /src/Job/Status.php: -------------------------------------------------------------------------------- 1 | 16 | * @author Chris Boulton 17 | * @copyright (c) 2010 Chris Boulton 18 | * @license http://www.opensource.org/licenses/mit-license.php 19 | */ 20 | class Status 21 | { 22 | /**#@+ 23 | * How many seconds until a status entry should expire 24 | * 25 | * In the previous implementation, incomplete statuses were not given a 26 | * TTL at all. This can make Redis treat the keys differently, depending 27 | * on your maxmemory-policy (for example, volative-lru will only remove 28 | * keys with an expire set). 29 | * 30 | * @var int 31 | */ 32 | const COMPLETE_TTL = 86400; // 24 hours 33 | const INCOMPLETE_TTL = 604800; // A week 34 | /**#@-*/ 35 | 36 | /**#@+ 37 | * Status codes 38 | * 39 | * @var int 40 | */ 41 | const STATUS_WAITING = 1; 42 | const STATUS_RUNNING = 2; 43 | const STATUS_FAILED = 3; 44 | const STATUS_COMPLETE = 4; 45 | const STATUS_RECREATED = 5; 46 | /**#@-*/ 47 | 48 | /** 49 | * An array of valid statuses 50 | * 51 | * @var array 52 | */ 53 | public static $valid = array( 54 | self::STATUS_WAITING => 'waiting', 55 | self::STATUS_RUNNING => 'running', 56 | self::STATUS_FAILED => 'failed', 57 | self::STATUS_COMPLETE => 'complete', 58 | self::STATUS_RECREATED => 'recreated' 59 | ); 60 | 61 | /** 62 | * An array of complete statuses 63 | * 64 | * @var array 65 | */ 66 | public static $complete = array( 67 | self::STATUS_FAILED, 68 | self::STATUS_COMPLETE, 69 | self::STATUS_RECREATED 70 | ); 71 | 72 | /** 73 | * @var string The ID of the job this status class refers back to. 74 | */ 75 | protected $id; 76 | 77 | /** 78 | * Whether the status has been loaded from the database 79 | * 80 | * @var boolean 81 | */ 82 | protected $loaded = false; 83 | 84 | /** 85 | * @var array 86 | */ 87 | protected $attributes = array(); 88 | 89 | /** 90 | * @var \Resque\Client\ClientInterface 91 | */ 92 | protected $client; 93 | 94 | /** 95 | * @var boolean|null Cache variable if the status of this job is being 96 | * monitored or not. True/false when checked at least 97 | * once or null if not checked yet. 98 | */ 99 | protected $isTracking = null; 100 | 101 | /** 102 | * Setup a new instance of the job monitor class for the supplied job ID. 103 | * 104 | * @param string $id The ID of the job to manage the status for. 105 | * @param \Resque\Resque $resque 106 | */ 107 | public function __construct($id, Resque $resque) 108 | { 109 | $this->id = $id; 110 | $this->client = $resque->getClient(); 111 | } 112 | 113 | /** 114 | * @return string 115 | */ 116 | public function getId() 117 | { 118 | return $this->id; 119 | } 120 | 121 | /** 122 | * Create a new status monitor item for the supplied job ID. Will create 123 | * all necessary keys in Redis to monitor the status of a job. 124 | */ 125 | public function create() 126 | { 127 | $this->isTracking = true; 128 | 129 | $this->setAttributes(array( 130 | 'status' => self::STATUS_WAITING, 131 | 'created' => time(), 132 | 'updated' => time() 133 | )); 134 | } 135 | 136 | /** 137 | * Sets all the given attributes 138 | * 139 | * @param array $attributes 140 | * @return mixed 141 | */ 142 | public function setAttributes(array $attributes) 143 | { 144 | $this->attributes = array_merge($this->attributes, $attributes); 145 | 146 | $set = array(); 147 | foreach ($attributes as $name => $value) { 148 | if ($name == 'status') { 149 | $this->update($value); 150 | continue; 151 | } 152 | $set[$name] = $value; 153 | } 154 | 155 | return call_user_func(array($this->client, 'hmset'), $this->getHashKey(), $set); 156 | } 157 | 158 | /** 159 | * Sets an attribute 160 | * 161 | * @param string $name 162 | * @param string $value 163 | */ 164 | public function setAttribute($name, $value) 165 | { 166 | if ($name == 'status') { 167 | $this->update($value); 168 | } else { 169 | $this->attributes[$name] = $value; 170 | $this->client->hmset($this->getHashKey(), array( 171 | $name => $value, 172 | 'updated' => time() 173 | )); 174 | } 175 | } 176 | 177 | /** 178 | * Increments an attribute 179 | * 180 | * The attribute should be an integer field 181 | * 182 | * @param string $name 183 | * @param integer $by 184 | * @return integer The value after incrementing (see hincrby) 185 | */ 186 | public function incrementAttribute($name, $by = 1) 187 | { 188 | $pipeline = $this->client->pipeline(); 189 | $pipeline->hincrby($this->getHashKey(), $name, $by); 190 | $pipeline->hset($this->getHashKey(), 'updated', time()); 191 | $result = $pipeline->execute(); 192 | 193 | return $this->attributes[$name] = $result[0]; 194 | } 195 | 196 | /** 197 | * Update the status indicator for the current job with a new status. 198 | * 199 | * This method is called from setAttribute/s so that the expiry can be 200 | * properly updated. 201 | * 202 | * @param int $status The status of the job (see constants in Resque\Job\Status) 203 | * @throws \InvalidArgumentException 204 | * @return boolean 205 | */ 206 | public function update($status) 207 | { 208 | if (!isset(self::$valid[$status])) { 209 | throw new InvalidArgumentException('Invalid status'); 210 | } 211 | 212 | if (!$this->isTracking()) { 213 | return false; 214 | } 215 | 216 | $this->attributes['status'] = $status; 217 | $this->attributes['updated'] = time(); 218 | 219 | $success = $this->client->hmset($this->getHashKey(), array( 220 | 'status' => $this->attributes['status'], 221 | 'updated' => $this->attributes['updated'] 222 | )); 223 | 224 | // Expire the status for completed jobs after 24 hours 225 | if (in_array($status, self::$complete)) { 226 | $this->client->expire($this->getHashKey(), self::COMPLETE_TTL); 227 | } else { 228 | $this->client->expire($this->getHashKey(), self::INCOMPLETE_TTL); 229 | } 230 | 231 | return (boolean)$success; 232 | } 233 | 234 | /** 235 | * Check if we're actually checking the status of the loaded job status 236 | * instance. 237 | * 238 | * @return boolean True if the status is being monitored, false if not. 239 | */ 240 | public function isTracking() 241 | { 242 | if ($this->isTracking === null) { 243 | $this->isTracking = (boolean)$this->client->exists($this->getHashKey()); 244 | if ($this->isTracking) { 245 | $this->load(); 246 | } 247 | } 248 | return $this->isTracking; 249 | } 250 | 251 | /** 252 | * Loads all status attributes 253 | * 254 | * @throws LogicException 255 | */ 256 | public function load() 257 | { 258 | if ($this->loaded) { 259 | throw new \LogicException('The status is already loaded. Use another instance.'); 260 | } 261 | 262 | $this->attributes = array_merge($this->attributes, $this->client->hgetall($this->getHashKey())); 263 | $this->loaded = true; 264 | } 265 | 266 | /** 267 | * @return array 268 | */ 269 | public function getAll() 270 | { 271 | if ($this->loaded) { 272 | return $this->attributes; 273 | } 274 | 275 | return $this->client->hgetall($this->getHashKey()); 276 | } 277 | 278 | /** 279 | * Gets the time this status was updated 280 | */ 281 | public function getUpdated() 282 | { 283 | return $this->getAttribute('updated'); 284 | } 285 | 286 | /** 287 | * Gets the time this status was created 288 | */ 289 | public function getCreated() 290 | { 291 | return $this->getAttribute('created'); 292 | } 293 | 294 | /** 295 | * Fetch the status for the job being monitored. 296 | * 297 | * For consistency, this would be called getStatus(), but for BC, it's 298 | * just get(). 299 | * 300 | * @return null|integer 301 | */ 302 | public function get() 303 | { 304 | return $this->getAttribute('status'); 305 | } 306 | 307 | /** 308 | * @return string 309 | */ 310 | public function getStatus() 311 | { 312 | $status = $this->get(); 313 | 314 | if (isset(self::$valid[$status])) { 315 | return self::$valid[$status]; 316 | } 317 | 318 | return 'unknown'; 319 | } 320 | 321 | /** 322 | * Gets a single attribute value 323 | * 324 | * @param string $name 325 | * @param mixed $default 326 | * @return mixed 327 | */ 328 | public function getAttribute($name, $default = null) 329 | { 330 | if ($this->loaded) { 331 | return isset($this->attributes[$name]) ? $this->attributes[$name] : $default; 332 | } 333 | 334 | // Could be just hget, but Credis will return false?! 335 | $attributes = $this->client->hGetAll($this->getHashKey()); 336 | return isset($attributes[$name]) ? $attributes[$name] : $default; 337 | } 338 | 339 | /** 340 | * Stop tracking the status of a job. 341 | * 342 | * @return void 343 | */ 344 | public function stop() 345 | { 346 | $this->client->del($this->getHashKey()); 347 | } 348 | 349 | /** 350 | * A new key, because we're now using a hash format to store the status 351 | * 352 | * Used from outside this class to do status processing more efficiently 353 | * 354 | * @return string 355 | */ 356 | public function getHashKey() 357 | { 358 | return 'job:' . $this->id . ':status/hash'; 359 | } 360 | 361 | /** 362 | * Accessor to return valid statuses 363 | * 364 | * @return array 365 | */ 366 | public function getValid() 367 | { 368 | return self::$valid; 369 | } 370 | 371 | /** 372 | * Accessor to return complete statuses 373 | * 374 | * @return array 375 | */ 376 | public function getComplete() 377 | { 378 | return self::$complete; 379 | } 380 | 381 | /** 382 | * Convenience method to to check if a resque job has a complete status 383 | */ 384 | public function isComplete() 385 | { 386 | return in_array($this->get(), self::$complete); 387 | } 388 | } 389 | -------------------------------------------------------------------------------- /src/Worker.php: -------------------------------------------------------------------------------- 1 | 21 | * @license http://www.opensource.org/licenses/mit-license.php 22 | */ 23 | class Worker implements LoggerAwareInterface 24 | { 25 | /** 26 | * @var string String identifying this worker. 27 | */ 28 | protected $id; 29 | 30 | /** 31 | * @var LoggerInterface Logging object that implements the PSR-3 LoggerInterface 32 | */ 33 | protected $logger; 34 | 35 | /** 36 | * @var array Array of all associated queues for this worker. 37 | */ 38 | protected $queues = array(); 39 | 40 | /** 41 | * Whether the worker should refresh the list of queues on reserve, or just go with the queues it has been given 42 | * 43 | * Passing * as a queue name will cause the worker to listen on all queues, and also refresh them (so that you 44 | * don't need to restart workers when you add a queue) 45 | * 46 | * @var boolean 47 | */ 48 | protected $refreshQueues = false; 49 | 50 | /** 51 | * @var boolean True if on the next iteration, the worker should shutdown. 52 | */ 53 | protected $shutdown = false; 54 | 55 | /** 56 | * @var boolean True if this worker is paused. 57 | */ 58 | protected $paused = false; 59 | 60 | /** 61 | * @var JobInterface Current job, if any, being processed by this worker. 62 | */ 63 | protected $currentJob = null; 64 | 65 | /** 66 | * @var array 67 | */ 68 | protected $options = array(); 69 | 70 | /** 71 | * @var int Process ID of child worker processes. 72 | */ 73 | private $child = null; 74 | 75 | /** 76 | * @var Resque 77 | */ 78 | protected $resque; 79 | 80 | /** 81 | * Instantiate a new worker, given a list of queues that it should be working 82 | * on. The list of queues should be supplied in the priority that they should 83 | * be checked for jobs (first come, first served) 84 | * 85 | * Passing a single '*' allows the worker to work on all queues. 86 | * You can easily add new queues dynamically and have them worked on using this method. 87 | * 88 | * @param Resque $resque 89 | * @param string|array $queues String with a single queue name, array with multiple. 90 | * @param array $options 91 | */ 92 | public function __construct(Resque $resque, $queues, array $options = array()) 93 | { 94 | $this->configure($options); 95 | 96 | $this->queues = is_array($queues) ? $queues : array($queues); 97 | $this->resque = $resque; 98 | $this->logger = $this->resque->getLogger(); 99 | 100 | if (in_array('*', $this->queues) || empty($this->queues)) { 101 | $this->refreshQueues = true; 102 | $this->queues = $resque->queues(); 103 | } 104 | 105 | $this->configureId(); 106 | } 107 | 108 | /** 109 | * Configures options for the worker 110 | * 111 | * @param array $options 112 | * Including 113 | * Worker identification 114 | * - server_name => string, default is FQDN hostname 115 | * - pid => int, default is current PID 116 | * - id_format => string, suitable for sprintf 117 | * - id_location_preg => string, Perl compat regex, gets hostname and PID 118 | * out of worker ID 119 | * - shuffle_queues => bool, whether to shuffle the queues on reserve, so we evenly check all queues 120 | * - sort_queues => bool, whether to check the queues in alphabetical order (mutually exclusive with shuffle_queues) 121 | */ 122 | protected function configure(array $options) 123 | { 124 | $this->options = array_merge(array( 125 | 'server_name' => null, 126 | 'pid' => null, 127 | 'ps' => '/bin/ps', 128 | 'ps_args' => array('-o', 'pid,state', '-p'), 129 | 'id_format' => '%s:%d:%s', 130 | 'id_location_preg' => '/^([^:]+?):([0-9]+):/', 131 | 'shuffle_queues' => true, 132 | 'sort_queues' => false 133 | ), $options); 134 | 135 | if (!$this->options['server_name']) { 136 | $this->options['server_name'] = function_exists('gethostname') ? gethostname() : php_uname('n'); 137 | } 138 | 139 | if (!$this->options['pid']) { 140 | $this->options['pid'] = getmypid(); 141 | } 142 | } 143 | 144 | /** 145 | * Configures the ID of this worker 146 | */ 147 | protected function configureId() 148 | { 149 | $this->id = sprintf( 150 | $this->options['id_format'], 151 | $this->options['server_name'], 152 | $this->options['pid'], 153 | implode(',', $this->queues) 154 | ); 155 | } 156 | 157 | /** 158 | * @return Client\ClientInterface 159 | */ 160 | protected function getClient() 161 | { 162 | return $this->resque->getClient(); 163 | } 164 | 165 | /** 166 | * @param string $queue 167 | * @param array $payload 168 | * @throws JobClassNotFoundException 169 | * @throws JobInvalidException 170 | * @return JobInterface 171 | */ 172 | protected function createJobInstance($queue, array $payload) 173 | { 174 | if (!class_exists($payload['class'])) { 175 | throw new JobClassNotFoundException( 176 | 'Could not find job class ' . $payload['class'] . '.' 177 | ); 178 | } 179 | 180 | if (!is_subclass_of($payload['class'], 'Resque\JobInterface')) { 181 | throw new JobInvalidException(); 182 | } 183 | 184 | $job = new $payload['class']($queue, $payload); 185 | 186 | if (method_exists($job, 'setResque')) { 187 | $job->setResque($this->resque); 188 | } 189 | 190 | return $job; 191 | } 192 | 193 | /** 194 | * Parses a hostname and PID out of a string worker ID 195 | * 196 | * If you change the format of the ID, you should also change the definition 197 | * of this method. 198 | * 199 | * This method *always* parses the ID of the worker, rather than figuring out 200 | * the current processes' PID/hostname. This means you can use setId() to 201 | * interrogate the properties of other workers given their ID. 202 | * 203 | * @throws Exception 204 | * @return array 205 | */ 206 | protected function getLocation() 207 | { 208 | $matches = array(); 209 | 210 | if (!preg_match($this->options['id_location_preg'], $this->getId(), $matches)) { 211 | throw new Exception('Incompatible ID format: unable to determine worker location'); 212 | } 213 | 214 | if (!isset($matches[1]) || !$matches[1]) { 215 | throw new Exception('Invalid ID: invalid hostname'); 216 | } 217 | 218 | if (!isset($matches[2]) || !$matches[2] || !is_numeric($matches[2])) { 219 | throw new Exception('Invalid ID: invalid PID'); 220 | } 221 | 222 | return array($matches[1], (int)$matches[2]); 223 | } 224 | 225 | /** 226 | * Set the ID of this worker to a given ID string. 227 | * 228 | * @param string $id ID for the worker. 229 | */ 230 | public function setId($id) 231 | { 232 | $this->id = $id; 233 | } 234 | 235 | /** 236 | * Gives access to the main Resque system this worker belongs to 237 | * 238 | * @return \Resque\Resque 239 | */ 240 | public function getResque() 241 | { 242 | return $this->resque; 243 | } 244 | 245 | /** 246 | * Given a worker ID, check if it is registered/valid. 247 | * 248 | * @param string $id ID of the worker. 249 | * @return boolean True if the worker exists, false if not. 250 | */ 251 | public function exists($id) 252 | { 253 | return (bool)$this->resque->getClient()->sismember('workers', $id); 254 | } 255 | 256 | /** 257 | * The primary loop for a worker which when called on an instance starts 258 | * the worker's life cycle. 259 | * 260 | * Queues are checked every $interval (seconds) for new jobs. 261 | * 262 | * @param int $interval How often to check for new jobs across the queues. 263 | */ 264 | public function work($interval = 5) 265 | { 266 | $this->updateProcLine('Starting'); 267 | $this->startup(); 268 | 269 | while (true) { 270 | if ($this->shutdown) { 271 | $this->unregister(); 272 | return; 273 | } 274 | 275 | // Attempt to find and reserve a job 276 | $job = false; 277 | if (!$this->paused) { 278 | $job = $this->reserve(); 279 | } 280 | 281 | if (!$job) { 282 | // For an interval of 0, continue now - helps with unit testing etc 283 | if ($interval == 0) { 284 | break; 285 | } 286 | 287 | // If no job was found, we sleep for $interval before continuing and checking again 288 | if ($this->paused) { 289 | $this->updateProcLine('Paused'); 290 | } else { 291 | $this->updateProcLine('Waiting for ' . implode(',', $this->queues)); 292 | } 293 | 294 | usleep($interval * 1000000); 295 | continue; 296 | } 297 | 298 | $this->logger->info('got {job}', array('job' => $job)); 299 | $this->workingOn($job); 300 | 301 | $this->child = null; 302 | $this->child = $this->fork(); 303 | 304 | // Forked and we're the child. Run the job. 305 | if (!$this->child) { 306 | $status = 'Processing ' . $job->getQueue() . ' since ' . strftime('%F %T'); 307 | $this->updateProcLine($status); 308 | $this->logger->notice($status); 309 | $this->perform($job); 310 | 311 | exit(0); 312 | } elseif ($this->child > 0) { 313 | // Parent process, sit and wait 314 | $status = 'Forked ' . $this->child . ' at ' . strftime('%F %T'); 315 | $this->updateProcLine($status); 316 | $this->logger->info($status); 317 | 318 | // Wait until the child process finishes before continuing 319 | pcntl_wait($status); 320 | $exitStatus = pcntl_wexitstatus($status); 321 | 322 | if ($exitStatus !== 0) { 323 | $this->failJob($job, new DirtyExitException( 324 | 'Job exited with exit code ' . $exitStatus 325 | )); 326 | } else { 327 | $this->logger->debug('Job returned status code {code}', array('code' => $exitStatus)); 328 | } 329 | } 330 | 331 | $this->doneWorking(); 332 | } 333 | } 334 | 335 | /** 336 | * Process a single job. 337 | * 338 | * @param JobInterface $job The job to be processed. 339 | */ 340 | public function perform(JobInterface $job) 341 | { 342 | try { 343 | $job->perform(); 344 | } catch (Exception $e) { 345 | $this->logger->notice('{job} failed: {exception}', array( 346 | 'job' => $job, 347 | 'exception' => $e 348 | )); 349 | $this->failJob($job, $e); 350 | return; 351 | } 352 | 353 | try { 354 | $this->resque->getStatusFactory()->forJob($job)->update(Status::STATUS_COMPLETE); 355 | } catch (JobIdException $e) { 356 | $this->logger->warning('Could not mark job complete: no ID in payload - {exception}', array('exception' => $e)); 357 | } 358 | 359 | $payload = $job->getPayload(); 360 | 361 | $this->logger->notice('Finished job {queue}/{class} (ID: {id})', array( 362 | 'queue' => $job->getQueue(), 363 | 'class' => get_class($job), 364 | 'id' => isset($payload['id']) ? $payload['id'] : 'unknown' 365 | )); 366 | 367 | $this->logger->debug('Done with {job}', array('job' => $job)); 368 | } 369 | 370 | /** 371 | * Marks the given job as failed 372 | * 373 | * This happens whenever the job's perform() method emits an exception 374 | * 375 | * @param JobInterface|array $job 376 | * @param Exception $exception 377 | */ 378 | protected function failJob($job, Exception $exception) 379 | { 380 | if ($job instanceof JobInterface) { 381 | $payload = $job->getPayload(); 382 | $queue = $job->getQueue(); 383 | } else { 384 | $payload = $job; 385 | $queue = isset($job['queue']) ? $job['queue'] : null; 386 | } 387 | 388 | $id = isset($job['id']) ? $job['id'] : null; 389 | 390 | if ($id) { 391 | try { 392 | $status = $this->resque->getStatusFactory()->forId($id); 393 | $status->update(Status::STATUS_FAILED); 394 | } catch (JobIdException $e) { 395 | $this->logger->warning($e); 396 | } 397 | } // else no status to update 398 | 399 | $this->resque->getFailureBackend()->receiveFailure( 400 | $payload, 401 | $exception, 402 | $this, 403 | $queue 404 | ); 405 | 406 | $this->getResque()->getStatistic('failed')->incr(); 407 | $this->getStatistic('failed')->incr(); 408 | } 409 | 410 | /** 411 | * Prepares the list of queues for a job to reserved 412 | * 413 | * Updates/sorts/shuffles the array ahead of the call to reserve a job from one of them 414 | * 415 | * @return void 416 | */ 417 | protected function refreshQueues() 418 | { 419 | if ($this->refreshQueues) { 420 | $this->queues = $this->resque->queues(); 421 | } 422 | 423 | if (!$this->queues) { 424 | if ($this->refreshQueues) { 425 | $this->logger->info('Refreshing queues dynamically, but there are no queues yet'); 426 | } else { 427 | $this->logger->notice('Not listening to any queues, and dynamic queue refreshing is disabled'); 428 | $this->shutdownNow(); 429 | } 430 | } 431 | 432 | // Each call to reserve, we check the queues in a different order 433 | if ($this->options['shuffle_queues']) { 434 | shuffle($this->queues); 435 | } elseif ($this->options['sort_queues']) { 436 | sort($this->queues); 437 | } 438 | } 439 | 440 | /** 441 | * Attempt to find a job from the top of one of the queues for this worker. 442 | * 443 | * @return JobInterface|null Instance of JobInterface if a job is found, null if not. 444 | */ 445 | public function reserve() 446 | { 447 | $this->refreshQueues(); 448 | 449 | $this->logger->debug('Attempting to reserve job from {queues}', array( 450 | 'queues' => empty($this->queues) ? 'empty queue list' : implode(', ', $this->queues) 451 | )); 452 | 453 | foreach ($this->queues as $queue) { 454 | $payload = $this->resque->pop($queue); 455 | 456 | if (!is_array($payload)) { 457 | continue; 458 | } 459 | 460 | $payload['queue'] = $queue; 461 | 462 | try { 463 | $job = $this->createJobInstance($queue, $payload); 464 | } catch (ResqueException $exception) { 465 | $this->failJob($payload, $exception); 466 | return null; 467 | } 468 | 469 | if ($job) { 470 | $this->logger->info('Found job on {queue}', array('queue' => $queue)); 471 | return $job; 472 | } 473 | } 474 | 475 | return null; 476 | } 477 | 478 | /** 479 | * Attempt to fork a child process from the parent to run a job in. 480 | * 481 | * Return values are those of pcntl_fork(). 482 | * 483 | * @throws \RuntimeException 484 | * @throws \Exception 485 | * @return int -1 if the fork failed, 0 for the forked child, the PID of the child for the parent. 486 | */ 487 | private function fork() 488 | { 489 | if (!function_exists('pcntl_fork')) { 490 | throw new \Exception('pcntl not available, could not fork'); 491 | } 492 | 493 | // Immediately before a fork, disconnect the redis client 494 | $this->resque->disconnect(); 495 | 496 | $this->logger->notice('Forking...'); 497 | 498 | $pid = (int)pcntl_fork(); 499 | 500 | // And reconnect 501 | $this->resque->connect(); 502 | 503 | if ($pid === -1) { 504 | throw new RuntimeException('Unable to fork child worker.'); 505 | } 506 | 507 | return $pid; 508 | } 509 | 510 | /** 511 | * Perform necessary actions to start a worker. 512 | */ 513 | protected function startup() 514 | { 515 | $this->registerSigHandlers(); 516 | $this->pruneDeadWorkers(); 517 | $this->register(); 518 | } 519 | 520 | /** 521 | * On supported systems (with the PECL proctitle module installed), update 522 | * the name of the currently running process to indicate the current state 523 | * of a worker. 524 | * 525 | * @param string $status The updated process title. 526 | */ 527 | protected function updateProcLine($status) 528 | { 529 | if (function_exists('setproctitle')) { 530 | setproctitle('resque-' . Version::VERSION . ': ' . $status); 531 | } 532 | } 533 | 534 | /** 535 | * Register signal handlers that a worker should respond to. 536 | * 537 | * TERM: Shutdown immediately and stop processing jobs. 538 | * INT: Shutdown immediately and stop processing jobs. 539 | * QUIT: Shutdown after the current job finishes processing. 540 | * USR1: Kill the forked child immediately and continue processing jobs. 541 | */ 542 | protected function registerSigHandlers() 543 | { 544 | if (!function_exists('pcntl_signal')) { 545 | $this->logger->warning('Cannot register signal handlers'); 546 | return; 547 | } 548 | 549 | declare(ticks = 1); 550 | pcntl_signal(SIGTERM, array($this, 'shutDownNow')); 551 | pcntl_signal(SIGINT, array($this, 'shutDownNow')); 552 | pcntl_signal(SIGQUIT, array($this, 'shutdown')); 553 | pcntl_signal(SIGUSR1, array($this, 'killChild')); 554 | pcntl_signal(SIGUSR2, array($this, 'pauseProcessing')); 555 | pcntl_signal(SIGCONT, array($this, 'unPauseProcessing')); 556 | pcntl_signal(SIGPIPE, array($this, 'reestablishRedisConnection')); 557 | 558 | $this->logger->notice('Registered signals'); 559 | } 560 | 561 | /** 562 | * Signal handler callback for USR2, pauses processing of new jobs. 563 | */ 564 | public function pauseProcessing() 565 | { 566 | $this->logger->notice('USR2 received; pausing job processing'); 567 | $this->paused = true; 568 | } 569 | 570 | /** 571 | * Signal handler callback for CONT, resumes worker allowing it to pick 572 | * up new jobs. 573 | */ 574 | public function unPauseProcessing() 575 | { 576 | $this->logger->notice('CONT received; resuming job processing'); 577 | $this->paused = false; 578 | } 579 | 580 | /** 581 | * Signal handler for SIGPIPE, in the event the redis connection has gone away. 582 | * Attempts to reconnect to redis, or raises an Exception. 583 | */ 584 | public function reestablishRedisConnection() 585 | { 586 | $this->logger->notice('SIGPIPE received; attempting to reconnect'); 587 | $this->resque->reconnect(); 588 | } 589 | 590 | /** 591 | * Schedule a worker for shutdown. Will finish processing the current job 592 | * and when the timeout interval is reached, the worker will shut down. 593 | */ 594 | public function shutdown() 595 | { 596 | $this->shutdown = true; 597 | $this->logger->notice('Exiting...'); 598 | } 599 | 600 | /** 601 | * Force an immediate shutdown of the worker, killing any child jobs 602 | * currently running. 603 | */ 604 | public function shutdownNow() 605 | { 606 | $this->shutdown(); 607 | $this->killChild(); 608 | } 609 | 610 | /** 611 | * Kill a forked child job immediately. The job it is processing will not 612 | * be completed. 613 | */ 614 | public function killChild() 615 | { 616 | if (!$this->child) { 617 | $this->logger->notice('No child to kill.'); 618 | return; 619 | } 620 | 621 | $this->logger->notice('Killing child at {pid}', array('pid' => $this->child)); 622 | 623 | $command = escapeshellcmd($this->options['ps']); 624 | 625 | foreach ($this->options['ps_args'] as $arg) { 626 | $command .= ' ' . escapeshellarg($arg); 627 | } 628 | 629 | if (exec('ps ' . $this->child, $output, $returnCode) && $returnCode != 1) { 630 | $this->logger->notice('Killing child at ' . $this->child); 631 | posix_kill($this->child, SIGKILL); 632 | $this->child = null; 633 | } else { 634 | $this->logger->notice('Child ' . $this->child . ' not found, restarting.'); 635 | $this->shutdown(); 636 | } 637 | } 638 | 639 | /** 640 | * Look for any workers which should be running on this server and if 641 | * they're not, remove them from Redis. 642 | * 643 | * This is a form of garbage collection to handle cases where the 644 | * server may have been killed and the Resque workers did not die gracefully 645 | * and therefore leave state information in Redis. 646 | */ 647 | public function pruneDeadWorkers() 648 | { 649 | $pids = $this->resque->getWorkerPids(); 650 | $ids = $this->resque->getWorkerIds(); 651 | 652 | foreach ($ids as $id) { 653 | $worker = clone $this; 654 | $worker->setId($id); 655 | 656 | list($host, $pid) = $worker->getLocation(); 657 | 658 | // Ignore workers on other hosts 659 | if ($host != $this->options['server_name']) { 660 | continue; 661 | } 662 | 663 | // Ignore this process 664 | if ($pid == $this->options['pid']) { 665 | continue; 666 | } 667 | 668 | // Ignore workers still running 669 | if (in_array($pid, $pids)) { 670 | continue; 671 | } 672 | 673 | $this->logger->warning('Pruning dead worker: {id}', array('id' => $id)); 674 | $worker->unregister(); 675 | } 676 | } 677 | 678 | /** 679 | * Gets the ID of this worker 680 | */ 681 | public function getId() 682 | { 683 | return $this->id; 684 | } 685 | 686 | /** 687 | * Register this worker in Redis. 688 | */ 689 | public function register() 690 | { 691 | $this->logger->debug('Registering worker ' . $this->getId()); 692 | $this->resque->getClient()->sadd($this->resque->getKey(Resque::WORKERS_KEY), $this->getId()); 693 | $this->resque->getClient()->set($this->getJobKey() . ':started', strftime('%a %b %d %H:%M:%S %Z %Y')); 694 | } 695 | 696 | /** 697 | * Unregister this worker in Redis. (shutdown etc) 698 | */ 699 | public function unregister() 700 | { 701 | $this->logger->debug('Unregistering worker ' . $this->getId()); 702 | 703 | if ($this->currentJob) { 704 | $this->failJob($this->currentJob, new DirtyExitException()); 705 | } 706 | 707 | $this->resque->getClient()->srem($this->resque->getKey(Resque::WORKERS_KEY), $this->getId()); 708 | $this->resque->getClient()->del($this->getJobKey()); 709 | $this->resque->getClient()->del($this->getJobKey() . ':started'); 710 | 711 | $this->getStatistic('processed')->clear(); 712 | $this->getStatistic('failed')->clear(); 713 | $this->getStatistic('shutdown')->clear(); 714 | } 715 | 716 | /** 717 | * Tell Redis which job we're currently working on. 718 | * 719 | * @param JobInterface $job Job instance we're working on. 720 | */ 721 | public function workingOn(JobInterface $job) 722 | { 723 | if (method_exists($job, 'setWorker')) { 724 | $job->setWorker($this); 725 | } 726 | 727 | $this->currentJob = $job; 728 | 729 | $this->resque->getStatusFactory()->forJob($job)->update(Status::STATUS_RUNNING); 730 | 731 | $data = json_encode(array( 732 | 'queue' => $job->getQueue(), 733 | 'run_at' => strftime('%a %b %d %H:%M:%S %Z %Y'), 734 | 'payload' => $job->getPayload() 735 | )); 736 | 737 | $this->resque->getClient()->set($this->getJobKey(), $data); 738 | } 739 | 740 | /** 741 | * Notify Redis that we've finished working on a job, clearing the working 742 | * state and incrementing the job stats. 743 | */ 744 | public function doneWorking() 745 | { 746 | $this->currentJob = null; 747 | $this->resque->getStatistic('processed')->incr(); 748 | $this->getStatistic('processed')->incr(); 749 | $this->resque->getClient()->del($this->getJobKey()); 750 | } 751 | 752 | /** 753 | * Generate a string representation of this worker. 754 | * 755 | * @return string String identifier for this worker instance. 756 | * @deprecated Just use getId(). Explicit, simpler, less magic. 757 | */ 758 | public function __toString() 759 | { 760 | return $this->id; 761 | } 762 | 763 | /** 764 | * Gets the key for where this worker will store its active job 765 | * 766 | * @return string 767 | */ 768 | protected function getJobKey() 769 | { 770 | return $this->getResque()->getKey('worker:' . $this->getId()); 771 | } 772 | 773 | /** 774 | * Return an object describing the job this worker is currently working on. 775 | * 776 | * @return array Object with details of current job. 777 | */ 778 | public function job() 779 | { 780 | $job = $this->resque->getClient()->get($this->getJobKey()); 781 | 782 | if (!$job) { 783 | return array(); 784 | } else { 785 | return json_decode($job, true); 786 | } 787 | } 788 | 789 | /** 790 | * Get a statistic belonging to this worker. 791 | * 792 | * @param string $name Statistic to fetch. 793 | * @return Statistic 794 | */ 795 | public function getStatistic($name) 796 | { 797 | return new Statistic($this->resque, $name. ':' . $this->getId()); 798 | } 799 | 800 | /** 801 | * Inject the logging object into the worker 802 | * 803 | * @param LoggerInterface $logger 804 | * @return void 805 | */ 806 | public function setLogger(LoggerInterface $logger) 807 | { 808 | $this->logger = $logger; 809 | } 810 | } 811 | -------------------------------------------------------------------------------- /composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", 5 | "This file is @generated automatically" 6 | ], 7 | "hash": "04cb0cf92e92dbc220792caf6141c3c9", 8 | "packages": [ 9 | { 10 | "name": "psr/log", 11 | "version": "1.0.0", 12 | "source": { 13 | "type": "git", 14 | "url": "https://github.com/php-fig/log.git", 15 | "reference": "fe0936ee26643249e916849d48e3a51d5f5e278b" 16 | }, 17 | "dist": { 18 | "type": "zip", 19 | "url": "https://api.github.com/repos/php-fig/log/zipball/fe0936ee26643249e916849d48e3a51d5f5e278b", 20 | "reference": "fe0936ee26643249e916849d48e3a51d5f5e278b", 21 | "shasum": "" 22 | }, 23 | "type": "library", 24 | "autoload": { 25 | "psr-0": { 26 | "Psr\\Log\\": "" 27 | } 28 | }, 29 | "notification-url": "https://packagist.org/downloads/", 30 | "license": [ 31 | "MIT" 32 | ], 33 | "authors": [ 34 | { 35 | "name": "PHP-FIG", 36 | "homepage": "http://www.php-fig.org/" 37 | } 38 | ], 39 | "description": "Common interface for logging libraries", 40 | "keywords": [ 41 | "log", 42 | "psr", 43 | "psr-3" 44 | ], 45 | "time": "2012-12-21 11:40:51" 46 | }, 47 | { 48 | "name": "symfony/console", 49 | "version": "v2.5.6", 50 | "target-dir": "Symfony/Component/Console", 51 | "source": { 52 | "type": "git", 53 | "url": "https://github.com/symfony/Console.git", 54 | "reference": "6f177fca24200a5b97aef5ce7a5c98124a0f0db0" 55 | }, 56 | "dist": { 57 | "type": "zip", 58 | "url": "https://api.github.com/repos/symfony/Console/zipball/6f177fca24200a5b97aef5ce7a5c98124a0f0db0", 59 | "reference": "6f177fca24200a5b97aef5ce7a5c98124a0f0db0", 60 | "shasum": "" 61 | }, 62 | "require": { 63 | "php": ">=5.3.3" 64 | }, 65 | "require-dev": { 66 | "psr/log": "~1.0", 67 | "symfony/event-dispatcher": "~2.1" 68 | }, 69 | "suggest": { 70 | "psr/log": "For using the console logger", 71 | "symfony/event-dispatcher": "" 72 | }, 73 | "type": "library", 74 | "extra": { 75 | "branch-alias": { 76 | "dev-master": "2.5-dev" 77 | } 78 | }, 79 | "autoload": { 80 | "psr-0": { 81 | "Symfony\\Component\\Console\\": "" 82 | } 83 | }, 84 | "notification-url": "https://packagist.org/downloads/", 85 | "license": [ 86 | "MIT" 87 | ], 88 | "authors": [ 89 | { 90 | "name": "Symfony Community", 91 | "homepage": "http://symfony.com/contributors" 92 | }, 93 | { 94 | "name": "Fabien Potencier", 95 | "email": "fabien@symfony.com" 96 | } 97 | ], 98 | "description": "Symfony Console Component", 99 | "homepage": "http://symfony.com", 100 | "time": "2014-10-05 13:57:04" 101 | } 102 | ], 103 | "packages-dev": [ 104 | { 105 | "name": "colinmollenhour/credis", 106 | "version": "1.3", 107 | "source": { 108 | "type": "git", 109 | "url": "https://github.com/colinmollenhour/credis.git", 110 | "reference": "af95629093b98eb8193b3ee18b277109a25e30a2" 111 | }, 112 | "dist": { 113 | "type": "zip", 114 | "url": "https://api.github.com/repos/colinmollenhour/credis/zipball/af95629093b98eb8193b3ee18b277109a25e30a2", 115 | "reference": "af95629093b98eb8193b3ee18b277109a25e30a2", 116 | "shasum": "" 117 | }, 118 | "require": { 119 | "php": ">=5.3.0" 120 | }, 121 | "type": "library", 122 | "autoload": { 123 | "classmap": [ 124 | "Client.php", 125 | "Cluster.php" 126 | ] 127 | }, 128 | "notification-url": "https://packagist.org/downloads/", 129 | "license": [ 130 | "MIT" 131 | ], 132 | "authors": [ 133 | { 134 | "name": "Colin Mollenhour", 135 | "email": "colin@mollenhour.com" 136 | } 137 | ], 138 | "description": "Credis is a lightweight interface to the Redis key-value store which wraps the phpredis library when available for better performance.", 139 | "homepage": "https://github.com/colinmollenhour/credis", 140 | "time": "2013-11-01 22:46:58" 141 | }, 142 | { 143 | "name": "doctrine/instantiator", 144 | "version": "1.0.4", 145 | "source": { 146 | "type": "git", 147 | "url": "https://github.com/doctrine/instantiator.git", 148 | "reference": "f976e5de371104877ebc89bd8fecb0019ed9c119" 149 | }, 150 | "dist": { 151 | "type": "zip", 152 | "url": "https://api.github.com/repos/doctrine/instantiator/zipball/f976e5de371104877ebc89bd8fecb0019ed9c119", 153 | "reference": "f976e5de371104877ebc89bd8fecb0019ed9c119", 154 | "shasum": "" 155 | }, 156 | "require": { 157 | "php": ">=5.3,<8.0-DEV" 158 | }, 159 | "require-dev": { 160 | "athletic/athletic": "~0.1.8", 161 | "ext-pdo": "*", 162 | "ext-phar": "*", 163 | "phpunit/phpunit": "~4.0", 164 | "squizlabs/php_codesniffer": "2.0.*@ALPHA" 165 | }, 166 | "type": "library", 167 | "extra": { 168 | "branch-alias": { 169 | "dev-master": "1.0.x-dev" 170 | } 171 | }, 172 | "autoload": { 173 | "psr-0": { 174 | "Doctrine\\Instantiator\\": "src" 175 | } 176 | }, 177 | "notification-url": "https://packagist.org/downloads/", 178 | "license": [ 179 | "MIT" 180 | ], 181 | "authors": [ 182 | { 183 | "name": "Marco Pivetta", 184 | "email": "ocramius@gmail.com", 185 | "homepage": "http://ocramius.github.com/" 186 | } 187 | ], 188 | "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", 189 | "homepage": "https://github.com/doctrine/instantiator", 190 | "keywords": [ 191 | "constructor", 192 | "instantiate" 193 | ], 194 | "time": "2014-10-13 12:58:55" 195 | }, 196 | { 197 | "name": "monolog/monolog", 198 | "version": "1.7.0", 199 | "source": { 200 | "type": "git", 201 | "url": "https://github.com/Seldaek/monolog.git", 202 | "reference": "6225b22de9dcf36546be3a0b2fa8e3d986153f57" 203 | }, 204 | "dist": { 205 | "type": "zip", 206 | "url": "https://api.github.com/repos/Seldaek/monolog/zipball/6225b22de9dcf36546be3a0b2fa8e3d986153f57", 207 | "reference": "6225b22de9dcf36546be3a0b2fa8e3d986153f57", 208 | "shasum": "" 209 | }, 210 | "require": { 211 | "php": ">=5.3.0", 212 | "psr/log": "~1.0" 213 | }, 214 | "require-dev": { 215 | "aws/aws-sdk-php": "~2.4.8", 216 | "doctrine/couchdb": "dev-master", 217 | "mlehner/gelf-php": "1.0.*", 218 | "phpunit/phpunit": "~3.7.0", 219 | "raven/raven": "0.5.*", 220 | "ruflin/elastica": "0.90.*" 221 | }, 222 | "suggest": { 223 | "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", 224 | "doctrine/couchdb": "Allow sending log messages to a CouchDB server", 225 | "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)", 226 | "ext-mongo": "Allow sending log messages to a MongoDB server", 227 | "mlehner/gelf-php": "Allow sending log messages to a GrayLog2 server", 228 | "raven/raven": "Allow sending log messages to a Sentry server", 229 | "ruflin/elastica": "Allow sending log messages to an Elastic Search server" 230 | }, 231 | "type": "library", 232 | "extra": { 233 | "branch-alias": { 234 | "dev-master": "1.7.x-dev" 235 | } 236 | }, 237 | "autoload": { 238 | "psr-0": { 239 | "Monolog": "src/" 240 | } 241 | }, 242 | "notification-url": "https://packagist.org/downloads/", 243 | "license": [ 244 | "MIT" 245 | ], 246 | "authors": [ 247 | { 248 | "name": "Jordi Boggiano", 249 | "email": "j.boggiano@seld.be", 250 | "homepage": "http://seld.be", 251 | "role": "Developer" 252 | } 253 | ], 254 | "description": "Sends your logs to files, sockets, inboxes, databases and various web services", 255 | "homepage": "http://github.com/Seldaek/monolog", 256 | "keywords": [ 257 | "log", 258 | "logging", 259 | "psr-3" 260 | ], 261 | "time": "2013-11-14 19:48:31" 262 | }, 263 | { 264 | "name": "phpunit/php-code-coverage", 265 | "version": "2.0.11", 266 | "source": { 267 | "type": "git", 268 | "url": "https://github.com/sebastianbergmann/php-code-coverage.git", 269 | "reference": "53603b3c995f5aab6b59c8e08c3a663d2cc810b7" 270 | }, 271 | "dist": { 272 | "type": "zip", 273 | "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/53603b3c995f5aab6b59c8e08c3a663d2cc810b7", 274 | "reference": "53603b3c995f5aab6b59c8e08c3a663d2cc810b7", 275 | "shasum": "" 276 | }, 277 | "require": { 278 | "php": ">=5.3.3", 279 | "phpunit/php-file-iterator": "~1.3", 280 | "phpunit/php-text-template": "~1.2", 281 | "phpunit/php-token-stream": "~1.3", 282 | "sebastian/environment": "~1.0", 283 | "sebastian/version": "~1.0" 284 | }, 285 | "require-dev": { 286 | "ext-xdebug": ">=2.1.4", 287 | "phpunit/phpunit": "~4.1" 288 | }, 289 | "suggest": { 290 | "ext-dom": "*", 291 | "ext-xdebug": ">=2.2.1", 292 | "ext-xmlwriter": "*" 293 | }, 294 | "type": "library", 295 | "extra": { 296 | "branch-alias": { 297 | "dev-master": "2.0.x-dev" 298 | } 299 | }, 300 | "autoload": { 301 | "classmap": [ 302 | "src/" 303 | ] 304 | }, 305 | "notification-url": "https://packagist.org/downloads/", 306 | "include-path": [ 307 | "" 308 | ], 309 | "license": [ 310 | "BSD-3-Clause" 311 | ], 312 | "authors": [ 313 | { 314 | "name": "Sebastian Bergmann", 315 | "email": "sb@sebastian-bergmann.de", 316 | "role": "lead" 317 | } 318 | ], 319 | "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", 320 | "homepage": "https://github.com/sebastianbergmann/php-code-coverage", 321 | "keywords": [ 322 | "coverage", 323 | "testing", 324 | "xunit" 325 | ], 326 | "time": "2014-08-31 06:33:04" 327 | }, 328 | { 329 | "name": "phpunit/php-file-iterator", 330 | "version": "1.3.4", 331 | "source": { 332 | "type": "git", 333 | "url": "https://github.com/sebastianbergmann/php-file-iterator.git", 334 | "reference": "acd690379117b042d1c8af1fafd61bde001bf6bb" 335 | }, 336 | "dist": { 337 | "type": "zip", 338 | "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/acd690379117b042d1c8af1fafd61bde001bf6bb", 339 | "reference": "acd690379117b042d1c8af1fafd61bde001bf6bb", 340 | "shasum": "" 341 | }, 342 | "require": { 343 | "php": ">=5.3.3" 344 | }, 345 | "type": "library", 346 | "autoload": { 347 | "classmap": [ 348 | "File/" 349 | ] 350 | }, 351 | "notification-url": "https://packagist.org/downloads/", 352 | "include-path": [ 353 | "" 354 | ], 355 | "license": [ 356 | "BSD-3-Clause" 357 | ], 358 | "authors": [ 359 | { 360 | "name": "Sebastian Bergmann", 361 | "email": "sb@sebastian-bergmann.de", 362 | "role": "lead" 363 | } 364 | ], 365 | "description": "FilterIterator implementation that filters files based on a list of suffixes.", 366 | "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", 367 | "keywords": [ 368 | "filesystem", 369 | "iterator" 370 | ], 371 | "time": "2013-10-10 15:34:57" 372 | }, 373 | { 374 | "name": "phpunit/php-text-template", 375 | "version": "1.2.0", 376 | "source": { 377 | "type": "git", 378 | "url": "https://github.com/sebastianbergmann/php-text-template.git", 379 | "reference": "206dfefc0ffe9cebf65c413e3d0e809c82fbf00a" 380 | }, 381 | "dist": { 382 | "type": "zip", 383 | "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/206dfefc0ffe9cebf65c413e3d0e809c82fbf00a", 384 | "reference": "206dfefc0ffe9cebf65c413e3d0e809c82fbf00a", 385 | "shasum": "" 386 | }, 387 | "require": { 388 | "php": ">=5.3.3" 389 | }, 390 | "type": "library", 391 | "autoload": { 392 | "classmap": [ 393 | "Text/" 394 | ] 395 | }, 396 | "notification-url": "https://packagist.org/downloads/", 397 | "include-path": [ 398 | "" 399 | ], 400 | "license": [ 401 | "BSD-3-Clause" 402 | ], 403 | "authors": [ 404 | { 405 | "name": "Sebastian Bergmann", 406 | "email": "sb@sebastian-bergmann.de", 407 | "role": "lead" 408 | } 409 | ], 410 | "description": "Simple template engine.", 411 | "homepage": "https://github.com/sebastianbergmann/php-text-template/", 412 | "keywords": [ 413 | "template" 414 | ], 415 | "time": "2014-01-30 17:20:04" 416 | }, 417 | { 418 | "name": "phpunit/php-timer", 419 | "version": "1.0.5", 420 | "source": { 421 | "type": "git", 422 | "url": "https://github.com/sebastianbergmann/php-timer.git", 423 | "reference": "19689d4354b295ee3d8c54b4f42c3efb69cbc17c" 424 | }, 425 | "dist": { 426 | "type": "zip", 427 | "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/19689d4354b295ee3d8c54b4f42c3efb69cbc17c", 428 | "reference": "19689d4354b295ee3d8c54b4f42c3efb69cbc17c", 429 | "shasum": "" 430 | }, 431 | "require": { 432 | "php": ">=5.3.3" 433 | }, 434 | "type": "library", 435 | "autoload": { 436 | "classmap": [ 437 | "PHP/" 438 | ] 439 | }, 440 | "notification-url": "https://packagist.org/downloads/", 441 | "include-path": [ 442 | "" 443 | ], 444 | "license": [ 445 | "BSD-3-Clause" 446 | ], 447 | "authors": [ 448 | { 449 | "name": "Sebastian Bergmann", 450 | "email": "sb@sebastian-bergmann.de", 451 | "role": "lead" 452 | } 453 | ], 454 | "description": "Utility class for timing", 455 | "homepage": "https://github.com/sebastianbergmann/php-timer/", 456 | "keywords": [ 457 | "timer" 458 | ], 459 | "time": "2013-08-02 07:42:54" 460 | }, 461 | { 462 | "name": "phpunit/php-token-stream", 463 | "version": "1.3.0", 464 | "source": { 465 | "type": "git", 466 | "url": "https://github.com/sebastianbergmann/php-token-stream.git", 467 | "reference": "f8d5d08c56de5cfd592b3340424a81733259a876" 468 | }, 469 | "dist": { 470 | "type": "zip", 471 | "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/f8d5d08c56de5cfd592b3340424a81733259a876", 472 | "reference": "f8d5d08c56de5cfd592b3340424a81733259a876", 473 | "shasum": "" 474 | }, 475 | "require": { 476 | "ext-tokenizer": "*", 477 | "php": ">=5.3.3" 478 | }, 479 | "require-dev": { 480 | "phpunit/phpunit": "~4.2" 481 | }, 482 | "type": "library", 483 | "extra": { 484 | "branch-alias": { 485 | "dev-master": "1.3-dev" 486 | } 487 | }, 488 | "autoload": { 489 | "classmap": [ 490 | "src/" 491 | ] 492 | }, 493 | "notification-url": "https://packagist.org/downloads/", 494 | "license": [ 495 | "BSD-3-Clause" 496 | ], 497 | "authors": [ 498 | { 499 | "name": "Sebastian Bergmann", 500 | "email": "sebastian@phpunit.de" 501 | } 502 | ], 503 | "description": "Wrapper around PHP's tokenizer extension.", 504 | "homepage": "https://github.com/sebastianbergmann/php-token-stream/", 505 | "keywords": [ 506 | "tokenizer" 507 | ], 508 | "time": "2014-08-31 06:12:13" 509 | }, 510 | { 511 | "name": "phpunit/phpunit", 512 | "version": "4.3.5", 513 | "source": { 514 | "type": "git", 515 | "url": "https://github.com/sebastianbergmann/phpunit.git", 516 | "reference": "2dab9d593997db4abcf58d0daf798eb4e9cecfe1" 517 | }, 518 | "dist": { 519 | "type": "zip", 520 | "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/2dab9d593997db4abcf58d0daf798eb4e9cecfe1", 521 | "reference": "2dab9d593997db4abcf58d0daf798eb4e9cecfe1", 522 | "shasum": "" 523 | }, 524 | "require": { 525 | "ext-dom": "*", 526 | "ext-json": "*", 527 | "ext-pcre": "*", 528 | "ext-reflection": "*", 529 | "ext-spl": "*", 530 | "php": ">=5.3.3", 531 | "phpunit/php-code-coverage": "~2.0", 532 | "phpunit/php-file-iterator": "~1.3.2", 533 | "phpunit/php-text-template": "~1.2", 534 | "phpunit/php-timer": "~1.0.2", 535 | "phpunit/phpunit-mock-objects": "~2.3", 536 | "sebastian/comparator": "~1.0", 537 | "sebastian/diff": "~1.1", 538 | "sebastian/environment": "~1.0", 539 | "sebastian/exporter": "~1.0", 540 | "sebastian/version": "~1.0", 541 | "symfony/yaml": "~2.0" 542 | }, 543 | "suggest": { 544 | "phpunit/php-invoker": "~1.1" 545 | }, 546 | "bin": [ 547 | "phpunit" 548 | ], 549 | "type": "library", 550 | "extra": { 551 | "branch-alias": { 552 | "dev-master": "4.3.x-dev" 553 | } 554 | }, 555 | "autoload": { 556 | "classmap": [ 557 | "src/" 558 | ] 559 | }, 560 | "notification-url": "https://packagist.org/downloads/", 561 | "include-path": [ 562 | "", 563 | "../../symfony/yaml/" 564 | ], 565 | "license": [ 566 | "BSD-3-Clause" 567 | ], 568 | "authors": [ 569 | { 570 | "name": "Sebastian Bergmann", 571 | "email": "sebastian@phpunit.de", 572 | "role": "lead" 573 | } 574 | ], 575 | "description": "The PHP Unit Testing framework.", 576 | "homepage": "http://www.phpunit.de/", 577 | "keywords": [ 578 | "phpunit", 579 | "testing", 580 | "xunit" 581 | ], 582 | "time": "2014-11-11 10:11:09" 583 | }, 584 | { 585 | "name": "phpunit/phpunit-mock-objects", 586 | "version": "2.3.0", 587 | "source": { 588 | "type": "git", 589 | "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git", 590 | "reference": "c63d2367247365f688544f0d500af90a11a44c65" 591 | }, 592 | "dist": { 593 | "type": "zip", 594 | "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/c63d2367247365f688544f0d500af90a11a44c65", 595 | "reference": "c63d2367247365f688544f0d500af90a11a44c65", 596 | "shasum": "" 597 | }, 598 | "require": { 599 | "doctrine/instantiator": "~1.0,>=1.0.1", 600 | "php": ">=5.3.3", 601 | "phpunit/php-text-template": "~1.2" 602 | }, 603 | "require-dev": { 604 | "phpunit/phpunit": "~4.3" 605 | }, 606 | "suggest": { 607 | "ext-soap": "*" 608 | }, 609 | "type": "library", 610 | "extra": { 611 | "branch-alias": { 612 | "dev-master": "2.3.x-dev" 613 | } 614 | }, 615 | "autoload": { 616 | "classmap": [ 617 | "src/" 618 | ] 619 | }, 620 | "notification-url": "https://packagist.org/downloads/", 621 | "license": [ 622 | "BSD-3-Clause" 623 | ], 624 | "authors": [ 625 | { 626 | "name": "Sebastian Bergmann", 627 | "email": "sb@sebastian-bergmann.de", 628 | "role": "lead" 629 | } 630 | ], 631 | "description": "Mock Object library for PHPUnit", 632 | "homepage": "https://github.com/sebastianbergmann/phpunit-mock-objects/", 633 | "keywords": [ 634 | "mock", 635 | "xunit" 636 | ], 637 | "time": "2014-10-03 05:12:11" 638 | }, 639 | { 640 | "name": "predis/predis", 641 | "version": "v1.0.0", 642 | "source": { 643 | "type": "git", 644 | "url": "https://github.com/nrk/predis.git", 645 | "reference": "d4be306d0aca28b5633b96adef03b29fea569c2f" 646 | }, 647 | "dist": { 648 | "type": "zip", 649 | "url": "https://api.github.com/repos/nrk/predis/zipball/d4be306d0aca28b5633b96adef03b29fea569c2f", 650 | "reference": "d4be306d0aca28b5633b96adef03b29fea569c2f", 651 | "shasum": "" 652 | }, 653 | "require": { 654 | "php": ">=5.3.2" 655 | }, 656 | "require-dev": { 657 | "phpunit/phpunit": "~4.0" 658 | }, 659 | "suggest": { 660 | "ext-curl": "Allows access to Webdis when paired with phpiredis", 661 | "ext-phpiredis": "Allows faster serialization and deserialization of the Redis protocol" 662 | }, 663 | "type": "library", 664 | "extra": { 665 | "branch-alias": { 666 | "dev-master": "1.0-dev" 667 | } 668 | }, 669 | "autoload": { 670 | "psr-4": { 671 | "Predis\\": "src/" 672 | } 673 | }, 674 | "notification-url": "https://packagist.org/downloads/", 675 | "license": [ 676 | "MIT" 677 | ], 678 | "authors": [ 679 | { 680 | "name": "Daniele Alessandri", 681 | "email": "suppakilla@gmail.com", 682 | "homepage": "http://clorophilla.net" 683 | } 684 | ], 685 | "description": "Flexible and feature-complete PHP client library for Redis", 686 | "homepage": "http://github.com/nrk/predis", 687 | "keywords": [ 688 | "nosql", 689 | "predis", 690 | "redis" 691 | ], 692 | "time": "2014-08-01 09:59:50" 693 | }, 694 | { 695 | "name": "sebastian/comparator", 696 | "version": "1.0.1", 697 | "source": { 698 | "type": "git", 699 | "url": "https://github.com/sebastianbergmann/comparator.git", 700 | "reference": "e54a01c0da1b87db3c5a3c4c5277ddf331da4aef" 701 | }, 702 | "dist": { 703 | "type": "zip", 704 | "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/e54a01c0da1b87db3c5a3c4c5277ddf331da4aef", 705 | "reference": "e54a01c0da1b87db3c5a3c4c5277ddf331da4aef", 706 | "shasum": "" 707 | }, 708 | "require": { 709 | "php": ">=5.3.3", 710 | "sebastian/diff": "~1.1", 711 | "sebastian/exporter": "~1.0" 712 | }, 713 | "require-dev": { 714 | "phpunit/phpunit": "~4.1" 715 | }, 716 | "type": "library", 717 | "extra": { 718 | "branch-alias": { 719 | "dev-master": "1.0.x-dev" 720 | } 721 | }, 722 | "autoload": { 723 | "classmap": [ 724 | "src/" 725 | ] 726 | }, 727 | "notification-url": "https://packagist.org/downloads/", 728 | "license": [ 729 | "BSD-3-Clause" 730 | ], 731 | "authors": [ 732 | { 733 | "name": "Jeff Welch", 734 | "email": "whatthejeff@gmail.com" 735 | }, 736 | { 737 | "name": "Volker Dusch", 738 | "email": "github@wallbash.com" 739 | }, 740 | { 741 | "name": "Bernhard Schussek", 742 | "email": "bschussek@2bepublished.at" 743 | }, 744 | { 745 | "name": "Sebastian Bergmann", 746 | "email": "sebastian@phpunit.de" 747 | } 748 | ], 749 | "description": "Provides the functionality to compare PHP values for equality", 750 | "homepage": "http://www.github.com/sebastianbergmann/comparator", 751 | "keywords": [ 752 | "comparator", 753 | "compare", 754 | "equality" 755 | ], 756 | "time": "2014-05-11 23:00:21" 757 | }, 758 | { 759 | "name": "sebastian/diff", 760 | "version": "1.2.0", 761 | "source": { 762 | "type": "git", 763 | "url": "https://github.com/sebastianbergmann/diff.git", 764 | "reference": "5843509fed39dee4b356a306401e9dd1a931fec7" 765 | }, 766 | "dist": { 767 | "type": "zip", 768 | "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/5843509fed39dee4b356a306401e9dd1a931fec7", 769 | "reference": "5843509fed39dee4b356a306401e9dd1a931fec7", 770 | "shasum": "" 771 | }, 772 | "require": { 773 | "php": ">=5.3.3" 774 | }, 775 | "require-dev": { 776 | "phpunit/phpunit": "~4.2" 777 | }, 778 | "type": "library", 779 | "extra": { 780 | "branch-alias": { 781 | "dev-master": "1.2-dev" 782 | } 783 | }, 784 | "autoload": { 785 | "classmap": [ 786 | "src/" 787 | ] 788 | }, 789 | "notification-url": "https://packagist.org/downloads/", 790 | "license": [ 791 | "BSD-3-Clause" 792 | ], 793 | "authors": [ 794 | { 795 | "name": "Kore Nordmann", 796 | "email": "mail@kore-nordmann.de" 797 | }, 798 | { 799 | "name": "Sebastian Bergmann", 800 | "email": "sebastian@phpunit.de" 801 | } 802 | ], 803 | "description": "Diff implementation", 804 | "homepage": "http://www.github.com/sebastianbergmann/diff", 805 | "keywords": [ 806 | "diff" 807 | ], 808 | "time": "2014-08-15 10:29:00" 809 | }, 810 | { 811 | "name": "sebastian/environment", 812 | "version": "1.2.0", 813 | "source": { 814 | "type": "git", 815 | "url": "https://github.com/sebastianbergmann/environment.git", 816 | "reference": "0d9bf79554d2a999da194a60416c15cf461eb67d" 817 | }, 818 | "dist": { 819 | "type": "zip", 820 | "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/0d9bf79554d2a999da194a60416c15cf461eb67d", 821 | "reference": "0d9bf79554d2a999da194a60416c15cf461eb67d", 822 | "shasum": "" 823 | }, 824 | "require": { 825 | "php": ">=5.3.3" 826 | }, 827 | "require-dev": { 828 | "phpunit/phpunit": "~4.3" 829 | }, 830 | "type": "library", 831 | "extra": { 832 | "branch-alias": { 833 | "dev-master": "1.2.x-dev" 834 | } 835 | }, 836 | "autoload": { 837 | "classmap": [ 838 | "src/" 839 | ] 840 | }, 841 | "notification-url": "https://packagist.org/downloads/", 842 | "license": [ 843 | "BSD-3-Clause" 844 | ], 845 | "authors": [ 846 | { 847 | "name": "Sebastian Bergmann", 848 | "email": "sebastian@phpunit.de" 849 | } 850 | ], 851 | "description": "Provides functionality to handle HHVM/PHP environments", 852 | "homepage": "http://www.github.com/sebastianbergmann/environment", 853 | "keywords": [ 854 | "Xdebug", 855 | "environment", 856 | "hhvm" 857 | ], 858 | "time": "2014-10-22 06:38:05" 859 | }, 860 | { 861 | "name": "sebastian/exporter", 862 | "version": "1.0.2", 863 | "source": { 864 | "type": "git", 865 | "url": "https://github.com/sebastianbergmann/exporter.git", 866 | "reference": "c7d59948d6e82818e1bdff7cadb6c34710eb7dc0" 867 | }, 868 | "dist": { 869 | "type": "zip", 870 | "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/c7d59948d6e82818e1bdff7cadb6c34710eb7dc0", 871 | "reference": "c7d59948d6e82818e1bdff7cadb6c34710eb7dc0", 872 | "shasum": "" 873 | }, 874 | "require": { 875 | "php": ">=5.3.3" 876 | }, 877 | "require-dev": { 878 | "phpunit/phpunit": "~4.0" 879 | }, 880 | "type": "library", 881 | "extra": { 882 | "branch-alias": { 883 | "dev-master": "1.0.x-dev" 884 | } 885 | }, 886 | "autoload": { 887 | "classmap": [ 888 | "src/" 889 | ] 890 | }, 891 | "notification-url": "https://packagist.org/downloads/", 892 | "license": [ 893 | "BSD-3-Clause" 894 | ], 895 | "authors": [ 896 | { 897 | "name": "Jeff Welch", 898 | "email": "whatthejeff@gmail.com" 899 | }, 900 | { 901 | "name": "Volker Dusch", 902 | "email": "github@wallbash.com" 903 | }, 904 | { 905 | "name": "Bernhard Schussek", 906 | "email": "bschussek@2bepublished.at" 907 | }, 908 | { 909 | "name": "Sebastian Bergmann", 910 | "email": "sebastian@phpunit.de" 911 | }, 912 | { 913 | "name": "Adam Harvey", 914 | "email": "aharvey@php.net" 915 | } 916 | ], 917 | "description": "Provides the functionality to export PHP variables for visualization", 918 | "homepage": "http://www.github.com/sebastianbergmann/exporter", 919 | "keywords": [ 920 | "export", 921 | "exporter" 922 | ], 923 | "time": "2014-09-10 00:51:36" 924 | }, 925 | { 926 | "name": "sebastian/version", 927 | "version": "1.0.3", 928 | "source": { 929 | "type": "git", 930 | "url": "https://github.com/sebastianbergmann/version.git", 931 | "reference": "b6e1f0cf6b9e1ec409a0d3e2f2a5fb0998e36b43" 932 | }, 933 | "dist": { 934 | "type": "zip", 935 | "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/b6e1f0cf6b9e1ec409a0d3e2f2a5fb0998e36b43", 936 | "reference": "b6e1f0cf6b9e1ec409a0d3e2f2a5fb0998e36b43", 937 | "shasum": "" 938 | }, 939 | "type": "library", 940 | "autoload": { 941 | "classmap": [ 942 | "src/" 943 | ] 944 | }, 945 | "notification-url": "https://packagist.org/downloads/", 946 | "license": [ 947 | "BSD-3-Clause" 948 | ], 949 | "authors": [ 950 | { 951 | "name": "Sebastian Bergmann", 952 | "email": "sebastian@phpunit.de", 953 | "role": "lead" 954 | } 955 | ], 956 | "description": "Library that helps with managing the version number of Git-hosted PHP projects", 957 | "homepage": "https://github.com/sebastianbergmann/version", 958 | "time": "2014-03-07 15:35:33" 959 | }, 960 | { 961 | "name": "symfony/yaml", 962 | "version": "v2.5.6", 963 | "target-dir": "Symfony/Component/Yaml", 964 | "source": { 965 | "type": "git", 966 | "url": "https://github.com/symfony/Yaml.git", 967 | "reference": "2d9f527449cabfa8543dd7fa3a466d6ae83d6726" 968 | }, 969 | "dist": { 970 | "type": "zip", 971 | "url": "https://api.github.com/repos/symfony/Yaml/zipball/2d9f527449cabfa8543dd7fa3a466d6ae83d6726", 972 | "reference": "2d9f527449cabfa8543dd7fa3a466d6ae83d6726", 973 | "shasum": "" 974 | }, 975 | "require": { 976 | "php": ">=5.3.3" 977 | }, 978 | "type": "library", 979 | "extra": { 980 | "branch-alias": { 981 | "dev-master": "2.5-dev" 982 | } 983 | }, 984 | "autoload": { 985 | "psr-0": { 986 | "Symfony\\Component\\Yaml\\": "" 987 | } 988 | }, 989 | "notification-url": "https://packagist.org/downloads/", 990 | "license": [ 991 | "MIT" 992 | ], 993 | "authors": [ 994 | { 995 | "name": "Symfony Community", 996 | "homepage": "http://symfony.com/contributors" 997 | }, 998 | { 999 | "name": "Fabien Potencier", 1000 | "email": "fabien@symfony.com" 1001 | } 1002 | ], 1003 | "description": "Symfony Yaml Component", 1004 | "homepage": "http://symfony.com", 1005 | "time": "2014-10-01 05:50:18" 1006 | } 1007 | ], 1008 | "aliases": [], 1009 | "minimum-stability": "stable", 1010 | "stability-flags": [], 1011 | "prefer-stable": false, 1012 | "prefer-lowest": false, 1013 | "platform": { 1014 | "php": ">=5.3.7", 1015 | "ext-pcntl": "*" 1016 | }, 1017 | "platform-dev": [] 1018 | } 1019 | --------------------------------------------------------------------------------