├── .gitignore
├── .travis.yml
├── CHANGELOG.md
├── README.md
├── composer.json
├── composer.lock
├── phpunit.xml
├── ruleset.xml
├── sonar-project.properties
├── src
├── Behaviors
│ ├── ActiveRecordDeferredEventBehavior.php
│ ├── ActiveRecordDeferredEventHandler.php
│ ├── ActiveRecordDeferredEventRoutingBehavior.php
│ ├── DeferredEventBehavior.php
│ ├── DeferredEventHandler.php
│ ├── DeferredEventInterface.php
│ ├── DeferredEventRoutingBehavior.php
│ └── DeferredEventTrait.php
├── Console
│ └── Controller.php
├── Event.php
├── Job.php
├── ProcessRunner.php
├── Queue.php
├── Queues
│ ├── DbQueue.php
│ ├── DummyQueue.php
│ ├── MemoryQueue.php
│ ├── MultipleQueue.php
│ ├── RedisQueue.php
│ └── SqsQueue.php
├── Strategies
│ ├── RandomStrategy.php
│ ├── Strategy.php
│ └── WeightedStrategy.php
├── Web
│ ├── Controller.php
│ └── WorkerController.php
└── Worker
│ └── Controller.php
└── tests
├── Behaviors
├── ActiveRecordDeferredEventBehaviorTest.php
├── ActiveRecordDeferredEventHandlerTest.php
├── ActiveRecordDeferredEventRoutingBehaviorTest.php
├── DeferredEventBehaviorTest.php
├── DeferredEventHandlerTest.php
└── DeferredEventRoutingBehaviorTest.php
├── EventTest.php
├── ProcessRunnerTest.php
├── QueueTest.php
├── Queues
├── DbQueueTest.php
├── MemoryQueueTest.php
├── MultipleQueueTest.php
├── RedisQueueTest.php
└── SqsQueueTest.php
├── TestCase.php
└── bootstrap.php
/.gitignore:
--------------------------------------------------------------------------------
1 | /vendor/
2 | .idea/*
3 | reports
4 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: php
2 | dist: trusty
3 | php:
4 | - 7.2
5 | - 7.3
6 |
7 | addons:
8 | sonarcloud:
9 | organization: "urbanindo"
10 | token:
11 | secure: "SoALFI3R7htTg/uwUNZ1f1xqxCBLUgm1XVg9lK0+SUBBZiLkBdLhPT5AvSjaqOP0nh9WSrJHzgKkzsJHikEw+OGXPkKoLVFC18S2TtO6/rh14Wpdqg1n6+wZ2Y5cTmylRgQ+6b91ERMkpJXLXTnwG0slkQIc485FB9Ch/zV6ssElT9rMsC4JgM8hIj3flcCz1QPVsc/LlvAql70irgeB8hbjO4EmCrpHTqVy55hHcKwSWIQaUOXYTFJBuDCXpmQKT1+taT+6PLQmAskFaUU6hhH3GDTvtQiJqIv7FymRYQIDPUHYsCt/Kp64F+BBXBUT71/Q7qeIbQP+6XK7ofI0zfJ4NYmGp8z94ygNzjS6xZdBWOgEBPbGkoV1YB/kopOmlYak+6emkrzIY0ybQhjr4wrFyDdG/apwjmgLhjG2Hnbl1qoEQ4OimHDQdBqHb2Eh1SFfwrYyLM06aNe3/4hawacUgja3n05XE/+Bw52vVW6eGx5elNjC2yUqjSFvch7mhkKxWrt+2mKgA9gHxbN++uaeR/Ty3xBE+cHeKynb/YYFfXukEee/68HsdXR10qkdfQGQVI7i38F6o9+D+twNQXuqt93OPrS/4EZdpIjjTl1qm3xNYRWcUJwMiH1KdZb4mGiRodvrqAqkcYaraT8b0kzzxGiy6kzNQU6zD8QtSvY="
12 |
13 | services:
14 | - mysql
15 | - redis-server
16 |
17 | cache:
18 | directories:
19 | - $HOME/.composer/cache/files
20 |
21 | script:
22 | - ./vendor/bin/phpunit --coverage-clover 'reports/clover.xml'
23 |
24 | before_script:
25 | #MySQL database init
26 | - mysql -uroot -e "CREATE DATABASE IF NOT EXISTS test;"
27 | - mysql -uroot -e "CREATE USER 'test'@'localhost' IDENTIFIED BY 'test';"
28 | - mysql -uroot -e "GRANT ALL PRIVILEGES ON test.* TO 'test'@'localhost' IDENTIFIED BY 'test';"
29 |
30 | install:
31 | - travis_retry composer self-update && composer --version
32 | - travis_retry composer install --prefer-dist --no-interaction
33 |
34 | after_success:
35 | - sonar-scanner
36 | - bash <(curl -s https://codecov.io/bash)
37 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Change Log
2 | All notable changes to this project will be documented in this file.
3 |
4 | ## 1.3.0
5 | - Added implementation for DbQueue and RedisQueue.
6 | - Added events for queue.
7 | - Added `purge` method for queue.
8 | - Refactoring code.
9 |
10 | ## 1.2.3
11 | - Passing scenario for model and active record.
12 |
13 | ## 1.2.2
14 | - Removing deprecated method `call_user_method`.
15 |
16 | ## 1.2.1
17 | - Added `DeferredEventTrait`
18 |
19 | ## 1.2.0
20 | - Added tests
21 | - Added `MemoryQueue`, `DeferredEventHandler`, and `ActiveRecordDeferredEventHandler`.
22 |
23 | ## 1.0.1
24 |
25 | ### Changed
26 | - Refactoring controller classes to Web, Console, and Worker.
27 |
28 | ### Added
29 | - Added Web endpoint for posting queue.
30 |
31 | ## 2015-02-25
32 |
33 | ### Changed
34 | - Shorten `postJob`, `getJob`, `deleteJob`, `runJob` method name to `post`,
35 | `fetch`, `delete`, `run`.
36 |
37 | ### Fixed
38 | - Error when closure is not returning boolean variable.
39 |
40 | ### Added
41 | - DeferredEventBehavior for deferring event handler to the task queue.
42 | - Peek and Purging in the console command.
43 | - MultipleQueue for multiple queue and priority queue.
44 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Queue Component for Yii2
2 |
3 | This provides queue component for Yii2.
4 |
5 | [](https://packagist.org/packages/urbanindo/yii2-queue)
6 | [](https://packagist.org/packages/urbanindo/yii2-queue)
7 | [](https://packagist.org/packages/urbanindo/yii2-queue)
8 | [](https://travis-ci.org/urbanindo/yii2-queue)
9 | [](https://codecov.io/gh/urbanindo/yii2-queue)
10 |
11 | ## Requirements
12 | You need [PCNT extension](http://php.net/manual/en/book.pcntl.php) enabled to run listener
13 |
14 | ## Installation
15 |
16 | The preferred way to install this extension is through [composer](http://getcomposer.org/download/).
17 |
18 | Either run
19 |
20 | ```
21 | php composer.phar require --prefer-dist urbanindo/yii2-queue "*"
22 | ```
23 |
24 | or add
25 |
26 | ```
27 | "urbanindo/yii2-queue": "*"
28 | ```
29 |
30 | to the require section of your `composer.json` file.
31 |
32 | To use Redis queue or RabbitMQ, you have to add `yiisoft/yii2-redis:*` or
33 | `videlalvaro/php-amqplib: 2.5.*` respectively.
34 |
35 | ## Setting Up
36 |
37 | After the installation, first step is to set the console controller.
38 |
39 | ```php
40 | return [
41 | // ...
42 | 'controllerMap' => [
43 | 'queue' => [
44 | 'class' => 'UrbanIndo\Yii2\Queue\Console\Controller',
45 | //'sleepTimeout' => 1
46 | ],
47 | ],
48 | ];
49 | ```
50 |
51 | For the task worker, set a new module, e.g. `task` and declare it in the config.
52 |
53 | ```php
54 | 'modules' => [
55 | 'task' => [
56 | 'class' => 'app\modules\task\Module',
57 | ]
58 | ]
59 | ```
60 |
61 | And then set the queue component. Don't forget to set the module name that runs
62 | the task in the component. For example, queue using AWS SQS
63 |
64 | ```php
65 | 'components' => [
66 | 'queue' => [
67 | 'class' => 'UrbanIndo\Yii2\Queue\Queues\SqsQueue',
68 | 'module' => 'task',
69 | 'url' => 'https://sqs.ap-southeast-1.amazonaws.com/123456789012/queue',
70 | 'config' => [
71 | 'credentials' => [
72 | 'key' => 'AKIA1234567890123456',
73 | 'secret' => '1234567890123456789012345678901234567890'
74 | ],
75 | 'region' => 'ap-southeast-1',
76 | 'version' => 'latest'
77 | ]
78 | ]
79 | ]
80 | ]
81 | ```
82 |
83 | Or using Database queue
84 |
85 | ```php
86 | 'components' => [
87 | 'db' => [
88 | // the db component
89 | ],
90 | 'queue' => [
91 | 'class' => 'UrbanIndo\Yii2\Queue\Queues\DbQueue',
92 | 'db' => 'db',
93 | 'tableName' => 'queue',
94 | 'module' => 'task',
95 | // sleep for 10 seconds if there's no item in the queue (to save CPU)
96 | 'waitSecondsIfNoQueue' => 10,
97 | ]
98 | ]
99 | ```
100 |
101 | ## Usage
102 |
103 | ### Creating A Worker
104 |
105 | Creating a worker is just the same with creating console or web controller.
106 | In the task module create a controller that extends `UrbanIndo\Yii2\Queue\Worker\Controller`
107 |
108 | e.g.
109 |
110 | ```php
111 | class FooController extends UrbanIndo\Yii2\Queue\Worker\Controller
112 | {
113 | public function actionBar($param1, $param2)
114 | {
115 | echo $param1;
116 | }
117 | }
118 | ```
119 |
120 | To prevent the job got deleted from the queue, for example when the job is not
121 | completed, return `false` in the action. The job will be run again the next
122 | chance.
123 |
124 | e.g.
125 |
126 | ```php
127 | class FooController extends UrbanIndo\Yii2\Queue\Worker\Controller
128 | {
129 | public function actionBar($param1, $param2)
130 | {
131 | try {
132 | // do some stuff
133 | } catch (\Exception $ex) {
134 | \Yii::error('Ouch something just happened');
135 | return false;
136 | }
137 | }
138 | }
139 | ```
140 |
141 | ### Running The Listener
142 |
143 | To run the listener, run the console that set in the above config. If the
144 | controller mapped as `queue` then run.
145 |
146 | ```
147 | yii queue/listen
148 | ```
149 |
150 | ### Posting A Job
151 |
152 | To post a job from source code, put something like this.
153 |
154 | ```php
155 | use UrbanIndo\Yii2\Queue\Job;
156 |
157 | $route = 'foo/bar';
158 | $data = ['param1' => 'foo', 'param2' => 'bar'];
159 | Yii::$app->queue->post(new Job(['route' => $route, 'data' => $data]));
160 | ```
161 |
162 | Job can also be posted from the console. The data in the second parameter is in
163 | JSON string.
164 |
165 | ```
166 | yii queue/post 'foo/bar' '{"param1": "foo", "param2": "bar"}'
167 | ```
168 |
169 | Job can also be posted as anonymous function. Be careful using this.
170 |
171 | ```php
172 | Yii::$app->queue->post(new Job(function() {
173 | echo 'Hello World!';
174 | }));
175 | ```
176 |
177 | ### Deferred Event
178 |
179 | In this queue, there is a feature called **Deferred Event**. Basically using this
180 | feature, we can defer a process executed after a certain event using queue.
181 |
182 | To use this, add behavior in a component and implement the defined event handler.
183 |
184 | ```php
185 | public function behaviors()
186 | {
187 | return [
188 | [
189 | 'class' => \UrbanIndo\Yii2\Queue\Behaviors\DeferredEventBehavior::class,
190 | 'events' => [
191 | self::EVENT_AFTER_VALIDATE => 'deferAfterValidate',
192 | ]
193 | ]
194 | ];
195 | }
196 |
197 | public function deferAfterValidate()
198 | {
199 | // do something here
200 | }
201 | ```
202 |
203 | **NOTE**
204 | Due to reducing the message size, the `$event` object that usually passed when
205 | triggered the event will not be passed to the deferred event. Also, the object
206 | in which the method invoked is merely a clone object, so it won't have the
207 | behavior and the event attached in the original object.
208 |
209 | As for `ActiveRecord` class, since the object can not be passed due to limitation
210 | of SuperClosure in serializing PDO (I personally think that's bad too), the
211 | behavior should use `\UrbanIndo\Yii2\Queue\Behaviors\ActiveRecordDeferredEventBehavior`
212 | instead. The difference is in the object in which the deferred event handler
213 | invoked.
214 |
215 | Since we can not pass the original object, the invoking object will be re-fetched
216 | from the table using the primary key. And for the `afterDelete` event, since
217 | the respective row is not in the table anymore, the invoking object is a new
218 | object whose attributes are assigned from the attributes of the original object.
219 |
220 | ### Web End Point
221 |
222 | We can use web endpoint to use the queue by adding `\UrbanIndo\Yii2\Queue\Web\Controller`
223 | to the controller map.
224 |
225 | For example
226 |
227 | ```php
228 | 'controllerMap' => [
229 | 'queue' => [
230 | /* @var $queue UrbanIndo\Yii2\Queue\Web\Controller */
231 | 'class' => 'UrbanIndo\Yii2\Queue\Web\Controller'
232 | ]
233 | ],
234 | ```
235 |
236 | To post this use
237 |
238 | ```
239 | curl -XPOST http://example.com/queue/post --data route='test/test' --data data='{"data":"data"}'
240 | ```
241 |
242 | To limit the access to the controller, we can use `\yii\filters\AccessControl` filter.
243 |
244 | For example to filter by IP address, we can use something like this.
245 |
246 | ```php
247 | 'controllerMap' => [
248 | 'queue' => [
249 | /* @var $queue UrbanIndo\Yii2\Queue\Web\Controller */
250 | 'class' => 'UrbanIndo\Yii2\Queue\Web\Controller',
251 | 'as access' => [
252 | 'class' => '\yii\filters\AccessControl',
253 | 'rules' => [
254 | [
255 | 'allow' => true,
256 | 'ips' => [
257 | '127.0.0.1'
258 | ]
259 | ]
260 | ]
261 | ]
262 | ]
263 | ],
264 | ```
265 |
266 | ## Testing
267 |
268 | To run the tests, in the root directory execute below.
269 |
270 | ```
271 | ./vendor/bin/phpunit
272 | ```
273 |
274 | ## Road Map
275 |
276 | - Add more queue provider such as MemCache, IronMQ, RabbitMQ.
277 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "urbanindo/yii2-queue",
3 | "description": "Queue component for Yii2",
4 | "license": "MIT",
5 | "authors": [
6 | {
7 | "name": "Petra Barus",
8 | "email": "petra.barus@gmail.com"
9 | }
10 | ],
11 | "minimum-stability": "stable",
12 | "require": {
13 | "php": "^7.2",
14 | "ext-pcntl": "*",
15 | "yiisoft/yii2": ">=2.0.15",
16 | "aws/aws-sdk-php": ">=2.4",
17 | "symfony/process": ">=2.4",
18 | "jeremeamia/SuperClosure": ">=2.0"
19 | },
20 | "require-dev": {
21 | "phpunit/phpunit": "^6.5",
22 | "phpunit/dbunit": "^3.0",
23 | "phpunit/php-code-coverage": "^5.2",
24 | "fzaninotto/faker": "dev-master",
25 | "flow/jsonpath": "dev-master",
26 | "yiisoft/yii2-coding-standards": "*",
27 | "yiisoft/yii2-redis": "*",
28 | "videlalvaro/php-amqplib": "2.5.*",
29 | "squizlabs/php_codesniffer": "^3.3"
30 | },
31 | "autoload": {
32 | "psr-4": {
33 | "UrbanIndo\\Yii2\\Queue\\": "src/"
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | ./tests
6 |
7 |
8 |
--------------------------------------------------------------------------------
/ruleset.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | UrbanIndo Coding Standard
4 |
5 | */tests/*
6 | */test/*
7 | */data/*
8 | */config/*
9 | */views/*
10 | */migrations/*
11 | */messages/id/*
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 | */migrations/*
58 |
59 |
60 | */migrations/*
61 |
62 |
63 |
--------------------------------------------------------------------------------
/sonar-project.properties:
--------------------------------------------------------------------------------
1 | sonar.projectKey=urbanindo_yii2-queue
2 | sonar.sources=./src
--------------------------------------------------------------------------------
/src/Behaviors/ActiveRecordDeferredEventBehavior.php:
--------------------------------------------------------------------------------
1 |
5 | * @since 2015.02.25
6 | */
7 |
8 | namespace UrbanIndo\Yii2\Queue\Behaviors;
9 |
10 | use yii\db\ActiveRecord;
11 |
12 | /**
13 | * ActiveRecordDeferredEventBehavior is deferred event behavior handler for
14 | * ActiveRecord.
15 | *
16 | * Due to SuperClosure limitation to serialize classes like PDO, this will
17 | * only pass the class, primary key, or attributes to the closure. The closure
18 | * then will operate on the object that refetched from the database from primary
19 | * key or object whose attribute repopulated in case of EVENT_AFTER_DELETE.
20 | *
21 | * @property-read ActiveRecord $owner the owner.
22 | *
23 | * @author Petra Barus
24 | * @since 2015.02.25
25 | */
26 | class ActiveRecordDeferredEventBehavior extends DeferredEventBehavior
27 | {
28 |
29 | /**
30 | * @var array
31 | */
32 | public $events = [
33 | ActiveRecord::EVENT_AFTER_INSERT,
34 | ActiveRecord::EVENT_AFTER_UPDATE,
35 | ActiveRecord::EVENT_AFTER_DELETE,
36 | ];
37 |
38 | /**
39 | * Default events that usually use deferred.
40 | * @return array
41 | */
42 | public static function getDefaultEvents()
43 | {
44 | return [
45 | ActiveRecord::EVENT_AFTER_INSERT,
46 | ActiveRecord::EVENT_AFTER_UPDATE,
47 | ActiveRecord::EVENT_AFTER_DELETE,
48 | ];
49 | }
50 |
51 | /**
52 | * Call the behavior owner to handle the deferred event.
53 | *
54 | * Since there is a limitation on the SuperClosure on PDO, the closure will
55 | * operate the object that is re-fetched from the database using primary key.
56 | * In the case of the after delete, since the row is already deleted from
57 | * the table, the closure will operate from the object whose attributes.
58 | * @param \yii\base\Event $event The event.
59 | * @return void
60 | * @throws \Exception Exception.
61 | */
62 | public function postDeferredEvent(\yii\base\Event $event)
63 | {
64 | $class = get_class($this->owner);
65 | $eventName = $event->name;
66 | $handlers = ($this->_hasEventHandlers) ? $this->events : false;
67 | if (isset($this->_serializer)) {
68 | $serializer = $this->_serializer;
69 | } else {
70 | $serializer = null;
71 | }
72 | $scenario = $this->owner->scenario;
73 | if ($eventName == ActiveRecord::EVENT_AFTER_DELETE) {
74 | $attributes = $this->owner->getAttributes();
75 | $this->queue->post(new \UrbanIndo\Yii2\Queue\Job([
76 | 'route' => function () use ($class, $attributes, $handlers, $eventName, $serializer, $scenario) {
77 | $object = \Yii::createObject($class);
78 | /* @var $object ActiveRecord */
79 | $object->scenario = $scenario;
80 | $object->setAttributes($attributes, false);
81 | if ($handlers) {
82 | $handler = $handlers[$eventName];
83 | if ($serializer !== null) {
84 | try {
85 | $unserialized = $serializer->unserialize($handler);
86 | $unserialized($object);
87 | } catch (\Exception $exc) {
88 | return call_user_func([$object, $handler]);
89 | }
90 | } else {
91 | return call_user_func([$object, $handler]);
92 | }
93 | } else if ($object instanceof DeferredEventInterface) {
94 | /* @var $object DeferredEventInterface */
95 | return $object->handleDeferredEvent($eventName);
96 | } else {
97 | throw new \Exception('Model is not instance of DeferredEventInterface');
98 | }
99 | }
100 | ]));
101 | } else {
102 | $pk = $this->owner->getPrimaryKey();
103 | $this->queue->post(new \UrbanIndo\Yii2\Queue\Job([
104 | 'route' => function () use ($class, $pk, $handlers, $eventName, $serializer, $scenario) {
105 | $object = $class::findOne($pk);
106 | if ($object === null) {
107 | throw new \Exception("Model #{$pk} is not found");
108 | }
109 | $object->scenario = $scenario;
110 | if ($handlers) {
111 | $handler = $handlers[$eventName];
112 | if ($serializer !== null) {
113 | try {
114 | $unserialized = $serializer->unserialize($handler);
115 | $unserialized($object);
116 | } catch (\Exception $exc) {
117 | return call_user_func([$object, $handler]);
118 | }
119 | } else {
120 | return call_user_func([$object, $handler]);
121 | }
122 | } else if ($object instanceof DeferredEventInterface) {
123 | /* @var $object DeferredEventInterface */
124 | return $object->handleDeferredEvent($eventName);
125 | } else {
126 | throw new \Exception('Model is not instance of DeferredEventInterface');
127 | }
128 | }
129 | ]));
130 | }
131 | }
132 | }
133 |
--------------------------------------------------------------------------------
/src/Behaviors/ActiveRecordDeferredEventHandler.php:
--------------------------------------------------------------------------------
1 |
5 | */
6 |
7 | namespace UrbanIndo\Yii2\Queue\Behaviors;
8 |
9 | use yii\db\ActiveRecord;
10 |
11 | /**
12 | * DeferredActiveRecordEventHandler is deferred event behavior handler for
13 | * ActiveRecord.
14 | *
15 | * Due to SuperClosure limitation to serialize classes like PDO, this will
16 | * only pass the class, primary key, or attributes to the closure. The closure
17 | * then will operate on the object that refetched from the database from primary
18 | * key or object whose attribute repopulated in case of EVENT_AFTER_DELETE.
19 | *
20 | * @property-read ActiveRecord $owner the owner.
21 | *
22 | * @author Petra Barus
23 | */
24 | abstract class ActiveRecordDeferredEventHandler extends DeferredEventHandler
25 | {
26 |
27 | /**
28 | * @param \yii\base\Event $event The event to handle.
29 | * @return void
30 | * @throws \Exception Exception.
31 | */
32 | public function deferEvent(\yii\base\Event $event)
33 | {
34 | $class = get_class($this->owner);
35 | $pk = $this->owner->getPrimaryKey();
36 | $attributes = $this->owner->getAttributes();
37 | $scenario = $this->owner->scenario;
38 | $eventName = $event->name;
39 | $queue = $this->queue;
40 | $handler = clone $this;
41 | $handler->queue = null;
42 | $handler->owner = null;
43 | /* @var $queue Queue */
44 | if ($eventName == ActiveRecord::EVENT_AFTER_DELETE) {
45 | $queue->post(new \UrbanIndo\Yii2\Queue\Job([
46 | 'route' => function () use ($class, $pk, $attributes, $handler, $eventName, $scenario) {
47 | $object = \Yii::createObject($class);
48 | /* @var $object ActiveRecord */
49 | $object->setAttributes($attributes, false);
50 | $object->scenario = $scenario;
51 | $handler->handleEvent($object);
52 | }
53 | ]));
54 |
55 | } else {
56 | $queue->post(new \UrbanIndo\Yii2\Queue\Job([
57 | 'route' => function () use ($class, $pk, $attributes, $handler, $eventName, $scenario) {
58 | $object = $class::findOne($pk);
59 | if ($object === null) {
60 | throw new \Exception('Model is not found');
61 | }
62 | $object->scenario = $scenario;
63 | /* @var $object ActiveRecord */
64 | $handler->handleEvent($object);
65 | }
66 | ]));
67 | }
68 |
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/Behaviors/ActiveRecordDeferredEventRoutingBehavior.php:
--------------------------------------------------------------------------------
1 |
6 | * @since 2015.02.25
7 | */
8 |
9 | namespace UrbanIndo\Yii2\Queue\Behaviors;
10 |
11 | use yii\db\ActiveRecord;
12 |
13 | /**
14 | * ActiveRecordDeferredRoutingBehavior provides matching between controller in
15 | * task worker with the appropriate event.
16 | *
17 | * @property-read ActiveRecord $owner the owner.
18 | *
19 | * @author Petra Barus
20 | * @since 2015.02.25
21 | */
22 | class ActiveRecordDeferredEventRoutingBehavior extends DeferredEventRoutingBehavior
23 | {
24 |
25 | /**
26 | * The attribute name.
27 | * @var string
28 | */
29 | public $pkAttribute = 'id';
30 |
31 | /**
32 | * Whether to add the primary key to the data.
33 | * @var boolean
34 | */
35 | public $addPkToData = true;
36 |
37 | /**
38 | * @param \yii\base\Event $event The event to handle.
39 | * @return void
40 | */
41 | public function routeEvent(\yii\base\Event $event)
42 | {
43 | /* @var $owner ActiveRecord */
44 |
45 | $eventName = $event->name;
46 | $handler = $this->events[$eventName];
47 | if (is_callable($handler)) {
48 | $handler = call_user_func($handler, $this->owner);
49 | } else if ($this->addPkToData) {
50 | $pk = $this->owner->getPrimaryKey();
51 | if (is_array($pk)) {
52 | $handler = array_merge($handler, $pk);
53 | } else {
54 | $handler[$this->pkAttribute] = $pk;
55 | }
56 | }
57 | $route = $handler[0];
58 | unset($handler[0]);
59 | $handler['scenario'] = $this->owner->getScenario();
60 | $data = $handler;
61 | $this->queue->post(new \UrbanIndo\Yii2\Queue\Job([
62 | 'route' => $route,
63 | 'data' => $data
64 | ]));
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/Behaviors/DeferredEventBehavior.php:
--------------------------------------------------------------------------------
1 |
6 | * @since 2015.02.25
7 | */
8 |
9 | namespace UrbanIndo\Yii2\Queue\Behaviors;
10 |
11 | use UrbanIndo\Yii2\Queue\Queue;
12 |
13 | /**
14 | * DeferredEventBehavior post a deferred code on event call.
15 | *
16 | * To use this, attach the behavior on the model, and implements the
17 | * DeferredEventInterface.
18 | *
19 | * NOTE: Due to some limitation on the superclosure, the model shouldn't have
20 | * unserializable class instances such as PDO etc.
21 | *
22 | * @property-read DeferredEventInterface $owner the owner of this behavior.
23 | *
24 | * @author Petra Barus
25 | * @since 2015.02.25
26 | */
27 | class DeferredEventBehavior extends \yii\base\Behavior
28 | {
29 |
30 | /**
31 | * The queue that post the deferred event.
32 | * @var string|array|Queue
33 | */
34 | public $queue = 'queue';
35 |
36 | /**
37 | * List events that handled by the behavior.
38 | *
39 | * This has two formats. The first one is "index",
40 | *
41 | * [self::EVENT_AFTER_SAVE, EVENT_AFTER_VALIDATE]]
42 | *
43 | * and the second one is "key=>value". e.g.
44 | *
45 | * [
46 | * self::EVENT_AFTER_SAVE => 'deferAfterSave',
47 | * self::EVENT_AFTER_VALIDATE => 'deferAfterValidate'
48 | * ]
49 | *
50 | * For the first one, the object should implement DeferredEventInterface.
51 | * As for the second one, the handler will use the respective method of the
52 | * event.
53 | *
54 | * e.g.
55 | *
56 | * [
57 | * self::EVENT_AFTER_SAVE => 'deferAfterSave',
58 | * self::EVENT_AFTER_VALIDATE => 'deferAfterValidate'
59 | * ]
60 | *
61 | * the model should implement
62 | *
63 | * public function deferAfterSave(){
64 | * }
65 | *
66 | * Note that the method doesn't receive $event just like any event handler.
67 | * This is because the $event object can be too large for the queue.
68 | * Also note that object that run the method is a clone.
69 | *
70 | * @var array
71 | */
72 | public $events = [];
73 |
74 | /**
75 | * Whether each events has its own event handler in the owner.
76 | * @var boolean
77 | */
78 | protected $_hasEventHandlers = false;
79 |
80 | /**
81 | * Whether has serialized event.handler.
82 | * @var \SuperClosure\Serializer
83 | */
84 | protected $_serializer;
85 |
86 | /**
87 | * Declares event handlers for the [[owner]]'s events.
88 | * @return array
89 | */
90 | public function events()
91 | {
92 | parent::events();
93 | if (!$this->_hasEventHandlers) {
94 | return array_fill_keys($this->events, 'postDeferredEvent');
95 | } else {
96 | return array_fill_keys(
97 | array_keys($this->events),
98 | 'postDeferredEvent'
99 | );
100 | }
101 | }
102 |
103 | /**
104 | * Initialize the queue.
105 | * @return void
106 | */
107 | public function init()
108 | {
109 | parent::init();
110 | $this->queue = \yii\di\Instance::ensure($this->queue, Queue::className());
111 | $this->_hasEventHandlers = !\yii\helpers\ArrayHelper::isIndexed(
112 | $this->events,
113 | true
114 | );
115 | if ($this->_hasEventHandlers) {
116 | foreach ($this->events as $attr => $handler) {
117 | if (is_callable($handler)) {
118 | if (!isset($this->_serializer)) {
119 | $this->_serializer = new \SuperClosure\Serializer();
120 | }
121 | $this->events[$attr] = $this->_serializer->serialize($handler);
122 | }
123 | }
124 | }
125 | }
126 |
127 | /**
128 | * Call the behavior owner to handle the deferred event.
129 | * @param \yii\base\Event $event The event to process.
130 | * @return void
131 | * @throws \Exception When the sender is not DeferredEventInterface.
132 | */
133 | public function postDeferredEvent(\yii\base\Event $event)
134 | {
135 | $object = clone $this->owner;
136 | if (!$this->_hasEventHandlers && !$object instanceof DeferredEventInterface) {
137 | throw new \Exception('Model is not instance of DeferredEventInterface');
138 | }
139 | $handlers = ($this->_hasEventHandlers) ? $this->events : false;
140 | $eventName = $event->name;
141 | if (isset($this->_serializer)) {
142 | $serializer = $this->_serializer;
143 | } else {
144 | $serializer = null;
145 | }
146 | $this->queue->post(new \UrbanIndo\Yii2\Queue\Job([
147 | 'route' => function () use ($object, $eventName, $handlers, $serializer) {
148 | if ($handlers) {
149 | $handler = $handlers[$eventName];
150 | if ($serializer !== null) {
151 | try {
152 | $unserialized = $serializer->unserialize($handler);
153 | $unserialized($object);
154 | } catch (\Exception $exc) {
155 | return call_user_func([$object, $handler]);
156 | }
157 | } else {
158 | return call_user_func([$object, $handler]);
159 | }
160 | } else if ($object instanceof DeferredEventInterface) {
161 | /* @var $object DeferredEventInterface */
162 | return $object->handleDeferredEvent($eventName);
163 | } else {
164 | throw new \Exception(
165 | "Model doesn't have handlers for the event or is not instance of DeferredEventInterface"
166 | );
167 | }
168 | }
169 | ]));
170 | }
171 | }
172 |
--------------------------------------------------------------------------------
/src/Behaviors/DeferredEventHandler.php:
--------------------------------------------------------------------------------
1 |
6 | */
7 |
8 | namespace UrbanIndo\Yii2\Queue\Behaviors;
9 |
10 | use Yii;
11 | use UrbanIndo\Yii2\Queue\Queue;
12 |
13 | /**
14 | * DeferredEventHandler handles the event inside the behavior instance, instead
15 | * of inside the model.
16 | *
17 | * @author Petra Barus
18 | */
19 | abstract class DeferredEventHandler extends \yii\base\Behavior
20 | {
21 |
22 | /**
23 | * The queue that post the deferred event.
24 | * @var \UrbanIndo\Yii2\Queue\Queue
25 | */
26 | public $queue = 'queue';
27 |
28 | /**
29 | * Declares the events of the object that is being handled.
30 | *
31 | * @var array
32 | */
33 | public $events = [];
34 |
35 | /**
36 | * @return void
37 | */
38 | public function init()
39 | {
40 | parent::init();
41 | $this->queue = \yii\di\Instance::ensure($this->queue, Queue::className());
42 | }
43 |
44 | /**
45 | * @inheritdoc
46 | * @return array
47 | */
48 | public function events()
49 | {
50 | return array_fill_keys($this->events, 'deferEvent');
51 | }
52 |
53 | /**
54 | * @param \yii\base\Event $event The event to handle.
55 | * @return array
56 | */
57 | public function deferEvent(\yii\base\Event $event)
58 | {
59 | $event; //unused
60 | $owner = clone $this->owner;
61 | $queue = $this->queue;
62 | $handler = clone $this;
63 | $handler->queue = null;
64 | $handler->owner = null;
65 | /* @var $queue Queue */
66 | $queue->post(new \UrbanIndo\Yii2\Queue\Job([
67 | 'route' => function () use ($owner, $handler) {
68 | return $handler->handleEvent($owner);
69 | }
70 | ]));
71 | }
72 |
73 | /**
74 | * Handle event.
75 | * @param mixed $owner The owner of the behavior.
76 | * @return void
77 | */
78 | abstract public function handleEvent($owner);
79 | }
80 |
--------------------------------------------------------------------------------
/src/Behaviors/DeferredEventInterface.php:
--------------------------------------------------------------------------------
1 |
6 | * @since 2015.02.25
7 | */
8 |
9 | namespace UrbanIndo\Yii2\Queue\Behaviors;
10 |
11 | /**
12 | * DeferredEventInterface provides method interface for handling the deferred
13 | * event.
14 | *
15 | * @author Petra Barus
16 | * @since 2015.02.25
17 | */
18 | interface DeferredEventInterface
19 | {
20 |
21 | /**
22 | * @param string $eventName The name of the event.
23 | * @return void
24 | */
25 | public function handleDeferredEvent($eventName);
26 | }
27 |
--------------------------------------------------------------------------------
/src/Behaviors/DeferredEventRoutingBehavior.php:
--------------------------------------------------------------------------------
1 |
5 | * @since 2015.02.25
6 | */
7 |
8 | namespace UrbanIndo\Yii2\Queue\Behaviors;
9 |
10 | use yii\db\ActiveRecord;
11 | use UrbanIndo\Yii2\Queue\Queue;
12 |
13 | /**
14 | * DeferredEventRoutingBehavior provides matching between controller in
15 | * task worker with the appropriate event.
16 | *
17 | * @property-read ActiveRecord $owner the owner.
18 | *
19 | * @author Petra Barus
20 | * @since 2015.02.25
21 | */
22 | class DeferredEventRoutingBehavior extends \yii\base\Behavior
23 | {
24 |
25 | /**
26 | * The queue that post the deferred event.
27 | * @var string|array|Queue
28 | */
29 | public $queue = 'queue';
30 |
31 | /**
32 | * List events that handler and the appropriate routing. The routing can be
33 | * generated via callable or array.
34 | *
35 | * e.g.
36 | *
37 | * [
38 | * self::EVENT_AFTER_SAVE => ['test/index'],
39 | * self::EVENT_AFTER_VALIDATE => ['test/index']
40 | * ]
41 | *
42 | * or
43 | *
44 | * [
45 | * self::EVENT_AFTER_SAVE => function($model) {
46 | * return ['test/index', 'id' => $model->id];
47 | * }
48 | * self::EVENT_AFTER_VALIDATE => function($model) {
49 | * return ['test/index', 'id' => $model->id];
50 | * }
51 | * ]
52 | *
53 | * @var array
54 | */
55 | public $events = [];
56 |
57 | /**
58 | * Initialize the queue.
59 | * @return void
60 | */
61 | public function init()
62 | {
63 | parent::init();
64 | $this->queue = \yii\di\Instance::ensure($this->queue, Queue::className());
65 | }
66 |
67 | /**
68 | * Declares event handlers for the [[owner]]'s events.
69 | * @return array
70 | */
71 | public function events()
72 | {
73 | parent::events();
74 | return array_fill_keys(array_keys($this->events), 'routeEvent');
75 | }
76 |
77 | /**
78 | * @param \yii\base\Event $event The event to handle.
79 | * @return void
80 | */
81 | public function routeEvent(\yii\base\Event $event)
82 | {
83 | $eventName = $event->name;
84 | $handler = $this->events[$eventName];
85 | if (is_callable($handler)) {
86 | $handler = call_user_func($handler, $this->owner);
87 | }
88 | $route = $handler[0];
89 | unset($handler[0]);
90 | $data = $handler;
91 | $this->queue->post(new \UrbanIndo\Yii2\Queue\Job([
92 | 'route' => $route,
93 | 'data' => $data
94 | ]));
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/src/Behaviors/DeferredEventTrait.php:
--------------------------------------------------------------------------------
1 |
5 | * @since 2015.06.12
6 | */
7 |
8 | namespace UrbanIndo\Yii2\Queue\Behaviors;
9 |
10 | use UrbanIndo\Yii2\Queue\Job;
11 | use UrbanIndo\Yii2\Queue\Queue;
12 |
13 | /**
14 | * ActiveRecordDeferredEventBehavior is deferred event function for active record.
15 | *
16 | * Due to SuperClosure limitation to serialize classes like PDO, this will
17 | * only pass the class, primary key.
18 | *
19 | * @author Petra Barus
20 | * @since 2015.06.12
21 | */
22 | trait DeferredEventTrait
23 | {
24 |
25 | /**
26 | * @return Queue
27 | */
28 | public function getQueue()
29 | {
30 | return \Yii::$app->queue;
31 | }
32 |
33 |
34 | /**
35 | * Defer event.
36 | *
37 | * To use this, attach the behavior and call
38 | *
39 | * $model->deferAction(function($model) {
40 | * $model->doSomething();
41 | * });
42 | *
43 | * @param \Closure $callback The callback.
44 | * @return void
45 | */
46 | public function deferAction(\Closure $callback)
47 | {
48 | if ($this instanceof ActiveRecord) {
49 | $job = $this->deferActiveRecordAction($callback);
50 | } else if ($this instanceof \yii\base\Model) {
51 | $job = $this->deferModelAction($callback);
52 | } else {
53 | $job = $this->deferObjectAction($callback);
54 | }
55 | $queue = $this->getQueue();
56 | $queue->post($job);
57 | }
58 |
59 | /**
60 | * @param \Closure $callback The callback.
61 | * @return array
62 | */
63 | private function serializeCallback(\Closure $callback)
64 | {
65 | $serializer = new \SuperClosure\Serializer();
66 | $serialized = $serializer->serialize($callback);
67 | return [$serializer, $serialized];
68 | }
69 |
70 | /**
71 | * @param \Closure $callback The callback.
72 | * @return array
73 | */
74 | private function deferActiveRecordAction(\Closure $callback)
75 | {
76 | $class = get_class($this);
77 | $pk = $this->getPrimaryKey();
78 | list($serializer, $serialized) = $this->serializeCallback($callback);
79 | return new Job([
80 | 'route' => function () use ($class, $pk, $serialized, $serializer) {
81 | $model = $class::findOne($pk);
82 | $unserialized = $serializer->unserialize($serialized);
83 | call_user_func($unserialized, $model);
84 | }
85 | ]);
86 | }
87 |
88 | /**
89 | * @param \Closure $callback The callback to defer.
90 | * @return Job
91 | */
92 | private function deferModelAction(\Closure $callback)
93 | {
94 | $class = get_class($this);
95 | $attributes = $this->getAttributes();
96 | list($serializer, $serialized) = $this->serializeCallback($callback);
97 | return new \UrbanIndo\Yii2\Queue\Job([
98 | 'route' => function () use ($class, $attributes, $serialized, $serializer) {
99 | $model = new $class;
100 | $model->setAttributes($attributes, false);
101 | $unserialized = $serializer->unserialize($serialized);
102 | call_user_func($unserialized, $model);
103 | }
104 | ]);
105 | }
106 |
107 | /**
108 | * @param \Closure $callback The callback.
109 | * @return Job
110 | */
111 | private function deferObject(\Closure $callback)
112 | {
113 | $object = $this;
114 | list($serializer, $serialized) = $this->serializeCallback($callback);
115 | return new \UrbanIndo\Yii2\Queue\Job([
116 | 'route' => function () use ($object, $serialized, $serializer) {
117 | $unserialized = $serializer->unserialize($serialized);
118 | call_user_func($unserialized, $object);
119 | }
120 | ]);
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/src/Console/Controller.php:
--------------------------------------------------------------------------------
1 |
6 | * @since 2015.02.24
7 | */
8 |
9 | namespace UrbanIndo\Yii2\Queue\Console;
10 |
11 | use Yii;
12 | use UrbanIndo\Yii2\Queue\Job;
13 | use UrbanIndo\Yii2\Queue\Queue;
14 | use yii\base\InvalidParamException;
15 |
16 | /**
17 | * QueueController handles console command for running the queue.
18 | *
19 | * To use the controller, update the controllerMap.
20 | *
21 | * return [
22 | * // ...
23 | * 'controllerMap' => [
24 | * 'queue' => 'UrbanIndo\Yii2\Queue\Console\QueueController'
25 | * ],
26 | * ];
27 | *
28 | * OR
29 | *
30 | * return [
31 | * // ...
32 | * 'controllerMap' => [
33 | * 'queue' => [
34 | * 'class' => 'UrbanIndo\Yii2\Queue\Console\QueueController',
35 | * 'sleepTimeout' => 1
36 | * ]
37 | * ],
38 | * ];
39 | *
40 | * To run
41 | *
42 | * yii queue
43 | *
44 | * @author Petra Barus
45 | * @since 2015.02.24
46 | */
47 | class Controller extends \yii\console\Controller
48 | {
49 |
50 | /**
51 | * @var string|array|Queue the name of the queue component. default to 'queue'.
52 | */
53 | public $queue = 'queue';
54 |
55 | /**
56 | * @var integer sleep timeout for infinite loop in second
57 | */
58 | public $sleepTimeout = 0;
59 |
60 | /**
61 | * @var string the name of the command.
62 | */
63 | private $_name = 'queue';
64 |
65 | /**
66 | * @return void
67 | */
68 | public function init()
69 | {
70 | parent::init();
71 |
72 | if (!is_numeric($this->sleepTimeout)) {
73 | throw new InvalidParamException('($sleepTimeout) must be an number');
74 | }
75 |
76 | if ($this->sleepTimeout < 0) {
77 | throw new InvalidParamException('($sleepTimeout) must be greater or equal than 0');
78 | }
79 |
80 | $this->queue = \yii\di\Instance::ensure($this->queue, Queue::className());
81 | $this->queue->processRunner->setScriptPath($this->getScriptPath());
82 | }
83 |
84 | /**
85 | * @inheritdoc
86 | * @param string $actionID The action id of the current request.
87 | * @return array the names of the options valid for the action
88 | */
89 | public function options($actionID)
90 | {
91 | return array_merge(parent::options($actionID), [
92 | 'queue'
93 | ]);
94 | }
95 |
96 | /**
97 | * Returns the script path.
98 | * @return string
99 | */
100 | protected function getScriptPath()
101 | {
102 | return realpath($_SERVER['argv'][0]);
103 | }
104 |
105 | /**
106 | * This will continuously run new subprocesses to fetch job from the queue.
107 | *
108 | * @param string $cwd The working directory.
109 | * @param integer $timeout Timeout.
110 | * @param array $env The environment to passed to the sub process.
111 | * The format for each element is 'KEY=VAL'.
112 | * @return void
113 | */
114 | public function actionListen(
115 | $cwd = null,
116 | $timeout = null, // moved to queue config
117 | array $env = null
118 | ) {
119 | $this->stdout("Listening to queue...\n");
120 |
121 | try {
122 | $this->queue->processRunner->listen($cwd,$timeout,$env);
123 | }
124 | catch (Exception $e) {
125 | Yii::error($e->getMessage(),__METHOD__);
126 | }
127 |
128 | $this->stdout("Exiting...\n");
129 | }
130 |
131 | /**
132 | * Fetch a job from the queue.
133 | * @return void
134 | */
135 | public function actionRun()
136 | {
137 | $job = $this->queue->fetch();
138 | if ($job !== false) {
139 | $this->stdout("Running job #: {$job->id}" . PHP_EOL);
140 | $this->queue->run($job);
141 | } else {
142 | $this->stdout("No job\n");
143 | }
144 | }
145 |
146 | /**
147 | * Post a job to the queue.
148 | * @param string $route The route.
149 | * @param string $data The data in JSON format.
150 | * @return void
151 | */
152 | public function actionPost($route, $data = '{}')
153 | {
154 | $this->stdout("Posting job to queue...\n");
155 | $job = $this->createJob($route, $data);
156 | $this->queue->post($job);
157 | }
158 |
159 | /**
160 | * Run a task without going to queue.
161 | *
162 | * This is useful to test the task controller.
163 | *
164 | * @param string $route The route.
165 | * @param string $data The data in JSON format.
166 | * @return void
167 | */
168 | public function actionRunTask($route, $data = '{}')
169 | {
170 | $this->stdout('Running task queue...');
171 | $job = $this->createJob($route, $data);
172 | $this->queue->run($job);
173 | }
174 |
175 | /**
176 | * @return void
177 | */
178 | public function actionTest()
179 | {
180 | $this->queue->post(new Job([
181 | 'route' => 'test/test',
182 | 'data' => ['halohalo' => 10, 'test2' => 100],
183 | ]));
184 | }
185 |
186 | /**
187 | * Create a job from route and data.
188 | *
189 | * @param string $route The route.
190 | * @param string $data The JSON data.
191 | * @return Job
192 | */
193 | protected function createJob($route, $data = '{}')
194 | {
195 | return new Job([
196 | 'route' => $route,
197 | 'data' => \yii\helpers\Json::decode($data),
198 | ]);
199 | }
200 |
201 | /**
202 | * Peek messages from queue that are still active.
203 | *
204 | * @param integer $count Number of messages to peek.
205 | * @return void
206 | */
207 | public function actionPeek($count = 1)
208 | {
209 | $this->stdout('Peeking queue...');
210 | for ($i = 0; $i < $count; $i++) {
211 | $job = $this->queue->fetch();
212 | if ($job !== false) {
213 | $this->stdout("Peeking job #: {$job->id}" . PHP_EOL);
214 | $this->stdout(\yii\helpers\Json::encode($job));
215 | }
216 | }
217 | }
218 |
219 | /**
220 | * Purging messages from queue that are still active.
221 | *
222 | * @param integer $count Number of messages to delete.
223 | * @return void
224 | */
225 | public function actionPurge($count = 1)
226 | {
227 | $this->stdout('Purging queue...');
228 | $queue = $this->queue;
229 | for ($i = 0; $i < $count; $i++) {
230 | $job = $queue->fetch();
231 | if ($job !== false) {
232 | $this->stdout("Purging job #: {$job->id}" . PHP_EOL);
233 | $queue->delete($job);
234 | }
235 | }
236 | }
237 |
238 | /**
239 | * Sets the name of the command. This should be overriden in the config.
240 | * @param string $value The value.
241 | * @return void
242 | */
243 | public function setName($value)
244 | {
245 | $this->_name = $value;
246 | }
247 | }
248 |
--------------------------------------------------------------------------------
/src/Event.php:
--------------------------------------------------------------------------------
1 |
5 | * @since 2016.01.16
6 | */
7 |
8 | namespace UrbanIndo\Yii2\Queue;
9 |
10 | /**
11 | * @author Petra Barus
12 | * @since 2016.01.16
13 | */
14 | class Event extends \yii\base\Event
15 | {
16 |
17 | /**
18 | * @var Job
19 | */
20 | public $job;
21 |
22 | /**
23 | * The return value after a job is being executed.
24 | * @var mixed
25 | */
26 | public $returnValue;
27 |
28 | /**
29 | * Whether the next process should continue or not.
30 | * @var boolean
31 | */
32 | public $isValid = true;
33 | }
34 |
--------------------------------------------------------------------------------
/src/Job.php:
--------------------------------------------------------------------------------
1 |
6 | * @since 2015.02.24
7 | */
8 |
9 | namespace UrbanIndo\Yii2\Queue;
10 |
11 | /**
12 | * Job is model for a job message.
13 | *
14 | * @author Petra Barus
15 | * @since 2015.02.24
16 | */
17 | class Job extends \yii\base\BaseObject
18 | {
19 |
20 | /**
21 | * When the job is regular job using routing.
22 | */
23 | const TYPE_REGULAR = 0;
24 |
25 | /**
26 | * When the job contains closure.
27 | */
28 | const TYPE_CALLABLE = 1;
29 |
30 | /**
31 | * The ID of the message. This should be set on the job receive.
32 | * @var integer
33 | */
34 | public $id;
35 |
36 | /**
37 | * Stores the header.
38 | * This can be different for each queue provider.
39 | *
40 | * @var array
41 | */
42 | public $header = [];
43 |
44 | /**
45 | * The route for the job.
46 | * This can either be string that represents the controller/action or
47 | * a anonymous function that will be executed.
48 | *
49 | * @var string|\Closure
50 | */
51 | public $route;
52 |
53 | /**
54 | * @var array
55 | */
56 | public $data = [];
57 |
58 | /**
59 | * whether the task is callable.
60 | * @return boolean
61 | */
62 | public function isCallable()
63 | {
64 | return is_callable($this->route);
65 | }
66 |
67 | /**
68 | * Run the callable task.
69 | *
70 | * The callable should return true if the job is going to be deleted from
71 | * queue.
72 | *
73 | * @return boolean
74 | */
75 | public function runCallable()
76 | {
77 | $return = call_user_func_array($this->route, $this->data);
78 | return $return !== false;
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/src/ProcessRunner.php:
--------------------------------------------------------------------------------
1 |
6 | * @since 2017.08.01
7 | */
8 |
9 | namespace UrbanIndo\Yii2\Queue;
10 |
11 | use IteratorAggregate;
12 | use ArrayIterator;
13 | use Symfony\Component\Process\Process;
14 | use Yii;
15 | use yii\helpers\Console;
16 | use yii\base\InvalidConfigException;
17 |
18 | /**
19 | * The process runner is responsible for all the threads management
20 | * Listens to the queue, based on the config launches x number of processes
21 | * Cleans zombies after they are done, in single threaded mode, runs processes in foreground
22 | *
23 | * @author Marek Petras
24 | * @since 2017.08.01
25 | */
26 | class ProcessRunner extends \yii\base\Component implements IteratorAggregate
27 | {
28 | /**
29 | * @var string $cwd working directory to launch the sub processes in; default to current
30 | */
31 | protected $cwd = null;
32 |
33 | /**
34 | * @var array $env enviromental vars to be passed to the sub process
35 | */
36 | protected $env = [];
37 |
38 | /**
39 | * @var string $scriptPath the yii executable
40 | */
41 | private $_scriptPath = null;
42 |
43 | /**
44 | * @var Queue $queue queue
45 | */
46 | private $_queue;
47 |
48 | /**
49 | * @var array $procs current processes
50 | */
51 | private $procs = [];
52 |
53 | /**
54 | * queue setter
55 | * @param Queue $queue the job queue
56 | * @return self
57 | */
58 | public function setQueue( Queue $queue )
59 | {
60 | $this->_queue = $queue;
61 | return $this;
62 | }
63 |
64 | /**
65 | * queue getter
66 | * @return Queue
67 | */
68 | public function getQueue()
69 | {
70 | return $this->_queue;
71 | }
72 |
73 | /**
74 | * set yii executable
75 | * @param string $scriptPath real path to the file
76 | * @return self
77 | * @throws InvalidConfigException on non existent file
78 | */
79 | public function setScriptPath( $scriptPath )
80 | {
81 | if ( !is_executable($scriptPath) ) {
82 | throw new InvalidConfigException('Invalid script path:' . $scriptPath);
83 | }
84 |
85 | $this->_scriptPath = $scriptPath;
86 | return $this;
87 | }
88 |
89 | /**
90 | * retreive current script path
91 | * @return string script path
92 | * @throws InvalidConfigException on non existent file
93 | */
94 | public function getScriptPath()
95 | {
96 | if ( !is_executable($this->_scriptPath) ) {
97 | throw new InvalidConfigException('Invalid script path:' . $this->_scriptPath);
98 | }
99 |
100 | return $this->_scriptPath;
101 | }
102 |
103 | /**
104 | * IteratorAggregate implementation
105 | * @return ArrayIterator running processes
106 | */
107 | public function getIterator()
108 | {
109 | return new ArrayIterator($this->procs);
110 | }
111 |
112 | /**
113 | * listen to the queue, launch processes based on queue settings
114 | * clean up, catch signals, propagate to sub processes if required or wait for completion
115 | * launches new jobs from the queue when current < maxprocs
116 | * @param string $cwd current working dir
117 | * @param int $timeout timeout to be passed on to the sub processes
118 | * @param array $env enviromental variables for the sub proc
119 | * @return void
120 | */
121 | public function listen( $cwd = null, $timeout = 0, array $env = null )
122 | {
123 | $this->cwd = $cwd;
124 | $this->env = $env;
125 |
126 | $this->initSignalHandler();
127 |
128 | declare(ticks = 1);
129 |
130 | while (true) {
131 |
132 | // determine the size of the queue
133 | $queueSize = $this->getQueueSize();
134 |
135 | $this->stdout(sprintf('queueSize: %d , opened: %d , limit: %d ',
136 | $queueSize,$this->getOpenedProcsCount(),$this->getMaxProcesses()).PHP_EOL);
137 |
138 | // check for defunct processes
139 | $this->cleanUpProcs();
140 |
141 | // if we have queue and open spots, launch new ones
142 | if ( $queueSize ) {
143 | if ( $this->getCanOpenNew() ) {
144 | $this->stdout("Running new process...\n");
145 | $this->runProcess(
146 | $this->buildCommand()
147 | );
148 | }
149 | else {
150 | $this->stdout(sprintf('Nothing to do, Waiting for processes to finish; queueSize: %d , opened: %d , limit: %d ',
151 | $queueSize,$this->getOpenedProcsCount(),$this->getMaxProcesses()).PHP_EOL);
152 | sleep($this->queue->waitSecondsIfNoProcesses); // wait x seconds then try cleaning up
153 | }
154 | }
155 | else {
156 | if ( $this->queue->waitSecondsIfNoQueue > 0 ) {
157 | $this->stdout('NO Queue, Waiting '.$this->queue->waitSecondsIfNoQueue.' to save cpu...' . PHP_EOL);
158 | sleep($this->queue->waitSecondsIfNoQueue);
159 | }
160 | }
161 |
162 | // sleep if we want to between lanuching new processes
163 | if ($this->getSleepTimeout() > 0) {
164 | sleep($this->getSleepTimeout());
165 | }
166 | }
167 | }
168 |
169 | /**
170 | * run the sub process, register it with others,
171 | * if we are in single threaded mode, wait for it to finish before moving on
172 | * @param string $command the command to exec
173 | * @param string $cwd
174 | * @return void
175 | */
176 | public function runProcess( $command )
177 | {
178 | $process = new Process(
179 | $command,
180 | $this->cwd ? $this->cwd : getcwd(),
181 | $this->env
182 | );
183 |
184 | $this->stdout('Running ' . $command . ' (mode: ' . ($this->getIsSingleThreaded() ? 'single' : 'multi') . ')' . PHP_EOL);
185 |
186 | $process->setTimeout($this->getTimeout());
187 | $process->setIdleTimeout($this->getIdleTimeout());
188 | $process->start();
189 |
190 | $this->addProcess($process);
191 |
192 | if ( $this->getIsSingleThreaded() ) {
193 |
194 | $this->stdout('Running in sync mode' . PHP_EOL);
195 |
196 | $pid = $process->getPid();
197 |
198 | $process->wait(function($type,$data){
199 | $method = 'std'.$type;
200 | $this->{$method}($data);
201 | });
202 |
203 | $this->stdout('Done, cleaning:' . $pid . PHP_EOL);
204 |
205 | $this->cleanUpProc($process, $pid);
206 | }
207 | }
208 |
209 | /**
210 | * add the process to the currently running to be cleaned up after finish
211 | * @param Process $process the process object
212 | * @return self
213 | */
214 | public function addProcess( Process $process )
215 | {
216 | $this->procs[$process->getPid()] = $process;
217 | return $this;
218 | }
219 |
220 | /**
221 | * clean up defunct processes running in background
222 | * @return void
223 | */
224 | public function cleanUpProcs()
225 | {
226 | if ( is_array($this->procs) && ($cntProcs = count($this->procs)) > 0 ) {
227 |
228 | $this->stdout('Currently see ' . $cntProcs . ' processes' . PHP_EOL);
229 |
230 | foreach ( $this->procs as $pid => $proc) {
231 | $this->cleanUpProc($proc,$pid);
232 | }
233 | }
234 | }
235 |
236 | /**
237 | * build the command to launch sub process
238 | * @return string command
239 | */
240 | protected function buildCommand()
241 | {
242 | // using setsid to stop signal propagation to allow background processes to finish even if we receive a signal
243 | return "setsid " . PHP_BINARY . " {$this->scriptPath} {$this->getCommand()}";
244 | }
245 |
246 | /**
247 | * check if process is still running, if not get stdout/error and clean up process
248 | * @param Process $process the background process
249 | * @param int $pid process pid
250 | * @return void
251 | */
252 | public function cleanUpProc(Process $process, $pid)
253 | {
254 | $process->checkTimeout();
255 |
256 | if ( !$process->isRunning() ) {
257 |
258 | $this->stdout('Cleanning up ' . $pid . PHP_EOL);
259 |
260 | $process->stop();
261 |
262 | $out = $process->getOutput();
263 | $err = $process->getErrorOutput();
264 |
265 | if ($process->isSuccessful()) {
266 | $this->stdout('Success' . PHP_EOL);
267 |
268 | // we already display output as it is piped in in single threaded mode
269 | if ( !$this->getIsSingleThreaded() ) {
270 | $this->stdout($out . PHP_EOL);
271 | $this->stdout($err . PHP_EOL);
272 | }
273 |
274 | } else {
275 | $this->stdout('Error' . PHP_EOL);
276 | $this->stderr($out . PHP_EOL);
277 | $this->stderr($err . PHP_EOL);
278 | Yii::warning($out, 'yii2queue');
279 | Yii::warning($err, 'yii2queue');
280 | }
281 |
282 | unset($this->procs[$pid]);
283 | }
284 | }
285 |
286 | /**
287 | * wait for all to finish
288 | * @return void
289 | */
290 | public function cleanUpAll( $signal = null, $propagate = false )
291 | {
292 | $this->stdout('Cleaning processes: ' . $this->getOpenedProcsCount() . PHP_EOL);
293 |
294 | while ( $this->getOpenedProcsCount() ) {
295 |
296 | foreach ( $this->procs as $pid => $process ) {
297 |
298 | if ( $process->isRunning()
299 | && $process->getPid()
300 | && $propagate
301 | && $signal )
302 | {
303 | $this->stdout(sprintf('Sending signal %d to pid %d', $signal, $pid) . PHP_EOL);
304 |
305 | try {
306 | $process->signal($signal);
307 | } catch ( \Symfony\Component\Process\Exception\LogicException $e ) {
308 | $this->stdout('Process was already stopped.');
309 | }
310 | }
311 |
312 | $this->cleanUpProc($process, $pid);
313 | }
314 |
315 | sleep(1);
316 | }
317 | }
318 |
319 | /**
320 | * Initialize signal handler for the process.
321 | * @return void
322 | */
323 | protected function initSignalHandler()
324 | {
325 | $signalHandler = function ($signal) {
326 | switch ($signal) {
327 | case SIGTERM:
328 | // wait for procs to finish then quit
329 | $this->stderr('Caught SIGTERM, cleaning up'.PHP_EOL);
330 | $this->cleanUpAll($signal, $this->getPropagateSignals());
331 | Yii::error('Caught SIGTERM', 'yii2queue');
332 | exit;
333 | case SIGINT:
334 | // wait for procs to finish then quit
335 | $this->stderr('Caught SIGINT, cleaning up'.PHP_EOL);
336 | $this->cleanUpAll($signal, $this->getPropagateSignals());
337 | Yii::error('Caught SIGINT', 'yii2queue');
338 | exit;
339 | }
340 | };
341 | pcntl_signal(SIGTERM, $signalHandler);
342 | pcntl_signal(SIGINT, $signalHandler);
343 | }
344 |
345 | /**
346 | * get the idle timeout to be passed on to Process
347 | * @return ?int idle timeout
348 | */
349 | protected function getIdleTimeout()
350 | {
351 | return $this->getQueue()->idleTimeout;
352 | }
353 |
354 | /**
355 | * get the timeout from the queue, seconds after which the process will timeout
356 | * @return ?int timeout in seconds
357 | */
358 | protected function getTimeout()
359 | {
360 | return $this->getQueue()->timeout;
361 | }
362 |
363 | /**
364 | * get sleep timeout to be slept after eaech process is launched
365 | * @return ?int sleep timeout in seconds
366 | */
367 | protected function getSleepTimeout()
368 | {
369 | return $this->getQueue()->sleepTimeout;
370 | }
371 |
372 | /**
373 | * check if we are running in single thread mode
374 | * @return bool
375 | */
376 | protected function getIsSingleThreaded()
377 | {
378 | return $this->getMaxProcesses() === 1;
379 | }
380 |
381 | /**
382 | * retrieve the size of the queue
383 | * @return int size
384 | */
385 | protected function getQueueSize()
386 | {
387 | return intval($this->getQueue()->getSize());
388 | }
389 |
390 | /**
391 | * retrieve number of opened processes
392 | * @return int
393 | */
394 | protected function getOpenedProcsCount()
395 | {
396 | return is_array($this->procs) ? count($this->procs) : 0;
397 | }
398 |
399 | /**
400 | * retrieve max processes from the queue
401 | * @return int maximum number of concurent processes
402 | */
403 | protected function getMaxProcesses()
404 | {
405 | return $this->getQueue()->maxProcesses;
406 | }
407 |
408 | /**
409 | * check if we can open new ones
410 | * @return bool
411 | */
412 | protected function getCanOpenNew()
413 | {
414 | return $this->getOpenedProcsCount() < $this->getMaxProcesses();
415 | }
416 |
417 | /**
418 | * if we should propagate signals to children
419 | * @return bool
420 | */
421 | protected function getPropagateSignals()
422 | {
423 | return $this->getQueue()->propagateSignals;
424 | }
425 |
426 | /**
427 | * get the command to launch the process
428 | * @return string command
429 | */
430 | protected function getCommand()
431 | {
432 | return $this->getQueue()->command;
433 | }
434 |
435 | /**
436 | * @inheritdoc
437 | */
438 | protected function stdout($string)
439 | {
440 | return Console::stdout($string);
441 | }
442 |
443 | /**
444 | * @inheritdoc
445 | */
446 | protected function stderr($string)
447 | {
448 | return Console::stderr($string);
449 | }
450 | }
451 |
--------------------------------------------------------------------------------
/src/Queue.php:
--------------------------------------------------------------------------------
1 |
6 | * @since 2015.02.24
7 | */
8 |
9 | namespace UrbanIndo\Yii2\Queue;
10 |
11 | use Exception;
12 |
13 | /**
14 | * Queue provides basic functionality for queue provider.
15 | * @author Petra Barus
16 | * @since 2015.02.24
17 | */
18 | abstract class Queue extends \yii\base\Component
19 | {
20 |
21 | /**
22 | * Json serializer.
23 | */
24 | const SERIALIZER_JSON = 'json';
25 |
26 | /**
27 | * PHP serializer.
28 | */
29 | const SERIALIZER_PHP = 'php';
30 |
31 | /**
32 | * Event executed before a job is posted to the queue.
33 | */
34 | const EVENT_BEFORE_POST = 'beforePost';
35 |
36 | /**
37 | * Event executed before a job is posted to the queue.
38 | */
39 | const EVENT_AFTER_POST = 'afterPost';
40 |
41 | /**
42 | * Event executed before a job is being fetched from the queue.
43 | */
44 | const EVENT_BEFORE_FETCH = 'beforeFetch';
45 |
46 | /**
47 | * Event executed after a job is being fetched from the queue.
48 | */
49 | const EVENT_AFTER_FETCH = 'afterFetch';
50 |
51 | /**
52 | * Event executed before a job is being deleted from the queue.
53 | */
54 | const EVENT_BEFORE_DELETE = 'beforeDelete';
55 |
56 | /**
57 | * Event executed after a job is being deleted from the queue.
58 | */
59 | const EVENT_AFTER_DELETE = 'afterDelete';
60 |
61 | /**
62 | * Event executed before a job is being released from the queue.
63 | */
64 | const EVENT_BEFORE_RELEASE = 'beforeRelease';
65 |
66 | /**
67 | * Event executed after a job is being released from the queue.
68 | */
69 | const EVENT_AFTER_RELEASE = 'afterRelease';
70 |
71 | /**
72 | * Event executed before a job is being executed.
73 | */
74 | const EVENT_BEFORE_RUN = 'beforeRun';
75 |
76 | /**
77 | * Event executed after a job is being executed.
78 | */
79 | const EVENT_AFTER_RUN = 'afterRun';
80 |
81 | /**
82 | * The module where the task is located.
83 | *
84 | * To add the module, create a new module in the config
85 | * e.g. create a module named 'task'.
86 | *
87 | * 'modules' => [
88 | * 'task' => [
89 | * 'class' => 'app\modules\task\Module',
90 | * ]
91 | * ]
92 | *
93 | * and then add the module to the queue config.
94 | *
95 | * 'components' => [
96 | * 'queue' => [
97 | * 'module' => 'task'
98 | * ]
99 | * ]
100 | *
101 | * @var \yii\base\Module
102 | */
103 | public $module;
104 |
105 | /**
106 | * Choose the serializer.
107 | * @var string
108 | */
109 | public $serializer = 'json';
110 |
111 | /**
112 | * This will release automatically on execution failure. i.e. when
113 | * the `run` method returns false or catch exception.
114 | * @var boolean
115 | */
116 | public $releaseOnFailure = true;
117 |
118 | /**
119 | * Set a value of seconds to wait during the listener loop if there is no queue
120 | * to save CPU.
121 | * @var integer
122 | */
123 | public $waitSecondsIfNoQueue = 0;
124 |
125 | /**
126 | * @var int multi threading
127 | */
128 | public $maxProcesses = 1;
129 |
130 | /**
131 | * @var int interval to check if processes have finished
132 | */
133 | public $waitSecondsIfNoProcesses = 5;
134 |
135 | /**
136 | * @var bool whether we want a kill signal to propagate to threads
137 | * or if we want them to finish on their own, treat carefully
138 | */
139 | public $propagateSignals = false;
140 |
141 | /**
142 | * @var int idle timeout on each single process
143 | * e.g. timeout process after x seconds if there is no output
144 | */
145 | public $idleTimeout = null;
146 |
147 | /**
148 | * @var int $sleepTimeout sleep timeout to be slept after each process is launched
149 | * forced to a minimum of one in order to register the process after its run
150 | */
151 | public $sleepTimeout = 1;
152 |
153 | /**
154 | * @var int $timeout seconds after which the process will timeout
155 | */
156 | public $timeout = null;
157 |
158 | /**
159 | * @var obj ProcessRunner the process runner dependency
160 | */
161 | public $processRunner = null;
162 |
163 | /**
164 | * @var str $command command to run single jobs process
165 | */
166 | public $command = 'queue/run';
167 |
168 | /**
169 | * register the process runner depndency
170 | * @param ProcessRunner $processRunner
171 | * @param array $config additional params
172 | */
173 | public function __construct(ProcessRunner $processRunner, array $config = [])
174 | {
175 | parent::__construct($config);
176 | $this->processRunner = $processRunner->setQueue($this);
177 | }
178 |
179 | /**
180 | * Initializes the module.
181 | * @return void
182 | */
183 | public function init()
184 | {
185 | parent::init();
186 | $this->module = \Yii::$app->getModule($this->module);
187 | }
188 |
189 | /**
190 | * Post new job to the queue. This will trigger event EVENT_BEFORE_POST and
191 | * EVENT_AFTER_POST.
192 | *
193 | * @param Job $job The job.
194 | * @return boolean Whether operation succeed.
195 | */
196 | public function post(Job $job)
197 | {
198 | $this->trigger(self::EVENT_BEFORE_POST, $beforeEvent = new Event(['job' => $job]));
199 | if (!$beforeEvent->isValid) {
200 | return false;
201 | }
202 |
203 | $return = $this->postJob($job);
204 | if (!$return) {
205 | return false;
206 | }
207 |
208 | $this->trigger(self::EVENT_AFTER_POST, new Event(['job' => $job]));
209 | return true;
210 | }
211 |
212 | /**
213 | * Post new job to the queue. Override this for queue implementation.
214 | *
215 | * @param Job $job The job.
216 | * @return boolean Whether operation succeed.
217 | */
218 | abstract protected function postJob(Job $job);
219 |
220 | /**
221 | * Return next job from the queue. This will trigger event EVENT_BEFORE_FETCH
222 | * and event EVENT_AFTER_FETCH
223 | *
224 | * @return Job|boolean the job or false if not found.
225 | */
226 | public function fetch()
227 | {
228 | $this->trigger(self::EVENT_BEFORE_FETCH);
229 |
230 | $job = $this->fetchJob();
231 | if ($job == false) {
232 | return false;
233 | }
234 |
235 | $this->trigger(self::EVENT_AFTER_FETCH, new Event(['job' => $job]));
236 | return $job;
237 | }
238 |
239 | /**
240 | * Return next job from the queue. Override this for queue implementation.
241 | * @return Job|boolean the job or false if not found.
242 | */
243 | abstract protected function fetchJob();
244 |
245 | /**
246 | * Run the job.
247 | *
248 | * @param Job $job The job to be executed.
249 | * @return void
250 | * @throws \yii\base\Exception Exception.
251 | */
252 | public function run(Job $job)
253 | {
254 | $this->trigger(self::EVENT_BEFORE_RUN, $beforeEvent = new Event(['job' => $job]));
255 | if (!$beforeEvent->isValid) {
256 | return;
257 | }
258 | \Yii::info("Running job #: {$job->id}", 'yii2queue');
259 | try {
260 | if ($job->isCallable()) {
261 | $retval = $job->runCallable();
262 | } else {
263 | $retval = $this->module->runAction($job->route, $job->data);
264 | }
265 | } catch (\Exception $e) {
266 | if ($job->isCallable()) {
267 | if (isset($job->header['signature']) && isset($job->header['signature']['route'])) {
268 | $id = $job->id . ' ' . \yii\helpers\Json::encode($job->header['signature']['route']);
269 | } else {
270 | $id = $job->id . ' callable';
271 | }
272 | } else {
273 | $id = $job->route;
274 | }
275 | $params = json_encode($job->data);
276 | \Yii::error(
277 | "Fatal Error: Error running route '{$id}'. Message: {$e->getMessage()}. Parameters: {$params}",
278 | 'yii2queue'
279 | );
280 | if ($this->releaseOnFailure) {
281 | $this->release($job);
282 | }
283 | throw new \yii\base\Exception(
284 | "Error running route '{$id}'. " .
285 | "Message: {$e->getMessage()}. " .
286 | "File: {$e->getFile()}[{$e->getLine()}]. Stack Trace: {$e->getTraceAsString()}",
287 | 500
288 | );
289 | }
290 |
291 | $this->trigger(self::EVENT_AFTER_RUN, new Event(['job' => $job, 'returnValue' => $retval]));
292 |
293 | if ($retval !== false) {
294 | \Yii::info("Deleting job #: {$job->id}", 'yii2queue');
295 | $this->delete($job);
296 | } else if ($this->releaseOnFailure) {
297 | $this->release($job);
298 | }
299 | }
300 |
301 | /**
302 | * Delete the job. This will trigger event EVENT_BEFORE_DELETE and
303 | * EVENT_AFTER_DELETE.
304 | *
305 | * @param Job $job The job to delete.
306 | * @return boolean whether the operation succeed.
307 | */
308 | public function delete(Job $job)
309 | {
310 | $this->trigger(self::EVENT_BEFORE_DELETE, $beforeEvent = new Event(['job' => $job]));
311 | if (!$beforeEvent->isValid) {
312 | return false;
313 | }
314 |
315 | $return = $this->deleteJob($job);
316 | if (!$return) {
317 | return false;
318 | }
319 |
320 | $this->trigger(self::EVENT_AFTER_DELETE, new Event(['job' => $job]));
321 | return true;
322 | }
323 |
324 | /**
325 | * Delete the job. Override this for the queue implementation.
326 | *
327 | * @param Job $job The job to delete.
328 | * @return boolean whether the operation succeed.
329 | */
330 | abstract protected function deleteJob(Job $job);
331 |
332 | /**
333 | * Release the job. This will trigger event EVENT_BEFORE_RELEASE and
334 | * EVENT_AFTER_RELEASE.
335 | *
336 | * @param Job $job The job to delete.
337 | * @return boolean whether the operation succeed.
338 | */
339 | public function release(Job $job)
340 | {
341 | $this->trigger(self::EVENT_BEFORE_RELEASE, $beforeEvent = new Event(['job' => $job]));
342 | if (!$beforeEvent->isValid) {
343 | return false;
344 | }
345 |
346 | $return = $this->releaseJob($job);
347 | if (!$return) {
348 | return false;
349 | }
350 |
351 | $this->trigger(self::EVENT_AFTER_RELEASE, new Event(['job' => $job]));
352 | return true;
353 | }
354 |
355 | /**
356 | * Release the job. Override this for the queue implementation.
357 | *
358 | * @param Job $job The job to release.
359 | * @return boolean whether the operation succeed.
360 | */
361 | abstract protected function releaseJob(Job $job);
362 |
363 | /**
364 | * Deserialize job to be executed.
365 | *
366 | * @param string $message The json string.
367 | * @return Job The job.
368 | * @throws \yii\base\Exception If there is no route detected.
369 | */
370 | protected function deserialize($message)
371 | {
372 | $job = $this->deserializeMessage($message);
373 | if (!isset($job['route'])) {
374 | throw new \yii\base\Exception('No route detected');
375 | }
376 | $route = $job['route'];
377 | $signature = [];
378 | if (isset($job['type']) && $job['type'] == Job::TYPE_CALLABLE) {
379 | $serializer = new \SuperClosure\Serializer();
380 | $signature['route'] = $route;
381 | $route = $serializer->unserialize($route);
382 | }
383 | $data = \yii\helpers\ArrayHelper::getValue($job, 'data', []);
384 | $obj = new Job([
385 | 'route' => $route,
386 | 'data' => $data,
387 | ]);
388 | $obj->header['signature'] = $signature;
389 | return $obj;
390 | }
391 |
392 | /**
393 | * @param array $array The message to be deserialize.
394 | * @return array
395 | * @throws Exception Exception.
396 | */
397 | protected function deserializeMessage($array)
398 | {
399 | switch ($this->serializer) {
400 | case self::SERIALIZER_PHP:
401 | $data = unserialize($array);
402 | break;
403 | case self::SERIALIZER_JSON:
404 | $data = \yii\helpers\Json::decode($array);
405 | break;
406 | }
407 | if (empty($data)) {
408 | throw new Exception('Can not deserialize message');
409 | }
410 | return $data;
411 | }
412 |
413 | /**
414 | * Pack job so that it can be send.
415 | *
416 | * @param Job $job The job to serialize.
417 | * @return string JSON string.
418 | */
419 | protected function serialize(Job $job)
420 | {
421 | $return = [];
422 | if ($job->isCallable()) {
423 | $return['type'] = Job::TYPE_CALLABLE;
424 | $serializer = new \SuperClosure\Serializer();
425 | $return['route'] = $serializer->serialize($job->route);
426 | } else {
427 | $return['type'] = Job::TYPE_REGULAR;
428 | $return['route'] = $job->route;
429 | }
430 | $return['data'] = $job->data;
431 | return $this->serializeMessage($return);
432 | }
433 |
434 | /**
435 | * @param mixed $array Array to serialize.
436 | * @return array
437 | * @throws Exception When the message cannot be deserialized.
438 | */
439 | protected function serializeMessage($array)
440 | {
441 | switch ($this->serializer) {
442 | case self::SERIALIZER_PHP:
443 | $data = serialize($array);
444 | break;
445 | case self::SERIALIZER_JSON:
446 | $data = \yii\helpers\Json::encode($array);
447 | break;
448 | }
449 | if (empty($data)) {
450 | throw new Exception('Can not deserialize message');
451 | }
452 | return $data;
453 | }
454 |
455 | /**
456 | * Returns the number of queue size.
457 | * @return integer
458 | */
459 | abstract public function getSize();
460 |
461 | /**
462 | * Purge the whole queue.
463 | * @return boolean
464 | */
465 | abstract public function purge();
466 | }
467 |
--------------------------------------------------------------------------------
/src/Queues/DbQueue.php:
--------------------------------------------------------------------------------
1 |
6 | * @since 2016.01.16
7 | */
8 |
9 | namespace UrbanIndo\Yii2\Queue\Queues;
10 |
11 | use UrbanIndo\Yii2\Queue\Job;
12 |
13 | /**
14 | * DbQueue provides Yii2 database storing for Queue.
15 | *
16 | * The schema of the table should follow:
17 | *
18 | * CREATE TABLE queue (
19 | * id BIGINT NOT NULL PRIMARY KEY AUTO_INCREMENT,
20 | * status TINYINT NOT NULL DEFAULT 0,
21 | * timestamp DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
22 | * data BLOB
23 | * );
24 | *
25 | * The queue works under the asumption that the `id` fields is AUTO_INCREMENT and
26 | * the `timestamp` will be set using current timestamp.
27 | *
28 | * For other implementation, override the `fetchLatestRow` method and `postJob`
29 | * method.
30 | *
31 | * @author Petra Barus
32 | * @since 2016.01.16
33 | */
34 | class DbQueue extends \UrbanIndo\Yii2\Queue\Queue
35 | {
36 | /**
37 | * Status when the job is ready.
38 | */
39 | const STATUS_READY = 0;
40 |
41 | /**
42 | * Status when the job is being runned by the worker.
43 | */
44 | const STATUS_ACTIVE = 1;
45 |
46 | /**
47 | * Status when the job is deleted.
48 | */
49 | const STATUS_DELETED = 2;
50 |
51 | /**
52 | * The database used for the queue.
53 | *
54 | * This will use default `db` component from Yii application.
55 | * @var string|\yii\db\Connection
56 | */
57 | public $db = 'db';
58 |
59 | /**
60 | * The name of the table to store the queue.
61 | *
62 | * The table should be pre-created as follows for MySQL:
63 | *
64 | * ```php
65 | * CREATE TABLE queue (
66 | * id BIGINT NOT NULL PRIMARY KEY AUTO_INCREMENT,
67 | * status TINYINT NOT NULL DEFAULT 0,
68 | * timestamp DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
69 | * data LONGBLOB
70 | * );
71 | * ```
72 | * @var string
73 | */
74 | public $tableName = '{{%queue}}';
75 |
76 | /**
77 | * Whether to do hard delete of the deleted job, instead of just flagging the
78 | * status.
79 | * @var boolean
80 | */
81 | public $hardDelete = true;
82 |
83 | /**
84 | * @return void
85 | */
86 | public function init()
87 | {
88 | parent::init();
89 | $this->db = \yii\di\Instance::ensure($this->db, \yii\db\Connection::className());
90 | }
91 |
92 | /**
93 | * Return next job from the queue.
94 | * @return Job|boolean the job or false if not found.
95 | */
96 | protected function fetchJob()
97 | {
98 | //Avoiding multiple job.
99 | $transaction = $this->db->beginTransaction();
100 | $row = $this->fetchLatestRow();
101 | if ($row == false || !$this->flagRunningRow($row)) {
102 | $transaction->rollBack();
103 | return false;
104 | }
105 | $transaction->commit();
106 |
107 | $job = $this->deserialize($row['data']);
108 | $job->id = $row['id'];
109 | $job->header['timestamp'] = $row['timestamp'];
110 |
111 | return $job;
112 | }
113 |
114 | /**
115 | * Fetch latest ready job from the table.
116 | *
117 | * Due to the use of AUTO_INCREMENT ID, this will fetch the job with the
118 | * largest ID.
119 | *
120 | * @return array
121 | */
122 | protected function fetchLatestRow()
123 | {
124 | return (new \yii\db\Query())
125 | ->select('*')
126 | ->from($this->tableName)
127 | ->where(['status' => self::STATUS_READY])
128 | ->orderBy(['id' => SORT_ASC])
129 | ->limit(1)
130 | ->one($this->db);
131 | }
132 |
133 | /**
134 | * Flag a row as running. This will update the row ID and status if ready.
135 | *
136 | * @param array $row The row to update.
137 | * @return boolean Whether successful or not.
138 | */
139 | protected function flagRunningRow(array $row)
140 | {
141 | $updated = $this->db->createCommand()
142 | ->update(
143 | $this->tableName,
144 | ['status' => self::STATUS_ACTIVE],
145 | [
146 | 'id' => $row['id'],
147 | 'status' => self::STATUS_READY,
148 | ]
149 | )->execute();
150 | return $updated == 1;
151 | }
152 |
153 | /**
154 | * Post new job to the queue. This contains implementation for database.
155 | *
156 | * @param Job $job The job to post.
157 | * @return boolean whether operation succeed.
158 | */
159 | protected function postJob(Job $job)
160 | {
161 | return $this->db->createCommand()->insert($this->tableName, [
162 | 'timestamp' => new \yii\db\Expression('NOW()'),
163 | 'data' => $this->serialize($job),
164 | ])->execute() == 1;
165 | }
166 |
167 | /**
168 | * Delete the job. Override this for the queue implementation.
169 | *
170 | * @param Job $job The job to delete.
171 | * @return boolean whether the operation succeed.
172 | */
173 | public function deleteJob(Job $job)
174 | {
175 | if ($this->hardDelete) {
176 | return $this->db->createCommand()->delete($this->tableName, [
177 | 'id' => $job->id,
178 | ])->execute() == 1;
179 | } else {
180 | return $this->db->createCommand()->update(
181 | $this->tableName,
182 | ['status' => self::STATUS_DELETED],
183 | ['id' => $job->id]
184 | )->execute() == 1;
185 | }
186 | }
187 |
188 | /**
189 | * Restore job from active to ready.
190 | *
191 | * @param Job $job The job to restore.
192 | * @return boolean whether the operation succeed.
193 | */
194 | public function releaseJob(Job $job)
195 | {
196 | return $this->db->createCommand()->update(
197 | $this->tableName,
198 | ['status' => self::STATUS_READY],
199 | ['id' => $job->id]
200 | )->execute() == 1;
201 | }
202 |
203 | /**
204 | * Returns the number of queue size.
205 | * @return integer
206 | */
207 | public function getSize()
208 | {
209 | return (new \yii\db\Query())
210 | ->select('*')
211 | ->from($this->tableName)
212 | ->where(['status' => self::STATUS_READY])
213 | ->count('*', $this->db);
214 | }
215 |
216 | /**
217 | * Purge the whole queue.
218 | * @return boolean
219 | */
220 | public function purge()
221 | {
222 | return false;
223 | }
224 | }
225 |
--------------------------------------------------------------------------------
/src/Queues/DummyQueue.php:
--------------------------------------------------------------------------------
1 | run($job);
31 | return true;
32 | }
33 |
34 | /**
35 | * Return next job from the queue. Override this for queue implementation.
36 | * @return Job|boolean the job or false if not found.
37 | */
38 | protected function fetchJob()
39 | {
40 | return false;
41 | }
42 |
43 | /**
44 | * Delete the job. Override this for the queue implementation.
45 | *
46 | * @param Job $job The job to delete.
47 | * @return boolean whether the operation succeed.
48 | */
49 | protected function deleteJob(Job $job)
50 | {
51 | return true;
52 | }
53 |
54 | /**
55 | * Release the job. Override this for the queue implementation.
56 | *
57 | * @param Job $job The job to release.
58 | * @return boolean whether the operation succeed.
59 | */
60 | protected function releaseJob(Job $job)
61 | {
62 | return true;
63 | }
64 |
65 | /**
66 | * Returns the number of queue size.
67 | * @return integer
68 | */
69 | public function getSize()
70 | {
71 | return 0;
72 | }
73 |
74 | /**
75 | * Purge the whole queue.
76 | * @return boolean
77 | */
78 | public function purge()
79 | {
80 | return true;
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/src/Queues/MemoryQueue.php:
--------------------------------------------------------------------------------
1 |
6 | * @since 2015.06.01
7 | */
8 |
9 | namespace UrbanIndo\Yii2\Queue\Queues;
10 |
11 | use UrbanIndo\Yii2\Queue\Job;
12 |
13 | /**
14 | * MemoryQueue stores queue in the local variable.
15 | *
16 | * This will only work for one request.
17 | *
18 | * @author Petra Barus
19 | * @since 2015.06.01
20 | */
21 | class MemoryQueue extends \UrbanIndo\Yii2\Queue\Queue
22 | {
23 |
24 | /**
25 | * @var Job[]
26 | */
27 | private $_jobs = [];
28 |
29 | /**
30 | * @param Job $job The job to delete.
31 | * @return boolean Whether the deletion succeed.
32 | */
33 | public function deleteJob(Job $job)
34 | {
35 | foreach ($this->_jobs as $key => $val) {
36 | if ($val->id == $job->id) {
37 | unset($this->_jobs[$key]);
38 | $this->_jobs = array_values($this->_jobs);
39 | return true;
40 | }
41 | }
42 | return true;
43 | }
44 |
45 | /**
46 | * @return Job The job fetched from queue.
47 | */
48 | public function fetchJob()
49 | {
50 | if ($this->getSize() == 0) {
51 | return false;
52 | }
53 | $job = array_pop($this->_jobs);
54 | return $job;
55 | }
56 |
57 | /**
58 | * @param Job $job The job to be posted to the queueu.
59 | * @return boolean Whether the post succeed.
60 | */
61 | public function postJob(Job $job)
62 | {
63 | $job->id = mt_rand(0, 65535);
64 | $this->_jobs[] = $job;
65 | return true;
66 | }
67 |
68 | /**
69 | * Returns the jobs posted to the queue.
70 | * @return Job[]
71 | */
72 | public function getJobs()
73 | {
74 | return $this->_jobs;
75 | }
76 |
77 | /**
78 | * Release the job.
79 | *
80 | * @param Job $job The job to release.
81 | * @return boolean whether the operation succeed.
82 | */
83 | protected function releaseJob(Job $job)
84 | {
85 | $this->_jobs[] = $job;
86 | return true;
87 | }
88 |
89 | /**
90 | * Returns the number of queue size.
91 | * @return integer
92 | */
93 | public function getSize()
94 | {
95 | return count($this->_jobs);
96 | }
97 |
98 | /**
99 | * Purge the whole queue.
100 | * @return boolean
101 | */
102 | public function purge()
103 | {
104 | $this->_jobs = [];
105 | return true;
106 | }
107 |
108 | }
109 |
--------------------------------------------------------------------------------
/src/Queues/MultipleQueue.php:
--------------------------------------------------------------------------------
1 |
6 | * @since 2015.02.25
7 | */
8 |
9 | namespace UrbanIndo\Yii2\Queue\Queues;
10 |
11 | use UrbanIndo\Yii2\Queue\Job;
12 | use UrbanIndo\Yii2\Queue\Queue;
13 | use UrbanIndo\Yii2\Queue\Strategies\Strategy;
14 | use UrbanIndo\Yii2\Queue\Strategies\RandomStrategy;
15 |
16 | /**
17 | * MultipleQueue is a queue abstraction that handles multiple queue at once.
18 | *
19 | * @author Petra Barus
20 | * @since 2015.02.25
21 | */
22 | class MultipleQueue extends Queue
23 | {
24 | /**
25 | * Additional header for the job.
26 | */
27 | const HEADER_MULTIPLE_QUEUE_INDEX = 'MultipleQueueIndex';
28 |
29 | /**
30 | * Stores the queue.
31 | * @var Queue[]
32 | */
33 | public $queues = [];
34 |
35 | /**
36 | * The job fetching strategy.
37 | * @var \UrbanIndo\Yii2\Queue\Strategies\Strategy
38 | */
39 | public $strategy = ['class' => RandomStrategy::class];
40 |
41 | /**
42 | * Initialize the queue.
43 | * @return void
44 | * @throws \yii\base\InvalidConfigException If the strategy doesn't implement
45 | * UrbanIndo\Yii2\Queue\Strategies\Strategy.
46 | */
47 | public function init()
48 | {
49 | parent::init();
50 | $queueObjects = [];
51 | foreach ($this->queues as $id => $queue) {
52 | $queueObjects[$id] = \Yii::createObject($queue);
53 | }
54 | $this->queues = $queueObjects;
55 | if (is_array($this->strategy)) {
56 | $this->strategy = \Yii::createObject($this->strategy);
57 | } else if ($this->strategy instanceof Strategy) {
58 | throw new \yii\base\InvalidConfigException(
59 | 'The strategy field have to implement UrbanIndo\Yii2\Queue\Strategies\Strategy'
60 | );
61 | }
62 | $this->strategy->setQueue($this);
63 | }
64 |
65 | /**
66 | * @param integer $index The index of the queue.
67 | * @return Queue|null the queue or null if not exists.
68 | */
69 | public function getQueue($index)
70 | {
71 | return \yii\helpers\ArrayHelper::getValue($this->queues, $index);
72 | }
73 |
74 | /**
75 | * Delete the job.
76 | * @param Job $job The job.
77 | * @return boolean Whether the operation succeed.
78 | */
79 | protected function deleteJob(Job $job)
80 | {
81 | return $this->strategy->delete($job);
82 | }
83 |
84 | /**
85 | * Return next job from the queue.
86 | * @return Job|boolean The job fetched or false if not found.
87 | */
88 | protected function fetchJob()
89 | {
90 | return $this->strategy->fetch();
91 | }
92 |
93 | /**
94 | * Post new job to the queue.
95 | * @param Job $job The job.
96 | * @return boolean Whether operation succeed.
97 | */
98 | protected function postJob(Job $job)
99 | {
100 | return $this->postToQueue($job, 0);
101 | }
102 |
103 | /**
104 | * Post new job to a specific queue.
105 | * @param Job $job The job.
106 | * @param integer $index The queue index.
107 | * @return boolean Whether operation succeed.
108 | */
109 | public function postToQueue(Job &$job, $index)
110 | {
111 | $queue = $this->getQueue($index);
112 | if ($queue === null) {
113 | return false;
114 | }
115 | return $queue->post($job);
116 | }
117 |
118 | /**
119 | * Release the job.
120 | *
121 | * @param Job $job The job to release.
122 | * @return boolean whether the operation succeed.
123 | */
124 | protected function releaseJob(Job $job)
125 | {
126 | $index = $job->header[self::HEADER_MULTIPLE_QUEUE_INDEX];
127 | $queue = $this->getQueue($index);
128 | return $queue->release($job);
129 | }
130 |
131 | /**
132 | * Returns the total number of all queue size.
133 | * @return integer
134 | */
135 | public function getSize()
136 | {
137 | return array_sum(array_map(function (Queue $queue) {
138 | return $queue->getSize();
139 | }, $this->queues));
140 | }
141 |
142 | /**
143 | * Purge the whole queue.
144 | * @return boolean
145 | */
146 | public function purge()
147 | {
148 | foreach ($this->queues as $queue) {
149 | $queue->purge();
150 | }
151 | return true;
152 | }
153 | }
154 |
--------------------------------------------------------------------------------
/src/Queues/RedisQueue.php:
--------------------------------------------------------------------------------
1 |
6 | * @since 2016.01.16
7 | */
8 |
9 | namespace UrbanIndo\Yii2\Queue\Queues;
10 |
11 | use yii\redis\Connection;
12 | use UrbanIndo\Yii2\Queue\Job;
13 |
14 | /**
15 | * RedisQueue provides Redis storing for Queue.
16 | *
17 | * This uses `yiisoft/yii2-redis` extension that doesn't shipped in the default
18 | * composer dependency. To use this you have to manually add `yiisoft/yii2-redis`
19 | * in the `composer.json`.
20 | *
21 | * @author Petra Barus
22 | * @since 2016.01.16
23 | */
24 | class RedisQueue extends \UrbanIndo\Yii2\Queue\Queue
25 | {
26 | /**
27 | * Stores the redis connection.
28 | * @var string|array|Connection
29 | */
30 | public $db = 'redis';
31 |
32 | /**
33 | * The name of the key to store the queue.
34 | * @var string
35 | */
36 | public $key = 'queue';
37 |
38 | /**
39 | * @return void
40 | */
41 | public function init()
42 | {
43 | parent::init();
44 | $this->db = \yii\di\Instance::ensure($this->db, Connection::className());
45 | }
46 |
47 | /**
48 | * Delete the job.
49 | *
50 | * @param Job $job The job to delete.
51 | * @return boolean whether the operation succeed.
52 | */
53 | public function deleteJob(Job $job)
54 | {
55 | return true;
56 | }
57 |
58 | /**
59 | * Return next job from the queue.
60 | * @return Job|boolean the job or false if not found.
61 | */
62 | protected function fetchJob()
63 | {
64 | $json = $this->db->lpop($this->key);
65 | if ($json == false) {
66 | return false;
67 | }
68 | $data = \yii\helpers\Json::decode($json);
69 | $job = $this->deserialize($data['data']);
70 | $job->id = $data['id'];
71 | $job->header['serialized'] = $data['data'];
72 | return $job;
73 | }
74 |
75 | /**
76 | * Post new job to the queue. This contains implementation for database.
77 | *
78 | * @param Job $job The job to post.
79 | * @return boolean whether operation succeed.
80 | */
81 | protected function postJob(Job $job)
82 | {
83 | return $this->db->rpush($this->key, \yii\helpers\Json::encode([
84 | 'id' => uniqid('queue_', true),
85 | 'data' => $this->serialize($job),
86 | ]));
87 | }
88 |
89 | /**
90 | * Put back job to the queue.
91 | *
92 | * @param Job $job The job to restore.
93 | * @return boolean whether the operation succeed.
94 | */
95 | protected function releaseJob(Job $job)
96 | {
97 | return $this->db->rpush($this->key, \yii\helpers\Json::encode([
98 | 'id' => $job->id,
99 | 'data' => $job->header['serialized'],
100 | ]));
101 | }
102 |
103 | /**
104 | * Returns the total number of all queue size.
105 | * @return integer
106 | */
107 | public function getSize()
108 | {
109 | return $this->db->llen($this->key);
110 | }
111 |
112 | /**
113 | * Purge the whole queue.
114 | * @return boolean
115 | */
116 | public function purge()
117 | {
118 | return $this->db->del($this->key);
119 | }
120 | }
121 |
--------------------------------------------------------------------------------
/src/Queues/SqsQueue.php:
--------------------------------------------------------------------------------
1 |
6 | * @since 2015.02.24
7 | */
8 |
9 | namespace UrbanIndo\Yii2\Queue\Queues;
10 |
11 | use \Aws\Sqs\SqsClient;
12 | use UrbanIndo\Yii2\Queue\Job;
13 |
14 | /**
15 | * SqsQueue provides queue for AWS SQS.
16 | *
17 | * @author Petra Barus
18 | * @since 2015.02.24
19 | */
20 | class SqsQueue extends \UrbanIndo\Yii2\Queue\Queue
21 | {
22 |
23 | /**
24 | * The SQS url.
25 | * @var string
26 | */
27 | public $url;
28 |
29 | /**
30 | * The config for SqsClient.
31 | *
32 | * This will be used for SqsClient::factory($config);
33 | * @var array
34 | */
35 | public $config = [];
36 |
37 | /**
38 | * Due to ability of the queue message to be visible automatically after
39 | * a certain of time, this is not required.
40 | * @var boolean
41 | */
42 | public $releaseOnFailure = false;
43 |
44 | /**
45 | * Stores the SQS client.
46 | * @var \Aws\Sqs\SqsClient
47 | */
48 | private $_client;
49 |
50 | /**
51 | * Initialize the queue component.
52 | * @return void
53 | */
54 | public function init()
55 | {
56 | parent::init();
57 | $this->_client = SqsClient::factory($this->config);
58 | }
59 |
60 | /**
61 | * Return next job from the queue.
62 | * @return Job|boolean the job or false if not found.
63 | */
64 | public function fetchJob()
65 | {
66 | $message = $this->_client->receiveMessage([
67 | 'QueueUrl' => $this->url,
68 | 'AttributeNames' => ['ApproximateReceiveCount'],
69 | 'MaxNumberOfMessages' => 1,
70 | ]);
71 | if (isset($message['Messages']) && count($message['Messages']) > 0) {
72 | return $this->createJobFromMessage($message['Messages'][0]);
73 | } else {
74 | return false;
75 | }
76 | }
77 |
78 | /**
79 | * Create job from SQS message.
80 | *
81 | * @param array $message The message.
82 | * @return \UrbanIndo\Yii2\Queue\Job
83 | */
84 | private function createJobFromMessage($message)
85 | {
86 | $job = $this->deserialize($message['Body']);
87 | $job->header['ReceiptHandle'] = $message['ReceiptHandle'];
88 | $job->id = $message['MessageId'];
89 | return $job;
90 | }
91 |
92 | /**
93 | * Post the job to queue.
94 | *
95 | * @param Job $job The job posted to the queue.
96 | * @return boolean whether operation succeed.
97 | */
98 | public function postJob(Job $job)
99 | {
100 | $model = $this->_client->sendMessage([
101 | 'QueueUrl' => $this->url,
102 | 'MessageBody' => $this->serialize($job),
103 | ]);
104 | if ($model !== null) {
105 | $job->id = $model['MessageId'];
106 | return true;
107 | } else {
108 | return false;
109 | }
110 | }
111 |
112 | /**
113 | * Delete the job from the queue.
114 | *
115 | * @param Job $job The job to be deleted.
116 | * @return boolean whether the operation succeed.
117 | */
118 | public function deleteJob(Job $job)
119 | {
120 | if (!empty($job->header['ReceiptHandle'])) {
121 | $receiptHandle = $job->header['ReceiptHandle'];
122 | $response = $this->_client->deleteMessage([
123 | 'QueueUrl' => $this->url,
124 | 'ReceiptHandle' => $receiptHandle,
125 | ]);
126 | return $response !== null;
127 | } else {
128 | return false;
129 | }
130 | }
131 |
132 | /**
133 | * Release the job.
134 | *
135 | * @param Job $job The job to release.
136 | * @return boolean whether the operation succeed.
137 | */
138 | public function releaseJob(Job $job)
139 | {
140 | if (!empty($job->header['ReceiptHandle'])) {
141 | $receiptHandle = $job->header['ReceiptHandle'];
142 | $response = $this->_client->changeMessageVisibility([
143 | 'QueueUrl' => $this->url,
144 | 'ReceiptHandle' => $receiptHandle,
145 | 'VisibilityTimeout' => 0,
146 | ]);
147 | return $response !== null;
148 | } else {
149 | return false;
150 | }
151 | }
152 |
153 | /**
154 | * Returns the SQS client used.
155 | *
156 | * @return \Aws\Sqs\SqsClient
157 | */
158 | public function getClient()
159 | {
160 | return $this->_client;
161 | }
162 |
163 | /**
164 | * Returns the number of queue size.
165 | * @return integer
166 | */
167 | public function getSize()
168 | {
169 | $response = $this->getClient()->getQueueAttributes([
170 | 'QueueUrl' => $this->url,
171 | 'AttributeNames' => [
172 | 'ApproximateNumberOfMessages'
173 | ]
174 | ]);
175 | $attributes = $response->get('Attributes');
176 | return \yii\helpers\ArrayHelper::getValue($attributes, 'ApproximateNumberOfMessages', 0);
177 | }
178 |
179 | /**
180 | * Purge the whole queue.
181 | * @return boolean
182 | */
183 | public function purge()
184 | {
185 | $response = $this->getClient()->getQueueAttributes([
186 | 'QueueUrl' => $this->url,
187 | ]);
188 | return $response !== null;
189 | }
190 |
191 | }
192 |
--------------------------------------------------------------------------------
/src/Strategies/RandomStrategy.php:
--------------------------------------------------------------------------------
1 |
6 | * @since 2015.02.25
7 | */
8 |
9 | namespace UrbanIndo\Yii2\Queue\Strategies;
10 |
11 | use UrbanIndo\Yii2\Queue\Job;
12 |
13 | /**
14 | * RandomStrategy provides random choosing of the queue for getting the job.
15 | *
16 | * @author Petra Barus
17 | * @since 2015.02.25
18 | */
19 | class RandomStrategy extends Strategy
20 | {
21 |
22 | /**
23 | * @return void
24 | */
25 | public function init()
26 | {
27 | parent::init();
28 | srand();
29 | }
30 |
31 | /**
32 | * The number of attempt before returning false.
33 | * @var integer
34 | */
35 | public $maxAttempt = 5;
36 |
37 | /**
38 | * Returns the job.
39 | * @return Job|boolean the job or false if not found.
40 | */
41 | protected function getJobFromQueues()
42 | {
43 | $attempt = 0;
44 | $count = count($this->_queue->queues);
45 | while ($attempt < $this->maxAttempt) {
46 | $index = rand(0, $count - 1);
47 | $queue = $this->_queue->getQueue($index);
48 | $job = $queue->fetch();
49 | if ($job !== false) {
50 | return [$job, $index];
51 | }
52 | $attempt++;
53 | }
54 | return false;
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/Strategies/Strategy.php:
--------------------------------------------------------------------------------
1 |
6 | * @since 2015.02.25
7 | */
8 |
9 | namespace UrbanIndo\Yii2\Queue\Strategies;
10 |
11 | use UrbanIndo\Yii2\Queue\Queues\MultipleQueue;
12 | use UrbanIndo\Yii2\Queue\Job;
13 |
14 | /**
15 | * Strategy is abstract class fo all strategy that is used for MultipleQueue.
16 | *
17 | * @author Petra Barus
18 | * @since 2015.02.25
19 | */
20 | abstract class Strategy extends \yii\base\BaseObject
21 | {
22 |
23 | /**
24 | * Stores the queue.
25 | * @var \UrbanIndo\Yii2\Queue\Queues\MultipleQueue
26 | */
27 | protected $_queue;
28 |
29 | /**
30 | * Sets the queue.
31 | * @param MultipleQueue $queue The queue.
32 | * @return void
33 | */
34 | public function setQueue(MultipleQueue $queue)
35 | {
36 | $this->_queue = $queue;
37 | }
38 |
39 | /**
40 | * Implement this for the strategy of getting job from the queue.
41 | * @return mixed tuple of job and the queue index.
42 | */
43 | abstract protected function getJobFromQueues();
44 |
45 | /**
46 | * Returns the job.
47 | * @return Job|boolean The job or false if not found.
48 | */
49 | public function fetch()
50 | {
51 | $return = $this->getJobFromQueues();
52 | if ($return === false) {
53 | return false;
54 | }
55 | list($job, $index) = $return;
56 | /* @var $job Job */
57 | $job->header[MultipleQueue::HEADER_MULTIPLE_QUEUE_INDEX] = $index;
58 | return $job;
59 | }
60 |
61 | /**
62 | * Delete the job from the queue.
63 | *
64 | * @param Job $job The job.
65 | * @return boolean whether the operation succeed.
66 | */
67 | public function delete(Job $job)
68 | {
69 | $index = \yii\helpers\ArrayHelper::getValue(
70 | $job->header,
71 | MultipleQueue::HEADER_MULTIPLE_QUEUE_INDEX,
72 | null
73 | );
74 | if (!isset($index)) {
75 | return false;
76 | }
77 | $queue = $this->_queue->getQueue($index);
78 | if (!isset($index)) {
79 | return false;
80 | }
81 | return $queue->delete($job);
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/src/Strategies/WeightedStrategy.php:
--------------------------------------------------------------------------------
1 |
6 | * @since 2015.02.25
7 | */
8 |
9 | namespace UrbanIndo\Yii2\Queue\Strategies;
10 |
11 | /**
12 | * WeightedStrategy that will put weight to the queues.
13 | *
14 | * @author Petra Barus
15 | * @since 2015.02.25
16 | */
17 | class WeightedStrategy extends Strategy
18 | {
19 |
20 | /**
21 | * List of weights.
22 | *
23 | * The weight will be wielded to queue with the sampe index number. And the
24 | * number of the weight should be the same with the number of the queue.
25 | *
26 | * For example,
27 | * [10, 8, 5, 2]
28 | *
29 | * means, if the queue 0 will have weight 10, 1 will have 8, and so on.
30 | *
31 | * In other words, the weight will NOT be automatically sorted descending
32 | * and NOT be sliced to number of queue.
33 | *
34 | * @var array
35 | */
36 | public $weight = [];
37 |
38 | /**
39 | * @return void
40 | */
41 | public function init()
42 | {
43 | parent::init();
44 | // sort($this->weight, SORT_DESC);
45 | // $this->weight = array_slice($this->weight, 0,
46 | // count($this->_queue->queues));
47 | }
48 |
49 | /**
50 | * Implement this for the strategy of getting job from the queue.
51 | * @return mixed tuple of job and the queue index.
52 | */
53 | protected function getJobFromQueues()
54 | {
55 | $index = self::weightedRandom($this->weight);
56 | $count = count($this->_queue->queues);
57 | while ($index < $count) {
58 | $queue = $this->_queue->getQueue($index);
59 | $job = $queue->fetch();
60 | if ($job !== false) {
61 | return [$job, $index];
62 | }
63 | //will continue fetching to the lower priority.
64 | $index++;
65 | }
66 | return false;
67 | }
68 |
69 | /**
70 | * Return weighted random.
71 | *
72 | * @param array $array Array of value and weight.
73 | * @return string the value.
74 | */
75 | private static function weightedRandom($array)
76 | {
77 | $rand = mt_rand(1, (int) array_sum($array));
78 | foreach ($array as $key => $value) {
79 | $rand -= $value;
80 | if ($rand <= 0) {
81 | return $key;
82 | }
83 | }
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/src/Web/Controller.php:
--------------------------------------------------------------------------------
1 |
6 | */
7 |
8 | namespace UrbanIndo\Yii2\Queue\Web;
9 |
10 | use UrbanIndo\Yii2\Queue\Job;
11 | use UrbanIndo\Yii2\Queue\Queue;
12 | use UrbanIndo\Yii2\Queue\Queues\MultipleQueue;
13 |
14 | /**
15 | * QueueController is a web controller to post job via url.
16 | *
17 | * To use this use a controller map.
18 | *
19 | * 'controllerMap' => [
20 | * 'queue' => 'UrbanIndo\Yii2\Queue\Web\Controller',
21 | * ]
22 | *
23 | * And then send a POST to the endpoint
24 | *
25 | * curl -XPOST http://example.com/queue --data route=test/test --data={"data": "data"}
26 | *
27 | * @author Petra Barus
28 | * @author Adinata
29 | */
30 | class Controller extends \yii\web\Controller
31 | {
32 |
33 | /**
34 | * Disable class file.
35 | * @var boolean
36 | */
37 | public $enableCsrfValidation = false;
38 |
39 | /**
40 | * The queue to process.
41 | * @var string|array|\UrbanIndo\Yii2\Queue\Queue
42 | */
43 | public $queue = 'queue';
44 |
45 | /**
46 | * @return void
47 | */
48 | public function init()
49 | {
50 | parent::init();
51 | \Yii::$app->getResponse()->format = 'json';
52 | $this->queue = \yii\di\Instance::ensure($this->queue, Queue::className());
53 | }
54 |
55 | /**
56 | * @return Job
57 | * @throws \yii\web\ServerErrorHttpException When malformed request.
58 | */
59 | private function createJobFromRequest()
60 | {
61 | $route = \Yii::$app->getRequest()->post('route');
62 | $data = \Yii::$app->getRequest()->post('data', []);
63 |
64 | if (empty($route)) {
65 | throw new \yii\web\ServerErrorHttpException('Failed to post job');
66 | }
67 |
68 | if (is_string($data)) {
69 | $data = \yii\helpers\Json::decode($data);
70 | }
71 |
72 | return new Job([
73 | 'route' => $route,
74 | 'data' => $data
75 | ]);
76 | }
77 |
78 | /**
79 | * Endpoint to post a job to queue.
80 | * @return mixed
81 | * @throws \yii\web\ServerErrorHttpException When failed to post.
82 | */
83 | public function actionPost()
84 | {
85 | $job = $this->createJobFromRequest();
86 | /* @var $queue \UrbanIndo\Yii2\Queue\Queue */
87 | if ($this->queue->post($job)) {
88 | return ['status' => 'okay', 'jobId' => $job->id];
89 | } else {
90 | throw new \yii\web\ServerErrorHttpException('Failed to post job');
91 | }
92 | }
93 |
94 | /**
95 | * Endpoint to post a job to multiple queue.
96 | * @return mixed
97 | * @throws \InvalidArgumentException Queue has to be instance of \UrbanIndo\Yii2\Queue\MultipleQueue.
98 | * @throws \yii\web\ServerErrorHttpException When failed to post the job.
99 | */
100 | public function actionPostToQueue()
101 | {
102 | $job = $this->createJobFromRequest();
103 | $index = \Yii::$app->getRequest()->post('index');
104 | if (!isset($index)) {
105 | throw new \InvalidArgumentException('Index needed');
106 | }
107 | $queue = $this->queue;
108 | if (!$queue instanceof MultipleQueue) {
109 | throw new \InvalidArgumentException('Queue is not instance of \UrbanIndo\Yii2\Queue\MultipleQueue');
110 | }
111 | /* @var $queue MultipleQueue */
112 |
113 | if ($queue->postToQueue($job, $index)) {
114 | return ['status' => 'okay', 'jobId' => $job->id];
115 | } else {
116 | throw new \yii\web\ServerErrorHttpException('Failed to post job');
117 | }
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/src/Web/WorkerController.php:
--------------------------------------------------------------------------------
1 |
6 | * @author Adinata
7 | */
8 |
9 | namespace UrbanIndo\Yii2\Queue\Web;
10 |
11 | use UrbanIndo\Yii2\Queue\Job;
12 | use UrbanIndo\Yii2\Queue\Queue;
13 | use UrbanIndo\Yii2\Queue\Queues\DummyQueue;
14 | use Yii;
15 | use yii\base\NotSupportedException;
16 |
17 | /**
18 | * WorkerController is a web controller class that fetches work from queue and
19 | * then run the job.
20 | * The motivation comes from the HHVM limitation for running PHP terminal script.
21 | *
22 | * @author Petra Barus
23 | * @author Adinata
24 | */
25 | class WorkerController extends \yii\web\Controller
26 | {
27 |
28 | /**
29 | * @var boolean
30 | */
31 | public $enableCsrfValidation = false;
32 |
33 | /**
34 | * The default queue component name or component configuration to use when
35 | * there is no queue param sent in the request.
36 | * @var string|array
37 | */
38 | public $defaultQueue = 'queue';
39 |
40 | /**
41 | * The key name of the request param that contains the name of the queue component
42 | * to use.
43 | * This will check the parameter of the POST first. If there is no value for the param,
44 | * then it will check the GET. If there is still no value, then the queue component
45 | * name will use the defaultQueue.
46 | * @var string
47 | */
48 | public $queueParamName = 'queue';
49 |
50 | /**
51 | * Run a task without going to queue.
52 | *
53 | * This is useful to test the task controller. The `route` and `data` will be
54 | * retrieved from POST data.
55 | */
56 | public function actionRunTask()
57 | {
58 | $route = \Yii::$app->getRequest()->post('route');
59 | $data = \Yii::$app->getRequest()->post('data');
60 | $job = new \UrbanIndo\Yii2\Queue\Job([
61 | 'route' => $route,
62 | 'data' => \yii\helpers\Json::decode($data),
63 | ]);
64 | $queue = new DummyQueue([
65 | 'module' => $this->getDummyModule()
66 | ]);
67 | return $this->executeJob($queue, $job);
68 | }
69 |
70 | /**
71 | * @throws NotSupportedException Inherit this class to use run-task.
72 | * @return string Module name.
73 | */
74 | protected function getDummyModule()
75 | {
76 | throw new NotSupportedException();
77 | }
78 |
79 | /**
80 | * Run a task by request.
81 | * @return mixed
82 | */
83 | public function actionRun()
84 | {
85 | $queue = $this->getQueue();
86 | $job = $queue->fetch();
87 | if ($job == false) {
88 | return ['status' => 'nojob'];
89 | }
90 | return $this->executeJob($queue, $job);
91 | }
92 |
93 | /**
94 | * @param Queue $queue Queue the job located.
95 | * @param Job $job Job to be executed.
96 | * @return array
97 | */
98 | protected function executeJob(Queue $queue, Job $job)
99 | {
100 | $start = time();
101 | $return = [
102 | 'jobId' => $job->id,
103 | 'route' => $job->isCallable() ? 'callable' : $job->route,
104 | 'data' => $job->data,
105 | 'time' => date('Y-m-d H:i:s', $start)
106 | ];
107 | try {
108 | ob_start();
109 | $queue->run($job);
110 | $output = ob_get_clean();
111 | $return2 = [
112 | 'status' => 'success',
113 | 'level' => 'info',
114 | ];
115 | } catch (\Exception $exc) {
116 | $output = ob_get_clean();
117 | Yii::$app->getResponse()->statusCode = 500;
118 | $return2 = [
119 | 'status' => 'failed',
120 | 'level' => 'error',
121 | 'reason' => $exc->getMessage(),
122 | 'trace' => $exc->getTraceAsString(),
123 | ];
124 | }
125 | $return3 = [
126 | 'stdout' => $output,
127 | 'duration' => time() - $start,
128 | ];
129 | return array_merge($return, $return2, $return3);
130 | }
131 |
132 | /**
133 | * Returns the queue component.
134 | * This will check if there is a queue component from.
135 | *
136 | * @return \UrbanIndo\Yii2\Queue\Queue
137 | */
138 | protected function getQueue() {
139 | $queueComponent = $this->getComponentParamFromRequest();
140 | if (empty($queueComponent)) {
141 | $queueComponent = $this->defaultQueue;
142 | }
143 | return \yii\di\Instance::ensure($queueComponent, '\UrbanIndo\Yii2\Queue\Queue');
144 | }
145 |
146 | /**
147 | * @return string
148 | */
149 | private function getComponentParamFromRequest()
150 | {
151 | $request = Yii::$app->getRequest();
152 | if ($request->isPost) {
153 | return $request->post($this->queueParamName);
154 | } else {
155 | return $request->get($this->queueParamName);
156 | }
157 | }
158 | }
159 |
--------------------------------------------------------------------------------
/src/Worker/Controller.php:
--------------------------------------------------------------------------------
1 |
6 | * @since 2015.02.24
7 | */
8 |
9 | namespace UrbanIndo\Yii2\Queue\Worker;
10 |
11 | use yii\base\InlineAction;
12 |
13 | /**
14 | * Controller is base class for task controllers.
15 | *
16 | * The usage is pretty much the same with the web or the console. The different
17 | * is that if the action return false, the job will not deleted. Otherwise
18 | * the job will be deleted from the queue.
19 | *
20 | * @author Petra Barus
21 | * @since 2015.02.24
22 | */
23 | abstract class Controller extends \yii\base\Controller
24 | {
25 |
26 | /**
27 | * Stores all params even some elements are not assigned in the action method.
28 | * @var array
29 | */
30 | private $_params = [];
31 |
32 | /**
33 | * Returns action params from the queue job.
34 | * @return array
35 | */
36 | public function getActionParams()
37 | {
38 | return $this->_params;
39 | }
40 |
41 | /**
42 | * Binds the parameters to the action.
43 | * This method is invoked by [[Action]] when it begins to run with the given parameters.
44 | * This method will first bind the parameters with the [[options()|options]]
45 | * available to the action. It then validates the given arguments.
46 | * @param \yii\base\Action $action The action To be bound with parameters.
47 | * @param array $params The parameters to be bound to the action.
48 | * @return array the valid parameters that the action can run with.
49 | * @throws \Exception If there are unknown options or missing arguments.
50 | */
51 | public function bindActionParams($action, $params)
52 | {
53 | $this->_params = $params;
54 |
55 | if ($action instanceof InlineAction) {
56 | $method = new \ReflectionMethod($this, $action->actionMethod);
57 | } else {
58 | $method = new \ReflectionMethod($action, 'run');
59 | }
60 |
61 | $args = [];
62 |
63 | $missing = [];
64 |
65 | foreach ($method->getParameters() as $i => $param) {
66 | /* @var $param \ReflectionParameter */
67 | $name = $param->getName();
68 | if (isset($params[$name])) {
69 | if ($param->isArray() && !is_array($params[$name])) {
70 | $args[] = preg_split('/\s*,\s*/', $params[$name]);
71 | } else {
72 | $args[] = $params[$name];
73 | }
74 | } else if ($param->isDefaultValueAvailable()) {
75 | $args[] = $param->getDefaultValue();
76 | } else {
77 | $missing[] = $name;
78 | }
79 | }
80 |
81 | if (!empty($missing)) {
82 | throw new \Exception(
83 | \Yii::t('yii', 'Missing required arguments: {params}', [
84 | 'params' => implode(', ', $missing)])
85 | );
86 | }
87 |
88 | return $args;
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/tests/Behaviors/ActiveRecordDeferredEventBehaviorTest.php:
--------------------------------------------------------------------------------
1 | getDb()->createCommand()->createTable('test_active_record_deferred_event_behaviors', [
15 | 'id' => 'pk',
16 | 'name' => 'string',
17 | ])->execute();
18 | Yii::$app->queue->purge();
19 | }
20 |
21 | public function testEventHandler()
22 | {
23 | $queue = Yii::$app->queue;
24 | /* @var $queue \UrbanIndo\Yii2\Queue\Queues\MemoryQueue */
25 | $this->assertEquals(0, $queue->getSize());
26 | $object1 = new TestActiveRecord();
27 | $this->assertTrue($object1 instanceof TestActiveRecord);
28 | $object1->id = 1;
29 | $object1->name = 'start';
30 | $object1->save();
31 | $this->assertEquals(1, $queue->getSize());
32 | $job = $queue->fetch();
33 | $this->assertEquals(0, $queue->getSize());
34 | $queue->run($job);
35 | $sameObject1 = TestActiveRecord::findOne(1);
36 | $this->assertEquals('done', $sameObject1->name);
37 | //
38 | $object1->name = 'test';
39 | $object1->save(false);
40 | $this->assertEquals(1, $queue->getSize());
41 | $job = $queue->fetch();
42 | $this->assertEquals(0, $queue->getSize());
43 | $queue->run($job);
44 | $sameObject1 = TestActiveRecord::findOne(1);
45 | $this->assertEquals('updated', $sameObject1->name);
46 |
47 | $object2 = new TestActiveRecord();
48 | $this->assertTrue($object2 instanceof TestActiveRecord);
49 | $object2->id = 2;
50 | $object2->name = 'start';
51 | $object2->scenario = 'test';
52 | $object2->save();
53 | $this->assertEquals(1, $queue->getSize());
54 | $job = $queue->fetch();
55 | $this->assertEquals(0, $queue->getSize());
56 | $queue->run($job);
57 | $sameObject2 = TestActiveRecord::findOne(2);
58 | $this->assertEquals('test', $sameObject2->name);
59 |
60 | }
61 |
62 | }
63 |
64 | class TestActiveRecord extends \yii\db\ActiveRecord
65 | {
66 |
67 | public static function tableName()
68 | {
69 | return 'test_active_record_deferred_event_behaviors';
70 | }
71 |
72 | public function behaviors()
73 | {
74 | return [
75 | [
76 | 'class' => ActiveRecordDeferredEventBehavior::class,
77 | 'events' => [
78 | self::EVENT_AFTER_INSERT => 'deferAfterInsert',
79 | self::EVENT_AFTER_UPDATE => 'deferAfterUpdate',
80 | self::EVENT_AFTER_DELETE => 'deferAfterDelete',
81 | ]
82 | ]
83 | ];
84 | }
85 |
86 | public function scenarios()
87 | {
88 | return [
89 | 'default' => ['name', 'id'],
90 | 'test' => ['name', 'id'],
91 | ];
92 | }
93 |
94 | public function deferAfterInsert()
95 | {
96 | $this->name = $this->scenario == 'test' ? 'test' : 'done';
97 | $this->updateAttributes(['name']);
98 | }
99 |
100 | public function deferAfterUpdate()
101 | {
102 | $this->name = 'updated';
103 | $this->updateAttributes(['name']);
104 | }
105 |
106 | }
--------------------------------------------------------------------------------
/tests/Behaviors/ActiveRecordDeferredEventHandlerTest.php:
--------------------------------------------------------------------------------
1 | getDb()->createCommand()->createTable('deferred_active_record_event_handler_test', [
14 | 'id' => 'pk',
15 | 'name' => 'string',
16 | ])->execute();
17 | Yii::$app->queue->purge();
18 | }
19 |
20 | public function testEventHandlerInActiveRecord() {
21 | $queue = Yii::$app->queue;
22 | /* @var $queue \UrbanIndo\Yii2\Queue\Queues\MemoryQueue */
23 | $this->assertEquals(0, $queue->getSize());
24 | $object1 = new ActiveRecordDeferredEventHandlerTestActiveRecord();
25 | $object1->id = 1;
26 | $object1->name = 'test';
27 | $object1->save(false);
28 | $this->assertEquals(1, $queue->getSize());
29 | $job = $queue->fetch();
30 | $this->assertEquals(0, $queue->getSize());
31 | $queue->run($job);
32 | $object1->refresh();
33 | $this->assertEquals('done', $object1->name);
34 |
35 |
36 | $this->assertEquals(0, $queue->getSize());
37 | $object2 = new ActiveRecordDeferredEventHandlerTestActiveRecord();
38 | $object2->id = 2;
39 | $object2->name = 'test';
40 | $object2->scenario = 'test';
41 | $object2->save(false);
42 | $this->assertEquals(1, $queue->getSize());
43 | $job = $queue->fetch();
44 | $this->assertEquals(0, $queue->getSize());
45 | $queue->run($job);
46 | $object2->refresh();
47 | $this->assertEquals('test', $object2->name);
48 |
49 | }
50 |
51 | }
52 |
53 | class ActiveRecordDeferredEventHandlerImpl extends \UrbanIndo\Yii2\Queue\Behaviors\ActiveRecordDeferredEventHandler {
54 | public function handleEvent($owner) {
55 | $owner->updateModel();
56 | return true;
57 | }
58 | }
59 |
60 | class ActiveRecordDeferredEventHandlerTestActiveRecord extends \yii\db\ActiveRecord {
61 |
62 | public static function tableName() {
63 | return 'deferred_active_record_event_handler_test';
64 | }
65 |
66 | public function behaviors() {
67 | return [
68 | [
69 | 'class' => ActiveRecordDeferredEventHandlerImpl::class,
70 | 'events' => [self::EVENT_AFTER_INSERT],
71 | ]
72 | ];
73 | }
74 |
75 | public function scenarios() {
76 | return [
77 | 'default' => ['name', 'id'],
78 | 'test' => ['name', 'id'],
79 | ];
80 | }
81 |
82 | public function updateModel() {
83 | $this->name = $this->scenario == 'test' ? 'test' : 'done';
84 | $this->update(false);
85 | }
86 | }
--------------------------------------------------------------------------------
/tests/Behaviors/ActiveRecordDeferredEventRoutingBehaviorTest.php:
--------------------------------------------------------------------------------
1 | getDb()->createCommand()->createTable('test_active_record_deferred_event_routing', [
13 | 'id' => 'pk',
14 | 'name' => 'string',
15 | ])->execute();
16 | Yii::$app->queue->purge();
17 | }
18 |
19 | public function testEventRouting() {
20 |
21 | $queue = Yii::$app->queue;
22 | /* @var $queue \UrbanIndo\Yii2\Queue\Queues\MemoryQueue */
23 | $this->assertEquals(0, $queue->getSize());
24 | $model = new DeferredEventRoutingBehaviorTestActiveRecord();
25 | $model->id = 5;
26 | $model->save(false);
27 | $model->trigger('eventTest');
28 | $this->assertEquals(1, $queue->getSize());
29 |
30 | $job = $queue->fetch();
31 | $this->assertEquals('test/index', $job->route);
32 | $this->assertFalse($job->isCallable());
33 | $this->assertEquals(0, $queue->getSize());
34 | $this->assertEquals([
35 | 'id' => 5,
36 | 'scenario' => 'default',
37 | ], $job->data);
38 | $model->trigger('eventTest2');
39 | $this->assertEquals(1, $queue->getSize());
40 | $job = $queue->fetch();
41 | $this->assertEquals('test/halo', $job->route);
42 | $this->assertFalse($job->isCallable());
43 | $this->assertEquals(0, $queue->getSize());
44 | $this->assertEquals([
45 | 'halo' => 5,
46 | 'scenario' => 'default',
47 | ], $job->data);
48 |
49 | }
50 | }
51 |
52 | class DeferredEventRoutingBehaviorTestActiveRecord extends \yii\db\ActiveRecord {
53 |
54 | const EVENT_TEST = 'eventTest';
55 | const EVENT_TEST2 = 'eventTest2';
56 |
57 | public static function tableName() {
58 | return 'test_active_record_deferred_event_routing';
59 | }
60 |
61 | public function behaviors() {
62 | return [
63 | [
64 | 'class' => 'UrbanIndo\Yii2\Queue\Behaviors\ActiveRecordDeferredEventRoutingBehavior',
65 | 'events' => [
66 | self::EVENT_TEST => ['test/index'],
67 | self::EVENT_TEST2 => function($model) {
68 | return ['test/halo', 'halo' => $model->id];
69 | }
70 | ]
71 | ]
72 | ];
73 | }
74 |
75 |
76 | }
--------------------------------------------------------------------------------
/tests/Behaviors/DeferredEventBehaviorTest.php:
--------------------------------------------------------------------------------
1 | getDb()->createCommand()->createTable('test_deferred_event_behaviors', [
13 | 'id' => 'pk',
14 | 'name' => 'string',
15 | ])->execute();
16 | Yii::$app->queue->purge();
17 | }
18 |
19 | public function testEventHandler() {
20 | $queue = Yii::$app->queue;
21 | /* @var $queue \UrbanIndo\Yii2\Queue\Queues\MemoryQueue */
22 | $this->assertEquals(0, $queue->getSize());
23 |
24 | $model = new TestModel();
25 | $model->recordId = 1;
26 | $model->createRecord();
27 | $model->triggerEvent();
28 |
29 | $this->assertEquals(1, $queue->getSize());
30 | $job = $queue->fetch();
31 | $this->assertEquals(0, $queue->getSize());
32 | $queue->run($job);
33 |
34 | $sameModel = DeferredEventBehaviorTestActiveRecord::findOne($model->recordId);
35 | $this->assertEquals('done', $sameModel->name);
36 | }
37 |
38 | }
39 |
40 | class TestModel extends \yii\base\Model {
41 |
42 | const EVENT_TEST = 'eventTest';
43 |
44 | public $recordId;
45 |
46 | public function behaviors() {
47 | return [
48 | [
49 | 'class' => \UrbanIndo\Yii2\Queue\Behaviors\DeferredEventBehavior::class,
50 | 'events' => [
51 | self::EVENT_TEST => 'deferEvent',
52 | ]
53 | ]
54 | ];
55 | }
56 |
57 | public function createRecord() {
58 | //See the execution via database.
59 | $model = new DeferredEventBehaviorTestActiveRecord();
60 | $model->id = $this->recordId;
61 | $model->name = 'test';
62 | $model->save(false);
63 | }
64 |
65 | public function triggerEvent() {
66 | $this->trigger(self::EVENT_TEST);
67 | }
68 |
69 | public function deferEvent() {
70 | $model = DeferredEventBehaviorTestActiveRecord::findOne($this->recordId);
71 | $model->name = 'done';
72 | $model->save(false);
73 | }
74 |
75 | }
76 |
77 | class DeferredEventBehaviorTestActiveRecord extends \yii\db\ActiveRecord {
78 |
79 | public static function tableName() {
80 | return 'test_deferred_event_behaviors';
81 | }
82 | }
--------------------------------------------------------------------------------
/tests/Behaviors/DeferredEventHandlerTest.php:
--------------------------------------------------------------------------------
1 | getDb()->createCommand()->createTable('deferred_event_handler_test', [
14 | 'id' => 'pk',
15 | 'name' => 'string',
16 | ])->execute();
17 | Yii::$app->queue->purge();
18 | }
19 |
20 | public function testEventHandlerInSimpleComponent()
21 | {
22 | $queue = Yii::$app->queue;
23 | /* @var $queue \UrbanIndo\Yii2\Queue\Queues\MemoryQueue */
24 | $this->assertEquals(0, $queue->getSize());
25 | $component = new DeferredEventHandlerTestComponent();
26 | $component->recordId = 1;
27 | $component->triggerEvent();
28 |
29 | $model = DeferredEventHandlerTestActiveRecord::findOne($component->recordId);
30 | $this->assertNotNull($model);
31 | $this->assertEquals('test', $model->name);
32 |
33 | $this->assertEquals(1, $queue->getSize());
34 | $job = $queue->fetch();
35 | $this->assertEquals(0, $queue->getSize());
36 | $queue->run($job);
37 |
38 | $model->refresh();
39 | $this->assertEquals('done', $model->name);
40 | }
41 |
42 | public function testEventHandlerInSimpleModel()
43 | {
44 | $queue = Yii::$app->queue;
45 | /* @var $queue \UrbanIndo\Yii2\Queue\Queues\MemoryQueue */
46 | $this->assertEquals(0, $queue->getSize());
47 | $model = new DeferredEventHandlerTestModel();
48 | $model->recordId = 2;
49 | $model->triggerEvent();
50 |
51 | $model = DeferredEventHandlerTestActiveRecord::findOne($model->recordId);
52 | $this->assertNotNull($model);
53 | $this->assertEquals('test', $model->name);
54 |
55 | $this->assertEquals(1, $queue->getSize());
56 | $job = $queue->fetch();
57 | $this->assertEquals(0, $queue->getSize());
58 | $queue->run($job);
59 |
60 | $model->refresh();
61 | $this->assertEquals('done', $model->name);
62 | }
63 | }
64 |
65 | class DeferredEventHandlerImpl extends \UrbanIndo\Yii2\Queue\Behaviors\DeferredEventHandler
66 | {
67 | public function handleEvent($owner)
68 | {
69 | $owner->updateModel();
70 | return true;
71 | }
72 | }
73 |
74 | class DeferredEventHandlerTestComponent extends \yii\base\Component
75 | {
76 |
77 | const EVENT_TEST = 'eventTest';
78 |
79 | public $recordId;
80 |
81 | public function behaviors()
82 | {
83 | return [
84 | [
85 | 'class' => DeferredEventHandlerImpl::class,
86 | 'events' => [self::EVENT_TEST],
87 | ]
88 | ];
89 | }
90 |
91 | public function triggerEvent()
92 | {
93 | $model = new DeferredEventHandlerTestActiveRecord();
94 | $model->id = $this->recordId;
95 | $model->name = 'test';
96 | $model->save(false);
97 | $this->trigger(self::EVENT_TEST);
98 | }
99 |
100 | public function updateModel()
101 | {
102 | $model = DeferredEventHandlerTestActiveRecord::findOne($this->recordId);
103 | $model->name = 'done';
104 | $model->save(false);
105 | }
106 |
107 | }
108 |
109 | class DeferredEventHandlerTestModel extends \yii\base\Model
110 | {
111 |
112 | const EVENT_TEST = 'eventTest';
113 |
114 | public $recordId;
115 |
116 | public function behaviors()
117 | {
118 | return [
119 | [
120 | 'class' => DeferredEventHandlerImpl::class,
121 | 'events' => [self::EVENT_TEST],
122 | ]
123 | ];
124 | }
125 |
126 | public function triggerEvent()
127 | {
128 | $model = new DeferredEventHandlerTestActiveRecord();
129 | $model->id = $this->recordId;
130 | $model->name = 'test';
131 | $model->save(false);
132 | $this->trigger(self::EVENT_TEST);
133 | }
134 |
135 | public function updateModel()
136 | {
137 | $model = DeferredEventHandlerTestActiveRecord::findOne($this->recordId);
138 | $model->name = 'done';
139 | $model->save(false);
140 | }
141 |
142 | }
143 |
144 | class DeferredEventHandlerTestActiveRecord extends \yii\db\ActiveRecord
145 | {
146 |
147 | public static function tableName()
148 | {
149 | return 'deferred_event_handler_test';
150 | }
151 | }
--------------------------------------------------------------------------------
/tests/Behaviors/DeferredEventRoutingBehaviorTest.php:
--------------------------------------------------------------------------------
1 | queue;
15 | /* @var $queue \UrbanIndo\Yii2\Queue\Queues\MemoryQueue */
16 | $this->assertEquals(0, $queue->getSize());
17 | $model = new DeferredEventRoutingBehaviorTestModel();
18 | $model->trigger('eventTest');
19 | $this->assertEquals(1, $queue->getSize());
20 | $model->id = 5;
21 | $job = $queue->fetch();
22 | $this->assertEquals('test/index', $job->route);
23 | $this->assertFalse($job->isCallable());
24 | $this->assertEquals(0, $queue->getSize());
25 | $this->assertEquals([
26 | 'id' => 1,
27 | 'test' => 2,
28 | ], $job->data);
29 | $model->trigger('eventTest2');
30 | $this->assertEquals(1, $queue->getSize());
31 | $job = $queue->fetch();
32 | $this->assertEquals('test/halo', $job->route);
33 | $this->assertFalse($job->isCallable());
34 | $this->assertEquals(0, $queue->getSize());
35 | $this->assertEquals([
36 | 'halo' => 5
37 | ], $job->data);
38 |
39 | }
40 | }
41 |
42 | class DeferredEventRoutingBehaviorTestModel extends \yii\base\Model {
43 |
44 | const EVENT_TEST = 'eventTest';
45 | const EVENT_TEST2 = 'eventTest2';
46 |
47 | public $id;
48 |
49 | public function behaviors() {
50 | return [
51 | [
52 | 'class' => 'UrbanIndo\Yii2\Queue\Behaviors\DeferredEventRoutingBehavior',
53 | 'events' => [
54 | self::EVENT_TEST => ['test/index', 'id' => 1, 'test' => 2],
55 | self::EVENT_TEST2 => function($model) {
56 | return ['test/halo', 'halo' => $model->id];
57 | }
58 | ]
59 | ]
60 | ];
61 | }
62 |
63 |
64 | }
--------------------------------------------------------------------------------
/tests/EventTest.php:
--------------------------------------------------------------------------------
1 | counter += 1;
24 | }
25 | );
26 |
27 | Event::on(
28 | Queue::class,
29 | Queue::EVENT_AFTER_FETCH,
30 | function ($event) {
31 | $this->counter += 2;
32 | }
33 | );
34 |
35 | Event::on(
36 | Queue::class,
37 | Queue::EVENT_AFTER_DELETE,
38 | function ($event) {
39 | $this->counter += 3;
40 | }
41 | );
42 |
43 | $queue = Yii::createObject([
44 | 'class' => MemoryQueue::class,
45 | ]);
46 |
47 | $this->assertEquals($this->counter, 0);
48 |
49 | /* @var $queue Queues\MemoryQueue */
50 | $queue->post(new Job([
51 | 'route' => function() {
52 | //Do something
53 | }
54 | ]));
55 |
56 | $this->assertEquals($this->counter, 1);
57 |
58 | $job = $queue->fetch();
59 |
60 | $this->assertEquals($this->counter, 3);
61 |
62 | $queue->delete($job);
63 |
64 | $this->assertEquals($this->counter, 6);
65 | }
66 |
67 | }
68 |
--------------------------------------------------------------------------------
/tests/ProcessRunnerTest.php:
--------------------------------------------------------------------------------
1 | /dev/null';
12 | public function testRunnerRunSingle()
13 | {
14 | $runner = $this->getRunner();
15 | $this->assertEquals($runner->getIterator()->count(),0);
16 |
17 | $runner->runProcess(self::CMD);
18 |
19 | $this->assertEquals($runner->getIterator()->count(),0);
20 |
21 | $runner->cleanUpAll();
22 |
23 | $this->assertEquals($runner->getIterator()->count(),0);
24 | }
25 | public function testRunnerRunMultiple()
26 | {
27 | $runner = $this->getRunner(2);
28 |
29 | $this->assertEquals($runner->getIterator()->count(),0);
30 |
31 | $runner->runProcess(self::CMD);
32 | $runner->runProcess(self::CMD);
33 |
34 | $this->assertEquals($runner->getIterator()->count(),2);
35 |
36 | $runner->cleanUpAll();
37 | $this->assertEquals($runner->getIterator()->count(),0);
38 | }
39 | public function testRunnerCleanUpProcess()
40 | {
41 | $runner = $this->getRunner();
42 |
43 | $process = new Process(self::CMD);
44 | $process->run();
45 |
46 | $runner->addProcess($process);
47 | $this->assertEquals($runner->getIterator()->count(),1);
48 |
49 | $runner->cleanUpProc($process, $process->getPid());
50 |
51 | $this->assertEquals($runner->getIterator()->count(),0);
52 | }
53 | public function testRunnerPropagateSignals()
54 | {
55 | $runner = $this->getRunner(2);
56 | $start = time();
57 |
58 | $runner->runProcess('setsid php -r "sleep(10);" > /dev/null');
59 | $this->assertEquals($runner->getIterator()->count(),1);
60 | $runner->runProcess('setsid php -r "sleep(10);" > /dev/null');
61 | $this->assertEquals($runner->getIterator()->count(),2);
62 |
63 | $runner->cleanUpAll(SIGKILL, true);
64 | $duration = time() - $start;
65 | $this->assertEquals($runner->getIterator()->count(),0);
66 | $this->assertLessThan(10, $duration);
67 | }
68 | protected function getRunner($proc = 1)
69 | {
70 | $queue = Yii::createObject([
71 | 'class' => '\UrbanIndo\Yii2\Queue\Queues\MemoryQueue',
72 | 'maxProcesses' => $proc,
73 | ]);
74 | $runner = new ProcessRunner();
75 | $runner->setQueue($queue);
76 |
77 | return $runner;
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/tests/QueueTest.php:
--------------------------------------------------------------------------------
1 | expectException(\yii\base\Exception::class);
16 | $queue = Yii::createObject([
17 | 'class' => MemoryQueue::class,
18 | ]);
19 |
20 | /* @var $queue \UrbanIndo\Yii2\Queue\Queues\MemoryQueue */
21 | $queue->post(new Job([
22 | 'route' => function() {
23 | throw new \Exception('Test');
24 | }
25 | ]));
26 | $this->assertEquals(1, $queue->getSize());
27 | $job = $queue->fetch();
28 | $this->assertEquals(0, $queue->getSize());
29 | $queue->run($job);
30 | }
31 | }
32 |
33 |
34 |
--------------------------------------------------------------------------------
/tests/Queues/DbQueueTest.php:
--------------------------------------------------------------------------------
1 | firstNameMale;
22 | $this->mockApplication([
23 | 'components' => [
24 | 'db' => [
25 | 'class' => '\yii\db\Connection',
26 | 'dsn' => 'mysql:host=127.0.0.1;dbname=test',
27 | 'username' => 'test',
28 | 'password' => 'test',
29 | ],
30 | 'queue' => [
31 | 'class' => '\UrbanIndo\Yii2\Queue\Queues\DbQueue',
32 | 'tableName' => $tableName,
33 | ]
34 | ]
35 | ]);
36 |
37 | if (in_array($tableName, Yii::$app->db->getSchema()->getTableNames())) {
38 | Yii::$app->db->createCommand()->dropTable($tableName)->execute();
39 | }
40 | Yii::$app->db->createCommand()
41 | ->createTable($tableName, [
42 | 'id' => 'BIGINT NOT NULL PRIMARY KEY AUTO_INCREMENT',
43 | 'status' => 'TINYINT NOT NULL DEFAULT 0',
44 | 'timestamp' => 'DATETIME NOT NULL',
45 | 'data' => 'LONGBLOB',
46 | ])->execute();
47 |
48 | }
49 |
50 | protected function tearDown()
51 | {
52 | parent::tearDown();
53 | Yii::$app->db->createCommand()
54 | ->dropTable(Yii::$app->queue->tableName);
55 | }
56 |
57 | /**
58 | *
59 | * @return DbQueue
60 | */
61 | protected function getQueue()
62 | {
63 | return Yii::$app->queue;
64 | }
65 |
66 | protected function countTable($condition = null) {
67 | $query = (new yii\db\Query)
68 | ->select('COUNT(*)')
69 | ->from($this->getQueue()->tableName);
70 | if ($condition) {
71 | $query->where($condition);
72 | }
73 | return $query->scalar();
74 | }
75 |
76 | public function testPost()
77 | {
78 | $queue = $this->getQueue();
79 | $db = Yii::$app->db;
80 | $tableName = $queue->tableName;
81 |
82 | $this->assertEquals(0, $this->countTable());
83 |
84 | $queue->post(new Job(['route' => function () {
85 | DbQueueTest::$counter += 1;
86 | }]));
87 |
88 | $this->assertEquals(1, $this->countTable());
89 |
90 | $this->assertEquals(1, $this->countTable(['status' => DbQueue::STATUS_READY]));
91 |
92 | $queue->post(new Job(['route' => function () {
93 | DbQueueTest::$counter += 1;
94 | }]));
95 |
96 | $this->assertEquals(2, $this->countTable());
97 |
98 | $this->assertEquals(2, $this->countTable(['status' => DbQueue::STATUS_READY]));
99 | }
100 |
101 | public function testFetch()
102 | {
103 | $queue = $this->getQueue();
104 | $db = Yii::$app->db;
105 | $tableName = $queue->tableName;
106 |
107 | $this->assertEquals(0, $this->countTable());
108 |
109 | $job = $queue->fetch();
110 |
111 | $this->assertFalse($job);
112 |
113 | $this->assertEquals(0, $this->countTable(['status' => DbQueue::STATUS_ACTIVE]));
114 |
115 | $queue->post(new Job(['route' => function () {
116 | $this->counter += 1;
117 | }]));
118 |
119 | $job = $queue->fetch();
120 |
121 | $this->assertEquals(1, $this->countTable(['status' => DbQueue::STATUS_ACTIVE]));
122 |
123 | $this->assertTrue($job instanceof Job);
124 | }
125 |
126 | public function testRun()
127 | {
128 | $queue = $this->getQueue();
129 | $db = Yii::$app->db;
130 | $tableName = $queue->tableName;
131 |
132 | $this->assertEquals(0, $this->countTable());
133 |
134 | $job = $queue->fetch();
135 |
136 | $this->assertFalse($job);
137 |
138 | $this->assertEquals(0, $this->countTable(['status' => DbQueue::STATUS_ACTIVE]));
139 |
140 | $queue->post(new Job(['route' => function () {
141 | DbQueueTest::$counter += 1;
142 | }]));
143 |
144 | $job = $queue->fetch();
145 |
146 | $this->assertEquals(1, $this->countTable(['status' => DbQueue::STATUS_ACTIVE]));
147 |
148 | $this->assertTrue($job instanceof Job);
149 |
150 | $queue->run($job);
151 |
152 | $this->assertEquals(1, DbQueueTest::$counter);
153 |
154 | $queue->post(new Job(['route' => function () {
155 | DbQueueTest::$counter += 2;
156 | }]));
157 |
158 | $job = $queue->fetch();
159 |
160 | $queue->run($job);
161 |
162 | $this->assertEquals(3, DbQueueTest::$counter);
163 | }
164 |
165 | public function testHardDelete()
166 | {
167 | $queue = $this->getQueue();
168 | $db = Yii::$app->db;
169 | $tableName = $queue->tableName;
170 |
171 | $this->assertEquals(0, $this->countTable());
172 |
173 | $queue->post(new Job(['route' => function () {
174 | DbQueueTest::$counter += 1;
175 | }]));
176 |
177 | $queue->post(new Job(['route' => function () {
178 | DbQueueTest::$counter += 1;
179 | }]));
180 |
181 | $this->assertEquals(2, $this->countTable());
182 |
183 | $job = $queue->fetch();
184 |
185 | $this->assertEquals(2, $this->countTable());
186 |
187 | $queue->delete($job);
188 |
189 | $this->assertEquals(1, $this->countTable());
190 |
191 | }
192 |
193 | public function testSoftDelete()
194 | {
195 | $queue = $this->getQueue();
196 | $queue->hardDelete = false;
197 | $db = Yii::$app->db;
198 | $tableName = $queue->tableName;
199 |
200 | $this->assertEquals(0, $this->countTable());
201 |
202 | $queue->post(new Job(['route' => function () {
203 | DbQueueTest::$counter += 1;
204 | }]));
205 |
206 | $queue->post(new Job(['route' => function () {
207 | DbQueueTest::$counter += 1;
208 | }]));
209 |
210 | $this->assertEquals(2, $this->countTable(['status' => DbQueue::STATUS_READY]));
211 | $this->assertEquals(2, $this->countTable());
212 |
213 | $job = $queue->fetch();
214 |
215 | $this->assertEquals(1, $this->countTable(['status' => DbQueue::STATUS_READY]));
216 | $this->assertEquals(1, $this->countTable(['status' => DbQueue::STATUS_ACTIVE]));
217 | $this->assertEquals(2, $this->countTable());
218 |
219 | $queue->delete($job);
220 |
221 | $this->assertEquals(2, $this->countTable());
222 | $this->assertEquals(1, $this->countTable(['status' => DbQueue::STATUS_READY]));
223 | $this->assertEquals(0, $this->countTable(['status' => DbQueue::STATUS_ACTIVE]));
224 | $this->assertEquals(1, $this->countTable(['status' => DbQueue::STATUS_DELETED]));
225 |
226 | }
227 |
228 | public function testRelease()
229 | {
230 | $queue = $this->getQueue();
231 | $db = Yii::$app->db;
232 | $tableName = $queue->tableName;
233 |
234 | $this->assertEquals(0, $this->countTable());
235 |
236 | $job = $queue->fetch();
237 |
238 | $this->assertFalse($job);
239 |
240 | $this->assertEquals(0, $this->countTable(['status' => DbQueue::STATUS_ACTIVE]));
241 |
242 | $queue->post(new Job(['route' => function () {
243 | DbQueueTest::$counter += 1;
244 | }]));
245 |
246 | $job = $queue->fetch();
247 | $this->assertTrue($job instanceof Job);
248 |
249 | $this->assertEquals(1, $this->countTable(['status' => DbQueue::STATUS_ACTIVE]));
250 |
251 | $queue->release($job);
252 |
253 | $this->assertEquals(1, $this->countTable(['status' => DbQueue::STATUS_READY]));
254 | }
255 | }
256 |
--------------------------------------------------------------------------------
/tests/Queues/MemoryQueueTest.php:
--------------------------------------------------------------------------------
1 | mockApplication([
20 | 'components' => [
21 | 'queue' => [
22 | 'class' => '\UrbanIndo\Yii2\Queue\Queues\MemoryQueue',
23 | ]
24 | ]
25 | ]);
26 | }
27 |
28 | /**
29 | *
30 | * @return \UrbanIndo\Yii2\Queue\Queues\MemoryQueue
31 | */
32 | protected function getQueue()
33 | {
34 | return Yii::$app->queue;
35 | }
36 |
37 | public function testPost()
38 | {
39 | $queue = $this->getQueue();
40 |
41 | $this->assertEquals(0, $queue->getSize());
42 |
43 | $queue->post(new Job(['route' => function () {
44 | self::$counter += 1;
45 | }]));
46 |
47 | $this->assertEquals(1, $queue->getSize());
48 |
49 | $queue->post(new Job(['route' => function () {
50 | self::$counter += 1;
51 | }]));
52 |
53 | $this->assertEquals(2, $queue->getSize());
54 | }
55 |
56 | public function testFetch()
57 | {
58 | $queue = $this->getQueue();
59 |
60 | $this->assertEquals(0, $queue->getSize());
61 |
62 | $job = $queue->fetch();
63 |
64 | $this->assertFalse($job);
65 |
66 | $queue->post(new Job(['route' => function () {
67 | $this->counter += 1;
68 | }]));
69 |
70 | $this->assertEquals(1, $queue->getSize());
71 |
72 | $job = $queue->fetch();
73 |
74 | $this->assertEquals(0, $queue->getSize());
75 |
76 | $this->assertTrue($job instanceof Job);
77 | }
78 |
79 | public function testRun()
80 | {
81 | $queue = $this->getQueue();
82 |
83 | $this->assertEquals(0, $queue->getSize());
84 |
85 | $job = $queue->fetch();
86 |
87 | $this->assertFalse($job);
88 |
89 | $queue->post(new Job(['route' => function () {
90 | self::$counter += 1;
91 | }]));
92 |
93 | $job = $queue->fetch();
94 |
95 | $this->assertTrue($job instanceof Job);
96 |
97 | $queue->run($job);
98 |
99 | $this->assertEquals(1, self::$counter);
100 |
101 | $queue->post(new Job(['route' => function () {
102 | self::$counter += 2;
103 | }]));
104 |
105 | $job = $queue->fetch();
106 |
107 | $queue->run($job);
108 |
109 | $this->assertEquals(3, self::$counter);
110 | }
111 |
112 | public function testDelete()
113 | {
114 | $queue = $this->getQueue();
115 |
116 | $this->assertEquals(0, $queue->getSize());
117 |
118 | $queue->post(new Job(['route' => function () {
119 | self::$counter += 1;
120 | }]));
121 |
122 | $queue->post(new Job(['route' => function () {
123 | self::$counter += 1;
124 | }]));
125 |
126 | $this->assertEquals(2, $queue->getSize());
127 |
128 | $job = $queue->fetch();
129 |
130 | $this->assertEquals(1, $queue->getSize());
131 |
132 | $queue->delete($job);
133 |
134 | $this->assertEquals(1, $queue->getSize());
135 |
136 | }
137 |
138 | public function testRelease()
139 | {
140 | $queue = $this->getQueue();
141 |
142 | $this->assertEquals(0, $queue->getSize());
143 |
144 | $job = $queue->fetch();
145 |
146 | $this->assertFalse($job);
147 |
148 | $queue->post(new Job(['route' => function () {
149 | self::$counter += 1;
150 | }]));
151 |
152 | $job = $queue->fetch();
153 |
154 | $this->assertEquals(0, $queue->getSize());
155 |
156 | $this->assertTrue($job instanceof Job);
157 |
158 | $queue->release($job);
159 |
160 | $this->assertEquals(1, $queue->getSize());
161 |
162 | }
163 | }
164 |
--------------------------------------------------------------------------------
/tests/Queues/MultipleQueueTest.php:
--------------------------------------------------------------------------------
1 | MultipleQueue::class,
20 | 'queues' => [
21 | [
22 | 'class' => MemoryQueue::class,
23 | ],
24 | [
25 | 'class' => MemoryQueue::class,
26 | ],
27 | [
28 | 'class' => MemoryQueue::class,
29 | ],
30 | [
31 | 'class' => MemoryQueue::class,
32 | ]
33 | ],
34 | 'strategy' => [
35 | 'class' => RandomStrategy::class,
36 | ]
37 | ]);
38 |
39 | $this->assertTrue($queue instanceof MultipleQueue);
40 | /* @var $queue MultipleQueue */
41 | $this->assertCount(4, $queue->queues);
42 | foreach($queue->queues as $tqueue) {
43 | $this->assertTrue($tqueue instanceof MemoryQueue);
44 | }
45 | $this->assertTrue($queue->strategy instanceof Strategy);
46 | $this->assertTrue($queue->strategy instanceof RandomStrategy);
47 |
48 | $queue0 = $queue->getQueue(0);
49 | $this->assertTrue($queue0 instanceof MemoryQueue);
50 | $queue4 = $queue->getQueue(4);
51 | $this->assertNull($queue4);
52 |
53 | $njob = $queue->strategy->fetch();
54 | $this->assertFalse($njob);
55 | $i = 0;
56 | $queue->post(new Job([
57 | 'route' => function() use (&$i) {
58 | $i += 1;
59 | }
60 | ]));
61 | do {
62 | //this some times will exist
63 | $fjob1 = $queue->fetch();
64 | } while ($fjob1 == false);
65 | $this->assertTrue($fjob1 instanceof Job);
66 | /* @var $fjob1 Job */
67 | $index = $fjob1->header[MultipleQueue::HEADER_MULTIPLE_QUEUE_INDEX];
68 | $this->assertContains($index, range(0, 3));
69 | $fjob1->runCallable();
70 | $this->assertEquals(1, $i);
71 |
72 | $job = new Job([
73 | 'route' => function() use (&$i) {
74 | $i += 1;
75 | }
76 | ]);
77 | $queue->postToQueue($job, 3);
78 |
79 | do {
80 | //this some times will exist
81 | $fjob2 = $queue->fetch();
82 | } while ($fjob2 == false);
83 | $this->assertTrue($fjob2 instanceof Job);
84 | $index2 = $fjob2->header[MultipleQueue::HEADER_MULTIPLE_QUEUE_INDEX];
85 | $this->assertEquals(3, $index2);
86 | $fjob2->runCallable();
87 | $this->assertEquals(2, $i);
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/tests/Queues/RedisQueueTest.php:
--------------------------------------------------------------------------------
1 | firstNameMale;
22 | $this->mockApplication([
23 | 'components' => [
24 | 'redis' => [
25 | 'class' => Connection::class,
26 | 'hostname' => 'localhost',
27 | 'port' => 6379,
28 | ],
29 | 'queue' => [
30 | 'class' => RedisQueue::class,
31 | 'key' => $queueName,
32 | ]
33 | ]
34 | ]);
35 | }
36 |
37 | /**
38 | *
39 | * @return \UrbanIndo\Yii2\Queue\Queues\RedisQueue
40 | */
41 | public function getQueue()
42 | {
43 | return Yii::$app->queue;
44 | }
45 |
46 | public function getCountItems()
47 | {
48 | $queue = $this->getQueue();
49 | $key = $queue->key;
50 | return Yii::$app->redis->llen($key);
51 | }
52 |
53 | public function testPost()
54 | {
55 | $queue = $this->getQueue();
56 | $this->assertEquals(0, $this->getCountItems());
57 |
58 | $queue->post(new Job(['route' => function () {
59 | RedisQueueTest::$counter += 1;
60 | }]));
61 | $this->assertEquals(1, $this->getCountItems());
62 |
63 | $queue->post(new Job(['route' => function () {
64 | RedisQueueTest::$counter += 1;
65 | }]));
66 | $this->assertEquals(2, $this->getCountItems());
67 | }
68 |
69 | public function testFetch()
70 | {
71 | $queue = $this->getQueue();
72 | $key = $queue->key;
73 | $this->assertEquals(0, $this->getCountItems());
74 |
75 | $job = $queue->fetch();
76 | $this->assertFalse($job);
77 |
78 | $queue->post(new Job(['route' => function () {
79 | RedisQueueTest::$counter += 1;
80 | }]));
81 | $this->assertEquals(1, $this->getCountItems());
82 |
83 | $queue->post(new Job(['route' => function () {
84 | RedisQueueTest::$counter += 1;
85 | }]));
86 | $this->assertEquals(2, $this->getCountItems());
87 |
88 | $job = $queue->fetch();
89 | $this->assertTrue($job instanceof Job);
90 |
91 | $this->assertEquals(1, $this->getCountItems());
92 | }
93 |
94 | public function testRun()
95 | {
96 | $queue = $this->getQueue();
97 |
98 | $job = $queue->fetch();
99 |
100 | $this->assertFalse($job);
101 |
102 | $queue->post(new Job(['route' => function () {
103 | RedisQueueTest::$counter += 1;
104 | }]));
105 |
106 | $job = $queue->fetch();
107 |
108 | $this->assertTrue($job instanceof Job);
109 |
110 | $queue->run($job);
111 |
112 | $this->assertEquals(1, RedisQueueTest::$counter);
113 |
114 | $queue->post(new Job(['route' => function () {
115 | RedisQueueTest::$counter += 2;
116 | }]));
117 |
118 | $job = $queue->fetch();
119 |
120 | $queue->run($job);
121 |
122 | $this->assertEquals(3, RedisQueueTest::$counter);
123 | }
124 |
125 | public function testRelease()
126 | {
127 | $queue = $this->getQueue();
128 | $key = $queue->key;
129 | $this->assertEquals(0, $this->getCountItems());
130 |
131 | $queue->post(new Job(['route' => function () {
132 | RedisQueueTest::$counter += 1;
133 | }]));
134 | $this->assertEquals(1, $this->getCountItems());
135 |
136 | $job = $queue->fetch();
137 |
138 | $this->assertTrue($job instanceof Job);
139 |
140 | $this->assertEquals(0, $this->getCountItems());
141 |
142 | $queue->release($job);
143 |
144 | $this->assertEquals(1, $this->getCountItems());
145 |
146 | $job = $queue->fetch();
147 |
148 | $this->assertEquals(0, $this->getCountItems());
149 | }
150 |
151 | }
152 |
--------------------------------------------------------------------------------
/tests/Queues/SqsQueueTest.php:
--------------------------------------------------------------------------------
1 | assertTrue(true);
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/tests/TestCase.php:
--------------------------------------------------------------------------------
1 | 'testapp',
21 | 'basePath' => __DIR__,
22 | 'vendorPath' => __DIR__ . '/../vendor',
23 | ], $config));
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/tests/bootstrap.php:
--------------------------------------------------------------------------------
1 | 'Yii2 Queue Test',
10 | 'basePath' => dirname(__FILE__),
11 | 'components' => [
12 | 'db' => [
13 | 'class' => '\yii\db\Connection',
14 | 'dsn' => 'sqlite::memory:',
15 | ],
16 | 'queue' => [
17 | 'class' => '\UrbanIndo\Yii2\Queue\Queues\MemoryQueue'
18 | ]
19 | ]
20 | ];
21 |
22 | $application = new yii\console\Application($config);
--------------------------------------------------------------------------------