├── src ├── debug │ ├── views │ │ ├── summary.php │ │ └── detail.php │ └── Panel.php ├── gii │ ├── form.php │ ├── default │ │ └── job.php │ └── Generator.php ├── Job.php ├── cli │ ├── Verbose.php │ ├── LoopInterface.php │ ├── WorkerEvent.php │ ├── Action.php │ ├── Signal.php │ ├── InfoAction.php │ ├── SignalLoop.php │ ├── Queue.php │ ├── VerboseBehavior.php │ └── Command.php ├── interfaces │ ├── DoneCountInterface.php │ ├── DelayedCountInterface.php │ ├── WaitingCountInterface.php │ ├── ReservedCountInterface.php │ └── StatisticsProviderInterface.php ├── RetryableJob.php ├── PushEvent.php ├── serializers │ ├── Serializer.php │ ├── SerializerInterface.php │ ├── PhpSerializer.php │ ├── IgbinarySerializer.php │ └── JsonSerializer.php ├── JobInterface.php ├── RetryableJobInterface.php ├── JobEvent.php ├── drivers │ ├── db │ │ ├── migrations │ │ │ ├── M170307170300Later.php │ │ │ ├── M211218163000JobQueueSize.php │ │ │ ├── M170601155600Priority.php │ │ │ ├── M161119140200Queue.php │ │ │ └── M170509001400Retry.php │ │ ├── StatisticsProvider.php │ │ ├── Command.php │ │ ├── InfoAction.php │ │ └── Queue.php │ ├── amqp_interop │ │ ├── Command.php │ │ └── Queue.php │ ├── beanstalk │ │ ├── InfoAction.php │ │ ├── Command.php │ │ └── Queue.php │ ├── amqp │ │ ├── Command.php │ │ └── Queue.php │ ├── gearman │ │ ├── Command.php │ │ └── Queue.php │ ├── stomp │ │ ├── Command.php │ │ └── Queue.php │ ├── redis │ │ ├── InfoAction.php │ │ ├── StatisticsProvider.php │ │ ├── Command.php │ │ └── Queue.php │ ├── sqs │ │ ├── Command.php │ │ └── Queue.php │ ├── file │ │ ├── StatisticsProvider.php │ │ ├── Command.php │ │ ├── InfoAction.php │ │ └── Queue.php │ └── sync │ │ └── Queue.php ├── closure │ ├── Job.php │ └── Behavior.php ├── ExecEvent.php ├── InvalidJobException.php ├── LogBehavior.php └── Queue.php ├── LICENSE.md ├── README.md ├── composer.json ├── UPGRADE.md └── CHANGELOG.md /src/debug/views/summary.php: -------------------------------------------------------------------------------- 1 | 8 |
9 | 10 | Queue 11 | 12 | 13 | 14 | 15 |
16 | 17 | -------------------------------------------------------------------------------- /src/gii/form.php: -------------------------------------------------------------------------------- 1 | 8 | field($generator, 'jobClass')->textInput(['autofocus' => true]) ?> 9 | field($generator, 'properties') ?> 10 | field($generator, 'retryable')->checkbox() ?> 11 | field($generator, 'ns') ?> 12 | field($generator, 'baseClass') ?> 13 | -------------------------------------------------------------------------------- /src/Job.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | interface Job extends JobInterface 18 | { 19 | } 20 | -------------------------------------------------------------------------------- /src/cli/Verbose.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | class Verbose extends VerboseBehavior 18 | { 19 | } 20 | -------------------------------------------------------------------------------- /src/interfaces/DoneCountInterface.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | interface DoneCountInterface 16 | { 17 | /** 18 | * @return int 19 | */ 20 | public function getDoneCount(); 21 | } 22 | -------------------------------------------------------------------------------- /src/RetryableJob.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | interface RetryableJob extends RetryableJobInterface 18 | { 19 | } 20 | -------------------------------------------------------------------------------- /src/interfaces/DelayedCountInterface.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | interface DelayedCountInterface 16 | { 17 | /** 18 | * @return int 19 | */ 20 | public function getDelayedCount(); 21 | } 22 | -------------------------------------------------------------------------------- /src/interfaces/WaitingCountInterface.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | interface WaitingCountInterface 16 | { 17 | /** 18 | * @return int 19 | */ 20 | public function getWaitingCount(); 21 | } 22 | -------------------------------------------------------------------------------- /src/interfaces/ReservedCountInterface.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | interface ReservedCountInterface 16 | { 17 | /** 18 | * @return int 19 | */ 20 | public function getReservedCount(); 21 | } 22 | -------------------------------------------------------------------------------- /src/PushEvent.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | class PushEvent extends JobEvent 16 | { 17 | /** 18 | * @var int 19 | */ 20 | public $delay; 21 | /** 22 | * @var mixed 23 | */ 24 | public $priority; 25 | } 26 | -------------------------------------------------------------------------------- /src/serializers/Serializer.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | interface Serializer extends SerializerInterface 18 | { 19 | } 20 | -------------------------------------------------------------------------------- /src/cli/LoopInterface.php: -------------------------------------------------------------------------------- 1 | 14 | * @since 2.0.2 15 | */ 16 | interface LoopInterface 17 | { 18 | /** 19 | * @return bool whether to continue listening of the queue. 20 | */ 21 | public function canContinue(); 22 | } 23 | -------------------------------------------------------------------------------- /src/interfaces/StatisticsProviderInterface.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | interface StatisticsProviderInterface 16 | { 17 | /** 18 | * @return int 19 | */ 20 | public function getStatisticsProvider(); 21 | } 22 | -------------------------------------------------------------------------------- /src/JobInterface.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | interface JobInterface 16 | { 17 | /** 18 | * @param Queue $queue which pushed and is handling the job 19 | * @return void|mixed result of the job execution 20 | */ 21 | public function execute($queue); 22 | } 23 | -------------------------------------------------------------------------------- /src/cli/WorkerEvent.php: -------------------------------------------------------------------------------- 1 | 16 | * @since 2.0.2 17 | */ 18 | class WorkerEvent extends Event 19 | { 20 | /** 21 | * @var Queue 22 | * @inheritdoc 23 | */ 24 | public $sender; 25 | /** 26 | * @var LoopInterface 27 | */ 28 | public $loop; 29 | /** 30 | * @var null|int exit code 31 | */ 32 | public $exitCode; 33 | } 34 | -------------------------------------------------------------------------------- /src/serializers/SerializerInterface.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | interface SerializerInterface 18 | { 19 | /** 20 | * @param JobInterface|mixed $job 21 | * @return string 22 | */ 23 | public function serialize($job); 24 | 25 | /** 26 | * @param string $serialized 27 | * @return JobInterface 28 | */ 29 | public function unserialize($serialized); 30 | } 31 | -------------------------------------------------------------------------------- /src/RetryableJobInterface.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | interface RetryableJobInterface extends JobInterface 16 | { 17 | /** 18 | * @return int time to reserve in seconds 19 | */ 20 | public function getTtr(); 21 | 22 | /** 23 | * @param int $attempt number 24 | * @param \Exception|\Throwable $error from last execute of the job 25 | * @return bool 26 | */ 27 | public function canRetry($attempt, $error); 28 | } 29 | -------------------------------------------------------------------------------- /src/JobEvent.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | abstract class JobEvent extends Event 18 | { 19 | /** 20 | * @var Queue 21 | * @inheritdoc 22 | */ 23 | public $sender; 24 | /** 25 | * @var string|null unique id of a job 26 | */ 27 | public $id; 28 | /** 29 | * @var JobInterface|null 30 | */ 31 | public $job; 32 | /** 33 | * @var int time to reserve in seconds of the job 34 | */ 35 | public $ttr; 36 | } 37 | -------------------------------------------------------------------------------- /src/serializers/PhpSerializer.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | class PhpSerializer extends BaseObject implements SerializerInterface 18 | { 19 | /** 20 | * @inheritdoc 21 | */ 22 | public function serialize($job) 23 | { 24 | return serialize($job); 25 | } 26 | 27 | /** 28 | * @inheritdoc 29 | */ 30 | public function unserialize($serialized) 31 | { 32 | return unserialize($serialized); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/drivers/db/migrations/M170307170300Later.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | class M170307170300Later extends Migration 18 | { 19 | public $tableName = '{{%queue}}'; 20 | 21 | 22 | public function up() 23 | { 24 | $this->addColumn($this->tableName, 'timeout', $this->integer()->defaultValue(0)->notNull()->after('created_at')); 25 | } 26 | 27 | public function down() 28 | { 29 | $this->dropColumn($this->tableName, 'timeout'); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/drivers/amqp_interop/Command.php: -------------------------------------------------------------------------------- 1 | 16 | * @since 2.0.2 17 | */ 18 | class Command extends CliCommand 19 | { 20 | /** 21 | * @var Queue 22 | */ 23 | public $queue; 24 | 25 | 26 | /** 27 | * @inheritdoc 28 | */ 29 | protected function isWorkerAction($actionID) 30 | { 31 | return $actionID === 'listen'; 32 | } 33 | 34 | /** 35 | * Listens amqp-queue and runs new jobs. 36 | * It can be used as daemon process. 37 | */ 38 | public function actionListen() 39 | { 40 | $this->queue->listen(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/closure/Job.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | class Job implements JobInterface 19 | { 20 | /** 21 | * @var string serialized closure 22 | */ 23 | public $serialized; 24 | 25 | 26 | /** 27 | * Unserializes and executes a closure. 28 | * @inheritdoc 29 | */ 30 | public function execute($queue) 31 | { 32 | $unserialized = opis_unserialize($this->serialized); 33 | if ($unserialized instanceof \Closure) { 34 | return $unserialized(); 35 | } 36 | return $unserialized->execute($queue); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/drivers/db/migrations/M211218163000JobQueueSize.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | class M211218163000JobQueueSize extends Migration 18 | { 19 | public $tableName = '{{%queue}}'; 20 | 21 | 22 | public function up() 23 | { 24 | if ($this->db->driverName === 'mysql') { 25 | $this->alterColumn($this->tableName, 'job', 'LONGBLOB NOT NULL'); 26 | } 27 | } 28 | 29 | public function down() 30 | { 31 | if ($this->db->driverName === 'mysql') { 32 | $this->alterColumn($this->tableName, 'job', $this->binary()->notNull()); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/serializers/IgbinarySerializer.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | class IgbinarySerializer extends BaseObject implements SerializerInterface 21 | { 22 | /** 23 | * @inheritdoc 24 | */ 25 | public function serialize($job) 26 | { 27 | return igbinary_serialize($job); 28 | } 29 | 30 | /** 31 | * @inheritdoc 32 | */ 33 | public function unserialize($serialized) 34 | { 35 | return igbinary_unserialize($serialized); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/drivers/beanstalk/InfoAction.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | class InfoAction extends Action 19 | { 20 | /** 21 | * @var Queue 22 | */ 23 | public $queue; 24 | 25 | 26 | /** 27 | * Info about queue status. 28 | */ 29 | public function run() 30 | { 31 | Console::output($this->format('Statistical information about the tube:', Console::FG_GREEN)); 32 | 33 | foreach ($this->queue->getStatsTube() as $key => $value) { 34 | Console::stdout($this->format("- $key: ", Console::FG_YELLOW)); 35 | Console::output($value); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/drivers/db/migrations/M170601155600Priority.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | class M170601155600Priority extends Migration 18 | { 19 | public $tableName = '{{%queue}}'; 20 | 21 | 22 | public function up() 23 | { 24 | $this->addColumn($this->tableName, 'priority', $this->integer()->unsigned()->notNull()->defaultValue(1024)->after('delay')); 25 | $this->createIndex('priority', $this->tableName, 'priority'); 26 | } 27 | 28 | public function down() 29 | { 30 | $this->dropIndex('priority', $this->tableName); 31 | $this->dropColumn($this->tableName, 'priority'); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/drivers/amqp/Command.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | class Command extends CliCommand 20 | { 21 | /** 22 | * @var Queue 23 | */ 24 | public $queue; 25 | 26 | 27 | /** 28 | * @inheritdoc 29 | */ 30 | protected function isWorkerAction($actionID) 31 | { 32 | return $actionID === 'listen'; 33 | } 34 | 35 | /** 36 | * Listens amqp-queue and runs new jobs. 37 | * It can be used as daemon process. 38 | */ 39 | public function actionListen() 40 | { 41 | $this->queue->listen(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/ExecEvent.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | class ExecEvent extends JobEvent 16 | { 17 | /** 18 | * @var int attempt number. 19 | * @see Queue::EVENT_BEFORE_EXEC 20 | * @see Queue::EVENT_AFTER_EXEC 21 | * @see Queue::EVENT_AFTER_ERROR 22 | */ 23 | public $attempt; 24 | /** 25 | * @var mixed result of a job execution in case job is done. 26 | * @see Queue::EVENT_AFTER_EXEC 27 | * @since 2.1.1 28 | */ 29 | public $result; 30 | /** 31 | * @var null|\Exception|\Throwable 32 | * @see Queue::EVENT_AFTER_ERROR 33 | * @since 2.1.1 34 | */ 35 | public $error; 36 | /** 37 | * @var null|bool 38 | * @see Queue::EVENT_AFTER_ERROR 39 | * @since 2.1.1 40 | */ 41 | public $retry; 42 | } 43 | -------------------------------------------------------------------------------- /src/gii/default/job.php: -------------------------------------------------------------------------------- 1 | 19 | 20 | namespace ; 21 | 22 | /** 23 | * Class . 24 | */ 25 | class extends 26 | 27 | { 28 | 29 | public $; 30 | 31 | 32 | /** 33 | * @inheritdoc 34 | */ 35 | public function execute($queue) 36 | { 37 | } 38 | retryable): ?> 39 | 40 | /** 41 | * @inheritdoc 42 | */ 43 | public function getTtr() 44 | { 45 | return 60; 46 | } 47 | 48 | /** 49 | * @inheritdoc 50 | */ 51 | public function canRetry($attempt, $error) 52 | { 53 | return $attempt < 3; 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /src/InvalidJobException.php: -------------------------------------------------------------------------------- 1 | 18 | * @since 2.1.1 19 | */ 20 | class InvalidJobException extends \Exception 21 | { 22 | /** 23 | * @var string 24 | */ 25 | private $serialized; 26 | 27 | 28 | /** 29 | * @param string $serialized 30 | * @param string $message 31 | * @param int $code 32 | * @param Throwable|null $previous 33 | */ 34 | public function __construct($serialized, $message = '', $code = 0, $previous = null) 35 | { 36 | $this->serialized = $serialized; 37 | parent::__construct($message, $code, $previous); 38 | } 39 | 40 | /** 41 | * @return string of serialized message that cannot be unserialized to a job 42 | */ 43 | final public function getSerialized() 44 | { 45 | return $this->serialized; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/drivers/gearman/Command.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | class Command extends CliCommand 18 | { 19 | /** 20 | * @var Queue 21 | */ 22 | public $queue; 23 | 24 | 25 | /** 26 | * @inheritdoc 27 | */ 28 | protected function isWorkerAction($actionID) 29 | { 30 | return in_array($actionID, ['run', 'listen'], true); 31 | } 32 | 33 | /** 34 | * Runs all jobs from gearman-queue. 35 | * It can be used as cron job. 36 | * 37 | * @return null|int exit code. 38 | */ 39 | public function actionRun() 40 | { 41 | return $this->queue->run(false); 42 | } 43 | 44 | /** 45 | * Listens gearman-queue and runs new jobs. 46 | * It can be used as daemon process. 47 | * 48 | * @return null|int exit code. 49 | */ 50 | public function actionListen() 51 | { 52 | return $this->queue->run(true); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/drivers/db/migrations/M161119140200Queue.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | class M161119140200Queue extends Migration 18 | { 19 | public $tableName = '{{%queue}}'; 20 | public $tableOptions; 21 | 22 | 23 | public function up() 24 | { 25 | $this->createTable($this->tableName, [ 26 | 'id' => $this->primaryKey(), 27 | 'channel' => $this->string()->notNull(), 28 | 'job' => $this->binary()->notNull(), 29 | 'created_at' => $this->integer()->notNull(), 30 | 'started_at' => $this->integer(), 31 | 'finished_at' => $this->integer(), 32 | ], $this->tableOptions); 33 | 34 | $this->createIndex('channel', $this->tableName, 'channel'); 35 | $this->createIndex('started_at', $this->tableName, 'started_at'); 36 | } 37 | 38 | public function down() 39 | { 40 | $this->dropTable($this->tableName); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/closure/Behavior.php: -------------------------------------------------------------------------------- 1 | push(function () use ($url, $file) { 23 | * file_put_contents($file, file_get_contents($url)); 24 | * }); 25 | * ``` 26 | * 27 | * @author Roman Zhuravlev 28 | */ 29 | class Behavior extends \yii\base\Behavior 30 | { 31 | /** 32 | * @var Queue 33 | */ 34 | public $owner; 35 | 36 | 37 | /** 38 | * @inheritdoc 39 | */ 40 | public function events() 41 | { 42 | return [ 43 | Queue::EVENT_BEFORE_PUSH => 'beforePush', 44 | ]; 45 | } 46 | 47 | /** 48 | * Converts the closure to a job object. 49 | * @param PushEvent $event 50 | */ 51 | public function beforePush(PushEvent $event) 52 | { 53 | $serialized = opis_serialize($event->job); 54 | $event->job = new Job(); 55 | $event->job->serialized = $serialized; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/cli/Action.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | abstract class Action extends BaseAction 20 | { 21 | /** 22 | * @var Queue 23 | */ 24 | public $queue; 25 | /** 26 | * @var Command|ConsoleController 27 | */ 28 | public $controller; 29 | 30 | 31 | /** 32 | * @inheritdoc 33 | */ 34 | public function init() 35 | { 36 | parent::init(); 37 | 38 | if (!$this->queue && ($this->controller instanceof Command)) { 39 | $this->queue = $this->controller->queue; 40 | } 41 | if (!($this->controller instanceof ConsoleController)) { 42 | throw new InvalidConfigException('The controller must be console controller.'); 43 | } 44 | if (!($this->queue instanceof Queue)) { 45 | throw new InvalidConfigException('The queue must be cli queue.'); 46 | } 47 | } 48 | 49 | /** 50 | * @param string $string 51 | * @return string 52 | */ 53 | protected function format($string) 54 | { 55 | return call_user_func_array([$this->controller, 'ansiFormat'], func_get_args()); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright © 2008 by Yii Software LLC (http://www.yiisoft.com) 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions 6 | are met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in 12 | the documentation and/or other materials provided with the 13 | distribution. 14 | * Neither the name of Yii Software LLC nor the names of its 15 | contributors may be used to endorse or promote products derived 16 | from this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 21 | FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 22 | COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 23 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 24 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 25 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 27 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 28 | ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 29 | POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /src/drivers/stomp/Command.php: -------------------------------------------------------------------------------- 1 | 17 | * @since 2.3.0 18 | */ 19 | class Command extends CliCommand 20 | { 21 | /** 22 | * @var Queue 23 | */ 24 | public $queue; 25 | 26 | 27 | /** 28 | * @inheritdoc 29 | */ 30 | protected function isWorkerAction($actionID) 31 | { 32 | return in_array($actionID, ['run', 'listen']); 33 | } 34 | 35 | 36 | /** 37 | * Runs all jobs from stomp-queue. 38 | * It can be used as cron job. 39 | * 40 | * @return null|int exit code. 41 | */ 42 | public function actionRun() 43 | { 44 | return $this->queue->run(false); 45 | } 46 | 47 | /** 48 | * Listens stomp-queue and runs new jobs. 49 | * It can be used as daemon process. 50 | * 51 | * @param int $timeout number of seconds to wait a job. 52 | * @throws Exception when params are invalid. 53 | * @return null|int exit code. 54 | */ 55 | public function actionListen($timeout = 3) 56 | { 57 | if (!is_numeric($timeout)) { 58 | throw new Exception('Timeout must be numeric.'); 59 | } 60 | if ($timeout < 1) { 61 | throw new Exception('Timeout must be greater that zero.'); 62 | } 63 | 64 | return $this->queue->run(true, $timeout); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/cli/Signal.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | class Signal 18 | { 19 | private static $exit = false; 20 | 21 | 22 | /** 23 | * Checks exit signals 24 | * Used mainly by [[yii\queue\Queue]] to check, whether job execution 25 | * loop can be continued. 26 | * @return bool 27 | */ 28 | public static function isExit() 29 | { 30 | if (function_exists('pcntl_signal')) { 31 | // Installs a signal handler 32 | static $handled = false; 33 | if (!$handled) { 34 | foreach ([SIGTERM, SIGINT, SIGHUP] as $signal) { 35 | pcntl_signal($signal, function () { 36 | static::setExitFlag(); 37 | }); 38 | } 39 | $handled = true; 40 | } 41 | 42 | // Checks signal 43 | if (!static::$exit) { 44 | pcntl_signal_dispatch(); 45 | } 46 | } 47 | 48 | return static::$exit; 49 | } 50 | 51 | /** 52 | * Sets exit flag to `true` 53 | * Method can be used to simulate exit signal for methods that use 54 | * [[isExit()]] to check whether execution loop can be continued. 55 | */ 56 | public static function setExitFlag() 57 | { 58 | static::$exit = true; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/drivers/redis/InfoAction.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | class InfoAction extends Action 21 | { 22 | /** 23 | * @var Queue 24 | */ 25 | public $queue; 26 | 27 | 28 | /** 29 | * Info about queue status. 30 | */ 31 | public function run() 32 | { 33 | $prefix = $this->queue->channel; 34 | $waiting = $this->queue->redis->llen("$prefix.waiting"); 35 | $delayed = $this->queue->redis->zcount("$prefix.delayed", '-inf', '+inf'); 36 | $reserved = $this->queue->redis->zcount("$prefix.reserved", '-inf', '+inf'); 37 | $total = $this->queue->redis->get("$prefix.message_id"); 38 | $done = $total - $waiting - $delayed - $reserved; 39 | 40 | Console::output($this->format('Jobs', Console::FG_GREEN)); 41 | 42 | Console::stdout($this->format('- waiting: ', Console::FG_YELLOW)); 43 | Console::output($waiting); 44 | 45 | Console::stdout($this->format('- delayed: ', Console::FG_YELLOW)); 46 | Console::output($delayed); 47 | 48 | Console::stdout($this->format('- reserved: ', Console::FG_YELLOW)); 49 | Console::output($reserved); 50 | 51 | Console::stdout($this->format('- done: ', Console::FG_YELLOW)); 52 | Console::output($done); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/drivers/sqs/Command.php: -------------------------------------------------------------------------------- 1 | 17 | * @author Manoj Malviya 18 | */ 19 | class Command extends CliCommand 20 | { 21 | /** 22 | * @var Queue 23 | */ 24 | public $queue; 25 | 26 | 27 | /** 28 | * Runs all jobs from sqs. 29 | * It can be used as cron job. 30 | * 31 | * @return null|int exit code. 32 | */ 33 | public function actionRun() 34 | { 35 | return $this->queue->run(false); 36 | } 37 | 38 | /** 39 | * Listens sqs and runs new jobs. 40 | * It can be used as demon process. 41 | * 42 | * @param int $timeout number of seconds to sleep before next reading of the queue. 43 | * @throws Exception when params are invalid. 44 | * @return null|int exit code. 45 | */ 46 | public function actionListen($timeout = 3) 47 | { 48 | if (!is_numeric($timeout)) { 49 | throw new Exception('Timeout must be numeric.'); 50 | } 51 | $timeout = (int) $timeout; 52 | 53 | if ($timeout < 1 || $timeout > 20) { 54 | throw new Exception('Timeout must be between 1 and 20'); 55 | } 56 | 57 | return $this->queue->run(true, $timeout); 58 | } 59 | 60 | /** 61 | * Clears the queue. 62 | */ 63 | public function actionClear() 64 | { 65 | if ($this->confirm('Are you sure?')) { 66 | $this->queue->clear(); 67 | $this->stdout("Queue has been cleared.\n"); 68 | } 69 | } 70 | 71 | /** 72 | * @inheritdoc 73 | */ 74 | protected function isWorkerAction($actionID) 75 | { 76 | return in_array($actionID, ['run', 'listen']); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/drivers/redis/StatisticsProvider.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | class StatisticsProvider extends BaseObject implements DoneCountInterface, WaitingCountInterface, DelayedCountInterface, ReservedCountInterface 22 | { 23 | /** 24 | * @var Queue 25 | */ 26 | protected $queue; 27 | 28 | 29 | public function __construct(Queue $queue, $config = []) 30 | { 31 | $this->queue = $queue; 32 | parent::__construct($config); 33 | } 34 | 35 | /** 36 | * @inheritdoc 37 | */ 38 | public function getWaitingCount() 39 | { 40 | $prefix = $this->queue->channel; 41 | return $this->queue->redis->llen("$prefix.waiting"); 42 | } 43 | 44 | /** 45 | * @inheritdoc 46 | */ 47 | public function getDelayedCount() 48 | { 49 | $prefix = $this->queue->channel; 50 | return $this->queue->redis->zcount("$prefix.delayed", '-inf', '+inf'); 51 | } 52 | 53 | /** 54 | * @inheritdoc 55 | */ 56 | public function getReservedCount() 57 | { 58 | $prefix = $this->queue->channel; 59 | return $this->queue->redis->zcount("$prefix.reserved", '-inf', '+inf'); 60 | } 61 | 62 | /** 63 | * @inheritdoc 64 | */ 65 | public function getDoneCount() 66 | { 67 | $prefix = $this->queue->channel; 68 | $waiting = $this->getWaitingCount(); 69 | $delayed = $this->getDelayedCount(); 70 | $reserved = $this->getReservedCount(); 71 | $total = $this->queue->redis->get("$prefix.message_id"); 72 | return $total - $waiting - $delayed - $reserved; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/cli/InfoAction.php: -------------------------------------------------------------------------------- 1 | 22 | */ 23 | class InfoAction extends Action 24 | { 25 | /** 26 | * @var Queue 27 | */ 28 | public $queue; 29 | 30 | 31 | /** 32 | * Info about queue status. 33 | */ 34 | public function run() 35 | { 36 | if (!($this->queue instanceof StatisticsProviderInterface)) { 37 | throw new NotSupportedException('Queue does not support ' . StatisticsProviderInterface::class); 38 | } 39 | 40 | $this->controller->stdout('Jobs' . PHP_EOL, Console::FG_GREEN); 41 | $statisticsProvider = $this->queue->getStatisticsProvider(); 42 | 43 | if ($statisticsProvider instanceof WaitingCountInterface) { 44 | $this->controller->stdout('- waiting: ', Console::FG_YELLOW); 45 | $this->controller->stdout($statisticsProvider->getWaitingCount() . PHP_EOL); 46 | } 47 | 48 | if ($statisticsProvider instanceof DelayedCountInterface) { 49 | $this->controller->stdout('- delayed: ', Console::FG_YELLOW); 50 | $this->controller->stdout($statisticsProvider->getDelayedCount() . PHP_EOL); 51 | } 52 | 53 | if ($statisticsProvider instanceof ReservedCountInterface) { 54 | $this->controller->stdout('- reserved: ', Console::FG_YELLOW); 55 | $this->controller->stdout($statisticsProvider->getReservedCount() . PHP_EOL); 56 | } 57 | 58 | if ($statisticsProvider instanceof DoneCountInterface) { 59 | $this->controller->stdout('- done: ', Console::FG_YELLOW); 60 | $this->controller->stdout($statisticsProvider->getDoneCount() . PHP_EOL); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/drivers/file/StatisticsProvider.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | class StatisticsProvider extends BaseObject implements DoneCountInterface, WaitingCountInterface, DelayedCountInterface, ReservedCountInterface 22 | { 23 | /** 24 | * @var Queue 25 | */ 26 | protected $queue; 27 | 28 | 29 | public function __construct(Queue $queue, $config = []) 30 | { 31 | $this->queue = $queue; 32 | parent::__construct($config); 33 | } 34 | 35 | /** 36 | * @inheritdoc 37 | */ 38 | public function getWaitingCount() 39 | { 40 | $data = $this->getIndexData(); 41 | return !empty($data['waiting']) ? count($data['waiting']) : 0; 42 | } 43 | 44 | /** 45 | * @inheritdoc 46 | */ 47 | public function getDelayedCount() 48 | { 49 | $data = $this->getIndexData(); 50 | return !empty($data['delayed']) ? count($data['delayed']) : 0; 51 | } 52 | 53 | /** 54 | * @inheritdoc 55 | */ 56 | public function getReservedCount() 57 | { 58 | $data = $this->getIndexData(); 59 | return !empty($data['reserved']) ? count($data['reserved']) : 0; 60 | } 61 | 62 | /** 63 | * @inheritdoc 64 | */ 65 | public function getDoneCount() 66 | { 67 | $data = $this->getIndexData(); 68 | $total = isset($data['lastId']) ? $data['lastId'] : 0; 69 | return $total - $this->getDelayedCount() - $this->getWaitingCount(); 70 | } 71 | 72 | protected function getIndexData() 73 | { 74 | $fileName = $this->queue->path . '/index.data'; 75 | if (file_exists($fileName)) { 76 | return call_user_func($this->queue->indexDeserializer, file_get_contents($fileName)); 77 | } else { 78 | return []; 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/drivers/beanstalk/Command.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | class Command extends CliCommand 19 | { 20 | /** 21 | * @var Queue 22 | */ 23 | public $queue; 24 | /** 25 | * @var string 26 | */ 27 | public $defaultAction = 'info'; 28 | 29 | 30 | /** 31 | * @inheritdoc 32 | */ 33 | public function actions() 34 | { 35 | return [ 36 | 'info' => InfoAction::class, 37 | ]; 38 | } 39 | 40 | /** 41 | * @inheritdoc 42 | */ 43 | protected function isWorkerAction($actionID) 44 | { 45 | return in_array($actionID, ['run', 'listen']); 46 | } 47 | 48 | /** 49 | * Runs all jobs from beanstalk-queue. 50 | * It can be used as cron job. 51 | * 52 | * @return null|int exit code. 53 | */ 54 | public function actionRun() 55 | { 56 | return $this->queue->run(false); 57 | } 58 | 59 | /** 60 | * Listens beanstalk-queue and runs new jobs. 61 | * It can be used as daemon process. 62 | * 63 | * @param int $timeout number of seconds to wait a job. 64 | * @throws Exception when params are invalid. 65 | * @return null|int exit code. 66 | */ 67 | public function actionListen($timeout = 3) 68 | { 69 | if (!is_numeric($timeout)) { 70 | throw new Exception('Timeout must be numeric.'); 71 | } 72 | if ($timeout < 1) { 73 | throw new Exception('Timeout must be greater than zero.'); 74 | } 75 | 76 | return $this->queue->run(true, $timeout); 77 | } 78 | 79 | /** 80 | * Removes a job by id. 81 | * 82 | * @param int $id of the job. 83 | * @throws Exception when the job is not found. 84 | * @since 2.0.1 85 | */ 86 | public function actionRemove($id) 87 | { 88 | if (!$this->queue->remove($id)) { 89 | throw new Exception('The job is not found.'); 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/drivers/db/StatisticsProvider.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | class StatisticsProvider extends BaseObject implements DoneCountInterface, WaitingCountInterface, DelayedCountInterface, ReservedCountInterface 23 | { 24 | /** 25 | * @var Queue 26 | */ 27 | protected $queue; 28 | 29 | 30 | public function __construct(Queue $queue, $config = []) 31 | { 32 | $this->queue = $queue; 33 | parent::__construct($config); 34 | } 35 | 36 | /** 37 | * @inheritdoc 38 | */ 39 | public function getWaitingCount() 40 | { 41 | return (new Query()) 42 | ->from($this->queue->tableName) 43 | ->andWhere(['channel' => $this->queue->channel]) 44 | ->andWhere(['reserved_at' => null]) 45 | ->andWhere(['delay' => 0])->count('*', $this->queue->db); 46 | } 47 | 48 | /** 49 | * @inheritdoc 50 | */ 51 | public function getDelayedCount() 52 | { 53 | return (new Query()) 54 | ->from($this->queue->tableName) 55 | ->andWhere(['channel' => $this->queue->channel]) 56 | ->andWhere(['reserved_at' => null]) 57 | ->andWhere(['>', 'delay', 0])->count('*', $this->queue->db); 58 | } 59 | 60 | /** 61 | * @inheritdoc 62 | */ 63 | public function getReservedCount() 64 | { 65 | return (new Query()) 66 | ->from($this->queue->tableName) 67 | ->andWhere(['channel' => $this->queue->channel]) 68 | ->andWhere('[[reserved_at]] is not null') 69 | ->andWhere(['done_at' => null])->count('*', $this->queue->db); 70 | } 71 | 72 | /** 73 | * @inheritdoc 74 | */ 75 | public function getDoneCount() 76 | { 77 | return (new Query()) 78 | ->from($this->queue->tableName) 79 | ->andWhere(['channel' => $this->queue->channel]) 80 | ->andWhere('[[done_at]] is not null')->count('*', $this->queue->db); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/drivers/sync/Queue.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | class Queue extends BaseQueue 21 | { 22 | /** 23 | * @var bool 24 | */ 25 | public $handle = false; 26 | 27 | /** 28 | * @var array of payloads 29 | */ 30 | private $payloads = []; 31 | /** 32 | * @var int last pushed ID 33 | */ 34 | private $pushedId = 0; 35 | /** 36 | * @var int started ID 37 | */ 38 | private $startedId = 0; 39 | /** 40 | * @var int last finished ID 41 | */ 42 | private $finishedId = 0; 43 | 44 | 45 | /** 46 | * @inheritdoc 47 | */ 48 | public function init() 49 | { 50 | parent::init(); 51 | if ($this->handle) { 52 | Yii::$app->on(Application::EVENT_AFTER_REQUEST, function () { 53 | ob_start(); 54 | $this->run(); 55 | ob_end_clean(); 56 | }); 57 | } 58 | } 59 | 60 | /** 61 | * Runs all jobs from queue. 62 | */ 63 | public function run() 64 | { 65 | while (($payload = array_shift($this->payloads)) !== null) { 66 | list($ttr, $message) = $payload; 67 | $this->startedId = $this->finishedId + 1; 68 | $this->handleMessage($this->startedId, $message, $ttr, 1); 69 | $this->finishedId = $this->startedId; 70 | $this->startedId = 0; 71 | } 72 | } 73 | 74 | /** 75 | * @inheritdoc 76 | */ 77 | protected function pushMessage($message, $ttr, $delay, $priority) 78 | { 79 | array_push($this->payloads, [$ttr, $message]); 80 | return ++$this->pushedId; 81 | } 82 | 83 | /** 84 | * @inheritdoc 85 | */ 86 | public function status($id) 87 | { 88 | if (!is_int($id) || $id <= 0 || $id > $this->pushedId) { 89 | throw new InvalidArgumentException("Unknown messages ID: $id."); 90 | } 91 | 92 | if ($id <= $this->finishedId) { 93 | return self::STATUS_DONE; 94 | } 95 | 96 | if ($id === $this->startedId) { 97 | return self::STATUS_RESERVED; 98 | } 99 | 100 | return self::STATUS_WAITING; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/drivers/redis/Command.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | class Command extends CliCommand 20 | { 21 | /** 22 | * @var Queue 23 | */ 24 | public $queue; 25 | /** 26 | * @var string 27 | */ 28 | public $defaultAction = 'info'; 29 | 30 | 31 | /** 32 | * @inheritdoc 33 | */ 34 | public function actions() 35 | { 36 | return [ 37 | 'info' => InfoAction::class, 38 | ]; 39 | } 40 | 41 | /** 42 | * @inheritdoc 43 | */ 44 | protected function isWorkerAction($actionID) 45 | { 46 | return in_array($actionID, ['run', 'listen'], true); 47 | } 48 | 49 | /** 50 | * Runs all jobs from redis-queue. 51 | * It can be used as cron job. 52 | * 53 | * @return null|int exit code. 54 | */ 55 | public function actionRun() 56 | { 57 | return $this->queue->run(false); 58 | } 59 | 60 | /** 61 | * Listens redis-queue and runs new jobs. 62 | * It can be used as daemon process. 63 | * 64 | * @param int $timeout number of seconds to wait a job. 65 | * @throws Exception when params are invalid. 66 | * @return null|int exit code. 67 | */ 68 | public function actionListen($timeout = 3) 69 | { 70 | if (!is_numeric($timeout)) { 71 | throw new Exception('Timeout must be numeric.'); 72 | } 73 | if ($timeout < 1) { 74 | throw new Exception('Timeout must be greater than zero.'); 75 | } 76 | 77 | return $this->queue->run(true, $timeout); 78 | } 79 | 80 | /** 81 | * Clears the queue. 82 | * 83 | * @since 2.0.1 84 | */ 85 | public function actionClear() 86 | { 87 | if ($this->confirm('Are you sure?')) { 88 | $this->queue->clear(); 89 | } 90 | } 91 | 92 | /** 93 | * Removes a job by id. 94 | * 95 | * @param int $id 96 | * @throws Exception when the job is not found. 97 | * @since 2.0.1 98 | */ 99 | public function actionRemove($id) 100 | { 101 | if (!$this->queue->remove($id)) { 102 | throw new Exception('The job is not found.'); 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/drivers/db/Command.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | class Command extends CliCommand 20 | { 21 | /** 22 | * @var Queue 23 | */ 24 | public $queue; 25 | /** 26 | * @var string 27 | */ 28 | public $defaultAction = 'info'; 29 | 30 | 31 | /** 32 | * @inheritdoc 33 | */ 34 | public function actions() 35 | { 36 | return [ 37 | 'info' => InfoAction::class, 38 | ]; 39 | } 40 | 41 | /** 42 | * @inheritdoc 43 | */ 44 | protected function isWorkerAction($actionID) 45 | { 46 | return in_array($actionID, ['run', 'listen'], true); 47 | } 48 | 49 | /** 50 | * Runs all jobs from db-queue. 51 | * It can be used as cron job. 52 | * 53 | * @return null|int exit code. 54 | */ 55 | public function actionRun() 56 | { 57 | return $this->queue->run(false); 58 | } 59 | 60 | /** 61 | * Listens db-queue and runs new jobs. 62 | * It can be used as daemon process. 63 | * 64 | * @param int $timeout number of seconds to sleep before next reading of the queue. 65 | * @throws Exception when params are invalid. 66 | * @return null|int exit code. 67 | */ 68 | public function actionListen($timeout = 3) 69 | { 70 | if (!is_numeric($timeout)) { 71 | throw new Exception('Timeout must be numeric.'); 72 | } 73 | if ($timeout < 1) { 74 | throw new Exception('Timeout must be greater than zero.'); 75 | } 76 | 77 | return $this->queue->run(true, $timeout); 78 | } 79 | 80 | /** 81 | * Clears the queue. 82 | * 83 | * @since 2.0.1 84 | */ 85 | public function actionClear() 86 | { 87 | if ($this->confirm('Are you sure?')) { 88 | $this->queue->clear(); 89 | } 90 | } 91 | 92 | /** 93 | * Removes a job by id. 94 | * 95 | * @param int $id 96 | * @throws Exception when the job is not found. 97 | * @since 2.0.1 98 | */ 99 | public function actionRemove($id) 100 | { 101 | if (!$this->queue->remove($id)) { 102 | throw new Exception('The job is not found.'); 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/drivers/file/Command.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | class Command extends CliCommand 20 | { 21 | /** 22 | * @var Queue 23 | */ 24 | public $queue; 25 | /** 26 | * @var string 27 | */ 28 | public $defaultAction = 'info'; 29 | 30 | 31 | /** 32 | * @inheritdoc 33 | */ 34 | public function actions() 35 | { 36 | return [ 37 | 'info' => InfoAction::class, 38 | ]; 39 | } 40 | 41 | /** 42 | * @inheritdoc 43 | */ 44 | protected function isWorkerAction($actionID) 45 | { 46 | return in_array($actionID, ['run', 'listen']); 47 | } 48 | 49 | /** 50 | * Runs all jobs from file-queue. 51 | * It can be used as cron job. 52 | * 53 | * @return null|int exit code. 54 | */ 55 | public function actionRun() 56 | { 57 | return $this->queue->run(false); 58 | } 59 | 60 | /** 61 | * Listens file-queue and runs new jobs. 62 | * It can be used as daemon process. 63 | * 64 | * @param int $timeout number of seconds to sleep before next reading of the queue. 65 | * @throws Exception when params are invalid. 66 | * @return null|int exit code. 67 | */ 68 | public function actionListen($timeout = 3) 69 | { 70 | if (!is_numeric($timeout)) { 71 | throw new Exception('Timeout must be numeric.'); 72 | } 73 | if ($timeout < 1) { 74 | throw new Exception('Timeout must be greater than zero.'); 75 | } 76 | 77 | return $this->queue->run(true, $timeout); 78 | } 79 | 80 | /** 81 | * Clears the queue. 82 | * 83 | * @since 2.0.1 84 | */ 85 | public function actionClear() 86 | { 87 | if ($this->confirm('Are you sure?')) { 88 | $this->queue->clear(); 89 | } 90 | } 91 | 92 | /** 93 | * Removes a job by id. 94 | * 95 | * @param int $id 96 | * @throws Exception when the job is not found. 97 | * @since 2.0.1 98 | */ 99 | public function actionRemove($id) 100 | { 101 | if (!$this->queue->remove((int) $id)) { 102 | throw new Exception('The job is not found.'); 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/drivers/db/InfoAction.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | class InfoAction extends Action 22 | { 23 | /** 24 | * @var Queue 25 | */ 26 | public $queue; 27 | 28 | 29 | /** 30 | * Info about queue status. 31 | */ 32 | public function run() 33 | { 34 | Console::output($this->format('Jobs', Console::FG_GREEN)); 35 | 36 | Console::stdout($this->format('- waiting: ', Console::FG_YELLOW)); 37 | Console::output($this->getWaiting()->count('*', $this->queue->db)); 38 | 39 | Console::stdout($this->format('- delayed: ', Console::FG_YELLOW)); 40 | Console::output($this->getDelayed()->count('*', $this->queue->db)); 41 | 42 | Console::stdout($this->format('- reserved: ', Console::FG_YELLOW)); 43 | Console::output($this->getReserved()->count('*', $this->queue->db)); 44 | 45 | Console::stdout($this->format('- done: ', Console::FG_YELLOW)); 46 | Console::output($this->getDone()->count('*', $this->queue->db)); 47 | } 48 | 49 | /** 50 | * @return Query 51 | */ 52 | protected function getWaiting() 53 | { 54 | return (new Query()) 55 | ->from($this->queue->tableName) 56 | ->andWhere(['channel' => $this->queue->channel]) 57 | ->andWhere(['reserved_at' => null]) 58 | ->andWhere(['delay' => 0]); 59 | } 60 | 61 | /** 62 | * @return Query 63 | */ 64 | protected function getDelayed() 65 | { 66 | return (new Query()) 67 | ->from($this->queue->tableName) 68 | ->andWhere(['channel' => $this->queue->channel]) 69 | ->andWhere(['reserved_at' => null]) 70 | ->andWhere(['>', 'delay', 0]); 71 | } 72 | 73 | /** 74 | * @return Query 75 | */ 76 | protected function getReserved() 77 | { 78 | return (new Query()) 79 | ->from($this->queue->tableName) 80 | ->andWhere(['channel' => $this->queue->channel]) 81 | ->andWhere('[[reserved_at]] is not null') 82 | ->andWhere(['done_at' => null]); 83 | } 84 | 85 | /** 86 | * @return Query 87 | */ 88 | protected function getDone() 89 | { 90 | return (new Query()) 91 | ->from($this->queue->tableName) 92 | ->andWhere(['channel' => $this->queue->channel]) 93 | ->andWhere('[[done_at]] is not null'); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/drivers/file/InfoAction.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | class InfoAction extends Action 21 | { 22 | /** 23 | * @var Queue 24 | */ 25 | public $queue; 26 | 27 | 28 | /** 29 | * Info about queue status. 30 | */ 31 | public function run() 32 | { 33 | Console::output($this->format('Jobs', Console::FG_GREEN)); 34 | 35 | Console::stdout($this->format('- waiting: ', Console::FG_YELLOW)); 36 | Console::output($this->getWaitingCount()); 37 | 38 | Console::stdout($this->format('- delayed: ', Console::FG_YELLOW)); 39 | Console::output($this->getDelayedCount()); 40 | 41 | Console::stdout($this->format('- reserved: ', Console::FG_YELLOW)); 42 | Console::output($this->getReservedCount()); 43 | 44 | Console::stdout($this->format('- done: ', Console::FG_YELLOW)); 45 | Console::output($this->getDoneCount()); 46 | } 47 | 48 | /** 49 | * @return int 50 | */ 51 | protected function getWaitingCount() 52 | { 53 | $data = $this->getIndexData(); 54 | return !empty($data['waiting']) ? count($data['waiting']) : 0; 55 | } 56 | 57 | /** 58 | * @return int 59 | */ 60 | protected function getDelayedCount() 61 | { 62 | $data = $this->getIndexData(); 63 | return !empty($data['delayed']) ? count($data['delayed']) : 0; 64 | } 65 | 66 | /** 67 | * @return int 68 | */ 69 | protected function getReservedCount() 70 | { 71 | $data = $this->getIndexData(); 72 | return !empty($data['reserved']) ? count($data['reserved']) : 0; 73 | } 74 | 75 | /** 76 | * @return int 77 | */ 78 | protected function getDoneCount() 79 | { 80 | $data = $this->getIndexData(); 81 | $total = isset($data['lastId']) ? $data['lastId'] : 0; 82 | return $total - $this->getDelayedCount() - $this->getWaitingCount(); 83 | } 84 | 85 | protected function getIndexData() 86 | { 87 | static $data; 88 | if ($data === null) { 89 | $fileName = $this->queue->path . '/index.data'; 90 | if (file_exists($fileName)) { 91 | $data = call_user_func($this->queue->indexDeserializer, file_get_contents($fileName)); 92 | } else { 93 | $data = []; 94 | } 95 | } 96 | 97 | return $data; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/debug/views/detail.php: -------------------------------------------------------------------------------- 1 | 'default', 10 | 'waiting' => 'info', 11 | 'reserved' => 'warning', 12 | 'done' => 'success', 13 | ]; 14 | ?> 15 |

Pushed jobs

16 | 17 | 18 |
19 |
20 |

21 | 22 | - 23 | 24 | 25 |

26 |
27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | $value): ?> 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 |
Sender
ID
TTR
Delay
Priority
Status
Class
Data
76 |
77 | 78 | registerCss( 80 | <<<'CSS' 81 | 82 | .panel > .table th { 83 | width: 25%; 84 | } 85 | 86 | CSS 87 | ); 88 | -------------------------------------------------------------------------------- /src/serializers/JsonSerializer.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | class JsonSerializer extends BaseObject implements SerializerInterface 21 | { 22 | /** 23 | * @var string 24 | */ 25 | public $classKey = 'class'; 26 | /** 27 | * @var int 28 | */ 29 | public $options = 0; 30 | 31 | 32 | /** 33 | * @inheritdoc 34 | */ 35 | public function serialize($job) 36 | { 37 | return Json::encode($this->toArray($job), $this->options); 38 | } 39 | 40 | /** 41 | * @inheritdoc 42 | */ 43 | public function unserialize($serialized) 44 | { 45 | return $this->fromArray(Json::decode($serialized)); 46 | } 47 | 48 | /** 49 | * @param mixed $data 50 | * @return array|mixed 51 | * @throws InvalidConfigException 52 | */ 53 | protected function toArray($data) 54 | { 55 | if (is_object($data)) { 56 | $result = [$this->classKey => get_class($data)]; 57 | foreach (get_object_vars($data) as $property => $value) { 58 | if ($property === $this->classKey) { 59 | throw new InvalidConfigException("Object cannot contain $this->classKey property."); 60 | } 61 | $result[$property] = $this->toArray($value); 62 | } 63 | 64 | return $result; 65 | } 66 | 67 | if (is_array($data)) { 68 | $result = []; 69 | foreach ($data as $key => $value) { 70 | if ($key === $this->classKey) { 71 | throw new InvalidConfigException("Array cannot contain $this->classKey key."); 72 | } 73 | $result[$key] = $this->toArray($value); 74 | } 75 | 76 | return $result; 77 | } 78 | 79 | return $data; 80 | } 81 | 82 | /** 83 | * @param array $data 84 | * @return mixed 85 | */ 86 | protected function fromArray($data) 87 | { 88 | if (!is_array($data)) { 89 | return $data; 90 | } 91 | 92 | if (!isset($data[$this->classKey])) { 93 | $result = []; 94 | foreach ($data as $key => $value) { 95 | $result[$key] = $this->fromArray($value); 96 | } 97 | 98 | return $result; 99 | } 100 | 101 | $config = ['class' => $data[$this->classKey]]; 102 | unset($data[$this->classKey]); 103 | foreach ($data as $property => $value) { 104 | $config[$property] = $this->fromArray($value); 105 | } 106 | 107 | return Yii::createObject($config); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/cli/SignalLoop.php: -------------------------------------------------------------------------------- 1 | 16 | * @since 2.0.2 17 | */ 18 | class SignalLoop extends BaseObject implements LoopInterface 19 | { 20 | /** 21 | * @var array of signals to exit from listening of the queue. 22 | */ 23 | public $exitSignals = [ 24 | 15, // SIGTERM 25 | 3, // SIGQUIT 26 | 2, // SIGINT 27 | 1, // SIGHUP 28 | ]; 29 | /** 30 | * @var array of signals to suspend listening of the queue. 31 | * For example: SIGTSTP 32 | */ 33 | public $suspendSignals = []; 34 | /** 35 | * @var array of signals to resume listening of the queue. 36 | * For example: SIGCONT 37 | */ 38 | public $resumeSignals = []; 39 | 40 | /** 41 | * @var Queue 42 | */ 43 | protected $queue; 44 | 45 | /** 46 | * @var bool status when exit signal was got. 47 | */ 48 | private static $exit = false; 49 | /** 50 | * @var bool status when suspend or resume signal was got. 51 | */ 52 | private static $pause = false; 53 | 54 | 55 | /** 56 | * @param Queue $queue 57 | * @inheritdoc 58 | */ 59 | public function __construct($queue, array $config = []) 60 | { 61 | $this->queue = $queue; 62 | parent::__construct($config); 63 | } 64 | 65 | /** 66 | * Sets signal handlers. 67 | * 68 | * @inheritdoc 69 | */ 70 | public function init() 71 | { 72 | parent::init(); 73 | if (extension_loaded('pcntl') && function_exists('pcntl_signal')) { 74 | foreach ($this->exitSignals as $signal) { 75 | pcntl_signal($signal, function () { 76 | self::$exit = true; 77 | }); 78 | } 79 | foreach ($this->suspendSignals as $signal) { 80 | pcntl_signal($signal, function () { 81 | self::$pause = true; 82 | }); 83 | } 84 | foreach ($this->resumeSignals as $signal) { 85 | pcntl_signal($signal, function () { 86 | self::$pause = false; 87 | }); 88 | } 89 | } 90 | } 91 | 92 | /** 93 | * Checks signals state. 94 | * 95 | * @inheritdoc 96 | */ 97 | public function canContinue() 98 | { 99 | if (extension_loaded('pcntl') && function_exists('pcntl_signal_dispatch')) { 100 | pcntl_signal_dispatch(); 101 | // Wait for resume signal until loop is suspended 102 | while (self::$pause && !self::$exit) { 103 | usleep(10000); 104 | pcntl_signal_dispatch(); 105 | } 106 | } 107 | 108 | return !self::$exit; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 | 5 |

Yii2 Queue Extension

6 |
7 |

8 | 9 | An extension for running tasks asynchronously via queues. 10 | 11 | It supports queues based on **DB**, **Redis**, **RabbitMQ**, **AMQP**, **Beanstalk**, **ActiveMQ** and **Gearman**. 12 | 13 | Documentation is at [docs/guide/README.md](docs/guide/README.md). 14 | 15 | [![Latest Stable Version](https://poser.pugx.org/yiisoft/yii2-queue/v/stable.svg)](https://packagist.org/packages/yiisoft/yii2-queue) 16 | [![Total Downloads](https://poser.pugx.org/yiisoft/yii2-queue/downloads.svg)](https://packagist.org/packages/yiisoft/yii2-queue) 17 | [![Build Status](https://github.com/yiisoft/yii2-queue/workflows/build/badge.svg)](https://github.com/yiisoft/yii2-queue/actions) 18 | 19 | Installation 20 | ------------ 21 | 22 | The preferred way to install this extension is through [composer](https://getcomposer.org/download/): 23 | 24 | ``` 25 | php composer.phar require --prefer-dist yiisoft/yii2-queue 26 | ``` 27 | 28 | Basic Usage 29 | ----------- 30 | 31 | Each task which is sent to queue should be defined as a separate class. 32 | For example, if you need to download and save a file the class may look like the following: 33 | 34 | ```php 35 | class DownloadJob extends BaseObject implements \yii\queue\JobInterface 36 | { 37 | public $url; 38 | public $file; 39 | 40 | public function execute($queue) 41 | { 42 | file_put_contents($this->file, file_get_contents($this->url)); 43 | } 44 | } 45 | ``` 46 | 47 | Here's how to send a task into the queue: 48 | 49 | ```php 50 | Yii::$app->queue->push(new DownloadJob([ 51 | 'url' => 'http://example.com/image.jpg', 52 | 'file' => '/tmp/image.jpg', 53 | ])); 54 | ``` 55 | To push a job into the queue that should run after 5 minutes: 56 | 57 | ```php 58 | Yii::$app->queue->delay(5 * 60)->push(new DownloadJob([ 59 | 'url' => 'http://example.com/image.jpg', 60 | 'file' => '/tmp/image.jpg', 61 | ])); 62 | ``` 63 | 64 | The exact way a task is executed depends on the used driver. Most drivers can be run using 65 | console commands, which the component automatically registers in your application. 66 | 67 | This command obtains and executes tasks in a loop until the queue is empty: 68 | 69 | ```sh 70 | yii queue/run 71 | ``` 72 | 73 | This command launches a daemon which infinitely queries the queue: 74 | 75 | ```sh 76 | yii queue/listen 77 | ``` 78 | 79 | See the documentation for more details about driver specific console commands and their options. 80 | 81 | The component also has the ability to track the status of a job which was pushed into queue. 82 | 83 | ```php 84 | // Push a job into the queue and get a message ID. 85 | $id = Yii::$app->queue->push(new SomeJob()); 86 | 87 | // Check whether the job is waiting for execution. 88 | Yii::$app->queue->isWaiting($id); 89 | 90 | // Check whether a worker got the job from the queue and executes it. 91 | Yii::$app->queue->isReserved($id); 92 | 93 | // Check whether a worker has executed the job. 94 | Yii::$app->queue->isDone($id); 95 | ``` 96 | 97 | For more details see [the guide](docs/guide/README.md). 98 | -------------------------------------------------------------------------------- /src/drivers/gearman/Queue.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | class Queue extends CliQueue 19 | { 20 | public $host = 'localhost'; 21 | public $port = 4730; 22 | public $channel = 'queue'; 23 | /** 24 | * @var string command class name 25 | */ 26 | public $commandClass = Command::class; 27 | 28 | 29 | /** 30 | * Listens queue and runs each job. 31 | * 32 | * @param bool $repeat whether to continue listening when queue is empty. 33 | * @return null|int exit code. 34 | * @internal for worker command only. 35 | * @since 2.0.2 36 | */ 37 | public function run($repeat) 38 | { 39 | return $this->runWorker(function (callable $canContinue) use ($repeat) { 40 | $worker = new \GearmanWorker(); 41 | $worker->addServer($this->host, $this->port); 42 | $worker->addFunction($this->channel, function (\GearmanJob $payload) { 43 | list($ttr, $message) = explode(';', $payload->workload(), 2); 44 | $this->handleMessage($payload->handle(), $message, $ttr, 1); 45 | }); 46 | $worker->setTimeout($repeat ? 1000 : 1); 47 | while ($canContinue()) { 48 | $result = $worker->work(); 49 | if (!$result && !$repeat) { 50 | break; 51 | } 52 | } 53 | }); 54 | } 55 | 56 | /** 57 | * @inheritdoc 58 | */ 59 | protected function pushMessage($message, $ttr, $delay, $priority) 60 | { 61 | if ($delay) { 62 | throw new NotSupportedException('Delayed work is not supported in the driver.'); 63 | } 64 | 65 | switch ($priority) { 66 | case 'high': 67 | return $this->getClient()->doHighBackground($this->channel, "$ttr;$message"); 68 | case 'low': 69 | return $this->getClient()->doLowBackground($this->channel, "$ttr;$message"); 70 | default: 71 | return $this->getClient()->doBackground($this->channel, "$ttr;$message"); 72 | } 73 | } 74 | 75 | /** 76 | * @inheritdoc 77 | */ 78 | public function status($id) 79 | { 80 | $status = $this->getClient()->jobStatus($id); 81 | if ($status[0] && !$status[1]) { 82 | return self::STATUS_WAITING; 83 | } 84 | 85 | if ($status[0] && $status[1]) { 86 | return self::STATUS_RESERVED; 87 | } 88 | 89 | return self::STATUS_DONE; 90 | } 91 | 92 | /** 93 | * @return \GearmanClient 94 | */ 95 | protected function getClient() 96 | { 97 | if (!$this->_client) { 98 | $this->_client = new \GearmanClient(); 99 | $this->_client->addServer($this->host, $this->port); 100 | } 101 | return $this->_client; 102 | } 103 | 104 | private $_client; 105 | } 106 | -------------------------------------------------------------------------------- /src/drivers/db/migrations/M170509001400Retry.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | class M170509001400Retry extends Migration 18 | { 19 | public $tableName = '{{%queue}}'; 20 | 21 | 22 | public function up() 23 | { 24 | if ($this->db->driverName !== 'sqlite') { 25 | $this->renameColumn($this->tableName, 'created_at', 'pushed_at'); 26 | $this->addColumn($this->tableName, 'ttr', $this->integer()->notNull()->after('pushed_at')); 27 | $this->renameColumn($this->tableName, 'timeout', 'delay'); 28 | $this->dropIndex('started_at', $this->tableName); 29 | $this->renameColumn($this->tableName, 'started_at', 'reserved_at'); 30 | $this->createIndex('reserved_at', $this->tableName, 'reserved_at'); 31 | $this->addColumn($this->tableName, 'attempt', $this->integer()->after('reserved_at')); 32 | $this->renameColumn($this->tableName, 'finished_at', 'done_at'); 33 | } else { 34 | $this->dropTable($this->tableName); 35 | $this->createTable($this->tableName, [ 36 | 'id' => $this->primaryKey(), 37 | 'channel' => $this->string()->notNull(), 38 | 'job' => $this->binary()->notNull(), 39 | 'pushed_at' => $this->integer()->notNull(), 40 | 'ttr' => $this->integer()->notNull(), 41 | 'delay' => $this->integer()->notNull(), 42 | 'reserved_at' => $this->integer(), 43 | 'attempt' => $this->integer(), 44 | 'done_at' => $this->integer(), 45 | ]); 46 | $this->createIndex('channel', $this->tableName, 'channel'); 47 | $this->createIndex('reserved_at', $this->tableName, 'reserved_at'); 48 | } 49 | } 50 | 51 | public function down() 52 | { 53 | if ($this->db->driverName !== 'sqlite') { 54 | $this->renameColumn($this->tableName, 'done_at', 'finished_at'); 55 | $this->dropColumn($this->tableName, 'attempt'); 56 | $this->dropIndex('reserved_at', $this->tableName); 57 | $this->renameColumn($this->tableName, 'reserved_at', 'started_at'); 58 | $this->createIndex('started_at', $this->tableName, 'started_at'); 59 | $this->renameColumn($this->tableName, 'delay', 'timeout'); 60 | $this->dropColumn($this->tableName, 'ttr'); 61 | $this->renameColumn($this->tableName, 'pushed_at', 'created_at'); 62 | } else { 63 | $this->dropTable($this->tableName); 64 | $this->createTable($this->tableName, [ 65 | 'id' => $this->primaryKey(), 66 | 'channel' => $this->string()->notNull(), 67 | 'job' => $this->binary()->notNull(), 68 | 'created_at' => $this->integer()->notNull(), 69 | 'timeout' => $this->integer()->notNull(), 70 | 'started_at' => $this->integer(), 71 | 'finished_at' => $this->integer(), 72 | ]); 73 | $this->createIndex('channel', $this->tableName, 'channel'); 74 | $this->createIndex('started_at', $this->tableName, 'started_at'); 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yiisoft/yii2-queue", 3 | "description": "Yii2 Queue Extension which supports queues based on DB, Redis, RabbitMQ, Beanstalk, SQS, and Gearman", 4 | "type": "yii2-extension", 5 | "keywords": ["yii", "queue", "async", "gii", "db", "redis", "rabbitmq", "beanstalk", "gearman", "sqs"], 6 | "license": "BSD-3-Clause", 7 | "authors": [ 8 | { 9 | "name": "Roman Zhuravlev", 10 | "email": "zhuravljov@gmail.com" 11 | } 12 | ], 13 | "support": { 14 | "issues": "https://github.com/yiisoft/yii2-queue/issues", 15 | "source": "https://github.com/yiisoft/yii2-queue", 16 | "docs": "https://github.com/yiisoft/yii2-queue/blob/master/docs/guide" 17 | }, 18 | "require": { 19 | "php": ">=5.5.0", 20 | "yiisoft/yii2": "~2.0.14", 21 | "symfony/process": "^3.3||^4.0||^5.0||^6.0||^7.0" 22 | }, 23 | "require-dev": { 24 | "yiisoft/yii2-redis": "2.0.19", 25 | "php-amqplib/php-amqplib": "^2.8.0||^3.0.0", 26 | "enqueue/amqp-lib": "^0.8||^0.9.10||^0.10.0", 27 | "pda/pheanstalk": "~3.2.1", 28 | "opis/closure": "*", 29 | "yiisoft/yii2-debug": "~2.1.0", 30 | "yiisoft/yii2-gii": "~2.2.0", 31 | "phpunit/phpunit": "4.8.34", 32 | "aws/aws-sdk-php": ">=2.4", 33 | "enqueue/stomp": "^0.8.39||0.10.19", 34 | "cweagans/composer-patches": "^1.7" 35 | }, 36 | "suggest": { 37 | "ext-pcntl": "Need for process signals.", 38 | "yiisoft/yii2-redis": "Need for Redis queue.", 39 | "pda/pheanstalk": "Need for Beanstalk queue.", 40 | "php-amqplib/php-amqplib": "Need for AMQP queue.", 41 | "enqueue/amqp-lib": "Need for AMQP interop queue.", 42 | "ext-gearman": "Need for Gearman queue.", 43 | "aws/aws-sdk-php": "Need for aws SQS.", 44 | "enqueue/stomp": "Need for Stomp queue." 45 | }, 46 | "autoload": { 47 | "psr-4": { 48 | "yii\\queue\\": "src", 49 | "yii\\queue\\amqp\\": "src/drivers/amqp", 50 | "yii\\queue\\amqp_interop\\": "src/drivers/amqp_interop", 51 | "yii\\queue\\beanstalk\\": "src/drivers/beanstalk", 52 | "yii\\queue\\db\\": "src/drivers/db", 53 | "yii\\queue\\file\\": "src/drivers/file", 54 | "yii\\queue\\gearman\\": "src/drivers/gearman", 55 | "yii\\queue\\redis\\": "src/drivers/redis", 56 | "yii\\queue\\sync\\": "src/drivers/sync", 57 | "yii\\queue\\sqs\\": "src/drivers/sqs", 58 | "yii\\queue\\stomp\\": "src/drivers/stomp" 59 | } 60 | }, 61 | "autoload-dev": { 62 | "psr-4": { 63 | "tests\\": "tests" 64 | } 65 | }, 66 | "config": { 67 | "allow-plugins": { 68 | "yiisoft/yii2-composer": true, 69 | "cweagans/composer-patches": true, 70 | "php-http/discovery": true 71 | } 72 | }, 73 | "extra": { 74 | "branch-alias": { 75 | "dev-master": "2.x-dev" 76 | }, 77 | "composer-exit-on-patch-failure": true, 78 | "patches": { 79 | "phpunit/phpunit-mock-objects": { 80 | "Fix PHP 7 and 8 compatibility": "https://yiisoft.github.io/phpunit-patches/phpunit_mock_objects.patch" 81 | }, 82 | "phpunit/phpunit": { 83 | "Fix PHP 7 compatibility": "https://yiisoft.github.io/phpunit-patches/phpunit_php7.patch", 84 | "Fix PHP 8 compatibility": "https://yiisoft.github.io/phpunit-patches/phpunit_php8.patch" 85 | } 86 | } 87 | }, 88 | "repositories": [ 89 | { 90 | "type": "composer", 91 | "url": "https://asset-packagist.org" 92 | } 93 | ] 94 | } 95 | -------------------------------------------------------------------------------- /src/debug/Panel.php: -------------------------------------------------------------------------------- 1 | 22 | */ 23 | class Panel extends \yii\debug\Panel implements ViewContextInterface 24 | { 25 | private $_jobs = []; 26 | 27 | 28 | /** 29 | * @inheritdoc 30 | */ 31 | public function getName() 32 | { 33 | return 'Queue'; 34 | } 35 | 36 | /** 37 | * @inheritdoc 38 | */ 39 | public function init() 40 | { 41 | parent::init(); 42 | PushEvent::on(Queue::class, Queue::EVENT_AFTER_PUSH, function (PushEvent $event) { 43 | $this->_jobs[] = $this->getPushData($event); 44 | }); 45 | } 46 | 47 | /** 48 | * @param PushEvent $event 49 | * @return array 50 | */ 51 | protected function getPushData(PushEvent $event) 52 | { 53 | $data = []; 54 | foreach (Yii::$app->getComponents(false) as $id => $component) { 55 | if ($component === $event->sender) { 56 | $data['sender'] = $id; 57 | break; 58 | } 59 | } 60 | $data['id'] = $event->id; 61 | $data['ttr'] = $event->ttr; 62 | $data['delay'] = $event->delay; 63 | $data['priority'] = $event->priority; 64 | if ($event->job instanceof JobInterface) { 65 | $data['class'] = get_class($event->job); 66 | $data['properties'] = []; 67 | foreach (get_object_vars($event->job) as $property => $value) { 68 | $data['properties'][$property] = VarDumper::dumpAsString($value); 69 | } 70 | } else { 71 | $data['data'] = VarDumper::dumpAsString($event->job); 72 | } 73 | 74 | return $data; 75 | } 76 | 77 | /** 78 | * @inheritdoc 79 | */ 80 | public function save() 81 | { 82 | return ['jobs' => $this->_jobs]; 83 | } 84 | 85 | /** 86 | * @inheritdoc 87 | */ 88 | public function getViewPath() 89 | { 90 | return __DIR__ . '/views'; 91 | } 92 | 93 | /** 94 | * @inheritdoc 95 | */ 96 | public function getSummary() 97 | { 98 | return Yii::$app->view->render('summary', [ 99 | 'url' => $this->getUrl(), 100 | 'count' => isset($this->data['jobs']) ? count($this->data['jobs']) : 0, 101 | ], $this); 102 | } 103 | 104 | /** 105 | * @inheritdoc 106 | */ 107 | public function getDetail() 108 | { 109 | $jobs = isset($this->data['jobs']) ? $this->data['jobs'] : []; 110 | foreach ($jobs as &$job) { 111 | $job['status'] = 'unknown'; 112 | /** @var Queue $queue */ 113 | if ($queue = Yii::$app->get($job['sender'], false)) { 114 | try { 115 | if ($queue->isWaiting($job['id'])) { 116 | $job['status'] = 'waiting'; 117 | } elseif ($queue->isReserved($job['id'])) { 118 | $job['status'] = 'reserved'; 119 | } elseif ($queue->isDone($job['id'])) { 120 | $job['status'] = 'done'; 121 | } 122 | } catch (NotSupportedException $e) { 123 | } catch (\Exception $e) { 124 | $job['status'] = $e->getMessage(); 125 | } 126 | } 127 | } 128 | unset($job); 129 | 130 | return Yii::$app->view->render('detail', compact('jobs'), $this); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /UPGRADE.md: -------------------------------------------------------------------------------- 1 | Upgrading Instructions 2 | ====================== 3 | 4 | This file contains the upgrade notes. These notes highlight changes that could break your 5 | application when you upgrade the package from one version to another. 6 | 7 | Upgrade to 2.3.6 8 | ---------------- 9 | 10 | * The `maxPriority` property was removed from [amqp_interop](docs/guide/driver-amqp-interop.md). 11 | Use property `queueOptionalArguments` argument `x-max-priority`. 12 | 13 | Upgrade to 2.1.1 14 | ---------------- 15 | 16 | * `\yii\queue\ErrorEvent` has been deprecated and will be removed in `3.0`. 17 | Use `\yii\queue\ExecEvent` instead. 18 | 19 | Upgrade from 2.0.1 to 2.0.2 20 | --------------------------- 21 | 22 | * The [Amqp driver](docs/guide/driver-amqp.md) has been deprecated and will be removed in `2.1`. 23 | It is advised to migrate to [Amqp Interop](docs/guide/driver-amqp-interop.md) instead. 24 | 25 | * Added `\yii\queue\cli\Command::isWorkerAction()` abstract method. If you use your own console 26 | controllers to run queue listeners, you must implement it. 27 | 28 | * `\yii\queue\cli\Signal` helper is deprecated and will be removed in `2.1`. The extension uses 29 | `\yii\queue\cli\SignalLoop` instead of the helper. 30 | 31 | * If you use your own console controller to listen to a queue, you must upgrade it. See the native 32 | console controllers for how to upgrade. 33 | 34 | Upgrade from 2.0.0 to 2.0.1 35 | --------------------------- 36 | 37 | * `yii\queue\cli\Verbose` behavior was renamed to `yii\queue\cli\VerboseBehavior`. The old class was 38 | marked as deprecated and will be removed in `2.1.0`. 39 | 40 | * `Job`, `RetryableJob` and `Serializer` interfaces were renamed to `JobInterface`, 41 | `RetryableJobInterface` and `SerializerInterface`. The old names are declared as deprecated 42 | and will be removed in `2.1.0`. 43 | 44 | Upgrade from 1.1.0 to 2.0.0 45 | --------------------------- 46 | 47 | * Code has been moved to yii namespace. Check and replace `zhuravljov\yii` to `yii` namespace for 48 | your project. 49 | 50 | Upgrade from 1.0.0 to 1.1.0 51 | --------------------------- 52 | 53 | * Event `Queue::EVENT_AFTER_EXEC_ERROR` renamed to `Queue::EVENT_AFTER_ERROR`. 54 | 55 | * Removed method `Queue::later()`. Use method chain `Yii::$app->queue->delay(60)->push()` instead. 56 | 57 | * Changed table schema for DB driver. Apply migration. 58 | 59 | 60 | Upgrade from 0.x to 1.0.0 61 | ------------------------- 62 | 63 | * Some methods and constants were modified. 64 | 65 | - Method `Job::run()` modified to `Job::execute($queue)`. 66 | - Const `Queue::EVENT_BEFORE_WORK` renamed to `Queue::EVENT_BEFORE_EXEC`. 67 | - Const `Queue::EVENT_AFTER_WORK` renamed to `Queue::EVENT_AFTER_EXEC`. 68 | - Const `Queue::EVENT_AFTER_ERROR` renamed to `Queue::EVENT_AFTER_EXEC_ERROR`. 69 | 70 | * Method `Queue::sendMessage` renamed to `Queue::pushMessage`. Check it if you use it in your own 71 | custom drivers. 72 | 73 | 74 | Upgrade from 0.10.1 75 | ------------------- 76 | 77 | * Driver property was removed and this functionality was moved into queue classes. If you use public 78 | methods of `Yii::$app->queue->driver` you need to use the methods of `Yii::$app->queue`. 79 | 80 | You also need to check your configs. For example, now the config for the db queue is: 81 | 82 | ```php 83 | 'queue' => [ 84 | 'class' => \zhuravljov\yii\queue\db\Queue::class, 85 | 'db' => 'db', 86 | 'tableName' => '{{%queue}}', 87 | 'channel' => 'default', 88 | 'mutex' => \yii\mutex\MysqlMutex::class, 89 | ], 90 | ``` 91 | 92 | Instead of the old variant: 93 | 94 | ```php 95 | 'queue' => [ 96 | 'class' => \zhuravljov\yii\queue\Queue::class, 97 | 'driver' => [ 98 | 'class' => \yii\queue\db\Driver::class, 99 | 'db' => 'db', 100 | 'tableName' => '{{%queue}}' 101 | 'channel' => 'default', 102 | 'mutex' => \yii\mutex\MysqlMutex::class, 103 | ], 104 | ], 105 | ``` 106 | -------------------------------------------------------------------------------- /src/LogBehavior.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | class LogBehavior extends Behavior 19 | { 20 | /** 21 | * @var Queue 22 | * @inheritdoc 23 | */ 24 | public $owner; 25 | /** 26 | * @var bool 27 | */ 28 | public $autoFlush = true; 29 | 30 | 31 | /** 32 | * @inheritdoc 33 | */ 34 | public function events() 35 | { 36 | return [ 37 | Queue::EVENT_AFTER_PUSH => 'afterPush', 38 | Queue::EVENT_BEFORE_EXEC => 'beforeExec', 39 | Queue::EVENT_AFTER_EXEC => 'afterExec', 40 | Queue::EVENT_AFTER_ERROR => 'afterError', 41 | cli\Queue::EVENT_WORKER_START => 'workerStart', 42 | cli\Queue::EVENT_WORKER_STOP => 'workerStop', 43 | ]; 44 | } 45 | 46 | /** 47 | * @param PushEvent $event 48 | */ 49 | public function afterPush(PushEvent $event) 50 | { 51 | $title = $this->getJobTitle($event); 52 | Yii::info("$title is pushed.", Queue::class); 53 | } 54 | 55 | /** 56 | * @param ExecEvent $event 57 | */ 58 | public function beforeExec(ExecEvent $event) 59 | { 60 | $title = $this->getExecTitle($event); 61 | Yii::info("$title is started.", Queue::class); 62 | Yii::beginProfile($title, Queue::class); 63 | } 64 | 65 | /** 66 | * @param ExecEvent $event 67 | */ 68 | public function afterExec(ExecEvent $event) 69 | { 70 | $title = $this->getExecTitle($event); 71 | Yii::endProfile($title, Queue::class); 72 | Yii::info("$title is finished.", Queue::class); 73 | if ($this->autoFlush) { 74 | Yii::getLogger()->flush(true); 75 | } 76 | } 77 | 78 | /** 79 | * @param ExecEvent $event 80 | */ 81 | public function afterError(ExecEvent $event) 82 | { 83 | $title = $this->getExecTitle($event); 84 | Yii::endProfile($title, Queue::class); 85 | Yii::error("$title is finished with error: $event->error.", Queue::class); 86 | if ($this->autoFlush) { 87 | Yii::getLogger()->flush(true); 88 | } 89 | } 90 | 91 | /** 92 | * @param cli\WorkerEvent $event 93 | * @since 2.0.2 94 | */ 95 | public function workerStart(cli\WorkerEvent $event) 96 | { 97 | $title = 'Worker ' . $event->sender->getWorkerPid(); 98 | Yii::info("$title is started.", Queue::class); 99 | Yii::beginProfile($title, Queue::class); 100 | if ($this->autoFlush) { 101 | Yii::getLogger()->flush(true); 102 | } 103 | } 104 | 105 | /** 106 | * @param cli\WorkerEvent $event 107 | * @since 2.0.2 108 | */ 109 | public function workerStop(cli\WorkerEvent $event) 110 | { 111 | $title = 'Worker ' . $event->sender->getWorkerPid(); 112 | Yii::endProfile($title, Queue::class); 113 | Yii::info("$title is stopped.", Queue::class); 114 | if ($this->autoFlush) { 115 | Yii::getLogger()->flush(true); 116 | } 117 | } 118 | 119 | /** 120 | * @param JobEvent $event 121 | * @return string 122 | * @since 2.0.2 123 | */ 124 | protected function getJobTitle(JobEvent $event) 125 | { 126 | $name = $event->job instanceof JobInterface ? get_class($event->job) : 'unknown job'; 127 | return "[$event->id] $name"; 128 | } 129 | 130 | /** 131 | * @param ExecEvent $event 132 | * @return string 133 | * @since 2.0.2 134 | */ 135 | protected function getExecTitle(ExecEvent $event) 136 | { 137 | $title = $this->getJobTitle($event); 138 | $extra = "attempt: $event->attempt"; 139 | if ($pid = $event->sender->getWorkerPid()) { 140 | $extra .= ", PID: $pid"; 141 | } 142 | return "$title ($extra)"; 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/drivers/beanstalk/Queue.php: -------------------------------------------------------------------------------- 1 | 23 | */ 24 | class Queue extends CliQueue 25 | { 26 | /** 27 | * @var string connection host 28 | */ 29 | public $host = 'localhost'; 30 | /** 31 | * @var int connection port 32 | */ 33 | public $port = PheanstalkInterface::DEFAULT_PORT; 34 | /** 35 | * @var string beanstalk tube 36 | */ 37 | public $tube = 'queue'; 38 | /** 39 | * @var string command class name 40 | */ 41 | public $commandClass = Command::class; 42 | 43 | 44 | /** 45 | * Listens queue and runs each job. 46 | * 47 | * @param bool $repeat whether to continue listening when queue is empty. 48 | * @param int $timeout number of seconds to wait for next message. 49 | * @return null|int exit code. 50 | * @internal for worker command only. 51 | * @since 2.0.2 52 | */ 53 | public function run($repeat, $timeout = 0) 54 | { 55 | return $this->runWorker(function (callable $canContinue) use ($repeat, $timeout) { 56 | while ($canContinue()) { 57 | if ($payload = $this->getPheanstalk()->reserveFromTube($this->tube, $timeout)) { 58 | $info = $this->getPheanstalk()->statsJob($payload); 59 | if ($this->handleMessage( 60 | $payload->getId(), 61 | $payload->getData(), 62 | $info->ttr, 63 | $info->reserves 64 | )) { 65 | $this->getPheanstalk()->delete($payload); 66 | } 67 | } elseif (!$repeat) { 68 | break; 69 | } 70 | } 71 | }); 72 | } 73 | 74 | /** 75 | * @inheritdoc 76 | */ 77 | public function status($id) 78 | { 79 | if (!is_numeric($id) || $id <= 0) { 80 | throw new InvalidArgumentException("Unknown message ID: $id."); 81 | } 82 | 83 | try { 84 | $stats = $this->getPheanstalk()->statsJob($id); 85 | if ($stats['state'] === 'reserved') { 86 | return self::STATUS_RESERVED; 87 | } 88 | 89 | return self::STATUS_WAITING; 90 | } catch (ServerException $e) { 91 | if ($e->getMessage() === 'Server reported NOT_FOUND') { 92 | return self::STATUS_DONE; 93 | } 94 | 95 | throw $e; 96 | } 97 | } 98 | 99 | /** 100 | * Removes a job by ID. 101 | * 102 | * @param int $id of a job 103 | * @return bool 104 | * @since 2.0.1 105 | */ 106 | public function remove($id) 107 | { 108 | try { 109 | $this->getPheanstalk()->delete(new Job($id, null)); 110 | return true; 111 | } catch (ServerException $e) { 112 | if (strpos($e->getMessage(), 'NOT_FOUND') === 0) { 113 | return false; 114 | } 115 | 116 | throw $e; 117 | } 118 | } 119 | 120 | /** 121 | * @inheritdoc 122 | */ 123 | protected function pushMessage($message, $ttr, $delay, $priority) 124 | { 125 | return $this->getPheanstalk()->putInTube( 126 | $this->tube, 127 | $message, 128 | $priority ?: PheanstalkInterface::DEFAULT_PRIORITY, 129 | $delay, 130 | $ttr 131 | ); 132 | } 133 | 134 | /** 135 | * @return object tube statistics 136 | */ 137 | public function getStatsTube() 138 | { 139 | return $this->getPheanstalk()->statsTube($this->tube); 140 | } 141 | 142 | /** 143 | * @return Pheanstalk 144 | */ 145 | protected function getPheanstalk() 146 | { 147 | if (!$this->_pheanstalk) { 148 | $this->_pheanstalk = new Pheanstalk($this->host, $this->port); 149 | } 150 | return $this->_pheanstalk; 151 | } 152 | 153 | private $_pheanstalk; 154 | } 155 | -------------------------------------------------------------------------------- /src/cli/Queue.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | abstract class Queue extends BaseQueue implements BootstrapInterface 23 | { 24 | /** 25 | * @event WorkerEvent that is triggered when the worker is started. 26 | * @since 2.0.2 27 | */ 28 | const EVENT_WORKER_START = 'workerStart'; 29 | /** 30 | * @event WorkerEvent that is triggered each iteration between requests to queue. 31 | * @since 2.0.3 32 | */ 33 | const EVENT_WORKER_LOOP = 'workerLoop'; 34 | /** 35 | * @event WorkerEvent that is triggered when the worker is stopped. 36 | * @since 2.0.2 37 | */ 38 | const EVENT_WORKER_STOP = 'workerStop'; 39 | 40 | /** 41 | * @var array|string 42 | * @since 2.0.2 43 | */ 44 | public $loopConfig = SignalLoop::class; 45 | /** 46 | * @var string command class name 47 | */ 48 | public $commandClass = Command::class; 49 | /** 50 | * @var array of additional options of command 51 | */ 52 | public $commandOptions = []; 53 | /** 54 | * @var callable|null 55 | * @internal for worker command only 56 | */ 57 | public $messageHandler; 58 | 59 | /** 60 | * @var int|null current process ID of a worker. 61 | * @since 2.0.2 62 | */ 63 | private $_workerPid; 64 | 65 | 66 | /** 67 | * @return string command id 68 | * @throws 69 | */ 70 | protected function getCommandId() 71 | { 72 | foreach (Yii::$app->getComponents(false) as $id => $component) { 73 | if ($component === $this) { 74 | return Inflector::camel2id($id); 75 | } 76 | } 77 | throw new InvalidConfigException('Queue must be an application component.'); 78 | } 79 | 80 | /** 81 | * @inheritdoc 82 | */ 83 | public function bootstrap($app) 84 | { 85 | if ($app instanceof ConsoleApp) { 86 | $app->controllerMap[$this->getCommandId()] = [ 87 | 'class' => $this->commandClass, 88 | 'queue' => $this, 89 | ] + $this->commandOptions; 90 | } 91 | } 92 | 93 | /** 94 | * Runs worker. 95 | * 96 | * @param callable $handler 97 | * @return null|int exit code 98 | * @since 2.0.2 99 | */ 100 | protected function runWorker(callable $handler) 101 | { 102 | $this->_workerPid = getmypid(); 103 | /** @var LoopInterface $loop */ 104 | $loop = Yii::createObject($this->loopConfig, [$this]); 105 | 106 | $event = new WorkerEvent(['loop' => $loop]); 107 | $this->trigger(self::EVENT_WORKER_START, $event); 108 | if ($event->exitCode !== null) { 109 | return $event->exitCode; 110 | } 111 | 112 | $exitCode = null; 113 | try { 114 | call_user_func($handler, function () use ($loop, $event) { 115 | $this->trigger(self::EVENT_WORKER_LOOP, $event); 116 | return $event->exitCode === null && $loop->canContinue(); 117 | }); 118 | } finally { 119 | $this->trigger(self::EVENT_WORKER_STOP, $event); 120 | $this->_workerPid = null; 121 | } 122 | 123 | return $event->exitCode; 124 | } 125 | 126 | /** 127 | * Gets process ID of a worker. 128 | * 129 | * @inheritdoc 130 | * @return int|null 131 | * @since 2.0.2 132 | */ 133 | public function getWorkerPid() 134 | { 135 | return $this->_workerPid; 136 | } 137 | 138 | /** 139 | * @inheritdoc 140 | */ 141 | protected function handleMessage($id, $message, $ttr, $attempt) 142 | { 143 | if ($this->messageHandler) { 144 | return call_user_func($this->messageHandler, $id, $message, $ttr, $attempt); 145 | } 146 | 147 | return parent::handleMessage($id, $message, $ttr, $attempt); 148 | } 149 | 150 | /** 151 | * @param string $id of a message 152 | * @param string $message 153 | * @param int $ttr time to reserve 154 | * @param int $attempt number 155 | * @param int|null $workerPid of worker process 156 | * @return bool 157 | * @internal for worker command only 158 | */ 159 | public function execute($id, $message, $ttr, $attempt, $workerPid) 160 | { 161 | $this->_workerPid = $workerPid; 162 | return parent::handleMessage($id, $message, $ttr, $attempt); 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/drivers/amqp/Queue.php: -------------------------------------------------------------------------------- 1 | 24 | */ 25 | class Queue extends CliQueue 26 | { 27 | public $host = 'localhost'; 28 | public $port = 5672; 29 | public $user = 'guest'; 30 | public $password = 'guest'; 31 | public $queueName = 'queue'; 32 | public $exchangeName = 'exchange'; 33 | public $vhost = '/'; 34 | /** 35 | * @var int The periods of time PHP pings the broker in order to prolong the connection timeout. In seconds. 36 | * @since 2.3.1 37 | */ 38 | public $heartbeat = 0; 39 | /** 40 | * Send keep-alive packets for a socket connection 41 | * @var bool 42 | * @since 2.3.6 43 | */ 44 | public $keepalive = false; 45 | /** 46 | * @var string command class name 47 | */ 48 | public $commandClass = Command::class; 49 | 50 | /** 51 | * @var AMQPStreamConnection 52 | */ 53 | protected $connection; 54 | /** 55 | * @var AMQPChannel 56 | */ 57 | protected $channel; 58 | 59 | 60 | /** 61 | * @inheritdoc 62 | */ 63 | public function init() 64 | { 65 | parent::init(); 66 | Event::on(BaseApp::class, BaseApp::EVENT_AFTER_REQUEST, function () { 67 | $this->close(); 68 | }); 69 | } 70 | 71 | /** 72 | * Listens amqp-queue and runs new jobs. 73 | */ 74 | public function listen() 75 | { 76 | $this->open(); 77 | $callback = function (AMQPMessage $payload) { 78 | $id = $payload->get('message_id'); 79 | list($ttr, $message) = explode(';', $payload->body, 2); 80 | if ($this->handleMessage($id, $message, $ttr, 1)) { 81 | $payload->delivery_info['channel']->basic_ack($payload->delivery_info['delivery_tag']); 82 | } 83 | }; 84 | $this->channel->basic_qos(null, 1, null); 85 | $this->channel->basic_consume($this->queueName, '', false, false, false, false, $callback); 86 | while (count($this->channel->callbacks)) { 87 | $this->channel->wait(); 88 | } 89 | } 90 | 91 | /** 92 | * @inheritdoc 93 | */ 94 | protected function pushMessage($message, $ttr, $delay, $priority) 95 | { 96 | if ($delay) { 97 | throw new NotSupportedException('Delayed work is not supported in the driver.'); 98 | } 99 | if ($priority !== null) { 100 | throw new NotSupportedException('Job priority is not supported in the driver.'); 101 | } 102 | 103 | $this->open(); 104 | $id = uniqid('', true); 105 | $this->channel->basic_publish( 106 | new AMQPMessage("$ttr;$message", [ 107 | 'delivery_mode' => AMQPMessage::DELIVERY_MODE_PERSISTENT, 108 | 'message_id' => $id, 109 | ]), 110 | $this->exchangeName 111 | ); 112 | 113 | return $id; 114 | } 115 | 116 | /** 117 | * @inheritdoc 118 | */ 119 | public function status($id) 120 | { 121 | throw new NotSupportedException('Status is not supported in the driver.'); 122 | } 123 | 124 | /** 125 | * Opens connection and channel. 126 | */ 127 | protected function open() 128 | { 129 | if ($this->channel) { 130 | return; 131 | } 132 | $this->connection = new AMQPStreamConnection( 133 | $this->host, 134 | $this->port, 135 | $this->user, 136 | $this->password, 137 | $this->vhost, 138 | false, 139 | 'AMQPLAIN', 140 | null, 141 | 'en_US', 142 | 3.0, 143 | 3.0, 144 | null, 145 | $this->keepalive, 146 | $this->heartbeat, 147 | 0.0, 148 | null 149 | ); 150 | $this->channel = $this->connection->channel(); 151 | $this->channel->queue_declare($this->queueName, false, true, false, false); 152 | $this->channel->exchange_declare($this->exchangeName, 'direct', false, true, false); 153 | $this->channel->queue_bind($this->queueName, $this->exchangeName); 154 | } 155 | 156 | /** 157 | * Closes connection and channel. 158 | */ 159 | protected function close() 160 | { 161 | if (!$this->channel) { 162 | return; 163 | } 164 | $this->channel->close(); 165 | $this->connection->close(); 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/gii/Generator.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | class Generator extends \yii\gii\Generator 22 | { 23 | public $jobClass; 24 | public $properties; 25 | public $retryable = false; 26 | public $ns = 'app\jobs'; 27 | public $baseClass = BaseObject::class; 28 | 29 | 30 | /** 31 | * @inheritdoc 32 | */ 33 | public function getName() 34 | { 35 | return 'Job Generator'; 36 | } 37 | 38 | /** 39 | * @inheritdoc 40 | */ 41 | public function getDescription() 42 | { 43 | return 'This generator generates a Job class for the queue.'; 44 | } 45 | 46 | /** 47 | * @inheritdoc 48 | */ 49 | public function rules() 50 | { 51 | return array_merge(parent::rules(), [ 52 | [['jobClass', 'properties', 'ns', 'baseClass'], 'trim'], 53 | [['jobClass', 'ns', 'baseClass'], 'required'], 54 | ['jobClass', 'match', 'pattern' => '/^\w+$/', 'message' => 'Only word characters are allowed.'], 55 | ['jobClass', 'validateJobClass'], 56 | ['properties', 'match', 'pattern' => '/^[a-z_][a-z0-9_,\\s]*$/i', 'message' => 'Must be valid class properties.'], 57 | ['retryable', 'boolean'], 58 | ['ns', 'validateNamespace'], 59 | ['baseClass', 'validateClass'], 60 | ]); 61 | } 62 | 63 | /** 64 | * @inheritdoc 65 | */ 66 | public function attributeLabels() 67 | { 68 | return array_merge(parent::attributeLabels(), [ 69 | 'jobClass' => 'Job Class', 70 | 'properties' => 'Job Properties', 71 | 'retryable' => 'Retryable Job', 72 | 'ns' => 'Namespace', 73 | 'baseClass' => 'Base Class', 74 | ]); 75 | } 76 | 77 | /** 78 | * @inheritdoc 79 | */ 80 | public function hints() 81 | { 82 | return array_merge(parent::hints(), [ 83 | 'jobClass' => 'This is the name of the Job class to be generated, e.g., SomeJob.', 84 | 'properties' => 'Job object property names. Separate multiple properties with commas or spaces, e.g., prop1, prop2.', 85 | 'retryable' => 'Job object will implement RetryableJobInterface interface.', 86 | 'ns' => 'This is the namespace of the Job class to be generated.', 87 | 'baseClass' => 'This is the class that the new Job class will extend from.', 88 | ]); 89 | } 90 | 91 | /** 92 | * @inheritdoc 93 | */ 94 | public function stickyAttributes() 95 | { 96 | return array_merge(parent::stickyAttributes(), ['ns', 'baseClass']); 97 | } 98 | 99 | /** 100 | * @inheritdoc 101 | */ 102 | public function requiredTemplates() 103 | { 104 | return ['job.php']; 105 | } 106 | 107 | /** 108 | * @inheritdoc 109 | */ 110 | public function generate() 111 | { 112 | $params = []; 113 | $params['jobClass'] = $this->jobClass; 114 | $params['ns'] = $this->ns; 115 | $params['baseClass'] = '\\' . ltrim($this->baseClass, '\\'); 116 | $params['interfaces'] = []; 117 | if (!$this->retryable) { 118 | if (!is_a($this->baseClass, JobInterface::class, true)) { 119 | $params['interfaces'][] = '\\' . JobInterface::class; 120 | } 121 | } else { 122 | if (!is_a($this->baseClass, RetryableJobInterface::class, true)) { 123 | $params['interfaces'][] = '\\' . RetryableJobInterface::class; 124 | } 125 | } 126 | $params['properties'] = array_unique(preg_split('/[\s,]+/', $this->properties, -1, PREG_SPLIT_NO_EMPTY)); 127 | 128 | $jobFile = new CodeFile( 129 | Yii::getAlias('@' . str_replace('\\', '/', $this->ns)) . '/' . $this->jobClass . '.php', 130 | $this->render('job.php', $params) 131 | ); 132 | 133 | return [$jobFile]; 134 | } 135 | 136 | /** 137 | * Validates the job class. 138 | * 139 | * @param string $attribute job attribute name. 140 | */ 141 | public function validateJobClass($attribute) 142 | { 143 | if ($this->isReservedKeyword($this->$attribute)) { 144 | $this->addError($attribute, 'Class name cannot be a reserved PHP keyword.'); 145 | } 146 | } 147 | 148 | /** 149 | * Validates the namespace. 150 | * 151 | * @param string $attribute Namespace attribute name. 152 | */ 153 | public function validateNamespace($attribute) 154 | { 155 | $value = $this->$attribute; 156 | $value = ltrim($value, '\\'); 157 | $path = Yii::getAlias('@' . str_replace('\\', '/', $value), false); 158 | if ($path === false) { 159 | $this->addError($attribute, 'Namespace must be associated with an existing directory.'); 160 | } 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /src/cli/VerboseBehavior.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | class VerboseBehavior extends Behavior 22 | { 23 | /** 24 | * @var Queue 25 | */ 26 | public $owner; 27 | /** 28 | * @var Controller 29 | */ 30 | public $command; 31 | 32 | /** 33 | * @var float timestamp 34 | */ 35 | private $jobStartedAt; 36 | /** 37 | * @var int timestamp 38 | */ 39 | private $workerStartedAt; 40 | 41 | 42 | /** 43 | * @inheritdoc 44 | */ 45 | public function events() 46 | { 47 | return [ 48 | Queue::EVENT_BEFORE_EXEC => 'beforeExec', 49 | Queue::EVENT_AFTER_EXEC => 'afterExec', 50 | Queue::EVENT_AFTER_ERROR => 'afterError', 51 | Queue::EVENT_WORKER_START => 'workerStart', 52 | Queue::EVENT_WORKER_STOP => 'workerStop', 53 | ]; 54 | } 55 | 56 | /** 57 | * @param ExecEvent $event 58 | */ 59 | public function beforeExec(ExecEvent $event) 60 | { 61 | $this->jobStartedAt = microtime(true); 62 | $this->command->stdout(date('Y-m-d H:i:s'), Console::FG_YELLOW); 63 | $this->command->stdout($this->jobTitle($event), Console::FG_GREY); 64 | $this->command->stdout(' - ', Console::FG_YELLOW); 65 | $this->command->stdout('Started', Console::FG_GREEN); 66 | $this->command->stdout(PHP_EOL); 67 | } 68 | 69 | /** 70 | * @param ExecEvent $event 71 | */ 72 | public function afterExec(ExecEvent $event) 73 | { 74 | $this->command->stdout(date('Y-m-d H:i:s'), Console::FG_YELLOW); 75 | $this->command->stdout($this->jobTitle($event), Console::FG_GREY); 76 | $this->command->stdout(' - ', Console::FG_YELLOW); 77 | $this->command->stdout('Done', Console::FG_GREEN); 78 | $duration = number_format(round(microtime(true) - $this->jobStartedAt, 3), 3); 79 | $memory = round(memory_get_peak_usage(false)/1024/1024, 2); 80 | $this->command->stdout(" ($duration s, $memory MiB)", Console::FG_YELLOW); 81 | $this->command->stdout(PHP_EOL); 82 | } 83 | 84 | /** 85 | * @param ExecEvent $event 86 | */ 87 | public function afterError(ExecEvent $event) 88 | { 89 | $this->command->stdout(date('Y-m-d H:i:s'), Console::FG_YELLOW); 90 | $this->command->stdout($this->jobTitle($event), Console::FG_GREY); 91 | $this->command->stdout(' - ', Console::FG_YELLOW); 92 | $this->command->stdout('Error', Console::BG_RED); 93 | if ($this->jobStartedAt) { 94 | $duration = number_format(round(microtime(true) - $this->jobStartedAt, 3), 3); 95 | $this->command->stdout(" ($duration s)", Console::FG_YELLOW); 96 | } 97 | $this->command->stdout(PHP_EOL); 98 | $this->command->stdout('> ' . get_class($event->error) . ': ', Console::FG_RED); 99 | $message = explode("\n", ltrim($event->error->getMessage()), 2)[0]; // First line 100 | $this->command->stdout($message, Console::FG_GREY); 101 | $this->command->stdout(PHP_EOL); 102 | $this->command->stdout('Stack trace:', Console::FG_GREY); 103 | $this->command->stdout(PHP_EOL); 104 | $this->command->stdout($event->error->getTraceAsString(), Console::FG_GREY); 105 | $this->command->stdout(PHP_EOL); 106 | } 107 | 108 | /** 109 | * @param ExecEvent $event 110 | * @return string 111 | * @since 2.0.2 112 | */ 113 | protected function jobTitle(ExecEvent $event) 114 | { 115 | $name = $event->job instanceof JobInterface ? get_class($event->job) : 'unknown job'; 116 | $extra = "attempt: $event->attempt"; 117 | if ($pid = $event->sender->getWorkerPid()) { 118 | $extra .= ", pid: $pid"; 119 | } 120 | return " [$event->id] $name ($extra)"; 121 | } 122 | 123 | /** 124 | * @param WorkerEvent $event 125 | * @since 2.0.2 126 | */ 127 | public function workerStart(WorkerEvent $event) 128 | { 129 | $this->workerStartedAt = time(); 130 | $this->command->stdout(date('Y-m-d H:i:s'), Console::FG_YELLOW); 131 | $pid = $event->sender->getWorkerPid(); 132 | $this->command->stdout(" [pid: $pid]", Console::FG_GREY); 133 | $this->command->stdout(" - Worker is started\n", Console::FG_GREEN); 134 | } 135 | 136 | /** 137 | * @param WorkerEvent $event 138 | * @since 2.0.2 139 | */ 140 | public function workerStop(WorkerEvent $event) 141 | { 142 | $this->command->stdout(date('Y-m-d H:i:s'), Console::FG_YELLOW); 143 | $pid = $event->sender->getWorkerPid(); 144 | $this->command->stdout(" [pid: $pid]", Console::FG_GREY); 145 | $this->command->stdout(' - Worker is stopped ', Console::FG_GREEN); 146 | $duration = $this->formatDuration(time() - $this->workerStartedAt); 147 | $this->command->stdout("($duration)\n", Console::FG_YELLOW); 148 | } 149 | 150 | /** 151 | * @param int $value 152 | * @return string 153 | * @since 2.0.2 154 | */ 155 | protected function formatDuration($value) 156 | { 157 | $seconds = $value % 60; 158 | $value = ($value - $seconds) / 60; 159 | $minutes = $value % 60; 160 | $value = ($value - $minutes) / 60; 161 | $hours = $value % 24; 162 | $days = ($value - $hours) / 24; 163 | 164 | if ($days > 0) { 165 | return sprintf('%d:%02d:%02d:%02d', $days, $hours, $minutes, $seconds); 166 | } 167 | 168 | return sprintf('%d:%02d:%02d', $hours, $minutes, $seconds); 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /src/cli/Command.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | abstract class Command extends Controller 22 | { 23 | /** 24 | * The exit code of the exec action which is returned when job was done. 25 | */ 26 | const EXEC_DONE = 0; 27 | /** 28 | * The exit code of the exec action which is returned when job wasn't done and wanted next attempt. 29 | */ 30 | const EXEC_RETRY = 3; 31 | 32 | /** 33 | * @var Queue 34 | */ 35 | public $queue; 36 | /** 37 | * @var bool verbose mode of a job execute. If enabled, execute result of each job 38 | * will be printed. 39 | */ 40 | public $verbose = false; 41 | /** 42 | * @var array additional options to the verbose behavior. 43 | * @since 2.0.2 44 | */ 45 | public $verboseConfig = [ 46 | 'class' => VerboseBehavior::class, 47 | ]; 48 | /** 49 | * @var bool isolate mode. It executes a job in a child process. 50 | */ 51 | public $isolate = true; 52 | /** 53 | * @var string path to php interpreter that uses to run child processes. 54 | * If it is undefined, PHP_BINARY will be used. 55 | * @since 2.0.3 56 | */ 57 | public $phpBinary; 58 | 59 | 60 | /** 61 | * @inheritdoc 62 | */ 63 | public function options($actionID) 64 | { 65 | $options = parent::options($actionID); 66 | if ($this->canVerbose($actionID)) { 67 | $options[] = 'verbose'; 68 | } 69 | if ($this->canIsolate($actionID)) { 70 | $options[] = 'isolate'; 71 | $options[] = 'phpBinary'; 72 | } 73 | 74 | return $options; 75 | } 76 | 77 | /** 78 | * @inheritdoc 79 | */ 80 | public function optionAliases() 81 | { 82 | return array_merge(parent::optionAliases(), [ 83 | 'v' => 'verbose', 84 | ]); 85 | } 86 | 87 | /** 88 | * @param string $actionID 89 | * @return bool 90 | * @since 2.0.2 91 | */ 92 | abstract protected function isWorkerAction($actionID); 93 | 94 | /** 95 | * @param string $actionID 96 | * @return bool 97 | */ 98 | protected function canVerbose($actionID) 99 | { 100 | return $actionID === 'exec' || $this->isWorkerAction($actionID); 101 | } 102 | 103 | /** 104 | * @param string $actionID 105 | * @return bool 106 | */ 107 | protected function canIsolate($actionID) 108 | { 109 | return $this->isWorkerAction($actionID); 110 | } 111 | 112 | /** 113 | * @inheritdoc 114 | */ 115 | public function beforeAction($action) 116 | { 117 | if ($this->canVerbose($action->id) && $this->verbose) { 118 | $this->queue->attachBehavior('verbose', ['command' => $this] + $this->verboseConfig); 119 | } 120 | 121 | if ($this->canIsolate($action->id) && $this->isolate) { 122 | if ($this->phpBinary === null) { 123 | $this->phpBinary = PHP_BINARY; 124 | } 125 | $this->queue->messageHandler = function ($id, $message, $ttr, $attempt) { 126 | return $this->handleMessage($id, $message, $ttr, $attempt); 127 | }; 128 | } 129 | 130 | return parent::beforeAction($action); 131 | } 132 | 133 | /** 134 | * Executes a job. 135 | * The command is internal, and used to isolate a job execution. Manual usage is not provided. 136 | * 137 | * @param string|null $id of a message 138 | * @param int $ttr time to reserve 139 | * @param int $attempt number 140 | * @param int $pid of a worker 141 | * @return int exit code 142 | * @internal It is used with isolate mode. 143 | */ 144 | public function actionExec($id, $ttr, $attempt, $pid) 145 | { 146 | if ($this->queue->execute($id, file_get_contents('php://stdin'), $ttr, $attempt, $pid ?: null)) { 147 | return self::EXEC_DONE; 148 | } 149 | return self::EXEC_RETRY; 150 | } 151 | 152 | /** 153 | * Handles message using child process. 154 | * 155 | * @param string|null $id of a message 156 | * @param string $message 157 | * @param int $ttr time to reserve 158 | * @param int $attempt number 159 | * @return bool 160 | * @throws 161 | * @see actionExec() 162 | */ 163 | protected function handleMessage($id, $message, $ttr, $attempt) 164 | { 165 | // Child process command: php yii queue/exec "id" "ttr" "attempt" "pid" 166 | $cmd = [ 167 | $this->phpBinary, 168 | $_SERVER['SCRIPT_FILENAME'], 169 | $this->uniqueId . '/exec', 170 | $id, 171 | $ttr, 172 | $attempt, 173 | $this->queue->getWorkerPid() ?: 0, 174 | ]; 175 | 176 | foreach ($this->getPassedOptions() as $name) { 177 | if (in_array($name, $this->options('exec'), true)) { 178 | $cmd[] = '--' . $name . '=' . $this->$name; 179 | } 180 | } 181 | if (!in_array('color', $this->getPassedOptions(), true)) { 182 | $cmd[] = '--color=' . $this->isColorEnabled(); 183 | } 184 | $env = isset($_ENV) ? $_ENV : null; 185 | $process = new Process($cmd, null, $env, $message, $ttr); 186 | try { 187 | $result = $process->run(function ($type, $buffer) { 188 | if ($type === Process::ERR) { 189 | $this->stderr($buffer); 190 | } else { 191 | $this->stdout($buffer); 192 | } 193 | }); 194 | if (!in_array($result, [self::EXEC_DONE, self::EXEC_RETRY])) { 195 | throw new ProcessFailedException($process); 196 | } 197 | return $result === self::EXEC_DONE; 198 | } catch (ProcessRuntimeException $error) { 199 | list($job) = $this->queue->unserializeMessage($message); 200 | return $this->queue->handleError(new ExecEvent([ 201 | 'id' => $id, 202 | 'job' => $job, 203 | 'ttr' => $ttr, 204 | 'attempt' => $attempt, 205 | 'error' => $error, 206 | ])); 207 | } 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /src/drivers/redis/Queue.php: -------------------------------------------------------------------------------- 1 | 23 | */ 24 | class Queue extends CliQueue implements StatisticsProviderInterface 25 | { 26 | /** 27 | * @var Connection|array|string 28 | */ 29 | public $redis = 'redis'; 30 | /** 31 | * @var string 32 | */ 33 | public $channel = 'queue'; 34 | /** 35 | * @var string command class name 36 | */ 37 | public $commandClass = Command::class; 38 | 39 | 40 | /** 41 | * @inheritdoc 42 | */ 43 | public function init() 44 | { 45 | parent::init(); 46 | $this->redis = Instance::ensure($this->redis, Connection::class); 47 | } 48 | 49 | /** 50 | * Listens queue and runs each job. 51 | * 52 | * @param bool $repeat whether to continue listening when queue is empty. 53 | * @param int $timeout number of seconds to wait for next message. 54 | * @return null|int exit code. 55 | * @internal for worker command only. 56 | * @since 2.0.2 57 | */ 58 | public function run($repeat, $timeout = 0) 59 | { 60 | return $this->runWorker(function (callable $canContinue) use ($repeat, $timeout) { 61 | while ($canContinue()) { 62 | if (($payload = $this->reserve($timeout)) !== null) { 63 | list($id, $message, $ttr, $attempt) = $payload; 64 | if ($this->handleMessage($id, $message, $ttr, $attempt)) { 65 | $this->delete($id); 66 | } 67 | } elseif (!$repeat) { 68 | break; 69 | } 70 | } 71 | }); 72 | } 73 | 74 | /** 75 | * @inheritdoc 76 | */ 77 | public function status($id) 78 | { 79 | if (!is_numeric($id) || $id <= 0) { 80 | throw new InvalidArgumentException("Unknown message ID: $id."); 81 | } 82 | 83 | if ($this->redis->hexists("$this->channel.attempts", $id)) { 84 | return self::STATUS_RESERVED; 85 | } 86 | 87 | if ($this->redis->hexists("$this->channel.messages", $id)) { 88 | return self::STATUS_WAITING; 89 | } 90 | 91 | return self::STATUS_DONE; 92 | } 93 | 94 | /** 95 | * Clears the queue. 96 | * 97 | * @since 2.0.1 98 | */ 99 | public function clear() 100 | { 101 | while (!$this->redis->set("$this->channel.moving_lock", true, 'NX')) { 102 | usleep(10000); 103 | } 104 | $this->redis->executeCommand('DEL', $this->redis->keys("$this->channel.*")); 105 | } 106 | 107 | /** 108 | * Removes a job by ID. 109 | * 110 | * @param int $id of a job 111 | * @return bool 112 | * @since 2.0.1 113 | */ 114 | public function remove($id) 115 | { 116 | while (!$this->redis->set("$this->channel.moving_lock", true, 'NX', 'EX', 1)) { 117 | usleep(10000); 118 | } 119 | if ($this->redis->hdel("$this->channel.messages", $id)) { 120 | $this->redis->zrem("$this->channel.delayed", $id); 121 | $this->redis->zrem("$this->channel.reserved", $id); 122 | $this->redis->lrem("$this->channel.waiting", 0, $id); 123 | $this->redis->hdel("$this->channel.attempts", $id); 124 | 125 | return true; 126 | } 127 | 128 | return false; 129 | } 130 | 131 | /** 132 | * @param int $timeout timeout 133 | * @return array|null payload 134 | */ 135 | protected function reserve($timeout) 136 | { 137 | // Moves delayed and reserved jobs into waiting list with lock for one second 138 | if ($this->redis->set("$this->channel.moving_lock", true, 'NX', 'EX', 1)) { 139 | $this->moveExpired("$this->channel.delayed"); 140 | $this->moveExpired("$this->channel.reserved"); 141 | } 142 | 143 | // Find a new waiting message 144 | $id = null; 145 | if (!$timeout) { 146 | $id = $this->redis->rpop("$this->channel.waiting"); 147 | } elseif ($result = $this->redis->brpop("$this->channel.waiting", $timeout)) { 148 | $id = $result[1]; 149 | } 150 | if (!$id) { 151 | return null; 152 | } 153 | 154 | $payload = $this->redis->hget("$this->channel.messages", $id); 155 | if (null === $payload) { 156 | return null; 157 | } 158 | 159 | list($ttr, $message) = explode(';', $payload, 2); 160 | $this->redis->zadd("$this->channel.reserved", time() + $ttr, $id); 161 | $attempt = $this->redis->hincrby("$this->channel.attempts", $id, 1); 162 | 163 | return [$id, $message, $ttr, $attempt]; 164 | } 165 | 166 | /** 167 | * @param string $from 168 | */ 169 | protected function moveExpired($from) 170 | { 171 | $now = time(); 172 | if ($expired = $this->redis->zrevrangebyscore($from, $now, '-inf')) { 173 | foreach ($expired as $id) { 174 | $this->redis->rpush("$this->channel.waiting", $id); 175 | } 176 | $this->redis->zremrangebyscore($from, '-inf', $now); 177 | } 178 | } 179 | 180 | /** 181 | * Deletes message by ID. 182 | * 183 | * @param int $id of a message 184 | */ 185 | protected function delete($id) 186 | { 187 | $this->redis->zrem("$this->channel.reserved", $id); 188 | $this->redis->hdel("$this->channel.attempts", $id); 189 | $this->redis->hdel("$this->channel.messages", $id); 190 | } 191 | 192 | /** 193 | * @inheritdoc 194 | */ 195 | protected function pushMessage($message, $ttr, $delay, $priority) 196 | { 197 | if ($priority !== null) { 198 | throw new NotSupportedException('Job priority is not supported in the driver.'); 199 | } 200 | 201 | $id = $this->redis->incr("$this->channel.message_id"); 202 | $this->redis->hset("$this->channel.messages", $id, "$ttr;$message"); 203 | if (!$delay) { 204 | $this->redis->lpush("$this->channel.waiting", $id); 205 | } else { 206 | $this->redis->zadd("$this->channel.delayed", time() + $delay, $id); 207 | } 208 | 209 | return $id; 210 | } 211 | 212 | private $_statistcsProvider; 213 | 214 | /** 215 | * @return StatisticsProvider 216 | */ 217 | public function getStatisticsProvider() 218 | { 219 | if (!$this->_statistcsProvider) { 220 | $this->_statistcsProvider = new StatisticsProvider($this); 221 | } 222 | return $this->_statistcsProvider; 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /src/drivers/sqs/Queue.php: -------------------------------------------------------------------------------- 1 | 20 | * @author Manoj Malviya 21 | */ 22 | class Queue extends CliQueue 23 | { 24 | /** 25 | * The SQS url. 26 | * @var string 27 | */ 28 | public $url; 29 | /** 30 | * aws access key. 31 | * @var string|null 32 | */ 33 | public $key; 34 | /** 35 | * aws secret. 36 | * @var string|null 37 | */ 38 | public $secret; 39 | /** 40 | * region where queue is hosted. 41 | * @var string 42 | */ 43 | public $region = ''; 44 | /** 45 | * API version. 46 | * @var string 47 | */ 48 | public $version = 'latest'; 49 | /** 50 | * Message Group ID for FIFO queues. 51 | * @var string 52 | * @since 2.2.1 53 | */ 54 | public $messageGroupId = 'default'; 55 | /** 56 | * @var string command class name 57 | * @inheritdoc 58 | */ 59 | public $commandClass = Command::class; 60 | /** 61 | * Json serializer by default. 62 | * @inheritdoc 63 | */ 64 | public $serializer = JsonSerializer::class; 65 | 66 | /** 67 | * @var SqsClient 68 | */ 69 | private $_client; 70 | 71 | 72 | /** 73 | * @inheritdoc 74 | */ 75 | public function init() 76 | { 77 | parent::init(); 78 | } 79 | 80 | /** 81 | * Listens queue and runs each job. 82 | * 83 | * @param bool $repeat whether to continue listening when queue is empty. 84 | * @param int $timeout number of seconds to sleep before next iteration. 85 | * @return null|int exit code. 86 | * @internal for worker command only 87 | */ 88 | public function run($repeat, $timeout = 0) 89 | { 90 | return $this->runWorker(function (callable $canContinue) use ($repeat, $timeout) { 91 | while ($canContinue()) { 92 | if (($payload = $this->reserve($timeout)) !== null) { 93 | $id = $payload['MessageId']; 94 | $message = $payload['Body']; 95 | $ttr = (int) $payload['MessageAttributes']['TTR']['StringValue']; 96 | $attempt = (int) $payload['Attributes']['ApproximateReceiveCount']; 97 | if ($this->handleMessage($id, $message, $ttr, $attempt)) { 98 | $this->delete($payload); 99 | } 100 | } elseif (!$repeat) { 101 | break; 102 | } 103 | } 104 | }); 105 | } 106 | 107 | /** 108 | * Gets a single message from SQS queue and sets the visibility to reserve message. 109 | * 110 | * @param int $timeout number of seconds for long polling. Must be between 0 and 20. 111 | * @return null|array payload. 112 | */ 113 | protected function reserve($timeout) 114 | { 115 | $response = $this->getClient()->receiveMessage([ 116 | 'QueueUrl' => $this->url, 117 | 'AttributeNames' => ['ApproximateReceiveCount'], 118 | 'MessageAttributeNames' => ['TTR'], 119 | 'MaxNumberOfMessages' => 1, 120 | 'VisibilityTimeout' => $this->ttr, 121 | 'WaitTimeSeconds' => $timeout, 122 | ]); 123 | if (!$response['Messages']) { 124 | return null; 125 | } 126 | 127 | $payload = reset($response['Messages']); 128 | 129 | $ttr = (int) $payload['MessageAttributes']['TTR']['StringValue']; 130 | if ($ttr != $this->ttr) { 131 | $this->getClient()->changeMessageVisibility([ 132 | 'QueueUrl' => $this->url, 133 | 'ReceiptHandle' => $payload['ReceiptHandle'], 134 | 'VisibilityTimeout' => $ttr, 135 | ]); 136 | } 137 | 138 | return $payload; 139 | } 140 | 141 | /** 142 | * Deletes the message after successfully handling. 143 | * 144 | * @param array $payload 145 | */ 146 | protected function delete($payload) 147 | { 148 | $this->getClient()->deleteMessage([ 149 | 'QueueUrl' => $this->url, 150 | 'ReceiptHandle' => $payload['ReceiptHandle'], 151 | ]); 152 | } 153 | 154 | /** 155 | * Clears the queue. 156 | */ 157 | public function clear() 158 | { 159 | $this->getClient()->purgeQueue([ 160 | 'QueueUrl' => $this->url, 161 | ]); 162 | } 163 | 164 | /** 165 | * @inheritdoc 166 | */ 167 | public function status($id) 168 | { 169 | throw new NotSupportedException('Status is not supported in the driver.'); 170 | } 171 | 172 | /** 173 | * Provides public access for `handleMessage` 174 | * 175 | * @param $id string 176 | * @param $message string 177 | * @param $ttr int 178 | * @param $attempt int 179 | * @return bool 180 | * @since 2.2.1 181 | */ 182 | public function handle($id, $message, $ttr, $attempt) 183 | { 184 | return $this->handleMessage($id, $message, $ttr, $attempt); 185 | } 186 | 187 | /** 188 | * @inheritdoc 189 | */ 190 | protected function pushMessage($message, $ttr, $delay, $priority) 191 | { 192 | if ($priority) { 193 | throw new NotSupportedException('Priority is not supported in this driver'); 194 | } 195 | 196 | $request = [ 197 | 'QueueUrl' => $this->url, 198 | 'MessageBody' => $message, 199 | 'DelaySeconds' => $delay, 200 | 'MessageAttributes' => [ 201 | 'TTR' => [ 202 | 'DataType' => 'Number', 203 | 'StringValue' => (string) $ttr, 204 | ], 205 | ], 206 | ]; 207 | 208 | if (substr($this->url, -5) === '.fifo') { 209 | $request['MessageGroupId'] = $this->messageGroupId; 210 | $request['MessageDeduplicationId'] = hash('sha256', $message); 211 | } 212 | 213 | $response = $this->getClient()->sendMessage($request); 214 | return $response['MessageId']; 215 | } 216 | 217 | /** 218 | * @return \Aws\Sqs\SqsClient 219 | */ 220 | protected function getClient() 221 | { 222 | if ($this->_client) { 223 | return $this->_client; 224 | } 225 | 226 | if ($this->key !== null && $this->secret !== null) { 227 | $credentials = [ 228 | 'key' => $this->key, 229 | 'secret' => $this->secret, 230 | ]; 231 | } else { 232 | // use default provider if no key and secret passed 233 | //see - https://docs.aws.amazon.com/aws-sdk-php/v3/guide/guide/credentials.html#credential-profiles 234 | $credentials = CredentialProvider::defaultProvider(); 235 | } 236 | 237 | $this->_client = new SqsClient([ 238 | 'credentials' => $credentials, 239 | 'region' => $this->region, 240 | 'version' => $this->version, 241 | ]); 242 | return $this->_client; 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /src/drivers/stomp/Queue.php: -------------------------------------------------------------------------------- 1 | 21 | * @since 2.3.0 22 | */ 23 | class Queue extends CliQueue 24 | { 25 | const ATTEMPT = 'yii-attempt'; 26 | const TTR = 'yii-ttr'; 27 | 28 | /** 29 | * The message queue broker's host. 30 | * 31 | * @var string|null 32 | */ 33 | public $host; 34 | /** 35 | * The message queue broker's port. 36 | * 37 | * @var string|null 38 | */ 39 | public $port; 40 | /** 41 | * This is user which is used to login on the broker. 42 | * 43 | * @var string|null 44 | */ 45 | public $user; 46 | /** 47 | * This is password which is used to login on the broker. 48 | * 49 | * @var string|null 50 | */ 51 | public $password; 52 | /** 53 | * Sets an fixed vhostname, which will be passed on connect as header['host']. 54 | * 55 | * @var string|null 56 | */ 57 | public $vhost; 58 | /** 59 | * @var int 60 | */ 61 | public $bufferSize; 62 | /** 63 | * @var int 64 | */ 65 | public $connectionTimeout; 66 | /** 67 | * Perform request synchronously. 68 | * @var bool 69 | */ 70 | public $sync; 71 | /** 72 | * The connection will be established as later as possible if set true. 73 | * 74 | * @var bool|null 75 | */ 76 | public $lazy; 77 | /** 78 | * Defines whether secure connection should be used or not. 79 | * 80 | * @var bool|null 81 | */ 82 | public $sslOn; 83 | /** 84 | * The queue used to consume messages from. 85 | * 86 | * @var string 87 | */ 88 | public $queueName = 'stomp_queue'; 89 | /** 90 | * The property contains a command class which used in cli. 91 | * 92 | * @var string command class name 93 | */ 94 | public $commandClass = Command::class; 95 | /** 96 | * Set the read timeout. 97 | * @var int 98 | */ 99 | public $readTimeOut = 0; 100 | 101 | /** 102 | * @var StompContext 103 | */ 104 | protected $context; 105 | 106 | 107 | /** 108 | * @inheritdoc 109 | */ 110 | public function init() 111 | { 112 | parent::init(); 113 | Event::on(BaseApp::class, BaseApp::EVENT_AFTER_REQUEST, function () { 114 | $this->close(); 115 | }); 116 | } 117 | 118 | /** 119 | * Opens connection. 120 | */ 121 | protected function open() 122 | { 123 | if ($this->context) { 124 | return; 125 | } 126 | 127 | $config = [ 128 | 'host' => $this->host, 129 | 'port' => $this->port, 130 | 'login' => $this->user, 131 | 'password' => $this->password, 132 | 'vhost' => $this->vhost, 133 | 'buffer_size' => $this->bufferSize, 134 | 'connection_timeout' => $this->connectionTimeout, 135 | 'sync' => $this->sync, 136 | 'lazy' => $this->lazy, 137 | 'ssl_on' => $this->sslOn, 138 | ]; 139 | 140 | $config = array_filter($config, function ($value) { 141 | return null !== $value; 142 | }); 143 | 144 | $factory = new StompConnectionFactory($config); 145 | 146 | $this->context = $factory->createContext(); 147 | } 148 | 149 | /** 150 | * Listens queue and runs each job. 151 | * 152 | * @param $repeat 153 | * @param int $timeout 154 | * @return int|null 155 | */ 156 | public function run($repeat, $timeout = 0) 157 | { 158 | return $this->runWorker(function (callable $canContinue) use ($repeat, $timeout) { 159 | $this->open(); 160 | $queue = $this->createQueue($this->queueName); 161 | $consumer = $this->context->createConsumer($queue); 162 | 163 | while ($canContinue()) { 164 | if ($message = ($this->readTimeOut > 0 ? $consumer->receive($this->readTimeOut) : $consumer->receiveNoWait())) { 165 | $messageId = $message->getMessageId(); 166 | if (!$messageId) { 167 | $message = $this->setMessageId($message); 168 | } 169 | 170 | if ($message->isRedelivered()) { 171 | $consumer->acknowledge($message); 172 | 173 | $this->redeliver($message); 174 | 175 | continue; 176 | } 177 | 178 | $ttr = $message->getProperty(self::TTR, $this->ttr); 179 | $attempt = $message->getProperty(self::ATTEMPT, 1); 180 | 181 | if ($this->handleMessage($message->getMessageId(), $message->getBody(), $ttr, $attempt)) { 182 | $consumer->acknowledge($message); 183 | } else { 184 | $consumer->acknowledge($message); 185 | 186 | $this->redeliver($message); 187 | } 188 | } elseif (!$repeat) { 189 | break; 190 | } elseif ($timeout) { 191 | sleep($timeout); 192 | $this->context->getStomp()->getConnection()->sendAlive(); 193 | } 194 | } 195 | }); 196 | } 197 | 198 | /** 199 | * @param StompMessage $message 200 | * @return StompMessage 201 | * @throws \Interop\Queue\Exception 202 | */ 203 | protected function setMessageId(StompMessage $message) 204 | { 205 | $message->setMessageId(uniqid('', true)); 206 | return $message; 207 | } 208 | 209 | /** 210 | * @inheritdoc 211 | * @throws \Interop\Queue\Exception 212 | * @throws NotSupportedException 213 | */ 214 | protected function pushMessage($message, $ttr, $delay, $priority) 215 | { 216 | $this->open(); 217 | 218 | $queue = $this->createQueue($this->queueName); 219 | $message = $this->context->createMessage($message); 220 | $message = $this->setMessageId($message); 221 | $message->setPersistent(true); 222 | $message->setProperty(self::ATTEMPT, 1); 223 | $message->setProperty(self::TTR, $ttr); 224 | 225 | $producer = $this->context->createProducer(); 226 | 227 | if ($delay) { 228 | throw new NotSupportedException('Delayed work is not supported in the driver.'); 229 | } 230 | 231 | if ($priority) { 232 | throw new NotSupportedException('Job priority is not supported in the driver.'); 233 | } 234 | 235 | $producer->send($queue, $message); 236 | 237 | return $message->getMessageId(); 238 | } 239 | 240 | /** 241 | * Closes connection. 242 | */ 243 | protected function close() 244 | { 245 | if (!$this->context) { 246 | return; 247 | } 248 | 249 | $this->context->close(); 250 | $this->context = null; 251 | } 252 | 253 | /** 254 | * @inheritdoc 255 | * @throws NotSupportedException 256 | */ 257 | public function status($id) 258 | { 259 | throw new NotSupportedException('Status is not supported in the driver.'); 260 | } 261 | 262 | /** 263 | * @param StompMessage $message 264 | * @throws \Interop\Queue\Exception 265 | */ 266 | protected function redeliver(StompMessage $message) 267 | { 268 | $attempt = $message->getProperty(self::ATTEMPT, 1); 269 | 270 | $newMessage = $this->context->createMessage($message->getBody(), $message->getProperties(), $message->getHeaders()); 271 | $newMessage->setProperty(self::ATTEMPT, ++$attempt); 272 | 273 | $this->context->createProducer()->send( 274 | $this->createQueue($this->queueName), 275 | $newMessage 276 | ); 277 | } 278 | 279 | /** 280 | * @param $name 281 | * @return \Enqueue\Stomp\StompDestination 282 | */ 283 | private function createQueue($name) 284 | { 285 | $queue = $this->context->createQueue($name); 286 | $queue->setDurable(true); 287 | $queue->setAutoDelete(false); 288 | $queue->setExclusive(false); 289 | 290 | return $queue; 291 | } 292 | } 293 | -------------------------------------------------------------------------------- /src/drivers/db/Queue.php: -------------------------------------------------------------------------------- 1 | 25 | */ 26 | class Queue extends CliQueue implements StatisticsProviderInterface 27 | { 28 | /** 29 | * @var Connection|array|string 30 | */ 31 | public $db = 'db'; 32 | /** 33 | * @var Mutex|array|string 34 | */ 35 | public $mutex = 'mutex'; 36 | /** 37 | * @var int timeout 38 | */ 39 | public $mutexTimeout = 3; 40 | /** 41 | * @var string table name 42 | */ 43 | public $tableName = '{{%queue}}'; 44 | /** 45 | * @var string 46 | */ 47 | public $channel = 'queue'; 48 | /** 49 | * @var bool ability to delete released messages from table 50 | */ 51 | public $deleteReleased = true; 52 | /** 53 | * @var string command class name 54 | */ 55 | public $commandClass = Command::class; 56 | 57 | 58 | /** 59 | * @inheritdoc 60 | */ 61 | public function init() 62 | { 63 | parent::init(); 64 | $this->db = Instance::ensure($this->db, Connection::class); 65 | $this->mutex = Instance::ensure($this->mutex, Mutex::class); 66 | } 67 | 68 | /** 69 | * Listens queue and runs each job. 70 | * 71 | * @param bool $repeat whether to continue listening when queue is empty. 72 | * @param int $timeout number of seconds to sleep before next iteration. 73 | * @return null|int exit code. 74 | * @internal for worker command only 75 | * @since 2.0.2 76 | */ 77 | public function run($repeat, $timeout = 0) 78 | { 79 | return $this->runWorker(function (callable $canContinue) use ($repeat, $timeout) { 80 | while ($canContinue()) { 81 | if ($payload = $this->reserve()) { 82 | if ($this->handleMessage( 83 | $payload['id'], 84 | $payload['job'], 85 | $payload['ttr'], 86 | $payload['attempt'] 87 | )) { 88 | $this->release($payload); 89 | } 90 | } elseif (!$repeat) { 91 | break; 92 | } elseif ($timeout) { 93 | sleep($timeout); 94 | } 95 | } 96 | }); 97 | } 98 | 99 | /** 100 | * @inheritdoc 101 | */ 102 | public function status($id) 103 | { 104 | $payload = (new Query()) 105 | ->from($this->tableName) 106 | ->where(['id' => $id]) 107 | ->one($this->db); 108 | 109 | if (!$payload) { 110 | if ($this->deleteReleased) { 111 | return self::STATUS_DONE; 112 | } 113 | 114 | throw new InvalidArgumentException("Unknown message ID: $id."); 115 | } 116 | 117 | if (!$payload['reserved_at']) { 118 | return self::STATUS_WAITING; 119 | } 120 | 121 | if (!$payload['done_at']) { 122 | return self::STATUS_RESERVED; 123 | } 124 | 125 | return self::STATUS_DONE; 126 | } 127 | 128 | /** 129 | * Clears the queue. 130 | * 131 | * @since 2.0.1 132 | */ 133 | public function clear() 134 | { 135 | $this->db->createCommand() 136 | ->delete($this->tableName, ['channel' => $this->channel]) 137 | ->execute(); 138 | } 139 | 140 | /** 141 | * Removes a job by ID. 142 | * 143 | * @param int $id of a job 144 | * @return bool 145 | * @since 2.0.1 146 | */ 147 | public function remove($id) 148 | { 149 | return (bool) $this->db->createCommand() 150 | ->delete($this->tableName, ['channel' => $this->channel, 'id' => $id]) 151 | ->execute(); 152 | } 153 | 154 | /** 155 | * @inheritdoc 156 | */ 157 | protected function pushMessage($message, $ttr, $delay, $priority) 158 | { 159 | $this->db->createCommand()->insert($this->tableName, [ 160 | 'channel' => $this->channel, 161 | 'job' => $message, 162 | 'pushed_at' => time(), 163 | 'ttr' => $ttr, 164 | 'delay' => $delay, 165 | 'priority' => $priority ?: 1024, 166 | ])->execute(); 167 | $tableSchema = $this->db->getTableSchema($this->tableName); 168 | return $this->db->getLastInsertID($tableSchema->sequenceName); 169 | } 170 | 171 | /** 172 | * Takes one message from waiting list and reserves it for handling. 173 | * 174 | * @return array|false payload 175 | * @throws Exception in case it hasn't waited the lock 176 | */ 177 | protected function reserve() 178 | { 179 | return $this->db->useMaster(function () { 180 | if (!$this->mutex->acquire(__CLASS__ . $this->channel, $this->mutexTimeout)) { 181 | throw new Exception('Has not waited the lock.'); 182 | } 183 | 184 | try { 185 | $this->moveExpired(); 186 | 187 | // Reserve one message 188 | $payload = (new Query()) 189 | ->from($this->tableName) 190 | ->andWhere(['channel' => $this->channel, 'reserved_at' => null]) 191 | ->andWhere('[[pushed_at]] <= :time - [[delay]]', [':time' => time()]) 192 | ->orderBy(['priority' => SORT_ASC, 'id' => SORT_ASC]) 193 | ->limit(1) 194 | ->one($this->db); 195 | if (is_array($payload)) { 196 | $payload['reserved_at'] = time(); 197 | $payload['attempt'] = (int) $payload['attempt'] + 1; 198 | $this->db->createCommand()->update($this->tableName, [ 199 | 'reserved_at' => $payload['reserved_at'], 200 | 'attempt' => $payload['attempt'], 201 | ], [ 202 | 'id' => $payload['id'], 203 | ])->execute(); 204 | 205 | // pgsql 206 | if (is_resource($payload['job'])) { 207 | $payload['job'] = stream_get_contents($payload['job']); 208 | } 209 | } 210 | } finally { 211 | $this->mutex->release(__CLASS__ . $this->channel); 212 | } 213 | 214 | return $payload; 215 | }); 216 | } 217 | 218 | 219 | 220 | /** 221 | * @param array $payload 222 | */ 223 | protected function release($payload) 224 | { 225 | if ($this->deleteReleased) { 226 | $this->db->createCommand()->delete( 227 | $this->tableName, 228 | ['id' => $payload['id']] 229 | )->execute(); 230 | } else { 231 | $this->db->createCommand()->update( 232 | $this->tableName, 233 | ['done_at' => time()], 234 | ['id' => $payload['id']] 235 | )->execute(); 236 | } 237 | } 238 | 239 | protected $reserveTime; 240 | 241 | /** 242 | * Moves expired messages into waiting list. 243 | */ 244 | protected function moveExpired() 245 | { 246 | if ($this->reserveTime !== time()) { 247 | $this->reserveTime = time(); 248 | $this->db->createCommand()->update( 249 | $this->tableName, 250 | ['reserved_at' => null], 251 | // `reserved_at IS NOT NULL` forces db to use index on column, 252 | // otherwise a full scan of the table will be performed 253 | '[[reserved_at]] is not null and [[reserved_at]] < :time - [[ttr]] and [[done_at]] is null', 254 | [':time' => $this->reserveTime] 255 | )->execute(); 256 | } 257 | } 258 | 259 | private $_statistcsProvider; 260 | 261 | /** 262 | * @return StatisticsProvider 263 | */ 264 | public function getStatisticsProvider() 265 | { 266 | if (!$this->_statistcsProvider) { 267 | $this->_statistcsProvider = new StatisticsProvider($this); 268 | } 269 | return $this->_statistcsProvider; 270 | } 271 | } 272 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Yii2 Queue Extension Change Log 2 | =============================== 3 | 4 | 2.3.8 under development 5 | ----------------------- 6 | - Enh #516: Ensure Redis driver messages are consumed at least once (soul11201) 7 | - Bug #522: Fix SQS driver type error with custom value passed to `queue/listen` (flaviovs) 8 | - Bug #528: Prevent multiple execution of aborted jobs (luke-) 9 | - Enh #493: Pass environment variables to sub-processes (mgrechanik) 10 | - Bug #538: Fix type hint for previous parameter in `InvalidJobException` class constructor in PHP `8.4` (implicitly marking parameter nullable) (terabytesoftw) 11 | 12 | 2.3.7 April 29, 2024 13 | -------------------- 14 | 15 | - Enh #509: Add StatisticsProviderInterface to get statistics from queue (kalmer) 16 | 17 | 18 | 2.3.6 October 03, 2023 19 | ---------------------- 20 | 21 | - Bug #373: Fixed error if payload in Redis is null (sanwv, magarzon) 22 | - Enh #372: Add ability to configure keepalive and heartbeat for AMQP and AMQP interop (vyachin) 23 | - Enh #464: Delete property `maxPriority` (skolkin-worker) 24 | - Enh #486: `SignalLoop::$exitSignals` now includes `SIGQUIT` (rhertogh) 25 | - Enh #487: Add ability to push message with headers for AMQP interop driver (s1lver) 26 | 27 | 28 | 2.3.5 November 18, 2022 29 | ----------------------- 30 | 31 | - Enh #457: Upgraded `jeremeamia/superclosure` library to `opis/closure`, adding the possibility to have closures as properties of the jobs (mp1509) 32 | - Enh #459: Added the ability to sets of flags for the AMQP queue and exchange (s1lver) 33 | 34 | 35 | 2.3.4 March 31, 2022 36 | -------------------- 37 | 38 | - Enh #449: Force db to use the index on the `reserved_at` column to unlock unfinished tasks in DB driver (erickskrauch) 39 | 40 | 41 | 2.3.3 December 30, 2021 42 | ----------------------- 43 | 44 | - Enh #257: Increase MySQL db job size to more than 65KB (lourdas) 45 | - Enh #394: Added stack trace on error in verbose mode (germanow) 46 | - Enh #405: Change access modifier of `moveExpired` in DB drivers (matiosfree) 47 | - Enh #427: Added configurable AMQP `routingKey` options (alisin, s1lver) 48 | - Enh #430: Added configurable AMQP Exchange type (s1lver) 49 | - Enh #435: Added the ability to set optional arguments for the AMQP queue (s1lver) 50 | - Enh #445: Display memory peak usage when verbose output is enabled (nadar) 51 | 52 | 53 | 2.3.2 May 05, 2021 54 | ------------------ 55 | 56 | - Bug #414: Fixed PHP errors when PCNTL functions were disallowed (brandonkelly) 57 | 58 | 59 | 2.3.1 December 23, 2020 60 | ----------------------- 61 | 62 | - Bug #380: Fixed amqp-interop queue/listen signal handling (tarinu) 63 | - Enh #388: `symfony/process 5.0` compatibility (leandrogehlen) 64 | 65 | 66 | 2.3.0 June 04, 2019 67 | ------------------- 68 | 69 | - Enh #260: Added STOMP driver (versh23) 70 | 71 | 72 | 2.2.1 May 21, 2019 73 | ------------------ 74 | 75 | - Bug #220: Updated to the latest amqp-lib (alexkart) 76 | - Enh #293: Add `handle` method to `\yii\queue\sqs\Queue` that provides public access for `handleMessage` which can be 77 | useful for handling jobs by webhooks (alexkart) 78 | - Enh #332: Add AWS SQS FIFO support (kringkaste, alexkart) 79 | 80 | 81 | 2.2.0 Mar 20, 2019 82 | ------------------ 83 | 84 | - Bug #220: Fixed deadlock problem of DB driver (zhuravljov) 85 | - Bug #258: Worker in isolated mode fails if PHP_BINARY contains spaces (luke-) 86 | - Bug #267: Fixed symfony/process incompatibility (rob006) 87 | - Bug #269: Handling of broken messages that are not unserialized correctly (zhuravljov) 88 | - Bug #299: Queue config param validation (zhuravljov) 89 | - Enh #248: Reduce roundtrips to beanstalk server when removing job (SamMousa) 90 | - Enh #318: Added check result call function flock (evaldemar) 91 | - Enh: Job execution result is now forwarded to the event handler (zhuravljov) 92 | - Enh: `ErrorEvent` was marked as deprecated (zhuravljov) 93 | 94 | 2.1.0 May 24, 2018 95 | ------------------ 96 | 97 | - Bug #126: Handles a fatal error of the job execution in isolate mode (zhuravljov) 98 | - Bug #207: Console params validation (zhuravljov) 99 | - Bug #210: Worker option to define php bin path to run child process (zhuravljov) 100 | - Bug #224: Invalid identifier "DELAY" (lar-dragon) 101 | - Enh #192: AWS SQS implementation (elitemaks, manoj-girnar) 102 | - Enh: Worker loop event (zhuravljov) 103 | 104 | 2.0.2 December 26, 2017 105 | ----------------------- 106 | 107 | - Bug #92: Resolve issue in debug panel (farmani-eigital) 108 | - Bug #99: Retry connecting after connection has timed out for redis driver (cebe) 109 | - Bug #180: Fixed info command of file driver (victorruan) 110 | - Enh #158: Add Amqp Interop driver (makasim) 111 | - Enh #185: Loop object instead of Signal helper (zhuravljov) 112 | - Enh #188: Configurable verbose mode (zhuravljov) 113 | - Enh: Start and stop events of a worker (zhuravljov) 114 | 115 | 2.0.1 November 13, 2017 116 | ----------------------- 117 | 118 | - Bug #98: Fixed timeout error handler (zhuravljov) 119 | - Bug #112: Queue command inside module (tsingsun) 120 | - Bug #118: Synchronized moving of delayed and reserved jobs to waiting list (zhuravljov) 121 | - Bug #155: Slave DB breaks listener (zhuravljov) 122 | - Enh #97: `Queue::status` is public method (zhuravljov) 123 | - Enh #116: Add Chinese Guide (kids-return) 124 | - Enh #122: Rename `Job` to `JobInterface` (zhuravljov) 125 | - Enh #137: All throwable errors caused by jobs are now caught (brandonkelly) 126 | - Enh #141: Clear and remove commands for File, DB, Beanstalk and Redis drivers (zhuravljov) 127 | - Enh #147: Igbinary job serializer (xutl) 128 | - Enh #148: Allow to change vhost setting for RabbitMQ (ischenko) 129 | - Enh #151: Compatibility with Yii 2.0.13 and PHP 7.2 (zhuravljov) 130 | - Enh #160: Benchmark of job wait time (zhuravljov) 131 | - Enh: Rename `cli\Verbose` behavior to `cli\VerboseBehavior` (zhuravljov) 132 | - Enh: Rename `serializers\Serializer` interface to `serializers\SerializerInterface` (zhuravljov) 133 | - Enh: Added `Signal::setExitFlag()` to stop `Queue::run()` loop manually (silverfire) 134 | 135 | 2.0.0 July 15, 2017 136 | ------------------- 137 | 138 | - Enh: The package is moved to yiisoft/yii2-queue (zhuravljov) 139 | 140 | 1.1.0 July 12, 2017 141 | ------------------- 142 | 143 | - Enh #50 Documentation about worker starting control (zhuravljov) 144 | - Enh #70: Durability for rabbitmq queues (mkubenka) 145 | - Enh: Detailed error about job type in message handling (zhuravljov) 146 | - Enh #60: Enhanced event handling (zhuravljov) 147 | - Enh: Job priority for DB driver (zhuravljov) 148 | - Enh: File mode options of file driver (zhuravljov) 149 | - Enh #47: Redis queue listen timeout (zhuravljov) 150 | - Enh #23: Retryable jobs (zhuravljov) 151 | 152 | 1.0.1 June 7, 2017 153 | ------------------ 154 | 155 | - Enh #58: Deleting failed jobs from queue (zhuravljov) 156 | - Enh #55: Job priority (zhuravljov) 157 | 158 | 1.0.0 May 4, 2017 159 | ----------------- 160 | 161 | - Enh: Improvements of log behavior (zhuravljov) 162 | - Enh: File driver stat info (zhuravljov) 163 | - Enh: Beanstalk stat info (zhuravljov) 164 | - Enh: Colorized driver info actions (zhuravljov) 165 | - Enh: Colorized verbose mode (zhuravljov) 166 | - Enh: Improvements of debug panel (zhuravljov) 167 | - Enh: Queue job message statuses (zhuravljov) 168 | - Enh: Gii job generator (zhuravljov) 169 | - Enh: Enhanced gearman driver (zhuravljov) 170 | - Enh: Queue message identifiers (zhuravljov) 171 | - Enh: File queue (zhuravljov) 172 | 173 | 0.12.2 April 29, 2017 174 | --------------------- 175 | 176 | - Enh #10: Separate option that turn off isolate mode of job execute (zhuravljov) 177 | 178 | 0.12.1 April 20, 2017 179 | --------------------- 180 | 181 | - Bug #37: Fixed opening of a child process (zhuravljov) 182 | - Enh: Ability to push a closure (zhuravljov) 183 | - Enh: Before push event (zhuravljov) 184 | 185 | 0.12.0 April 14, 2017 186 | --------------------- 187 | 188 | - Enh #18: Executes a job in a child process (zhuravljov) 189 | - Bug #25: Enabled output buffer breaks output streams (luke-) 190 | - Enh: After push event (zhuravljov) 191 | 192 | 0.11.0 April 2, 2017 193 | -------------------- 194 | 195 | - Enh #21: Delayed jobs for redis queue (zhuravljov) 196 | - Enh: Info action for db and redis queue command (zhuravljov) 197 | 198 | 0.10.1 March 29, 2017 199 | --------------------- 200 | 201 | - Bug: Fixed db driver for pgsql (zhuravljov) 202 | - Bug #16: Timeout of  queue reading lock for db driver (zhuravljov) 203 | - Enh: Minor code style enhancements (SilverFire) 204 | 205 | 0.10.0 March 22, 2017 206 | --------------------- 207 | 208 | - Enh #14: Json job serializer (zhuravljov) 209 | - Enh: Delayed running of a job (zhuravljov) 210 | 211 | 0.9.1 March 6, 2017 212 | ------------------- 213 | 214 | - Bug #13: Fixed reading of DB queue (zhuravljov) 215 | 216 | 0.9.0 March 6, 2017 217 | ------------------- 218 | 219 | - Enh: Signal handlers (zhuravljov) 220 | - Enh: Add exchange for AMQP driver (airani) 221 | - Enh: Beanstalk driver (zhuravljov) 222 | - Enh: Added English docs (samdark) 223 | -------------------------------------------------------------------------------- /src/Queue.php: -------------------------------------------------------------------------------- 1 | 23 | */ 24 | abstract class Queue extends Component 25 | { 26 | /** 27 | * @event PushEvent 28 | */ 29 | const EVENT_BEFORE_PUSH = 'beforePush'; 30 | /** 31 | * @event PushEvent 32 | */ 33 | const EVENT_AFTER_PUSH = 'afterPush'; 34 | /** 35 | * @event ExecEvent 36 | */ 37 | const EVENT_BEFORE_EXEC = 'beforeExec'; 38 | /** 39 | * @event ExecEvent 40 | */ 41 | const EVENT_AFTER_EXEC = 'afterExec'; 42 | /** 43 | * @event ExecEvent 44 | */ 45 | const EVENT_AFTER_ERROR = 'afterError'; 46 | /** 47 | * @see Queue::isWaiting() 48 | */ 49 | const STATUS_WAITING = 1; 50 | /** 51 | * @see Queue::isReserved() 52 | */ 53 | const STATUS_RESERVED = 2; 54 | /** 55 | * @see Queue::isDone() 56 | */ 57 | const STATUS_DONE = 3; 58 | 59 | /** 60 | * @var bool whether to enable strict job type control. 61 | * Note that in order to enable type control, a pushing job must be [[JobInterface]] instance. 62 | * @since 2.0.1 63 | */ 64 | public $strictJobType = true; 65 | /** 66 | * @var SerializerInterface|array 67 | */ 68 | public $serializer = PhpSerializer::class; 69 | /** 70 | * @var int default time to reserve a job 71 | */ 72 | public $ttr = 300; 73 | /** 74 | * @var int default attempt count 75 | */ 76 | public $attempts = 1; 77 | 78 | private $pushTtr; 79 | private $pushDelay; 80 | private $pushPriority; 81 | 82 | 83 | /** 84 | * @inheritdoc 85 | */ 86 | public function init() 87 | { 88 | parent::init(); 89 | 90 | $this->serializer = Instance::ensure($this->serializer, SerializerInterface::class); 91 | 92 | if (!is_numeric($this->ttr)) { 93 | throw new InvalidConfigException('Default TTR must be integer.'); 94 | } 95 | $this->ttr = (int) $this->ttr; 96 | if ($this->ttr <= 0) { 97 | throw new InvalidConfigException('Default TTR must be greater that zero.'); 98 | } 99 | 100 | if (!is_numeric($this->attempts)) { 101 | throw new InvalidConfigException('Default attempts count must be integer.'); 102 | } 103 | $this->attempts = (int) $this->attempts; 104 | if ($this->attempts <= 0) { 105 | throw new InvalidConfigException('Default attempts count must be greater that zero.'); 106 | } 107 | } 108 | 109 | /** 110 | * Sets TTR for job execute. 111 | * 112 | * @param int|mixed $value 113 | * @return $this 114 | */ 115 | public function ttr($value) 116 | { 117 | $this->pushTtr = $value; 118 | return $this; 119 | } 120 | 121 | /** 122 | * Sets delay for later execute. 123 | * 124 | * @param int|mixed $value 125 | * @return $this 126 | */ 127 | public function delay($value) 128 | { 129 | $this->pushDelay = $value; 130 | return $this; 131 | } 132 | 133 | /** 134 | * Sets job priority. 135 | * 136 | * @param mixed $value 137 | * @return $this 138 | */ 139 | public function priority($value) 140 | { 141 | $this->pushPriority = $value; 142 | return $this; 143 | } 144 | 145 | /** 146 | * Pushes job into queue. 147 | * 148 | * @param JobInterface|mixed $job 149 | * @return string|null id of a job message 150 | */ 151 | public function push($job) 152 | { 153 | $event = new PushEvent([ 154 | 'job' => $job, 155 | 'ttr' => $this->pushTtr ?: ( 156 | $job instanceof RetryableJobInterface 157 | ? $job->getTtr() 158 | : $this->ttr 159 | ), 160 | 'delay' => $this->pushDelay ?: 0, 161 | 'priority' => $this->pushPriority, 162 | ]); 163 | $this->pushTtr = null; 164 | $this->pushDelay = null; 165 | $this->pushPriority = null; 166 | 167 | $this->trigger(self::EVENT_BEFORE_PUSH, $event); 168 | if ($event->handled) { 169 | return null; 170 | } 171 | 172 | if ($this->strictJobType && !($event->job instanceof JobInterface)) { 173 | throw new InvalidArgumentException('Job must be instance of JobInterface.'); 174 | } 175 | 176 | if (!is_numeric($event->ttr)) { 177 | throw new InvalidArgumentException('Job TTR must be integer.'); 178 | } 179 | $event->ttr = (int) $event->ttr; 180 | if ($event->ttr <= 0) { 181 | throw new InvalidArgumentException('Job TTR must be greater that zero.'); 182 | } 183 | 184 | if (!is_numeric($event->delay)) { 185 | throw new InvalidArgumentException('Job delay must be integer.'); 186 | } 187 | $event->delay = (int) $event->delay; 188 | if ($event->delay < 0) { 189 | throw new InvalidArgumentException('Job delay must be positive.'); 190 | } 191 | 192 | $message = $this->serializer->serialize($event->job); 193 | $event->id = $this->pushMessage($message, $event->ttr, $event->delay, $event->priority); 194 | $this->trigger(self::EVENT_AFTER_PUSH, $event); 195 | 196 | return $event->id; 197 | } 198 | 199 | /** 200 | * @param string $message 201 | * @param int $ttr time to reserve in seconds 202 | * @param int $delay 203 | * @param mixed $priority 204 | * @return string id of a job message 205 | */ 206 | abstract protected function pushMessage($message, $ttr, $delay, $priority); 207 | 208 | /** 209 | * Uses for CLI drivers and gets process ID of a worker. 210 | * 211 | * @since 2.0.2 212 | */ 213 | public function getWorkerPid() 214 | { 215 | return null; 216 | } 217 | 218 | /** 219 | * @param string $id of a job message 220 | * @param string $message 221 | * @param int $ttr time to reserve 222 | * @param int $attempt number 223 | * @return bool 224 | */ 225 | protected function handleMessage($id, $message, $ttr, $attempt) 226 | { 227 | list($job, $error) = $this->unserializeMessage($message); 228 | 229 | // Handle aborted jobs without throwing an error. 230 | if ($attempt > 1 && 231 | (($job instanceof RetryableJobInterface && !$job->canRetry($attempt - 1, $error)) 232 | || (!($job instanceof RetryableJobInterface) && $attempt > $this->attempts))) { 233 | return true; 234 | } 235 | 236 | $event = new ExecEvent([ 237 | 'id' => $id, 238 | 'job' => $job, 239 | 'ttr' => $ttr, 240 | 'attempt' => $attempt, 241 | 'error' => $error, 242 | ]); 243 | $this->trigger(self::EVENT_BEFORE_EXEC, $event); 244 | if ($event->handled) { 245 | return true; 246 | } 247 | if ($event->error) { 248 | return $this->handleError($event); 249 | } 250 | try { 251 | $event->result = $event->job->execute($this); 252 | } catch (\Exception $error) { 253 | $event->error = $error; 254 | return $this->handleError($event); 255 | } catch (\Throwable $error) { 256 | $event->error = $error; 257 | return $this->handleError($event); 258 | } 259 | $this->trigger(self::EVENT_AFTER_EXEC, $event); 260 | return true; 261 | } 262 | 263 | /** 264 | * Unserializes. 265 | * 266 | * @param string $id of the job 267 | * @param string $serialized message 268 | * @return array pair of a job and error that 269 | */ 270 | public function unserializeMessage($serialized) 271 | { 272 | try { 273 | $job = $this->serializer->unserialize($serialized); 274 | } catch (\Exception $e) { 275 | return [null, new InvalidJobException($serialized, $e->getMessage(), 0, $e)]; 276 | } 277 | 278 | if ($job instanceof JobInterface) { 279 | return [$job, null]; 280 | } 281 | 282 | return [null, new InvalidJobException($serialized, sprintf( 283 | 'Job must be a JobInterface instance instead of %s.', 284 | VarDumper::dumpAsString($job) 285 | ))]; 286 | } 287 | 288 | /** 289 | * @param ExecEvent $event 290 | * @return bool 291 | * @internal 292 | */ 293 | public function handleError(ExecEvent $event) 294 | { 295 | $event->retry = $event->attempt < $this->attempts; 296 | if ($event->error instanceof InvalidJobException) { 297 | $event->retry = false; 298 | } elseif ($event->job instanceof RetryableJobInterface) { 299 | $event->retry = $event->job->canRetry($event->attempt, $event->error); 300 | } 301 | $this->trigger(self::EVENT_AFTER_ERROR, $event); 302 | return !$event->retry; 303 | } 304 | 305 | /** 306 | * @param string $id of a job message 307 | * @return bool 308 | */ 309 | public function isWaiting($id) 310 | { 311 | return $this->status($id) === self::STATUS_WAITING; 312 | } 313 | 314 | /** 315 | * @param string $id of a job message 316 | * @return bool 317 | */ 318 | public function isReserved($id) 319 | { 320 | return $this->status($id) === self::STATUS_RESERVED; 321 | } 322 | 323 | /** 324 | * @param string $id of a job message 325 | * @return bool 326 | */ 327 | public function isDone($id) 328 | { 329 | return $this->status($id) === self::STATUS_DONE; 330 | } 331 | 332 | /** 333 | * @param string $id of a job message 334 | * @return int status code 335 | */ 336 | abstract public function status($id); 337 | } 338 | -------------------------------------------------------------------------------- /src/drivers/file/Queue.php: -------------------------------------------------------------------------------- 1 | 24 | */ 25 | class Queue extends CliQueue implements StatisticsProviderInterface 26 | { 27 | /** 28 | * @var string 29 | */ 30 | public $path = '@runtime/queue'; 31 | /** 32 | * @var int 33 | */ 34 | public $dirMode = 0755; 35 | /** 36 | * @var int|null 37 | */ 38 | public $fileMode; 39 | /** 40 | * @var callable 41 | */ 42 | public $indexSerializer = 'serialize'; 43 | /** 44 | * @var callable 45 | */ 46 | public $indexDeserializer = 'unserialize'; 47 | /** 48 | * @var string 49 | */ 50 | public $commandClass = Command::class; 51 | 52 | 53 | /** 54 | * @inheritdoc 55 | */ 56 | public function init() 57 | { 58 | parent::init(); 59 | $this->path = Yii::getAlias($this->path); 60 | if (!is_dir($this->path)) { 61 | FileHelper::createDirectory($this->path, $this->dirMode, true); 62 | } 63 | } 64 | 65 | /** 66 | * Listens queue and runs each job. 67 | * 68 | * @param bool $repeat whether to continue listening when queue is empty. 69 | * @param int $timeout number of seconds to sleep before next iteration. 70 | * @return null|int exit code. 71 | * @internal for worker command only. 72 | * @since 2.0.2 73 | */ 74 | public function run($repeat, $timeout = 0) 75 | { 76 | return $this->runWorker(function (callable $canContinue) use ($repeat, $timeout) { 77 | while ($canContinue()) { 78 | if (($payload = $this->reserve()) !== null) { 79 | list($id, $message, $ttr, $attempt) = $payload; 80 | if ($this->handleMessage($id, $message, $ttr, $attempt)) { 81 | $this->delete($payload); 82 | } 83 | } elseif (!$repeat) { 84 | break; 85 | } elseif ($timeout) { 86 | sleep($timeout); 87 | } 88 | } 89 | }); 90 | } 91 | 92 | /** 93 | * @inheritdoc 94 | */ 95 | public function status($id) 96 | { 97 | if (!is_numeric($id) || $id <= 0) { 98 | throw new InvalidArgumentException("Unknown message ID: $id."); 99 | } 100 | 101 | if (file_exists("$this->path/job$id.data")) { 102 | return self::STATUS_WAITING; 103 | } 104 | 105 | return self::STATUS_DONE; 106 | } 107 | 108 | /** 109 | * Clears the queue. 110 | * 111 | * @since 2.0.1 112 | */ 113 | public function clear() 114 | { 115 | $this->touchIndex(function (&$data) { 116 | $data = []; 117 | foreach (glob("$this->path/job*.data") as $fileName) { 118 | unlink($fileName); 119 | } 120 | }); 121 | } 122 | 123 | /** 124 | * Removes a job by ID. 125 | * 126 | * @param int $id of a job 127 | * @return bool 128 | * @since 2.0.1 129 | */ 130 | public function remove($id) 131 | { 132 | $removed = false; 133 | $this->touchIndex(function (&$data) use ($id, &$removed) { 134 | if (!empty($data['waiting'])) { 135 | foreach ($data['waiting'] as $key => $payload) { 136 | if ($payload[0] === $id) { 137 | unset($data['waiting'][$key]); 138 | $removed = true; 139 | break; 140 | } 141 | } 142 | } 143 | if (!$removed && !empty($data['delayed'])) { 144 | foreach ($data['delayed'] as $key => $payload) { 145 | if ($payload[0] === $id) { 146 | unset($data['delayed'][$key]); 147 | $removed = true; 148 | break; 149 | } 150 | } 151 | } 152 | if (!$removed && !empty($data['reserved'])) { 153 | foreach ($data['reserved'] as $key => $payload) { 154 | if ($payload[0] === $id) { 155 | unset($data['reserved'][$key]); 156 | $removed = true; 157 | break; 158 | } 159 | } 160 | } 161 | if ($removed) { 162 | unlink("$this->path/job$id.data"); 163 | } 164 | }); 165 | 166 | return $removed; 167 | } 168 | 169 | /** 170 | * Reserves message for execute. 171 | * 172 | * @return array|null payload 173 | */ 174 | protected function reserve() 175 | { 176 | $id = null; 177 | $ttr = null; 178 | $attempt = null; 179 | $this->touchIndex(function (&$data) use (&$id, &$ttr, &$attempt) { 180 | if (!empty($data['reserved'])) { 181 | foreach ($data['reserved'] as $key => $payload) { 182 | if ($payload[1] + $payload[3] < time()) { 183 | list($id, $ttr, $attempt, $time) = $payload; 184 | $data['reserved'][$key][2] = ++$attempt; 185 | $data['reserved'][$key][3] = time(); 186 | return; 187 | } 188 | } 189 | } 190 | 191 | if (!empty($data['delayed']) && $data['delayed'][0][2] <= time()) { 192 | list($id, $ttr, $time) = array_shift($data['delayed']); 193 | } elseif (!empty($data['waiting'])) { 194 | list($id, $ttr) = array_shift($data['waiting']); 195 | } 196 | if ($id) { 197 | $attempt = 1; 198 | $data['reserved']["job$id"] = [$id, $ttr, $attempt, time()]; 199 | } 200 | }); 201 | 202 | if ($id) { 203 | return [$id, file_get_contents("$this->path/job$id.data"), $ttr, $attempt]; 204 | } 205 | 206 | return null; 207 | } 208 | 209 | /** 210 | * Deletes reserved message. 211 | * 212 | * @param array $payload 213 | */ 214 | protected function delete($payload) 215 | { 216 | $id = $payload[0]; 217 | $this->touchIndex(function (&$data) use ($id) { 218 | foreach ($data['reserved'] as $key => $payload) { 219 | if ($payload[0] === $id) { 220 | unset($data['reserved'][$key]); 221 | break; 222 | } 223 | } 224 | }); 225 | unlink("$this->path/job$id.data"); 226 | } 227 | 228 | /** 229 | * @inheritdoc 230 | */ 231 | protected function pushMessage($message, $ttr, $delay, $priority) 232 | { 233 | if ($priority !== null) { 234 | throw new NotSupportedException('Job priority is not supported in the driver.'); 235 | } 236 | 237 | $this->touchIndex(function (&$data) use ($message, $ttr, $delay, &$id) { 238 | if (!isset($data['lastId'])) { 239 | $data['lastId'] = 0; 240 | } 241 | $id = ++$data['lastId']; 242 | $fileName = "$this->path/job$id.data"; 243 | file_put_contents($fileName, $message); 244 | if ($this->fileMode !== null) { 245 | chmod($fileName, $this->fileMode); 246 | } 247 | if (!$delay) { 248 | $data['waiting'][] = [$id, $ttr, 0]; 249 | } else { 250 | $data['delayed'][] = [$id, $ttr, time() + $delay]; 251 | usort($data['delayed'], function ($a, $b) { 252 | if ($a[2] < $b[2]) { 253 | return -1; 254 | } 255 | if ($a[2] > $b[2]) { 256 | return 1; 257 | } 258 | if ($a[0] < $b[0]) { 259 | return -1; 260 | } 261 | if ($a[0] > $b[0]) { 262 | return 1; 263 | } 264 | return 0; 265 | }); 266 | } 267 | }); 268 | 269 | return $id; 270 | } 271 | 272 | /** 273 | * @param callable $callback 274 | * @throws InvalidConfigException 275 | */ 276 | private function touchIndex($callback) 277 | { 278 | $fileName = "$this->path/index.data"; 279 | $isNew = !file_exists($fileName); 280 | touch($fileName); 281 | if ($isNew && $this->fileMode !== null) { 282 | chmod($fileName, $this->fileMode); 283 | } 284 | if (($file = fopen($fileName, 'rb+')) === false) { 285 | throw new InvalidConfigException("Unable to open index file: $fileName"); 286 | } 287 | if (!flock($file, LOCK_EX)) { 288 | fclose($file); 289 | throw new InvalidConfigException("Unable to flock index file: $fileName"); 290 | } 291 | $data = []; 292 | $content = stream_get_contents($file); 293 | if ($content !== '') { 294 | $data = call_user_func($this->indexDeserializer, $content); 295 | } 296 | try { 297 | $callback($data); 298 | $newContent = call_user_func($this->indexSerializer, $data); 299 | if ($newContent !== $content) { 300 | ftruncate($file, 0); 301 | rewind($file); 302 | fwrite($file, $newContent); 303 | fflush($file); 304 | } 305 | } finally { 306 | flock($file, LOCK_UN); 307 | fclose($file); 308 | } 309 | } 310 | 311 | private $_statistcsProvider; 312 | 313 | /** 314 | * @return StatisticsProvider 315 | */ 316 | public function getStatisticsProvider() 317 | { 318 | if (!$this->_statistcsProvider) { 319 | $this->_statistcsProvider = new StatisticsProvider($this); 320 | } 321 | return $this->_statistcsProvider; 322 | } 323 | } 324 | -------------------------------------------------------------------------------- /src/drivers/amqp_interop/Queue.php: -------------------------------------------------------------------------------- 1 | 34 | * @since 2.0.2 35 | */ 36 | class Queue extends CliQueue 37 | { 38 | const ATTEMPT = 'yii-attempt'; 39 | const TTR = 'yii-ttr'; 40 | const DELAY = 'yii-delay'; 41 | const PRIORITY = 'yii-priority'; 42 | const ENQUEUE_AMQP_LIB = 'enqueue/amqp-lib'; 43 | const ENQUEUE_AMQP_EXT = 'enqueue/amqp-ext'; 44 | const ENQUEUE_AMQP_BUNNY = 'enqueue/amqp-bunny'; 45 | 46 | /** 47 | * The connection to the broker could be configured as an array of options 48 | * or as a DSN string like amqp:, amqps:, amqps://user:pass@localhost:1000/vhost. 49 | * 50 | * @var string 51 | */ 52 | public $dsn; 53 | /** 54 | * The message queue broker's host. 55 | * 56 | * @var string|null 57 | */ 58 | public $host; 59 | /** 60 | * The message queue broker's port. 61 | * 62 | * @var string|null 63 | */ 64 | public $port; 65 | /** 66 | * This is RabbitMQ user which is used to login on the broker. 67 | * 68 | * @var string|null 69 | */ 70 | public $user; 71 | /** 72 | * This is RabbitMQ password which is used to login on the broker. 73 | * 74 | * @var string|null 75 | */ 76 | public $password; 77 | /** 78 | * Virtual hosts provide logical grouping and separation of resources. 79 | * 80 | * @var string|null 81 | */ 82 | public $vhost; 83 | /** 84 | * The time PHP socket waits for an information while reading. In seconds. 85 | * 86 | * @var float|null 87 | */ 88 | public $readTimeout; 89 | /** 90 | * The time PHP socket waits for an information while witting. In seconds. 91 | * 92 | * @var float|null 93 | */ 94 | public $writeTimeout; 95 | /** 96 | * The time RabbitMQ keeps the connection on idle. In seconds. 97 | * 98 | * @var float|null 99 | */ 100 | public $connectionTimeout; 101 | /** 102 | * The periods of time PHP pings the broker in order to prolong the connection timeout. In seconds. 103 | * 104 | * @var float|null 105 | */ 106 | public $heartbeat; 107 | /** 108 | * PHP uses one shared connection if set true. 109 | * 110 | * @var bool|null 111 | */ 112 | public $persisted; 113 | /** 114 | * Send keep-alive packets for a socket connection 115 | * @var bool 116 | * @since 2.3.6 117 | */ 118 | public $keepalive; 119 | /** 120 | * The connection will be established as later as possible if set true. 121 | * 122 | * @var bool|null 123 | */ 124 | public $lazy; 125 | /** 126 | * If false prefetch_count option applied separately to each new consumer on the channel 127 | * If true prefetch_count option shared across all consumers on the channel. 128 | * 129 | * @var bool|null 130 | */ 131 | public $qosGlobal; 132 | /** 133 | * Defines number of message pre-fetched in advance on a channel basis. 134 | * 135 | * @var int|null 136 | */ 137 | public $qosPrefetchSize; 138 | /** 139 | * Defines number of message pre-fetched in advance per consumer. 140 | * 141 | * @var int|null 142 | */ 143 | public $qosPrefetchCount; 144 | /** 145 | * Defines whether secure connection should be used or not. 146 | * 147 | * @var bool|null 148 | */ 149 | public $sslOn; 150 | /** 151 | * Require verification of SSL certificate used. 152 | * 153 | * @var bool|null 154 | */ 155 | public $sslVerify; 156 | /** 157 | * Location of Certificate Authority file on local filesystem which should be used with the verify_peer context option to authenticate the identity of the remote peer. 158 | * 159 | * @var string|null 160 | */ 161 | public $sslCacert; 162 | /** 163 | * Path to local certificate file on filesystem. 164 | * 165 | * @var string|null 166 | */ 167 | public $sslCert; 168 | /** 169 | * Path to local private key file on filesystem in case of separate files for certificate (local_cert) and private key. 170 | * 171 | * @var string|null 172 | */ 173 | public $sslKey; 174 | /** 175 | * The queue used to consume messages from. 176 | * 177 | * @var string 178 | */ 179 | public $queueName = 'interop_queue'; 180 | /** 181 | * Setting optional arguments for the queue (key-value pairs) 182 | * ```php 183 | * [ 184 | * 'x-expires' => 300000, 185 | * 'x-max-priority' => 10 186 | * ] 187 | * ``` 188 | * 189 | * @var array 190 | * @since 2.3.3 191 | * @see https://www.rabbitmq.com/queues.html#optional-arguments 192 | */ 193 | public $queueOptionalArguments = []; 194 | /** 195 | * Set of flags for the queue 196 | * @var int 197 | * @since 2.3.5 198 | * @see AmqpDestination 199 | */ 200 | public $queueFlags = AmqpQueue::FLAG_DURABLE; 201 | /** 202 | * The exchange used to publish messages to. 203 | * 204 | * @var string 205 | */ 206 | public $exchangeName = 'exchange'; 207 | /** 208 | * The exchange type. Can take values: direct, fanout, topic, headers 209 | * @var string 210 | * @since 2.3.3 211 | */ 212 | public $exchangeType = AmqpTopic::TYPE_DIRECT; 213 | /** 214 | * Set of flags for the exchange 215 | * @var int 216 | * @since 2.3.5 217 | * @see AmqpDestination 218 | */ 219 | public $exchangeFlags = AmqpTopic::FLAG_DURABLE; 220 | /** 221 | * Routing key for publishing messages. Default is NULL. 222 | * 223 | * @var string|null 224 | */ 225 | public $routingKey; 226 | /** 227 | * Defines the amqp interop transport being internally used. Currently supports lib, ext and bunny values. 228 | * 229 | * @var string 230 | */ 231 | public $driver = self::ENQUEUE_AMQP_LIB; 232 | /** 233 | * The property contains a command class which used in cli. 234 | * 235 | * @var string command class name 236 | */ 237 | public $commandClass = Command::class; 238 | /** 239 | * Headers to send along with the message 240 | * ```php 241 | * [ 242 | * 'header-1' => 'header-value-1', 243 | * 'header-2' => 'header-value-2', 244 | * ] 245 | * ``` 246 | * 247 | * @var array 248 | * @since 3.0.0 249 | */ 250 | public $setMessageHeaders = []; 251 | 252 | /** 253 | * Amqp interop context. 254 | * 255 | * @var AmqpContext 256 | */ 257 | protected $context; 258 | /** 259 | * List of supported amqp interop drivers. 260 | * 261 | * @var string[] 262 | */ 263 | protected $supportedDrivers = [self::ENQUEUE_AMQP_LIB, self::ENQUEUE_AMQP_EXT, self::ENQUEUE_AMQP_BUNNY]; 264 | /** 265 | * The property tells whether the setupBroker method was called or not. 266 | * Having it we can do broker setup only once per process. 267 | * 268 | * @var bool 269 | */ 270 | protected $setupBrokerDone = false; 271 | 272 | 273 | /** 274 | * @inheritdoc 275 | */ 276 | public function init() 277 | { 278 | parent::init(); 279 | Event::on(BaseApp::class, BaseApp::EVENT_AFTER_REQUEST, function () { 280 | $this->close(); 281 | }); 282 | 283 | if (extension_loaded('pcntl') && function_exists('pcntl_signal') && PHP_MAJOR_VERSION >= 7) { 284 | // https://github.com/php-amqplib/php-amqplib#unix-signals 285 | $signals = [SIGTERM, SIGQUIT, SIGINT, SIGHUP]; 286 | 287 | foreach ($signals as $signal) { 288 | $oldHandler = null; 289 | // This got added in php 7.1 and might not exist on all supported versions 290 | if (function_exists('pcntl_signal_get_handler')) { 291 | $oldHandler = pcntl_signal_get_handler($signal); 292 | } 293 | 294 | pcntl_signal($signal, static function ($signal) use ($oldHandler) { 295 | if ($oldHandler && is_callable($oldHandler)) { 296 | $oldHandler($signal); 297 | } 298 | 299 | pcntl_signal($signal, SIG_DFL); 300 | posix_kill(posix_getpid(), $signal); 301 | }); 302 | } 303 | } 304 | } 305 | 306 | /** 307 | * Listens amqp-queue and runs new jobs. 308 | */ 309 | public function listen() 310 | { 311 | $this->open(); 312 | $this->setupBroker(); 313 | 314 | $queue = $this->context->createQueue($this->queueName); 315 | $consumer = $this->context->createConsumer($queue); 316 | 317 | $callback = function (AmqpMessage $message, AmqpConsumer $consumer) { 318 | if ($message->isRedelivered()) { 319 | $consumer->acknowledge($message); 320 | 321 | $this->redeliver($message); 322 | 323 | return true; 324 | } 325 | 326 | $ttr = $message->getProperty(self::TTR); 327 | $attempt = $message->getProperty(self::ATTEMPT, 1); 328 | 329 | if ($this->handleMessage($message->getMessageId(), $message->getBody(), $ttr, $attempt)) { 330 | $consumer->acknowledge($message); 331 | } else { 332 | $consumer->acknowledge($message); 333 | 334 | $this->redeliver($message); 335 | } 336 | 337 | return true; 338 | }; 339 | 340 | $subscriptionConsumer = $this->context->createSubscriptionConsumer(); 341 | $subscriptionConsumer->subscribe($consumer, $callback); 342 | $subscriptionConsumer->consume(); 343 | } 344 | 345 | /** 346 | * @return AmqpContext 347 | */ 348 | public function getContext() 349 | { 350 | $this->open(); 351 | 352 | return $this->context; 353 | } 354 | 355 | /** 356 | * @inheritdoc 357 | */ 358 | protected function pushMessage($payload, $ttr, $delay, $priority) 359 | { 360 | $this->open(); 361 | $this->setupBroker(); 362 | 363 | $topic = $this->context->createTopic($this->exchangeName); 364 | 365 | $message = $this->context->createMessage($payload); 366 | $message->setDeliveryMode(AmqpMessage::DELIVERY_MODE_PERSISTENT); 367 | $message->setMessageId(uniqid('', true)); 368 | $message->setTimestamp(time()); 369 | $message->setProperties(array_merge( 370 | $this->setMessageHeaders, 371 | [ 372 | self::ATTEMPT => 1, 373 | self::TTR => $ttr, 374 | ] 375 | )); 376 | 377 | $producer = $this->context->createProducer(); 378 | 379 | if ($delay) { 380 | $message->setProperty(self::DELAY, $delay); 381 | $producer->setDeliveryDelay($delay * 1000); 382 | } 383 | 384 | if ($priority) { 385 | $message->setProperty(self::PRIORITY, $priority); 386 | $producer->setPriority($priority); 387 | } 388 | 389 | if (null !== $this->routingKey) { 390 | $message->setRoutingKey($this->routingKey); 391 | } 392 | 393 | $producer->send($topic, $message); 394 | 395 | return $message->getMessageId(); 396 | } 397 | 398 | /** 399 | * @inheritdoc 400 | */ 401 | public function status($id) 402 | { 403 | throw new NotSupportedException('Status is not supported in the driver.'); 404 | } 405 | 406 | /** 407 | * Opens connection and channel. 408 | */ 409 | protected function open() 410 | { 411 | if ($this->context) { 412 | return; 413 | } 414 | 415 | switch ($this->driver) { 416 | case self::ENQUEUE_AMQP_LIB: 417 | $connectionClass = AmqpLibConnectionFactory::class; 418 | break; 419 | case self::ENQUEUE_AMQP_EXT: 420 | $connectionClass = AmqpExtConnectionFactory::class; 421 | break; 422 | case self::ENQUEUE_AMQP_BUNNY: 423 | $connectionClass = AmqpBunnyConnectionFactory::class; 424 | break; 425 | default: 426 | throw new \LogicException(sprintf('The given driver "%s" is not supported. Drivers supported are "%s"', $this->driver, implode('", "', $this->supportedDrivers))); 427 | } 428 | 429 | $config = [ 430 | 'dsn' => $this->dsn, 431 | 'host' => $this->host, 432 | 'port' => $this->port, 433 | 'user' => $this->user, 434 | 'pass' => $this->password, 435 | 'vhost' => $this->vhost, 436 | 'read_timeout' => $this->readTimeout, 437 | 'write_timeout' => $this->writeTimeout, 438 | 'connection_timeout' => $this->connectionTimeout, 439 | 'heartbeat' => $this->heartbeat, 440 | 'persisted' => $this->persisted, 441 | 'keepalive' => $this->keepalive, 442 | 'lazy' => $this->lazy, 443 | 'qos_global' => $this->qosGlobal, 444 | 'qos_prefetch_size' => $this->qosPrefetchSize, 445 | 'qos_prefetch_count' => $this->qosPrefetchCount, 446 | 'ssl_on' => $this->sslOn, 447 | 'ssl_verify' => $this->sslVerify, 448 | 'ssl_cacert' => $this->sslCacert, 449 | 'ssl_cert' => $this->sslCert, 450 | 'ssl_key' => $this->sslKey, 451 | ]; 452 | 453 | $config = array_filter($config, function ($value) { 454 | return null !== $value; 455 | }); 456 | 457 | /** @var AmqpConnectionFactory $factory */ 458 | $factory = new $connectionClass($config); 459 | 460 | $this->context = $factory->createContext(); 461 | 462 | if ($this->context instanceof DelayStrategyAware) { 463 | $this->context->setDelayStrategy(new RabbitMqDlxDelayStrategy()); 464 | } 465 | } 466 | 467 | protected function setupBroker() 468 | { 469 | if ($this->setupBrokerDone) { 470 | return; 471 | } 472 | 473 | $queue = $this->context->createQueue($this->queueName); 474 | $queue->setFlags($this->queueFlags); 475 | $queue->setArguments($this->queueOptionalArguments); 476 | $this->context->declareQueue($queue); 477 | 478 | $topic = $this->context->createTopic($this->exchangeName); 479 | $topic->setType($this->exchangeType); 480 | $topic->setFlags($this->exchangeFlags); 481 | $this->context->declareTopic($topic); 482 | 483 | $this->context->bind(new AmqpBind($queue, $topic, $this->routingKey)); 484 | 485 | $this->setupBrokerDone = true; 486 | } 487 | 488 | /** 489 | * Closes connection and channel. 490 | */ 491 | protected function close() 492 | { 493 | if (!$this->context) { 494 | return; 495 | } 496 | 497 | $this->context->close(); 498 | $this->context = null; 499 | $this->setupBrokerDone = false; 500 | } 501 | 502 | /** 503 | * {@inheritdoc} 504 | */ 505 | protected function redeliver(AmqpMessage $message) 506 | { 507 | $attempt = $message->getProperty(self::ATTEMPT, 1); 508 | 509 | $newMessage = $this->context->createMessage($message->getBody(), $message->getProperties(), $message->getHeaders()); 510 | $newMessage->setDeliveryMode($message->getDeliveryMode()); 511 | $newMessage->setProperty(self::ATTEMPT, ++$attempt); 512 | 513 | $this->context->createProducer()->send( 514 | $this->context->createQueue($this->queueName), 515 | $newMessage 516 | ); 517 | } 518 | } 519 | --------------------------------------------------------------------------------