first commit

This commit is contained in:
2026-01-25 18:18:09 +08:00
commit 509312e604
8136 changed files with 2349298 additions and 0 deletions

224
vendor/yiisoft/yii2-queue/CHANGELOG.md vendored Normal file
View File

@ -0,0 +1,224 @@
Yii2 Queue Extension Change Log
===============================
2.3.8 January 08, 2026
----------------------
- Bug #522: Fix SQS driver type error with custom value passed to `queue/listen` (flaviovs)
- Bug #528: Prevent multiple execution of aborted jobs (luke-)
- Bug #538: Fix type hint for previous parameter in `InvalidJobException` class constructor in PHP `8.4` (implicitly marking parameter nullable) (terabytesoftw)
- Enh #493: Pass environment variables to sub-processes (mgrechanik)
- Enh #516: Ensure Redis driver messages are consumed at least once (soul11201)
2.3.7 April 29, 2024
--------------------
- Enh #509: Add StatisticsProviderInterface to get statistics from queue (kalmer)
2.3.6 October 03, 2023
----------------------
- Bug #373: Fixed error if payload in Redis is null (sanwv, magarzon)
- Enh #372: Add ability to configure keepalive and heartbeat for AMQP and AMQP interop (vyachin)
- Enh #464: Delete property `maxPriority` (skolkin-worker)
- Enh #486: `SignalLoop::$exitSignals` now includes `SIGQUIT` (rhertogh)
- Enh #487: Add ability to push message with headers for AMQP interop driver (s1lver)
2.3.5 November 18, 2022
-----------------------
- Enh #457: Upgraded `jeremeamia/superclosure` library to `opis/closure`, adding the possibility to have closures as properties of the jobs (mp1509)
- Enh #459: Added the ability to sets of flags for the AMQP queue and exchange (s1lver)
2.3.4 March 31, 2022
--------------------
- Enh #449: Force db to use the index on the `reserved_at` column to unlock unfinished tasks in DB driver (erickskrauch)
2.3.3 December 30, 2021
-----------------------
- Enh #257: Increase MySQL db job size to more than 65KB (lourdas)
- Enh #394: Added stack trace on error in verbose mode (germanow)
- Enh #405: Change access modifier of `moveExpired` in DB drivers (matiosfree)
- Enh #427: Added configurable AMQP `routingKey` options (alisin, s1lver)
- Enh #430: Added configurable AMQP Exchange type (s1lver)
- Enh #435: Added the ability to set optional arguments for the AMQP queue (s1lver)
- Enh #445: Display memory peak usage when verbose output is enabled (nadar)
2.3.2 May 05, 2021
------------------
- Bug #414: Fixed PHP errors when PCNTL functions were disallowed (brandonkelly)
2.3.1 December 23, 2020
-----------------------
- Bug #380: Fixed amqp-interop queue/listen signal handling (tarinu)
- Enh #388: `symfony/process 5.0` compatibility (leandrogehlen)
2.3.0 June 04, 2019
-------------------
- Enh #260: Added STOMP driver (versh23)
2.2.1 May 21, 2019
------------------
- Bug #220: Updated to the latest amqp-lib (alexkart)
- Enh #293: Add `handle` method to `\yii\queue\sqs\Queue` that provides public access for `handleMessage` which can be
useful for handling jobs by webhooks (alexkart)
- Enh #332: Add AWS SQS FIFO support (kringkaste, alexkart)
2.2.0 Mar 20, 2019
------------------
- Bug #220: Fixed deadlock problem of DB driver (zhuravljov)
- Bug #258: Worker in isolated mode fails if PHP_BINARY contains spaces (luke-)
- Bug #267: Fixed symfony/process incompatibility (rob006)
- Bug #269: Handling of broken messages that are not unserialized correctly (zhuravljov)
- Bug #299: Queue config param validation (zhuravljov)
- Enh #248: Reduce roundtrips to beanstalk server when removing job (SamMousa)
- Enh #318: Added check result call function flock (evaldemar)
- Enh: Job execution result is now forwarded to the event handler (zhuravljov)
- Enh: `ErrorEvent` was marked as deprecated (zhuravljov)
2.1.0 May 24, 2018
------------------
- Bug #126: Handles a fatal error of the job execution in isolate mode (zhuravljov)
- Bug #207: Console params validation (zhuravljov)
- Bug #210: Worker option to define php bin path to run child process (zhuravljov)
- Bug #224: Invalid identifier "DELAY" (lar-dragon)
- Enh #192: AWS SQS implementation (elitemaks, manoj-girnar)
- Enh: Worker loop event (zhuravljov)
2.0.2 December 26, 2017
-----------------------
- Bug #92: Resolve issue in debug panel (farmani-eigital)
- Bug #99: Retry connecting after connection has timed out for redis driver (cebe)
- Bug #180: Fixed info command of file driver (victorruan)
- Enh #158: Add Amqp Interop driver (makasim)
- Enh #185: Loop object instead of Signal helper (zhuravljov)
- Enh #188: Configurable verbose mode (zhuravljov)
- Enh: Start and stop events of a worker (zhuravljov)
2.0.1 November 13, 2017
-----------------------
- Bug #98: Fixed timeout error handler (zhuravljov)
- Bug #112: Queue command inside module (tsingsun)
- Bug #118: Synchronized moving of delayed and reserved jobs to waiting list (zhuravljov)
- Bug #155: Slave DB breaks listener (zhuravljov)
- Enh #97: `Queue::status` is public method (zhuravljov)
- Enh #116: Add Chinese Guide (kids-return)
- Enh #122: Rename `Job` to `JobInterface` (zhuravljov)
- Enh #137: All throwable errors caused by jobs are now caught (brandonkelly)
- Enh #141: Clear and remove commands for File, DB, Beanstalk and Redis drivers (zhuravljov)
- Enh #147: Igbinary job serializer (xutl)
- Enh #148: Allow to change vhost setting for RabbitMQ (ischenko)
- Enh #151: Compatibility with Yii 2.0.13 and PHP 7.2 (zhuravljov)
- Enh #160: Benchmark of job wait time (zhuravljov)
- Enh: Rename `cli\Verbose` behavior to `cli\VerboseBehavior` (zhuravljov)
- Enh: Rename `serializers\Serializer` interface to `serializers\SerializerInterface` (zhuravljov)
- Enh: Added `Signal::setExitFlag()` to stop `Queue::run()` loop manually (silverfire)
2.0.0 July 15, 2017
-------------------
- Enh: The package is moved to yiisoft/yii2-queue (zhuravljov)
1.1.0 July 12, 2017
-------------------
- Enh #50 Documentation about worker starting control (zhuravljov)
- Enh #70: Durability for rabbitmq queues (mkubenka)
- Enh: Detailed error about job type in message handling (zhuravljov)
- Enh #60: Enhanced event handling (zhuravljov)
- Enh: Job priority for DB driver (zhuravljov)
- Enh: File mode options of file driver (zhuravljov)
- Enh #47: Redis queue listen timeout (zhuravljov)
- Enh #23: Retryable jobs (zhuravljov)
1.0.1 June 7, 2017
------------------
- Enh #58: Deleting failed jobs from queue (zhuravljov)
- Enh #55: Job priority (zhuravljov)
1.0.0 May 4, 2017
-----------------
- Enh: Improvements of log behavior (zhuravljov)
- Enh: File driver stat info (zhuravljov)
- Enh: Beanstalk stat info (zhuravljov)
- Enh: Colorized driver info actions (zhuravljov)
- Enh: Colorized verbose mode (zhuravljov)
- Enh: Improvements of debug panel (zhuravljov)
- Enh: Queue job message statuses (zhuravljov)
- Enh: Gii job generator (zhuravljov)
- Enh: Enhanced gearman driver (zhuravljov)
- Enh: Queue message identifiers (zhuravljov)
- Enh: File queue (zhuravljov)
0.12.2 April 29, 2017
---------------------
- Enh #10: Separate option that turn off isolate mode of job execute (zhuravljov)
0.12.1 April 20, 2017
---------------------
- Bug #37: Fixed opening of a child process (zhuravljov)
- Enh: Ability to push a closure (zhuravljov)
- Enh: Before push event (zhuravljov)
0.12.0 April 14, 2017
---------------------
- Enh #18: Executes a job in a child process (zhuravljov)
- Bug #25: Enabled output buffer breaks output streams (luke-)
- Enh: After push event (zhuravljov)
0.11.0 April 2, 2017
--------------------
- Enh #21: Delayed jobs for redis queue (zhuravljov)
- Enh: Info action for db and redis queue command (zhuravljov)
0.10.1 March 29, 2017
---------------------
- Bug: Fixed db driver for pgsql (zhuravljov)
- Bug #16: Timeout of  queue reading lock for db driver (zhuravljov)
- Enh: Minor code style enhancements (SilverFire)
0.10.0 March 22, 2017
---------------------
- Enh #14: Json job serializer (zhuravljov)
- Enh: Delayed running of a job (zhuravljov)
0.9.1 March 6, 2017
-------------------
- Bug #13: Fixed reading of DB queue (zhuravljov)
0.9.0 March 6, 2017
-------------------
- Enh: Signal handlers (zhuravljov)
- Enh: Add exchange for AMQP driver (airani)
- Enh: Beanstalk driver (zhuravljov)
- Enh: Added English docs (samdark)

29
vendor/yiisoft/yii2-queue/LICENSE.md vendored Normal file
View File

@ -0,0 +1,29 @@
Copyright © 2008 by Yii Software LLC (http://www.yiisoft.com)
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in
the documentation and/or other materials provided with the
distribution.
* Neither the name of Yii Software LLC nor the names of its
contributors may be used to endorse or promote products derived
from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.

97
vendor/yiisoft/yii2-queue/README.md vendored Normal file
View File

@ -0,0 +1,97 @@
<p align="center">
<a href="https://github.com/yiisoft" target="_blank">
<img src="https://avatars0.githubusercontent.com/u/993323" height="100px">
</a>
<h1 align="center">Yii2 Queue Extension</h1>
<br>
</p>
An extension for running tasks asynchronously via queues.
It supports queues based on **DB**, **Redis**, **RabbitMQ**, **AMQP**, **Beanstalk**, **ActiveMQ** and **Gearman**.
Documentation is at [docs/guide/README.md](docs/guide/README.md).
[![Latest Stable Version](https://poser.pugx.org/yiisoft/yii2-queue/v/stable.svg)](https://packagist.org/packages/yiisoft/yii2-queue)
[![Total Downloads](https://poser.pugx.org/yiisoft/yii2-queue/downloads.svg)](https://packagist.org/packages/yiisoft/yii2-queue)
[![Build Status](https://github.com/yiisoft/yii2-queue/workflows/build/badge.svg)](https://github.com/yiisoft/yii2-queue/actions)
Installation
------------
The preferred way to install this extension is through [composer](https://getcomposer.org/download/):
```
php composer.phar require --prefer-dist yiisoft/yii2-queue
```
Basic Usage
-----------
Each task which is sent to queue should be defined as a separate class.
For example, if you need to download and save a file the class may look like the following:
```php
class DownloadJob extends BaseObject implements \yii\queue\JobInterface
{
public $url;
public $file;
public function execute($queue)
{
file_put_contents($this->file, file_get_contents($this->url));
}
}
```
Here's how to send a task into the queue:
```php
Yii::$app->queue->push(new DownloadJob([
'url' => 'http://example.com/image.jpg',
'file' => '/tmp/image.jpg',
]));
```
To push a job into the queue that should run after 5 minutes:
```php
Yii::$app->queue->delay(5 * 60)->push(new DownloadJob([
'url' => 'http://example.com/image.jpg',
'file' => '/tmp/image.jpg',
]));
```
The exact way a task is executed depends on the used driver. Most drivers can be run using
console commands, which the component automatically registers in your application.
This command obtains and executes tasks in a loop until the queue is empty:
```sh
yii queue/run
```
This command launches a daemon which infinitely queries the queue:
```sh
yii queue/listen
```
See the documentation for more details about driver specific console commands and their options.
The component also has the ability to track the status of a job which was pushed into queue.
```php
// Push a job into the queue and get a message ID.
$id = Yii::$app->queue->push(new SomeJob());
// Check whether the job is waiting for execution.
Yii::$app->queue->isWaiting($id);
// Check whether a worker got the job from the queue and executes it.
Yii::$app->queue->isReserved($id);
// Check whether a worker has executed the job.
Yii::$app->queue->isDone($id);
```
For more details see [the guide](docs/guide/README.md).

105
vendor/yiisoft/yii2-queue/UPGRADE.md vendored Normal file
View File

@ -0,0 +1,105 @@
Upgrading Instructions
======================
This file contains the upgrade notes. These notes highlight changes that could break your
application when you upgrade the package from one version to another.
Upgrade to 2.3.6
----------------
* The `maxPriority` property was removed from [amqp_interop](docs/guide/driver-amqp-interop.md).
Use property `queueOptionalArguments` argument `x-max-priority`.
Upgrade to 2.1.1
----------------
* `\yii\queue\ErrorEvent` has been deprecated and will be removed in `3.0`.
Use `\yii\queue\ExecEvent` instead.
Upgrade from 2.0.1 to 2.0.2
---------------------------
* The [Amqp driver](docs/guide/driver-amqp.md) has been deprecated and will be removed in `2.1`.
It is advised to migrate to [Amqp Interop](docs/guide/driver-amqp-interop.md) instead.
* Added `\yii\queue\cli\Command::isWorkerAction()` abstract method. If you use your own console
controllers to run queue listeners, you must implement it.
* `\yii\queue\cli\Signal` helper is deprecated and will be removed in `2.1`. The extension uses
`\yii\queue\cli\SignalLoop` instead of the helper.
* If you use your own console controller to listen to a queue, you must upgrade it. See the native
console controllers for how to upgrade.
Upgrade from 2.0.0 to 2.0.1
---------------------------
* `yii\queue\cli\Verbose` behavior was renamed to `yii\queue\cli\VerboseBehavior`. The old class was
marked as deprecated and will be removed in `2.1.0`.
* `Job`, `RetryableJob` and `Serializer` interfaces were renamed to `JobInterface`,
`RetryableJobInterface` and `SerializerInterface`. The old names are declared as deprecated
and will be removed in `2.1.0`.
Upgrade from 1.1.0 to 2.0.0
---------------------------
* Code has been moved to yii namespace. Check and replace `zhuravljov\yii` to `yii` namespace for
your project.
Upgrade from 1.0.0 to 1.1.0
---------------------------
* Event `Queue::EVENT_AFTER_EXEC_ERROR` renamed to `Queue::EVENT_AFTER_ERROR`.
* Removed method `Queue::later()`. Use method chain `Yii::$app->queue->delay(60)->push()` instead.
* Changed table schema for DB driver. Apply migration.
Upgrade from 0.x to 1.0.0
-------------------------
* Some methods and constants were modified.
- Method `Job::run()` modified to `Job::execute($queue)`.
- Const `Queue::EVENT_BEFORE_WORK` renamed to `Queue::EVENT_BEFORE_EXEC`.
- Const `Queue::EVENT_AFTER_WORK` renamed to `Queue::EVENT_AFTER_EXEC`.
- Const `Queue::EVENT_AFTER_ERROR` renamed to `Queue::EVENT_AFTER_EXEC_ERROR`.
* Method `Queue::sendMessage` renamed to `Queue::pushMessage`. Check it if you use it in your own
custom drivers.
Upgrade from 0.10.1
-------------------
* Driver property was removed and this functionality was moved into queue classes. If you use public
methods of `Yii::$app->queue->driver` you need to use the methods of `Yii::$app->queue`.
You also need to check your configs. For example, now the config for the db queue is:
```php
'queue' => [
'class' => \zhuravljov\yii\queue\db\Queue::class,
'db' => 'db',
'tableName' => '{{%queue}}',
'channel' => 'default',
'mutex' => \yii\mutex\MysqlMutex::class,
],
```
Instead of the old variant:
```php
'queue' => [
'class' => \zhuravljov\yii\queue\Queue::class,
'driver' => [
'class' => \yii\queue\db\Driver::class,
'db' => 'db',
'tableName' => '{{%queue}}'
'channel' => 'default',
'mutex' => \yii\mutex\MysqlMutex::class,
],
],
```

94
vendor/yiisoft/yii2-queue/composer.json vendored Normal file
View File

@ -0,0 +1,94 @@
{
"name": "yiisoft/yii2-queue",
"description": "Yii2 Queue Extension which supports queues based on DB, Redis, RabbitMQ, Beanstalk, SQS, and Gearman",
"type": "yii2-extension",
"keywords": ["yii", "queue", "async", "gii", "db", "redis", "rabbitmq", "beanstalk", "gearman", "sqs"],
"license": "BSD-3-Clause",
"authors": [
{
"name": "Roman Zhuravlev",
"email": "zhuravljov@gmail.com"
}
],
"support": {
"issues": "https://github.com/yiisoft/yii2-queue/issues",
"source": "https://github.com/yiisoft/yii2-queue",
"docs": "https://github.com/yiisoft/yii2-queue/blob/master/docs/guide"
},
"require": {
"php": ">=5.5.0",
"yiisoft/yii2": "~2.0.14",
"symfony/process": "^3.3||^4.0||^5.0||^6.0||^7.0"
},
"require-dev": {
"yiisoft/yii2-redis": "2.0.19",
"php-amqplib/php-amqplib": "^2.8.0||^3.0.0",
"enqueue/amqp-lib": "^0.8||^0.9.10||^0.10.0",
"pda/pheanstalk": "~3.2.1",
"opis/closure": "*",
"yiisoft/yii2-debug": "~2.1.0",
"yiisoft/yii2-gii": "~2.2.0",
"phpunit/phpunit": "4.8.34",
"aws/aws-sdk-php": ">=2.4",
"enqueue/stomp": "^0.8.39||0.10.19",
"cweagans/composer-patches": "^1.7"
},
"suggest": {
"ext-pcntl": "Need for process signals.",
"yiisoft/yii2-redis": "Need for Redis queue.",
"pda/pheanstalk": "Need for Beanstalk queue.",
"php-amqplib/php-amqplib": "Need for AMQP queue.",
"enqueue/amqp-lib": "Need for AMQP interop queue.",
"ext-gearman": "Need for Gearman queue.",
"aws/aws-sdk-php": "Need for aws SQS.",
"enqueue/stomp": "Need for Stomp queue."
},
"autoload": {
"psr-4": {
"yii\\queue\\": "src",
"yii\\queue\\amqp\\": "src/drivers/amqp",
"yii\\queue\\amqp_interop\\": "src/drivers/amqp_interop",
"yii\\queue\\beanstalk\\": "src/drivers/beanstalk",
"yii\\queue\\db\\": "src/drivers/db",
"yii\\queue\\file\\": "src/drivers/file",
"yii\\queue\\gearman\\": "src/drivers/gearman",
"yii\\queue\\redis\\": "src/drivers/redis",
"yii\\queue\\sync\\": "src/drivers/sync",
"yii\\queue\\sqs\\": "src/drivers/sqs",
"yii\\queue\\stomp\\": "src/drivers/stomp"
}
},
"autoload-dev": {
"psr-4": {
"tests\\": "tests"
}
},
"config": {
"allow-plugins": {
"yiisoft/yii2-composer": true,
"cweagans/composer-patches": true,
"php-http/discovery": true
}
},
"extra": {
"branch-alias": {
"dev-master": "2.x-dev"
},
"composer-exit-on-patch-failure": true,
"patches": {
"phpunit/phpunit-mock-objects": {
"Fix PHP 7 and 8 compatibility": "https://yiisoft.github.io/phpunit-patches/phpunit_mock_objects.patch"
},
"phpunit/phpunit": {
"Fix PHP 7 compatibility": "https://yiisoft.github.io/phpunit-patches/phpunit_php7.patch",
"Fix PHP 8 compatibility": "https://yiisoft.github.io/phpunit-patches/phpunit_php8.patch"
}
}
},
"repositories": [
{
"type": "composer",
"url": "https://asset-packagist.org"
}
]
}

View File

@ -0,0 +1,42 @@
<?php
/**
* @link https://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license https://www.yiiframework.com/license/
*/
namespace yii\queue;
/**
* Exec Event.
*
* @author Roman Zhuravlev <zhuravljov@gmail.com>
*/
class ExecEvent extends JobEvent
{
/**
* @var int attempt number.
* @see Queue::EVENT_BEFORE_EXEC
* @see Queue::EVENT_AFTER_EXEC
* @see Queue::EVENT_AFTER_ERROR
*/
public $attempt;
/**
* @var mixed result of a job execution in case job is done.
* @see Queue::EVENT_AFTER_EXEC
* @since 2.1.1
*/
public $result;
/**
* @var null|\Exception|\Throwable
* @see Queue::EVENT_AFTER_ERROR
* @since 2.1.1
*/
public $error;
/**
* @var null|bool
* @see Queue::EVENT_AFTER_ERROR
* @since 2.1.1
*/
public $retry;
}

View File

@ -0,0 +1,47 @@
<?php
/**
* @link https://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license https://www.yiiframework.com/license/
*/
namespace yii\queue;
use Throwable;
/**
* Invalid Job Exception.
*
* Throws when serialized message cannot be unserialized to a job.
*
* @author Roman Zhuravlev <zhuravljov@gmail.com>
* @since 2.1.1
*/
class InvalidJobException extends \Exception
{
/**
* @var string
*/
private $serialized;
/**
* @param string $serialized
* @param string $message
* @param int $code
* @param Throwable|null $previous
*/
public function __construct($serialized, $message = '', $code = 0, $previous = null)
{
$this->serialized = $serialized;
parent::__construct($message, $code, $previous);
}
/**
* @return string of serialized message that cannot be unserialized to a job
*/
final public function getSerialized()
{
return $this->serialized;
}
}

19
vendor/yiisoft/yii2-queue/src/Job.php vendored Normal file
View File

@ -0,0 +1,19 @@
<?php
/**
* @link https://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license https://www.yiiframework.com/license/
*/
namespace yii\queue;
/**
* Job Interface.
*
* @deprecated Will be removed in 3.0. Use JobInterface instead of Job.
*
* @author Roman Zhuravlev <zhuravljov@gmail.com>
*/
interface Job extends JobInterface
{
}

View File

@ -0,0 +1,36 @@
<?php
/**
* @link https://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license https://www.yiiframework.com/license/
*/
namespace yii\queue;
use yii\base\Event;
/**
* Job Event.
*
* @author Roman Zhuravlev <zhuravljov@gmail.com>
*/
abstract class JobEvent extends Event
{
/**
* @var Queue
* @inheritdoc
*/
public $sender;
/**
* @var string|null unique id of a job
*/
public $id;
/**
* @var JobInterface|null
*/
public $job;
/**
* @var int time to reserve in seconds of the job
*/
public $ttr;
}

View File

@ -0,0 +1,22 @@
<?php
/**
* @link https://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license https://www.yiiframework.com/license/
*/
namespace yii\queue;
/**
* Job Interface.
*
* @author Roman Zhuravlev <zhuravljov@gmail.com>
*/
interface JobInterface
{
/**
* @param Queue $queue which pushed and is handling the job
* @return void|mixed result of the job execution
*/
public function execute($queue);
}

View File

@ -0,0 +1,144 @@
<?php
/**
* @link https://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license https://www.yiiframework.com/license/
*/
namespace yii\queue;
use Yii;
use yii\base\Behavior;
/**
* Log Behavior.
*
* @author Roman Zhuravlev <zhuravljov@gmail.com>
*/
class LogBehavior extends Behavior
{
/**
* @var Queue
* @inheritdoc
*/
public $owner;
/**
* @var bool
*/
public $autoFlush = true;
/**
* @inheritdoc
*/
public function events()
{
return [
Queue::EVENT_AFTER_PUSH => 'afterPush',
Queue::EVENT_BEFORE_EXEC => 'beforeExec',
Queue::EVENT_AFTER_EXEC => 'afterExec',
Queue::EVENT_AFTER_ERROR => 'afterError',
cli\Queue::EVENT_WORKER_START => 'workerStart',
cli\Queue::EVENT_WORKER_STOP => 'workerStop',
];
}
/**
* @param PushEvent $event
*/
public function afterPush(PushEvent $event)
{
$title = $this->getJobTitle($event);
Yii::info("$title is pushed.", Queue::class);
}
/**
* @param ExecEvent $event
*/
public function beforeExec(ExecEvent $event)
{
$title = $this->getExecTitle($event);
Yii::info("$title is started.", Queue::class);
Yii::beginProfile($title, Queue::class);
}
/**
* @param ExecEvent $event
*/
public function afterExec(ExecEvent $event)
{
$title = $this->getExecTitle($event);
Yii::endProfile($title, Queue::class);
Yii::info("$title is finished.", Queue::class);
if ($this->autoFlush) {
Yii::getLogger()->flush(true);
}
}
/**
* @param ExecEvent $event
*/
public function afterError(ExecEvent $event)
{
$title = $this->getExecTitle($event);
Yii::endProfile($title, Queue::class);
Yii::error("$title is finished with error: $event->error.", Queue::class);
if ($this->autoFlush) {
Yii::getLogger()->flush(true);
}
}
/**
* @param cli\WorkerEvent $event
* @since 2.0.2
*/
public function workerStart(cli\WorkerEvent $event)
{
$title = 'Worker ' . $event->sender->getWorkerPid();
Yii::info("$title is started.", Queue::class);
Yii::beginProfile($title, Queue::class);
if ($this->autoFlush) {
Yii::getLogger()->flush(true);
}
}
/**
* @param cli\WorkerEvent $event
* @since 2.0.2
*/
public function workerStop(cli\WorkerEvent $event)
{
$title = 'Worker ' . $event->sender->getWorkerPid();
Yii::endProfile($title, Queue::class);
Yii::info("$title is stopped.", Queue::class);
if ($this->autoFlush) {
Yii::getLogger()->flush(true);
}
}
/**
* @param JobEvent $event
* @return string
* @since 2.0.2
*/
protected function getJobTitle(JobEvent $event)
{
$name = $event->job instanceof JobInterface ? get_class($event->job) : 'unknown job';
return "[$event->id] $name";
}
/**
* @param ExecEvent $event
* @return string
* @since 2.0.2
*/
protected function getExecTitle(ExecEvent $event)
{
$title = $this->getJobTitle($event);
$extra = "attempt: $event->attempt";
if ($pid = $event->sender->getWorkerPid()) {
$extra .= ", PID: $pid";
}
return "$title ($extra)";
}
}

View File

@ -0,0 +1,25 @@
<?php
/**
* @link https://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license https://www.yiiframework.com/license/
*/
namespace yii\queue;
/**
* Push Event.
*
* @author Roman Zhuravlev <zhuravljov@gmail.com>
*/
class PushEvent extends JobEvent
{
/**
* @var int
*/
public $delay;
/**
* @var mixed
*/
public $priority;
}

337
vendor/yiisoft/yii2-queue/src/Queue.php vendored Normal file
View File

@ -0,0 +1,337 @@
<?php
/**
* @link https://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license https://www.yiiframework.com/license/
*/
namespace yii\queue;
use Yii;
use yii\base\Component;
use yii\base\InvalidArgumentException;
use yii\base\InvalidConfigException;
use yii\di\Instance;
use yii\helpers\VarDumper;
use yii\queue\serializers\PhpSerializer;
use yii\queue\serializers\SerializerInterface;
/**
* Base Queue.
*
* @author Roman Zhuravlev <zhuravljov@gmail.com>
*/
abstract class Queue extends Component
{
/**
* @event PushEvent
*/
const EVENT_BEFORE_PUSH = 'beforePush';
/**
* @event PushEvent
*/
const EVENT_AFTER_PUSH = 'afterPush';
/**
* @event ExecEvent
*/
const EVENT_BEFORE_EXEC = 'beforeExec';
/**
* @event ExecEvent
*/
const EVENT_AFTER_EXEC = 'afterExec';
/**
* @event ExecEvent
*/
const EVENT_AFTER_ERROR = 'afterError';
/**
* @see Queue::isWaiting()
*/
const STATUS_WAITING = 1;
/**
* @see Queue::isReserved()
*/
const STATUS_RESERVED = 2;
/**
* @see Queue::isDone()
*/
const STATUS_DONE = 3;
/**
* @var bool whether to enable strict job type control.
* Note that in order to enable type control, a pushing job must be [[JobInterface]] instance.
* @since 2.0.1
*/
public $strictJobType = true;
/**
* @var SerializerInterface|array
*/
public $serializer = PhpSerializer::class;
/**
* @var int default time to reserve a job
*/
public $ttr = 300;
/**
* @var int default attempt count
*/
public $attempts = 1;
private $pushTtr;
private $pushDelay;
private $pushPriority;
/**
* @inheritdoc
*/
public function init()
{
parent::init();
$this->serializer = Instance::ensure($this->serializer, SerializerInterface::class);
if (!is_numeric($this->ttr)) {
throw new InvalidConfigException('Default TTR must be integer.');
}
$this->ttr = (int) $this->ttr;
if ($this->ttr <= 0) {
throw new InvalidConfigException('Default TTR must be greater that zero.');
}
if (!is_numeric($this->attempts)) {
throw new InvalidConfigException('Default attempts count must be integer.');
}
$this->attempts = (int) $this->attempts;
if ($this->attempts <= 0) {
throw new InvalidConfigException('Default attempts count must be greater that zero.');
}
}
/**
* Sets TTR for job execute.
*
* @param int|mixed $value
* @return $this
*/
public function ttr($value)
{
$this->pushTtr = $value;
return $this;
}
/**
* Sets delay for later execute.
*
* @param int|mixed $value
* @return $this
*/
public function delay($value)
{
$this->pushDelay = $value;
return $this;
}
/**
* Sets job priority.
*
* @param mixed $value
* @return $this
*/
public function priority($value)
{
$this->pushPriority = $value;
return $this;
}
/**
* Pushes job into queue.
*
* @param JobInterface|mixed $job
* @return string|null id of a job message
*/
public function push($job)
{
$event = new PushEvent([
'job' => $job,
'ttr' => $this->pushTtr ?: (
$job instanceof RetryableJobInterface
? $job->getTtr()
: $this->ttr
),
'delay' => $this->pushDelay ?: 0,
'priority' => $this->pushPriority,
]);
$this->pushTtr = null;
$this->pushDelay = null;
$this->pushPriority = null;
$this->trigger(self::EVENT_BEFORE_PUSH, $event);
if ($event->handled) {
return null;
}
if ($this->strictJobType && !($event->job instanceof JobInterface)) {
throw new InvalidArgumentException('Job must be instance of JobInterface.');
}
if (!is_numeric($event->ttr)) {
throw new InvalidArgumentException('Job TTR must be integer.');
}
$event->ttr = (int) $event->ttr;
if ($event->ttr <= 0) {
throw new InvalidArgumentException('Job TTR must be greater that zero.');
}
if (!is_numeric($event->delay)) {
throw new InvalidArgumentException('Job delay must be integer.');
}
$event->delay = (int) $event->delay;
if ($event->delay < 0) {
throw new InvalidArgumentException('Job delay must be positive.');
}
$message = $this->serializer->serialize($event->job);
$event->id = $this->pushMessage($message, $event->ttr, $event->delay, $event->priority);
$this->trigger(self::EVENT_AFTER_PUSH, $event);
return $event->id;
}
/**
* @param string $message
* @param int $ttr time to reserve in seconds
* @param int $delay
* @param mixed $priority
* @return string id of a job message
*/
abstract protected function pushMessage($message, $ttr, $delay, $priority);
/**
* Uses for CLI drivers and gets process ID of a worker.
*
* @since 2.0.2
*/
public function getWorkerPid()
{
return null;
}
/**
* @param string $id of a job message
* @param string $message
* @param int $ttr time to reserve
* @param int $attempt number
* @return bool
*/
protected function handleMessage($id, $message, $ttr, $attempt)
{
list($job, $error) = $this->unserializeMessage($message);
// Handle aborted jobs without throwing an error.
if ($attempt > 1 &&
(($job instanceof RetryableJobInterface && !$job->canRetry($attempt - 1, $error))
|| (!($job instanceof RetryableJobInterface) && $attempt > $this->attempts))) {
return true;
}
$event = new ExecEvent([
'id' => $id,
'job' => $job,
'ttr' => $ttr,
'attempt' => $attempt,
'error' => $error,
]);
$this->trigger(self::EVENT_BEFORE_EXEC, $event);
if ($event->handled) {
return true;
}
if ($event->error) {
return $this->handleError($event);
}
try {
$event->result = $event->job->execute($this);
} catch (\Exception $error) {
$event->error = $error;
return $this->handleError($event);
} catch (\Throwable $error) {
$event->error = $error;
return $this->handleError($event);
}
$this->trigger(self::EVENT_AFTER_EXEC, $event);
return true;
}
/**
* Unserializes.
*
* @param string $id of the job
* @param string $serialized message
* @return array pair of a job and error that
*/
public function unserializeMessage($serialized)
{
try {
$job = $this->serializer->unserialize($serialized);
} catch (\Exception $e) {
return [null, new InvalidJobException($serialized, $e->getMessage(), 0, $e)];
}
if ($job instanceof JobInterface) {
return [$job, null];
}
return [null, new InvalidJobException($serialized, sprintf(
'Job must be a JobInterface instance instead of %s.',
VarDumper::dumpAsString($job)
))];
}
/**
* @param ExecEvent $event
* @return bool
* @internal
*/
public function handleError(ExecEvent $event)
{
$event->retry = $event->attempt < $this->attempts;
if ($event->error instanceof InvalidJobException) {
$event->retry = false;
} elseif ($event->job instanceof RetryableJobInterface) {
$event->retry = $event->job->canRetry($event->attempt, $event->error);
}
$this->trigger(self::EVENT_AFTER_ERROR, $event);
return !$event->retry;
}
/**
* @param string $id of a job message
* @return bool
*/
public function isWaiting($id)
{
return $this->status($id) === self::STATUS_WAITING;
}
/**
* @param string $id of a job message
* @return bool
*/
public function isReserved($id)
{
return $this->status($id) === self::STATUS_RESERVED;
}
/**
* @param string $id of a job message
* @return bool
*/
public function isDone($id)
{
return $this->status($id) === self::STATUS_DONE;
}
/**
* @param string $id of a job message
* @return int status code
*/
abstract public function status($id);
}

View File

@ -0,0 +1,19 @@
<?php
/**
* @link https://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license https://www.yiiframework.com/license/
*/
namespace yii\queue;
/**
* Retryable Job Interface.
*
* @deprecated Will be removed in 3.0. Use RetryableJobInterface instead of RetryableJob.
*
* @author Roman Zhuravlev <zhuravljov@gmail.com>
*/
interface RetryableJob extends RetryableJobInterface
{
}

View File

@ -0,0 +1,28 @@
<?php
/**
* @link https://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license https://www.yiiframework.com/license/
*/
namespace yii\queue;
/**
* Retryable Job Interface.
*
* @author Roman Zhuravlev <zhuravljov@gmail.com>
*/
interface RetryableJobInterface extends JobInterface
{
/**
* @return int time to reserve in seconds
*/
public function getTtr();
/**
* @param int $attempt number
* @param \Exception|\Throwable $error from last execute of the job
* @return bool
*/
public function canRetry($attempt, $error);
}

View File

@ -0,0 +1,57 @@
<?php
/**
* @link https://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license https://www.yiiframework.com/license/
*/
namespace yii\queue\cli;
use yii\base\Action as BaseAction;
use yii\base\InvalidConfigException;
use yii\console\Controller as ConsoleController;
/**
* Base Command Action.
*
* @author Roman Zhuravlev <zhuravljov@gmail.com>
*/
abstract class Action extends BaseAction
{
/**
* @var Queue
*/
public $queue;
/**
* @var Command|ConsoleController
*/
public $controller;
/**
* @inheritdoc
*/
public function init()
{
parent::init();
if (!$this->queue && ($this->controller instanceof Command)) {
$this->queue = $this->controller->queue;
}
if (!($this->controller instanceof ConsoleController)) {
throw new InvalidConfigException('The controller must be console controller.');
}
if (!($this->queue instanceof Queue)) {
throw new InvalidConfigException('The queue must be cli queue.');
}
}
/**
* @param string $string
* @return string
*/
protected function format($string)
{
return call_user_func_array([$this->controller, 'ansiFormat'], func_get_args());
}
}

View File

@ -0,0 +1,209 @@
<?php
/**
* @link https://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license https://www.yiiframework.com/license/
*/
namespace yii\queue\cli;
use Symfony\Component\Process\Exception\ProcessFailedException;
use Symfony\Component\Process\Exception\RuntimeException as ProcessRuntimeException;
use Symfony\Component\Process\Process;
use yii\console\Controller;
use yii\queue\ExecEvent;
/**
* Base Command.
*
* @author Roman Zhuravlev <zhuravljov@gmail.com>
*/
abstract class Command extends Controller
{
/**
* The exit code of the exec action which is returned when job was done.
*/
const EXEC_DONE = 0;
/**
* The exit code of the exec action which is returned when job wasn't done and wanted next attempt.
*/
const EXEC_RETRY = 3;
/**
* @var Queue
*/
public $queue;
/**
* @var bool verbose mode of a job execute. If enabled, execute result of each job
* will be printed.
*/
public $verbose = false;
/**
* @var array additional options to the verbose behavior.
* @since 2.0.2
*/
public $verboseConfig = [
'class' => VerboseBehavior::class,
];
/**
* @var bool isolate mode. It executes a job in a child process.
*/
public $isolate = true;
/**
* @var string path to php interpreter that uses to run child processes.
* If it is undefined, PHP_BINARY will be used.
* @since 2.0.3
*/
public $phpBinary;
/**
* @inheritdoc
*/
public function options($actionID)
{
$options = parent::options($actionID);
if ($this->canVerbose($actionID)) {
$options[] = 'verbose';
}
if ($this->canIsolate($actionID)) {
$options[] = 'isolate';
$options[] = 'phpBinary';
}
return $options;
}
/**
* @inheritdoc
*/
public function optionAliases()
{
return array_merge(parent::optionAliases(), [
'v' => 'verbose',
]);
}
/**
* @param string $actionID
* @return bool
* @since 2.0.2
*/
abstract protected function isWorkerAction($actionID);
/**
* @param string $actionID
* @return bool
*/
protected function canVerbose($actionID)
{
return $actionID === 'exec' || $this->isWorkerAction($actionID);
}
/**
* @param string $actionID
* @return bool
*/
protected function canIsolate($actionID)
{
return $this->isWorkerAction($actionID);
}
/**
* @inheritdoc
*/
public function beforeAction($action)
{
if ($this->canVerbose($action->id) && $this->verbose) {
$this->queue->attachBehavior('verbose', ['command' => $this] + $this->verboseConfig);
}
if ($this->canIsolate($action->id) && $this->isolate) {
if ($this->phpBinary === null) {
$this->phpBinary = PHP_BINARY;
}
$this->queue->messageHandler = function ($id, $message, $ttr, $attempt) {
return $this->handleMessage($id, $message, $ttr, $attempt);
};
}
return parent::beforeAction($action);
}
/**
* Executes a job.
* The command is internal, and used to isolate a job execution. Manual usage is not provided.
*
* @param string|null $id of a message
* @param int $ttr time to reserve
* @param int $attempt number
* @param int $pid of a worker
* @return int exit code
* @internal It is used with isolate mode.
*/
public function actionExec($id, $ttr, $attempt, $pid)
{
if ($this->queue->execute($id, file_get_contents('php://stdin'), $ttr, $attempt, $pid ?: null)) {
return self::EXEC_DONE;
}
return self::EXEC_RETRY;
}
/**
* Handles message using child process.
*
* @param string|null $id of a message
* @param string $message
* @param int $ttr time to reserve
* @param int $attempt number
* @return bool
* @throws
* @see actionExec()
*/
protected function handleMessage($id, $message, $ttr, $attempt)
{
// Child process command: php yii queue/exec "id" "ttr" "attempt" "pid"
$cmd = [
$this->phpBinary,
$_SERVER['SCRIPT_FILENAME'],
$this->uniqueId . '/exec',
$id,
$ttr,
$attempt,
$this->queue->getWorkerPid() ?: 0,
];
foreach ($this->getPassedOptions() as $name) {
if (in_array($name, $this->options('exec'), true)) {
$cmd[] = '--' . $name . '=' . $this->$name;
}
}
if (!in_array('color', $this->getPassedOptions(), true)) {
$cmd[] = '--color=' . $this->isColorEnabled();
}
$env = isset($_ENV) ? $_ENV : null;
$process = new Process($cmd, null, $env, $message, $ttr);
try {
$result = $process->run(function ($type, $buffer) {
if ($type === Process::ERR) {
$this->stderr($buffer);
} else {
$this->stdout($buffer);
}
});
if (!in_array($result, [self::EXEC_DONE, self::EXEC_RETRY])) {
throw new ProcessFailedException($process);
}
return $result === self::EXEC_DONE;
} catch (ProcessRuntimeException $error) {
list($job) = $this->queue->unserializeMessage($message);
return $this->queue->handleError(new ExecEvent([
'id' => $id,
'job' => $job,
'ttr' => $ttr,
'attempt' => $attempt,
'error' => $error,
]));
}
}
}

View File

@ -0,0 +1,63 @@
<?php
/**
* @link https://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license https://www.yiiframework.com/license/
*/
namespace yii\queue\cli;
use yii\base\NotSupportedException;
use yii\helpers\Console;
use yii\queue\interfaces\DelayedCountInterface;
use yii\queue\interfaces\DoneCountInterface;
use yii\queue\interfaces\ReservedCountInterface;
use yii\queue\interfaces\StatisticsProviderInterface;
use yii\queue\interfaces\WaitingCountInterface;
/**
* Info about queue status.
*
* @author Kalmer Kaurson <kalmerkaurson@gmail.com>
*/
class InfoAction extends Action
{
/**
* @var Queue
*/
public $queue;
/**
* Info about queue status.
*/
public function run()
{
if (!($this->queue instanceof StatisticsProviderInterface)) {
throw new NotSupportedException('Queue does not support ' . StatisticsProviderInterface::class);
}
$this->controller->stdout('Jobs' . PHP_EOL, Console::FG_GREEN);
$statisticsProvider = $this->queue->getStatisticsProvider();
if ($statisticsProvider instanceof WaitingCountInterface) {
$this->controller->stdout('- waiting: ', Console::FG_YELLOW);
$this->controller->stdout($statisticsProvider->getWaitingCount() . PHP_EOL);
}
if ($statisticsProvider instanceof DelayedCountInterface) {
$this->controller->stdout('- delayed: ', Console::FG_YELLOW);
$this->controller->stdout($statisticsProvider->getDelayedCount() . PHP_EOL);
}
if ($statisticsProvider instanceof ReservedCountInterface) {
$this->controller->stdout('- reserved: ', Console::FG_YELLOW);
$this->controller->stdout($statisticsProvider->getReservedCount() . PHP_EOL);
}
if ($statisticsProvider instanceof DoneCountInterface) {
$this->controller->stdout('- done: ', Console::FG_YELLOW);
$this->controller->stdout($statisticsProvider->getDoneCount() . PHP_EOL);
}
}
}

View File

@ -0,0 +1,22 @@
<?php
/**
* @link https://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license https://www.yiiframework.com/license/
*/
namespace yii\queue\cli;
/**
* Loop Interface.
*
* @author Roman Zhuravlev <zhuravljov@gmail.com>
* @since 2.0.2
*/
interface LoopInterface
{
/**
* @return bool whether to continue listening of the queue.
*/
public function canContinue();
}

View File

@ -0,0 +1,166 @@
<?php
/**
* @link https://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license https://www.yiiframework.com/license/
*/
namespace yii\queue\cli;
use Yii;
use yii\base\BootstrapInterface;
use yii\base\InvalidConfigException;
use yii\console\Application as ConsoleApp;
use yii\helpers\Inflector;
use yii\queue\Queue as BaseQueue;
/**
* Queue with CLI.
*
* @property-read int|null $workerPid
*
* @author Roman Zhuravlev <zhuravljov@gmail.com>
*/
abstract class Queue extends BaseQueue implements BootstrapInterface
{
/**
* @event WorkerEvent that is triggered when the worker is started.
* @since 2.0.2
*/
const EVENT_WORKER_START = 'workerStart';
/**
* @event WorkerEvent that is triggered each iteration between requests to queue.
* @since 2.0.3
*/
const EVENT_WORKER_LOOP = 'workerLoop';
/**
* @event WorkerEvent that is triggered when the worker is stopped.
* @since 2.0.2
*/
const EVENT_WORKER_STOP = 'workerStop';
/**
* @var array|string
* @since 2.0.2
*/
public $loopConfig = SignalLoop::class;
/**
* @var string command class name
*/
public $commandClass = Command::class;
/**
* @var array of additional options of command
*/
public $commandOptions = [];
/**
* @var callable|null
* @internal for worker command only
*/
public $messageHandler;
/**
* @var int|null current process ID of a worker.
* @since 2.0.2
*/
private $_workerPid;
/**
* @return string command id
* @throws
*/
protected function getCommandId()
{
foreach (Yii::$app->getComponents(false) as $id => $component) {
if ($component === $this) {
return Inflector::camel2id($id);
}
}
throw new InvalidConfigException('Queue must be an application component.');
}
/**
* @inheritdoc
*/
public function bootstrap($app)
{
if ($app instanceof ConsoleApp) {
$app->controllerMap[$this->getCommandId()] = [
'class' => $this->commandClass,
'queue' => $this,
] + $this->commandOptions;
}
}
/**
* Runs worker.
*
* @param callable $handler
* @return null|int exit code
* @since 2.0.2
*/
protected function runWorker(callable $handler)
{
$this->_workerPid = getmypid();
/** @var LoopInterface $loop */
$loop = Yii::createObject($this->loopConfig, [$this]);
$event = new WorkerEvent(['loop' => $loop]);
$this->trigger(self::EVENT_WORKER_START, $event);
if ($event->exitCode !== null) {
return $event->exitCode;
}
$exitCode = null;
try {
call_user_func($handler, function () use ($loop, $event) {
$this->trigger(self::EVENT_WORKER_LOOP, $event);
return $event->exitCode === null && $loop->canContinue();
});
} finally {
$this->trigger(self::EVENT_WORKER_STOP, $event);
$this->_workerPid = null;
}
return $event->exitCode;
}
/**
* Gets process ID of a worker.
*
* @inheritdoc
* @return int|null
* @since 2.0.2
*/
public function getWorkerPid()
{
return $this->_workerPid;
}
/**
* @inheritdoc
*/
protected function handleMessage($id, $message, $ttr, $attempt)
{
if ($this->messageHandler) {
return call_user_func($this->messageHandler, $id, $message, $ttr, $attempt);
}
return parent::handleMessage($id, $message, $ttr, $attempt);
}
/**
* @param string $id of a message
* @param string $message
* @param int $ttr time to reserve
* @param int $attempt number
* @param int|null $workerPid of worker process
* @return bool
* @internal for worker command only
*/
public function execute($id, $message, $ttr, $attempt, $workerPid)
{
$this->_workerPid = $workerPid;
return parent::handleMessage($id, $message, $ttr, $attempt);
}
}

View File

@ -0,0 +1,60 @@
<?php
/**
* @link https://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license https://www.yiiframework.com/license/
*/
namespace yii\queue\cli;
/**
* Process Signal Helper.
*
* @deprecated since 2.0.2 and will be removed in 3.0. Use SignalLoop instead.
*
* @author Roman Zhuravlev <zhuravljov@gmail.com>
*/
class Signal
{
private static $exit = false;
/**
* Checks exit signals
* Used mainly by [[yii\queue\Queue]] to check, whether job execution
* loop can be continued.
* @return bool
*/
public static function isExit()
{
if (function_exists('pcntl_signal')) {
// Installs a signal handler
static $handled = false;
if (!$handled) {
foreach ([SIGTERM, SIGINT, SIGHUP] as $signal) {
pcntl_signal($signal, function () {
static::setExitFlag();
});
}
$handled = true;
}
// Checks signal
if (!static::$exit) {
pcntl_signal_dispatch();
}
}
return static::$exit;
}
/**
* Sets exit flag to `true`
* Method can be used to simulate exit signal for methods that use
* [[isExit()]] to check whether execution loop can be continued.
*/
public static function setExitFlag()
{
static::$exit = true;
}
}

View File

@ -0,0 +1,110 @@
<?php
/**
* @link https://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license https://www.yiiframework.com/license/
*/
namespace yii\queue\cli;
use yii\base\BaseObject;
/**
* Signal Loop.
*
* @author Roman Zhuravlev <zhuravljov@gmail.com>
* @since 2.0.2
*/
class SignalLoop extends BaseObject implements LoopInterface
{
/**
* @var array of signals to exit from listening of the queue.
*/
public $exitSignals = [
15, // SIGTERM
3, // SIGQUIT
2, // SIGINT
1, // SIGHUP
];
/**
* @var array of signals to suspend listening of the queue.
* For example: SIGTSTP
*/
public $suspendSignals = [];
/**
* @var array of signals to resume listening of the queue.
* For example: SIGCONT
*/
public $resumeSignals = [];
/**
* @var Queue
*/
protected $queue;
/**
* @var bool status when exit signal was got.
*/
private static $exit = false;
/**
* @var bool status when suspend or resume signal was got.
*/
private static $pause = false;
/**
* @param Queue $queue
* @inheritdoc
*/
public function __construct($queue, array $config = [])
{
$this->queue = $queue;
parent::__construct($config);
}
/**
* Sets signal handlers.
*
* @inheritdoc
*/
public function init()
{
parent::init();
if (extension_loaded('pcntl') && function_exists('pcntl_signal')) {
foreach ($this->exitSignals as $signal) {
pcntl_signal($signal, function () {
self::$exit = true;
});
}
foreach ($this->suspendSignals as $signal) {
pcntl_signal($signal, function () {
self::$pause = true;
});
}
foreach ($this->resumeSignals as $signal) {
pcntl_signal($signal, function () {
self::$pause = false;
});
}
}
}
/**
* Checks signals state.
*
* @inheritdoc
*/
public function canContinue()
{
if (extension_loaded('pcntl') && function_exists('pcntl_signal_dispatch')) {
pcntl_signal_dispatch();
// Wait for resume signal until loop is suspended
while (self::$pause && !self::$exit) {
usleep(10000);
pcntl_signal_dispatch();
}
}
return !self::$exit;
}
}

View File

@ -0,0 +1,19 @@
<?php
/**
* @link https://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license https://www.yiiframework.com/license/
*/
namespace yii\queue\cli;
/**
* Verbose Behavior.
*
* @deprecated Will be removed in 3.0. Use VerboseBehavior instead of Verbose.
*
* @author Roman Zhuravlev <zhuravljov@gmail.com>
*/
class Verbose extends VerboseBehavior
{
}

View File

@ -0,0 +1,170 @@
<?php
/**
* @link https://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license https://www.yiiframework.com/license/
*/
namespace yii\queue\cli;
use yii\base\Behavior;
use yii\console\Controller;
use yii\helpers\Console;
use yii\queue\ExecEvent;
use yii\queue\JobInterface;
/**
* Verbose Behavior.
*
* @author Roman Zhuravlev <zhuravljov@gmail.com>
*/
class VerboseBehavior extends Behavior
{
/**
* @var Queue
*/
public $owner;
/**
* @var Controller
*/
public $command;
/**
* @var float timestamp
*/
private $jobStartedAt;
/**
* @var int timestamp
*/
private $workerStartedAt;
/**
* @inheritdoc
*/
public function events()
{
return [
Queue::EVENT_BEFORE_EXEC => 'beforeExec',
Queue::EVENT_AFTER_EXEC => 'afterExec',
Queue::EVENT_AFTER_ERROR => 'afterError',
Queue::EVENT_WORKER_START => 'workerStart',
Queue::EVENT_WORKER_STOP => 'workerStop',
];
}
/**
* @param ExecEvent $event
*/
public function beforeExec(ExecEvent $event)
{
$this->jobStartedAt = microtime(true);
$this->command->stdout(date('Y-m-d H:i:s'), Console::FG_YELLOW);
$this->command->stdout($this->jobTitle($event), Console::FG_GREY);
$this->command->stdout(' - ', Console::FG_YELLOW);
$this->command->stdout('Started', Console::FG_GREEN);
$this->command->stdout(PHP_EOL);
}
/**
* @param ExecEvent $event
*/
public function afterExec(ExecEvent $event)
{
$this->command->stdout(date('Y-m-d H:i:s'), Console::FG_YELLOW);
$this->command->stdout($this->jobTitle($event), Console::FG_GREY);
$this->command->stdout(' - ', Console::FG_YELLOW);
$this->command->stdout('Done', Console::FG_GREEN);
$duration = number_format(round(microtime(true) - $this->jobStartedAt, 3), 3);
$memory = round(memory_get_peak_usage(false)/1024/1024, 2);
$this->command->stdout(" ($duration s, $memory MiB)", Console::FG_YELLOW);
$this->command->stdout(PHP_EOL);
}
/**
* @param ExecEvent $event
*/
public function afterError(ExecEvent $event)
{
$this->command->stdout(date('Y-m-d H:i:s'), Console::FG_YELLOW);
$this->command->stdout($this->jobTitle($event), Console::FG_GREY);
$this->command->stdout(' - ', Console::FG_YELLOW);
$this->command->stdout('Error', Console::BG_RED);
if ($this->jobStartedAt) {
$duration = number_format(round(microtime(true) - $this->jobStartedAt, 3), 3);
$this->command->stdout(" ($duration s)", Console::FG_YELLOW);
}
$this->command->stdout(PHP_EOL);
$this->command->stdout('> ' . get_class($event->error) . ': ', Console::FG_RED);
$message = explode("\n", ltrim($event->error->getMessage()), 2)[0]; // First line
$this->command->stdout($message, Console::FG_GREY);
$this->command->stdout(PHP_EOL);
$this->command->stdout('Stack trace:', Console::FG_GREY);
$this->command->stdout(PHP_EOL);
$this->command->stdout($event->error->getTraceAsString(), Console::FG_GREY);
$this->command->stdout(PHP_EOL);
}
/**
* @param ExecEvent $event
* @return string
* @since 2.0.2
*/
protected function jobTitle(ExecEvent $event)
{
$name = $event->job instanceof JobInterface ? get_class($event->job) : 'unknown job';
$extra = "attempt: $event->attempt";
if ($pid = $event->sender->getWorkerPid()) {
$extra .= ", pid: $pid";
}
return " [$event->id] $name ($extra)";
}
/**
* @param WorkerEvent $event
* @since 2.0.2
*/
public function workerStart(WorkerEvent $event)
{
$this->workerStartedAt = time();
$this->command->stdout(date('Y-m-d H:i:s'), Console::FG_YELLOW);
$pid = $event->sender->getWorkerPid();
$this->command->stdout(" [pid: $pid]", Console::FG_GREY);
$this->command->stdout(" - Worker is started\n", Console::FG_GREEN);
}
/**
* @param WorkerEvent $event
* @since 2.0.2
*/
public function workerStop(WorkerEvent $event)
{
$this->command->stdout(date('Y-m-d H:i:s'), Console::FG_YELLOW);
$pid = $event->sender->getWorkerPid();
$this->command->stdout(" [pid: $pid]", Console::FG_GREY);
$this->command->stdout(' - Worker is stopped ', Console::FG_GREEN);
$duration = $this->formatDuration(time() - $this->workerStartedAt);
$this->command->stdout("($duration)\n", Console::FG_YELLOW);
}
/**
* @param int $value
* @return string
* @since 2.0.2
*/
protected function formatDuration($value)
{
$seconds = $value % 60;
$value = ($value - $seconds) / 60;
$minutes = $value % 60;
$value = ($value - $minutes) / 60;
$hours = $value % 24;
$days = ($value - $hours) / 24;
if ($days > 0) {
return sprintf('%d:%02d:%02d:%02d', $days, $hours, $minutes, $seconds);
}
return sprintf('%d:%02d:%02d', $hours, $minutes, $seconds);
}
}

View File

@ -0,0 +1,33 @@
<?php
/**
* @link https://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license https://www.yiiframework.com/license/
*/
namespace yii\queue\cli;
use yii\base\Event;
/**
* Worker Event.
*
* @author Roman Zhuravlev <zhuravljov@gmail.com>
* @since 2.0.2
*/
class WorkerEvent extends Event
{
/**
* @var Queue
* @inheritdoc
*/
public $sender;
/**
* @var LoopInterface
*/
public $loop;
/**
* @var null|int exit code
*/
public $exitCode;
}

View File

@ -0,0 +1,57 @@
<?php
/**
* @link https://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license https://www.yiiframework.com/license/
*/
namespace yii\queue\closure;
use function Opis\Closure\serialize as opis_serialize;
use yii\queue\PushEvent;
use yii\queue\Queue;
/**
* Closure Behavior.
*
* If you use the behavior, you can push closures into queue. For example:
*
* ```php
* $url = 'http://example.com/name.jpg';
* $file = '/tmp/name.jpg';
* Yii::$app->push(function () use ($url, $file) {
* file_put_contents($file, file_get_contents($url));
* });
* ```
*
* @author Roman Zhuravlev <zhuravljov@gmail.com>
*/
class Behavior extends \yii\base\Behavior
{
/**
* @var Queue
*/
public $owner;
/**
* @inheritdoc
*/
public function events()
{
return [
Queue::EVENT_BEFORE_PUSH => 'beforePush',
];
}
/**
* Converts the closure to a job object.
* @param PushEvent $event
*/
public function beforePush(PushEvent $event)
{
$serialized = opis_serialize($event->job);
$event->job = new Job();
$event->job->serialized = $serialized;
}
}

View File

@ -0,0 +1,38 @@
<?php
/**
* @link https://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license https://www.yiiframework.com/license/
*/
namespace yii\queue\closure;
use function Opis\Closure\unserialize as opis_unserialize;
use yii\queue\JobInterface;
/**
* Closure Job.
*
* @author Roman Zhuravlev <zhuravljov@gmail.com>
*/
class Job implements JobInterface
{
/**
* @var string serialized closure
*/
public $serialized;
/**
* Unserializes and executes a closure.
* @inheritdoc
*/
public function execute($queue)
{
$unserialized = opis_unserialize($this->serialized);
if ($unserialized instanceof \Closure) {
return $unserialized();
}
return $unserialized->execute($queue);
}
}

View File

@ -0,0 +1,132 @@
<?php
/**
* @link https://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license https://www.yiiframework.com/license/
*/
namespace yii\queue\debug;
use Yii;
use yii\base\NotSupportedException;
use yii\base\ViewContextInterface;
use yii\helpers\VarDumper;
use yii\queue\JobInterface;
use yii\queue\PushEvent;
use yii\queue\Queue;
/**
* Debug Panel.
*
* @author Roman Zhuravlev <zhuravljov@gmail.com>
*/
class Panel extends \yii\debug\Panel implements ViewContextInterface
{
private $_jobs = [];
/**
* @inheritdoc
*/
public function getName()
{
return 'Queue';
}
/**
* @inheritdoc
*/
public function init()
{
parent::init();
PushEvent::on(Queue::class, Queue::EVENT_AFTER_PUSH, function (PushEvent $event) {
$this->_jobs[] = $this->getPushData($event);
});
}
/**
* @param PushEvent $event
* @return array
*/
protected function getPushData(PushEvent $event)
{
$data = [];
foreach (Yii::$app->getComponents(false) as $id => $component) {
if ($component === $event->sender) {
$data['sender'] = $id;
break;
}
}
$data['id'] = $event->id;
$data['ttr'] = $event->ttr;
$data['delay'] = $event->delay;
$data['priority'] = $event->priority;
if ($event->job instanceof JobInterface) {
$data['class'] = get_class($event->job);
$data['properties'] = [];
foreach (get_object_vars($event->job) as $property => $value) {
$data['properties'][$property] = VarDumper::dumpAsString($value);
}
} else {
$data['data'] = VarDumper::dumpAsString($event->job);
}
return $data;
}
/**
* @inheritdoc
*/
public function save()
{
return ['jobs' => $this->_jobs];
}
/**
* @inheritdoc
*/
public function getViewPath()
{
return __DIR__ . '/views';
}
/**
* @inheritdoc
*/
public function getSummary()
{
return Yii::$app->view->render('summary', [
'url' => $this->getUrl(),
'count' => isset($this->data['jobs']) ? count($this->data['jobs']) : 0,
], $this);
}
/**
* @inheritdoc
*/
public function getDetail()
{
$jobs = isset($this->data['jobs']) ? $this->data['jobs'] : [];
foreach ($jobs as &$job) {
$job['status'] = 'unknown';
/** @var Queue $queue */
if ($queue = Yii::$app->get($job['sender'], false)) {
try {
if ($queue->isWaiting($job['id'])) {
$job['status'] = 'waiting';
} elseif ($queue->isReserved($job['id'])) {
$job['status'] = 'reserved';
} elseif ($queue->isDone($job['id'])) {
$job['status'] = 'done';
}
} catch (NotSupportedException $e) {
} catch (\Exception $e) {
$job['status'] = $e->getMessage();
}
}
}
unset($job);
return Yii::$app->view->render('detail', compact('jobs'), $this);
}
}

View File

@ -0,0 +1,87 @@
<?php
/**
* @var \yii\web\View $this
* @var array $jobs
*/
use yii\helpers\Html;
$styles = [
'unknown' => 'default',
'waiting' => 'info',
'reserved' => 'warning',
'done' => 'success',
];
?>
<h1>Pushed <?= count($jobs) ?> jobs</h1>
<?php foreach ($jobs as $job): ?>
<div class="panel panel-<?= isset($styles[$job['status']]) ? $styles[$job['status']] : 'danger' ?>">
<div class="panel-heading">
<h3 class="panel-title">
<?php if (is_string($job['id'])): ?>
<?= Html::encode($job['id']) ?> -
<?php endif; ?>
<?= isset($job['class']) ? Html::encode($job['class']) : 'Mixed data' ?>
</h3>
</div>
<table class="table">
<tr>
<th>Sender</th>
<td><?= Html::encode($job['sender']) ?></td>
</tr>
<?php if (isset($job['id'])): ?>
<tr>
<th>ID</th>
<td><?= Html::encode($job['id']) ?></td>
</tr>
<?php endif; ?>
<tr>
<th>TTR</th>
<td><?= Html::encode($job['ttr']) ?></td>
</tr>
<?php if ($job['delay']): ?>
<tr>
<th>Delay</th>
<td><?= Html::encode($job['delay']) ?></td>
</tr>
<?php endif; ?>
<?php if (isset($job['priority'])): ?>
<tr>
<th>Priority</th>
<td><?= Html::encode($job['priority']) ?></td>
</tr>
<?php endif; ?>
<tr>
<th>Status</th>
<td><?= Html::encode($job['status']) ?></td>
</tr>
<?php if (isset($job['class'])): ?>
<tr>
<th>Class</th>
<td><?= Html::encode($job['class']) ?></td>
</tr>
<?php foreach ($job['properties'] as $property => $value): ?>
<tr>
<th><?= Html::encode($property) ?></th>
<td><?= Html::encode($value) ?></td>
</tr>
<?php endforeach; ?>
<?php else: ?>
<tr>
<th>Data</th>
<td><?= Html::encode($job['data']) ?></td>
</tr>
<?php endif; ?>
</table>
</div>
<?php endforeach; ?>
<?php
$this->registerCss(
<<<'CSS'
.panel > .table th {
width: 25%;
}
CSS
);

View File

@ -0,0 +1,16 @@
<?php
/**
* @var \yii\web\View $this
* @var string $url
* @var int $count
*/
?>
<div class="yii-debug-toolbar__block">
<a href="<?= $url ?>">
Queue
<span class="yii-debug-toolbar__label yii-debug-toolbar__label_<?= $count ? 'info' : 'default' ?>">
<?= $count ?>
</span>
</a>
</div>

View File

@ -0,0 +1,43 @@
<?php
/**
* @link https://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license https://www.yiiframework.com/license/
*/
namespace yii\queue\amqp;
use yii\queue\cli\Command as CliCommand;
/**
* Manages application amqp-queue.
*
* @deprecated since 2.0.2 and will be removed in 3.0. Consider using amqp_interop driver instead
*
* @author Roman Zhuravlev <zhuravljov@gmail.com>
*/
class Command extends CliCommand
{
/**
* @var Queue
*/
public $queue;
/**
* @inheritdoc
*/
protected function isWorkerAction($actionID)
{
return $actionID === 'listen';
}
/**
* Listens amqp-queue and runs new jobs.
* It can be used as daemon process.
*/
public function actionListen()
{
$this->queue->listen();
}
}

View File

@ -0,0 +1,167 @@
<?php
/**
* @link https://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license https://www.yiiframework.com/license/
*/
namespace yii\queue\amqp;
use PhpAmqpLib\Channel\AMQPChannel;
use PhpAmqpLib\Connection\AMQPStreamConnection;
use PhpAmqpLib\Message\AMQPMessage;
use yii\base\Application as BaseApp;
use yii\base\Event;
use yii\base\NotSupportedException;
use yii\queue\cli\Queue as CliQueue;
/**
* Amqp Queue.
*
* @deprecated since 2.0.2 and will be removed in 3.0. Consider using amqp_interop driver instead.
*
* @author Roman Zhuravlev <zhuravljov@gmail.com>
*/
class Queue extends CliQueue
{
public $host = 'localhost';
public $port = 5672;
public $user = 'guest';
public $password = 'guest';
public $queueName = 'queue';
public $exchangeName = 'exchange';
public $vhost = '/';
/**
* @var int The periods of time PHP pings the broker in order to prolong the connection timeout. In seconds.
* @since 2.3.1
*/
public $heartbeat = 0;
/**
* Send keep-alive packets for a socket connection
* @var bool
* @since 2.3.6
*/
public $keepalive = false;
/**
* @var string command class name
*/
public $commandClass = Command::class;
/**
* @var AMQPStreamConnection
*/
protected $connection;
/**
* @var AMQPChannel
*/
protected $channel;
/**
* @inheritdoc
*/
public function init()
{
parent::init();
Event::on(BaseApp::class, BaseApp::EVENT_AFTER_REQUEST, function () {
$this->close();
});
}
/**
* Listens amqp-queue and runs new jobs.
*/
public function listen()
{
$this->open();
$callback = function (AMQPMessage $payload) {
$id = $payload->get('message_id');
list($ttr, $message) = explode(';', $payload->body, 2);
if ($this->handleMessage($id, $message, $ttr, 1)) {
$payload->delivery_info['channel']->basic_ack($payload->delivery_info['delivery_tag']);
}
};
$this->channel->basic_qos(null, 1, null);
$this->channel->basic_consume($this->queueName, '', false, false, false, false, $callback);
while (count($this->channel->callbacks)) {
$this->channel->wait();
}
}
/**
* @inheritdoc
*/
protected function pushMessage($message, $ttr, $delay, $priority)
{
if ($delay) {
throw new NotSupportedException('Delayed work is not supported in the driver.');
}
if ($priority !== null) {
throw new NotSupportedException('Job priority is not supported in the driver.');
}
$this->open();
$id = uniqid('', true);
$this->channel->basic_publish(
new AMQPMessage("$ttr;$message", [
'delivery_mode' => AMQPMessage::DELIVERY_MODE_PERSISTENT,
'message_id' => $id,
]),
$this->exchangeName
);
return $id;
}
/**
* @inheritdoc
*/
public function status($id)
{
throw new NotSupportedException('Status is not supported in the driver.');
}
/**
* Opens connection and channel.
*/
protected function open()
{
if ($this->channel) {
return;
}
$this->connection = new AMQPStreamConnection(
$this->host,
$this->port,
$this->user,
$this->password,
$this->vhost,
false,
'AMQPLAIN',
null,
'en_US',
3.0,
3.0,
null,
$this->keepalive,
$this->heartbeat,
0.0,
null
);
$this->channel = $this->connection->channel();
$this->channel->queue_declare($this->queueName, false, true, false, false);
$this->channel->exchange_declare($this->exchangeName, 'direct', false, true, false);
$this->channel->queue_bind($this->queueName, $this->exchangeName);
}
/**
* Closes connection and channel.
*/
protected function close()
{
if (!$this->channel) {
return;
}
$this->channel->close();
$this->connection->close();
}
}

View File

@ -0,0 +1,42 @@
<?php
/**
* @link https://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license https://www.yiiframework.com/license/
*/
namespace yii\queue\amqp_interop;
use yii\queue\cli\Command as CliCommand;
/**
* Manages application amqp-queue.
*
* @author Maksym Kotliar <kotlyar.maksim@gmail.com>
* @since 2.0.2
*/
class Command extends CliCommand
{
/**
* @var Queue
*/
public $queue;
/**
* @inheritdoc
*/
protected function isWorkerAction($actionID)
{
return $actionID === 'listen';
}
/**
* Listens amqp-queue and runs new jobs.
* It can be used as daemon process.
*/
public function actionListen()
{
$this->queue->listen();
}
}

View File

@ -0,0 +1,518 @@
<?php
/**
* @link https://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license https://www.yiiframework.com/license/
*/
namespace yii\queue\amqp_interop;
use Enqueue\AmqpBunny\AmqpConnectionFactory as AmqpBunnyConnectionFactory;
use Enqueue\AmqpExt\AmqpConnectionFactory as AmqpExtConnectionFactory;
use Enqueue\AmqpLib\AmqpConnectionFactory as AmqpLibConnectionFactory;
use Enqueue\AmqpTools\DelayStrategyAware;
use Enqueue\AmqpTools\RabbitMqDlxDelayStrategy;
use Interop\Amqp\AmqpConnectionFactory;
use Interop\Amqp\AmqpConsumer;
use Interop\Amqp\AmqpContext;
use Interop\Amqp\AmqpDestination;
use Interop\Amqp\AmqpMessage;
use Interop\Amqp\AmqpQueue;
use Interop\Amqp\AmqpTopic;
use Interop\Amqp\Impl\AmqpBind;
use yii\base\Application as BaseApp;
use yii\base\Event;
use yii\base\NotSupportedException;
use yii\queue\cli\Queue as CliQueue;
/**
* Amqp Queue.
*
* @property-read AmqpContext $context
*
* @author Maksym Kotliar <kotlyar.maksim@gmail.com>
* @since 2.0.2
*/
class Queue extends CliQueue
{
const ATTEMPT = 'yii-attempt';
const TTR = 'yii-ttr';
const DELAY = 'yii-delay';
const PRIORITY = 'yii-priority';
const ENQUEUE_AMQP_LIB = 'enqueue/amqp-lib';
const ENQUEUE_AMQP_EXT = 'enqueue/amqp-ext';
const ENQUEUE_AMQP_BUNNY = 'enqueue/amqp-bunny';
/**
* The connection to the broker could be configured as an array of options
* or as a DSN string like amqp:, amqps:, amqps://user:pass@localhost:1000/vhost.
*
* @var string
*/
public $dsn;
/**
* The message queue broker's host.
*
* @var string|null
*/
public $host;
/**
* The message queue broker's port.
*
* @var string|null
*/
public $port;
/**
* This is RabbitMQ user which is used to login on the broker.
*
* @var string|null
*/
public $user;
/**
* This is RabbitMQ password which is used to login on the broker.
*
* @var string|null
*/
public $password;
/**
* Virtual hosts provide logical grouping and separation of resources.
*
* @var string|null
*/
public $vhost;
/**
* The time PHP socket waits for an information while reading. In seconds.
*
* @var float|null
*/
public $readTimeout;
/**
* The time PHP socket waits for an information while witting. In seconds.
*
* @var float|null
*/
public $writeTimeout;
/**
* The time RabbitMQ keeps the connection on idle. In seconds.
*
* @var float|null
*/
public $connectionTimeout;
/**
* The periods of time PHP pings the broker in order to prolong the connection timeout. In seconds.
*
* @var float|null
*/
public $heartbeat;
/**
* PHP uses one shared connection if set true.
*
* @var bool|null
*/
public $persisted;
/**
* Send keep-alive packets for a socket connection
* @var bool
* @since 2.3.6
*/
public $keepalive;
/**
* The connection will be established as later as possible if set true.
*
* @var bool|null
*/
public $lazy;
/**
* If false prefetch_count option applied separately to each new consumer on the channel
* If true prefetch_count option shared across all consumers on the channel.
*
* @var bool|null
*/
public $qosGlobal;
/**
* Defines number of message pre-fetched in advance on a channel basis.
*
* @var int|null
*/
public $qosPrefetchSize;
/**
* Defines number of message pre-fetched in advance per consumer.
*
* @var int|null
*/
public $qosPrefetchCount;
/**
* Defines whether secure connection should be used or not.
*
* @var bool|null
*/
public $sslOn;
/**
* Require verification of SSL certificate used.
*
* @var bool|null
*/
public $sslVerify;
/**
* 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.
*
* @var string|null
*/
public $sslCacert;
/**
* Path to local certificate file on filesystem.
*
* @var string|null
*/
public $sslCert;
/**
* Path to local private key file on filesystem in case of separate files for certificate (local_cert) and private key.
*
* @var string|null
*/
public $sslKey;
/**
* The queue used to consume messages from.
*
* @var string
*/
public $queueName = 'interop_queue';
/**
* Setting optional arguments for the queue (key-value pairs)
* ```php
* [
* 'x-expires' => 300000,
* 'x-max-priority' => 10
* ]
* ```
*
* @var array
* @since 2.3.3
* @see https://www.rabbitmq.com/queues.html#optional-arguments
*/
public $queueOptionalArguments = [];
/**
* Set of flags for the queue
* @var int
* @since 2.3.5
* @see AmqpDestination
*/
public $queueFlags = AmqpQueue::FLAG_DURABLE;
/**
* The exchange used to publish messages to.
*
* @var string
*/
public $exchangeName = 'exchange';
/**
* The exchange type. Can take values: direct, fanout, topic, headers
* @var string
* @since 2.3.3
*/
public $exchangeType = AmqpTopic::TYPE_DIRECT;
/**
* Set of flags for the exchange
* @var int
* @since 2.3.5
* @see AmqpDestination
*/
public $exchangeFlags = AmqpTopic::FLAG_DURABLE;
/**
* Routing key for publishing messages. Default is NULL.
*
* @var string|null
*/
public $routingKey;
/**
* Defines the amqp interop transport being internally used. Currently supports lib, ext and bunny values.
*
* @var string
*/
public $driver = self::ENQUEUE_AMQP_LIB;
/**
* The property contains a command class which used in cli.
*
* @var string command class name
*/
public $commandClass = Command::class;
/**
* Headers to send along with the message
* ```php
* [
* 'header-1' => 'header-value-1',
* 'header-2' => 'header-value-2',
* ]
* ```
*
* @var array
* @since 3.0.0
*/
public $setMessageHeaders = [];
/**
* Amqp interop context.
*
* @var AmqpContext
*/
protected $context;
/**
* List of supported amqp interop drivers.
*
* @var string[]
*/
protected $supportedDrivers = [self::ENQUEUE_AMQP_LIB, self::ENQUEUE_AMQP_EXT, self::ENQUEUE_AMQP_BUNNY];
/**
* The property tells whether the setupBroker method was called or not.
* Having it we can do broker setup only once per process.
*
* @var bool
*/
protected $setupBrokerDone = false;
/**
* @inheritdoc
*/
public function init()
{
parent::init();
Event::on(BaseApp::class, BaseApp::EVENT_AFTER_REQUEST, function () {
$this->close();
});
if (extension_loaded('pcntl') && function_exists('pcntl_signal') && PHP_MAJOR_VERSION >= 7) {
// https://github.com/php-amqplib/php-amqplib#unix-signals
$signals = [SIGTERM, SIGQUIT, SIGINT, SIGHUP];
foreach ($signals as $signal) {
$oldHandler = null;
// This got added in php 7.1 and might not exist on all supported versions
if (function_exists('pcntl_signal_get_handler')) {
$oldHandler = pcntl_signal_get_handler($signal);
}
pcntl_signal($signal, static function ($signal) use ($oldHandler) {
if ($oldHandler && is_callable($oldHandler)) {
$oldHandler($signal);
}
pcntl_signal($signal, SIG_DFL);
posix_kill(posix_getpid(), $signal);
});
}
}
}
/**
* Listens amqp-queue and runs new jobs.
*/
public function listen()
{
$this->open();
$this->setupBroker();
$queue = $this->context->createQueue($this->queueName);
$consumer = $this->context->createConsumer($queue);
$callback = function (AmqpMessage $message, AmqpConsumer $consumer) {
if ($message->isRedelivered()) {
$consumer->acknowledge($message);
$this->redeliver($message);
return true;
}
$ttr = $message->getProperty(self::TTR);
$attempt = $message->getProperty(self::ATTEMPT, 1);
if ($this->handleMessage($message->getMessageId(), $message->getBody(), $ttr, $attempt)) {
$consumer->acknowledge($message);
} else {
$consumer->acknowledge($message);
$this->redeliver($message);
}
return true;
};
$subscriptionConsumer = $this->context->createSubscriptionConsumer();
$subscriptionConsumer->subscribe($consumer, $callback);
$subscriptionConsumer->consume();
}
/**
* @return AmqpContext
*/
public function getContext()
{
$this->open();
return $this->context;
}
/**
* @inheritdoc
*/
protected function pushMessage($payload, $ttr, $delay, $priority)
{
$this->open();
$this->setupBroker();
$topic = $this->context->createTopic($this->exchangeName);
$message = $this->context->createMessage($payload);
$message->setDeliveryMode(AmqpMessage::DELIVERY_MODE_PERSISTENT);
$message->setMessageId(uniqid('', true));
$message->setTimestamp(time());
$message->setProperties(array_merge(
$this->setMessageHeaders,
[
self::ATTEMPT => 1,
self::TTR => $ttr,
]
));
$producer = $this->context->createProducer();
if ($delay) {
$message->setProperty(self::DELAY, $delay);
$producer->setDeliveryDelay($delay * 1000);
}
if ($priority) {
$message->setProperty(self::PRIORITY, $priority);
$producer->setPriority($priority);
}
if (null !== $this->routingKey) {
$message->setRoutingKey($this->routingKey);
}
$producer->send($topic, $message);
return $message->getMessageId();
}
/**
* @inheritdoc
*/
public function status($id)
{
throw new NotSupportedException('Status is not supported in the driver.');
}
/**
* Opens connection and channel.
*/
protected function open()
{
if ($this->context) {
return;
}
switch ($this->driver) {
case self::ENQUEUE_AMQP_LIB:
$connectionClass = AmqpLibConnectionFactory::class;
break;
case self::ENQUEUE_AMQP_EXT:
$connectionClass = AmqpExtConnectionFactory::class;
break;
case self::ENQUEUE_AMQP_BUNNY:
$connectionClass = AmqpBunnyConnectionFactory::class;
break;
default:
throw new \LogicException(sprintf('The given driver "%s" is not supported. Drivers supported are "%s"', $this->driver, implode('", "', $this->supportedDrivers)));
}
$config = [
'dsn' => $this->dsn,
'host' => $this->host,
'port' => $this->port,
'user' => $this->user,
'pass' => $this->password,
'vhost' => $this->vhost,
'read_timeout' => $this->readTimeout,
'write_timeout' => $this->writeTimeout,
'connection_timeout' => $this->connectionTimeout,
'heartbeat' => $this->heartbeat,
'persisted' => $this->persisted,
'keepalive' => $this->keepalive,
'lazy' => $this->lazy,
'qos_global' => $this->qosGlobal,
'qos_prefetch_size' => $this->qosPrefetchSize,
'qos_prefetch_count' => $this->qosPrefetchCount,
'ssl_on' => $this->sslOn,
'ssl_verify' => $this->sslVerify,
'ssl_cacert' => $this->sslCacert,
'ssl_cert' => $this->sslCert,
'ssl_key' => $this->sslKey,
];
$config = array_filter($config, function ($value) {
return null !== $value;
});
/** @var AmqpConnectionFactory $factory */
$factory = new $connectionClass($config);
$this->context = $factory->createContext();
if ($this->context instanceof DelayStrategyAware) {
$this->context->setDelayStrategy(new RabbitMqDlxDelayStrategy());
}
}
protected function setupBroker()
{
if ($this->setupBrokerDone) {
return;
}
$queue = $this->context->createQueue($this->queueName);
$queue->setFlags($this->queueFlags);
$queue->setArguments($this->queueOptionalArguments);
$this->context->declareQueue($queue);
$topic = $this->context->createTopic($this->exchangeName);
$topic->setType($this->exchangeType);
$topic->setFlags($this->exchangeFlags);
$this->context->declareTopic($topic);
$this->context->bind(new AmqpBind($queue, $topic, $this->routingKey));
$this->setupBrokerDone = true;
}
/**
* Closes connection and channel.
*/
protected function close()
{
if (!$this->context) {
return;
}
$this->context->close();
$this->context = null;
$this->setupBrokerDone = false;
}
/**
* {@inheritdoc}
*/
protected function redeliver(AmqpMessage $message)
{
$attempt = $message->getProperty(self::ATTEMPT, 1);
$newMessage = $this->context->createMessage($message->getBody(), $message->getProperties(), $message->getHeaders());
$newMessage->setDeliveryMode($message->getDeliveryMode());
$newMessage->setProperty(self::ATTEMPT, ++$attempt);
$this->context->createProducer()->send(
$this->context->createQueue($this->queueName),
$newMessage
);
}
}

View File

@ -0,0 +1,92 @@
<?php
/**
* @link https://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license https://www.yiiframework.com/license/
*/
namespace yii\queue\beanstalk;
use yii\console\Exception;
use yii\queue\cli\Command as CliCommand;
/**
* Manages application beanstalk-queue.
*
* @author Roman Zhuravlev <zhuravljov@gmail.com>
*/
class Command extends CliCommand
{
/**
* @var Queue
*/
public $queue;
/**
* @var string
*/
public $defaultAction = 'info';
/**
* @inheritdoc
*/
public function actions()
{
return [
'info' => InfoAction::class,
];
}
/**
* @inheritdoc
*/
protected function isWorkerAction($actionID)
{
return in_array($actionID, ['run', 'listen']);
}
/**
* Runs all jobs from beanstalk-queue.
* It can be used as cron job.
*
* @return null|int exit code.
*/
public function actionRun()
{
return $this->queue->run(false);
}
/**
* Listens beanstalk-queue and runs new jobs.
* It can be used as daemon process.
*
* @param int $timeout number of seconds to wait a job.
* @throws Exception when params are invalid.
* @return null|int exit code.
*/
public function actionListen($timeout = 3)
{
if (!is_numeric($timeout)) {
throw new Exception('Timeout must be numeric.');
}
if ($timeout < 1) {
throw new Exception('Timeout must be greater than zero.');
}
return $this->queue->run(true, $timeout);
}
/**
* Removes a job by id.
*
* @param int $id of the job.
* @throws Exception when the job is not found.
* @since 2.0.1
*/
public function actionRemove($id)
{
if (!$this->queue->remove($id)) {
throw new Exception('The job is not found.');
}
}
}

View File

@ -0,0 +1,38 @@
<?php
/**
* @link https://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license https://www.yiiframework.com/license/
*/
namespace yii\queue\beanstalk;
use yii\helpers\Console;
use yii\queue\cli\Action;
/**
* Info about queue status.
*
* @author Roman Zhuravlev <zhuravljov@gmail.com>
*/
class InfoAction extends Action
{
/**
* @var Queue
*/
public $queue;
/**
* Info about queue status.
*/
public function run()
{
Console::output($this->format('Statistical information about the tube:', Console::FG_GREEN));
foreach ($this->queue->getStatsTube() as $key => $value) {
Console::stdout($this->format("- $key: ", Console::FG_YELLOW));
Console::output($value);
}
}
}

View File

@ -0,0 +1,154 @@
<?php
/**
* @link https://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license https://www.yiiframework.com/license/
*/
namespace yii\queue\beanstalk;
use Pheanstalk\Exception\ServerException;
use Pheanstalk\Job;
use Pheanstalk\Pheanstalk;
use Pheanstalk\PheanstalkInterface;
use yii\base\InvalidArgumentException;
use yii\queue\cli\Queue as CliQueue;
/**
* Beanstalk Queue.
*
* @property-read object $statsTube Tube statistics.
*
* @author Roman Zhuravlev <zhuravljov@gmail.com>
*/
class Queue extends CliQueue
{
/**
* @var string connection host
*/
public $host = 'localhost';
/**
* @var int connection port
*/
public $port = PheanstalkInterface::DEFAULT_PORT;
/**
* @var string beanstalk tube
*/
public $tube = 'queue';
/**
* @var string command class name
*/
public $commandClass = Command::class;
/**
* Listens queue and runs each job.
*
* @param bool $repeat whether to continue listening when queue is empty.
* @param int $timeout number of seconds to wait for next message.
* @return null|int exit code.
* @internal for worker command only.
* @since 2.0.2
*/
public function run($repeat, $timeout = 0)
{
return $this->runWorker(function (callable $canContinue) use ($repeat, $timeout) {
while ($canContinue()) {
if ($payload = $this->getPheanstalk()->reserveFromTube($this->tube, $timeout)) {
$info = $this->getPheanstalk()->statsJob($payload);
if ($this->handleMessage(
$payload->getId(),
$payload->getData(),
$info->ttr,
$info->reserves
)) {
$this->getPheanstalk()->delete($payload);
}
} elseif (!$repeat) {
break;
}
}
});
}
/**
* @inheritdoc
*/
public function status($id)
{
if (!is_numeric($id) || $id <= 0) {
throw new InvalidArgumentException("Unknown message ID: $id.");
}
try {
$stats = $this->getPheanstalk()->statsJob($id);
if ($stats['state'] === 'reserved') {
return self::STATUS_RESERVED;
}
return self::STATUS_WAITING;
} catch (ServerException $e) {
if ($e->getMessage() === 'Server reported NOT_FOUND') {
return self::STATUS_DONE;
}
throw $e;
}
}
/**
* Removes a job by ID.
*
* @param int $id of a job
* @return bool
* @since 2.0.1
*/
public function remove($id)
{
try {
$this->getPheanstalk()->delete(new Job($id, null));
return true;
} catch (ServerException $e) {
if (strpos($e->getMessage(), 'NOT_FOUND') === 0) {
return false;
}
throw $e;
}
}
/**
* @inheritdoc
*/
protected function pushMessage($message, $ttr, $delay, $priority)
{
return $this->getPheanstalk()->putInTube(
$this->tube,
$message,
$priority ?: PheanstalkInterface::DEFAULT_PRIORITY,
$delay,
$ttr
);
}
/**
* @return object tube statistics
*/
public function getStatsTube()
{
return $this->getPheanstalk()->statsTube($this->tube);
}
/**
* @return Pheanstalk
*/
protected function getPheanstalk()
{
if (!$this->_pheanstalk) {
$this->_pheanstalk = new Pheanstalk($this->host, $this->port);
}
return $this->_pheanstalk;
}
private $_pheanstalk;
}

View File

@ -0,0 +1,105 @@
<?php
/**
* @link https://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license https://www.yiiframework.com/license/
*/
namespace yii\queue\db;
use yii\console\Exception;
use yii\queue\cli\Command as CliCommand;
use yii\queue\cli\InfoAction;
/**
* Manages application db-queue.
*
* @author Roman Zhuravlev <zhuravljov@gmail.com>
*/
class Command extends CliCommand
{
/**
* @var Queue
*/
public $queue;
/**
* @var string
*/
public $defaultAction = 'info';
/**
* @inheritdoc
*/
public function actions()
{
return [
'info' => InfoAction::class,
];
}
/**
* @inheritdoc
*/
protected function isWorkerAction($actionID)
{
return in_array($actionID, ['run', 'listen'], true);
}
/**
* Runs all jobs from db-queue.
* It can be used as cron job.
*
* @return null|int exit code.
*/
public function actionRun()
{
return $this->queue->run(false);
}
/**
* Listens db-queue and runs new jobs.
* It can be used as daemon process.
*
* @param int $timeout number of seconds to sleep before next reading of the queue.
* @throws Exception when params are invalid.
* @return null|int exit code.
*/
public function actionListen($timeout = 3)
{
if (!is_numeric($timeout)) {
throw new Exception('Timeout must be numeric.');
}
if ($timeout < 1) {
throw new Exception('Timeout must be greater than zero.');
}
return $this->queue->run(true, $timeout);
}
/**
* Clears the queue.
*
* @since 2.0.1
*/
public function actionClear()
{
if ($this->confirm('Are you sure?')) {
$this->queue->clear();
}
}
/**
* Removes a job by id.
*
* @param int $id
* @throws Exception when the job is not found.
* @since 2.0.1
*/
public function actionRemove($id)
{
if (!$this->queue->remove($id)) {
throw new Exception('The job is not found.');
}
}
}

View File

@ -0,0 +1,95 @@
<?php
/**
* @link https://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license https://www.yiiframework.com/license/
*/
namespace yii\queue\db;
use yii\db\Query;
use yii\helpers\Console;
use yii\queue\cli\Action;
/**
* Info about queue status.
*
* @deprecated Will be removed in 3.0. Use yii\queue\cli\InfoAction instead.
*
* @author Roman Zhuravlev <zhuravljov@gmail.com>
*/
class InfoAction extends Action
{
/**
* @var Queue
*/
public $queue;
/**
* Info about queue status.
*/
public function run()
{
Console::output($this->format('Jobs', Console::FG_GREEN));
Console::stdout($this->format('- waiting: ', Console::FG_YELLOW));
Console::output($this->getWaiting()->count('*', $this->queue->db));
Console::stdout($this->format('- delayed: ', Console::FG_YELLOW));
Console::output($this->getDelayed()->count('*', $this->queue->db));
Console::stdout($this->format('- reserved: ', Console::FG_YELLOW));
Console::output($this->getReserved()->count('*', $this->queue->db));
Console::stdout($this->format('- done: ', Console::FG_YELLOW));
Console::output($this->getDone()->count('*', $this->queue->db));
}
/**
* @return Query
*/
protected function getWaiting()
{
return (new Query())
->from($this->queue->tableName)
->andWhere(['channel' => $this->queue->channel])
->andWhere(['reserved_at' => null])
->andWhere(['delay' => 0]);
}
/**
* @return Query
*/
protected function getDelayed()
{
return (new Query())
->from($this->queue->tableName)
->andWhere(['channel' => $this->queue->channel])
->andWhere(['reserved_at' => null])
->andWhere(['>', 'delay', 0]);
}
/**
* @return Query
*/
protected function getReserved()
{
return (new Query())
->from($this->queue->tableName)
->andWhere(['channel' => $this->queue->channel])
->andWhere('[[reserved_at]] is not null')
->andWhere(['done_at' => null]);
}
/**
* @return Query
*/
protected function getDone()
{
return (new Query())
->from($this->queue->tableName)
->andWhere(['channel' => $this->queue->channel])
->andWhere('[[done_at]] is not null');
}
}

View File

@ -0,0 +1,271 @@
<?php
/**
* @link https://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license https://www.yiiframework.com/license/
*/
namespace yii\queue\db;
use yii\base\Exception;
use yii\base\InvalidArgumentException;
use yii\db\Connection;
use yii\db\Query;
use yii\di\Instance;
use yii\mutex\Mutex;
use yii\queue\cli\Queue as CliQueue;
use yii\queue\interfaces\StatisticsProviderInterface;
/**
* Db Queue.
*
* @property-read StatisticsProvider $statisticsProvider
*
* @author Roman Zhuravlev <zhuravljov@gmail.com>
*/
class Queue extends CliQueue implements StatisticsProviderInterface
{
/**
* @var Connection|array|string
*/
public $db = 'db';
/**
* @var Mutex|array|string
*/
public $mutex = 'mutex';
/**
* @var int timeout
*/
public $mutexTimeout = 3;
/**
* @var string table name
*/
public $tableName = '{{%queue}}';
/**
* @var string
*/
public $channel = 'queue';
/**
* @var bool ability to delete released messages from table
*/
public $deleteReleased = true;
/**
* @var string command class name
*/
public $commandClass = Command::class;
/**
* @inheritdoc
*/
public function init()
{
parent::init();
$this->db = Instance::ensure($this->db, Connection::class);
$this->mutex = Instance::ensure($this->mutex, Mutex::class);
}
/**
* Listens queue and runs each job.
*
* @param bool $repeat whether to continue listening when queue is empty.
* @param int $timeout number of seconds to sleep before next iteration.
* @return null|int exit code.
* @internal for worker command only
* @since 2.0.2
*/
public function run($repeat, $timeout = 0)
{
return $this->runWorker(function (callable $canContinue) use ($repeat, $timeout) {
while ($canContinue()) {
if ($payload = $this->reserve()) {
if ($this->handleMessage(
$payload['id'],
$payload['job'],
$payload['ttr'],
$payload['attempt']
)) {
$this->release($payload);
}
} elseif (!$repeat) {
break;
} elseif ($timeout) {
sleep($timeout);
}
}
});
}
/**
* @inheritdoc
*/
public function status($id)
{
$payload = (new Query())
->from($this->tableName)
->where(['id' => $id])
->one($this->db);
if (!$payload) {
if ($this->deleteReleased) {
return self::STATUS_DONE;
}
throw new InvalidArgumentException("Unknown message ID: $id.");
}
if (!$payload['reserved_at']) {
return self::STATUS_WAITING;
}
if (!$payload['done_at']) {
return self::STATUS_RESERVED;
}
return self::STATUS_DONE;
}
/**
* Clears the queue.
*
* @since 2.0.1
*/
public function clear()
{
$this->db->createCommand()
->delete($this->tableName, ['channel' => $this->channel])
->execute();
}
/**
* Removes a job by ID.
*
* @param int $id of a job
* @return bool
* @since 2.0.1
*/
public function remove($id)
{
return (bool) $this->db->createCommand()
->delete($this->tableName, ['channel' => $this->channel, 'id' => $id])
->execute();
}
/**
* @inheritdoc
*/
protected function pushMessage($message, $ttr, $delay, $priority)
{
$this->db->createCommand()->insert($this->tableName, [
'channel' => $this->channel,
'job' => $message,
'pushed_at' => time(),
'ttr' => $ttr,
'delay' => $delay,
'priority' => $priority ?: 1024,
])->execute();
$tableSchema = $this->db->getTableSchema($this->tableName);
return $this->db->getLastInsertID($tableSchema->sequenceName);
}
/**
* Takes one message from waiting list and reserves it for handling.
*
* @return array|false payload
* @throws Exception in case it hasn't waited the lock
*/
protected function reserve()
{
return $this->db->useMaster(function () {
if (!$this->mutex->acquire(__CLASS__ . $this->channel, $this->mutexTimeout)) {
throw new Exception('Has not waited the lock.');
}
try {
$this->moveExpired();
// Reserve one message
$payload = (new Query())
->from($this->tableName)
->andWhere(['channel' => $this->channel, 'reserved_at' => null])
->andWhere('[[pushed_at]] <= :time - [[delay]]', [':time' => time()])
->orderBy(['priority' => SORT_ASC, 'id' => SORT_ASC])
->limit(1)
->one($this->db);
if (is_array($payload)) {
$payload['reserved_at'] = time();
$payload['attempt'] = (int) $payload['attempt'] + 1;
$this->db->createCommand()->update($this->tableName, [
'reserved_at' => $payload['reserved_at'],
'attempt' => $payload['attempt'],
], [
'id' => $payload['id'],
])->execute();
// pgsql
if (is_resource($payload['job'])) {
$payload['job'] = stream_get_contents($payload['job']);
}
}
} finally {
$this->mutex->release(__CLASS__ . $this->channel);
}
return $payload;
});
}
/**
* @param array $payload
*/
protected function release($payload)
{
if ($this->deleteReleased) {
$this->db->createCommand()->delete(
$this->tableName,
['id' => $payload['id']]
)->execute();
} else {
$this->db->createCommand()->update(
$this->tableName,
['done_at' => time()],
['id' => $payload['id']]
)->execute();
}
}
protected $reserveTime;
/**
* Moves expired messages into waiting list.
*/
protected function moveExpired()
{
if ($this->reserveTime !== time()) {
$this->reserveTime = time();
$this->db->createCommand()->update(
$this->tableName,
['reserved_at' => null],
// `reserved_at IS NOT NULL` forces db to use index on column,
// otherwise a full scan of the table will be performed
'[[reserved_at]] is not null and [[reserved_at]] < :time - [[ttr]] and [[done_at]] is null',
[':time' => $this->reserveTime]
)->execute();
}
}
private $_statistcsProvider;
/**
* @return StatisticsProvider
*/
public function getStatisticsProvider()
{
if (!$this->_statistcsProvider) {
$this->_statistcsProvider = new StatisticsProvider($this);
}
return $this->_statistcsProvider;
}
}

View File

@ -0,0 +1,82 @@
<?php
/**
* @link https://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license https://www.yiiframework.com/license/
*/
namespace yii\queue\db;
use yii\base\BaseObject;
use yii\db\Query;
use yii\queue\interfaces\DelayedCountInterface;
use yii\queue\interfaces\DoneCountInterface;
use yii\queue\interfaces\ReservedCountInterface;
use yii\queue\interfaces\WaitingCountInterface;
/**
* Statistics Provider
*
* @author Kalmer Kaurson <kalmerkaurson@gmail.com>
*/
class StatisticsProvider extends BaseObject implements DoneCountInterface, WaitingCountInterface, DelayedCountInterface, ReservedCountInterface
{
/**
* @var Queue
*/
protected $queue;
public function __construct(Queue $queue, $config = [])
{
$this->queue = $queue;
parent::__construct($config);
}
/**
* @inheritdoc
*/
public function getWaitingCount()
{
return (new Query())
->from($this->queue->tableName)
->andWhere(['channel' => $this->queue->channel])
->andWhere(['reserved_at' => null])
->andWhere(['delay' => 0])->count('*', $this->queue->db);
}
/**
* @inheritdoc
*/
public function getDelayedCount()
{
return (new Query())
->from($this->queue->tableName)
->andWhere(['channel' => $this->queue->channel])
->andWhere(['reserved_at' => null])
->andWhere(['>', 'delay', 0])->count('*', $this->queue->db);
}
/**
* @inheritdoc
*/
public function getReservedCount()
{
return (new Query())
->from($this->queue->tableName)
->andWhere(['channel' => $this->queue->channel])
->andWhere('[[reserved_at]] is not null')
->andWhere(['done_at' => null])->count('*', $this->queue->db);
}
/**
* @inheritdoc
*/
public function getDoneCount()
{
return (new Query())
->from($this->queue->tableName)
->andWhere(['channel' => $this->queue->channel])
->andWhere('[[done_at]] is not null')->count('*', $this->queue->db);
}
}

View File

@ -0,0 +1,42 @@
<?php
/**
* @link https://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license https://www.yiiframework.com/license/
*/
namespace yii\queue\db\migrations;
use yii\db\Migration;
/**
* Example of migration for queue message storage.
*
* @author Roman Zhuravlev <zhuravljov@gmail.com>
*/
class M161119140200Queue extends Migration
{
public $tableName = '{{%queue}}';
public $tableOptions;
public function up()
{
$this->createTable($this->tableName, [
'id' => $this->primaryKey(),
'channel' => $this->string()->notNull(),
'job' => $this->binary()->notNull(),
'created_at' => $this->integer()->notNull(),
'started_at' => $this->integer(),
'finished_at' => $this->integer(),
], $this->tableOptions);
$this->createIndex('channel', $this->tableName, 'channel');
$this->createIndex('started_at', $this->tableName, 'started_at');
}
public function down()
{
$this->dropTable($this->tableName);
}
}

View File

@ -0,0 +1,31 @@
<?php
/**
* @link https://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license https://www.yiiframework.com/license/
*/
namespace yii\queue\db\migrations;
use yii\db\Migration;
/**
* Example of migration for queue message storage.
*
* @author Roman Zhuravlev <zhuravljov@gmail.com>
*/
class M170307170300Later extends Migration
{
public $tableName = '{{%queue}}';
public function up()
{
$this->addColumn($this->tableName, 'timeout', $this->integer()->defaultValue(0)->notNull()->after('created_at'));
}
public function down()
{
$this->dropColumn($this->tableName, 'timeout');
}
}

View File

@ -0,0 +1,77 @@
<?php
/**
* @link https://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license https://www.yiiframework.com/license/
*/
namespace yii\queue\db\migrations;
use yii\db\Migration;
/**
* Example of migration for queue message storage.
*
* @author Roman Zhuravlev <zhuravljov@gmail.com>
*/
class M170509001400Retry extends Migration
{
public $tableName = '{{%queue}}';
public function up()
{
if ($this->db->driverName !== 'sqlite') {
$this->renameColumn($this->tableName, 'created_at', 'pushed_at');
$this->addColumn($this->tableName, 'ttr', $this->integer()->notNull()->after('pushed_at'));
$this->renameColumn($this->tableName, 'timeout', 'delay');
$this->dropIndex('started_at', $this->tableName);
$this->renameColumn($this->tableName, 'started_at', 'reserved_at');
$this->createIndex('reserved_at', $this->tableName, 'reserved_at');
$this->addColumn($this->tableName, 'attempt', $this->integer()->after('reserved_at'));
$this->renameColumn($this->tableName, 'finished_at', 'done_at');
} else {
$this->dropTable($this->tableName);
$this->createTable($this->tableName, [
'id' => $this->primaryKey(),
'channel' => $this->string()->notNull(),
'job' => $this->binary()->notNull(),
'pushed_at' => $this->integer()->notNull(),
'ttr' => $this->integer()->notNull(),
'delay' => $this->integer()->notNull(),
'reserved_at' => $this->integer(),
'attempt' => $this->integer(),
'done_at' => $this->integer(),
]);
$this->createIndex('channel', $this->tableName, 'channel');
$this->createIndex('reserved_at', $this->tableName, 'reserved_at');
}
}
public function down()
{
if ($this->db->driverName !== 'sqlite') {
$this->renameColumn($this->tableName, 'done_at', 'finished_at');
$this->dropColumn($this->tableName, 'attempt');
$this->dropIndex('reserved_at', $this->tableName);
$this->renameColumn($this->tableName, 'reserved_at', 'started_at');
$this->createIndex('started_at', $this->tableName, 'started_at');
$this->renameColumn($this->tableName, 'delay', 'timeout');
$this->dropColumn($this->tableName, 'ttr');
$this->renameColumn($this->tableName, 'pushed_at', 'created_at');
} else {
$this->dropTable($this->tableName);
$this->createTable($this->tableName, [
'id' => $this->primaryKey(),
'channel' => $this->string()->notNull(),
'job' => $this->binary()->notNull(),
'created_at' => $this->integer()->notNull(),
'timeout' => $this->integer()->notNull(),
'started_at' => $this->integer(),
'finished_at' => $this->integer(),
]);
$this->createIndex('channel', $this->tableName, 'channel');
$this->createIndex('started_at', $this->tableName, 'started_at');
}
}
}

View File

@ -0,0 +1,33 @@
<?php
/**
* @link https://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license https://www.yiiframework.com/license/
*/
namespace yii\queue\db\migrations;
use yii\db\Migration;
/**
* Example of migration for queue message storage.
*
* @author Roman Zhuravlev <zhuravljov@gmail.com>
*/
class M170601155600Priority extends Migration
{
public $tableName = '{{%queue}}';
public function up()
{
$this->addColumn($this->tableName, 'priority', $this->integer()->unsigned()->notNull()->defaultValue(1024)->after('delay'));
$this->createIndex('priority', $this->tableName, 'priority');
}
public function down()
{
$this->dropIndex('priority', $this->tableName);
$this->dropColumn($this->tableName, 'priority');
}
}

View File

@ -0,0 +1,35 @@
<?php
/**
* @link https://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license https://www.yiiframework.com/license/
*/
namespace yii\queue\db\migrations;
use yii\db\Migration;
/**
* Example of migration for queue message storage.
*
* @author Roman Zhuravlev <zhuravljov@gmail.com>
*/
class M211218163000JobQueueSize extends Migration
{
public $tableName = '{{%queue}}';
public function up()
{
if ($this->db->driverName === 'mysql') {
$this->alterColumn($this->tableName, 'job', 'LONGBLOB NOT NULL');
}
}
public function down()
{
if ($this->db->driverName === 'mysql') {
$this->alterColumn($this->tableName, 'job', $this->binary()->notNull());
}
}
}

View File

@ -0,0 +1,105 @@
<?php
/**
* @link https://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license https://www.yiiframework.com/license/
*/
namespace yii\queue\file;
use yii\console\Exception;
use yii\queue\cli\Command as CliCommand;
use yii\queue\cli\InfoAction;
/**
* Manages application file-queue.
*
* @author Roman Zhuravlev <zhuravljov@gmail.com>
*/
class Command extends CliCommand
{
/**
* @var Queue
*/
public $queue;
/**
* @var string
*/
public $defaultAction = 'info';
/**
* @inheritdoc
*/
public function actions()
{
return [
'info' => InfoAction::class,
];
}
/**
* @inheritdoc
*/
protected function isWorkerAction($actionID)
{
return in_array($actionID, ['run', 'listen']);
}
/**
* Runs all jobs from file-queue.
* It can be used as cron job.
*
* @return null|int exit code.
*/
public function actionRun()
{
return $this->queue->run(false);
}
/**
* Listens file-queue and runs new jobs.
* It can be used as daemon process.
*
* @param int $timeout number of seconds to sleep before next reading of the queue.
* @throws Exception when params are invalid.
* @return null|int exit code.
*/
public function actionListen($timeout = 3)
{
if (!is_numeric($timeout)) {
throw new Exception('Timeout must be numeric.');
}
if ($timeout < 1) {
throw new Exception('Timeout must be greater than zero.');
}
return $this->queue->run(true, $timeout);
}
/**
* Clears the queue.
*
* @since 2.0.1
*/
public function actionClear()
{
if ($this->confirm('Are you sure?')) {
$this->queue->clear();
}
}
/**
* Removes a job by id.
*
* @param int $id
* @throws Exception when the job is not found.
* @since 2.0.1
*/
public function actionRemove($id)
{
if (!$this->queue->remove((int) $id)) {
throw new Exception('The job is not found.');
}
}
}

View File

@ -0,0 +1,99 @@
<?php
/**
* @link https://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license https://www.yiiframework.com/license/
*/
namespace yii\queue\file;
use yii\helpers\Console;
use yii\queue\cli\Action;
/**
* Info about queue status.
*
* @deprecated Will be removed in 3.0. Use yii\queue\cli\InfoAction instead.
*
* @author Roman Zhuravlev <zhuravljov@gmail.com>
*/
class InfoAction extends Action
{
/**
* @var Queue
*/
public $queue;
/**
* Info about queue status.
*/
public function run()
{
Console::output($this->format('Jobs', Console::FG_GREEN));
Console::stdout($this->format('- waiting: ', Console::FG_YELLOW));
Console::output($this->getWaitingCount());
Console::stdout($this->format('- delayed: ', Console::FG_YELLOW));
Console::output($this->getDelayedCount());
Console::stdout($this->format('- reserved: ', Console::FG_YELLOW));
Console::output($this->getReservedCount());
Console::stdout($this->format('- done: ', Console::FG_YELLOW));
Console::output($this->getDoneCount());
}
/**
* @return int
*/
protected function getWaitingCount()
{
$data = $this->getIndexData();
return !empty($data['waiting']) ? count($data['waiting']) : 0;
}
/**
* @return int
*/
protected function getDelayedCount()
{
$data = $this->getIndexData();
return !empty($data['delayed']) ? count($data['delayed']) : 0;
}
/**
* @return int
*/
protected function getReservedCount()
{
$data = $this->getIndexData();
return !empty($data['reserved']) ? count($data['reserved']) : 0;
}
/**
* @return int
*/
protected function getDoneCount()
{
$data = $this->getIndexData();
$total = isset($data['lastId']) ? $data['lastId'] : 0;
return $total - $this->getDelayedCount() - $this->getWaitingCount();
}
protected function getIndexData()
{
static $data;
if ($data === null) {
$fileName = $this->queue->path . '/index.data';
if (file_exists($fileName)) {
$data = call_user_func($this->queue->indexDeserializer, file_get_contents($fileName));
} else {
$data = [];
}
}
return $data;
}
}

View File

@ -0,0 +1,323 @@
<?php
/**
* @link https://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license https://www.yiiframework.com/license/
*/
namespace yii\queue\file;
use Yii;
use yii\base\InvalidArgumentException;
use yii\base\InvalidConfigException;
use yii\base\NotSupportedException;
use yii\helpers\FileHelper;
use yii\queue\cli\Queue as CliQueue;
use yii\queue\interfaces\StatisticsProviderInterface;
/**
* File Queue.
*
* @property-read StatisticsProvider $statisticsProvider
*
* @author Roman Zhuravlev <zhuravljov@gmail.com>
*/
class Queue extends CliQueue implements StatisticsProviderInterface
{
/**
* @var string
*/
public $path = '@runtime/queue';
/**
* @var int
*/
public $dirMode = 0755;
/**
* @var int|null
*/
public $fileMode;
/**
* @var callable
*/
public $indexSerializer = 'serialize';
/**
* @var callable
*/
public $indexDeserializer = 'unserialize';
/**
* @var string
*/
public $commandClass = Command::class;
/**
* @inheritdoc
*/
public function init()
{
parent::init();
$this->path = Yii::getAlias($this->path);
if (!is_dir($this->path)) {
FileHelper::createDirectory($this->path, $this->dirMode, true);
}
}
/**
* Listens queue and runs each job.
*
* @param bool $repeat whether to continue listening when queue is empty.
* @param int $timeout number of seconds to sleep before next iteration.
* @return null|int exit code.
* @internal for worker command only.
* @since 2.0.2
*/
public function run($repeat, $timeout = 0)
{
return $this->runWorker(function (callable $canContinue) use ($repeat, $timeout) {
while ($canContinue()) {
if (($payload = $this->reserve()) !== null) {
list($id, $message, $ttr, $attempt) = $payload;
if ($this->handleMessage($id, $message, $ttr, $attempt)) {
$this->delete($payload);
}
} elseif (!$repeat) {
break;
} elseif ($timeout) {
sleep($timeout);
}
}
});
}
/**
* @inheritdoc
*/
public function status($id)
{
if (!is_numeric($id) || $id <= 0) {
throw new InvalidArgumentException("Unknown message ID: $id.");
}
if (file_exists("$this->path/job$id.data")) {
return self::STATUS_WAITING;
}
return self::STATUS_DONE;
}
/**
* Clears the queue.
*
* @since 2.0.1
*/
public function clear()
{
$this->touchIndex(function (&$data) {
$data = [];
foreach (glob("$this->path/job*.data") as $fileName) {
unlink($fileName);
}
});
}
/**
* Removes a job by ID.
*
* @param int $id of a job
* @return bool
* @since 2.0.1
*/
public function remove($id)
{
$removed = false;
$this->touchIndex(function (&$data) use ($id, &$removed) {
if (!empty($data['waiting'])) {
foreach ($data['waiting'] as $key => $payload) {
if ($payload[0] === $id) {
unset($data['waiting'][$key]);
$removed = true;
break;
}
}
}
if (!$removed && !empty($data['delayed'])) {
foreach ($data['delayed'] as $key => $payload) {
if ($payload[0] === $id) {
unset($data['delayed'][$key]);
$removed = true;
break;
}
}
}
if (!$removed && !empty($data['reserved'])) {
foreach ($data['reserved'] as $key => $payload) {
if ($payload[0] === $id) {
unset($data['reserved'][$key]);
$removed = true;
break;
}
}
}
if ($removed) {
unlink("$this->path/job$id.data");
}
});
return $removed;
}
/**
* Reserves message for execute.
*
* @return array|null payload
*/
protected function reserve()
{
$id = null;
$ttr = null;
$attempt = null;
$this->touchIndex(function (&$data) use (&$id, &$ttr, &$attempt) {
if (!empty($data['reserved'])) {
foreach ($data['reserved'] as $key => $payload) {
if ($payload[1] + $payload[3] < time()) {
list($id, $ttr, $attempt, $time) = $payload;
$data['reserved'][$key][2] = ++$attempt;
$data['reserved'][$key][3] = time();
return;
}
}
}
if (!empty($data['delayed']) && $data['delayed'][0][2] <= time()) {
list($id, $ttr, $time) = array_shift($data['delayed']);
} elseif (!empty($data['waiting'])) {
list($id, $ttr) = array_shift($data['waiting']);
}
if ($id) {
$attempt = 1;
$data['reserved']["job$id"] = [$id, $ttr, $attempt, time()];
}
});
if ($id) {
return [$id, file_get_contents("$this->path/job$id.data"), $ttr, $attempt];
}
return null;
}
/**
* Deletes reserved message.
*
* @param array $payload
*/
protected function delete($payload)
{
$id = $payload[0];
$this->touchIndex(function (&$data) use ($id) {
foreach ($data['reserved'] as $key => $payload) {
if ($payload[0] === $id) {
unset($data['reserved'][$key]);
break;
}
}
});
unlink("$this->path/job$id.data");
}
/**
* @inheritdoc
*/
protected function pushMessage($message, $ttr, $delay, $priority)
{
if ($priority !== null) {
throw new NotSupportedException('Job priority is not supported in the driver.');
}
$this->touchIndex(function (&$data) use ($message, $ttr, $delay, &$id) {
if (!isset($data['lastId'])) {
$data['lastId'] = 0;
}
$id = ++$data['lastId'];
$fileName = "$this->path/job$id.data";
file_put_contents($fileName, $message);
if ($this->fileMode !== null) {
chmod($fileName, $this->fileMode);
}
if (!$delay) {
$data['waiting'][] = [$id, $ttr, 0];
} else {
$data['delayed'][] = [$id, $ttr, time() + $delay];
usort($data['delayed'], function ($a, $b) {
if ($a[2] < $b[2]) {
return -1;
}
if ($a[2] > $b[2]) {
return 1;
}
if ($a[0] < $b[0]) {
return -1;
}
if ($a[0] > $b[0]) {
return 1;
}
return 0;
});
}
});
return $id;
}
/**
* @param callable $callback
* @throws InvalidConfigException
*/
private function touchIndex($callback)
{
$fileName = "$this->path/index.data";
$isNew = !file_exists($fileName);
touch($fileName);
if ($isNew && $this->fileMode !== null) {
chmod($fileName, $this->fileMode);
}
if (($file = fopen($fileName, 'rb+')) === false) {
throw new InvalidConfigException("Unable to open index file: $fileName");
}
if (!flock($file, LOCK_EX)) {
fclose($file);
throw new InvalidConfigException("Unable to flock index file: $fileName");
}
$data = [];
$content = stream_get_contents($file);
if ($content !== '') {
$data = call_user_func($this->indexDeserializer, $content);
}
try {
$callback($data);
$newContent = call_user_func($this->indexSerializer, $data);
if ($newContent !== $content) {
ftruncate($file, 0);
rewind($file);
fwrite($file, $newContent);
fflush($file);
}
} finally {
flock($file, LOCK_UN);
fclose($file);
}
}
private $_statistcsProvider;
/**
* @return StatisticsProvider
*/
public function getStatisticsProvider()
{
if (!$this->_statistcsProvider) {
$this->_statistcsProvider = new StatisticsProvider($this);
}
return $this->_statistcsProvider;
}
}

View File

@ -0,0 +1,81 @@
<?php
/**
* @link https://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license https://www.yiiframework.com/license/
*/
namespace yii\queue\file;
use yii\base\BaseObject;
use yii\queue\interfaces\DelayedCountInterface;
use yii\queue\interfaces\DoneCountInterface;
use yii\queue\interfaces\ReservedCountInterface;
use yii\queue\interfaces\WaitingCountInterface;
/**
* Statistics Provider
*
* @author Kalmer Kaurson <kalmerkaurson@gmail.com>
*/
class StatisticsProvider extends BaseObject implements DoneCountInterface, WaitingCountInterface, DelayedCountInterface, ReservedCountInterface
{
/**
* @var Queue
*/
protected $queue;
public function __construct(Queue $queue, $config = [])
{
$this->queue = $queue;
parent::__construct($config);
}
/**
* @inheritdoc
*/
public function getWaitingCount()
{
$data = $this->getIndexData();
return !empty($data['waiting']) ? count($data['waiting']) : 0;
}
/**
* @inheritdoc
*/
public function getDelayedCount()
{
$data = $this->getIndexData();
return !empty($data['delayed']) ? count($data['delayed']) : 0;
}
/**
* @inheritdoc
*/
public function getReservedCount()
{
$data = $this->getIndexData();
return !empty($data['reserved']) ? count($data['reserved']) : 0;
}
/**
* @inheritdoc
*/
public function getDoneCount()
{
$data = $this->getIndexData();
$total = isset($data['lastId']) ? $data['lastId'] : 0;
return $total - $this->getDelayedCount() - $this->getWaitingCount();
}
protected function getIndexData()
{
$fileName = $this->queue->path . '/index.data';
if (file_exists($fileName)) {
return call_user_func($this->queue->indexDeserializer, file_get_contents($fileName));
} else {
return [];
}
}
}

View File

@ -0,0 +1,54 @@
<?php
/**
* @link https://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license https://www.yiiframework.com/license/
*/
namespace yii\queue\gearman;
use yii\queue\cli\Command as CliCommand;
/**
* Manages application gearman-queue.
*
* @author Roman Zhuravlev <zhuravljov@gmail.com>
*/
class Command extends CliCommand
{
/**
* @var Queue
*/
public $queue;
/**
* @inheritdoc
*/
protected function isWorkerAction($actionID)
{
return in_array($actionID, ['run', 'listen'], true);
}
/**
* Runs all jobs from gearman-queue.
* It can be used as cron job.
*
* @return null|int exit code.
*/
public function actionRun()
{
return $this->queue->run(false);
}
/**
* Listens gearman-queue and runs new jobs.
* It can be used as daemon process.
*
* @return null|int exit code.
*/
public function actionListen()
{
return $this->queue->run(true);
}
}

View File

@ -0,0 +1,105 @@
<?php
/**
* @link https://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license https://www.yiiframework.com/license/
*/
namespace yii\queue\gearman;
use yii\base\NotSupportedException;
use yii\queue\cli\Queue as CliQueue;
/**
* Gearman Queue.
*
* @author Roman Zhuravlev <zhuravljov@gmail.com>
*/
class Queue extends CliQueue
{
public $host = 'localhost';
public $port = 4730;
public $channel = 'queue';
/**
* @var string command class name
*/
public $commandClass = Command::class;
/**
* Listens queue and runs each job.
*
* @param bool $repeat whether to continue listening when queue is empty.
* @return null|int exit code.
* @internal for worker command only.
* @since 2.0.2
*/
public function run($repeat)
{
return $this->runWorker(function (callable $canContinue) use ($repeat) {
$worker = new \GearmanWorker();
$worker->addServer($this->host, $this->port);
$worker->addFunction($this->channel, function (\GearmanJob $payload) {
list($ttr, $message) = explode(';', $payload->workload(), 2);
$this->handleMessage($payload->handle(), $message, $ttr, 1);
});
$worker->setTimeout($repeat ? 1000 : 1);
while ($canContinue()) {
$result = $worker->work();
if (!$result && !$repeat) {
break;
}
}
});
}
/**
* @inheritdoc
*/
protected function pushMessage($message, $ttr, $delay, $priority)
{
if ($delay) {
throw new NotSupportedException('Delayed work is not supported in the driver.');
}
switch ($priority) {
case 'high':
return $this->getClient()->doHighBackground($this->channel, "$ttr;$message");
case 'low':
return $this->getClient()->doLowBackground($this->channel, "$ttr;$message");
default:
return $this->getClient()->doBackground($this->channel, "$ttr;$message");
}
}
/**
* @inheritdoc
*/
public function status($id)
{
$status = $this->getClient()->jobStatus($id);
if ($status[0] && !$status[1]) {
return self::STATUS_WAITING;
}
if ($status[0] && $status[1]) {
return self::STATUS_RESERVED;
}
return self::STATUS_DONE;
}
/**
* @return \GearmanClient
*/
protected function getClient()
{
if (!$this->_client) {
$this->_client = new \GearmanClient();
$this->_client->addServer($this->host, $this->port);
}
return $this->_client;
}
private $_client;
}

View File

@ -0,0 +1,105 @@
<?php
/**
* @link https://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license https://www.yiiframework.com/license/
*/
namespace yii\queue\redis;
use yii\console\Exception;
use yii\queue\cli\Command as CliCommand;
use yii\queue\cli\InfoAction;
/**
* Manages application redis-queue.
*
* @author Roman Zhuravlev <zhuravljov@gmail.com>
*/
class Command extends CliCommand
{
/**
* @var Queue
*/
public $queue;
/**
* @var string
*/
public $defaultAction = 'info';
/**
* @inheritdoc
*/
public function actions()
{
return [
'info' => InfoAction::class,
];
}
/**
* @inheritdoc
*/
protected function isWorkerAction($actionID)
{
return in_array($actionID, ['run', 'listen'], true);
}
/**
* Runs all jobs from redis-queue.
* It can be used as cron job.
*
* @return null|int exit code.
*/
public function actionRun()
{
return $this->queue->run(false);
}
/**
* Listens redis-queue and runs new jobs.
* It can be used as daemon process.
*
* @param int $timeout number of seconds to wait a job.
* @throws Exception when params are invalid.
* @return null|int exit code.
*/
public function actionListen($timeout = 3)
{
if (!is_numeric($timeout)) {
throw new Exception('Timeout must be numeric.');
}
if ($timeout < 1) {
throw new Exception('Timeout must be greater than zero.');
}
return $this->queue->run(true, $timeout);
}
/**
* Clears the queue.
*
* @since 2.0.1
*/
public function actionClear()
{
if ($this->confirm('Are you sure?')) {
$this->queue->clear();
}
}
/**
* Removes a job by id.
*
* @param int $id
* @throws Exception when the job is not found.
* @since 2.0.1
*/
public function actionRemove($id)
{
if (!$this->queue->remove($id)) {
throw new Exception('The job is not found.');
}
}
}

View File

@ -0,0 +1,54 @@
<?php
/**
* @link https://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license https://www.yiiframework.com/license/
*/
namespace yii\queue\redis;
use yii\helpers\Console;
use yii\queue\cli\Action;
/**
* Info about queue status.
*
* @deprecated Will be removed in 3.0. Use yii\queue\cli\InfoAction instead.
*
* @author Roman Zhuravlev <zhuravljov@gmail.com>
*/
class InfoAction extends Action
{
/**
* @var Queue
*/
public $queue;
/**
* Info about queue status.
*/
public function run()
{
$prefix = $this->queue->channel;
$waiting = $this->queue->redis->llen("$prefix.waiting");
$delayed = $this->queue->redis->zcount("$prefix.delayed", '-inf', '+inf');
$reserved = $this->queue->redis->zcount("$prefix.reserved", '-inf', '+inf');
$total = $this->queue->redis->get("$prefix.message_id");
$done = $total - $waiting - $delayed - $reserved;
Console::output($this->format('Jobs', Console::FG_GREEN));
Console::stdout($this->format('- waiting: ', Console::FG_YELLOW));
Console::output($waiting);
Console::stdout($this->format('- delayed: ', Console::FG_YELLOW));
Console::output($delayed);
Console::stdout($this->format('- reserved: ', Console::FG_YELLOW));
Console::output($reserved);
Console::stdout($this->format('- done: ', Console::FG_YELLOW));
Console::output($done);
}
}

View File

@ -0,0 +1,224 @@
<?php
/**
* @link https://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license https://www.yiiframework.com/license/
*/
namespace yii\queue\redis;
use yii\base\InvalidArgumentException;
use yii\base\NotSupportedException;
use yii\di\Instance;
use yii\queue\cli\Queue as CliQueue;
use yii\queue\interfaces\StatisticsProviderInterface;
use yii\redis\Connection;
/**
* Redis Queue.
*
* @property-read StatisticsProvider $statisticsProvider
*
* @author Roman Zhuravlev <zhuravljov@gmail.com>
*/
class Queue extends CliQueue implements StatisticsProviderInterface
{
/**
* @var Connection|array|string
*/
public $redis = 'redis';
/**
* @var string
*/
public $channel = 'queue';
/**
* @var string command class name
*/
public $commandClass = Command::class;
/**
* @inheritdoc
*/
public function init()
{
parent::init();
$this->redis = Instance::ensure($this->redis, Connection::class);
}
/**
* Listens queue and runs each job.
*
* @param bool $repeat whether to continue listening when queue is empty.
* @param int $timeout number of seconds to wait for next message.
* @return null|int exit code.
* @internal for worker command only.
* @since 2.0.2
*/
public function run($repeat, $timeout = 0)
{
return $this->runWorker(function (callable $canContinue) use ($repeat, $timeout) {
while ($canContinue()) {
if (($payload = $this->reserve($timeout)) !== null) {
list($id, $message, $ttr, $attempt) = $payload;
if ($this->handleMessage($id, $message, $ttr, $attempt)) {
$this->delete($id);
}
} elseif (!$repeat) {
break;
}
}
});
}
/**
* @inheritdoc
*/
public function status($id)
{
if (!is_numeric($id) || $id <= 0) {
throw new InvalidArgumentException("Unknown message ID: $id.");
}
if ($this->redis->hexists("$this->channel.attempts", $id)) {
return self::STATUS_RESERVED;
}
if ($this->redis->hexists("$this->channel.messages", $id)) {
return self::STATUS_WAITING;
}
return self::STATUS_DONE;
}
/**
* Clears the queue.
*
* @since 2.0.1
*/
public function clear()
{
while (!$this->redis->set("$this->channel.moving_lock", true, 'NX')) {
usleep(10000);
}
$this->redis->executeCommand('DEL', $this->redis->keys("$this->channel.*"));
}
/**
* Removes a job by ID.
*
* @param int $id of a job
* @return bool
* @since 2.0.1
*/
public function remove($id)
{
while (!$this->redis->set("$this->channel.moving_lock", true, 'NX', 'EX', 1)) {
usleep(10000);
}
if ($this->redis->hdel("$this->channel.messages", $id)) {
$this->redis->zrem("$this->channel.delayed", $id);
$this->redis->zrem("$this->channel.reserved", $id);
$this->redis->lrem("$this->channel.waiting", 0, $id);
$this->redis->hdel("$this->channel.attempts", $id);
return true;
}
return false;
}
/**
* @param int $timeout timeout
* @return array|null payload
*/
protected function reserve($timeout)
{
// Moves delayed and reserved jobs into waiting list with lock for one second
if ($this->redis->set("$this->channel.moving_lock", true, 'NX', 'EX', 1)) {
$this->moveExpired("$this->channel.delayed");
$this->moveExpired("$this->channel.reserved");
}
// Find a new waiting message
$id = null;
if (!$timeout) {
$id = $this->redis->rpop("$this->channel.waiting");
} elseif ($result = $this->redis->brpop("$this->channel.waiting", $timeout)) {
$id = $result[1];
}
if (!$id) {
return null;
}
$payload = $this->redis->hget("$this->channel.messages", $id);
if (null === $payload) {
return null;
}
list($ttr, $message) = explode(';', $payload, 2);
$this->redis->zadd("$this->channel.reserved", time() + $ttr, $id);
$attempt = $this->redis->hincrby("$this->channel.attempts", $id, 1);
return [$id, $message, $ttr, $attempt];
}
/**
* @param string $from
*/
protected function moveExpired($from)
{
$now = time();
if ($expired = $this->redis->zrevrangebyscore($from, $now, '-inf')) {
foreach ($expired as $id) {
$this->redis->rpush("$this->channel.waiting", $id);
}
$this->redis->zremrangebyscore($from, '-inf', $now);
}
}
/**
* Deletes message by ID.
*
* @param int $id of a message
*/
protected function delete($id)
{
$this->redis->zrem("$this->channel.reserved", $id);
$this->redis->hdel("$this->channel.attempts", $id);
$this->redis->hdel("$this->channel.messages", $id);
}
/**
* @inheritdoc
*/
protected function pushMessage($message, $ttr, $delay, $priority)
{
if ($priority !== null) {
throw new NotSupportedException('Job priority is not supported in the driver.');
}
$id = $this->redis->incr("$this->channel.message_id");
$this->redis->hset("$this->channel.messages", $id, "$ttr;$message");
if (!$delay) {
$this->redis->lpush("$this->channel.waiting", $id);
} else {
$this->redis->zadd("$this->channel.delayed", time() + $delay, $id);
}
return $id;
}
private $_statistcsProvider;
/**
* @return StatisticsProvider
*/
public function getStatisticsProvider()
{
if (!$this->_statistcsProvider) {
$this->_statistcsProvider = new StatisticsProvider($this);
}
return $this->_statistcsProvider;
}
}

View File

@ -0,0 +1,74 @@
<?php
/**
* @link https://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license https://www.yiiframework.com/license/
*/
namespace yii\queue\redis;
use yii\base\BaseObject;
use yii\queue\interfaces\DelayedCountInterface;
use yii\queue\interfaces\DoneCountInterface;
use yii\queue\interfaces\ReservedCountInterface;
use yii\queue\interfaces\WaitingCountInterface;
/**
* Statistics Provider
*
* @author Kalmer Kaurson <kalmerkaurson@gmail.com>
*/
class StatisticsProvider extends BaseObject implements DoneCountInterface, WaitingCountInterface, DelayedCountInterface, ReservedCountInterface
{
/**
* @var Queue
*/
protected $queue;
public function __construct(Queue $queue, $config = [])
{
$this->queue = $queue;
parent::__construct($config);
}
/**
* @inheritdoc
*/
public function getWaitingCount()
{
$prefix = $this->queue->channel;
return $this->queue->redis->llen("$prefix.waiting");
}
/**
* @inheritdoc
*/
public function getDelayedCount()
{
$prefix = $this->queue->channel;
return $this->queue->redis->zcount("$prefix.delayed", '-inf', '+inf');
}
/**
* @inheritdoc
*/
public function getReservedCount()
{
$prefix = $this->queue->channel;
return $this->queue->redis->zcount("$prefix.reserved", '-inf', '+inf');
}
/**
* @inheritdoc
*/
public function getDoneCount()
{
$prefix = $this->queue->channel;
$waiting = $this->getWaitingCount();
$delayed = $this->getDelayedCount();
$reserved = $this->getReservedCount();
$total = $this->queue->redis->get("$prefix.message_id");
return $total - $waiting - $delayed - $reserved;
}
}

View File

@ -0,0 +1,78 @@
<?php
/**
* @link https://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license https://www.yiiframework.com/license/
*/
namespace yii\queue\sqs;
use yii\console\Exception;
use yii\queue\cli\Command as CliCommand;
/**
* Manages application aws sqs-queue.
*
* @author Max Kozlovsky <kozlovskymaxim@gmail.com>
* @author Manoj Malviya <manojm@girnarsoft.com>
*/
class Command extends CliCommand
{
/**
* @var Queue
*/
public $queue;
/**
* Runs all jobs from sqs.
* It can be used as cron job.
*
* @return null|int exit code.
*/
public function actionRun()
{
return $this->queue->run(false);
}
/**
* Listens sqs and runs new jobs.
* It can be used as demon process.
*
* @param int $timeout number of seconds to sleep before next reading of the queue.
* @throws Exception when params are invalid.
* @return null|int exit code.
*/
public function actionListen($timeout = 3)
{
if (!is_numeric($timeout)) {
throw new Exception('Timeout must be numeric.');
}
$timeout = (int) $timeout;
if ($timeout < 1 || $timeout > 20) {
throw new Exception('Timeout must be between 1 and 20');
}
return $this->queue->run(true, $timeout);
}
/**
* Clears the queue.
*/
public function actionClear()
{
if ($this->confirm('Are you sure?')) {
$this->queue->clear();
$this->stdout("Queue has been cleared.\n");
}
}
/**
* @inheritdoc
*/
protected function isWorkerAction($actionID)
{
return in_array($actionID, ['run', 'listen']);
}
}

View File

@ -0,0 +1,244 @@
<?php
/**
* @link https://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license https://www.yiiframework.com/license/
*/
namespace yii\queue\sqs;
use Aws\Credentials\CredentialProvider;
use Aws\Sqs\SqsClient;
use yii\base\NotSupportedException;
use yii\queue\cli\Queue as CliQueue;
use yii\queue\serializers\JsonSerializer;
/**
* SQS Queue.
*
* @author Max Kozlovsky <kozlovskymaxim@gmail.com>
* @author Manoj Malviya <manojm@girnarsoft.com>
*/
class Queue extends CliQueue
{
/**
* The SQS url.
* @var string
*/
public $url;
/**
* aws access key.
* @var string|null
*/
public $key;
/**
* aws secret.
* @var string|null
*/
public $secret;
/**
* region where queue is hosted.
* @var string
*/
public $region = '';
/**
* API version.
* @var string
*/
public $version = 'latest';
/**
* Message Group ID for FIFO queues.
* @var string
* @since 2.2.1
*/
public $messageGroupId = 'default';
/**
* @var string command class name
* @inheritdoc
*/
public $commandClass = Command::class;
/**
* Json serializer by default.
* @inheritdoc
*/
public $serializer = JsonSerializer::class;
/**
* @var SqsClient
*/
private $_client;
/**
* @inheritdoc
*/
public function init()
{
parent::init();
}
/**
* Listens queue and runs each job.
*
* @param bool $repeat whether to continue listening when queue is empty.
* @param int $timeout number of seconds to sleep before next iteration.
* @return null|int exit code.
* @internal for worker command only
*/
public function run($repeat, $timeout = 0)
{
return $this->runWorker(function (callable $canContinue) use ($repeat, $timeout) {
while ($canContinue()) {
if (($payload = $this->reserve($timeout)) !== null) {
$id = $payload['MessageId'];
$message = $payload['Body'];
$ttr = (int) $payload['MessageAttributes']['TTR']['StringValue'];
$attempt = (int) $payload['Attributes']['ApproximateReceiveCount'];
if ($this->handleMessage($id, $message, $ttr, $attempt)) {
$this->delete($payload);
}
} elseif (!$repeat) {
break;
}
}
});
}
/**
* Gets a single message from SQS queue and sets the visibility to reserve message.
*
* @param int $timeout number of seconds for long polling. Must be between 0 and 20.
* @return null|array payload.
*/
protected function reserve($timeout)
{
$response = $this->getClient()->receiveMessage([
'QueueUrl' => $this->url,
'AttributeNames' => ['ApproximateReceiveCount'],
'MessageAttributeNames' => ['TTR'],
'MaxNumberOfMessages' => 1,
'VisibilityTimeout' => $this->ttr,
'WaitTimeSeconds' => $timeout,
]);
if (!$response['Messages']) {
return null;
}
$payload = reset($response['Messages']);
$ttr = (int) $payload['MessageAttributes']['TTR']['StringValue'];
if ($ttr != $this->ttr) {
$this->getClient()->changeMessageVisibility([
'QueueUrl' => $this->url,
'ReceiptHandle' => $payload['ReceiptHandle'],
'VisibilityTimeout' => $ttr,
]);
}
return $payload;
}
/**
* Deletes the message after successfully handling.
*
* @param array $payload
*/
protected function delete($payload)
{
$this->getClient()->deleteMessage([
'QueueUrl' => $this->url,
'ReceiptHandle' => $payload['ReceiptHandle'],
]);
}
/**
* Clears the queue.
*/
public function clear()
{
$this->getClient()->purgeQueue([
'QueueUrl' => $this->url,
]);
}
/**
* @inheritdoc
*/
public function status($id)
{
throw new NotSupportedException('Status is not supported in the driver.');
}
/**
* Provides public access for `handleMessage`
*
* @param $id string
* @param $message string
* @param $ttr int
* @param $attempt int
* @return bool
* @since 2.2.1
*/
public function handle($id, $message, $ttr, $attempt)
{
return $this->handleMessage($id, $message, $ttr, $attempt);
}
/**
* @inheritdoc
*/
protected function pushMessage($message, $ttr, $delay, $priority)
{
if ($priority) {
throw new NotSupportedException('Priority is not supported in this driver');
}
$request = [
'QueueUrl' => $this->url,
'MessageBody' => $message,
'DelaySeconds' => $delay,
'MessageAttributes' => [
'TTR' => [
'DataType' => 'Number',
'StringValue' => (string) $ttr,
],
],
];
if (substr($this->url, -5) === '.fifo') {
$request['MessageGroupId'] = $this->messageGroupId;
$request['MessageDeduplicationId'] = hash('sha256', $message);
}
$response = $this->getClient()->sendMessage($request);
return $response['MessageId'];
}
/**
* @return \Aws\Sqs\SqsClient
*/
protected function getClient()
{
if ($this->_client) {
return $this->_client;
}
if ($this->key !== null && $this->secret !== null) {
$credentials = [
'key' => $this->key,
'secret' => $this->secret,
];
} else {
// use default provider if no key and secret passed
//see - https://docs.aws.amazon.com/aws-sdk-php/v3/guide/guide/credentials.html#credential-profiles
$credentials = CredentialProvider::defaultProvider();
}
$this->_client = new SqsClient([
'credentials' => $credentials,
'region' => $this->region,
'version' => $this->version,
]);
return $this->_client;
}
}

View File

@ -0,0 +1,66 @@
<?php
/**
* @link https://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license https://www.yiiframework.com/license/
*/
namespace yii\queue\stomp;
use yii\console\Exception;
use yii\queue\cli\Command as CliCommand;
/**
* Manages application stomp-queue.
*
* @author Sergey Vershinin <versh23@gmail.com>
* @since 2.3.0
*/
class Command extends CliCommand
{
/**
* @var Queue
*/
public $queue;
/**
* @inheritdoc
*/
protected function isWorkerAction($actionID)
{
return in_array($actionID, ['run', 'listen']);
}
/**
* Runs all jobs from stomp-queue.
* It can be used as cron job.
*
* @return null|int exit code.
*/
public function actionRun()
{
return $this->queue->run(false);
}
/**
* Listens stomp-queue and runs new jobs.
* It can be used as daemon process.
*
* @param int $timeout number of seconds to wait a job.
* @throws Exception when params are invalid.
* @return null|int exit code.
*/
public function actionListen($timeout = 3)
{
if (!is_numeric($timeout)) {
throw new Exception('Timeout must be numeric.');
}
if ($timeout < 1) {
throw new Exception('Timeout must be greater that zero.');
}
return $this->queue->run(true, $timeout);
}
}

View File

@ -0,0 +1,292 @@
<?php
/**
* @link https://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license https://www.yiiframework.com/license/
*/
namespace yii\queue\stomp;
use Enqueue\Stomp\StompConnectionFactory;
use Enqueue\Stomp\StompContext;
use Enqueue\Stomp\StompMessage;
use yii\base\Application as BaseApp;
use yii\base\Event;
use yii\base\NotSupportedException;
use yii\queue\cli\Queue as CliQueue;
/**
* Stomp Queue.
* @author Sergey Vershinin <versh23@gmail.com>
* @since 2.3.0
*/
class Queue extends CliQueue
{
const ATTEMPT = 'yii-attempt';
const TTR = 'yii-ttr';
/**
* The message queue broker's host.
*
* @var string|null
*/
public $host;
/**
* The message queue broker's port.
*
* @var string|null
*/
public $port;
/**
* This is user which is used to login on the broker.
*
* @var string|null
*/
public $user;
/**
* This is password which is used to login on the broker.
*
* @var string|null
*/
public $password;
/**
* Sets an fixed vhostname, which will be passed on connect as header['host'].
*
* @var string|null
*/
public $vhost;
/**
* @var int
*/
public $bufferSize;
/**
* @var int
*/
public $connectionTimeout;
/**
* Perform request synchronously.
* @var bool
*/
public $sync;
/**
* The connection will be established as later as possible if set true.
*
* @var bool|null
*/
public $lazy;
/**
* Defines whether secure connection should be used or not.
*
* @var bool|null
*/
public $sslOn;
/**
* The queue used to consume messages from.
*
* @var string
*/
public $queueName = 'stomp_queue';
/**
* The property contains a command class which used in cli.
*
* @var string command class name
*/
public $commandClass = Command::class;
/**
* Set the read timeout.
* @var int
*/
public $readTimeOut = 0;
/**
* @var StompContext
*/
protected $context;
/**
* @inheritdoc
*/
public function init()
{
parent::init();
Event::on(BaseApp::class, BaseApp::EVENT_AFTER_REQUEST, function () {
$this->close();
});
}
/**
* Opens connection.
*/
protected function open()
{
if ($this->context) {
return;
}
$config = [
'host' => $this->host,
'port' => $this->port,
'login' => $this->user,
'password' => $this->password,
'vhost' => $this->vhost,
'buffer_size' => $this->bufferSize,
'connection_timeout' => $this->connectionTimeout,
'sync' => $this->sync,
'lazy' => $this->lazy,
'ssl_on' => $this->sslOn,
];
$config = array_filter($config, function ($value) {
return null !== $value;
});
$factory = new StompConnectionFactory($config);
$this->context = $factory->createContext();
}
/**
* Listens queue and runs each job.
*
* @param $repeat
* @param int $timeout
* @return int|null
*/
public function run($repeat, $timeout = 0)
{
return $this->runWorker(function (callable $canContinue) use ($repeat, $timeout) {
$this->open();
$queue = $this->createQueue($this->queueName);
$consumer = $this->context->createConsumer($queue);
while ($canContinue()) {
if ($message = ($this->readTimeOut > 0 ? $consumer->receive($this->readTimeOut) : $consumer->receiveNoWait())) {
$messageId = $message->getMessageId();
if (!$messageId) {
$message = $this->setMessageId($message);
}
if ($message->isRedelivered()) {
$consumer->acknowledge($message);
$this->redeliver($message);
continue;
}
$ttr = $message->getProperty(self::TTR, $this->ttr);
$attempt = $message->getProperty(self::ATTEMPT, 1);
if ($this->handleMessage($message->getMessageId(), $message->getBody(), $ttr, $attempt)) {
$consumer->acknowledge($message);
} else {
$consumer->acknowledge($message);
$this->redeliver($message);
}
} elseif (!$repeat) {
break;
} elseif ($timeout) {
sleep($timeout);
$this->context->getStomp()->getConnection()->sendAlive();
}
}
});
}
/**
* @param StompMessage $message
* @return StompMessage
* @throws \Interop\Queue\Exception
*/
protected function setMessageId(StompMessage $message)
{
$message->setMessageId(uniqid('', true));
return $message;
}
/**
* @inheritdoc
* @throws \Interop\Queue\Exception
* @throws NotSupportedException
*/
protected function pushMessage($message, $ttr, $delay, $priority)
{
$this->open();
$queue = $this->createQueue($this->queueName);
$message = $this->context->createMessage($message);
$message = $this->setMessageId($message);
$message->setPersistent(true);
$message->setProperty(self::ATTEMPT, 1);
$message->setProperty(self::TTR, $ttr);
$producer = $this->context->createProducer();
if ($delay) {
throw new NotSupportedException('Delayed work is not supported in the driver.');
}
if ($priority) {
throw new NotSupportedException('Job priority is not supported in the driver.');
}
$producer->send($queue, $message);
return $message->getMessageId();
}
/**
* Closes connection.
*/
protected function close()
{
if (!$this->context) {
return;
}
$this->context->close();
$this->context = null;
}
/**
* @inheritdoc
* @throws NotSupportedException
*/
public function status($id)
{
throw new NotSupportedException('Status is not supported in the driver.');
}
/**
* @param StompMessage $message
* @throws \Interop\Queue\Exception
*/
protected function redeliver(StompMessage $message)
{
$attempt = $message->getProperty(self::ATTEMPT, 1);
$newMessage = $this->context->createMessage($message->getBody(), $message->getProperties(), $message->getHeaders());
$newMessage->setProperty(self::ATTEMPT, ++$attempt);
$this->context->createProducer()->send(
$this->createQueue($this->queueName),
$newMessage
);
}
/**
* @param $name
* @return \Enqueue\Stomp\StompDestination
*/
private function createQueue($name)
{
$queue = $this->context->createQueue($name);
$queue->setDurable(true);
$queue->setAutoDelete(false);
$queue->setExclusive(false);
return $queue;
}
}

View File

@ -0,0 +1,102 @@
<?php
/**
* @link https://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license https://www.yiiframework.com/license/
*/
namespace yii\queue\sync;
use Yii;
use yii\base\Application;
use yii\base\InvalidArgumentException;
use yii\queue\Queue as BaseQueue;
/**
* Sync Queue.
*
* @author Roman Zhuravlev <zhuravljov@gmail.com>
*/
class Queue extends BaseQueue
{
/**
* @var bool
*/
public $handle = false;
/**
* @var array of payloads
*/
private $payloads = [];
/**
* @var int last pushed ID
*/
private $pushedId = 0;
/**
* @var int started ID
*/
private $startedId = 0;
/**
* @var int last finished ID
*/
private $finishedId = 0;
/**
* @inheritdoc
*/
public function init()
{
parent::init();
if ($this->handle) {
Yii::$app->on(Application::EVENT_AFTER_REQUEST, function () {
ob_start();
$this->run();
ob_end_clean();
});
}
}
/**
* Runs all jobs from queue.
*/
public function run()
{
while (($payload = array_shift($this->payloads)) !== null) {
list($ttr, $message) = $payload;
$this->startedId = $this->finishedId + 1;
$this->handleMessage($this->startedId, $message, $ttr, 1);
$this->finishedId = $this->startedId;
$this->startedId = 0;
}
}
/**
* @inheritdoc
*/
protected function pushMessage($message, $ttr, $delay, $priority)
{
array_push($this->payloads, [$ttr, $message]);
return ++$this->pushedId;
}
/**
* @inheritdoc
*/
public function status($id)
{
if (!is_int($id) || $id <= 0 || $id > $this->pushedId) {
throw new InvalidArgumentException("Unknown messages ID: $id.");
}
if ($id <= $this->finishedId) {
return self::STATUS_DONE;
}
if ($id === $this->startedId) {
return self::STATUS_RESERVED;
}
return self::STATUS_WAITING;
}
}

View File

@ -0,0 +1,162 @@
<?php
/**
* @link https://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license https://www.yiiframework.com/license/
*/
namespace yii\queue\gii;
use Yii;
use yii\base\BaseObject;
use yii\gii\CodeFile;
use yii\queue\JobInterface;
use yii\queue\RetryableJobInterface;
/**
* This generator will generate a job.
*
* @author Roman Zhuravlev <zhuravljov@gmail.com>
*/
class Generator extends \yii\gii\Generator
{
public $jobClass;
public $properties;
public $retryable = false;
public $ns = 'app\jobs';
public $baseClass = BaseObject::class;
/**
* @inheritdoc
*/
public function getName()
{
return 'Job Generator';
}
/**
* @inheritdoc
*/
public function getDescription()
{
return 'This generator generates a Job class for the queue.';
}
/**
* @inheritdoc
*/
public function rules()
{
return array_merge(parent::rules(), [
[['jobClass', 'properties', 'ns', 'baseClass'], 'trim'],
[['jobClass', 'ns', 'baseClass'], 'required'],
['jobClass', 'match', 'pattern' => '/^\w+$/', 'message' => 'Only word characters are allowed.'],
['jobClass', 'validateJobClass'],
['properties', 'match', 'pattern' => '/^[a-z_][a-z0-9_,\\s]*$/i', 'message' => 'Must be valid class properties.'],
['retryable', 'boolean'],
['ns', 'validateNamespace'],
['baseClass', 'validateClass'],
]);
}
/**
* @inheritdoc
*/
public function attributeLabels()
{
return array_merge(parent::attributeLabels(), [
'jobClass' => 'Job Class',
'properties' => 'Job Properties',
'retryable' => 'Retryable Job',
'ns' => 'Namespace',
'baseClass' => 'Base Class',
]);
}
/**
* @inheritdoc
*/
public function hints()
{
return array_merge(parent::hints(), [
'jobClass' => 'This is the name of the Job class to be generated, e.g., <code>SomeJob</code>.',
'properties' => 'Job object property names. Separate multiple properties with commas or spaces, e.g., <code>prop1, prop2</code>.',
'retryable' => 'Job object will implement <code>RetryableJobInterface</code> interface.',
'ns' => 'This is the namespace of the Job class to be generated.',
'baseClass' => 'This is the class that the new Job class will extend from.',
]);
}
/**
* @inheritdoc
*/
public function stickyAttributes()
{
return array_merge(parent::stickyAttributes(), ['ns', 'baseClass']);
}
/**
* @inheritdoc
*/
public function requiredTemplates()
{
return ['job.php'];
}
/**
* @inheritdoc
*/
public function generate()
{
$params = [];
$params['jobClass'] = $this->jobClass;
$params['ns'] = $this->ns;
$params['baseClass'] = '\\' . ltrim($this->baseClass, '\\');
$params['interfaces'] = [];
if (!$this->retryable) {
if (!is_a($this->baseClass, JobInterface::class, true)) {
$params['interfaces'][] = '\\' . JobInterface::class;
}
} else {
if (!is_a($this->baseClass, RetryableJobInterface::class, true)) {
$params['interfaces'][] = '\\' . RetryableJobInterface::class;
}
}
$params['properties'] = array_unique(preg_split('/[\s,]+/', $this->properties, -1, PREG_SPLIT_NO_EMPTY));
$jobFile = new CodeFile(
Yii::getAlias('@' . str_replace('\\', '/', $this->ns)) . '/' . $this->jobClass . '.php',
$this->render('job.php', $params)
);
return [$jobFile];
}
/**
* Validates the job class.
*
* @param string $attribute job attribute name.
*/
public function validateJobClass($attribute)
{
if ($this->isReservedKeyword($this->$attribute)) {
$this->addError($attribute, 'Class name cannot be a reserved PHP keyword.');
}
}
/**
* Validates the namespace.
*
* @param string $attribute Namespace attribute name.
*/
public function validateNamespace($attribute)
{
$value = $this->$attribute;
$value = ltrim($value, '\\');
$path = Yii::getAlias('@' . str_replace('\\', '/', $value), false);
if ($path === false) {
$this->addError($attribute, 'Namespace must be associated with an existing directory.');
}
}
}

View File

@ -0,0 +1,56 @@
<?php
/**
* @var \yii\web\View $this
* @var \yii\queue\gii\Generator $generator
* @var string $jobClass
* @var string $$ns
* @var string $baseClass
* @var string[] $interfaces
* @var string[] $properties
*/
if ($interfaces) {
$implements = 'implements ' . implode(', ', $interfaces);
} else {
$implements = '';
}
echo "<?php\n";
?>
namespace <?= $ns ?>;
/**
* Class <?= $jobClass ?>.
*/
class <?= $jobClass ?> extends <?= $baseClass ?> <?= $implements ?>
{
<?php foreach ($properties as $property): ?>
public $<?= $property ?>;
<?php endforeach; ?>
/**
* @inheritdoc
*/
public function execute($queue)
{
}
<?php if ($generator->retryable): ?>
/**
* @inheritdoc
*/
public function getTtr()
{
return 60;
}
/**
* @inheritdoc
*/
public function canRetry($attempt, $error)
{
return $attempt < 3;
}
<?php endif; ?>
}

View File

@ -0,0 +1,12 @@
<?php
/**
* @var \yii\web\View $this
* @var \yii\widgets\ActiveForm $form
* @var \yii\queue\gii\Generator $generator
*/
?>
<?= $form->field($generator, 'jobClass')->textInput(['autofocus' => true]) ?>
<?= $form->field($generator, 'properties') ?>
<?= $form->field($generator, 'retryable')->checkbox() ?>
<?= $form->field($generator, 'ns') ?>
<?= $form->field($generator, 'baseClass') ?>

View File

@ -0,0 +1,21 @@
<?php
/**
* @link https://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license https://www.yiiframework.com/license/
*/
namespace yii\queue\interfaces;
/**
* Delayed Count Interface
*
* @author Kalmer Kaurson <kalmerkaurson@gmail.com>
*/
interface DelayedCountInterface
{
/**
* @return int
*/
public function getDelayedCount();
}

View File

@ -0,0 +1,21 @@
<?php
/**
* @link https://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license https://www.yiiframework.com/license/
*/
namespace yii\queue\interfaces;
/**
* Done Count Interface
*
* @author Kalmer Kaurson <kalmerkaurson@gmail.com>
*/
interface DoneCountInterface
{
/**
* @return int
*/
public function getDoneCount();
}

View File

@ -0,0 +1,21 @@
<?php
/**
* @link https://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license https://www.yiiframework.com/license/
*/
namespace yii\queue\interfaces;
/**
* Reserved Count Interface
*
* @author Kalmer Kaurson <kalmerkaurson@gmail.com>
*/
interface ReservedCountInterface
{
/**
* @return int
*/
public function getReservedCount();
}

View File

@ -0,0 +1,21 @@
<?php
/**
* @link https://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license https://www.yiiframework.com/license/
*/
namespace yii\queue\interfaces;
/**
* Statistics Provider Interface
*
* @author Kalmer Kaurson <kalmerkaurson@gmail.com>
*/
interface StatisticsProviderInterface
{
/**
* @return int
*/
public function getStatisticsProvider();
}

View File

@ -0,0 +1,21 @@
<?php
/**
* @link https://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license https://www.yiiframework.com/license/
*/
namespace yii\queue\interfaces;
/**
* Waiting Count Interface
*
* @author Kalmer Kaurson <kalmerkaurson@gmail.com>
*/
interface WaitingCountInterface
{
/**
* @return int
*/
public function getWaitingCount();
}

View File

@ -0,0 +1,37 @@
<?php
/**
* @link https://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license https://www.yiiframework.com/license/
*/
namespace yii\queue\serializers;
use yii\base\BaseObject;
/**
* Igbinary Serializer.
*
* It uses an alternative serializer available via PECL extension which produces
* more compact data chunks significantly faster that native PHP one.
*
* @author xutl <xutongle@gmail.com>
*/
class IgbinarySerializer extends BaseObject implements SerializerInterface
{
/**
* @inheritdoc
*/
public function serialize($job)
{
return igbinary_serialize($job);
}
/**
* @inheritdoc
*/
public function unserialize($serialized)
{
return igbinary_unserialize($serialized);
}
}

View File

@ -0,0 +1,109 @@
<?php
/**
* @link https://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license https://www.yiiframework.com/license/
*/
namespace yii\queue\serializers;
use Yii;
use yii\base\BaseObject;
use yii\base\InvalidConfigException;
use yii\helpers\Json;
/**
* Json Serializer.
*
* @author Roman Zhuravlev <zhuravljov@gmail.com>
*/
class JsonSerializer extends BaseObject implements SerializerInterface
{
/**
* @var string
*/
public $classKey = 'class';
/**
* @var int
*/
public $options = 0;
/**
* @inheritdoc
*/
public function serialize($job)
{
return Json::encode($this->toArray($job), $this->options);
}
/**
* @inheritdoc
*/
public function unserialize($serialized)
{
return $this->fromArray(Json::decode($serialized));
}
/**
* @param mixed $data
* @return array|mixed
* @throws InvalidConfigException
*/
protected function toArray($data)
{
if (is_object($data)) {
$result = [$this->classKey => get_class($data)];
foreach (get_object_vars($data) as $property => $value) {
if ($property === $this->classKey) {
throw new InvalidConfigException("Object cannot contain $this->classKey property.");
}
$result[$property] = $this->toArray($value);
}
return $result;
}
if (is_array($data)) {
$result = [];
foreach ($data as $key => $value) {
if ($key === $this->classKey) {
throw new InvalidConfigException("Array cannot contain $this->classKey key.");
}
$result[$key] = $this->toArray($value);
}
return $result;
}
return $data;
}
/**
* @param array $data
* @return mixed
*/
protected function fromArray($data)
{
if (!is_array($data)) {
return $data;
}
if (!isset($data[$this->classKey])) {
$result = [];
foreach ($data as $key => $value) {
$result[$key] = $this->fromArray($value);
}
return $result;
}
$config = ['class' => $data[$this->classKey]];
unset($data[$this->classKey]);
foreach ($data as $property => $value) {
$config[$property] = $this->fromArray($value);
}
return Yii::createObject($config);
}
}

View File

@ -0,0 +1,34 @@
<?php
/**
* @link https://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license https://www.yiiframework.com/license/
*/
namespace yii\queue\serializers;
use yii\base\BaseObject;
/**
* Php Serializer.
*
* @author Roman Zhuravlev <zhuravljov@gmail.com>
*/
class PhpSerializer extends BaseObject implements SerializerInterface
{
/**
* @inheritdoc
*/
public function serialize($job)
{
return serialize($job);
}
/**
* @inheritdoc
*/
public function unserialize($serialized)
{
return unserialize($serialized);
}
}

View File

@ -0,0 +1,19 @@
<?php
/**
* @link https://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license https://www.yiiframework.com/license/
*/
namespace yii\queue\serializers;
/**
* Interface Serializer.
*
* @deprecated Will be removed in 3.0. Use SerializerInterface instead of Serializer.
*
* @author Roman Zhuravlev <zhuravljov@gmail.com>
*/
interface Serializer extends SerializerInterface
{
}

View File

@ -0,0 +1,30 @@
<?php
/**
* @link https://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license https://www.yiiframework.com/license/
*/
namespace yii\queue\serializers;
use yii\queue\JobInterface;
/**
* Serializer Interface.
*
* @author Roman Zhuravlev <zhuravljov@gmail.com>
*/
interface SerializerInterface
{
/**
* @param JobInterface|mixed $job
* @return string
*/
public function serialize($job);
/**
* @param string $serialized
* @return JobInterface
*/
public function unserialize($serialized);
}