├── 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 |
16 | 17 | -------------------------------------------------------------------------------- /src/gii/form.php: -------------------------------------------------------------------------------- 1 | 8 | = $form->field($generator, 'jobClass')->textInput(['autofocus' => true]) ?> 9 | = $form->field($generator, 'properties') ?> 10 | = $form->field($generator, 'retryable')->checkbox() ?> 11 | = $form->field($generator, 'ns') ?> 12 | = $form->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 = $ns ?>; 21 | 22 | /** 23 | * Class = $jobClass ?>. 24 | */ 25 | class = $jobClass ?> extends = $baseClass ?> = $implements ?> 26 | 27 | { 28 | 29 | public $= $property ?>; 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| Sender | 30 |= Html::encode($job['sender']) ?> | 31 |
|---|---|
| ID | 35 |= Html::encode($job['id']) ?> | 36 |
| TTR | 40 |= Html::encode($job['ttr']) ?> | 41 |
| Delay | 45 |= Html::encode($job['delay']) ?> | 46 |
| Priority | 51 |= Html::encode($job['priority']) ?> | 52 |
| Status | 56 |= Html::encode($job['status']) ?> | 57 |
| Class | 61 |= Html::encode($job['class']) ?> | 62 |
| = Html::encode($property) ?> | 66 |= Html::encode($value) ?> | 67 |
| Data | 72 |= Html::encode($job['data']) ?> | 73 |
2 |
3 |
4 |
5 |
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