├── src ├── Exception │ ├── Cancel.php │ ├── Zombie.php │ ├── Dirty.php │ └── Shutdown.php ├── Socket │ ├── SocketException.php │ ├── Client.php │ └── Server.php ├── Helpers │ ├── ClosureJob.php │ ├── CatchOutput.php │ ├── Table.php │ ├── Stats.php │ ├── Util.php │ └── SerializableClosure.php ├── Logger │ ├── Handler │ │ ├── Connector │ │ │ ├── OffConnector.php │ │ │ ├── CouchDBConnector.php │ │ │ ├── ErrorLogConnector.php │ │ │ ├── SyslogConnector.php │ │ │ ├── StreamConnector.php │ │ │ ├── SocketConnector.php │ │ │ ├── RotateConnector.php │ │ │ ├── CubeConnector.php │ │ │ ├── ConsoleConnector.php │ │ │ ├── AmqpConnector.php │ │ │ ├── MongoDBConnector.php │ │ │ ├── RedisConnector.php │ │ │ ├── ConnectorInterface.php │ │ │ └── AbstractConnector.php │ │ ├── ConsoleHandler.php │ │ └── Connector.php │ ├── Formatter │ │ └── ConsoleFormatter.php │ └── Processor │ │ ├── ConsoleProcessor.php │ │ └── StripFormatProcessor.php ├── Blueprint │ └── Job.php ├── Console │ ├── Command │ │ ├── Hosts.php │ │ ├── Cleanup.php │ │ ├── Clear.php │ │ ├── Queues.php │ │ ├── Workers.php │ │ ├── Worker │ │ │ ├── Pause.php │ │ │ ├── Resume.php │ │ │ ├── Stop.php │ │ │ ├── Cancel.php │ │ │ ├── Start.php │ │ │ └── Restart.php │ │ ├── Socket │ │ │ ├── Send.php │ │ │ ├── Connect.php │ │ │ └── Receive.php │ │ ├── Job │ │ │ └── Queue.php │ │ ├── SpeedTest.php │ │ └── Command.php │ └── ResqueApplication.php ├── Resque.php ├── Host.php ├── Logger.php ├── Event.php ├── Queue.php ├── Config.php └── Redis.php ├── LICENSE ├── bin └── resque ├── autoload.php ├── composer.json ├── CHANGELOG.md └── README.md /src/Exception/Cancel.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Resque\Exception; 13 | 14 | /** 15 | * Resque cancel job exception 16 | * 17 | * @package Resque 18 | * @author Michael Haynes 19 | */ 20 | final class Cancel extends \Exception 21 | { 22 | } 23 | -------------------------------------------------------------------------------- /src/Exception/Zombie.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Resque\Exception; 13 | 14 | /** 15 | * Resque zombie job exception 16 | * 17 | * @package Resque 18 | * @author Michael Haynes 19 | */ 20 | final class Zombie extends \Exception 21 | { 22 | } 23 | -------------------------------------------------------------------------------- /src/Socket/SocketException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Resque\Socket; 13 | 14 | /** 15 | * Socket Exceptions 16 | * 17 | * @package Resque 18 | * @author Michael Haynes 19 | */ 20 | class SocketException extends \Exception 21 | { 22 | } 23 | -------------------------------------------------------------------------------- /src/Exception/Dirty.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Resque\Exception; 13 | 14 | /** 15 | * Resque dirty job fail exception 16 | * 17 | * @package Resque 18 | * @author Michael Haynes 19 | */ 20 | final class Dirty extends \RuntimeException 21 | { 22 | } 23 | -------------------------------------------------------------------------------- /src/Exception/Shutdown.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Resque\Exception; 13 | 14 | /** 15 | * Resque shutdown worker job exception 16 | * 17 | * @package Resque 18 | * @author Michael Haynes 19 | */ 20 | final class Shutdown extends \RuntimeException 21 | { 22 | } 23 | -------------------------------------------------------------------------------- /src/Helpers/ClosureJob.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Resque\Helpers; 13 | 14 | /** 15 | * Executes stored closure job 16 | * 17 | * @package Resque 18 | * @author Michael Haynes 19 | */ 20 | final class ClosureJob 21 | { 22 | /** 23 | * Fire the Closure based queue job. 24 | * 25 | * @return void 26 | */ 27 | public function perform(array $data, \Resque\Job $job): void 28 | { 29 | $closure = unserialize($data['closure']); 30 | $closure($job); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Logger/Handler/Connector/OffConnector.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Resque\Logger\Handler\Connector; 13 | 14 | use Monolog\Handler\NullHandler; 15 | use Symfony\Component\Console\Command\Command; 16 | use Symfony\Component\Console\Input\InputInterface; 17 | use Symfony\Component\Console\Output\OutputInterface; 18 | 19 | /** 20 | * Null monolog connector class 21 | * 22 | * @package Resque 23 | * @author Michael Haynes 24 | */ 25 | class OffConnector extends AbstractConnector 26 | { 27 | public function resolve(Command $command, InputInterface $input, OutputInterface $output, array $args): NullHandler 28 | { 29 | return new NullHandler(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Logger/Handler/Connector/CouchDBConnector.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Resque\Logger\Handler\Connector; 13 | 14 | use Monolog\Handler\CouchDBHandler; 15 | use Symfony\Component\Console\Command\Command; 16 | use Symfony\Component\Console\Input\InputInterface; 17 | use Symfony\Component\Console\Output\OutputInterface; 18 | 19 | /** 20 | * CouchDB monolog connector class 21 | * 22 | * @package Resque 23 | * @author Michael Haynes 24 | */ 25 | class CouchDBConnector extends AbstractConnector 26 | { 27 | public function resolve(Command $command, InputInterface $input, OutputInterface $output, array $args): CouchDBHandler 28 | { 29 | return new CouchDBHandler($args); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Logger/Handler/Connector/ErrorLogConnector.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Resque\Logger\Handler\Connector; 13 | 14 | use Monolog\Handler\ErrorLogHandler; 15 | use Symfony\Component\Console\Command\Command; 16 | use Symfony\Component\Console\Input\InputInterface; 17 | use Symfony\Component\Console\Output\OutputInterface; 18 | 19 | /** 20 | * Error log monolog connector class 21 | * 22 | * @package Resque 23 | * @author Michael Haynes 24 | */ 25 | class ErrorLogConnector extends AbstractConnector 26 | { 27 | public function resolve(Command $command, InputInterface $input, OutputInterface $output, array $args): ErrorLogHandler 28 | { 29 | return new ErrorLogHandler($args['type']); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Logger/Handler/Connector/SyslogConnector.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Resque\Logger\Handler\Connector; 13 | 14 | use Monolog\Handler\SyslogHandler; 15 | use Symfony\Component\Console\Command\Command; 16 | use Symfony\Component\Console\Input\InputInterface; 17 | use Symfony\Component\Console\Output\OutputInterface; 18 | 19 | /** 20 | * Syslog monolog connector class 21 | * 22 | * @package Resque 23 | * @author Michael Haynes 24 | */ 25 | class SyslogConnector extends AbstractConnector 26 | { 27 | public function resolve(Command $command, InputInterface $input, OutputInterface $output, array $args): SyslogHandler 28 | { 29 | return new SyslogHandler($args['ident'], $args['facility']); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Logger/Handler/Connector/StreamConnector.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Resque\Logger\Handler\Connector; 13 | 14 | use Monolog\Handler\StreamHandler; 15 | use Symfony\Component\Console\Command\Command; 16 | use Symfony\Component\Console\Input\InputInterface; 17 | use Symfony\Component\Console\Output\OutputInterface; 18 | 19 | /** 20 | * Stream monolog connector class 21 | * 22 | * @package Resque 23 | * @author Michael Haynes 24 | */ 25 | class StreamConnector extends AbstractConnector 26 | { 27 | public function resolve(Command $command, InputInterface $input, OutputInterface $output, array $args): StreamHandler 28 | { 29 | return new StreamHandler($this->replacePlaceholders($args['stream'])); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Logger/Formatter/ConsoleFormatter.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Resque\Logger\Formatter; 13 | 14 | use Monolog\Formatter\LineFormatter; 15 | 16 | /** 17 | * Formatter for console output 18 | * 19 | * @package Resque 20 | * @author Michael Haynes 21 | */ 22 | class ConsoleFormatter extends LineFormatter 23 | { 24 | public const SIMPLE_FORMAT = "%start_tag%%message%%end_tag%\n"; 25 | 26 | /** 27 | * {@inheritdoc} 28 | */ 29 | public function format(array $record): string 30 | { 31 | $tag = strtolower($record['level_name']); 32 | 33 | $record['start_tag'] = '<'.$tag.'>'; 34 | $record['end_tag'] = ''; 35 | 36 | return parent::format($record); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Logger/Handler/Connector/SocketConnector.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Resque\Logger\Handler\Connector; 13 | 14 | use Monolog\Handler\SocketHandler; 15 | use Symfony\Component\Console\Command\Command; 16 | use Symfony\Component\Console\Input\InputInterface; 17 | use Symfony\Component\Console\Output\OutputInterface; 18 | 19 | /** 20 | * Socket monolog connector class 21 | * 22 | * @package Resque 23 | * @author Michael Haynes 24 | */ 25 | class SocketConnector extends AbstractConnector 26 | { 27 | public function resolve(Command $command, InputInterface $input, OutputInterface $output, array $args): SocketHandler 28 | { 29 | return new SocketHandler($this->replacePlaceholders($args['connection'])); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Logger/Handler/Connector/RotateConnector.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Resque\Logger\Handler\Connector; 13 | 14 | use Monolog\Handler\RotatingFileHandler; 15 | use Symfony\Component\Console\Command\Command; 16 | use Symfony\Component\Console\Input\InputInterface; 17 | use Symfony\Component\Console\Output\OutputInterface; 18 | 19 | /** 20 | * Rotating file monolog connector class 21 | * 22 | * @package Resque 23 | * @author Michael Haynes 24 | */ 25 | class RotateConnector extends AbstractConnector 26 | { 27 | public function resolve(Command $command, InputInterface $input, OutputInterface $output, array $args): RotatingFileHandler 28 | { 29 | return new RotatingFileHandler($this->replacePlaceholders($args['file']), $args['max_files']); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Logger/Handler/Connector/CubeConnector.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Resque\Logger\Handler\Connector; 13 | 14 | use Monolog\Handler\CubeHandler; 15 | use Symfony\Component\Console\Command\Command; 16 | use Symfony\Component\Console\Input\InputInterface; 17 | use Symfony\Component\Console\Output\OutputInterface; 18 | 19 | /** 20 | * Cube monolog connector class 21 | * 22 | * @package Resque 23 | * @author Michael Haynes 24 | * @deprecated Since 4.0.0, Cube appears abandoned and thus support for its connector will be dropped in the future 25 | */ 26 | class CubeConnector extends AbstractConnector 27 | { 28 | public function resolve(Command $command, InputInterface $input, OutputInterface $output, array $args): CubeHandler 29 | { 30 | return new CubeHandler($this->replacePlaceholders($args['url'])); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Michael Haynes 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /src/Logger/Handler/Connector/ConsoleConnector.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Resque\Logger\Handler\Connector; 13 | 14 | use Resque\Logger\Handler\ConsoleHandler; 15 | use Resque\Logger\Processor\ConsoleProcessor; 16 | use Symfony\Component\Console\Command\Command; 17 | use Symfony\Component\Console\Input\InputInterface; 18 | use Symfony\Component\Console\Output\OutputInterface; 19 | 20 | /** 21 | * Console monolog connector class 22 | * 23 | * @package Resque 24 | * @author Michael Haynes 25 | */ 26 | class ConsoleConnector extends AbstractConnector 27 | { 28 | public function resolve(Command $command, InputInterface $input, OutputInterface $output, array $args): ConsoleHandler 29 | { 30 | return new ConsoleHandler($output); 31 | } 32 | 33 | public function processor(Command $command, InputInterface $input, OutputInterface $output, array $args): ConsoleProcessor 34 | { 35 | return new ConsoleProcessor($command, $input, $output); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /bin/resque: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | if (!ini_get('date.timezone') or ! date_default_timezone_get()) { 14 | date_default_timezone_set('UTC'); 15 | } 16 | 17 | define('RESQUE_BIN_DIR', realpath(__DIR__)); 18 | define('RESQUE_DIR', realpath(dirname(RESQUE_BIN_DIR))); 19 | 20 | $files = [ 21 | RESQUE_DIR . '/vendor/autoload.php', // composer dependency 22 | RESQUE_DIR . '/../../autoload.php' // stand-alone package 23 | ]; 24 | 25 | $loaded = false; 26 | 27 | foreach ($files as $file) { 28 | if (is_file($file)) { 29 | require_once $file; 30 | $loaded = true; 31 | break; 32 | } 33 | } 34 | 35 | if (!$loaded) { 36 | exit( 37 | 'You need to set up the project dependencies using the following commands:' . PHP_EOL . 38 | 'curl -sS https://getcomposer.org/installer | php' . PHP_EOL . 39 | 'php composer.phar install' . PHP_EOL 40 | ); 41 | } 42 | 43 | $application = new Resque\Console\ResqueApplication(); 44 | $application->run(); 45 | -------------------------------------------------------------------------------- /src/Blueprint/Job.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Resque\Blueprint; 15 | 16 | /** 17 | * Blueprint for all jobs to extend. 18 | * 19 | * @package Resque 20 | * @author Paul Litovka 21 | */ 22 | abstract class Job 23 | { 24 | /** 25 | * Runs any required logic before the job is performed. 26 | * 27 | * @param \Resque\Job $job Current job instance 28 | */ 29 | public function setUp(\Resque\Job $job): void 30 | { 31 | } 32 | 33 | /** 34 | * Actual job logic. 35 | * 36 | * @param array $args Arguments passed to the job 37 | * @param \Resque\Job $job Current job instance 38 | */ 39 | abstract public function perform(array $args, \Resque\Job $job): void; 40 | 41 | /** 42 | * Runs after the job is performed. 43 | * 44 | * @param \Resque\Job $job Current job instance 45 | */ 46 | public function tearDown(\Resque\Job $job): void 47 | { 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Logger/Handler/Connector/AmqpConnector.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Resque\Logger\Handler\Connector; 13 | 14 | use Monolog\Handler\AmqpHandler; 15 | use Symfony\Component\Console\Command\Command; 16 | use Symfony\Component\Console\Input\InputInterface; 17 | use Symfony\Component\Console\Output\OutputInterface; 18 | 19 | /** 20 | * Amqp monolog connector class 21 | * 22 | * @package Resque 23 | * @author Michael Haynes 24 | */ 25 | class AmqpConnector extends AbstractConnector 26 | { 27 | public function resolve(Command $command, InputInterface $input, OutputInterface $output, array $args): AmqpHandler 28 | { 29 | $options = array_merge([ 30 | 'host' => 'localhost', 31 | 'port' => 5763, 32 | 'login' => null, 33 | 'password' => null, 34 | ], $args); 35 | 36 | $conn = new \AMQPConnection($options); 37 | $conn->connect(); 38 | 39 | $channel = new \AMQPChannel($conn); 40 | 41 | return new AmqpHandler(new \AMQPExchange($channel), $this->replacePlaceholders($args['name'])); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Logger/Handler/Connector/MongoDBConnector.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Resque\Logger\Handler\Connector; 13 | 14 | use MongoDB\Client; 15 | use Monolog\Handler\MongoDBHandler; 16 | use Symfony\Component\Console\Command\Command; 17 | use Symfony\Component\Console\Input\InputInterface; 18 | use Symfony\Component\Console\Output\OutputInterface; 19 | 20 | /** 21 | * MongoDB monolog connector class 22 | * 23 | * @package Resque 24 | * @author Michael Haynes 25 | */ 26 | class MongoDBConnector extends AbstractConnector 27 | { 28 | public function resolve(Command $command, InputInterface $input, OutputInterface $output, array $args): MongoDBHandler 29 | { 30 | $dsn = strtr('mongodb://host:port', $args); 31 | $options = []; 32 | 33 | if (!class_exists(Client::class)) { 34 | throw new \RuntimeException('The MongoDB PHP extension is not installed. Please install mongodb/mongodb and ext-mongodb.'); 35 | } 36 | 37 | $mongodb = new Client($dsn, $options); 38 | 39 | return new MongoDBHandler($mongodb, $this->replacePlaceholders($args['dbname']), $this->replacePlaceholders($args['collection'])); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /autoload.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | /** 13 | * Autoload file for php-resque speedtest feature. 14 | */ 15 | 16 | use Monolog\Handler\StreamHandler; 17 | use Resque\Event; 18 | use Resque\Logger; 19 | use Resque\Resque; 20 | 21 | // Test job class 22 | class TestJob 23 | { 24 | public function perform($args): void 25 | { 26 | // Don't do anything 27 | } 28 | } 29 | 30 | $logger = new Logger([new StreamHandler('php://stdout')]); 31 | 32 | // Lets record the forking time 33 | Event::listen([Event::WORKER_FORK, Event::WORKER_FORK_CHILD], function ($event, $job) use ($logger): void { 34 | static $start = 0; 35 | 36 | if ($event === Event::WORKER_FORK_CHILD) { 37 | $exec = microtime(true) - $start; 38 | $logger->log('Forking process took '.round($exec * 1000, 2).'ms', Logger::DEBUG); 39 | } else { 40 | $start = microtime(true); 41 | } 42 | }); 43 | 44 | // When the job is about to be run, queue another one 45 | Event::listen(Event::JOB_PERFORM, function ($event, $job) use ($logger): void { 46 | Resque::push('TestJob'); 47 | }); 48 | 49 | // Add a few jobs to the default queue 50 | for ($i = 0; $i < 10; $i++) { 51 | Resque::push('TestJob'); 52 | } 53 | -------------------------------------------------------------------------------- /src/Console/Command/Hosts.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Resque\Console\Command; 13 | 14 | use Resque\Host; 15 | use Resque\Redis; 16 | use Symfony\Component\Console\Input\InputInterface; 17 | use Symfony\Component\Console\Output\OutputInterface; 18 | 19 | /** 20 | * Hosts command 21 | * 22 | * @package Resque 23 | * @author Michael Haynes 24 | */ 25 | final class Hosts extends Command 26 | { 27 | protected function configure(): void 28 | { 29 | $this->setName('hosts') 30 | ->setDefinition($this->mergeDefinitions([])) 31 | ->setDescription('List hosts with running workers') 32 | ->setHelp('List hosts with running workers'); 33 | } 34 | 35 | protected function execute(InputInterface $input, OutputInterface $output): int 36 | { 37 | $hosts = Redis::instance()->smembers(Host::redisKey()); 38 | 39 | if (empty($hosts)) { 40 | $this->log('There are no hosts with running workers.'); 41 | return Command::FAILURE; 42 | } 43 | 44 | $table = new \Resque\Helpers\Table($this); 45 | $table->setHeaders(['#', 'Hostname', '# workers']); 46 | 47 | foreach ($hosts as $i => $hostname) { 48 | $host = new Host($hostname); 49 | $workers = Redis::instance()->scard(Host::redisKey($host)); 50 | 51 | $table->addRow([$i + 1, $hostname, $workers]); 52 | } 53 | 54 | $this->log((string)$table); 55 | 56 | return Command::SUCCESS; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Logger/Handler/Connector/RedisConnector.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Resque\Logger\Handler\Connector; 13 | 14 | use Monolog\Handler\RedisHandler; 15 | use Resque\Config; 16 | use Resque\Redis; 17 | use Symfony\Component\Console\Command\Command; 18 | use Symfony\Component\Console\Input\InputInterface; 19 | use Symfony\Component\Console\Output\OutputInterface; 20 | 21 | /** 22 | * Redis monolog connector class 23 | * 24 | * @package Resque 25 | * @author Michael Haynes 26 | */ 27 | class RedisConnector extends AbstractConnector 28 | { 29 | public function resolve(Command $command, InputInterface $input, OutputInterface $output, array $args): RedisHandler 30 | { 31 | $options = [ 32 | 'scheme' => 'tcp', 33 | 'host' => $args['host'], 34 | 'port' => $args['port'], 35 | ]; 36 | 37 | $password = Config::read('redis.password', Redis::DEFAULT_PASSWORD); 38 | if ($password !== null && $password !== false && trim($password) !== '') { 39 | $options['password'] = $password; 40 | } 41 | 42 | $redis = new \Predis\Client($options); 43 | 44 | $namespace = Config::read('redis.namespace', Redis::DEFAULT_NS); 45 | if (substr($namespace, -1) !== ':') { 46 | $namespace .= ':'; 47 | } 48 | 49 | $key = $this->replacePlaceholders($args['key']); 50 | if (strpos($key, $namespace) !== 0) { 51 | $key = $namespace.$key; 52 | } 53 | 54 | return new RedisHandler($redis, $key); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Helpers/CatchOutput.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Resque\Helpers; 13 | 14 | use Symfony\Component\Console\Formatter\OutputFormatterInterface; 15 | 16 | /** 17 | * Catch output and store 18 | * 19 | * @package Resque 20 | * @author Michael Haynes 21 | */ 22 | final class CatchOutput extends \Symfony\Component\Console\Output\Output 23 | { 24 | /** 25 | * @var string Stored output string 26 | */ 27 | protected $written = ''; 28 | 29 | /** 30 | * {@inheritdoc} 31 | */ 32 | public function __construct( 33 | int $verbosity = self::VERBOSITY_NORMAL, 34 | bool $decorated = true, 35 | ?OutputFormatterInterface $formatter = null 36 | ) { 37 | parent::__construct($verbosity, $decorated, $formatter); 38 | } 39 | 40 | /** 41 | * {@inheritdoc} 42 | */ 43 | public function write($messages, bool $newline = false, $type = self::OUTPUT_RAW): void 44 | { 45 | parent::write($messages, $newline, $type); 46 | } 47 | 48 | /** 49 | * Stores message in a local string 50 | * 51 | * @param string $message A message to write to the output 52 | * @param bool $newline Whether to add a newline or not 53 | */ 54 | protected function doWrite(string $message, bool $newline): void 55 | { 56 | $this->written .= $message.($newline ? PHP_EOL : ''); 57 | } 58 | 59 | /** 60 | * Returns written string so far 61 | * 62 | * @return string 63 | */ 64 | public function written(): string 65 | { 66 | return $this->written; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Logger/Processor/ConsoleProcessor.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Resque\Logger\Processor; 13 | 14 | use Resque\Console\Command\Command; 15 | use Symfony\Component\Console\Input\InputInterface; 16 | use Symfony\Component\Console\Output\OutputInterface; 17 | 18 | /** 19 | * Process output for console display 20 | * 21 | * @package Resque 22 | * @author Michael Haynes 23 | */ 24 | class ConsoleProcessor 25 | { 26 | /** 27 | * @var Command command instance 28 | */ 29 | protected $command; 30 | 31 | /** 32 | * @var InputInterface input instance 33 | */ 34 | protected $input; 35 | 36 | /** 37 | * @var OutputInterface output instance 38 | */ 39 | protected $output; 40 | 41 | /** 42 | * Create a new instance 43 | */ 44 | public function __construct(Command $command, InputInterface $input, OutputInterface $output) 45 | { 46 | $this->command = $command; 47 | $this->input = $input; 48 | $this->output = $output; 49 | } 50 | 51 | /** 52 | * @param array $record 53 | * 54 | * @return array 55 | */ 56 | public function __invoke(array $record): array 57 | { 58 | if ($this->command->pollingConsoleOutput()) { 59 | if ($this->output->getVerbosity() >= OutputInterface::VERBOSITY_VERBOSE) { 60 | $record['message'] = sprintf('** [%s] %s', date('H:i:s Y-m-d'), $record['message']); 61 | } else { 62 | $record['message'] = sprintf('** %s', $record['message']); 63 | } 64 | } 65 | 66 | return $record; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Logger/Handler/Connector/ConnectorInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Resque\Logger\Handler\Connector; 13 | 14 | use Monolog\Handler\AbstractProcessingHandler; 15 | use Symfony\Component\Console\Command\Command; 16 | use Symfony\Component\Console\Input\InputInterface; 17 | use Symfony\Component\Console\Output\OutputInterface; 18 | 19 | /** 20 | * Monolog connector interface class 21 | * 22 | * @package Resque 23 | * @author Michael Haynes 24 | */ 25 | interface ConnectorInterface 26 | { 27 | /** 28 | * Resolves the handler class 29 | * 30 | * @param Command $command 31 | * @param InputInterface $input 32 | * @param OutputInterface $output 33 | * @param array $args 34 | * 35 | * @return AbstractProcessingHandler|\Monolog\Handler\NullHandler 36 | */ 37 | public function resolve(Command $command, InputInterface $input, OutputInterface $output, array $args); 38 | 39 | /** 40 | * Returns the processor for this handler 41 | * 42 | * @param Command $command 43 | * @param InputInterface $input 44 | * @param OutputInterface $output 45 | * @param array $args 46 | * 47 | * @return callable 48 | */ 49 | public function processor(Command $command, InputInterface $input, OutputInterface $output, array $args); 50 | 51 | /** 52 | * Replaces all instances of [%host%, %worker%, %pid%, %date%, %time%] 53 | * in logger target key so can be unique log per worker 54 | * 55 | * @param string $string Input string 56 | * 57 | * @return string 58 | */ 59 | public function replacePlaceholders(string $string): string; 60 | } 61 | -------------------------------------------------------------------------------- /src/Console/Command/Cleanup.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Resque\Console\Command; 13 | 14 | use Symfony\Component\Console\Input\InputInterface; 15 | use Symfony\Component\Console\Output\OutputInterface; 16 | 17 | /** 18 | * Clean up hosts and workers from Redis 19 | * 20 | * @package Resque 21 | * @author Michael Haynes 22 | */ 23 | final class Cleanup extends Command 24 | { 25 | protected function configure(): void 26 | { 27 | $this->setName('cleanup') 28 | ->setDefinition($this->mergeDefinitions([])) 29 | ->setDescription('Cleans up php-resque data, removing dead hosts, workers and jobs') 30 | ->setHelp('Cleans up php-resque data, removing dead hosts, workers and jobs'); 31 | } 32 | 33 | protected function execute(InputInterface $input, OutputInterface $output): int 34 | { 35 | $host = new \Resque\Host(); 36 | $cleaned_hosts = $host->cleanup(); 37 | 38 | $worker = new \Resque\Worker('*'); 39 | $cleaned_workers = $worker->cleanup(); 40 | $cleaned_hosts = array_merge_recursive($cleaned_hosts, $host->cleanup()); 41 | 42 | $cleaned_jobs = \Resque\Job::cleanup(); 43 | 44 | $this->log('Cleaned hosts: '.json_encode($cleaned_hosts['hosts']).''); 45 | $this->log('Cleaned workers: '.json_encode(array_merge($cleaned_hosts['workers'], $cleaned_workers)).''); 46 | $this->log('Cleaned '.$cleaned_jobs['zombie'].' zombie job'.($cleaned_jobs['zombie'] == 1 ? '' : 's')); 47 | $this->log('Cleared '.$cleaned_jobs['processed'].' processed job'.($cleaned_jobs['processed'] == 1 ? '' : 's')); 48 | 49 | return Command::SUCCESS; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Console/Command/Clear.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Resque\Console\Command; 13 | 14 | use Symfony\Component\Console\Input\InputInterface; 15 | use Symfony\Component\Console\Input\InputOption; 16 | use Symfony\Component\Console\Output\OutputInterface; 17 | use Symfony\Component\Console\Question\ConfirmationQuestion; 18 | 19 | /** 20 | * Deletes all resque data from redis 21 | * 22 | * @package Resque 23 | * @author Michael Haynes 24 | */ 25 | final class Clear extends Command 26 | { 27 | protected function configure(): void 28 | { 29 | $this->setName('clear') 30 | ->setDefinition($this->mergeDefinitions([ 31 | new InputOption('force', 'f', InputOption::VALUE_NONE, 'Force without asking.'), 32 | ])) 33 | ->setDescription('Clears all php-resque data from Redis') 34 | ->setHelp('Clears all php-resque data from Redis'); 35 | } 36 | 37 | protected function execute(InputInterface $input, OutputInterface $output): int 38 | { 39 | $helper = $this->getHelper('question'); 40 | 41 | $question = new ConfirmationQuestion('Continuing will clear all php-resque data from Redis. Are you sure? ', false); 42 | 43 | if ($input->getOption('force') || $helper->ask($input, $output, $question)) { 44 | $output->write('Clearing Redis php-resque data... '); 45 | 46 | $redis = \Resque\Redis::instance(); 47 | 48 | $keys = $redis->keys('*'); 49 | foreach ($keys as $key) { 50 | $redis->del($key); 51 | } 52 | 53 | $output->writeln('Done.'); 54 | 55 | return Command::SUCCESS; 56 | } 57 | 58 | return Command::SUCCESS; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "library", 3 | "license": "MIT", 4 | "name": "mjphaynes/php-resque", 5 | "description": "Redis backed library for creating background jobs and processing them later.", 6 | "homepage": "https://github.com/mjphaynes/php-resque/", 7 | "prefer-stable": true, 8 | "keywords": [ 9 | "job", 10 | "background", 11 | "redis", 12 | "resque", 13 | "php", 14 | "php-resque", 15 | "queue", 16 | "worker" 17 | ], 18 | "authors": [ 19 | { 20 | "name": "Michael Haynes", 21 | "email": "mike@mjphaynes.com" 22 | } 23 | ], 24 | "require": { 25 | "php": "^7.2|^8.0", 26 | "ext-pcntl": "*", 27 | "monolog/monolog": "^2.5", 28 | "predis/predis": "^2.1", 29 | "symfony/console": "^5.4|^6.0" 30 | }, 31 | "suggest": { 32 | "ext-mongodb": "For using the MongoDB logger.", 33 | "ext-phpiredis": "For using native Redis connectivity.", 34 | "mongodb/mongodb": "For using the MongoDB logger.", 35 | "symfony/process": "For using the speed test command.", 36 | "symfony/yaml": "For using the YAML configuration file." 37 | }, 38 | "require-dev": { 39 | "phpunit/phpunit": "^8.0|^9.0", 40 | "mongodb/mongodb": "^1.0", 41 | "symfony/process": "^5.4|^6.0", 42 | "symfony/yaml": "^5.4|^6.0" 43 | }, 44 | "autoload": { 45 | "psr-4": { 46 | "Resque\\": "src" 47 | } 48 | }, 49 | "autoload-dev": { 50 | "psr-4": { 51 | "Tests\\": "tests" 52 | } 53 | }, 54 | "bin": [ 55 | "bin/resque" 56 | ], 57 | "scripts": { 58 | "cs-ci": "tools/php-cs-fixer fix --verbose", 59 | "cs-fix": "tools/php-cs-fixer fix --verbose", 60 | "test": "phpunit" 61 | }, 62 | "config": { 63 | "optimize-autoloader": true, 64 | "preferred-install": "dist", 65 | "sort-packages": true 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Console/Command/Queues.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Resque\Console\Command; 13 | 14 | use Resque\Queue; 15 | use Resque\Redis; 16 | use Symfony\Component\Console\Input\InputInterface; 17 | use Symfony\Component\Console\Output\OutputInterface; 18 | 19 | /** 20 | * Queues command class 21 | * 22 | * @package Resque 23 | * @author Michael Haynes 24 | */ 25 | final class Queues extends Command 26 | { 27 | protected function configure(): void 28 | { 29 | $this->setName('queues') 30 | ->setDefinition($this->mergeDefinitions([])) 31 | ->setDescription('Get queue statistics') 32 | ->setHelp('Get queue statistics'); 33 | } 34 | 35 | protected function execute(InputInterface $input, OutputInterface $output): int 36 | { 37 | $queues = Redis::instance()->smembers('queues'); 38 | 39 | if (empty($queues)) { 40 | $this->log('There are no queues.'); 41 | return Command::FAILURE; 42 | } 43 | 44 | $table = new \Resque\Helpers\Table($this); 45 | $table->setHeaders(['#', 'Name', 'Queued', 'Delayed', 'Processed', 'Failed', 'Cancelled', 'Total']); 46 | 47 | foreach ($queues as $i => $queue) { 48 | $stats = Redis::instance()->hgetall(Queue::redisKey($queue, 'stats')); 49 | 50 | $table->addRow([ 51 | $i + 1, $queue, 52 | (int)@$stats['queued'], 53 | (int)@$stats['delayed'], 54 | (int)@$stats['processed'], 55 | (int)@$stats['failed'], 56 | (int)@$stats['cancelled'], 57 | (int)@$stats['total'], 58 | ]); 59 | } 60 | 61 | $this->log((string)$table); 62 | 63 | return Command::SUCCESS; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Logger/Handler/Connector/AbstractConnector.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Resque\Logger\Handler\Connector; 13 | 14 | use Resque\Logger\Processor\StripFormatProcessor; 15 | use Symfony\Component\Console\Command\Command; 16 | use Symfony\Component\Console\Input\InputInterface; 17 | use Symfony\Component\Console\Output\OutputInterface; 18 | 19 | /** 20 | * Abstract monolog connector class 21 | * 22 | * @package Resque 23 | * @author Michael Haynes 24 | */ 25 | abstract class AbstractConnector implements ConnectorInterface 26 | { 27 | /** 28 | * The default processor is the StripFormatProcessor which 29 | * removes all the console colour formatting from the string 30 | * 31 | * @param Command $command 32 | * @param InputInterface $input 33 | * @param OutputInterface $output 34 | * @param array $args 35 | * 36 | * @return StripFormatProcessor 37 | */ 38 | public function processor(Command $command, InputInterface $input, OutputInterface $output, array $args) 39 | { 40 | return new StripFormatProcessor($command, $input, $output); 41 | } 42 | 43 | /** 44 | * Replaces all instances of [%host%, %worker%, %pid%, %date%, %time%] 45 | * in logger target key so can be unique log per worker 46 | * 47 | * @param string $string Input string 48 | * 49 | * @return string 50 | */ 51 | public function replacePlaceholders(string $string): string 52 | { 53 | $placeholders = [ 54 | '%host%' => new \Resque\Host(), 55 | '%worker%' => new \Resque\Worker(), 56 | '%pid%' => getmypid(), 57 | '%date%' => date('Y-m-d'), 58 | '%time%' => date('H:i'), 59 | ]; 60 | 61 | return strtr($string, $placeholders); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Helpers/Table.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Resque\Helpers; 13 | 14 | use Symfony\Component\Console\Helper\Table as TableHelper; 15 | use Symfony\Component\Console\Helper\TableStyle; 16 | 17 | /** 18 | * Wrapper for Symfony table helper 19 | * 20 | * @package Resque 21 | * @author Michael Haynes 22 | */ 23 | final class Table 24 | { 25 | /** 26 | * @var TableHelper 27 | */ 28 | protected $table; 29 | 30 | /** 31 | * @var CatchOutput 32 | */ 33 | protected $output; 34 | 35 | /** 36 | * Render the table and pass the output back. 37 | * This is done this way because the table 38 | * helper dumps everything to the output and 39 | * there is no way to catch so have to override 40 | * with a special output. 41 | */ 42 | public function __construct() 43 | { 44 | $this->output = new CatchOutput(); 45 | 46 | $this->table = new TableHelper($this->output); 47 | $style = new TableStyle(); 48 | $style->setCellHeaderFormat('%s'); 49 | $this->table->setStyle($style); 50 | } 51 | 52 | /** 53 | * Render the table and pass the output back. 54 | * This is done this way because the table 55 | * helper dumps everything to the output and 56 | * there is no way to catch so have to override 57 | * with a special output. 58 | * 59 | * @return string 60 | */ 61 | public function __toString(): string 62 | { 63 | $this->table->render($this->output); 64 | 65 | return rtrim($this->output->written()); // Remove trailing \n 66 | } 67 | 68 | /** 69 | * Pass all called functions to the table helper 70 | * 71 | * @param string $method 72 | * @param array $parameters 73 | */ 74 | public function __call(string $method, array $parameters) 75 | { 76 | return call_user_func_array([$this->table, $method], $parameters); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/Logger/Processor/StripFormatProcessor.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Resque\Logger\Processor; 13 | 14 | use Symfony\Component\Console\Command\Command; 15 | use Symfony\Component\Console\Input\InputInterface; 16 | use Symfony\Component\Console\Output\OutputInterface; 17 | 18 | /** 19 | * Format output for non-console output 20 | * 21 | * @package Resque 22 | * @author Michael Haynes 23 | */ 24 | class StripFormatProcessor 25 | { 26 | /** 27 | * @var Command command instance 28 | */ 29 | protected $command; 30 | 31 | /** 32 | * @var InputInterface input instance 33 | */ 34 | protected $input; 35 | 36 | /** 37 | * @var OutputInterface output instance 38 | */ 39 | protected $output; 40 | 41 | /** 42 | * @var array list of formatting tags to strip out 43 | */ 44 | private $stripTags = [ 45 | 'info', 46 | 'notice', 47 | 'warning', 48 | 'debug', 49 | 'error', 50 | 'critical', 51 | 'alert', 52 | 'emergency', 53 | 'pop', 54 | 'warn', 55 | 'comment', 56 | 'question', 57 | ]; 58 | 59 | /** 60 | * Create a new instance 61 | */ 62 | public function __construct(Command $command, InputInterface $input, OutputInterface $output) 63 | { 64 | $this->command = $command; 65 | $this->input = $input; 66 | $this->output = $output; 67 | } 68 | 69 | /** 70 | * @param array $record 71 | * 72 | * @return array 73 | */ 74 | public function __invoke(array $record): array 75 | { 76 | static $find = []; 77 | 78 | if (empty($find)) { 79 | foreach ($this->stripTags as $tag) { 80 | $find[] = '<'.$tag.'>'; 81 | $find[] = ''; 82 | } 83 | } 84 | 85 | $record['message'] = str_replace($find, '', $record['message']); 86 | 87 | return $record; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/Console/Command/Workers.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Resque\Console\Command; 13 | 14 | use Resque\Helpers\Util; 15 | use Resque\Worker; 16 | use Symfony\Component\Console\Input\InputInterface; 17 | use Symfony\Component\Console\Output\OutputInterface; 18 | 19 | /** 20 | * Workers command class 21 | * 22 | * @package Resque 23 | * @author Michael Haynes 24 | */ 25 | final class Workers extends Command 26 | { 27 | protected function configure(): void 28 | { 29 | $this->setName('workers') 30 | ->setAliases(['worker:list']) 31 | ->setDefinition($this->mergeDefinitions([])) 32 | ->setDescription('List all running workers on host') 33 | ->setHelp('List all running workers on host'); 34 | } 35 | 36 | protected function execute(InputInterface $input, OutputInterface $output): int 37 | { 38 | $workers = Worker::hostWorkers(); 39 | 40 | if (empty($workers)) { 41 | $this->log('There are no workers on this host.'); 42 | return Command::FAILURE; 43 | } 44 | 45 | $table = new \Resque\Helpers\Table($this); 46 | $table->setHeaders(['#', 'Status', 'ID', 'Running for', 'Running job', 'P', 'C', 'F', 'Interval', 'Timeout', 'Memory (Limit)']); 47 | 48 | foreach ($workers as $i => $worker) { 49 | $packet = $worker->getPacket(); 50 | 51 | $table->addRow([ 52 | $i + 1, 53 | Worker::$statusText[$packet['status']], 54 | (string)$worker, 55 | Util::human_time_diff($packet['started']), 56 | (!empty($packet['job_id']) ? $packet['job_id'].' for '.Util::human_time_diff($packet['job_started']) : '-'), 57 | $packet['processed'], 58 | $packet['cancelled'], 59 | $packet['failed'], 60 | $packet['interval'], 61 | $packet['timeout'], 62 | Util::bytes($packet['memory']).' ('.$packet['memory_limit'].' MB)', 63 | ]); 64 | } 65 | 66 | $this->log((string)$table); 67 | 68 | return Command::SUCCESS; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Console/Command/Worker/Pause.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Resque\Console\Command\Worker; 13 | 14 | use Resque\Console\Command\Command; 15 | use Resque\Worker; 16 | use Symfony\Component\Console\Input\InputArgument; 17 | use Symfony\Component\Console\Input\InputInterface; 18 | use Symfony\Component\Console\Output\OutputInterface; 19 | 20 | /** 21 | * Worker pause command class 22 | * 23 | * @package Resque 24 | * @author Michael Haynes 25 | */ 26 | final class Pause extends Command 27 | { 28 | protected function configure(): void 29 | { 30 | $this->setName('worker:pause') 31 | ->setDefinition($this->mergeDefinitions([ 32 | new InputArgument('id', InputArgument::OPTIONAL, 'The id of the worker to pause (optional; if not present pauses all workers).'), 33 | ])) 34 | ->setDescription('Pause a running worker. If no worker id set then pauses all workers') 35 | ->setHelp('Pause a running worker. If no worker id set then pauses all workers'); 36 | } 37 | 38 | protected function execute(InputInterface $input, OutputInterface $output): int 39 | { 40 | $id = $input->getArgument('id'); 41 | 42 | // Do a cleanup 43 | $worker = new Worker('*'); 44 | $worker->cleanup(); 45 | 46 | if ($id) { 47 | if (false === ($worker = Worker::hostWorker($id))) { 48 | $this->log('There is no worker with id "'.$id.'".', \Resque\Logger::ERROR); 49 | return Command::FAILURE; 50 | } 51 | 52 | $workers = [$worker]; 53 | } else { 54 | $workers = Worker::hostWorkers(); 55 | } 56 | 57 | if (!count($workers)) { 58 | $this->log('There are no workers on this host.'); 59 | } 60 | 61 | foreach ($workers as $worker) { 62 | if (posix_kill($worker->getPid(), SIGUSR2)) { 63 | $this->log('Worker '.$worker.' USR2 signal sent.'); 64 | } else { 65 | $this->log('Worker '.$worker.' could not send USR2 signal.'); 66 | } 67 | } 68 | 69 | return Command::SUCCESS; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Console/Command/Worker/Resume.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Resque\Console\Command\Worker; 13 | 14 | use Resque\Console\Command\Command; 15 | use Resque\Worker; 16 | use Symfony\Component\Console\Input\InputArgument; 17 | use Symfony\Component\Console\Input\InputInterface; 18 | use Symfony\Component\Console\Output\OutputInterface; 19 | 20 | /** 21 | * Worker resume command class 22 | * 23 | * @package Resque 24 | * @author Michael Haynes 25 | */ 26 | final class Resume extends Command 27 | { 28 | protected function configure(): void 29 | { 30 | $this->setName('worker:resume') 31 | ->setDefinition($this->mergeDefinitions([ 32 | new InputArgument('id', InputArgument::OPTIONAL, 'The id of the worker to resume (optional; if not present resumes all workers).'), 33 | ])) 34 | ->setDescription('Resume a running worker. If no worker id set then resumes all workers') 35 | ->setHelp('Resume a running worker. If no worker id set then resumes all workers'); 36 | } 37 | 38 | protected function execute(InputInterface $input, OutputInterface $output): int 39 | { 40 | $id = $input->getArgument('id'); 41 | 42 | // Do a cleanup 43 | $worker = new Worker('*'); 44 | $worker->cleanup(); 45 | 46 | if ($id) { 47 | if (false === ($worker = Worker::hostWorker($id))) { 48 | $this->log('There is no worker with id "'.$id.'".', \Resque\Logger::ERROR); 49 | return Command::FAILURE; 50 | } 51 | 52 | $workers = [$worker]; 53 | } else { 54 | $workers = Worker::hostWorkers(); 55 | } 56 | 57 | if (!count($workers)) { 58 | $this->log('There are no workers on this host'); 59 | } 60 | 61 | foreach ($workers as $worker) { 62 | if (posix_kill($worker->getPid(), SIGCONT)) { 63 | $this->log('Worker '.$worker.' CONT signal sent.'); 64 | } else { 65 | $this->log('Worker '.$worker.' could not send CONT signal.'); 66 | } 67 | } 68 | 69 | return Command::SUCCESS; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Helpers/Stats.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Resque\Helpers; 13 | 14 | use Resque\Redis; 15 | 16 | /** 17 | * Stats recording 18 | * 19 | * @package Resque 20 | * @author Michael Haynes 21 | */ 22 | final class Stats 23 | { 24 | public const DEFAULT_KEY = 'stats'; 25 | 26 | /** 27 | * Get the value of the supplied statistic counter for the specified statistic 28 | * 29 | * @param string $stat The name of the statistic to get the stats for 30 | * @param string $key The stat key to use 31 | * @return int Value of the statistic. 32 | */ 33 | public static function get(string $stat, string $key = Stats::DEFAULT_KEY): int 34 | { 35 | return (int)Redis::instance()->hget($key, $stat); 36 | } 37 | 38 | /** 39 | * Increment the value of the specified statistic by a certain amount (default is 1) 40 | * 41 | * @param string $stat The name of the statistic to increment 42 | * @param int $by The amount to increment the statistic by 43 | * @param string $key The stat key to use 44 | * @return bool True if successful, false if not. 45 | */ 46 | public static function incr(string $stat, int $by = 1, string $key = Stats::DEFAULT_KEY): bool 47 | { 48 | return (bool)Redis::instance()->hincrby($key, $stat, $by); 49 | } 50 | 51 | /** 52 | * Decrement the value of the specified statistic by a certain amount (default is -1) 53 | * 54 | * @param string $stat The name of the statistic to decrement. 55 | * @param int $by The amount to decrement the statistic by. 56 | * @param string $key The stat key to use 57 | * @return bool True if successful, false if not. 58 | */ 59 | public static function decr(string $stat, int $by = 1, string $key = Stats::DEFAULT_KEY): bool 60 | { 61 | return (bool)Redis::instance()->hincrby($key, $stat, -1 * $by); 62 | } 63 | 64 | /** 65 | * Delete a statistic with the given name. 66 | * 67 | * @param string $stat The name of the statistic to delete. 68 | * @param string $key The stat key to use 69 | * @return bool True if successful, false if not. 70 | */ 71 | public static function clear(string $stat, string $key = Stats::DEFAULT_KEY): bool 72 | { 73 | return (bool)Redis::instance()->hdel($key, $stat); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Socket/Client.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Resque\Socket; 13 | 14 | /** 15 | * Socket client connection 16 | * 17 | * @package Resque 18 | * @author Michael Haynes 19 | */ 20 | class Client 21 | { 22 | /** 23 | * @var \Socket The client's socket resource, for sending and receiving data with 24 | */ 25 | protected $socket = null; 26 | 27 | /** 28 | * @var string The client's IP address, as seen by the server 29 | */ 30 | protected $ip; 31 | 32 | /** 33 | * @var int If given, this will hold the port associated to address 34 | */ 35 | protected $port; 36 | 37 | /** 38 | * The client's hostname, as seen by the server. This 39 | * variable is only set after calling lookup_hostname, 40 | * as hostname lookups can take up a decent amount of time. 41 | * 42 | * @var string|null 43 | */ 44 | protected $hostname = null; 45 | 46 | /** 47 | * Creates the client 48 | * 49 | * @param \Socket $socket The resource of the socket the client is connecting by, generally the master socket. 50 | */ 51 | public function __construct(&$socket) 52 | { 53 | if (false === ($this->socket = @socket_accept($socket))) { 54 | throw new SocketException(sprintf('socket_accept($socket) failed: [%d] %s', $code = socket_last_error(), socket_strerror($code))); 55 | } 56 | 57 | socket_getpeername($this->socket, $this->ip, $this->port); 58 | } 59 | 60 | public function __toString() 61 | { 62 | return $this->ip.':'.$this->port; 63 | } 64 | 65 | /** 66 | * Closes the socket 67 | */ 68 | public function disconnect(): void 69 | { 70 | if ($this->socket) { 71 | socket_close($this->socket); 72 | $this->socket = null; 73 | } 74 | } 75 | 76 | /** 77 | * Returns this clients socket 78 | * 79 | * @return \Socket 80 | */ 81 | public function getSocket() 82 | { 83 | return $this->socket; 84 | } 85 | 86 | /** 87 | * Gets the IP hostname 88 | * 89 | * @return string 90 | */ 91 | public function getHostname(): string 92 | { 93 | if (is_null($this->hostname)) { 94 | $this->hostname = gethostbyaddr($this->ip); 95 | } 96 | 97 | return $this->hostname; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/Console/Command/Worker/Stop.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Resque\Console\Command\Worker; 13 | 14 | use Resque\Console\Command\Command; 15 | use Resque\Worker; 16 | use Symfony\Component\Console\Input\InputArgument; 17 | use Symfony\Component\Console\Input\InputInterface; 18 | use Symfony\Component\Console\Input\InputOption; 19 | use Symfony\Component\Console\Output\OutputInterface; 20 | 21 | /** 22 | * Worker stop command class 23 | * 24 | * @package Resque 25 | * @author Michael Haynes 26 | */ 27 | final class Stop extends Command 28 | { 29 | protected function configure(): void 30 | { 31 | $this->setName('worker:stop') 32 | ->setDefinition($this->mergeDefinitions([ 33 | new InputArgument('id', InputArgument::OPTIONAL, 'The id of the worker to stop (optional; if not present stops all workers).'), 34 | new InputOption('force', 'f', InputOption::VALUE_NONE, 'Force worker to stop, cancelling any current job.'), 35 | ])) 36 | ->setDescription('Stop a running worker. If no worker id set then stops all workers') 37 | ->setHelp('Stop a running worker. If no worker id set then stops all workers'); 38 | } 39 | 40 | protected function execute(InputInterface $input, OutputInterface $output): int 41 | { 42 | $id = $input->getArgument('id'); 43 | 44 | // Do a cleanup 45 | $worker = new Worker('*'); 46 | $worker->cleanup(); 47 | 48 | if ($id) { 49 | if (false === ($worker = Worker::hostWorker($id))) { 50 | $this->log('There is no worker with id "'.$id.'".', \Resque\Logger::ERROR); 51 | return Command::FAILURE; 52 | } 53 | 54 | $workers = [$worker]; 55 | } else { 56 | $workers = Worker::hostWorkers(); 57 | } 58 | 59 | if (!count($workers)) { 60 | $this->log('There are no workers on this host'); 61 | } 62 | 63 | $sig = $input->getOption('force') ? 'TERM' : 'QUIT'; 64 | 65 | foreach ($workers as $worker) { 66 | if (posix_kill($worker->getPid(), constant('SIG'.$sig))) { 67 | $this->log('Worker '.$worker.' '.$sig.' signal sent.'); 68 | } else { 69 | $this->log('Worker '.$worker.' could not send '.$sig.' signal.'); 70 | } 71 | } 72 | 73 | return Command::SUCCESS; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Console/ResqueApplication.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Resque\Console; 13 | 14 | use Resque\Console\Command\Cleanup; 15 | use Resque\Console\Command\Clear; 16 | use Resque\Console\Command\Hosts; 17 | use Resque\Console\Command\Job\Queue; 18 | use Resque\Console\Command\Queues; 19 | use Resque\Console\Command\Socket\Connect; 20 | use Resque\Console\Command\Socket\Receive; 21 | use Resque\Console\Command\Socket\Send; 22 | use Resque\Console\Command\SpeedTest; 23 | use Resque\Console\Command\Worker\Cancel; 24 | use Resque\Console\Command\Worker\Pause; 25 | use Resque\Console\Command\Worker\Restart; 26 | use Resque\Console\Command\Worker\Resume; 27 | use Resque\Console\Command\Worker\Start; 28 | use Resque\Console\Command\Worker\Stop; 29 | use Resque\Console\Command\Workers; 30 | use Symfony\Component\Console\Application; 31 | use Symfony\Component\Console\Input\InputInterface; 32 | use Symfony\Component\Console\Output\OutputInterface; 33 | 34 | /** 35 | * Resque console application. 36 | * 37 | * @package Resque 38 | * @author Paul Litovka 39 | */ 40 | class ResqueApplication extends Application 41 | { 42 | /** 43 | * Initialize the Resque console application. 44 | */ 45 | public function __construct() 46 | { 47 | parent::__construct('Resque', \Resque\Resque::VERSION); 48 | 49 | $this->addCommands([ 50 | new Clear(), 51 | new Hosts(), 52 | new Queues(), 53 | new Cleanup(), 54 | new Workers(), 55 | new Queue(), 56 | new Send(), 57 | new Receive(), 58 | new Connect(), 59 | new Start(), 60 | new Stop(), 61 | new Restart(), 62 | new Pause(), 63 | new Resume(), 64 | new Cancel(), 65 | new SpeedTest() 66 | ]); 67 | } 68 | 69 | /** 70 | * Runs the current application. 71 | */ 72 | public function doRun(InputInterface $input, OutputInterface $output): int 73 | { 74 | // Always show the version information except when the user invokes the help 75 | if ($input->hasParameterOption('--no-info') === false) { 76 | if ($input->hasParameterOption(['--help', '-h']) || ($input->getFirstArgument() && $input->getFirstArgument() !== 'list')) { 77 | $output->writeln($this->getLongVersion()); 78 | $output->writeln(''); 79 | } 80 | } 81 | 82 | return parent::doRun($input, $output); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/Console/Command/Worker/Cancel.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Resque\Console\Command\Worker; 13 | 14 | use Resque\Console\Command\Command; 15 | use Resque\Worker; 16 | use Symfony\Component\Console\Input\InputArgument; 17 | use Symfony\Component\Console\Input\InputInterface; 18 | use Symfony\Component\Console\Output\OutputInterface; 19 | 20 | /** 21 | * Worker cancel command class 22 | * 23 | * @package Resque 24 | * @author Michael Haynes 25 | */ 26 | final class Cancel extends Command 27 | { 28 | protected function configure(): void 29 | { 30 | $this->setName('worker:cancel') 31 | ->setDefinition($this->mergeDefinitions([ 32 | new InputArgument('id', InputArgument::OPTIONAL, 'The id of the worker to cancel it\'s running job (optional; if not present cancels all workers).'), 33 | ])) 34 | ->setDescription('Cancel job on a running worker. If no worker id set then cancels all workers') 35 | ->setHelp('Cancel job on a running worker. If no worker id set then cancels all workers'); 36 | } 37 | 38 | protected function execute(InputInterface $input, OutputInterface $output): int 39 | { 40 | $id = $input->getArgument('id'); 41 | 42 | // Do a cleanup 43 | $worker = new Worker('*'); 44 | $worker->cleanup(); 45 | 46 | if ($id) { 47 | if (false === ($worker = Worker::hostWorker($id))) { 48 | $this->log('There is no worker with id "'.$id.'".', \Resque\Logger::ERROR); 49 | return Command::FAILURE; 50 | } 51 | 52 | $workers = [$worker]; 53 | } else { 54 | $workers = Worker::hostWorkers(); 55 | } 56 | 57 | if (!count($workers)) { 58 | $this->log('There are no workers on this host'); 59 | } 60 | 61 | foreach ($workers as $worker) { 62 | $packet = $worker->getPacket(); 63 | $job_pid = (int)$packet['job_pid']; 64 | 65 | if ($job_pid and posix_kill($job_pid, 0)) { 66 | if (posix_kill($job_pid, SIGUSR1)) { 67 | $this->log('Worker '.$worker.' running job SIGUSR1 signal sent.'); 68 | } else { 69 | $this->log('Worker '.$worker.' running job SIGUSR1 signal could not be sent.'); 70 | } 71 | } else { 72 | $this->log('Worker '.$worker.' has no running job.'); 73 | } 74 | } 75 | 76 | return Command::SUCCESS; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/Console/Command/Worker/Start.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Resque\Console\Command\Worker; 13 | 14 | use Resque\Console\Command\Command; 15 | use Symfony\Component\Console\Input\InputInterface; 16 | use Symfony\Component\Console\Input\InputOption; 17 | use Symfony\Component\Console\Output\OutputInterface; 18 | 19 | /** 20 | * Worker start command class 21 | * 22 | * @package Resque 23 | * @author Michael Haynes 24 | */ 25 | final class Start extends Command 26 | { 27 | protected function configure(): void 28 | { 29 | $this->setName('worker:start') 30 | ->setDefinition($this->mergeDefinitions([ 31 | new InputOption('queue', 'Q', InputOption::VALUE_OPTIONAL, 'The queue(s) to listen on, comma separated.', '*'), 32 | new InputOption('blocking', 'b', InputOption::VALUE_OPTIONAL, 'Use Redis pop blocking or time interval.', true), 33 | new InputOption('interval', 'i', InputOption::VALUE_OPTIONAL, 'Blocking timeout/interval speed in seconds.', 10), 34 | new InputOption('timeout', 't', InputOption::VALUE_OPTIONAL, 'Seconds a job may run before timing out.', 60), 35 | new InputOption('memory', 'm', InputOption::VALUE_OPTIONAL, 'The memory limit in megabytes.', 128), 36 | new InputOption('pid', 'P', InputOption::VALUE_OPTIONAL, 'Absolute path to PID file, must be writeable by worker.'), 37 | ])) 38 | ->setDescription('Polls for jobs on specified queues and executes job when found') 39 | ->setHelp('Polls for jobs on specified queues and executes job when found'); 40 | } 41 | 42 | protected function execute(InputInterface $input, OutputInterface $output): int 43 | { 44 | $queue = $this->getConfig('queue'); 45 | $blocking = filter_var($this->getConfig('blocking'), FILTER_VALIDATE_BOOLEAN); 46 | 47 | // Create worker instance 48 | $worker = new \Resque\Worker($queue, $blocking); 49 | $worker->setLogger($this->logger); 50 | 51 | if ($pidFile = $this->getConfig('pid')) { 52 | $worker->setPidFile($pidFile); 53 | } 54 | 55 | if ($interval = $this->getConfig('interval')) { 56 | $worker->setInterval($interval); 57 | } 58 | 59 | if ($timeout = $this->getConfig('timeout')) { 60 | $worker->setTimeout($timeout); 61 | } 62 | 63 | // The memory limit is the amount of memory we will allow the script to occupy 64 | // before killing it and letting a process manager restart it for us, which 65 | // is to protect us against any memory leaks that will be in the scripts. 66 | if ($memory = $this->getConfig('memory')) { 67 | $worker->setMemoryLimit($memory); 68 | } 69 | 70 | $worker->work(); 71 | 72 | return Command::SUCCESS; 73 | } 74 | 75 | public function pollingConsoleOutput(): bool 76 | { 77 | return true; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Console/Command/Socket/Send.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Resque\Console\Command\Socket; 13 | 14 | use Resque\Console\Command\Command; 15 | use Resque\Socket\Server; 16 | use Symfony\Component\Console\Input\InputArgument; 17 | use Symfony\Component\Console\Input\InputInterface; 18 | use Symfony\Component\Console\Input\InputOption; 19 | use Symfony\Component\Console\Output\OutputInterface; 20 | 21 | /** 22 | * TCP send command class 23 | * 24 | * @package Resque 25 | * @author Michael Haynes 26 | */ 27 | final class Send extends Command 28 | { 29 | protected function configure(): void 30 | { 31 | $this->setName('socket:send') 32 | ->setDefinition($this->mergeDefinitions([ 33 | new InputArgument('cmd', InputArgument::REQUIRED, 'The command to send to the receiver.'), 34 | new InputArgument('id', InputArgument::OPTIONAL, 'The id of the worker (optional; required for worker: commands).'), 35 | new InputOption('connecthost', null, InputOption::VALUE_OPTIONAL, 'The host to send to.', '127.0.0.1'), 36 | new InputOption('connectport', null, InputOption::VALUE_OPTIONAL, 'The port to send on.', Server::DEFAULT_PORT), 37 | new InputOption('connecttimeout', 't', InputOption::VALUE_OPTIONAL, 'The send request timeout time (seconds).', 10), 38 | new InputOption('force', 'f', InputOption::VALUE_NONE, 'Force the command.'), 39 | new InputOption('json', 'j', InputOption::VALUE_NONE, 'Whether to return the response in JSON format.'), 40 | ])) 41 | ->setDescription('Sends a command to a php-resque receiver socket') 42 | ->setHelp('Sends a command to a php-resque receiver socket'); 43 | } 44 | 45 | protected function execute(InputInterface $input, OutputInterface $output): int 46 | { 47 | $cmd = $input->getArgument('cmd'); 48 | $host = $this->getConfig('connecthost'); 49 | $port = $this->getConfig('connectport'); 50 | $timeout = $this->getConfig('connecttimeout'); 51 | 52 | if (!($fh = @fsockopen('tcp://'.$host, $port, $errno, $errstr, $timeout))) { 53 | $this->log('['.$errno.'] '.$errstr.' host '.$host.':'.$port, \Resque\Logger::ERROR); 54 | return Command::FAILURE; 55 | } 56 | 57 | stream_set_timeout($fh, 0, 500 * 1000); 58 | 59 | $payload = [ 60 | 'cmd' => $cmd, 61 | 'id' => $input->getArgument('id'), 62 | 'force' => $input->getOption('force'), 63 | 'json' => $this->getConfig('json'), 64 | ]; 65 | 66 | Server::fwrite($fh, json_encode($payload)); 67 | 68 | $response = ''; 69 | while (($buffer = fgets($fh, 1024)) !== false) { 70 | $response .= $buffer; 71 | } 72 | 73 | $this->log(''.trim($response).''); 74 | 75 | fclose($fh); 76 | 77 | return Command::SUCCESS; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Resque.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Resque; 13 | 14 | /** 15 | * Main Resque class 16 | * 17 | * @package Resque 18 | * @author Michael Haynes 19 | * 20 | * @method static string redisKey(?string $queue = null, ?string $suffix = null) Get the Queue key. 21 | * @method static \Resque\Job job(string $id) Get a job by id. 22 | * @method static \Resque\Job push($job, ?array $data = null, ?string $queue = null) Push a new job onto the queue. 23 | * @method static \Resque\Job later($delay, $job, array $data = [], ?string $queue = null) Queue a job for later retrieval. 24 | * @method static \Resque\Job|false pop(array $queues, int $timeout = 10, bool $blocking = true) Pop the next job off of the queue. 25 | * @method static int size(string $queue) Get the size (number of pending jobs) of the specified queue. 26 | * @method static int sizeDelayed(string $queue) Get the size (number of delayed jobs) of the specified queue. 27 | * @method static string getQueue(?string $queue) Get the queue or return the default. 28 | * @method static void loadConfig(string $file = self::DEFAULT_CONFIG_FILE) Read and load data from a config file 29 | * @method static void setConfig(array $config) Set the configuration array 30 | */ 31 | class Resque 32 | { 33 | /** 34 | * php-resque version 35 | */ 36 | public const VERSION = '4.0.0'; 37 | 38 | /** 39 | * @var Queue The queue instance. 40 | */ 41 | protected static $queue = null; 42 | 43 | /** 44 | * Create a queue instance. 45 | * 46 | * @return Queue 47 | */ 48 | public static function queue(): Queue 49 | { 50 | if (!static::$queue) { 51 | static::$queue = new Queue(); 52 | } 53 | 54 | return static::$queue; 55 | } 56 | 57 | /** 58 | * Set the queue instance. 59 | * 60 | * @param Queue $queue The queue instance 61 | * 62 | * @return void 63 | */ 64 | public static function setQueue(Queue $queue): void 65 | { 66 | static::$queue = $queue; 67 | } 68 | 69 | /** 70 | * Dynamically pass calls to the default connection. 71 | * 72 | * @param string $method The method to call 73 | * @param array $parameters The parameters to pass 74 | */ 75 | public static function __callStatic(string $method, array $parameters) 76 | { 77 | // Simplify the call to setConfig and loadConfig 78 | if (in_array($method, ['setConfig', 'loadConfig'])) { 79 | return call_user_func_array([Config::class, $method], $parameters); 80 | } 81 | 82 | return call_user_func_array([static::queue(), $method], $parameters); 83 | } 84 | 85 | /** 86 | * Gets Resque stats 87 | * 88 | * @return array 89 | */ 90 | public static function stats(): array 91 | { 92 | return Redis::instance()->hgetall('stats'); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/Console/Command/Worker/Restart.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Resque\Console\Command\Worker; 13 | 14 | use Resque\Console\Command\Command; 15 | use Resque\Worker; 16 | use Symfony\Component\Console\Input\InputArgument; 17 | use Symfony\Component\Console\Input\InputInterface; 18 | use Symfony\Component\Console\Output\OutputInterface; 19 | 20 | /** 21 | * Worker restart command class 22 | * 23 | * @package Resque 24 | * @author Michael Haynes 25 | */ 26 | final class Restart extends Command 27 | { 28 | protected function configure(): void 29 | { 30 | $this->setName('worker:restart') 31 | ->setDefinition($this->mergeDefinitions([ 32 | new InputArgument('id', InputArgument::OPTIONAL, 'The id of the worker to restart (optional; if not present restarts all workers).'), 33 | ])) 34 | ->setDescription('Restart a running worker. If no worker id set then restarts all workers') 35 | ->setHelp('Restart a running worker. If no worker id set then restarts all workers'); 36 | } 37 | 38 | protected function execute(InputInterface $input, OutputInterface $output): int 39 | { 40 | $id = $input->getArgument('id'); 41 | 42 | // Do a cleanup 43 | $worker = new Worker('*'); 44 | $worker->cleanup(); 45 | 46 | if ($id) { 47 | if (false === ($worker = Worker::hostWorker($id))) { 48 | $this->log('There is no worker with id "'.$id.'".', \Resque\Logger::ERROR); 49 | return Command::FAILURE; 50 | } 51 | 52 | $workers = [$worker]; 53 | } else { 54 | $workers = Worker::hostWorkers(); 55 | } 56 | 57 | if (!count($workers)) { 58 | $this->log('There are no workers on this host'); 59 | } 60 | 61 | foreach ($workers as $worker) { 62 | if (posix_kill($worker->getPid(), SIGTERM)) { 63 | $child = pcntl_fork(); 64 | 65 | // Failed 66 | if ($child == -1) { 67 | $this->log('Unable to fork, worker '.$worker.' has been stopped.', \Resque\Logger::CRITICAL); 68 | 69 | // Parent 70 | } elseif ($child > 0) { 71 | $this->log('Worker '.$worker.' restarted.'); 72 | continue; 73 | 74 | // Child 75 | } else { 76 | $new_worker = new Worker($worker->getQueues(), $worker->getBlocking()); 77 | $new_worker->setInterval($worker->getInterval()); 78 | $new_worker->setTimeout($worker->getTimeout()); 79 | $new_worker->setMemoryLimit($worker->getMemoryLimit()); 80 | $new_worker->setLogger($this->logger); 81 | $new_worker->work(); 82 | 83 | $this->log('Worker '.$worker.' restarted as '.$new_worker.'.'); 84 | } 85 | } else { 86 | $this->log('Worker '.$worker.' could not send TERM signal.'); 87 | } 88 | } 89 | 90 | return Command::SUCCESS; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/Console/Command/Job/Queue.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Resque\Console\Command\Job; 13 | 14 | use Resque\Resque; 15 | use Resque\Console\Command\Command; 16 | use Resque\Logger; 17 | use Symfony\Component\Console\Input\InputArgument; 18 | use Symfony\Component\Console\Input\InputInterface; 19 | use Symfony\Component\Console\Input\InputOption; 20 | use Symfony\Component\Console\Output\OutputInterface; 21 | 22 | /** 23 | * Job command class 24 | * 25 | * @package Resque 26 | * @author Michael Haynes 27 | */ 28 | final class Queue extends Command 29 | { 30 | protected function configure(): void 31 | { 32 | $this->setName('job:queue') 33 | ->setDefinition($this->mergeDefinitions([ 34 | new InputArgument('job', InputArgument::REQUIRED, 'The job to run.'), 35 | new InputArgument('args', InputArgument::OPTIONAL, 'The arguments to send with the job.'), 36 | new InputOption('queue', 'Q', InputOption::VALUE_OPTIONAL, 'The queue to add the job to.'), 37 | new InputOption('delay', 'D', InputOption::VALUE_OPTIONAL, 'The amount of time or a unix time to delay execution of job till.'), 38 | ])) 39 | ->setDescription('Queue a new job to run with optional delay') 40 | ->setHelp('Queue a new job to run'); 41 | } 42 | 43 | protected function execute(InputInterface $input, OutputInterface $output): int 44 | { 45 | $job = $input->getArgument('job'); 46 | $args = $input->getArgument('args'); 47 | $queue = $this->getConfig('queue'); 48 | $delay = $this->getConfig('delay'); 49 | 50 | if (json_decode($args, true)) { 51 | $args = (array)json_decode($args, true); 52 | } else { 53 | if (is_null($args)) { 54 | $args = []; 55 | } else { 56 | $args = explode(',', $args); 57 | 58 | $args = array_map(function ($v) { 59 | if (filter_var($v, FILTER_VALIDATE_INT)) { 60 | $v = (int)$v; 61 | } elseif (filter_var($v, FILTER_VALIDATE_FLOAT)) { 62 | $v = (float)$v; 63 | } 64 | return $v; 65 | }, $args); 66 | } 67 | } 68 | 69 | if (!$delay or filter_var($delay, FILTER_VALIDATE_INT)) { 70 | $delay = (int)$delay; 71 | } else { 72 | $this->log('Delay option "'.$delay.'" is invalid type "'.gettype($delay).'", value must be an integer.', Logger::ERROR); 73 | return Command::INVALID; 74 | } 75 | 76 | if ($delay) { 77 | if ($job = Resque::later($delay, $job, $args, $queue)) { 78 | $this->log('Job '.$job.' will be queued at '.date('r', $job->getDelayedTime()).' on '.$job->getQueue().' queue.'); 79 | return Command::SUCCESS; 80 | } 81 | } else { 82 | if ($job = Resque::push($job, $args, $queue)) { 83 | $this->log('Job '.$job.' added to '.$job->getQueue().' queue.'); 84 | return Command::SUCCESS; 85 | } 86 | } 87 | 88 | $this->log('Error, job was not queued. Please try again.', Logger::ERROR); 89 | 90 | return Command::FAILURE; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/Host.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Resque; 13 | 14 | /** 15 | * Resque host class 16 | * 17 | * @package Resque 18 | * @author Michael Haynes 19 | */ 20 | final class Host 21 | { 22 | /** 23 | * @var Redis The Redis instance 24 | */ 25 | protected $redis; 26 | 27 | /** 28 | * @var string The hostname 29 | */ 30 | protected $hostname; 31 | 32 | /** 33 | * @var int Host key timeout 34 | */ 35 | protected $timeout = 120; 36 | 37 | /** 38 | * Get the Redis key 39 | * 40 | * @param Host $host The host to get the key for 41 | * @param string $suffix To be appended to key 42 | * @return string 43 | */ 44 | public static function redisKey(?Host $host = null, ?string $suffix = null): string 45 | { 46 | if (is_null($host)) { 47 | return 'hosts'; 48 | } 49 | 50 | $hostname = $host instanceof Host ? $host->hostname : $host; 51 | return 'host:'.$hostname.($suffix ? ':'.$suffix : ''); 52 | } 53 | 54 | /** 55 | * Create a new host 56 | * @param null|string $hostname 57 | */ 58 | public function __construct(?string $hostname = null) 59 | { 60 | $this->redis = Redis::instance(); 61 | $this->hostname = $hostname ?: gethostname(); 62 | } 63 | 64 | /** 65 | * Generate a string representation of this worker. 66 | * 67 | * @return string Identifier for this worker instance. 68 | */ 69 | public function __toString() 70 | { 71 | return $this->hostname; 72 | } 73 | 74 | /** 75 | * Mark host as having an active worker 76 | * 77 | * @param Worker $worker the worker instance 78 | */ 79 | public function working(Worker $worker): void 80 | { 81 | $this->redis->sadd(self::redisKey(), $this->hostname); 82 | 83 | $this->redis->sadd(self::redisKey($this), (string)$worker); 84 | $this->redis->expire(self::redisKey($this), $this->timeout); 85 | } 86 | 87 | /** 88 | * Mark host as having a finished worker 89 | * 90 | * @param Worker $worker The worker instance 91 | */ 92 | public function finished(Worker $worker): void 93 | { 94 | $this->redis->srem(self::redisKey($this), (string)$worker); 95 | } 96 | 97 | /** 98 | * Cleans up any dead hosts 99 | * 100 | * @return array List of cleaned hosts 101 | */ 102 | public function cleanup(): array 103 | { 104 | $hosts = $this->redis->smembers(self::redisKey()); 105 | $workers = $this->redis->smembers(Worker::redisKey()); 106 | $cleaned = ['hosts' => [], 'workers' => []]; 107 | 108 | foreach ($hosts as $hostname) { 109 | $host = new static($hostname); 110 | 111 | if (!$this->redis->exists(self::redisKey($host))) { 112 | $this->redis->srem(self::redisKey(), (string)$host); 113 | $cleaned['hosts'][] = (string)$host; 114 | } else { 115 | $host_workers = $this->redis->smembers(self::redisKey($host)); 116 | 117 | foreach ($host_workers as $host_worker) { 118 | if (!in_array($host_worker, $workers)) { 119 | $cleaned['workers'][] = $host_worker; 120 | 121 | $this->redis->srem(self::redisKey($host), $host_worker); 122 | } 123 | } 124 | } 125 | } 126 | 127 | return $cleaned; 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/Logger.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Resque; 13 | 14 | use Monolog\Logger as Monolog; 15 | 16 | /** 17 | * Resque logger class. Wrapper for Monolog 18 | * 19 | * @package Resque 20 | * @author Michael Haynes 21 | */ 22 | final class Logger 23 | { 24 | /** 25 | * Detailed debug information 26 | */ 27 | public const DEBUG = Monolog::DEBUG; 28 | 29 | /** 30 | * Interesting events e.g. User logs in, SQL logs. 31 | */ 32 | public const INFO = Monolog::INFO; 33 | 34 | /** 35 | * Uncommon events 36 | */ 37 | public const NOTICE = Monolog::NOTICE; 38 | 39 | /** 40 | * Exceptional occurrences that are not errors e.g. Use of deprecated APIs, poor use of an API, undesirable things that are not necessarily wrong. 41 | */ 42 | public const WARNING = Monolog::WARNING; 43 | 44 | /** 45 | * Runtime errors 46 | */ 47 | public const ERROR = Monolog::ERROR; 48 | 49 | /** 50 | * Critical conditions e.g. Application component unavailable, unexpected exception. 51 | */ 52 | public const CRITICAL = Monolog::CRITICAL; 53 | 54 | /** 55 | * Action must be taken immediately e.g. Entire website down, database unavailable, etc. This should trigger the SMS alerts and wake you up. 56 | */ 57 | public const ALERT = Monolog::ALERT; 58 | 59 | /** 60 | * Urgent alert. 61 | */ 62 | public const EMERGENCY = Monolog::EMERGENCY; 63 | 64 | /** 65 | * @var array List of valid log levels 66 | */ 67 | protected $logTypes = [ 68 | self::DEBUG => 'debug', 69 | self::INFO => 'info', 70 | self::NOTICE => 'notice', 71 | self::WARNING => 'warning', 72 | self::ERROR => 'error', 73 | self::CRITICAL => 'critical', 74 | self::ALERT => 'alert', 75 | self::EMERGENCY => 'emergency', 76 | ]; 77 | 78 | /** 79 | * @var Monolog The monolog instance 80 | */ 81 | protected $instance; 82 | 83 | /** 84 | * Create a Monolog instance and attach a handler 85 | * 86 | * @see https://github.com/Seldaek/monolog#handlers Monolog handlers documentation 87 | * @param array $handlers Array of Monolog handlers 88 | */ 89 | public function __construct(array $handlers) 90 | { 91 | $this->instance = new Monolog('resque'); 92 | 93 | foreach ($handlers as $handler) { 94 | $this->instance->pushHandler($handler); 95 | } 96 | } 97 | 98 | /** 99 | * Return a Monolog Logger instance 100 | * 101 | * @return Monolog instance, ready to use 102 | */ 103 | public function getInstance(): Monolog 104 | { 105 | return $this->instance; 106 | } 107 | 108 | /** 109 | * Send log message to output interface 110 | * 111 | * @param string $message Message to output 112 | * @param mixed $context Some context around the log 113 | * @param int|null $logType The log type 114 | */ 115 | public function log(string $message, $context = null, ?int $logType = null) 116 | { 117 | if (is_int($context) and is_null($logType)) { 118 | $logType = $context; 119 | $context = []; 120 | } 121 | 122 | if (!is_array($context)) { 123 | $context = is_null($context) ? [] : [$context]; 124 | } 125 | 126 | if (!in_array($logType, $this->instance->getLevels())) { 127 | $logType = Monolog::INFO; 128 | } 129 | 130 | return call_user_func([$this->instance, $this->logTypes[$logType]], $message, $context); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/Console/Command/Socket/Connect.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Resque\Console\Command\Socket; 13 | 14 | use Resque\Console\Command\Command; 15 | use Resque\Socket\Server; 16 | use Symfony\Component\Console\Input\InputInterface; 17 | use Symfony\Component\Console\Input\InputOption; 18 | use Symfony\Component\Console\Output\OutputInterface; 19 | 20 | /** 21 | * TCP connect command class 22 | * 23 | * @package Resque 24 | * @author Michael Haynes 25 | */ 26 | final class Connect extends Command 27 | { 28 | protected function configure(): void 29 | { 30 | $this->setName('socket:connect') 31 | ->setDefinition($this->mergeDefinitions([ 32 | new InputOption('connecthost', null, InputOption::VALUE_OPTIONAL, 'The host to connect to.', '127.0.0.1'), 33 | new InputOption('connectport', null, InputOption::VALUE_OPTIONAL, 'The port to connect to.', Server::DEFAULT_PORT), 34 | new InputOption('connecttimeout', 't', InputOption::VALUE_OPTIONAL, 'The connection timeout time (seconds).', 10), 35 | ])) 36 | ->setDescription('Connects to a php-resque receiver socket') 37 | ->setHelp('Connects to a php-resque receiver socket'); 38 | } 39 | 40 | protected function execute(InputInterface $input, OutputInterface $output): int 41 | { 42 | $host = $this->getConfig('connecthost'); 43 | $port = $this->getConfig('connectport'); 44 | $timeout = $this->getConfig('connecttimeout'); 45 | 46 | $conn = $host.':'.$port; 47 | $prompt = 'php-resque '.$conn.'> '; 48 | 49 | $output->writeln('Connecting to '.$conn.'...'); 50 | 51 | if (!($fh = @fsockopen('tcp://'.$host, $port, $errno, $errstr, $timeout))) { 52 | $output->writeln('['.$errno.'] '.$errstr.' host '.$conn.''); 53 | return Command::FAILURE; 54 | } 55 | 56 | // Set socket timeout to 200ms 57 | stream_set_timeout($fh, 0, 200 * 1000); 58 | 59 | $stdin = fopen('php://stdin', 'r'); 60 | 61 | $prompting = false; 62 | 63 | Server::fwrite($fh, 'shell'); 64 | 65 | while (true) { 66 | if (feof($fh)) { 67 | $output->writeln('Connection to '.$conn.' closed.'); 68 | break; 69 | } 70 | 71 | $read = [$fh, $stdin]; 72 | $write = null; 73 | $except = null; 74 | 75 | $selected = @stream_select($read, $write, $except, 0); 76 | if ($selected > 0) { 77 | foreach ($read as $r) { 78 | if ($r == $stdin) { 79 | $input = trim(fgets($stdin)); 80 | 81 | if (empty($input)) { 82 | $output->write($prompt); 83 | $prompting = true; 84 | } else { 85 | Server::fwrite($fh, $input); 86 | $prompting = false; 87 | } 88 | } elseif ($r == $fh) { 89 | $input = ''; 90 | while (($buffer = fgets($fh, 1024)) !== false) { 91 | $input .= $buffer; 92 | } 93 | 94 | if ($prompting) { 95 | $output->writeln(''); 96 | } 97 | 98 | $output->writeln(''.trim($input).''); 99 | 100 | if (!feof($fh)) { 101 | $output->write($prompt); 102 | $prompting = true; 103 | } 104 | } 105 | } 106 | } 107 | 108 | // Sleep for 10ms to stop CPU spiking 109 | usleep(10 * 1000); 110 | } 111 | 112 | fclose($fh); 113 | 114 | return Command::SUCCESS; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/Console/Command/SpeedTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Resque\Console\Command; 13 | 14 | use Resque\Redis; 15 | use Symfony\Component\Console\Input\InputInterface; 16 | use Symfony\Component\Console\Input\InputOption; 17 | use Symfony\Component\Console\Output\OutputInterface; 18 | use Symfony\Component\Process\Process; 19 | 20 | /** 21 | * Performs a raw speed test 22 | * 23 | * @package Resque 24 | * @author Michael Haynes 25 | */ 26 | final class SpeedTest extends Command 27 | { 28 | protected function configure(): void 29 | { 30 | $this->setName('speed:test') 31 | ->setDefinition($this->mergeDefinitions([ 32 | new InputOption('time', 't', InputOption::VALUE_REQUIRED, 'Length of time to run the test for', 10), 33 | ])) 34 | ->setDescription('Performs a speed test on php-resque to see how many jobs/second it can compute') 35 | ->setHelp('Performs a speed test on php-resque to see how many jobs/second it can compute'); 36 | } 37 | 38 | protected function execute(InputInterface $input, OutputInterface $output): int 39 | { 40 | if (!class_exists(Process::class)) { 41 | throw new \Exception('The Symfony process component is required to run the speed test.'); 42 | } 43 | 44 | $redisNamespace = 'resque:speedtest'; 45 | $logFile = RESQUE_DIR.'/speed.log'; 46 | 47 | Redis::setConfig(['namespace' => $redisNamespace]); 48 | 49 | $testTime = (int)$input->getOption('time') ?: 5; 50 | 51 | @unlink($logFile); 52 | $process = new Process([ 53 | RESQUE_BIN_DIR.'/resque', 'worker:start', 54 | '-I', RESQUE_DIR.'/autoload.php', 55 | '--scheme', $this->config['scheme'], 56 | '-H', $this->config['host'], 57 | '-p', $this->config['port'], 58 | '--namespace', $redisNamespace, 59 | '--log', $logFile, 60 | '-b', true, 61 | '-i', 1, 62 | '-vv', 63 | ]); 64 | 65 | $start = microtime(true); 66 | $process->start(); 67 | 68 | do { 69 | $this->setProgress($output, \Resque\Resque::stats(), $testTime, $start); 70 | usleep(500); 71 | } while ($process->isRunning() and $testTime > (microtime(true) - $start)); 72 | 73 | $process->stop(0, SIGTERM); 74 | 75 | if (!$process->isSuccessful()) { 76 | [$error] = explode('Exception trace:', $process->getErrorOutput()); 77 | 78 | $output->write(''.$error.''); 79 | } 80 | 81 | // Clear down Redis 82 | $redis = Redis::instance(); 83 | $keys = $redis->keys('*'); 84 | foreach ($keys as $key) { 85 | $redis->del($key); 86 | } 87 | 88 | return Command::SUCCESS; 89 | } 90 | 91 | // http://www.tldp.org/HOWTO/Bash-Prompt-HOWTO/x361.html 92 | public function setProgress(OutputInterface $output, array $stats, float $testTime, float $start): void 93 | { 94 | static $reset = false; 95 | 96 | $exec_time = round(microtime(true) - $start); 97 | $rate = @$stats['processed'] / max($exec_time, 1); 98 | 99 | $progress_length = 35; 100 | $progress_percent = (microtime(true) - $start) / $testTime; 101 | 102 | $progress_bar = str_repeat('=', $progress_complete_length = round($progress_percent * $progress_length)); 103 | $progress_bar .= $progress_complete_length == $progress_length ? '' : '>'; 104 | $progress_bar .= str_repeat('-', max($progress_length - $progress_complete_length - 1, 0)); 105 | $progress_bar .= $progress_complete_length == $progress_length ? '' : ' '.round($progress_percent * 100).'%'; 106 | 107 | $display = <<%title% php-resque speed test%clr% 109 | %progress%%clr% 110 | Time: %in%%clr% 111 | Processed: %jobs%%clr% 112 | Speed: %speed%%clr% 113 | Avg job time: %time%%clr% 114 | STATS; 115 | 116 | $replace = [ 117 | '%title%' => $exec_time == $testTime ? 'Finished' : 'Running', 118 | '%progress%' => $progress_bar, 119 | '%jobs%' => @$stats['processed'].' job'.(@$stats['processed'] == 1 ? '' : 's'), 120 | '%in%' => $exec_time.'s'.($progress_complete_length == $progress_length ? '' : ' ('.$testTime.'s test)'), 121 | '%speed%' => round($rate, 1).' jobs/s', 122 | '%time%' => $rate > 0 ? round(1 / $rate * 1000, 1).' ms' : '-', 123 | '%clr%' => "\033[K", 124 | ]; 125 | 126 | $output->writeln(($reset ? "\033[6A" : '').strtr($display, $replace)); 127 | 128 | !$reset and $reset = true; 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/Logger/Handler/ConsoleHandler.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Resque\Logger\Handler; 13 | 14 | use Resque\Logger; 15 | use Monolog\Handler\AbstractProcessingHandler; 16 | use Symfony\Component\Console\Output\OutputInterface; 17 | use Symfony\Component\Console\Output\ConsoleOutputInterface; 18 | 19 | /** 20 | * Monolog console handler 21 | * 22 | * @package Resque 23 | * @author Michael Haynes 24 | */ 25 | class ConsoleHandler extends AbstractProcessingHandler 26 | { 27 | /** 28 | * @var OutputInterface The console output interface 29 | */ 30 | protected $output; 31 | 32 | /** 33 | * @var array Map log levels to output verbosity 34 | */ 35 | private $verbosityLevelMap = [ 36 | Logger::INFO => OutputInterface::VERBOSITY_NORMAL, 37 | Logger::NOTICE => OutputInterface::VERBOSITY_VERBOSE, 38 | Logger::WARNING => OutputInterface::VERBOSITY_VERY_VERBOSE, 39 | Logger::DEBUG => OutputInterface::VERBOSITY_DEBUG, 40 | 41 | Logger::ERROR => OutputInterface::VERBOSITY_NORMAL, 42 | Logger::CRITICAL => OutputInterface::VERBOSITY_NORMAL, 43 | Logger::ALERT => OutputInterface::VERBOSITY_NORMAL, 44 | Logger::EMERGENCY => OutputInterface::VERBOSITY_NORMAL, 45 | ]; 46 | 47 | /** 48 | * Colours: black, red, green, yellow, blue, magenta, cyan, white 49 | * Options: bold, underscore, blink, reverse, conceal 50 | * 51 | * @var array 52 | */ 53 | private $styleMap = [ 54 | 'info' => [], 55 | 'notice' => [], 56 | 'warning' => ['yellow'], 57 | 'debug' => ['blue'], 58 | 'error' => ['white', 'red'], 59 | 'critical' => ['white', 'red'], 60 | 'alert' => ['white', 'red'], 61 | 'emergency' => ['white', 'red'], 62 | 63 | 'pop' => ['green'], 64 | 'warn' => ['yellow'], 65 | 'comment' => ['yellow'], 66 | 'question' => ['black', 'cyan'], 67 | ]; 68 | 69 | /** 70 | * @param OutputInterface $output The output interface 71 | * @param int $level The minimum logging level at which this handler will be triggered 72 | * @param bool $bubble Whether the messages that are handled can bubble up the stack or not 73 | */ 74 | public function __construct(OutputInterface $output, int $level = Logger::DEBUG, bool $bubble = true) 75 | { 76 | parent::__construct($level, $bubble); 77 | 78 | $this->output = $output; 79 | 80 | foreach ($this->styleMap as $name => $styles) { 81 | $style = new \ReflectionClass('Symfony\Component\Console\Formatter\OutputFormatterStyle'); 82 | /** @var \Symfony\Component\Console\Formatter\OutputFormatterStyle */ 83 | $styleClass = $style->newInstanceArgs($styles); 84 | $this->output->getFormatter()->setStyle($name, $styleClass); 85 | 86 | if ($this->output instanceof ConsoleOutputInterface) { 87 | $this->output->getErrorOutput()->getFormatter()->setStyle($name, $styleClass); 88 | } 89 | } 90 | } 91 | 92 | /** 93 | * {@inheritdoc} 94 | */ 95 | public function isHandling(array $record): bool 96 | { 97 | return $this->updateLevel() and parent::isHandling($record); 98 | } 99 | 100 | /** 101 | * {@inheritdoc} 102 | */ 103 | public function handle(array $record): bool 104 | { 105 | // we have to update the logging level each time because the verbosity of the 106 | // console output might have changed in the meantime (it is not immutable) 107 | return $this->updateLevel() and parent::handle($record); 108 | } 109 | 110 | /** 111 | * {@inheritdoc} 112 | */ 113 | protected function write(array $record): void 114 | { 115 | if ( 116 | null === $this->output or 117 | OutputInterface::VERBOSITY_QUIET === ($verbosity = $this->output->getVerbosity()) or 118 | $verbosity < $this->verbosityLevelMap[$record['level']] 119 | ) { 120 | return; 121 | } 122 | 123 | if ($record['level'] >= Logger::ERROR and $this->output instanceof ConsoleOutputInterface) { 124 | $this->output->getErrorOutput()->write((string)$record['formatted']); 125 | } else { 126 | $this->output->write((string)$record['formatted']); 127 | } 128 | 129 | return; 130 | } 131 | 132 | /** 133 | * {@inheritdoc} 134 | */ 135 | protected function getDefaultFormatter(): \Monolog\Formatter\FormatterInterface 136 | { 137 | $formatter = new Logger\Formatter\ConsoleFormatter(); 138 | if (method_exists($formatter, 'allowInlineLineBreaks')) { 139 | $formatter->allowInlineLineBreaks(true); 140 | } 141 | return $formatter; 142 | } 143 | 144 | /** 145 | * Updates the logging level based on the verbosity setting of the console output. 146 | * 147 | * @return bool Whether the handler is enabled and verbosity is not set to quiet. 148 | */ 149 | private function updateLevel(): bool 150 | { 151 | if (null === $this->output or OutputInterface::VERBOSITY_QUIET === ($verbosity = $this->output->getVerbosity())) { 152 | return false; 153 | } 154 | 155 | $this->setLevel(Logger::DEBUG); 156 | return true; 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/Event.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Resque; 13 | 14 | /** 15 | * Resque event/hook system class 16 | * 17 | * @package Resque 18 | * @author Michael Haynes 19 | */ 20 | final class Event 21 | { 22 | // Worker event constants 23 | public const WORKER_INSTANCE = 100; 24 | public const WORKER_STARTUP = 101; 25 | public const WORKER_SHUTDOWN = 102; 26 | public const WORKER_FORCE_SHUTDOWN = 103; 27 | public const WORKER_REGISTER = 104; 28 | public const WORKER_UNREGISTER = 105; 29 | public const WORKER_WORK = 106; 30 | public const WORKER_FORK = 107; 31 | public const WORKER_FORK_ERROR = 108; 32 | public const WORKER_FORK_PARENT = 109; 33 | public const WORKER_FORK_CHILD = 110; 34 | public const WORKER_WORKING_ON = 111; 35 | public const WORKER_DONE_WORKING = 112; 36 | public const WORKER_KILLCHILD = 113; 37 | public const WORKER_PAUSE = 114; 38 | public const WORKER_RESUME = 115; 39 | public const WORKER_WAKEUP = 116; 40 | public const WORKER_CLEANUP = 117; 41 | public const WORKER_LOW_MEMORY = 118; 42 | public const WORKER_CORRUPT = 119; 43 | 44 | // Job event constants 45 | public const JOB_INSTANCE = 200; 46 | public const JOB_QUEUE = 201; 47 | public const JOB_QUEUED = 202; 48 | public const JOB_DELAY = 203; 49 | public const JOB_DELAYED = 204; 50 | public const JOB_QUEUE_DELAYED = 205; 51 | public const JOB_QUEUED_DELAYED = 206; 52 | public const JOB_PERFORM = 207; 53 | public const JOB_RUNNING = 208; 54 | public const JOB_COMPLETE = 209; 55 | public const JOB_CANCELLED = 210; 56 | public const JOB_FAILURE = 211; 57 | public const JOB_DONE = 212; 58 | 59 | /** 60 | * @var array containing all registered callbacks, indexed by event name 61 | */ 62 | protected static $events = []; 63 | 64 | /** 65 | * Listen in on a given event to have a specified callback fired. 66 | * 67 | * @param string|array $event Name of event to listen on. 68 | * @param callable $callback Any callback callable by call_user_func_array 69 | * @return void 70 | */ 71 | public static function listen($event, callable $callback): void 72 | { 73 | if (is_array($event)) { 74 | foreach ($event as $e) { 75 | self::listen($e, $callback); 76 | } 77 | return; 78 | } 79 | 80 | if ($event !== '*' and !self::eventName($event)) { 81 | throw new \InvalidArgumentException('Event "'.$event.'" is not a valid event'); 82 | } 83 | 84 | if (!isset(self::$events[$event])) { 85 | self::$events[$event] = []; 86 | } 87 | 88 | self::$events[$event][] = $callback; 89 | } 90 | 91 | /** 92 | * Raise a given event with the supplied data. 93 | * 94 | * @param string $event Name of event to be raised 95 | * @param mixed $data Data that should be passed to each callback (optional) 96 | * @return bool 97 | */ 98 | public static function fire(string $event, $data = null): bool 99 | { 100 | if (!is_array($data)) { 101 | $data = [$data]; 102 | } 103 | array_unshift($data, $event); 104 | 105 | $retval = true; 106 | 107 | foreach (['*', $event] as $e) { 108 | if (!array_key_exists($e, self::$events)) { 109 | continue; 110 | } 111 | 112 | foreach (self::$events[$e] as $callback) { 113 | if (!is_callable($callback)) { 114 | continue; 115 | } 116 | 117 | if (($retval = call_user_func_array($callback, $data)) === false) { 118 | break 2; 119 | } 120 | } 121 | } 122 | 123 | return $retval !== false; 124 | } 125 | 126 | /** 127 | * Stop a given callback from listening on a specific event. 128 | * 129 | * @param string $event Name of event 130 | * @param callable $callback The callback as defined when listen() was called 131 | * @return true 132 | */ 133 | public static function forget(string $event, callable $callback) 134 | { 135 | if (!isset(self::$events[$event])) { 136 | return true; 137 | } 138 | 139 | $key = array_search($callback, self::$events[$event]); 140 | 141 | if ($key !== false) { 142 | unset(self::$events[$event][$key]); 143 | } 144 | 145 | return true; 146 | } 147 | 148 | /** 149 | * Clear all registered listeners. 150 | */ 151 | public static function clear(): void 152 | { 153 | self::$events = []; 154 | } 155 | 156 | /** 157 | * Returns the name of the given event from constant 158 | * 159 | * @param int $event Event constant 160 | * @return string|false 161 | */ 162 | public static function eventName(int $event) 163 | { 164 | static $constants = null; 165 | 166 | if (is_null($constants)) { 167 | $class = new \ReflectionClass(Event::class); 168 | 169 | $constants = []; 170 | foreach ($class->getConstants() as $name => $value) { 171 | $constants[$value] = strtolower($name); 172 | } 173 | } 174 | 175 | return $constants[$event] ?? false; 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /src/Helpers/Util.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Resque\Helpers; 13 | 14 | /** 15 | * Resque Utilities 16 | * 17 | * @package Resque 18 | * @author Michael Haynes 19 | */ 20 | final class Util 21 | { 22 | /** 23 | * Returns human readable sizes. Based on original functions written by 24 | * [Aidan Lister](http://aidanlister.com/repos/v/function.size_readable.php) 25 | * and [Quentin Zervaas](http://www.phpriot.com/d/code/strings/filesize-format/). 26 | * 27 | * @param int $bytes size in bytes 28 | * @param string $force_unit a definitive unit 29 | * @param string $format the return string format 30 | * @param bool $si whether to use SI prefixes or IEC 31 | * 32 | * @return string 33 | */ 34 | public static function bytes(int $bytes, ?string $force_unit = '', ?string $format = null, bool $si = true): string 35 | { 36 | $format = ($format === null) ? '%01.2f %s' : (string) $format; 37 | 38 | // IEC prefixes (binary) 39 | if ($si == false or strpos($force_unit, 'i') !== false) { 40 | $units = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB']; 41 | $mod = 1024; 42 | 43 | // SI prefixes (decimal) 44 | } else { 45 | $units = ['B', 'kB', 'MB', 'GB', 'TB', 'PB']; 46 | $mod = 1000; 47 | } 48 | 49 | if (($power = array_search((string) $force_unit, $units)) === false) { 50 | $power = ($bytes > 0) ? floor(log($bytes, $mod)) : 0; 51 | } 52 | 53 | return sprintf($format, $bytes / pow($mod, $power), $units[$power]); 54 | } 55 | 56 | /** 57 | * Constants for human_time_diff() 58 | */ 59 | public const MINUTE_IN_SECONDS = 60; 60 | public const HOUR_IN_SECONDS = 3600; 61 | public const DAY_IN_SECONDS = 86400; 62 | public const WEEK_IN_SECONDS = 604800; 63 | public const YEAR_IN_SECONDS = 3.15569e7; 64 | 65 | /** 66 | * Determines the difference between two timestamps. 67 | * 68 | * The difference is returned in a human readable format such as "1 hour", 69 | * "5 mins", "2 days". 70 | * 71 | * @param int $from Unix timestamp from which the difference begins. 72 | * @param int $to Optional. Unix timestamp to end the time difference. Default becomes time() if not set. 73 | * 74 | * @return string Human readable time difference. 75 | */ 76 | public static function human_time_diff(int $from, ?int $to = null): string 77 | { 78 | $to = $to ?: time(); 79 | 80 | $diff = (int)abs($to - $from); 81 | 82 | if ($diff < self::MINUTE_IN_SECONDS) { 83 | $since = [$diff, 'sec']; 84 | } elseif ($diff < self::HOUR_IN_SECONDS) { 85 | $since = [round($diff / self::MINUTE_IN_SECONDS), 'min']; 86 | } elseif ($diff < self::DAY_IN_SECONDS and $diff >= self::HOUR_IN_SECONDS) { 87 | $since = [round($diff / self::HOUR_IN_SECONDS), 'hour']; 88 | } elseif ($diff < self::WEEK_IN_SECONDS and $diff >= self::DAY_IN_SECONDS) { 89 | $since = [round($diff / self::DAY_IN_SECONDS), 'day']; 90 | } elseif ($diff < 30 * self::DAY_IN_SECONDS and $diff >= self::WEEK_IN_SECONDS) { 91 | $since = [round($diff / self::WEEK_IN_SECONDS), 'week']; 92 | } elseif ($diff < self::YEAR_IN_SECONDS and $diff >= 30 * self::DAY_IN_SECONDS) { 93 | $since = [round($diff / (30 * self::DAY_IN_SECONDS)), 'month']; 94 | } elseif ($diff >= self::YEAR_IN_SECONDS) { 95 | $since = [round($diff / self::YEAR_IN_SECONDS), 'year']; 96 | } 97 | 98 | if ($since[0] <= 1) { 99 | $since[0] = 1; 100 | } 101 | 102 | return $since[0].' '.$since[1].($since[0] == 1 ? '' : 's'); 103 | } 104 | 105 | /** 106 | * Gets a value from an array using a dot separated path. 107 | * Returns true if found and false if not. 108 | * 109 | * @param array $array array to search 110 | * @param mixed $path key path string (delimiter separated) or array of keys 111 | * @param mixed $found value that was found 112 | * @param string $delimiter key path delimiter 113 | * 114 | * @return bool 115 | */ 116 | public static function path(array $array, $path, &$found, string $delimiter = '.'): bool 117 | { 118 | if (!is_array($array)) { 119 | return false; 120 | } 121 | 122 | if (is_array($path)) { 123 | $keys = $path; 124 | } else { 125 | if (array_key_exists($path, $array)) { 126 | $found = $array[$path]; // No need to do extra processing 127 | return true; 128 | } 129 | 130 | $keys = explode($delimiter, trim($path, "{$delimiter} ")); 131 | } 132 | 133 | do { 134 | $key = array_shift($keys); 135 | 136 | if (ctype_digit($key)) { 137 | $key = (int)$key; 138 | } 139 | 140 | if (isset($array[$key])) { 141 | if ($keys) { 142 | if (is_array($array[$key])) { 143 | $array = $array[$key]; 144 | } else { 145 | break; 146 | } 147 | } else { 148 | $found = $array[$key]; 149 | return true; 150 | } 151 | } else { 152 | break; 153 | } 154 | } while ($keys); 155 | 156 | // Unable to find the value requested 157 | return false; 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/Queue.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Resque; 13 | 14 | /** 15 | * Resque queue class 16 | * 17 | * @package Resque 18 | * @author Michael Haynes 19 | */ 20 | class Queue 21 | { 22 | /** 23 | * @var Redis The Redis instance 24 | */ 25 | protected $redis; 26 | 27 | /** 28 | * @var string The name of the default queue 29 | */ 30 | protected $default; 31 | 32 | /** 33 | * Create a new queue instance 34 | * 35 | * @param string $default Name of default queue to add job to 36 | */ 37 | public function __construct(?string $default = null) 38 | { 39 | $this->redis = Redis::instance(); 40 | 41 | $this->default = $default ?: Config::read('default.jobs.queue', 'default'); 42 | } 43 | 44 | /** 45 | * Get the Queue key. 46 | * 47 | * @param string|null $queue the worker to get the key for 48 | * @param string|null $suffix to be appended to key 49 | * 50 | * @return string 51 | */ 52 | public static function redisKey(?string $queue = null, ?string $suffix = null): string 53 | { 54 | if (is_null($queue)) { 55 | return 'queues'; 56 | } 57 | 58 | return (strpos($queue, 'queue:') !== 0 ? 'queue:' : '').$queue.($suffix ? ':'.$suffix : ''); 59 | } 60 | 61 | /** 62 | * Get a job by id. 63 | * 64 | * @param string $id Job id 65 | * 66 | * @return Job job instance 67 | */ 68 | public function job(string $id): Job 69 | { 70 | return Job::load($id); 71 | } 72 | 73 | /** 74 | * Push a new job onto the queue. 75 | * 76 | * @param string|callable $job The job class 77 | * @param array $data The job data 78 | * @param string $queue The queue to add the job to 79 | * 80 | * @return Job job instance 81 | */ 82 | public function push($job, ?array $data = [], ?string $queue = null): Job 83 | { 84 | if (false !== ($delay = Config::read('default.jobs.delay', false))) { 85 | return $this->later($delay, $job, $data, $queue); 86 | } 87 | 88 | return Job::create($this->getQueue($queue), $job, $data); 89 | } 90 | 91 | /** 92 | * Queue a job for later retrieval. 93 | * Jobs are unique per queue and 94 | * are deleted upon retrieval. If a given job (payload) already exists, 95 | * it is updated with the new delay. 96 | * 97 | * @param \DateTime|int $delay This can be number of seconds or unix timestamp 98 | * @param string|callable $job The job class 99 | * @param array $data The job data 100 | * @param string $queue The queue to add the job to 101 | * 102 | * @return Job job instance 103 | */ 104 | public function later($delay, $job, array $data = [], ?string $queue = null): Job 105 | { 106 | // If it's a datetime object conver to unix time 107 | if ($delay instanceof \DateTime) { 108 | $delay = $delay->getTimestamp(); 109 | } 110 | 111 | if (!is_numeric($delay)) { 112 | throw new \InvalidArgumentException('The delay "'.$delay.'" must be an integer or DateTime object.'); 113 | } 114 | 115 | // If the delay is smaller than 3 years then assume that an interval 116 | // has been passed i.e. 600 seconds, otherwise it's a unix timestamp 117 | if ($delay < 94608000) { 118 | $delay += time(); 119 | } 120 | 121 | return Job::create($this->getQueue($queue), $job, $data, $delay); 122 | } 123 | 124 | /** 125 | * Pop the next job off of the queue. 126 | * 127 | * @param array $queues Queues to watch for new jobs 128 | * @param int $timeout Timeout if blocking 129 | * @param bool $blocking Should Redis use blocking 130 | * 131 | * @return Job|false 132 | */ 133 | public function pop(array $queues, int $timeout = 10, bool $blocking = true) 134 | { 135 | $queue = $payload = null; 136 | 137 | foreach ($queues as $key => $queue) { 138 | $queues[$key] = self::redisKey($queue); 139 | } 140 | 141 | if ($blocking) { 142 | [$queue, $payload] = $this->redis->blpop($queues, $timeout); 143 | if ($queue) { 144 | $queue = $this->redis->removeNamespace($queue); 145 | } 146 | } else { 147 | foreach ($queues as $queue) { 148 | if ($payload = $this->redis->lpop($queue)) { 149 | break; 150 | } 151 | } 152 | } 153 | 154 | if (!$queue or !$payload) { 155 | return false; 156 | } 157 | 158 | $queue = substr($queue, strlen('queue:')); 159 | 160 | return Job::loadPayload($queue, $payload); 161 | } 162 | 163 | /** 164 | * Get the size (number of pending jobs) of the specified queue. 165 | * 166 | * @param string $queue name of the queue to be checked for pending jobs 167 | * @return int The size of the queue. 168 | */ 169 | public function size(string $queue): int 170 | { 171 | return $this->redis->llen(self::redisKey($this->getQueue($queue))); 172 | } 173 | 174 | /** 175 | * Get the size (number of delayed jobs) of the specified queue. 176 | * 177 | * @param string $queue name of the queue to be checked for delayed jobs 178 | * @return int The size of the delayed queue. 179 | */ 180 | public function sizeDelayed(string $queue): int 181 | { 182 | return $this->redis->zcard(self::redisKey($this->getQueue($queue), 'delayed')); 183 | } 184 | 185 | /** 186 | * Get the queue or return the default. 187 | * 188 | * @param string|null $queue Name of queue 189 | * @return string 190 | */ 191 | protected function getQueue(?string $queue): string 192 | { 193 | return $queue ?: $this->default; 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /src/Logger/Handler/Connector.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Resque\Logger\Handler; 13 | 14 | use Symfony\Component\Console\Command\Command; 15 | use Symfony\Component\Console\Input\InputInterface; 16 | use Symfony\Component\Console\Output\OutputInterface; 17 | 18 | /** 19 | * Monolog connector class 20 | * 21 | * @package Resque 22 | * @author Michael Haynes 23 | */ 24 | class Connector 25 | { 26 | /** 27 | * @var Command command instance 28 | */ 29 | protected $command; 30 | 31 | /** 32 | * @var InputInterface input instance 33 | */ 34 | protected $input; 35 | 36 | /** 37 | * @var OutputInterface output instance 38 | */ 39 | protected $output; 40 | 41 | /** 42 | * @var array output instance 43 | */ 44 | private $connectionMap = [ 45 | 'Redis' => 'redis://(?P[a-z0-9\._-]+):(?P\d+)/(?P.+)', // redis://127.0.0.1:6379/log:%worker$ 46 | 'MongoDB' => 'mongodb://(?P[a-z0-9\._-]+):(?P\d+)/(?P[a-z0-9_]+)/(?P.+)', // mongodb://127.0.0.1:27017/dbname/log:%worker% 47 | 'CouchDB' => 'couchdb://(?P[a-z0-9\._-]+):(?P\d+)/(?P[a-z0-9_]+)', // couchdb://127.0.0.1:27017/dbname 48 | 'Amqp' => 'amqp://(?P[a-z0-9\._-]+):(?P\d+)/(?P[a-z0-9_]+)', // amqp://127.0.0.1:5763/name 49 | 'Socket' => 'socket:(?P.+)', // socket:udp://127.0.0.1:80 50 | 'Syslog' => 'syslog:(?P[a-z]+)/(?P.+)', // syslog:myfacility/local6 51 | 'ErrorLog' => 'errorlog:(?P\d)', // errorlog:0 52 | 'Cube' => 'cube:(?P.+)', // cube:udp://localhost:5000 53 | 'Rotate' => 'rotate:(?P\d+):(?P.+)', // rotate:5:path/to/output.log 54 | 'Console' => '(console|echo)(?P\b)', // console 55 | 'Off' => '(off|null)(?P\b)', // off 56 | 'Stream' => '(?:stream:)?(?P[a-z0-9/\\\[\]\(\)\~%\._-]+)', // stream:path/to/output.log | path/to/output.log 57 | ]; 58 | 59 | /** 60 | * Create a new Connector instance 61 | */ 62 | public function __construct(Command $command, InputInterface $input, OutputInterface $output) 63 | { 64 | $this->command = $command; 65 | $this->input = $input; 66 | $this->output = $output; 67 | } 68 | 69 | /** 70 | * Resolve a Monolog handler from string input 71 | * 72 | * @param string $logFormat 73 | * @throws InvalidArgumentException 74 | * 75 | * @return \Monolog\Handler\HandlerInterface 76 | */ 77 | public function resolve(string $logFormat): \Monolog\Handler\HandlerInterface 78 | { 79 | // Loop over connectionMap and see if the log format matches any of them 80 | foreach ($this->connectionMap as $connection => $match) { 81 | // Because the last connection stream is an effective catch all i.e. just specifying a 82 | // path to a file, lets make sure the user wasn't trying to use another handler but got 83 | // the format wrong. If they did then show them the correct format 84 | if ($connection == 'Stream' and stripos($logFormat, 'stream') !== 0) { 85 | $pattern = '~^(?P'.implode('|', array_keys($this->connectionMap)).')(.*)$~i'; 86 | 87 | if ($possible = $this->matches($pattern, $logFormat)) { 88 | // Map to correct key case 89 | $handler = str_replace( 90 | array_map('strtolower', array_keys($this->connectionMap)), 91 | array_keys($this->connectionMap), 92 | strtolower($possible['handler']) 93 | ); 94 | 95 | // Tell them the error of their ways 96 | $format = str_replace(['(?:', ')?', '\)'], '', $this->connectionMap[$handler]); 97 | 98 | $format = preg_replace_callback('/\(\?P<([a-z_]+)>(?:.+?)\)/', function ($m) { 99 | return ($m[1] == 'ignore') ? '' : '<'.$m[1].'>'; 100 | }, $format); 101 | 102 | throw new \InvalidArgumentException('Invalid format "'.$logFormat.'" for "'.$handler.'" handler. Should be of format "'.$format.'"'); 103 | } 104 | } 105 | 106 | if ($args = $this->matches('~^'.$match.'$~i', $logFormat)) { 107 | $connectorClass = new \ReflectionClass('Resque\Logger\Handler\Connector\\'.$connection.'Connector'); 108 | /** @var ConnectorInterface */ 109 | $connectorClass = $connectorClass->newInstance(); 110 | 111 | $handler = $connectorClass->resolve($this->command, $this->input, $this->output, $args); 112 | $handler->pushProcessor($connectorClass->processor($this->command, $this->input, $this->output, $args)); 113 | 114 | return $handler; 115 | } 116 | } 117 | 118 | throw new \InvalidArgumentException('Log format "'.$logFormat.'" is invalid'); 119 | } 120 | 121 | /** 122 | * Performs a pattern match on a string and returns just 123 | * the named matches or false if no match 124 | * 125 | * @param string $pattern 126 | * @param string $subject 127 | * 128 | * @return array|false 129 | */ 130 | private function matches(string $pattern, string $subject) 131 | { 132 | if (preg_match($pattern, $subject, $matches)) { 133 | $args = []; 134 | 135 | foreach ($matches as $key => $value) { 136 | if (!is_int($key)) { 137 | $args[$key] = $value; 138 | } 139 | } 140 | 141 | return $args; 142 | } 143 | 144 | return false; 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/Helpers/SerializableClosure.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Resque\Helpers; 13 | 14 | /** 15 | * Serializes job closure 16 | * 17 | * @package Resque 18 | * @author Michael Haynes 19 | */ 20 | final class SerializableClosure implements \Serializable 21 | { 22 | /** 23 | * The Closure instance. 24 | * 25 | * @var \Closure 26 | */ 27 | protected $closure; 28 | 29 | /** 30 | * The ReflectionFunction instance of the Closure. 31 | * 32 | * @var \ReflectionFunction 33 | */ 34 | protected $reflection; 35 | 36 | /** 37 | * The code contained by the Closure. 38 | * 39 | * @var string 40 | */ 41 | protected $code; 42 | 43 | /** 44 | * Create a new serializable Closure instance. 45 | * 46 | * @param \Closure $closure 47 | */ 48 | public function __construct(\Closure $closure) 49 | { 50 | $this->closure = $closure; 51 | 52 | $this->reflection = new \ReflectionFunction($closure); 53 | } 54 | 55 | /** 56 | * Get the unserialized Closure instance. 57 | * 58 | * @return \Closure 59 | */ 60 | public function getClosure(): \Closure 61 | { 62 | return $this->closure; 63 | } 64 | 65 | /** 66 | * Get the code for the Closure. 67 | * 68 | * @return string 69 | */ 70 | public function getCode(): string 71 | { 72 | return $this->code ?: $this->code = $this->getCodeFromFile(); 73 | } 74 | 75 | /** 76 | * Extract the code from the Closure's file. 77 | * 78 | * @return string 79 | */ 80 | protected function getCodeFromFile(): string 81 | { 82 | $file = $this->getFile(); 83 | 84 | $code = ''; 85 | 86 | // Next, we will just loop through the lines of the file until we get to the end 87 | // of the Closure. Then, we will return the complete contents of this Closure 88 | // so it can be serialized with these variables and stored for later usage. 89 | while ($file->key() < $this->reflection->getEndLine()) { 90 | $code .= $file->current(); 91 | $file->next(); 92 | } 93 | 94 | preg_match('/function\s*\(/', $code, $matches, PREG_OFFSET_CAPTURE); 95 | $begin = $matches[0][1] ?? 0; 96 | 97 | return substr($code, $begin, strrpos($code, '}') - $begin + 1); 98 | } 99 | 100 | /** 101 | * Get an SplObjectFile object at the starting line of the Closure. 102 | * 103 | * @return \SplFileObject 104 | */ 105 | protected function getFile(): \SplFileObject 106 | { 107 | $file = new \SplFileObject($this->reflection->getFileName()); 108 | 109 | $file->seek($this->reflection->getStartLine() - 1); 110 | 111 | return $file; 112 | } 113 | 114 | /** 115 | * Get the variables used by the Closure. 116 | * 117 | * @return array 118 | */ 119 | public function getVariables(): array 120 | { 121 | if (!$this->getUseIndex()) { 122 | return []; 123 | } 124 | 125 | $staticVariables = $this->reflection->getStaticVariables(); 126 | 127 | // When looping through the variables, we will only take the variables that are 128 | // specified in the use clause, and will not take any other static variables 129 | // that may be used by the Closures, allowing this to re-create its state. 130 | $usedVariables = []; 131 | 132 | foreach ($this->getUseClauseVariables() as $variable) { 133 | $variable = trim($variable, ' $&'); 134 | 135 | $usedVariables[$variable] = $staticVariables[$variable]; 136 | } 137 | 138 | return $usedVariables; 139 | } 140 | 141 | /** 142 | * Get the variables from the "use" clause. 143 | * 144 | * @return array 145 | */ 146 | protected function getUseClauseVariables(): array 147 | { 148 | $begin = strpos($code = $this->getCode(), '(', $this->getUseIndex()) + 1; 149 | 150 | return explode(',', substr($code, $begin, strpos($code, ')', $begin) - $begin)); 151 | } 152 | 153 | /** 154 | * Get the index location of the "use" clause. 155 | * 156 | * @return int 157 | */ 158 | protected function getUseIndex(): int 159 | { 160 | return stripos(strtok($this->getCode(), PHP_EOL), ' use '); 161 | } 162 | 163 | /** 164 | * Serialize the Closure instance. 165 | * 166 | * @return array 167 | */ 168 | public function __serialize(): array 169 | { 170 | return [ 171 | 'code' => $this->getCode(), 'variables' => $this->getVariables(), 172 | ]; 173 | } 174 | 175 | /** 176 | * Unserialize the Closure instance. 177 | * 178 | * @param array $payload 179 | * @return void 180 | */ 181 | public function __unserialize(array $payload): void 182 | { 183 | // We will extract the variables into the current scope so that as the Closure 184 | // is built it will inherit the scope it had before it was serialized which 185 | // will emulate the Closures existing in that scope instead of right now. 186 | extract($payload['variables']); 187 | 188 | eval('$this->closure = '.$payload['code'].';'); 189 | 190 | $this->reflection = new \ReflectionFunction($this->closure); 191 | } 192 | 193 | /** 194 | * Serialize the Closure instance. (PHP <=7.3) 195 | * 196 | * @return string|null 197 | */ 198 | public function serialize(): ?string 199 | { 200 | return serialize([ 201 | 'code' => $this->getCode(), 'variables' => $this->getVariables() 202 | ]); 203 | } 204 | 205 | /** 206 | * Unserialize the Closure instance. (PHP >=7.4) 207 | * 208 | * @param string $payload 209 | * @return void 210 | */ 211 | public function unserialize($payload): void 212 | { 213 | $payload = unserialize($payload); 214 | 215 | // We will extract the variables into the current scope so that as the Closure 216 | // is built it will inherit the scope it had before it was serialized which 217 | // will emulate the Closures existing in that scope instead of right now. 218 | extract($payload['variables']); 219 | 220 | eval('$this->closure = '.$payload['code'].';'); 221 | 222 | $this->reflection = new \ReflectionFunction($this->closure); 223 | } 224 | 225 | /** 226 | * Invoke the contained Closure. 227 | */ 228 | public function __invoke() 229 | { 230 | return call_user_func_array($this->closure, func_get_args()); 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /src/Config.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Resque; 13 | 14 | use InvalidArgumentException; 15 | use Resque\Helpers\Util; 16 | use RuntimeException; 17 | use Symfony\Component\Yaml\Exception\ParseException; 18 | use Symfony\Component\Yaml\Yaml; 19 | 20 | /** 21 | * Resque configuration class. 22 | * 23 | * @package Resque 24 | * @author Paul Litovka 25 | */ 26 | final class Config 27 | { 28 | /** 29 | * Default config file name 30 | */ 31 | public const DEFAULT_CONFIG_FILE = 'resque.php'; 32 | 33 | /** 34 | * Supported config file extensions 35 | */ 36 | public const SUPPORTED_CONFIG_EXT = ['php', 'json', 'yaml', 'yml']; 37 | 38 | /** 39 | * How long the job and worker data will remain in Redis for 40 | * after completion/shutdown in seconds. Default is one week. 41 | */ 42 | public const DEFAULT_EXPIRY_TIME = 604800; 43 | 44 | /** 45 | * @var array Configuration settings array. 46 | */ 47 | protected static $config = []; 48 | 49 | /** 50 | * Read and load data from a config file 51 | * 52 | * @param string $file The config file path 53 | * 54 | * @throws InvalidArgumentException 55 | * @throws RuntimeException 56 | * 57 | * @return array The configuration array 58 | */ 59 | public static function loadConfig(string $file = self::DEFAULT_CONFIG_FILE): array 60 | { 61 | [$path, $ext] = static::getConfigDetails($file); 62 | 63 | $config = []; 64 | 65 | switch ($ext) { 66 | case 'php': 67 | $config = static::fromPhp($path); 68 | break; 69 | case 'json': 70 | $config = static::fromJson($path); 71 | break; 72 | case 'yaml': 73 | case 'yml': 74 | $config = static::fromYaml($path); 75 | break; 76 | default: 77 | throw new RuntimeException("Could not load config file $file"); 78 | } 79 | 80 | static::setConfig($config); 81 | 82 | return $config; 83 | } 84 | 85 | /** 86 | * Set the configuration array 87 | * 88 | * @param array $config The configuration array 89 | */ 90 | public static function setConfig(array $config): void 91 | { 92 | static::$config = $config; 93 | 94 | Redis::setConfig([ 95 | 'scheme' => static::read('redis.scheme', Redis::DEFAULT_SCHEME), 96 | 'host' => static::read('redis.host', Redis::DEFAULT_HOST), 97 | 'port' => static::read('redis.port', Redis::DEFAULT_PORT), 98 | 'namespace' => static::read('redis.namespace', Redis::DEFAULT_NS), 99 | 'password' => static::read('redis.password', Redis::DEFAULT_PASSWORD), 100 | 'rw_timeout' => static::read('redis.rw_timeout', Redis::DEFAULT_RW_TIMEOUT), 101 | 'phpiredis' => static::read('redis.phpiredis', Redis::DEFAULT_PHPIREDIS), 102 | 'predis' => static::read('predis'), 103 | ]); 104 | } 105 | 106 | /** 107 | * Gets a full path to a config file and its extension 108 | * 109 | * @throws \InvalidArgumentException 110 | * 111 | * @return string[] 112 | */ 113 | protected static function getConfigDetails(string $file = self::DEFAULT_CONFIG_FILE): array 114 | { 115 | [$filename, $ext] = explode('.', basename($file)); 116 | if (!in_array($ext, self::SUPPORTED_CONFIG_EXT)) { 117 | throw new InvalidArgumentException("The config file $file is not supported. Supported extensions are: ".implode(', ', self::SUPPORTED_CONFIG_EXT)); 118 | } 119 | 120 | // Check if provided file is a valid path to a readable file 121 | if (realpath($file) && is_readable($file)) { 122 | return [realpath($file), $ext]; 123 | } 124 | 125 | $baseDir = getcwd(); 126 | $searchDirs = [ 127 | $baseDir.'/', 128 | $baseDir.'/../', 129 | $baseDir.'/../../', 130 | $baseDir.'/config/', 131 | $baseDir.'/../config/', 132 | $baseDir.'/../../config/', 133 | ]; 134 | 135 | $configFile = null; 136 | 137 | // Search for config file with any supported extension 138 | foreach ($searchDirs as $dir) { 139 | foreach (self::SUPPORTED_CONFIG_EXT as $supportedExt) { 140 | $path = "$dir$filename.$supportedExt"; 141 | if (realpath($path) && is_readable($path)) { 142 | $configFile = realpath($path); 143 | $ext = $supportedExt; 144 | break; 145 | } 146 | } 147 | } 148 | 149 | // If the config is not found, throw an exception 150 | if (!$configFile) { 151 | throw new InvalidArgumentException("The config file $file cannot be found or read."); 152 | } 153 | 154 | return [$configFile, $ext]; 155 | } 156 | 157 | /** 158 | * Gets Resque config variable 159 | * 160 | * @param string $key The key to search for (optional) 161 | * @param mixed $default If key not found returns this (optional) 162 | */ 163 | public static function read(?string $key = null, $default = null) 164 | { 165 | if (!is_null($key)) { 166 | if (false !== Util::path(static::$config, $key, $found)) { 167 | return $found; 168 | } else { 169 | return $default; 170 | } 171 | } 172 | 173 | return static::$config; 174 | } 175 | 176 | /** 177 | * Parse a YAML config file and return the config array. 178 | * 179 | * @throws \RuntimeException 180 | */ 181 | protected static function fromYaml(string $configFilePath): array 182 | { 183 | if (!class_exists(Yaml::class)) { 184 | // @codeCoverageIgnoreStart 185 | throw new RuntimeException('Missing yaml parser, symfony/yaml package is not installed.'); 186 | // @codeCoverageIgnoreEnd 187 | } 188 | 189 | $configFile = file_get_contents($configFilePath); 190 | try { 191 | $configArray = Yaml::parse($configFile); 192 | } catch (ParseException $e) { 193 | throw new RuntimeException("File $configFilePath must be valid YAML: {$e->getMessage()}"); 194 | } 195 | 196 | if (!is_array($configArray)) { 197 | throw new RuntimeException("File $configFilePath must be valid YAML"); 198 | } 199 | 200 | return $configArray; 201 | } 202 | 203 | /** 204 | * Parse a JSON config file and return the config array. 205 | * 206 | * @throws \RuntimeException 207 | */ 208 | protected static function fromJson(string $configFilePath): array 209 | { 210 | if (!function_exists('json_decode')) { 211 | // @codeCoverageIgnoreStart 212 | throw new RuntimeException('Missing JSON parser, JSON extension is not installed.'); 213 | // @codeCoverageIgnoreEnd 214 | } 215 | 216 | $configArray = json_decode(file_get_contents($configFilePath), true); 217 | if (!is_array($configArray)) { 218 | throw new RuntimeException("File $configFilePath must be valid JSON"); 219 | } 220 | 221 | return $configArray; 222 | } 223 | 224 | /** 225 | * Parse a PHP config file and return the config array. 226 | * 227 | * @throws \RuntimeException 228 | */ 229 | protected static function fromPhp(string $configFilePath): array 230 | { 231 | ob_start(); 232 | $configArray = include $configFilePath; 233 | ob_end_clean(); 234 | 235 | if (!is_array($configArray)) { 236 | throw new RuntimeException("File $configFilePath must return an array"); 237 | } 238 | 239 | return $configArray; 240 | } 241 | } 242 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](http://keepachangelog.com/) 6 | and this project adheres to [Semantic Versioning](http://semver.org/). 7 | 8 | ## [Unreleased] 9 | 10 | ## [4.0.0] - 2023-03-31 11 | 12 | ### Added 13 | 14 | - Support for PHP and JSON configuration files 15 | 16 | - `setConfig` method to set the configuration array directly (as an alternative to `loadConfig`) 17 | 18 | - Extendable console application class 19 | 20 | - Job blueprint class as a base for all jobs 21 | 22 | - Jobs' `setUp` and `tearDown` methods now receive the `Resque\Job` instance as an argument 23 | 24 | - `setQueue` method to make it possible to override the queue behavior 25 | 26 | - Docker environment for development 27 | 28 | - More type declarations for method arguments and return types 29 | 30 | - Suggestions for `ext-mongodb` extension and `mongodb/mongodb` package 31 | 32 | - Suggestion for `symfony/yaml` package, it's now optional 33 | 34 | ### Changed 35 | 36 | - Minimum PHP version is now `7.2`. Supported PHP versions are `^7.2 || ^8.0` 37 | 38 | - Default configuration file should have the name `resque` instead of `config` (e.g. `resque.php`) 39 | 40 | - Refactored structure to follow PSR-4 autoloading standard (used to follow PSR-0) 41 | 42 | - Main `Resque` class is now located under the `Resque` namespace (e.g. `Resque\Resque::push()`) 43 | 44 | - Improved deployment. Now only mandatory files are included in the dist package 45 | 46 | - Improved code intelligence by adding PHP DocBlocks comments to the main `Resque` class 47 | 48 | - Worker will log on a DEBUG level when no queues found (instead of INFO) 49 | 50 | - Bumped `monolog/monolog` dependency to `^2.5` 51 | 52 | - Bumped `predis/predis` dependency to `^2.1` 53 | 54 | - Bumped `symfony/console`, `symfony/process`, and `symfony/yaml` dependencies to `^5.4|^6.0` 55 | 56 | - Bumped `phpunit/phpunit` dependency to `^8.0|^9.0` 57 | 58 | - Several code style improvements 59 | 60 | ### Deprecated 61 | 62 | - `phpiredis` configuration option. This extension is not maintained anymore and will be removed 63 | 64 | - `Cube` logger connector. This logger is not maintained anymore and will be removed 65 | 66 | ### Removed 67 | 68 | - `Sami` documentation generator 69 | 70 | ### Fixed 71 | 72 | - Passing a command as a string when creating a Process instance in the `SpeedTest` command 73 | 74 | - Custom configuration file not being loaded in the `SpeedTest` command 75 | 76 | - MongoDB logger connector using removed classes 77 | 78 | - Incorrect return type in commands 79 | 80 | - `SeializableClosure` deprecation notice 81 | 82 | - Various Logger usage errors 83 | 84 | - Incorrect type declarations causing errors in PHP 8 85 | 86 | - Incorrect type used in the `ConsoleProcessor` class 87 | 88 | - PHP deprecation notice in the `Util` class 89 | 90 | ### Security 91 | 92 | - Added `final` keyword to classes that should not be extended 93 | 94 | ## [3.1.1] - 2023-01-31 95 | 96 | - Improve PHP8 compatibility 97 | 98 | ## [3.1.0] - 2023-01-04 99 | 100 | - Allow canceling delayed jobs (PR [#99](https://github.com/mjphaynes/php-resque/pull/99)) 101 | - Remove the proctitle extension suggestion, as it is not needed on PHP7+ 102 | 103 | ## [3.0.0] - 2023-01-03 104 | 105 | - Reduce dependency coupling (PR [#94](https://github.com/mjphaynes/php-resque/pull/94)) 106 | - Clean up code 107 | - Support Symfony components v4 and v5 108 | - Bump minimum PHP version to 7.1 109 | 110 | ## [2.2.0] - 2019-04-22 111 | 112 | - Make signals configurable by event callback (PR [#85](https://github.com/mjphaynes/php-resque/pull/85)) 113 | - Provide predis native configuration (PR [#88](https://github.com/mjphaynes/php-resque/pull/88)) 114 | - Add Travis support and CS check (PR [#72](https://github.com/mjphaynes/php-resque/pull/72)) 115 | - Use pcntl_async_signals if available (PR [#65](https://github.com/mjphaynes/php-resque/pull/65)) 116 | - Fix cli_set_process_title error on macOS (PR [#92](https://github.com/mjphaynes/php-resque/pull/92)) 117 | 118 | ## [2.1.2] - 2017-09-19 119 | 120 | - Fix job processing of the last queue (Issue [#61](https://github.com/mjphaynes/php-resque/issues/61)) 121 | 122 | ## [2.1.1] - 2017-08-31 123 | 124 | - Fix "undefined index" notice (Issue [#59](https://github.com/mjphaynes/php-resque/issues/59)) 125 | 126 | ## [2.1.0] - 2017-08-31 127 | 128 | - Add JOB_DONE event (PR [#58](https://github.com/mjphaynes/php-resque/pull/58)) 129 | - Allow remote shutdown of workers (PR [#50](https://github.com/mjphaynes/php-resque/pull/50)) 130 | - Improve documentation 131 | 132 | ## [2.0.0] - 2017-03-01 133 | 134 | - Update required Symfony components to 2.7+ or 3.x 135 | - Update required Predis version to 1.1.x 136 | - Change worker wait for log level from INFO to DEBUG (Commit [4915d51](https://github.com/mjphaynes/php-resque/commit/4915d51ca2593a743cecbab9597ad6a1314bdbed)) 137 | - Add option to allow phpiredis support (Commit [4e22e0fb](https://github.com/mjphaynes/php-resque/commit/4e22e0fb31d8658c2a1ef73a5a44c927fd88d55c)) 138 | - Add option to set Redis to read/write timeout (PR [#27](https://github.com/mjphaynes/php-resque/pull/27)) 139 | - Change code style to PSR-2 (PR [#25](https://github.com/mjphaynes/php-resque/pull/25)) 140 | - Fix closures with whitespace in their declaration (Issue [#30](https://github.com/mjphaynes/php-resque/issues/30)) 141 | - Fix job stability by reconnecting to redis after forking (Commit [cadfb09e](https://github.com/mjphaynes/php-resque/commit/cadfb09e81152cf902ef7f20e6883d29e6d1373b)) 142 | - Fix crash if the status is not set (Commit [cadfb09e](https://github.com/mjphaynes/php-resque/commit/cadfb09e81152cf902ef7f20e6883d29e6d1373b)) 143 | - Improve code style to increase PSR-2 compliance (Commit [36daf9a](https://github.com/mjphaynes/php-resque/commit/36daf9a23128e75eab15522ecc595ece8e4b6874)) 144 | - Add this changelog! 145 | 146 | ## [1.3.0] - 2016-01-22 147 | 148 | - Remove optional proctitle extension dependency (PR #18) 149 | 150 | ## [1.2.4] - 2015-04-17 151 | 152 | - Monolog line break-fix 153 | 154 | ## [1.2.3] - 2015-04-02 155 | 156 | - Monolog composer fix 157 | 158 | ## [1.2.2] - 2014-11-05 159 | 160 | - Dev dependencies bug fix 161 | 162 | ## [1.2.1] - 2014-11-05 163 | 164 | - Dependencies bug fix 165 | 166 | ## [1.2.0] - 2014-11-05 167 | 168 | - Updated symfony dependencies 169 | 170 | ## [1.1.3] - 2014-06-23 171 | 172 | - ob_clean fix 173 | 174 | ## [1.1.2] - 2014-06-23 175 | 176 | - Config file error fix 177 | 178 | ## [1.1.1] - 2014-06-23 179 | 180 | - Autoload directory fix 181 | 182 | ## [1.1.0] - 2014-02-18 183 | 184 | - Bump lib versions for Monolog & Symfony 185 | 186 | ## 1.0.0 - 2013-10-09 187 | 188 | - First public release of php-resque 189 | 190 | [unreleased]: https://github.com/mjphaynes/php-resque/compare/4.0.0...HEAD 191 | [4.0.0]: https://github.com/mjphaynes/php-resque/compare/3.1.1...4.0.0 192 | [3.1.1]: https://github.com/mjphaynes/php-resque/compare/3.1.0...3.1.1 193 | [3.1.0]: https://github.com/mjphaynes/php-resque/compare/3.0.0...3.1.0 194 | [3.0.0]: https://github.com/mjphaynes/php-resque/compare/2.2.0...3.0.0 195 | [2.2.0]: https://github.com/mjphaynes/php-resque/compare/2.1.2...2.2.0 196 | [2.1.2]: https://github.com/mjphaynes/php-resque/compare/2.1.1...2.1.2 197 | [2.1.1]: https://github.com/mjphaynes/php-resque/compare/2.1.0...2.1.1 198 | [2.1.0]: https://github.com/mjphaynes/php-resque/compare/2.0.0...2.1.0 199 | [2.0.0]: https://github.com/mjphaynes/php-resque/compare/1.3.0...2.0.0 200 | [1.3.0]: https://github.com/mjphaynes/php-resque/compare/1.2.4...1.3.0 201 | [1.2.4]: https://github.com/mjphaynes/php-resque/compare/1.2.3...1.2.4 202 | [1.2.3]: https://github.com/mjphaynes/php-resque/compare/1.2.2...1.2.3 203 | [1.2.2]: https://github.com/mjphaynes/php-resque/compare/1.2.1...1.2.2 204 | [1.2.1]: https://github.com/mjphaynes/php-resque/compare/1.2.0...1.2.1 205 | [1.2.0]: https://github.com/mjphaynes/php-resque/compare/1.1.3...1.2.0 206 | [1.1.3]: https://github.com/mjphaynes/php-resque/compare/1.1.2...1.1.3 207 | [1.1.2]: https://github.com/mjphaynes/php-resque/compare/1.1.1...1.1.2 208 | [1.1.1]: https://github.com/mjphaynes/php-resque/compare/1.1.0...1.1.1 209 | [1.1.0]: https://github.com/mjphaynes/php-resque/compare/1.0.0...1.1.0 210 | -------------------------------------------------------------------------------- /src/Redis.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Resque; 13 | 14 | use Predis\Client; 15 | 16 | /** 17 | * Resque redis class 18 | * 19 | * @see https://redis.io/commands 20 | * 21 | * @package Resque 22 | * @author Michael Haynes 23 | */ 24 | class Redis 25 | { 26 | /** 27 | * Default Redis connection scheme 28 | */ 29 | public const DEFAULT_SCHEME = 'tcp'; 30 | 31 | /** 32 | * Default Redis connection host 33 | */ 34 | public const DEFAULT_HOST = '127.0.0.1'; 35 | 36 | /** 37 | * Default Redis connection port 38 | */ 39 | public const DEFAULT_PORT = 6379; 40 | 41 | /** 42 | * Default Redis namespace 43 | */ 44 | public const DEFAULT_NS = 'resque'; 45 | 46 | /** 47 | * Default Redis AUTH password 48 | */ 49 | public const DEFAULT_PASSWORD = null; 50 | 51 | /** 52 | * Default Redis Read Write Timeout 53 | */ 54 | public const DEFAULT_RW_TIMEOUT = 60; 55 | 56 | /** 57 | * Default Redis option for using phpiredis or not 58 | * @deprecated Since 4.0.0, ext-phpiredis is abandoned and will be removed in the future 59 | */ 60 | public const DEFAULT_PHPIREDIS = false; 61 | 62 | /** 63 | * @var array Default configuration 64 | */ 65 | protected static $config = [ 66 | 'scheme' => self::DEFAULT_SCHEME, 67 | 'host' => self::DEFAULT_HOST, 68 | 'port' => self::DEFAULT_PORT, 69 | 'namespace' => self::DEFAULT_NS, 70 | 'password' => self::DEFAULT_PASSWORD, 71 | 'rw_timeout' => self::DEFAULT_RW_TIMEOUT, 72 | 'phpiredis' => self::DEFAULT_PHPIREDIS, 73 | ]; 74 | 75 | /** 76 | * @var Redis Redis instance 77 | */ 78 | protected static $instance = null; 79 | 80 | /** 81 | * @var Client The Predis instance 82 | */ 83 | protected $redis; 84 | 85 | /** 86 | * @var string Redis namespace 87 | */ 88 | protected $namespace; 89 | 90 | /** 91 | * @var array List of all commands in Redis that supply a key as their 92 | * first argument. Used to prefix keys with the Resque namespace. 93 | */ 94 | protected $keyCommands = [ 95 | 'exists', 96 | 'del', 97 | 'type', 98 | 'keys', 99 | 'expire', 100 | 'ttl', 101 | 'move', 102 | 'set', 103 | 'setex', 104 | 'get', 105 | 'getset', 106 | 'setnx', 107 | 'incr', 108 | 'incrby', 109 | 'decr', 110 | 'decrby', 111 | 'rpush', 112 | 'lpush', 113 | 'llen', 114 | 'lrange', 115 | 'ltrim', 116 | 'lindex', 117 | 'lset', 118 | 'lrem', 119 | 'lpop', 120 | 'blpop', 121 | 'rpop', 122 | 'sadd', 123 | 'srem', 124 | 'spop', 125 | 'scard', 126 | 'sismember', 127 | 'smembers', 128 | 'srandmember', 129 | 'hdel', 130 | 'hexists', 131 | 'hget', 132 | 'hgetall', 133 | 'hincrby', 134 | 'hincrbyfloat', 135 | 'hkeys', 136 | 'hlen', 137 | 'hmget', 138 | 'hmset', 139 | 'hset', 140 | 'hsetnx', 141 | 'hvals', 142 | 'zadd', 143 | 'zrem', 144 | 'zrange', 145 | 'zrevrange', 146 | 'zrangebyscore', 147 | 'zrevrangebyscore', 148 | 'zcard', 149 | 'zscore', 150 | 'zremrangebyscore', 151 | 'sort', 152 | // sinterstore 153 | // sunion 154 | // sunionstore 155 | // sdiff 156 | // sdiffstore 157 | // sinter 158 | // smove 159 | // rename 160 | // rpoplpush 161 | // mget 162 | // msetnx 163 | // mset 164 | // renamenx 165 | ]; 166 | 167 | /** 168 | * Establish a Redis connection. 169 | * 170 | * @param array $config Array of configuration settings 171 | */ 172 | public function __construct(array $config = []) 173 | { 174 | $predisParams = []; 175 | $predisOptions = []; 176 | if (!empty($config['predis'])) { 177 | $predisParams = $config['predis']['config']; 178 | $predisOptions = $config['predis']['options']; 179 | } else { 180 | foreach (['scheme', 'host', 'port'] as $key) { 181 | if (!isset($config[$key])) { 182 | throw new \InvalidArgumentException("key '{$key}' is missing in redis configuration"); 183 | } 184 | } 185 | 186 | // non-optional configuration parameters 187 | $predisParams = [ 188 | 'scheme' => $config['scheme'], 189 | 'host' => $config['host'], 190 | 'port' => $config['port'], 191 | ]; 192 | 193 | // setup password 194 | if (!empty($config['password'])) { 195 | $predisParams['password'] = $config['password']; 196 | } 197 | 198 | // setup read/write timeout 199 | if (!empty($config['rw_timeout'])) { 200 | $predisParams['read_write_timeout'] = $config['rw_timeout']; 201 | } 202 | 203 | // setup predis client options 204 | if (!empty($config['phpiredis'])) { 205 | $predisOptions = [ 206 | 'connections' => [ 207 | 'tcp' => 'Predis\Connection\PhpiredisStreamConnection', 208 | 'unix' => 'Predis\Connection\PhpiredisSocketConnection', 209 | ], 210 | ]; 211 | } 212 | } 213 | 214 | // create Predis client 215 | $this->redis = $this->initializePredisClient($predisParams, $predisOptions); 216 | 217 | // setup namespace 218 | if (!empty($config['namespace'])) { 219 | $this->setNamespace($config['namespace']); 220 | } else { 221 | $this->setNamespace(self::DEFAULT_NS); 222 | } 223 | 224 | // Do this to test connection is working now rather than later 225 | $this->redis->connect(); 226 | } 227 | 228 | /** 229 | * Establish a Redis connection 230 | * 231 | * @return Redis 232 | */ 233 | public static function instance(): Redis 234 | { 235 | if (!static::$instance) { 236 | static::$instance = new static(static::$config); 237 | } 238 | 239 | return static::$instance; 240 | } 241 | 242 | /** 243 | * Set the Redis config 244 | * 245 | * @param array $config Array of configuration settings 246 | */ 247 | public static function setConfig(array $config): void 248 | { 249 | static::$config = array_merge(static::$config, $config); 250 | } 251 | 252 | /** 253 | * initialize the redis member with a predis client. 254 | * isolated call for testability 255 | * @param array $config predis config parameters 256 | * @param array $options predis optional parameters 257 | * 258 | * @return Client 259 | */ 260 | public function initializePredisClient(array $config, array $options): Client 261 | { 262 | return new Client($config, $options); 263 | } 264 | 265 | /** 266 | * Set Redis namespace 267 | * 268 | * @param string $namespace New namespace 269 | */ 270 | public function setNamespace(string $namespace): void 271 | { 272 | if (substr($namespace, -1) !== ':') { 273 | $namespace .= ':'; 274 | } 275 | 276 | $this->namespace = $namespace; 277 | } 278 | 279 | /** 280 | * Get Redis namespace 281 | * 282 | * @return string 283 | */ 284 | public function getNamespace(): string 285 | { 286 | return $this->namespace; 287 | } 288 | 289 | /** 290 | * Add Redis namespace to a string 291 | * 292 | * @param array|string $string String to namespace 293 | * @return array|string 294 | */ 295 | public function addNamespace($string) 296 | { 297 | if (is_array($string)) { 298 | foreach ($string as &$str) { 299 | $str = $this->addNamespace($str); 300 | } 301 | 302 | return $string; 303 | } 304 | 305 | if (strpos($string, $this->namespace) !== 0) { 306 | $string = $this->namespace . $string; 307 | } 308 | 309 | return $string; 310 | } 311 | 312 | /** 313 | * Remove Redis namespace from string 314 | * 315 | * @param string $string String to de-namespace 316 | * @return string 317 | */ 318 | public function removeNamespace(string $string): string 319 | { 320 | $prefix = $this->namespace; 321 | 322 | if (substr($string, 0, strlen($prefix)) == $prefix) { 323 | $string = substr($string, strlen($prefix), strlen($string)); 324 | } 325 | 326 | return $string; 327 | } 328 | 329 | /** 330 | * Dynamically pass calls to the Predis. 331 | * 332 | * @param string $method Method to call 333 | * @param array $parameters Arguments to send to method 334 | */ 335 | public function __call(string $method, array $parameters) 336 | { 337 | if (in_array($method, $this->keyCommands)) { 338 | $parameters[0] = $this->addNamespace($parameters[0]); 339 | } 340 | 341 | return call_user_func_array([$this->redis, $method], $parameters); 342 | } 343 | } 344 | -------------------------------------------------------------------------------- /src/Console/Command/Command.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Resque\Console\Command; 13 | 14 | use Resque\Config; 15 | use Resque\Event; 16 | use Resque\Helpers\Util; 17 | use Resque\Logger; 18 | use Resque\Redis; 19 | use Symfony\Component\Console\Input\InputInterface; 20 | use Symfony\Component\Console\Input\InputOption; 21 | use Symfony\Component\Console\Output\OutputInterface; 22 | 23 | /** 24 | * Main Command class 25 | * 26 | * @package Resque 27 | * @author Michael Haynes 28 | */ 29 | class Command extends \Symfony\Component\Console\Command\Command 30 | { 31 | /** 32 | * @var Logger The logger instance 33 | */ 34 | protected $logger; 35 | 36 | /** 37 | * @var array Config array 38 | */ 39 | protected $config = []; 40 | 41 | /** 42 | * @var array Config to options mapping 43 | */ 44 | protected $configOptionMap = [ 45 | 'include' => 'include', 46 | 'scheme' => 'redis.scheme', 47 | 'host' => 'redis.host', 48 | 'port' => 'redis.port', 49 | 'namespace' => 'redis.namespace', 50 | 'password' => 'redis.password', 51 | 'verbose' => 'default.verbose', 52 | 'queue' => 'default.jobs.queue', 53 | 'delay' => 'default.jobs.delay', 54 | 'queue' => 'default.workers.queue', 55 | 'blocking' => 'default.workers.blocking', 56 | 'interval' => 'default.workers.interval', 57 | 'timeout' => 'default.workers.timeout', 58 | 'memory' => 'default.workers.memory', 59 | 'log' => 'log', 60 | 'listenhost' => 'socket.listen.host', 61 | 'listenport' => 'socket.listen.port', 62 | 'listenretry' => 'socket.listen.retry', 63 | 'listentimeout' => 'socket.listen.timeout', 64 | 'connecthost' => 'socket.connect.host', 65 | 'connectport' => 'socket.connect.port', 66 | 'connecttimeout' => 'socket.connect.timeout', 67 | 'json' => 'socket.json', 68 | ]; 69 | 70 | /** 71 | * Globally sets some input options that are available for all commands 72 | * 73 | * @param array $definitions List of command definitions 74 | * @return array 75 | */ 76 | protected function mergeDefinitions(array $definitions): array 77 | { 78 | return array_merge( 79 | $definitions, 80 | [ 81 | new InputOption('config', 'c', InputOption::VALUE_OPTIONAL, 'Path to config file. Inline options override.', Config::DEFAULT_CONFIG_FILE), 82 | new InputOption('include', 'I', InputOption::VALUE_OPTIONAL, 'Path to include php file'), 83 | new InputOption('host', 'H', InputOption::VALUE_OPTIONAL, 'The Redis hostname.', Redis::DEFAULT_HOST), 84 | new InputOption('port', 'p', InputOption::VALUE_OPTIONAL, 'The Redis port.', Redis::DEFAULT_PORT), 85 | new InputOption('scheme', null, InputOption::VALUE_REQUIRED, 'The Redis scheme to use.', Redis::DEFAULT_SCHEME), 86 | new InputOption('namespace', null, InputOption::VALUE_REQUIRED, 'The Redis namespace to use. This is prefixed to all keys.', Redis::DEFAULT_NS), 87 | new InputOption('password', null, InputOption::VALUE_OPTIONAL, 'The Redis AUTH password.'), 88 | new InputOption('log', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Specify the handler(s) to use for logging.'), 89 | new InputOption('events', 'e', InputOption::VALUE_NONE, 'Outputs all events to the console, for debugging.'), 90 | ] 91 | ); 92 | } 93 | 94 | /** 95 | * Initialises the command just after the input has been validated. 96 | * 97 | * This is mainly useful when a lot of commands extends one main command 98 | * where some things need to be initialised based on the input arguments and options. 99 | * 100 | * @param InputInterface $input An InputInterface instance 101 | * @param OutputInterface $output An OutputInterface instance 102 | * 103 | * @return void 104 | */ 105 | protected function initialize(InputInterface $input, OutputInterface $output): void 106 | { 107 | $this->parseConfig($input->getOptions(), $this->getNativeDefinition()->getOptionDefaults()); 108 | $config = $this->getConfig(); 109 | 110 | // Configure Redis 111 | Redis::setConfig([ 112 | 'scheme' => $config['scheme'], 113 | 'host' => $config['host'], 114 | 'port' => $config['port'], 115 | 'namespace' => $config['namespace'], 116 | 'password' => $config['password'], 117 | ]); 118 | 119 | // Set the verbosity 120 | if (array_key_exists('verbose', $config)) { 121 | if (!$input->getOption('verbose') and !$input->getOption('quiet') and is_int($config['verbose'])) { 122 | $output->setVerbosity($config['verbose']); 123 | } else { 124 | $this->config['verbose'] = $output->getVerbosity(); 125 | } 126 | } 127 | 128 | // Set the monolog loggers, it's possible to speficfy multiple handlers 129 | $logs = array_key_exists('log', $config) ? array_unique($config['log']) : []; 130 | empty($logs) and $logs[] = 'console'; 131 | 132 | $handlerConnector = new \Resque\Logger\Handler\Connector($this, $input, $output); 133 | 134 | $handlers = []; 135 | foreach ($logs as $log) { 136 | $handlers[] = $handlerConnector->resolve($log); 137 | } 138 | 139 | $this->logger = new Logger($handlers); 140 | 141 | // Unset some variables so as not to pass to include file 142 | unset($logs, $handlerConnector, $handlers); 143 | 144 | // Include file? 145 | if (array_key_exists('include', $config) and !empty($include = $config['include'])) { 146 | if ( 147 | !($includeFile = realpath(dirname($include).'/'.basename($include))) or 148 | !is_readable($includeFile) or !is_file($includeFile) or 149 | substr($includeFile, -4) !== '.php' 150 | ) { 151 | throw new \InvalidArgumentException('The include file "'.$include.'" is not a readable php file.'); 152 | } 153 | 154 | try { 155 | require_once $includeFile; 156 | } catch (\Exception $e) { 157 | throw new \RuntimeException('The include file "'.$include.'" threw an exception: "'.$e->getMessage().'" on line '.$e->getLine()); 158 | } 159 | } 160 | 161 | // This outputs all the events that are fired, useful for learning 162 | // about when events are fired in the command flow 163 | if (array_key_exists('events', $config) and $config['events'] === true) { 164 | Event::listen('*', function ($event) use ($output): void { 165 | $data = array_map( 166 | function ($d) { 167 | /** @var mixed $d */ 168 | $d instanceof \Exception and ($d = '"'.$d->getMessage().'"'); 169 | is_array($d) and ($d = '['.implode(',', $d).']'); 170 | 171 | return (string)$d; 172 | }, 173 | array_slice(func_get_args(), 1) 174 | ); 175 | 176 | $output->writeln('-> event:'.Event::eventName($event).'('.implode(',', $data).')'); 177 | }); 178 | } 179 | } 180 | 181 | /** 182 | * Should the console output be of the polling format 183 | * 184 | * @return bool 185 | */ 186 | public function pollingConsoleOutput(): bool 187 | { 188 | return false; 189 | } 190 | 191 | /** 192 | * Helper function that passes through to logger instance 193 | * 194 | * @see Logger::log for more information 195 | */ 196 | public function log() 197 | { 198 | return call_user_func_array([$this->logger, 'log'], func_get_args()); 199 | } 200 | 201 | /** 202 | * Parses the configuration file 203 | * 204 | * @param array $config 205 | * @param array $defaults 206 | * 207 | * @return bool 208 | */ 209 | protected function parseConfig(array $config, array $defaults): bool 210 | { 211 | if (array_key_exists('config', $config)) { 212 | $configFileData = Config::loadConfig($config['config']); 213 | 214 | foreach ($config as $key => &$value) { 215 | // If the config value is equal to the default value set in the command then 216 | // have a look at the config file. This is so that the config options can be 217 | // over-ridden in the command line. 218 | if ( 219 | isset($this->configOptionMap[$key]) and 220 | ( 221 | ($key === 'verbose' or $value === $defaults[$key]) and 222 | (false !== Util::path($configFileData, $this->configOptionMap[$key], $found)) 223 | ) 224 | ) { 225 | switch ($key) { 226 | // Need to make sure the log handlers are in the correct format 227 | case 'log': 228 | $value = []; 229 | foreach ((array)$found as $handler => $target) { 230 | $handler = strtolower($handler); 231 | 232 | if ($target !== true) { 233 | $handler .= ':'; 234 | 235 | if (in_array($handler, ['redis:', 'mongodb:', 'couchdb:', 'amqp:'])) { 236 | $handler .= '//'; 237 | } 238 | 239 | $handler .= $target; 240 | } 241 | 242 | $value[] = $handler; 243 | } 244 | 245 | break; 246 | default: 247 | $value = $found; 248 | break; 249 | } 250 | } 251 | } 252 | 253 | $this->config = $config; 254 | 255 | return true; 256 | } 257 | 258 | return false; 259 | } 260 | 261 | /** 262 | * Returns all config items or a specific one 263 | * 264 | * @param string|null $key 265 | * @return array|string 266 | */ 267 | protected function getConfig(?string $key = null) 268 | { 269 | if (!is_null($key)) { 270 | if (!array_key_exists($key, $this->config)) { 271 | throw new \InvalidArgumentException('Config key "'.$key.'" does not exist. Valid keys are: "'.implode(', ', array_keys($this->config)).'"'); 272 | } 273 | 274 | return $this->config[$key]; 275 | } 276 | 277 | return $this->config; 278 | } 279 | } 280 | -------------------------------------------------------------------------------- /src/Console/Command/Socket/Receive.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Resque\Console\Command\Socket; 13 | 14 | use Resque\Job; 15 | use Resque\Host; 16 | use Resque\Worker; 17 | use Resque\Socket; 18 | use Resque\Helpers\Util; 19 | use Resque\Console\Command\Command; 20 | use Symfony\Component\Console\Input\InputArgument; 21 | use Symfony\Component\Console\Input\InputInterface; 22 | use Symfony\Component\Console\Input\InputDefinition; 23 | use Symfony\Component\Console\Input\InputOption; 24 | use Symfony\Component\Console\Input\StringInput; 25 | use Symfony\Component\Console\Output\OutputInterface; 26 | 27 | /** 28 | * TCP receive command class 29 | * 30 | * @package Resque 31 | * @author Michael Haynes 32 | */ 33 | final class Receive extends Command 34 | { 35 | protected function configure(): void 36 | { 37 | $this->setName('socket:receive') 38 | ->setDefinition($this->mergeDefinitions([ 39 | new InputOption('listenhost', null, InputOption::VALUE_OPTIONAL, 'The host to listen on.', '0.0.0.0'), 40 | new InputOption('listenport', null, InputOption::VALUE_OPTIONAL, 'The port to listen on.', Socket\Server::DEFAULT_PORT), 41 | new InputOption('listenretry', null, InputOption::VALUE_NONE, 'If can\'t bind address or port then retry every seconds until it can.'), 42 | new InputOption('listentimeout', 't', InputOption::VALUE_OPTIONAL, 'The retry timeout time (seconds).', 10), 43 | ])) 44 | ->setDescription('Listens to socket in order to receive events') 45 | ->setHelp('Listens to socket in order to receive events') 46 | ; 47 | } 48 | 49 | protected function execute(InputInterface $input, OutputInterface $output): int 50 | { 51 | $host = $this->getConfig('listenhost'); 52 | $port = $this->getConfig('listenport'); 53 | $retry = $this->getConfig('listenretry'); 54 | $timeout = $this->getConfig('listentimeout'); 55 | $server = new Socket\Server(['ip' => $host, 'port' => $port], $this->logger); 56 | 57 | do { 58 | try { 59 | $server->start(); 60 | } catch (Socket\SocketException $e) { 61 | if ($retry) { 62 | $server->log('Socket server failure: "'. $e->getMessage().'". Retrying in '.$timeout.' seconds...'); 63 | sleep($timeout); 64 | continue; 65 | } 66 | throw $e; 67 | } 68 | 69 | break; 70 | } while ($retry); 71 | 72 | $command = $this; 73 | 74 | $server->onConnect(function ($server, &$client, $input): void { 75 | // 76 | }); 77 | 78 | $server->onDisconnect(function ($server, &$client, $message): void { 79 | $server->send($client, $message); 80 | }); 81 | 82 | $server->onReceive(function ($server, &$client, $input) use ($command) { 83 | if (!($data = json_decode(trim($input), true))) { 84 | $data = trim($input); 85 | } 86 | 87 | if (is_array($data)) { 88 | $cmd = $data['cmd']; 89 | unset($data['cmd']); 90 | } else { 91 | try { 92 | $input = new StringInput($data, new InputDefinition([ 93 | new InputArgument('cmd', InputArgument::REQUIRED), 94 | new InputArgument('id', InputArgument::OPTIONAL), 95 | new InputOption('force', 'f', InputOption::VALUE_NONE), 96 | new InputOption('json', 'j', InputOption::VALUE_NONE), 97 | ])); 98 | 99 | $cmd = $input->getArgument('cmd'); 100 | $data = [ 101 | 'id' => $input->getArgument('id'), 102 | 'force' => $input->getOption('force'), 103 | 'json' => $input->getOption('json'), 104 | ]; 105 | } catch (\Exception $e) { 106 | $server->send($client, 'Command error: '.$e->getMessage()); 107 | return Command::FAILURE; 108 | } 109 | } 110 | 111 | switch (strtolower($cmd)) { 112 | case 'shell': 113 | $server->send($client, 'Connected to php-resque on '.$server.'. To quit, type "quit"'); 114 | break; 115 | 116 | case 'workers': 117 | $workers = Worker::hostWorkers(); 118 | 119 | if (empty($workers)) { 120 | $response = ['ok' => 0, 'message' => 'There are no workers running on this host.']; 121 | $server->send($client, $data['json'] ? json_encode($response) : $response['message']); 122 | return Command::FAILURE; 123 | } 124 | 125 | if ($data['json']) { 126 | $response = ['ok' => 1, 'data' => []]; 127 | 128 | foreach ($workers as $i => $worker) { 129 | $response['data'][] = $worker->getPacket(); 130 | } 131 | 132 | $server->send($client, json_encode($response)); 133 | } else { 134 | $table = new \Resque\Helpers\Table($command); 135 | $table->setHeaders(['#', 'Status', 'ID', 'Running for', 'Running job', 'P', 'C', 'F', 'Interval', 'Timeout', 'Memory (Limit)']); 136 | 137 | foreach ($workers as $i => $worker) { 138 | $packet = $worker->getPacket(); 139 | 140 | $table->addRow([ 141 | $i + 1, 142 | Worker::$statusText[$packet['status']], 143 | (string)$worker, 144 | Util::human_time_diff($packet['started']), 145 | !empty($packet['job_id']) ? $packet['job_id'].' for '.Util::human_time_diff($packet['job_started']) : '-', 146 | $packet['processed'], 147 | $packet['cancelled'], 148 | $packet['failed'], 149 | $packet['interval'], 150 | $packet['timeout'], 151 | Util::bytes($packet['memory']).' ('.$packet['memory_limit'].' MB)', 152 | ]); 153 | } 154 | 155 | $server->send($client, (string)$table); 156 | } 157 | 158 | break; 159 | case 'worker:start': 160 | case 'worker:restart': 161 | $response = ['ok' => 0, 'message' => 'This command is not yet supported remotely.']; 162 | $server->send($client, $data['json'] ? json_encode($response) : $response['message']); 163 | break; 164 | case 'worker:pause': 165 | case 'worker:resume': 166 | case 'worker:stop': 167 | case 'worker:cancel': 168 | $id = preg_replace('/[^a-z0-9\*:,\.;-]/i', '', $data['id']); 169 | 170 | if (!empty($id)) { 171 | if (false === ($worker = Worker::hostWorker($id))) { 172 | if ($data['json']) { 173 | $response = ['ok' => 0, 'message' => 'Invalid worker id.']; 174 | $server->send($client, json_encode($response)); 175 | } else { 176 | $server->send($client, "Usage:\n\t{$cmd} \n\n". 177 | "Help: You must specify a valid worker id, to get a \n". 178 | "list of workers use the \"workers\" command."); 179 | } 180 | return Command::INVALID; 181 | } 182 | 183 | $workers = [$worker]; 184 | } else { 185 | $workers = Worker::hostWorkers(); 186 | 187 | if (empty($workers)) { 188 | $response = ['ok' => 0, 'message' => 'There are no workers on this host.']; 189 | $server->send($client, $data['json'] ? json_encode($response) : $response['message']); 190 | return Command::FAILURE; 191 | } 192 | } 193 | 194 | $cmd = $data['force'] ? 'worker:term' : $cmd; 195 | 196 | $signals = [ 197 | 'worker:pause' => SIGUSR2, 198 | 'worker:resume' => SIGCONT, 199 | 'worker:stop' => SIGQUIT, 200 | 'worker:term' => SIGTERM, 201 | 'worker:cancel' => SIGUSR1, 202 | ]; 203 | 204 | $messages = [ 205 | 'worker:pause' => 'Paused worker %s', 206 | 'worker:resume' => 'Resumed worker %s', 207 | 'worker:stop' => 'Stopped worker %s', 208 | 'worker:term' => 'Force stopped worker %s', 209 | 'worker:cancel' => 'Cancelled running job on worker %s', 210 | ]; 211 | 212 | $response = ['ok' => 1, 'data' => []]; 213 | 214 | foreach ($workers as $worker) { 215 | $pid = $worker->getPid(); 216 | 217 | if ($cmd == 'worker:cancel') { 218 | $packet = $worker->getPacket(); 219 | $job_pid = (int)$packet['job_pid']; 220 | 221 | if ($job_pid and posix_kill($job_pid, 0)) { 222 | $pid = $job_pid; 223 | } else { 224 | $response['data'][] = ['ok' => 0, 'message' => 'The worker '.$worker.' has no running job to cancel.']; 225 | continue; 226 | } 227 | } 228 | 229 | if (posix_kill($pid, $signals[$cmd])) { 230 | $response['data'][] = ['ok' => 1, 'message' => sprintf($messages[$cmd], $worker)]; 231 | } else { 232 | $response['data'][] = ['ok' => 0, 'message' => 'There was an error sending the signal, please try again.']; 233 | } 234 | } 235 | 236 | $server->send($client, $data['json'] ? json_encode($response) : implode(PHP_EOL, array_map(function ($d) { 237 | return $d['message']; 238 | }, $response['data']))); 239 | 240 | break; 241 | case 'job:queue': 242 | $response = ['ok' => 0, 'message' => 'Cannot queue remotely as it makes no sense. Use command `resque job:queue [--queue= [--delay=]]` locally.']; 243 | $server->send($client, $data['json'] ? json_encode($response) : $response['message']); 244 | 245 | break; 246 | case 'cleanup': 247 | $host = new Host(); 248 | $cleaned_hosts = $host->cleanup(); 249 | 250 | $worker = new Worker('*'); 251 | $cleaned_workers = $worker->cleanup(); 252 | $cleaned_hosts = array_merge_recursive($cleaned_hosts, $host->cleanup()); 253 | 254 | $cleaned_jobs = Job::cleanup(); 255 | 256 | if ($data['json']) { 257 | $response = ['ok' => 1, 'data' => array_merge($cleaned_hosts, $cleaned_workers, $cleaned_jobs)]; 258 | $server->send($client, json_encode($response)); 259 | } else { 260 | $output = 'Cleaned hosts: '.json_encode($cleaned_hosts['hosts']).PHP_EOL. 261 | 'Cleaned workers: '.json_encode(array_merge($cleaned_hosts['workers'], $cleaned_workers)).PHP_EOL. 262 | 'Cleaned '.$cleaned_jobs['zombie'].' zombie job'.($cleaned_jobs['zombie'] == 1 ? '' : 's').PHP_EOL. 263 | 'Cleared '.$cleaned_jobs['processed'].' processed job'.($cleaned_jobs['processed'] == 1 ? '' : 's'); 264 | } 265 | 266 | $server->send($client, $output); 267 | 268 | break; 269 | case 'shutdown': 270 | $server->shutdown(); 271 | break; 272 | case 'quit': 273 | case 'exit': 274 | $server->disconnect($client); 275 | break; 276 | default: 277 | $response = ['ok' => 0, 'message' => 'Sorry, I don\'t know what to do with command "'.$cmd.'".']; 278 | $server->send($client, $data['json'] ? json_encode($response) : $response['message']); 279 | break; 280 | } 281 | }); 282 | 283 | $server->run(); 284 | 285 | return Command::SUCCESS; 286 | } 287 | 288 | public function pollingConsoleOutput(): bool 289 | { 290 | return true; 291 | } 292 | } 293 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # php-resque 2 | 3 | php-resque (pronounced like "rescue") is a Redis-backed library for creating 4 | background jobs, placing those jobs on multiple queues, and processing them later. 5 | 6 | --- 7 | 8 | #### Contents 9 | 10 | - [Background](#background) 11 | - [Requirements](#requirements) 12 | - [Getting Started](#getting-started) 13 | - [Jobs](#jobs) 14 | - [Defining Jobs](#defining-jobs) 15 | - [Queueing Jobs](#queueing-jobs) 16 | - [Delaying Jobs](#delaying-jobs) 17 | - [Job Statuses](#job-statuses) 18 | - [Workers](#workers) 19 | - [Signals](#signals) 20 | - [Forking](#forking) 21 | - [Autoload Job Classes](#autoload-job-classes) 22 | - [Commands & Options](#commands--options) 23 | - [Logging](#logging) 24 | - [Event/Hook System](#eventhook-system) 25 | - [Configuration Options](#configuration-options) 26 | - [Redis](#redis) 27 | - [Contributing](#contributing) 28 | - [Contributors](#contributors) 29 | 30 | --- 31 | 32 | ## Background 33 | 34 | This version of php-resque is based on the work originally done by [chrisboulton](https://github.com/chrisboulton/php-resque) where 35 | he ported the [ruby version](https://github.com/resque/resque) of the same name that was created by [GitHub](http://github.com/blog/542-introducing-resque). 36 | 37 | The reasoning behind rewriting the previous work is to add better support for horizontal scaling of worker 38 | servers and to improve job failure tolerance to create a very highly available system. Integration 39 | with Monolog means that very verbose logging is achievable which makes it far easier to solve bugs across 40 | distributed systems. And an extensive events/hooks system enables deeper integration and stats gathering. 41 | 42 | This version provides features such as: 43 | 44 | - Workers can be distributed between multiple machines. 45 | - Resilient to memory leaks (jobs are run on forked processes). 46 | - Expects and logs failure. 47 | - Logging uses Monolog. 48 | - Ability to push Closures to queues. 49 | - Job status and output tracking. 50 | - Jobs will fail cleanly if out of memory or if maximum execution time is reached. 51 | - Will mark a job as failed, if a forked child running a job does not exit with a status code of 0. 52 | - Has a built-in event system to enable hooks for deep integration. 53 | - Support for priorities (queues). 54 | 55 | _This version is not a direct port of Github's Resque and therefore is not compatible with it, or their web interface._ 56 | A Resque web interface built with Symfony 3.x for this version can be found [on GitHub](https://github.com/xelan/resque-webui-bundle/). 57 | 58 | ## Requirements 59 | 60 | You must have the following installed to run php-resque: 61 | 62 | - [Redis](http://redis.io/) 63 | - [PHP 7.2+](http://php.net/) 64 | - [PCNTL PHP extension](http://php.net/manual/en/book.pcntl.php) 65 | - [Composer](http://getcomposer.org/) 66 | 67 | Optional: 68 | 69 | - [Phpiredis](https://github.com/nrk/phpiredis) 70 | 71 | --- 72 | 73 | ## Getting Started 74 | 75 | The easiest way to work with php-resque is when it's installed as a [Composer package](https://packagist.org/packages/mjphaynes/php-resque) inside your project. 76 | 77 | Add php-resque to your application by running: 78 | 79 | composer require mjphaynes/php-resque 80 | 81 | If you haven't already, add the Composer autoloader to your project's bootstrap: 82 | 83 | ```php 84 | require 'vendor/autoload.php'; 85 | ``` 86 | 87 | ## Jobs 88 | 89 | ### Defining Jobs 90 | 91 | Each job should be in its class, and php-resque has a blueprint for building them. 92 | 93 | ```php 94 | use Resque\Blueprint\Job as JobBlueprint; 95 | use Resque\Job; 96 | 97 | class MyJob extends JobBlueprint 98 | { 99 | /** 100 | * Runs any required logic before the job is performed. 101 | * 102 | * @param Job $job Current job instance 103 | */ 104 | public function setUp(Job $job): void 105 | { 106 | } 107 | 108 | /** 109 | * Actual job logic. 110 | * 111 | * @param array $args Arguments passed to the job 112 | * @param Job $job Current job instance 113 | */ 114 | public function perform(array $args, Job $job): void 115 | { 116 | // Do some work 117 | } 118 | 119 | /** 120 | * Runs after the job is performed. 121 | * 122 | * @param Job $job Current job instance 123 | */ 124 | public function tearDown(Job $job): void 125 | { 126 | } 127 | } 128 | 129 | ``` 130 | 131 | When the job is run, the class will be instantiated and any arguments will be sent as 132 | arguments to the perform method. The current job instance (`Resque\Job`) is passed 133 | to the perform method as the second argument. 134 | 135 | Any exception thrown by a job will result in the job failing - be careful here and make 136 | sure you handle the exceptions that shouldn't result in a job failing. If you want to 137 | cancel a job (instead of having it fail) then you can throw a `Resque\Exception\Cancel` 138 | exception and the job will be marked as canceled. 139 | 140 | Jobs can also have `setUp` and `tearDown` methods. If a `setUp` method is defined, it will 141 | be called before the perform method is run. The `tearDown` method if defined, will be 142 | called after the job finishes. If an exception is thrown in the `setUp` method the perform 143 | method will not be run. This is useful for cases where you have different jobs that require 144 | the same bootstrap, for instance, a database connection. 145 | 146 | ### Queueing Jobs 147 | 148 | To add a new job to the queue use the `Resque::push` method. 149 | 150 | ```php 151 | $job = Resque::push(MyJob::class, ['arg1' => true, 'arg2']); 152 | ``` 153 | 154 | The first argument is the fully resolved classname for your job class (if you're wondering how 155 | php-resque knows about your job classes see [autoloading job classes](#autoload-job-classes)). 156 | The second argument is an array of any arguments you want to pass through to the job class. 157 | 158 | It is also possible to push a Closure onto the queue. This is very convenient for quick, 159 | simple tasks that need to be queued. When pushing Closures onto the queue, the `__DIR__` 160 | and `__FILE__` constants should not be used. 161 | 162 | ```php 163 | $job = Resque::push(function ($job) { 164 | echo "This is a inline job {$job->getId()}!"; 165 | }); 166 | ``` 167 | 168 | It is possible to push a job onto another queue (the default queue is called `default`) by passing 169 | through a third parameter to the `Resque::push` method which contains the queue name. 170 | 171 | ```php 172 | $job = Resque::push(SendEmail::class, [], 'email'); 173 | ``` 174 | 175 | ### Delaying Jobs 176 | 177 | It is possible to schedule a job to run at a specified time in the future using the `Resque::later` 178 | method. You can do this by either passing through an `int` or a `DateTime` object. 179 | 180 | ```php 181 | $job = Resque::later(60, MyJob::class); 182 | $job = Resque::later(1398643990, MyJob::class); 183 | $job = Resque::later(new \DateTime('+2 mins'), MyJob::class); 184 | $job = Resque::later(new \DateTime('2014-07-08 11:14:15'), MyJob::class); 185 | ``` 186 | 187 | If you pass through an integer and it is smaller than `94608000` seconds (3 years) php-resque will 188 | assume you want a time relative to the current time (I mean, who delays jobs for more than 3 years 189 | anyway??). Note that you must have a worker running at the specified time for the job to run. 190 | 191 | ### Job Statuses 192 | 193 | php-resque tracks the status of a job. The status information will allow you to check if a job is in the queue, currently being run, failed, etc. 194 | To track the status of a job you must capture the job id of a pushed job. 195 | 196 | ```php 197 | $job = Resque::push(MyJob::class); 198 | $jobId = $job->getId(); 199 | ``` 200 | 201 | To fetch the status of a job: 202 | 203 | ```php 204 | $job = Job::load($jobId); 205 | $status = $job->getStatus(); 206 | ``` 207 | 208 | Job statuses are defined as constants in the Job class. Valid statuses are: 209 | 210 | - `Job::STATUS_WAITING` - The job is still queued 211 | - `Job::STATUS_DELAYED` - Job is delayed 212 | - `Job::STATUS_RUNNING` - The job is currently running 213 | - `Job::STATUS_COMPLETE` - Job is complete 214 | - `Job::STATUS_CANCELLED` - The job has been canceled 215 | - `Job::STATUS_FAILED` - Job has failed 216 | - `false` - Failed to fetch the status - is the id valid? 217 | 218 | Statuses are available for up to 7 days after a job has been completed or failed, and are then automatically expired. 219 | This timeout can be changed in the configuration file. 220 | 221 | ## Workers 222 | 223 | To start a worker navigate to your project root and run: 224 | 225 | $ vendor/bin/resque worker:start 226 | 227 | Note that once this worker has started, it will continue to run until it is manually stopped. 228 | You may use a process monitor such as [Supervisor](http://supervisord.org/) to run the worker 229 | as a background process and to ensure that the worker does not stop running. 230 | 231 | If the worker is a background task you can stop, pause & restart the worker with the following commands: 232 | 233 | $ vendor/bin/resque worker:stop 234 | $ vendor/bin/resque worker:pause 235 | $ vendor/bin/resque worker:resume 236 | 237 | The commands take inline configuration options as well as reading from a [configuration file](https://github.com/mjphaynes/php-resque/blob/master/docs/configuration.md#file). 238 | 239 | For instance, to specify that the worker only processes jobs on the queues named `high` and `low`, as well as allowing 240 | a maximum of `30MB` of memory for the jobs, you can run the following: 241 | 242 | $ vendor/bin/resque worker:start --queue=high,low --memory=30 -vvv 243 | 244 | Note that this will check the `high` queue first and then the `low` queue, so it is possible to facilitate job queue 245 | priorities using this. To run all queues use `*` - this is the default value. The `-vvv` enables very verbose 246 | logging. To silence any logging the `-q` flag is used. 247 | 248 | For more commands and the full list of options please see 249 | the [commands](https://github.com/mjphaynes/php-resque/blob/master/docs/commands.md) documentation. 250 | 251 | In addition, if the workers are running on a different host, you may trigger a graceful shutdown of a worker remotely via the data in Redis. For example: 252 | 253 | ```php 254 | foreach(Worker::allWorkers() as $worker) { 255 | $worker->shutdown(); 256 | } 257 | ``` 258 | 259 | ### Signals 260 | 261 | Signals work on supported platforms. Signals sent to workers will have the following effect: 262 | 263 | - `QUIT` - Wait for the child to finish processing then exit 264 | - `TERM` / `INT` - Immediately kill child then exit 265 | - `USR1` - Immediately kill the child but don't exit 266 | - `USR2` - Pause worker, no new jobs will be processed 267 | - `CONT` - Resume worker 268 | 269 | ### Forking 270 | 271 | When php-resque runs a job it first forks the process to a child process. This is so that if the job fails 272 | the worker can detect that the job failed and will continue to run. The forked child will always exit as 273 | soon as the job finishes. 274 | 275 | The PECL module (http://php.net/manual/en/book.pcntl.php) must be installed to use php-resque. 276 | 277 | ### Autoload Job Classes 278 | 279 | Getting your application underway also requires telling the worker about your job classes, 280 | using either an autoloader or including them. If you're using Composer then it will 281 | be relatively straightforward to add your job classes there. 282 | 283 | Alternatively, you can do so in the `resque.yml` file or by setting the include argument: 284 | 285 | $ vendor/bin/resque worker:start --include=/path/to/your/include/file.php 286 | 287 | There is an example of how this all works in the `examples/` folder in this project. 288 | 289 | ## Commands & Options 290 | 291 | For the full list of php-resque commands and their associated arguments, please 292 | see the [commands documentation](https://github.com/mjphaynes/php-resque/blob/master/docs/commands.md). 293 | 294 | ## Logging 295 | 296 | php-resque is integrated with [Monolog](https://github.com/Seldaek/monolog) which enables extensive logging abilities. For full documentation 297 | please see the [logging documentation](https://github.com/mjphaynes/php-resque/blob/master/docs/logging.md). 298 | 299 | ## Event/Hook System 300 | 301 | php-resque has an extensive events/hook system to allow developers deep integration with the library without 302 | having to modify any core files. For full documentation and a list of all events please see the [hooks documentation](https://github.com/mjphaynes/php-resque/blob/master/docs/hooks.md). 303 | 304 | ## Configuration Options 305 | 306 | For a complete list of all configuration options, please 307 | see the [configuration documentation](https://github.com/mjphaynes/php-resque/blob/master/docs/configuration.md). 308 | 309 | ## Redis 310 | 311 | You can either set the Redis connection details inline or in the [configuration file](https://github.com/mjphaynes/php-resque/blob/master/docs/configuration.md). 312 | To set when running a command: 313 | 314 | $ vendor/bin/resque [command] --host= --port= 315 | 316 | ## Contributing 317 | 318 | PHP-Resque v4 is for PHP 7.2 or higher, new code must work with PHP 7.2. Please follow the [PSR-12 coding style](https://www.php-fig.org/psr/psr-12/) where possible. New features should come with tests. 319 | 320 | --- 321 | 322 | ## Contributors 323 | 324 | Contributing to the project would be a massive help in maintaining and extending the script. 325 | If you're interested in contributing, issue a pull request on GitHub. 326 | 327 | - [mjphaynes](https://github.com/mjphaynes) 328 | - [chrisboulton](https://github.com/chrisboulton) (original port) 329 | - [Project contributors](https://github.com/mjphaynes/php-resque/graphs/contributors) 330 | -------------------------------------------------------------------------------- /src/Socket/Server.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Resque\Socket; 13 | 14 | use Resque\Logger; 15 | 16 | /** 17 | * Socket server management 18 | * 19 | * @package Resque 20 | * @author Michael Haynes 21 | */ 22 | class Server 23 | { 24 | /** 25 | * Default IP to use 26 | */ 27 | public const DEFAULT_IP = '0.0.0.0'; 28 | 29 | /** 30 | * Which port to use 31 | */ 32 | public const DEFAULT_PORT = 7370; 33 | 34 | /** 35 | * Which protocol to use 36 | */ 37 | public const PROTOCOL = 'tcp'; 38 | 39 | /** 40 | * Client connect event 41 | */ 42 | public const CLIENT_CONNECT = 1; 43 | 44 | /** 45 | * Client receive message event 46 | */ 47 | public const CLIENT_RECEIVE = 2; 48 | 49 | /** 50 | * Client disconnect event 51 | */ 52 | public const CLIENT_DISCONNECT = 3; 53 | 54 | /** 55 | * @var array Configuration information used by the server. 56 | */ 57 | protected $config = []; 58 | 59 | /** 60 | * @var Logger Monolog logger interface 61 | */ 62 | protected $logger; 63 | 64 | /** 65 | * @var array Dictionary of events and the callbacks attached to them. 66 | */ 67 | protected $events = []; 68 | 69 | /** 70 | * @var \Socket The socket used by the server. 71 | */ 72 | protected $socket; 73 | 74 | /** 75 | * @var int The maximum number of clients allowed to connect. 76 | */ 77 | protected $max_clients = 10; 78 | 79 | /** 80 | * @var int The maximum number of bytes to read from a socket at a single time. 81 | */ 82 | protected $max_read = 1024; 83 | 84 | /** 85 | * @var int Connection timeout 86 | */ 87 | protected $tv_sec = 5; 88 | 89 | /** 90 | * @var bool if the server has started 91 | */ 92 | protected $started = false; 93 | 94 | /** 95 | * @var bool True if on the next iteration, the server should shutdown. 96 | */ 97 | protected $shutdown = false; 98 | 99 | /** 100 | * @var array The connected clients. 101 | */ 102 | protected $clients = []; 103 | 104 | /** 105 | * Creates the socket and starts listening to it. 106 | * 107 | * @param array $config Array of configuration options 108 | * @param Logger $logger Output logger 109 | */ 110 | public function __construct(array $config, Logger $logger) 111 | { 112 | $this->logger = $logger; 113 | 114 | $defaults = [ 115 | 'ip' => self::DEFAULT_IP, 116 | 'port' => self::DEFAULT_PORT, 117 | 'protocol' => self::PROTOCOL, 118 | ]; 119 | 120 | $this->config = array_merge($defaults, $config); 121 | } 122 | 123 | public function __toString() 124 | { 125 | return $this->config['ip'].':'.$this->config['port']; 126 | } 127 | 128 | /** 129 | * Starts the server 130 | */ 131 | public function start(): void 132 | { 133 | if (false === ($this->socket = @socket_create(AF_INET, SOCK_STREAM, getprotobyname($this->config['protocol'])))) { 134 | throw new SocketException(sprintf( 135 | 'socket_create(AF_INET, SOCK_STREAM, <%s>) failed: [%d] %s', 136 | $this->config['protocol'], 137 | $code = socket_last_error(), 138 | socket_strerror($code) 139 | )); 140 | } 141 | 142 | if (false === @socket_bind($this->socket, $this->config['ip'], $this->config['port'])) { 143 | throw new SocketException(sprintf( 144 | 'socket_bind($socket, "%s", %d) failed: [%d] %s', 145 | $this->config['ip'], 146 | $this->config['port'], 147 | $code = socket_last_error(), 148 | socket_strerror($code) 149 | )); 150 | } 151 | 152 | if (false === @socket_getsockname($this->socket, $this->config['ip'], $this->config['port'])) { 153 | throw new SocketException(sprintf( 154 | 'socket_getsockname($socket, "%s", %d) failed: [%d] %s', 155 | $this->config['ip'], 156 | $this->config['port'], 157 | $code = socket_last_error(), 158 | socket_strerror($code) 159 | )); 160 | } 161 | 162 | if (false === @socket_listen($this->socket)) { 163 | throw new SocketException(sprintf('socket_listen($socket) failed: [%d] %s', $code = socket_last_error(), socket_strerror($code))); 164 | } 165 | 166 | $this->started = true; 167 | 168 | $this->log('Listening for connections on '.$this.'', Logger::INFO); 169 | } 170 | 171 | /** 172 | * Schedule a shutdown. Will finish processing the current run. 173 | */ 174 | public function shutdown(): void 175 | { 176 | $this->shutdown = true; 177 | } 178 | 179 | /** 180 | * Closes the socket on shutdown 181 | */ 182 | public function close(): void 183 | { 184 | foreach ($this->clients as &$client) { 185 | $this->disconnect($client, 'Receiver shutting down... Goodbye.'); 186 | } 187 | 188 | socket_close($this->socket); 189 | } 190 | 191 | /** 192 | * Runs the server code until the server is shut down. 193 | */ 194 | public function run(): void 195 | { 196 | if (!$this->started) { 197 | $this->start(); 198 | } 199 | 200 | if (function_exists('pcntl_signal')) { 201 | // PHP 7.1 allows async signals 202 | if (function_exists('pcntl_async_signals')) { 203 | pcntl_async_signals(true); 204 | } else { 205 | declare(ticks=1); 206 | } 207 | pcntl_signal(SIGTERM, [$this, 'shutdown']); 208 | pcntl_signal(SIGINT, [$this, 'shutdown']); 209 | pcntl_signal(SIGQUIT, [$this, 'shutdown']); 210 | } 211 | 212 | register_shutdown_function([$this, 'close']); 213 | 214 | while (true) { 215 | if ($this->shutdown) { 216 | $this->log('Shutting down listener on '.$this.'', Logger::INFO); 217 | break; 218 | } 219 | 220 | $read = [$this->socket]; 221 | foreach ($this->clients as &$client) { 222 | $read[] = $client->getSocket(); 223 | } 224 | 225 | // Set up a blocking call to socket_select 226 | $write = $except = null; 227 | if ((@socket_select($read, $write, $except, $this->tv_sec)) < 1) { 228 | // $this->log('Waiting for socket update', self::LOG_VERBOSE); 229 | continue; 230 | } 231 | 232 | // Handle new Connections 233 | if (in_array($this->socket, $read)) { 234 | if (count($this->clients) >= $this->max_clients) { 235 | $this->log('New client trying to connect but maximum '.$this->max_clients.' clients already connected', Logger::INFO); 236 | 237 | $client = new Client($this->socket); 238 | $this->send($client, '{"ok":0,"message":"Could not connect, hit max number of connections"}'); 239 | $client->disconnect(); 240 | continue; 241 | } else { 242 | $this->clients[] = $client = new Client($this->socket); 243 | 244 | $this->log('New client connected: '.$client.'', Logger::INFO); 245 | 246 | $this->fire(self::CLIENT_CONNECT, $client); 247 | } 248 | } 249 | 250 | // Handle input for each client 251 | foreach ($this->clients as $i => $client) { 252 | if (in_array($client->getSocket(), $read)) { 253 | $data = @socket_read($client->getSocket(), $this->max_read); 254 | 255 | if ($data === false) { 256 | $this->disconnect($client); 257 | continue; 258 | } 259 | 260 | // Remove any control characters 261 | $data = preg_replace('/[\x00-\x1F\x7F]/', '', trim($data)); 262 | 263 | if (empty($data)) { 264 | // Send a null byte to flush 265 | $this->send($client, "\0"); 266 | } else { 267 | $this->log(sprintf('Received "%s" from %s', $data, $client), Logger::INFO); 268 | $this->fire(self::CLIENT_RECEIVE, $client, $data); 269 | } 270 | } 271 | } 272 | } 273 | } 274 | 275 | /** 276 | * Writes data to the socket, including the length of the data, and ends it with a CRLF unless specified. 277 | * It is perfectly valid for socket_write to return zero which means no bytes have been written. 278 | * Be sure to use the === operator to check for FALSE in case of an error. 279 | * 280 | * @param Client $client Connected client to write to 281 | * @param string $message Data to write to the socket. 282 | * @param bool $end Whether to end the message with a newline 283 | * @return int|bool Returns the number of bytes successfully written to the socket or FALSE on failure. 284 | * The error code can be retrieved with socket_last_error(). This code may be passed to 285 | * socket_strerror() to get a textual explanation of the error. 286 | */ 287 | public function send(Client &$client, string $message, bool $end = true) 288 | { 289 | $this->log('Messaging client '.$client.' with "'.str_replace("\n", '\n', $message).'"'); 290 | 291 | $end and $message = "$message\n"; 292 | $length = strlen($message); 293 | $sent = 0; 294 | 295 | while (true) { 296 | $attempt = @socket_write($client->getSocket(), $message, $length); 297 | 298 | if ($attempt === false) { 299 | return false; 300 | } 301 | 302 | $sent += $attempt; 303 | 304 | if ($attempt < $length) { 305 | $message = substr($message, $attempt); 306 | $length -= $attempt; 307 | } else { 308 | return $attempt; 309 | } 310 | } 311 | 312 | return false; 313 | } 314 | 315 | /** 316 | * Disconnect a client 317 | * 318 | * @param Client $client The client to disconnect 319 | * @param string $message Data to write to the socket. 320 | */ 321 | public function disconnect(Client $client, string $message = 'Goodbye.'): void 322 | { 323 | $this->fire(self::CLIENT_DISCONNECT, $client, $message); 324 | 325 | $this->log('Client disconnected: '.$client.'', Logger::INFO); 326 | 327 | $client->disconnect(); 328 | 329 | $i = array_search($client, $this->clients); 330 | unset($this->clients[$i]); 331 | } 332 | 333 | /** 334 | * Helper function to make using connect event easier 335 | * 336 | * @param callable $callback Any callback callable by call_user_func_array. 337 | * 338 | * @return bool 339 | */ 340 | public function onConnect(callable $callback): bool 341 | { 342 | return $this->listen(self::CLIENT_CONNECT, $callback); 343 | } 344 | 345 | /** 346 | * Helper function to make using receive event easier 347 | * 348 | * @param callable $callback Any callback callable by call_user_func_array. 349 | * 350 | * @return bool 351 | */ 352 | public function onReceive(callable $callback): bool 353 | { 354 | return $this->listen(self::CLIENT_RECEIVE, $callback); 355 | } 356 | 357 | /** 358 | * Helper function to make using disconnect event easier 359 | * 360 | * @param callable $callback Any callback callable by call_user_func_array. 361 | * 362 | * @return bool 363 | */ 364 | public function onDisconnect(callable $callback): bool 365 | { 366 | return $this->listen(self::CLIENT_DISCONNECT, $callback); 367 | } 368 | 369 | /** 370 | * Adds a function to be called whenever a certain action happens 371 | * 372 | * @param string $event Name of event to listen on. 373 | * @param mixed $callback Any callback callable by call_user_func_array. 374 | * 375 | * @return true 376 | */ 377 | public function listen(string $event, callable $callback): bool 378 | { 379 | if (!isset($this->events[$event])) { 380 | $this->events[$event] = []; 381 | } 382 | 383 | $this->events[$event][] = $callback; 384 | 385 | return true; 386 | } 387 | 388 | /** 389 | * Deletes a function from the call list for a certain action 390 | * 391 | * @param string $event Name of event. 392 | * @param mixed $callback The callback as defined when listen() was called. 393 | * 394 | * @return true 395 | */ 396 | public function forget(string $event, callable $callback): bool 397 | { 398 | if (!isset($this->events[$event])) { 399 | return true; 400 | } 401 | 402 | $key = array_search($callback, $this->events[$event]); 403 | 404 | if ($key !== false) { 405 | unset($this->events[$event][$key]); 406 | } 407 | 408 | return true; 409 | } 410 | 411 | /** 412 | * Raise a given event with the supplied data. 413 | * 414 | * @param string $event Name of event to be raised. 415 | * @param Client $client Connected client 416 | * @param mixed $data Optional, any data that should be passed to each callback. 417 | * 418 | * @return true 419 | */ 420 | public function fire(string $event, Client &$client, $data = null): bool 421 | { 422 | $retval = true; 423 | 424 | if (!array_key_exists($event, $this->events)) { 425 | return false; 426 | } 427 | 428 | foreach ($this->events[$event] as $callback) { 429 | if (!is_callable($callback)) { 430 | continue; 431 | } 432 | 433 | if (($retval = call_user_func($callback, $this, $client, $data)) === false) { 434 | break; 435 | } 436 | } 437 | 438 | return $retval !== false; 439 | } 440 | 441 | /** 442 | * Send log message to logger 443 | */ 444 | public function log() 445 | { 446 | return call_user_func_array([$this->logger, 'log'], func_get_args()); 447 | } 448 | 449 | /** 450 | * Write a string to a resource 451 | * 452 | * @param resource $fh The resource to write to 453 | * @param string $string The string to write 454 | */ 455 | public static function fwrite($fh, string $string) 456 | { 457 | $fwrite = 0; 458 | 459 | for ($written = 0; $written < strlen($string); $written += $fwrite) { 460 | if (($fwrite = fwrite($fh, substr($string, $written))) === false) { 461 | return $written; 462 | } 463 | } 464 | 465 | return $written; 466 | } 467 | } 468 | --------------------------------------------------------------------------------