├── .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 | [![Latest Stable Version](https://poser.pugx.org/urbanindo/yii2-queue/v/stable.svg)](https://packagist.org/packages/urbanindo/yii2-queue) 6 | [![Total Downloads](https://poser.pugx.org/urbanindo/yii2-queue/downloads.svg)](https://packagist.org/packages/urbanindo/yii2-queue) 7 | [![Latest Unstable Version](https://poser.pugx.org/urbanindo/yii2-queue/v/unstable.svg)](https://packagist.org/packages/urbanindo/yii2-queue) 8 | [![Build Status](https://travis-ci.org/urbanindo/yii2-queue.svg)](https://travis-ci.org/urbanindo/yii2-queue) 9 | [![codecov](https://codecov.io/gh/urbanindo/yii2-queue/branch/master/graph/badge.svg)](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); --------------------------------------------------------------------------------