├── .github └── FUNDING.yml ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── composer.json ├── phpunit.xml.dist ├── res ├── mysql │ ├── 10-table.sql │ └── 20-procedure.sql ├── pgsql │ ├── 10-table.sql │ └── 20-function.sql └── sqlite │ └── 10-table.sql ├── src ├── ExceptionalQueue.php ├── InMemoryQueue.php ├── MongoQueue.php ├── NoItemAvailableException.php ├── Pdo │ ├── GenericPdoQueue.php │ ├── PdoQueue.php │ └── SqlitePdoQueue.php ├── PheanstalkQueue.php ├── Queue.php ├── QueueException.php ├── QueueUtils.php ├── RedisQueue.php ├── SysVQueue.php ├── TarantoolQueue.php └── TypeSafeQueue.php └── tests ├── Handler ├── Handler.php ├── MongoHandler.php ├── PdoHandler.php ├── PheanstalkHandler.php ├── RedisHandler.php ├── SysVHandler.php └── TarantoolHandler.php ├── Queue ├── Concurrency.php ├── ExceptionalQueueTest.php ├── InMemoryQueueTest.php ├── MongoQueueTest.php ├── Pdo │ ├── MockPdo.php │ ├── MysqlPdoQueueTest.php │ ├── PdoQueueTest.php │ ├── PgsqlPdoQueueTest.php │ └── SqlitePdoQueueTest.php ├── Performance.php ├── Persistence.php ├── PheanstalkQueueTest.php ├── QueueExceptionTest.php ├── QueueTest.php ├── QueueUtilsTest.php ├── RedisQueueTest.php ├── SysVQueueTest.php ├── TarantoolQueueTest.php ├── TypeSafeQueueTest.php ├── Types.php └── Util.php ├── TimeUtils.php └── worker.php /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [rybakit] 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | composer.lock 3 | phpunit.xml 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | matrix: 4 | include: 5 | - php: 5.4 6 | - php: 5.5 7 | - php: 5.6 8 | - php: hhvm 9 | allow_failures: 10 | - php: hhvm 11 | 12 | services: 13 | - mongodb 14 | - redis-server 15 | 16 | before_install: 17 | # gearman 18 | - > 19 | if [[ $TRAVIS_PHP_VERSION != hhvm* ]]; then 20 | sudo apt-get install python-software-properties; 21 | sudo add-apt-repository -y ppa:gearman-developers/ppa; 22 | fi 23 | 24 | # tarantool (http://stable.tarantool.org/download.html) 25 | - > 26 | if [[ $TRAVIS_PHP_VERSION != hhvm* ]]; then 27 | wget http://tarantool.org/dist/public.key; 28 | sudo apt-key add ./public.key; 29 | release=`lsb_release -c -s`; 30 | echo "deb http://tarantool.org/dist/stable/ubuntu/ $release main" | sudo tee -a /etc/apt/sources.list.d/tarantool.list; 31 | echo "deb-src http://tarantool.org/dist/stable/ubuntu/ $release main" | sudo tee -a /etc/apt/sources.list.d/tarantool.list; 32 | fi 33 | 34 | - sudo apt-get update 35 | 36 | install: 37 | # gearman 38 | - > 39 | if [[ $TRAVIS_PHP_VERSION != hhvm* ]]; then 40 | sudo apt-get install gearman-job-server libgearman-dev; 41 | pecl install gearman; 42 | sudo service gearman-job-server stop; 43 | sudo gearmand -d; 44 | fi 45 | 46 | # tarantool-lts 47 | - > 48 | if [[ $TRAVIS_PHP_VERSION != hhvm* ]]; then 49 | sudo apt-get install tarantool-lts tarantool-lts-client; 50 | sudo wget https://raw.githubusercontent.com/tarantool/queue/stable/init.lua; 51 | sudo wget https://raw.githubusercontent.com/tarantool/queue/stable/tarantool.cfg -O /etc/tarantool/instances.enabled/queue.cfg && echo "script_dir = "`pwd` | sudo tee -a /etc/tarantool/instances.enabled/queue.cfg; 52 | sudo service tarantool-lts restart; 53 | fi 54 | 55 | #tarantool-php 56 | - > 57 | if [[ $TRAVIS_PHP_VERSION != hhvm* ]]; then 58 | git clone https://github.com/tarantool/tarantool-php.git; 59 | cd ./tarantool-php; 60 | git checkout stable; 61 | phpize; 62 | ./configure; 63 | make; 64 | sudo make install; 65 | cd ..; 66 | fi 67 | 68 | # beanstalk 69 | - sudo apt-get install -y beanstalkd 70 | - sudo beanstalkd -d -l 127.0.0.1 -p 11300 71 | 72 | # uopz 73 | - > 74 | if [[ $TRAVIS_PHP_VERSION != hhvm* ]]; then 75 | pecl install uopz; 76 | fi 77 | 78 | # php.ini 79 | - > 80 | if [[ $TRAVIS_PHP_VERSION != hhvm* ]]; then 81 | printf " 82 | extension = mongo.so\n 83 | extension = redis.so\n 84 | extension = tarantool.so\n 85 | " >> ~/.phpenv/versions/$TRAVIS_PHP_VERSION/etc/php.ini; 86 | fi 87 | 88 | before_script: 89 | - mysql -e 'create database phive_tests;' 90 | - psql -c 'create database phive_tests;' -U postgres 91 | 92 | # Mongofill 93 | - > 94 | if [[ $TRAVIS_PHP_VERSION == hhvm* ]]; then 95 | composer require mongofill/mongofill:dev-master; 96 | fi 97 | 98 | - composer install 99 | 100 | # gearman workers 101 | - > 102 | if [[ $TRAVIS_PHP_VERSION != hhvm* ]]; then 103 | (php tests/worker.php >> worker.log &); 104 | (php tests/worker.php >> worker.log &); 105 | (php tests/worker.php >> worker.log &); 106 | (php tests/worker.php >> worker.log &); 107 | fi 108 | 109 | script: 110 | - > 111 | if [[ $TRAVIS_PHP_VERSION == 5.6 ]]; then 112 | phpunit --coverage-clover coverage.clover; 113 | else 114 | phpunit; 115 | fi 116 | 117 | - > 118 | if [[ $TRAVIS_PHP_VERSION != hhvm* ]]; then 119 | phpunit --group concurrency; 120 | fi 121 | 122 | after_script: 123 | - > 124 | if [[ $TRAVIS_PHP_VERSION != hhvm* ]]; then 125 | cat worker.log; 126 | fi 127 | 128 | # code-coverage for scrutinizer-ci 129 | - > 130 | if [[ -f coverage.clover ]]; then 131 | wget https://scrutinizer-ci.com/ocular.phar; 132 | php ocular.phar code-coverage:upload --format=php-clover coverage.clover; 133 | fi 134 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012-2020 Eugene Leonovich 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Phive Queue 2 | =========== 3 | [![Build Status](https://secure.travis-ci.org/rybakit/phive-queue.svg?branch=master)](http://travis-ci.org/rybakit/phive-queue) 4 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/rybakit/phive-queue/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/rybakit/phive-queue/?branch=master) 5 | [![Code Coverage](https://scrutinizer-ci.com/g/rybakit/phive-queue/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/rybakit/phive-queue/?branch=master) 6 | 7 | Phive Queue is a time-based scheduling queue with multiple backend support. 8 | 9 | 10 | ## Table of contents 11 | 12 | * [Installation](#installation) 13 | * [Usage example](#usage-example) 14 | * [Queues](#queues) 15 | * [MongoQueue](#mongoqueue) 16 | * [RedisQueue](#redisqueue) 17 | * [TarantoolQueue](#tarantoolqueue) 18 | * [PheanstalkQueue](#pheanstalkqueue) 19 | * [GenericPdoQueue](#genericpdoqueue) 20 | * [SqlitePdoQueue](#sqlitepdoqueue) 21 | * [SysVQueue](#sysvqueue) 22 | * [InMemoryQueue](#inmemoryqueue) 23 | * [Item types](#item-types) 24 | * [Exceptions](#exceptions) 25 | * [Tests](#tests) 26 | * [Performance](#performance) 27 | * [Concurrency](#concurrency) 28 | * [License](#license) 29 | 30 | 31 | ## Installation 32 | 33 | The recommended way to install Phive Queue is through [Composer](http://getcomposer.org): 34 | 35 | ```sh 36 | $ composer require rybakit/phive-queue 37 | ``` 38 | 39 | 40 | ## Usage example 41 | 42 | ```php 43 | use Phive\Queue\InMemoryQueue; 44 | use Phive\Queue\NoItemAvailableException; 45 | 46 | $queue = new InMemoryQueue(); 47 | 48 | $queue->push('item1'); 49 | $queue->push('item2', new DateTime()); 50 | $queue->push('item3', time()); 51 | $queue->push('item4', '+5 seconds'); 52 | $queue->push('item5', 'next Monday'); 53 | 54 | // get the queue size 55 | $count = $queue->count(); // 5 56 | 57 | // pop items off the queue 58 | // note that is not guaranteed that the items with the same scheduled time 59 | // will be received in the same order in which they were added 60 | $item123 = $queue->pop(); 61 | $item123 = $queue->pop(); 62 | $item123 = $queue->pop(); 63 | 64 | try { 65 | $item4 = $queue->pop(); 66 | } catch (NoItemAvailableException $e) { 67 | // item4 is not yet available 68 | } 69 | 70 | sleep(5); 71 | $item4 = $queue->pop(); 72 | 73 | // clear the queue (will remove 'item5') 74 | $queue->clear(); 75 | ``` 76 | 77 | 78 | ## Queues 79 | 80 | Currently, there are the following queues available: 81 | 82 | * [MongoQueue](#mongoqueue) 83 | * [RedisQueue](#redisqueue) 84 | * [TarantoolQueue](#tarantoolqueue) 85 | * [PheanstalkQueue](#pheanstalkqueue) 86 | * [GenericPdoQueue](#genericpdoqueue) 87 | * [SqlitePdoQueue](#sqlitepdoqueue) 88 | * [SysVQueue](#sysvqueue) 89 | * [InMemoryQueue](#inmemoryqueue) 90 | 91 | #### MongoQueue 92 | 93 | The `MongoQueue` requires the [Mongo PECL](http://pecl.php.net/package/mongo) extension *(v1.3.0 or higher)*. 94 | 95 | 96 | *Tip:* Before making use of the queue, it's highly recommended to create an index on a `eta` field: 97 | 98 | ```sh 99 | $ mongo my_db --eval 'db.my_coll.ensureIndex({ eta: 1 })' 100 | ``` 101 | 102 | ##### Constructor 103 | 104 | ```php 105 | public MongoQueue::__construct(MongoClient $mongoClient, string $dbName, string $collName) 106 | ``` 107 | 108 | Parameters: 109 | 110 | > mongoClient The MongoClient instance
111 | > dbName The database name
112 | > collName The collection name
113 | 114 | ##### Example 115 | 116 | ```php 117 | use Phive\Queue\MongoQueue; 118 | 119 | $client = new MongoClient(); 120 | $queue = new MongoQueue($client, 'my_db', 'my_coll'); 121 | ``` 122 | 123 | #### RedisQueue 124 | 125 | For the `RedisQueue` you have to install the [Redis PECL](http://pecl.php.net/package/redis) extension *(v2.2.3 or higher)*. 126 | 127 | ##### Constructor 128 | 129 | ```php 130 | public RedisQueue::__construct(Redis $redis) 131 | ``` 132 | 133 | Parameters: 134 | 135 | > redis The Redis instance
136 | 137 | ##### Example 138 | 139 | ```php 140 | use Phive\Queue\RedisQueue; 141 | 142 | $redis = new Redis(); 143 | $redis->connect('127.0.0.1'); 144 | $redis->setOption(Redis::OPT_PREFIX, 'my_prefix:'); 145 | 146 | // Since the Redis client v2.2.5 the RedisQueue has the ability to utilize serialization: 147 | // $redis->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_PHP); 148 | 149 | $queue = new RedisQueue($redis); 150 | ``` 151 | 152 | #### TarantoolQueue 153 | 154 | To use the `TarantoolQueue` you have to install the [Tarantool PECL](https://github.com/tarantool/tarantool-php) 155 | extension and a [Lua script](https://github.com/tarantool/queue) for managing queues. 156 | 157 | ##### Constructor 158 | 159 | ```php 160 | public TarantoolQueue::__construct(Tarantool $tarantool, string $tubeName [, int $space = null ]) 161 | ``` 162 | 163 | Parameters: 164 | 165 | > tarantool The Tarantool instance
166 | > tubeName The tube name
167 | > space Optional. The space number. Default to 0 168 | 169 | ##### Example 170 | 171 | ```php 172 | use Phive\Queue\TarantoolQueue; 173 | 174 | $tarantool = new Tarantool('127.0.0.1', 33020); 175 | $queue = new TarantoolQueue($tarantool, 'my_tube'); 176 | ``` 177 | 178 | #### PheanstalkQueue 179 | 180 | The `PheanstalkQueue` requires the [Pheanstalk](https://github.com/pda/pheanstalk) 181 | library ([Beanstalk](http://kr.github.io/beanstalkd) client) to be installed: 182 | 183 | ```sh 184 | $ composer require pda/pheanstalk:~3.0 185 | ``` 186 | 187 | ##### Constructor 188 | 189 | ```php 190 | public PheanstalkQueue::__construct(Pheanstalk\PheanstalkInterface $pheanstalk, string $tubeName) 191 | ``` 192 | 193 | Parameters: 194 | 195 | > pheanstalk The Pheanstalk\PheanstalkInterface instance
196 | > tubeName The tube name
197 | 198 | ##### Example 199 | 200 | ```php 201 | use Pheanstalk\Pheanstalk; 202 | use Phive\Queue\PheanstalkQueue; 203 | 204 | $pheanstalk = new Pheanstalk('127.0.0.1'); 205 | $queue = new PheanstalkQueue($pheanstalk, 'my_tube'); 206 | ``` 207 | 208 | #### GenericPdoQueue 209 | 210 | The `GenericPdoQueue` is intended for PDO drivers whose databases support stored procedures/functions 211 | (in fact all drivers except SQLite). 212 | 213 | The `GenericPdoQueue` requires [PDO](http://php.net/pdo) and a [PDO driver](http://php.net/manual/en/pdo.drivers.php) 214 | for a particular database be installed. On top of that PDO error mode must be set to throw 215 | exceptions (`PDO::ERRMODE_EXCEPTION`). 216 | 217 | SQL files to create the table and the stored routine can be found in the [res](res) directory. 218 | 219 | ##### Constructor 220 | 221 | ```php 222 | public GenericPdoQueue::__construct(PDO $pdo, string $tableName [, string $routineName = null ] ) 223 | ``` 224 | 225 | Parameters: 226 | 227 | > pdo The PDO instance
228 | > tableName The table name
229 | > routineName Optional. The routine name. Default to tableName_pop
230 | 231 | ##### Example 232 | 233 | ```php 234 | use Phive\Queue\Pdo\GenericPdoQueue; 235 | 236 | $pdo = new PDO('pgsql:host=127.0.0.1;port=5432;dbname=my_db', 'db_user', 'db_pass'); 237 | $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); 238 | 239 | $queue = new GenericPdoQueue($pdo, 'my_table', 'my_routine'); 240 | ``` 241 | 242 | #### SqlitePdoQueue 243 | 244 | The `SqlitePdoQueue` requires [PDO](http://php.net/pdo) and [SQLite PDO driver](http://php.net/manual/en/ref.pdo-sqlite.php). 245 | On top of that PDO error mode must be set to throw exceptions (`PDO::ERRMODE_EXCEPTION`). 246 | 247 | SQL file to create the table can be found in the [res/sqlite](res/sqlite) directory. 248 | 249 | *Tip:* For performance reasons it's highly recommended to activate [WAL mode](http://www.sqlite.org/wal.html): 250 | 251 | ```php 252 | $pdo->exec('PRAGMA journal_mode=WAL'); 253 | ``` 254 | 255 | ##### Constructor 256 | 257 | ```php 258 | public SqlitePdoQueue::__construct(PDO $pdo, string $tableName) 259 | ``` 260 | 261 | Parameters: 262 | 263 | > pdo The PDO instance
264 | > tableName The table name
265 | 266 | ##### Example 267 | 268 | ```php 269 | use Phive\Queue\Pdo\SqlitePdoQueue; 270 | 271 | $pdo = new PDO('sqlite:/opt/databases/my_db.sq3'); 272 | $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); 273 | $pdo->exec('PRAGMA journal_mode=WAL'); 274 | 275 | $queue = new SqlitePdoQueue($pdo, 'my_table'); 276 | ``` 277 | 278 | #### SysVQueue 279 | 280 | The `SysVQueue` requires PHP to be compiled with the option **--enable-sysvmsg**. 281 | 282 | ##### Constructor 283 | 284 | ```php 285 | public SysVQueue::__construct(int $key [, bool $serialize = null [, int $perms = null ]] ) 286 | ``` 287 | 288 | Parameters: 289 | 290 | > key The message queue numeric ID
291 | > serialize Optional. Whether to serialize an item or not. Default to false
292 | > perms Optional. The queue permissions. Default to 0666
293 | 294 | ##### Example 295 | 296 | ```php 297 | use Phive\Queue\SysVQueue; 298 | 299 | $queue = new SysVQueue(123456); 300 | ``` 301 | 302 | #### InMemoryQueue 303 | 304 | The `InMemoryQueue` can be useful in cases where the persistence is not needed. It exists only in RAM 305 | and therefore operates faster than other queues. 306 | 307 | ##### Constructor 308 | 309 | ```php 310 | public InMemoryQueue::__construct() 311 | ``` 312 | 313 | ##### Example 314 | 315 | ```php 316 | use Phive\Queue\InMemoryQueue; 317 | 318 | $queue = new InMemoryQueue(); 319 | ``` 320 | 321 | 322 | ## Item types 323 | 324 | The following table details the various item types supported across queues. 325 | 326 | | Queue/Type | string | binary string | null | bool | int | float | array | object | 327 | |---------------------------------------|:-------:|:-------------:|:-------:|:-------:|:-------:|:-------:|:-------:|:-------:| 328 | | [MongoQueue](#mongoqueue) | ✓ | | ✓ | ✓ | ✓ | ✓ | ✓ | | 329 | | [RedisQueue](#redisqueue) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓* | ✓* | 330 | | [TarantoolQueue](#tarantoolqueue) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | | | 331 | | [PheanstalkQueue](#pheanstalkqueue) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | | | 332 | | [GenericPdoQueue](#genericpdoqueue) | ✓ | | ✓ | ✓ | ✓ | ✓ | | | 333 | | [SqlitePdoQueue](#sqlitepdoqueue) | ✓ | | ✓ | ✓ | ✓ | ✓ | | | 334 | | [SysVQueue](#sysvqueue) | ✓ | ✓ | ✓* | ✓ | ✓ | ✓ | ✓* | ✓* | 335 | | [InMemoryQueue](#inmemoryqueue) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | 336 | 337 | > ✓* — supported if the serializer is enabled. 338 | 339 | To bypass the limitation of unsupported types for the particular queue you could convert an item 340 | to a non-binary string before pushing it and then back after popping. The library ships with 341 | the `TypeSafeQueue` decorator which does that for you: 342 | 343 | ```php 344 | use Phive\Queue\GenericPdoQueue; 345 | use Phive\Queue\TypeSafeQueue; 346 | 347 | $queue = new GenericPdoQueue(...); 348 | $queue = new TypeSafeQueue($queue); 349 | 350 | $queue->push(['foo' => 'bar']); 351 | $array = $queue->pop(); // ['foo' => 'bar']; 352 | ``` 353 | 354 | 355 | ## Exceptions 356 | 357 | Every queue method declared in the [Queue](src/Queue.php) interface will throw an exception 358 | if a run-time error occurs at the time the method is called. 359 | 360 | For example, in the code below, the `push()` call will fail with a `MongoConnectionException` 361 | exception in a case a remote server unreachable: 362 | 363 | ```php 364 | use Phive\Queue\MongoQueue; 365 | 366 | $queue = new MongoQueue(...); 367 | 368 | // mongodb server goes down here 369 | 370 | $queue->push('item'); // throws MongoConnectionException 371 | ``` 372 | 373 | But sometimes you may want to catch exceptions coming from a queue regardless of the underlying driver. 374 | To do this just wrap your queue object with the `ExceptionalQueue` decorator: 375 | 376 | ```php 377 | use Phive\Queue\ExceptionalQueue; 378 | use Phive\Queue\MongoQueue; 379 | 380 | $queue = new MongoQueue(...); 381 | $queue = new ExceptionalQueue($queue); 382 | 383 | // mongodb server goes down here 384 | 385 | $queue->push('item'); // throws Phive\Queue\QueueException 386 | ``` 387 | 388 | And then, to catch queue level exceptions use the `QueueException` class: 389 | 390 | ```php 391 | use Phive\Queue\QueueException; 392 | 393 | ... 394 | 395 | try { 396 | do_something_with_a_queue(); 397 | } catch (QueueException $e) { 398 | // handle queue exception 399 | } catch (\Exception $e) { 400 | // handle base exception 401 | } 402 | ``` 403 | 404 | 405 | ## Tests 406 | 407 | Phive Queue uses [PHPUnit](http://phpunit.de) for unit and integration testing. 408 | In order to run the tests, you'll first need to install the library dependencies using composer: 409 | 410 | ```sh 411 | $ composer install 412 | ``` 413 | 414 | You can then run the tests: 415 | 416 | ```sh 417 | $ phpunit 418 | ``` 419 | 420 | You may also wish to specify your own default values of some tests (db names, passwords, queue sizes, etc.). 421 | You can do it by setting environment variables from the command line: 422 | 423 | ```sh 424 | $ export PHIVE_PDO_PGSQL_PASSWORD="pgsql_password" 425 | $ export PHIVE_PDO_MYSQL_PASSWORD="mysql_password" 426 | $ phpunit 427 | ``` 428 | 429 | You may also create your own `phpunit.xml` file by copying the [phpunit.xml.dist](phpunit.xml.dist) 430 | file and customize to your needs. 431 | 432 | 433 | #### Performance 434 | 435 | To check the performance of queues run: 436 | 437 | ```sh 438 | $ phpunit --group performance 439 | ``` 440 | 441 | This test inserts a number of items (1000 by default) into a queue, and then retrieves them back. 442 | It measures the average time for `push` and `pop` operations and outputs the resulting stats, e.g.: 443 | 444 | ```sh 445 | RedisQueue::push() 446 | Total operations: 1000 447 | Operations per second: 14031.762 [#/sec] 448 | Time per operation: 71.267 [ms] 449 | Time taken for test: 0.071 [sec] 450 | 451 | RedisQueue::pop() 452 | Total operations: 1000 453 | Operations per second: 16869.390 [#/sec] 454 | Time per operation: 59.279 [ms] 455 | Time taken for test: 0.059 [sec] 456 | . 457 | RedisQueue::push() (delayed) 458 | Total operations: 1000 459 | Operations per second: 15106.226 [#/sec] 460 | Time per operation: 66.198 [ms] 461 | Time taken for test: 0.066 [sec] 462 | 463 | RedisQueue::pop() (delayed) 464 | Total operations: 1000 465 | Operations per second: 14096.416 [#/sec] 466 | Time per operation: 70.940 [ms] 467 | Time taken for test: 0.071 [sec] 468 | ``` 469 | 470 | You may also change the number of items involved in the test by changing the `PHIVE_PERF_QUEUE_SIZE` 471 | value in your `phpunit.xml` file or by setting the environment variable from the command line: 472 | 473 | ```sh 474 | $ PHIVE_PERF_QUEUE_SIZE=5000 phpunit --group performance 475 | ``` 476 | 477 | 478 | #### Concurrency 479 | 480 | In order to check the concurrency you'll have to install the [Gearman](http://gearman.org) server 481 | and the [German PECL](http://pecl.php.net/package/gearman) extension. 482 | Once the server has been installed and started, create a number of processes (workers) by running: 483 | 484 | ```sh 485 | $ php tests/worker.php 486 | ``` 487 | 488 | Then run the tests: 489 | 490 | ```sh 491 | $ phpunit --group concurrency 492 | ``` 493 | 494 | This test inserts a number of items (100 by default) into a queue, and then each worker tries 495 | to retrieve them in parallel. 496 | 497 | You may also change the number of items involved in the test by changing the `PHIVE_CONCUR_QUEUE_SIZE` 498 | value in your `phpunit.xml` file or by setting the environment variable from the command line: 499 | 500 | ```sh 501 | $ PHIVE_CONCUR_QUEUE_SIZE=500 phpunit --group concurrency 502 | ``` 503 | 504 | 505 | ## License 506 | 507 | Phive Queue is released under the MIT License. See the bundled [LICENSE](LICENSE) file for details. 508 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rybakit/phive-queue", 3 | "description": "$queue->push('I can be popped off after', '10 minutes');", 4 | "keywords": ["queue", "schedule", "priority", "delayed", "mongodb", "redis", "tarantool", "beanstalk", "sysv", "postgres", "mysql", "sqlite", "pdo"], 5 | "homepage": "https://github.com/rybakit/phive-queue", 6 | "type": "library", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "Eugene Leonovich", 11 | "email": "gen.work@gmail.com" 12 | } 13 | ], 14 | "require": { 15 | "php": "^5.4|^7.0" 16 | }, 17 | "require-dev": { 18 | "pda/pheanstalk": "~3.0" 19 | }, 20 | "suggest": { 21 | "ext-mongo": ">=1.3.0", 22 | "ext-phpredis": ">=2.2.3", 23 | "ext-tarantool": "", 24 | "ext-pdo": "", 25 | "ext-pdo_mysql": "", 26 | "ext-pdo_pgsql": "", 27 | "ext-pdo_sqlite": "", 28 | "ext-sysvmsg": "", 29 | "pda/pheanstalk": ">=3.0" 30 | }, 31 | "autoload": { 32 | "psr-4": { 33 | "Phive\\Queue\\": "src/" 34 | } 35 | }, 36 | "autoload-dev" : { 37 | "psr-4": { 38 | "Phive\\Queue\\Tests\\": "tests/" 39 | } 40 | }, 41 | "extra": { 42 | "branch-alias": { 43 | "dev-master": "1.0.x-dev" 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 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 | tests 55 | 56 | 57 | 58 | 59 | 60 | concurrency 61 | performance 62 | 63 | 64 | 65 | 66 | 67 | src 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /res/mysql/10-table.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS {{table_name}} CASCADE; 2 | CREATE TABLE {{table_name}}(id SERIAL, eta integer NOT NULL, item text NOT NULL) ENGINE=InnoDB; 3 | -------------------------------------------------------------------------------- /res/mysql/20-procedure.sql: -------------------------------------------------------------------------------- 1 | DROP PROCEDURE IF EXISTS {{routine_name}}; 2 | CREATE PROCEDURE {{routine_name}}(IN now int) 3 | BEGIN 4 | DECLARE found_id int; 5 | DECLARE found_item text; 6 | 7 | START TRANSACTION; 8 | 9 | SELECT id, item INTO found_id, found_item 10 | FROM {{table_name}} 11 | WHERE eta <= now 12 | ORDER BY eta 13 | LIMIT 1 14 | FOR UPDATE; 15 | 16 | IF found_id IS NOT NULL THEN 17 | DELETE FROM {{table_name}} 18 | WHERE id = found_id; 19 | SELECT found_item; 20 | ELSE 21 | SELECT NULL LIMIT 0; 22 | END IF; 23 | 24 | COMMIT; 25 | END 26 | -------------------------------------------------------------------------------- /res/pgsql/10-table.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS {{table_name}} CASCADE; 2 | CREATE TABLE {{table_name}}(id SERIAL, eta integer NOT NULL, item text NOT NULL); 3 | -------------------------------------------------------------------------------- /res/pgsql/20-function.sql: -------------------------------------------------------------------------------- 1 | CREATE OR REPLACE FUNCTION {{routine_name}}(now {{table_name}}.eta%type) 2 | RETURNS SETOF {{table_name}} AS $$ 3 | DECLARE 4 | r {{table_name}}%rowtype; 5 | BEGIN 6 | SELECT * INTO r 7 | FROM {{table_name}} 8 | WHERE eta <= now 9 | ORDER BY eta 10 | LIMIT 1 11 | FOR UPDATE; 12 | 13 | IF FOUND THEN 14 | DELETE FROM {{table_name}} WHERE id = r.id; 15 | RETURN NEXT r; 16 | END IF; 17 | END; 18 | $$ LANGUAGE plpgsql; 19 | -------------------------------------------------------------------------------- /res/sqlite/10-table.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS {{table_name}}; 2 | CREATE TABLE {{table_name}}(id INTEGER PRIMARY KEY AUTOINCREMENT, eta integer NOT NULL, item text NOT NULL); 3 | -------------------------------------------------------------------------------- /src/ExceptionalQueue.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Phive\Queue; 13 | 14 | class ExceptionalQueue implements Queue 15 | { 16 | /** 17 | * @var Queue 18 | */ 19 | private $queue; 20 | 21 | public function __construct(Queue $queue) 22 | { 23 | $this->queue = $queue; 24 | } 25 | 26 | /** 27 | * {@inheritdoc} 28 | */ 29 | public function push($item, $eta = null) 30 | { 31 | $this->exceptional(function () use ($item, $eta) { 32 | $this->queue->push($item, $eta); 33 | }); 34 | } 35 | 36 | /** 37 | * {@inheritdoc} 38 | */ 39 | public function pop() 40 | { 41 | return $this->exceptional(function () { 42 | return $this->queue->pop(); 43 | }); 44 | } 45 | 46 | /** 47 | * {@inheritdoc} 48 | */ 49 | public function count() 50 | { 51 | return $this->exceptional(function () { 52 | return $this->queue->count(); 53 | }); 54 | } 55 | 56 | /** 57 | * {@inheritdoc} 58 | */ 59 | public function clear() 60 | { 61 | $this->exceptional(function () { 62 | $this->queue->clear(); 63 | }); 64 | } 65 | 66 | /** 67 | * @param \Closure $func The function to execute. 68 | * 69 | * @return mixed 70 | * 71 | * @throws QueueException 72 | */ 73 | protected function exceptional(\Closure $func) 74 | { 75 | try { 76 | $result = $func(); 77 | } catch (QueueException $e) { 78 | throw $e; 79 | } catch (\Exception $e) { 80 | throw new QueueException($this->queue, $e->getMessage(), 0, $e); 81 | } 82 | 83 | return $result; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/InMemoryQueue.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Phive\Queue; 13 | 14 | class InMemoryQueue implements Queue 15 | { 16 | /** 17 | * @var \SplPriorityQueue 18 | */ 19 | private $queue; 20 | 21 | /** 22 | * @var int 23 | */ 24 | private $queueOrder; 25 | 26 | public function __construct() 27 | { 28 | $this->queue = new \SplPriorityQueue(); 29 | $this->queueOrder = PHP_INT_MAX; 30 | } 31 | 32 | /** 33 | * {@inheritdoc} 34 | */ 35 | public function push($item, $eta = null) 36 | { 37 | $eta = QueueUtils::normalizeEta($eta); 38 | $this->queue->insert($item, [-$eta, $this->queueOrder--]); 39 | } 40 | 41 | /** 42 | * {@inheritdoc} 43 | */ 44 | public function pop() 45 | { 46 | if (!$this->queue->isEmpty()) { 47 | $this->queue->setExtractFlags(\SplPriorityQueue::EXTR_PRIORITY); 48 | $priority = $this->queue->top(); 49 | 50 | if (time() + $priority[0] >= 0) { 51 | $this->queue->setExtractFlags(\SplPriorityQueue::EXTR_DATA); 52 | 53 | return $this->queue->extract(); 54 | } 55 | } 56 | 57 | throw new NoItemAvailableException($this); 58 | } 59 | 60 | /** 61 | * {@inheritdoc} 62 | */ 63 | public function count() 64 | { 65 | return $this->queue->count(); 66 | } 67 | 68 | /** 69 | * {@inheritdoc} 70 | */ 71 | public function clear() 72 | { 73 | $this->queue = new \SplPriorityQueue(); 74 | $this->queueOrder = PHP_INT_MAX; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/MongoQueue.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Phive\Queue; 13 | 14 | class MongoQueue implements Queue 15 | { 16 | /** 17 | * @var \MongoClient 18 | */ 19 | private $mongoClient; 20 | 21 | /** 22 | * @var string 23 | */ 24 | private $dbName; 25 | 26 | /** 27 | * @var string 28 | */ 29 | private $collName; 30 | 31 | /** 32 | * @var \MongoCollection 33 | */ 34 | private $coll; 35 | 36 | public function __construct(\MongoClient $mongoClient, $dbName, $collName) 37 | { 38 | $this->mongoClient = $mongoClient; 39 | $this->dbName = $dbName; 40 | $this->collName = $collName; 41 | } 42 | 43 | /** 44 | * {@inheritdoc} 45 | */ 46 | public function push($item, $eta = null) 47 | { 48 | $doc = [ 49 | 'eta' => QueueUtils::normalizeEta($eta), 50 | 'item' => $item, 51 | ]; 52 | 53 | $this->getCollection()->insert($doc); 54 | } 55 | 56 | /** 57 | * {@inheritdoc} 58 | */ 59 | public function pop() 60 | { 61 | $result = $this->getCollection()->findAndModify( 62 | ['eta' => ['$lte' => time()]], 63 | [], 64 | ['item' => 1, '_id' => 0], 65 | ['remove' => 1, 'sort' => ['eta' => 1]] 66 | ); 67 | 68 | if ($result && array_key_exists('item', $result)) { 69 | return $result['item']; 70 | } 71 | 72 | throw new NoItemAvailableException($this); 73 | } 74 | 75 | /** 76 | * {@inheritdoc} 77 | */ 78 | public function count() 79 | { 80 | return $this->getCollection()->count(); 81 | } 82 | 83 | /** 84 | * {@inheritdoc} 85 | */ 86 | public function clear() 87 | { 88 | $this->getCollection()->remove(); 89 | } 90 | 91 | protected function getCollection() 92 | { 93 | if (!$this->coll) { 94 | $this->coll = $this->mongoClient->selectCollection( 95 | $this->dbName, 96 | $this->collName 97 | ); 98 | } 99 | 100 | return $this->coll; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/NoItemAvailableException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Phive\Queue; 13 | 14 | class NoItemAvailableException extends QueueException 15 | { 16 | public function __construct(Queue $queue, $message = null, $code = null, \Exception $previous = null) 17 | { 18 | parent::__construct($queue, $message ?: 'No items are available in the queue.', $code, $previous); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Pdo/GenericPdoQueue.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Phive\Queue\Pdo; 13 | 14 | use Phive\Queue\NoItemAvailableException; 15 | 16 | class GenericPdoQueue extends PdoQueue 17 | { 18 | /** 19 | * @var array 20 | */ 21 | protected static $popSqls = [ 22 | 'pgsql' => 'SELECT item FROM %s(%d)', 23 | 'firebird' => 'SELECT item FROM %s(%d)', 24 | 'informix' => 'EXECUTE PROCEDURE %s(%d)', 25 | 'mysql' => 'CALL %s(%d)', 26 | 'cubrid' => 'CALL %s(%d)', 27 | 'ibm' => 'CALL %s(%d)', 28 | 'oci' => 'CALL %s(%d)', 29 | 'odbc' => 'CALL %s(%d)', 30 | ]; 31 | 32 | /** 33 | * @var string 34 | */ 35 | private $routineName; 36 | 37 | public function __construct(\PDO $pdo, $tableName, $routineName = null) 38 | { 39 | parent::__construct($pdo, $tableName); 40 | 41 | $this->routineName = $routineName ?: $this->tableName.'_pop'; 42 | } 43 | 44 | /** 45 | * {@inheritdoc} 46 | */ 47 | public function pop() 48 | { 49 | $stmt = $this->pdo->query($this->getPopSql()); 50 | $result = $stmt->fetchColumn(); 51 | $stmt->closeCursor(); 52 | 53 | if (false === $result) { 54 | throw new NoItemAvailableException($this); 55 | } 56 | 57 | return $result; 58 | } 59 | 60 | protected function supportsDriver($driverName) 61 | { 62 | return isset(static::$popSqls[$driverName]); 63 | } 64 | 65 | protected function getPopSql() 66 | { 67 | return sprintf( 68 | static::$popSqls[$this->pdo->getAttribute(\PDO::ATTR_DRIVER_NAME)], 69 | $this->routineName, 70 | time() 71 | ); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Pdo/PdoQueue.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Phive\Queue\Pdo; 13 | 14 | use Phive\Queue\Queue; 15 | use Phive\Queue\QueueUtils; 16 | 17 | abstract class PdoQueue implements Queue 18 | { 19 | /** 20 | * @var \PDO 21 | */ 22 | protected $pdo; 23 | 24 | /** 25 | * @var string 26 | */ 27 | protected $tableName; 28 | 29 | public function __construct(\PDO $pdo, $tableName) 30 | { 31 | $driverName = $pdo->getAttribute(\PDO::ATTR_DRIVER_NAME); 32 | 33 | if (!$this->supportsDriver($driverName)) { 34 | throw new \InvalidArgumentException(sprintf( 35 | 'PDO driver "%s" is unsupported by "%s".', 36 | $driverName, 37 | get_class($this) 38 | )); 39 | } 40 | 41 | $this->pdo = $pdo; 42 | $this->tableName = $tableName; 43 | } 44 | 45 | /** 46 | * {@inheritdoc} 47 | */ 48 | public function push($item, $eta = null) 49 | { 50 | $sql = sprintf('INSERT INTO %s (eta, item) VALUES (%d, %s)', 51 | $this->tableName, 52 | QueueUtils::normalizeEta($eta), 53 | $this->pdo->quote($item) 54 | ); 55 | 56 | $this->pdo->exec($sql); 57 | } 58 | 59 | /** 60 | * {@inheritdoc} 61 | */ 62 | public function count() 63 | { 64 | $stmt = $this->pdo->query('SELECT COUNT(*) FROM '.$this->tableName); 65 | $result = $stmt->fetchColumn(); 66 | $stmt->closeCursor(); 67 | 68 | return $result; 69 | } 70 | 71 | /** 72 | * {@inheritdoc} 73 | */ 74 | public function clear() 75 | { 76 | $this->pdo->exec('DELETE FROM '.$this->tableName); 77 | } 78 | 79 | /** 80 | * @param string $driverName 81 | * 82 | * @return bool 83 | */ 84 | abstract protected function supportsDriver($driverName); 85 | } 86 | -------------------------------------------------------------------------------- /src/Pdo/SqlitePdoQueue.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Phive\Queue\Pdo; 13 | 14 | use Phive\Queue\NoItemAvailableException; 15 | 16 | class SqlitePdoQueue extends PdoQueue 17 | { 18 | /** 19 | * {@inheritdoc} 20 | */ 21 | public function pop() 22 | { 23 | $sql = sprintf( 24 | 'SELECT id, item FROM %s WHERE eta <= %d ORDER BY eta LIMIT 1', 25 | $this->tableName, 26 | time() 27 | ); 28 | 29 | $this->pdo->exec('BEGIN IMMEDIATE'); 30 | 31 | try { 32 | $stmt = $this->pdo->query($sql); 33 | $row = $stmt->fetch(\PDO::FETCH_ASSOC); 34 | $stmt->closeCursor(); 35 | 36 | if ($row) { 37 | $sql = sprintf('DELETE FROM %s WHERE id = %d', $this->tableName, $row['id']); 38 | $this->pdo->exec($sql); 39 | } 40 | 41 | $this->pdo->exec('COMMIT'); 42 | } catch (\Exception $e) { 43 | $this->pdo->exec('ROLLBACK'); 44 | throw $e; 45 | } 46 | 47 | if ($row) { 48 | return $row['item']; 49 | } 50 | 51 | throw new NoItemAvailableException($this); 52 | } 53 | 54 | protected function supportsDriver($driverName) 55 | { 56 | return 'sqlite' === $driverName; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/PheanstalkQueue.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Phive\Queue; 13 | 14 | use Pheanstalk\Exception\ServerException; 15 | use Pheanstalk\PheanstalkInterface; 16 | 17 | class PheanstalkQueue implements Queue 18 | { 19 | /** 20 | * @var PheanstalkInterface 21 | */ 22 | private $pheanstalk; 23 | 24 | /** 25 | * @var string 26 | */ 27 | private $tubeName; 28 | 29 | public function __construct(PheanstalkInterface $pheanstalk, $tubeName) 30 | { 31 | $this->pheanstalk = $pheanstalk; 32 | $this->tubeName = $tubeName; 33 | } 34 | 35 | /** 36 | * {@inheritdoc} 37 | */ 38 | public function push($item, $eta = null) 39 | { 40 | $this->pheanstalk->putInTube( 41 | $this->tubeName, 42 | $item, 43 | PheanstalkInterface::DEFAULT_PRIORITY, 44 | QueueUtils::calculateDelay($eta) 45 | ); 46 | } 47 | 48 | /** 49 | * {@inheritdoc} 50 | */ 51 | public function pop() 52 | { 53 | if (!$item = $this->pheanstalk->reserveFromTube($this->tubeName, 0)) { 54 | throw new NoItemAvailableException($this); 55 | } 56 | 57 | $this->pheanstalk->delete($item); 58 | 59 | return $item->getData(); 60 | } 61 | 62 | /** 63 | * {@inheritdoc} 64 | */ 65 | public function count() 66 | { 67 | $stats = $this->pheanstalk->statsTube($this->tubeName); 68 | 69 | return $stats['current-jobs-ready']; 70 | } 71 | 72 | /** 73 | * {@inheritdoc} 74 | */ 75 | public function clear() 76 | { 77 | $this->doClear('ready'); 78 | $this->doClear('buried'); 79 | $this->doClear('delayed'); 80 | } 81 | 82 | protected function doClear($state) 83 | { 84 | try { 85 | while ($item = $this->pheanstalk->{'peek'.$state}($this->tubeName)) { 86 | $this->pheanstalk->delete($item); 87 | } 88 | } catch (ServerException $e) { 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/Queue.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Phive\Queue; 13 | 14 | interface Queue extends \Countable 15 | { 16 | /** 17 | * Adds an item to the queue. 18 | * 19 | * @param mixed $item An item to be added. 20 | * @param mixed $eta The earliest time that an item can be popped. 21 | */ 22 | public function push($item, $eta = null); 23 | 24 | /** 25 | * Removes an item from the queue and returns it. 26 | * 27 | * @return mixed 28 | * 29 | * @throws NoItemAvailableException 30 | */ 31 | public function pop(); 32 | 33 | /** 34 | * Removes all items from the queue. 35 | */ 36 | public function clear(); 37 | } 38 | -------------------------------------------------------------------------------- /src/QueueException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Phive\Queue; 13 | 14 | class QueueException extends \RuntimeException 15 | { 16 | private $queue; 17 | 18 | public function __construct(Queue $queue, $message = null, $code = null, \Exception $previous = null) 19 | { 20 | parent::__construct($message, $code, $previous); 21 | 22 | $this->queue = $queue; 23 | } 24 | 25 | public function getQueue() 26 | { 27 | return $this->queue; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/QueueUtils.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Phive\Queue; 13 | 14 | abstract class QueueUtils 15 | { 16 | /** 17 | * @param mixed $eta 18 | * 19 | * @return int The Unix timestamp. 20 | * 21 | * @throws \InvalidArgumentException 22 | */ 23 | public static function normalizeEta($eta) 24 | { 25 | if (null === $eta) { 26 | return time(); 27 | } 28 | if (is_string($eta)) { 29 | $eta = date_create($eta); 30 | } 31 | if ($eta instanceof \DateTime || $eta instanceof \DateTimeInterface) { 32 | return $eta->getTimestamp(); 33 | } 34 | if (is_int($eta)) { 35 | return $eta; 36 | } 37 | 38 | throw new \InvalidArgumentException('The eta parameter is not valid.'); 39 | } 40 | 41 | /** 42 | * @param mixed $eta 43 | * 44 | * @return int 45 | */ 46 | public static function calculateDelay($eta) 47 | { 48 | if (null === $eta) { 49 | return 0; 50 | } 51 | 52 | $delay = -time() + self::normalizeEta($eta); 53 | 54 | return ($delay < 0) ? 0 : $delay; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/RedisQueue.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Phive\Queue; 13 | 14 | /** 15 | * RedisQueue requires redis server 2.6 or higher. 16 | */ 17 | class RedisQueue implements Queue 18 | { 19 | const SCRIPT_PUSH = <<<'LUA' 20 | local id = redis.call('INCR', KEYS[2]) 21 | return redis.call('ZADD', KEYS[1], ARGV[2], id..':'..ARGV[1]) 22 | LUA; 23 | 24 | const SCRIPT_POP = <<<'LUA' 25 | local items = redis.call('ZRANGEBYSCORE', KEYS[1], '-inf', ARGV[1], 'LIMIT', 0, 1) 26 | if 0 == #items then return -1 end 27 | redis.call('ZREM', KEYS[1], items[1]) 28 | return string.sub(items[1], string.find(items[1], ':') + 1) 29 | LUA; 30 | 31 | /** 32 | * @var \Redis 33 | */ 34 | private $redis; 35 | 36 | public function __construct(\Redis $redis) 37 | { 38 | $this->redis = $redis; 39 | } 40 | 41 | /** 42 | * {@inheritdoc} 43 | */ 44 | public function push($item, $eta = null) 45 | { 46 | $eta = QueueUtils::normalizeEta($eta); 47 | 48 | if (\Redis::SERIALIZER_NONE !== $this->redis->getOption(\Redis::OPT_SERIALIZER)) { 49 | $item = $this->redis->_serialize($item); 50 | } 51 | 52 | $result = $this->redis->evaluate(self::SCRIPT_PUSH, ['items', 'seq', $item, $eta], 2); 53 | $this->assertResult($result); 54 | } 55 | 56 | /** 57 | * {@inheritdoc} 58 | */ 59 | public function pop() 60 | { 61 | $result = $this->redis->evaluate(self::SCRIPT_POP, ['items', time()], 1); 62 | $this->assertResult($result); 63 | 64 | if (-1 === $result) { 65 | throw new NoItemAvailableException($this); 66 | } 67 | 68 | if (\Redis::SERIALIZER_NONE !== $this->redis->getOption(\Redis::OPT_SERIALIZER)) { 69 | return $this->redis->_unserialize($result); 70 | } 71 | 72 | return $result; 73 | } 74 | 75 | /** 76 | * {@inheritdoc} 77 | */ 78 | public function count() 79 | { 80 | $result = $this->redis->zCard('items'); 81 | $this->assertResult($result); 82 | 83 | return $result; 84 | } 85 | 86 | /** 87 | * {@inheritdoc} 88 | */ 89 | public function clear() 90 | { 91 | $result = $this->redis->del(['items', 'seq']); 92 | $this->assertResult($result); 93 | } 94 | 95 | /** 96 | * @param mixed $result 97 | * 98 | * @throws QueueException 99 | */ 100 | protected function assertResult($result) 101 | { 102 | if (false === $result) { 103 | throw new QueueException($this, $this->redis->getLastError()); 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/SysVQueue.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Phive\Queue; 13 | 14 | class SysVQueue implements Queue 15 | { 16 | /** 17 | * @var int 18 | */ 19 | private $key; 20 | 21 | /** 22 | * @var bool 23 | */ 24 | private $serialize; 25 | 26 | /** 27 | * @var int 28 | */ 29 | private $perms = 0666; 30 | 31 | /** 32 | * @var int 33 | */ 34 | private $itemMaxLength = 8192; 35 | 36 | /** 37 | * @var resource 38 | */ 39 | private $queue; 40 | 41 | public function __construct($key, $serialize = null, $perms = null) 42 | { 43 | $this->key = $key; 44 | $this->serialize = (bool) $serialize; 45 | 46 | if (null !== $perms) { 47 | $this->perms = $perms; 48 | } 49 | } 50 | 51 | public function setItemMaxLength($length) 52 | { 53 | $this->itemMaxLength = $length; 54 | } 55 | 56 | /** 57 | * {@inheritdoc} 58 | */ 59 | public function push($item, $eta = null) 60 | { 61 | $eta = QueueUtils::normalizeEta($eta); 62 | 63 | if (!msg_send($this->getQueue(), $eta, $item, $this->serialize, false, $errorCode)) { 64 | throw new QueueException($this, self::getErrorMessage($errorCode), $errorCode); 65 | } 66 | } 67 | 68 | /** 69 | * {@inheritdoc} 70 | */ 71 | public function pop() 72 | { 73 | if (!msg_receive($this->getQueue(), -time(), $eta, $this->itemMaxLength, $item, $this->serialize, MSG_IPC_NOWAIT, $errorCode)) { 74 | throw (MSG_ENOMSG === $errorCode) 75 | ? new NoItemAvailableException($this) 76 | : new QueueException($this, self::getErrorMessage($errorCode), $errorCode); 77 | } 78 | 79 | return $item; 80 | } 81 | 82 | /** 83 | * {@inheritdoc} 84 | */ 85 | public function count() 86 | { 87 | $data = msg_stat_queue($this->getQueue()); 88 | 89 | if (!is_array($data)) { 90 | throw new QueueException($this, 'Failed to get the meta data for the queue.'); 91 | } 92 | 93 | return $data['msg_qnum']; 94 | } 95 | 96 | /** 97 | * {@inheritdoc} 98 | */ 99 | public function clear() 100 | { 101 | if (!msg_remove_queue($this->getQueue())) { 102 | throw new QueueException($this, 'Failed to destroy the queue.'); 103 | } 104 | 105 | $this->queue = null; 106 | } 107 | 108 | private function getQueue() 109 | { 110 | if (!is_resource($this->queue)) { 111 | $this->queue = msg_get_queue($this->key, $this->perms); 112 | 113 | if (!is_resource($this->queue)) { 114 | throw new QueueException($this, 'Failed to create/attach to the queue.'); 115 | } 116 | } 117 | 118 | return $this->queue; 119 | } 120 | 121 | private static function getErrorMessage($errorCode) 122 | { 123 | if ($errorCode) { 124 | return posix_strerror($errorCode).'.'; 125 | } 126 | 127 | $error = error_get_last(); 128 | if ($error && 0 === strpos($error['message'], 'msg_')) { 129 | return preg_replace('/^msg_[^:]+?\:\s/', '', $error['message']); 130 | } 131 | 132 | return 'Unknown error.'; 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/TarantoolQueue.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Phive\Queue; 13 | 14 | class TarantoolQueue implements Queue 15 | { 16 | /** 17 | * @var \Tarantool 18 | */ 19 | private $tarantool; 20 | 21 | /** 22 | * @var string 23 | */ 24 | private $space; 25 | 26 | /** 27 | * @var string 28 | */ 29 | private $tubeName; 30 | 31 | public function __construct(\Tarantool $tarantool, $tubeName, $space = null) 32 | { 33 | $this->tarantool = $tarantool; 34 | $this->space = null === $space ? '0' : (string) $space; 35 | $this->tubeName = $tubeName; 36 | } 37 | 38 | /** 39 | * {@inheritdoc} 40 | */ 41 | public function push($item, $eta = null) 42 | { 43 | // see https://github.com/tarantool/tarantool/issues/336 44 | $item .= ' '; 45 | $eta = QueueUtils::calculateDelay($eta); 46 | 47 | $this->tarantool->call('queue.put', [ 48 | $this->space, 49 | $this->tubeName, 50 | (string) $eta, 51 | '0', 52 | '0', 53 | '0', 54 | $item, 55 | ]); 56 | } 57 | 58 | /** 59 | * {@inheritdoc} 60 | */ 61 | public function pop() 62 | { 63 | $result = $this->tarantool->call('queue.take', [ 64 | $this->space, 65 | $this->tubeName, 66 | '0.00000001', 67 | ]); 68 | 69 | if (0 === $result['count']) { 70 | throw new NoItemAvailableException($this); 71 | } 72 | 73 | $tuple = $result['tuples_list'][0]; 74 | 75 | $this->tarantool->call('queue.delete', [ 76 | $this->space, 77 | $tuple[0], 78 | ]); 79 | 80 | return substr($tuple[3], 0, -9); 81 | } 82 | 83 | /** 84 | * {@inheritdoc} 85 | */ 86 | public function count() 87 | { 88 | $result = $this->tarantool->call('queue.statistics', [ 89 | $this->space, 90 | $this->tubeName, 91 | ]); 92 | 93 | $tuple = $result['tuples_list'][0]; 94 | $index = array_search("space{$this->space}.{$this->tubeName}.tasks.total", $tuple, true); 95 | 96 | return (int) $tuple[$index + 1]; 97 | } 98 | 99 | /** 100 | * {@inheritdoc} 101 | */ 102 | public function clear() 103 | { 104 | $this->tarantool->call('queue.truncate', [$this->space, $this->tubeName]); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/TypeSafeQueue.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Phive\Queue; 13 | 14 | class TypeSafeQueue implements Queue 15 | { 16 | /** 17 | * @var Queue 18 | */ 19 | private $queue; 20 | 21 | public function __construct(Queue $queue) 22 | { 23 | $this->queue = $queue; 24 | } 25 | 26 | /** 27 | * {@inheritdoc} 28 | */ 29 | public function push($item, $eta = null) 30 | { 31 | $item = base64_encode(serialize($item)); 32 | 33 | $this->queue->push($item, $eta); 34 | } 35 | 36 | /** 37 | * {@inheritdoc} 38 | */ 39 | public function pop() 40 | { 41 | $item = $this->queue->pop(); 42 | 43 | return unserialize(base64_decode($item)); 44 | } 45 | 46 | /** 47 | * {@inheritdoc} 48 | */ 49 | public function count() 50 | { 51 | return $this->queue->count(); 52 | } 53 | 54 | /** 55 | * {@inheritdoc} 56 | */ 57 | public function clear() 58 | { 59 | $this->queue->clear(); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /tests/Handler/Handler.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Phive\Queue\Tests\Handler; 13 | 14 | use Phive\Queue\Queue; 15 | 16 | abstract class Handler implements \Serializable 17 | { 18 | /** 19 | * @var array 20 | */ 21 | private $options; 22 | 23 | public function __construct($options = null) 24 | { 25 | $this->options = (array) $options; 26 | $this->configure(); 27 | } 28 | 29 | /** 30 | * @return array 31 | */ 32 | public function getOptions() 33 | { 34 | return $this->options; 35 | } 36 | 37 | /** 38 | * @param string $name 39 | * 40 | * @return mixed 41 | * 42 | * @throws \InvalidArgumentException 43 | */ 44 | public function getOption($name) 45 | { 46 | if (array_key_exists($name, $this->options)) { 47 | return $this->options[$name]; 48 | } 49 | 50 | throw new \InvalidArgumentException(sprintf('Option "%s" is not found.', $name)); 51 | } 52 | 53 | public function serialize() 54 | { 55 | return serialize($this->options); 56 | } 57 | 58 | public function unserialize($data) 59 | { 60 | $this->options = unserialize($data); 61 | $this->configure(); 62 | } 63 | 64 | public function getQueueName(Queue $queue) 65 | { 66 | return get_class($queue); 67 | } 68 | 69 | public function reset() 70 | { 71 | } 72 | 73 | public function clear() 74 | { 75 | } 76 | 77 | protected function configure() 78 | { 79 | } 80 | 81 | /** 82 | * @return Queue 83 | */ 84 | abstract public function createQueue(); 85 | } 86 | -------------------------------------------------------------------------------- /tests/Handler/MongoHandler.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Phive\Queue\Tests\Handler; 13 | 14 | use Phive\Queue\MongoQueue; 15 | 16 | class MongoHandler extends Handler 17 | { 18 | /** 19 | * @var \MongoClient 20 | */ 21 | private $mongoClient; 22 | 23 | /** 24 | * @var \MongoCollection 25 | */ 26 | private $coll; 27 | 28 | public function createQueue() 29 | { 30 | return new MongoQueue( 31 | $this->mongoClient, 32 | $this->getOption('db_name'), 33 | $this->getOption('coll_name') 34 | ); 35 | } 36 | 37 | public function reset() 38 | { 39 | $this->mongoClient->selectDB($this->getOption('db_name'))->drop(); 40 | } 41 | 42 | public function clear() 43 | { 44 | $this->coll->remove(); 45 | } 46 | 47 | protected function configure() 48 | { 49 | $this->mongoClient = new \MongoClient($this->getOption('server')); 50 | $this->coll = $this->mongoClient->selectCollection($this->getOption('db_name'), $this->getOption('coll_name')); 51 | $this->coll->ensureIndex(['eta' => 1]); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /tests/Handler/PdoHandler.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Phive\Queue\Tests\Handler; 13 | 14 | use Phive\Queue\Queue; 15 | 16 | class PdoHandler extends Handler 17 | { 18 | /** 19 | * @var \PDO 20 | */ 21 | private $pdo; 22 | 23 | /** 24 | * @var string 25 | */ 26 | private $driverName; 27 | 28 | public function getQueueName(Queue $queue) 29 | { 30 | return parent::getQueueName($queue).'#'.$this->driverName; 31 | } 32 | 33 | public function getQueueClass() 34 | { 35 | $prefix = 'sqlite' === $this->driverName ? 'Sqlite' : 'Generic'; 36 | 37 | return '\\Phive\\Queue\\Pdo\\'.$prefix.'PdoQueue'; 38 | } 39 | 40 | public function createQueue() 41 | { 42 | $class = $this->getQueueClass(); 43 | 44 | return new $class($this->pdo, $this->getOption('table_name')); 45 | } 46 | 47 | public function reset() 48 | { 49 | $sqlDir = realpath(__DIR__.'/../../res/'.$this->driverName); 50 | 51 | foreach (glob($sqlDir.'/*.sql') as $file) { 52 | $sql = strtr(file_get_contents($file), [ 53 | '{{table_name}}' => $this->getOption('table_name'), 54 | '{{routine_name}}' => $this->getOption('table_name').'_pop', 55 | ]); 56 | 57 | $this->pdo->exec($sql); 58 | } 59 | } 60 | 61 | public function clear() 62 | { 63 | $this->pdo->exec('DELETE FROM '.$this->getOption('table_name')); 64 | } 65 | 66 | protected function configure() 67 | { 68 | $this->pdo = new \PDO( 69 | $this->getOption('dsn'), 70 | $this->getOption('username'), 71 | $this->getOption('password') 72 | ); 73 | $this->pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); 74 | $this->driverName = $this->pdo->getAttribute(\PDO::ATTR_DRIVER_NAME); 75 | 76 | $this->configureDriver(); 77 | } 78 | 79 | protected function configureDriver() 80 | { 81 | switch ($this->driverName) { 82 | case 'sqlite': 83 | $this->pdo->exec('PRAGMA journal_mode=WAL'); 84 | break; 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /tests/Handler/PheanstalkHandler.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Phive\Queue\Tests\Handler; 13 | 14 | use Pheanstalk\Exception\ServerException; 15 | use Pheanstalk\Pheanstalk; 16 | use Phive\Queue\PheanstalkQueue; 17 | 18 | class PheanstalkHandler extends Handler 19 | { 20 | /** 21 | * @var \Pheanstalk\PheanstalkInterface 22 | */ 23 | private $pheanstalk; 24 | 25 | public function createQueue() 26 | { 27 | return new PheanstalkQueue($this->pheanstalk, $this->getOption('tube_name')); 28 | } 29 | 30 | public function clear() 31 | { 32 | $tubeName = $this->getOption('tube_name'); 33 | 34 | $this->doClear($tubeName, 'ready'); 35 | $this->doClear($tubeName, 'buried'); 36 | $this->doClear($tubeName, 'delayed'); 37 | } 38 | 39 | protected function configure() 40 | { 41 | $this->pheanstalk = new Pheanstalk($this->getOption('host'), $this->getOption('port')); 42 | } 43 | 44 | private function doClear($tubeName, $state) 45 | { 46 | try { 47 | while ($item = $this->pheanstalk->{'peek'.$state}($tubeName)) { 48 | $this->pheanstalk->delete($item); 49 | } 50 | } catch (ServerException $e) { 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /tests/Handler/RedisHandler.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Phive\Queue\Tests\Handler; 13 | 14 | use Phive\Queue\RedisQueue; 15 | 16 | class RedisHandler extends Handler 17 | { 18 | /** 19 | * @var \Redis 20 | */ 21 | private $redis; 22 | 23 | public function createQueue() 24 | { 25 | return new RedisQueue($this->redis); 26 | } 27 | 28 | public function clear() 29 | { 30 | $this->redis->setOption(\Redis::OPT_SERIALIZER, \Redis::SERIALIZER_NONE); 31 | $prefix = $this->redis->getOption(\Redis::OPT_PREFIX); 32 | $offset = strlen($prefix); 33 | 34 | $keys = $this->redis->keys('*'); 35 | foreach ($keys as $key) { 36 | $this->redis->del(substr($key, $offset)); 37 | } 38 | } 39 | 40 | public function createRedis() 41 | { 42 | $redis = new \Redis(); 43 | $redis->connect($this->getOption('host'), $this->getOption('port')); 44 | $redis->setOption(\Redis::OPT_PREFIX, $this->getOption('prefix')); 45 | 46 | return $redis; 47 | } 48 | 49 | protected function configure() 50 | { 51 | $this->redis = $this->createRedis(); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /tests/Handler/SysVHandler.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Phive\Queue\Tests\Handler; 13 | 14 | use Phive\Queue\SysVQueue; 15 | 16 | class SysVHandler extends Handler 17 | { 18 | public function createQueue() 19 | { 20 | return new SysVQueue($this->getOption('key')); 21 | } 22 | 23 | public function clear() 24 | { 25 | msg_remove_queue(msg_get_queue($this->getOption('key'))); 26 | } 27 | 28 | public function getMeta() 29 | { 30 | return msg_stat_queue(msg_get_queue($this->getOption('key'))); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tests/Handler/TarantoolHandler.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Phive\Queue\Tests\Handler; 13 | 14 | use Phive\Queue\TarantoolQueue; 15 | 16 | class TarantoolHandler extends Handler 17 | { 18 | /** 19 | * @var \Tarantool 20 | */ 21 | private $tarantool; 22 | 23 | public function createQueue() 24 | { 25 | return new TarantoolQueue( 26 | $this->tarantool, 27 | $this->getOption('tube_name'), 28 | $this->getOption('space') 29 | ); 30 | } 31 | 32 | public function clear() 33 | { 34 | $this->tarantool->call('queue.truncate', [ 35 | $this->getOption('space'), 36 | $this->getOption('tube_name'), 37 | ]); 38 | } 39 | 40 | protected function configure() 41 | { 42 | $this->tarantool = new \Tarantool($this->getOption('host'), $this->getOption('port')); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /tests/Queue/Concurrency.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Phive\Queue\Tests\Queue; 13 | 14 | trait Concurrency 15 | { 16 | use Persistence; 17 | 18 | /** 19 | * @group concurrency 20 | */ 21 | public function testConcurrency() 22 | { 23 | if (!class_exists('GearmanClient', false)) { 24 | $this->markTestSkipped('pecl/gearman is required for this test to run.'); 25 | } 26 | 27 | $client = new \GearmanClient(); 28 | $client->addServer(); 29 | 30 | $workerIds = []; 31 | $poppedItems = []; 32 | $client->setCompleteCallback(function (\GearmanTask $task) use (&$workerIds, &$poppedItems) { 33 | $data = explode(':', $task->data(), 2); 34 | if (!is_array($data) || 2 !== count($data)) { 35 | return; 36 | } 37 | 38 | list($workerId, $item) = $data; 39 | 40 | $workerIds[$workerId] = true; 41 | 42 | if (!isset($poppedItems[$item])) { 43 | $poppedItems[$item] = true; 44 | } 45 | }); 46 | 47 | $queueSize = $this->getConcurrencyQueueSize(); 48 | $this->assertGreaterThan(10, $queueSize, 'Queue size is too small to test concurrency.'); 49 | 50 | $workload = serialize(self::getHandler()); 51 | for ($i = 1; $i <= $queueSize; $i++) { 52 | $this->queue->push($i); 53 | $client->addTask('pop', $workload); 54 | } 55 | 56 | try { 57 | // run the tasks in parallel (assuming multiple workers) 58 | $result = $client->runTasks(); 59 | } catch (\GearmanException $e) { 60 | $result = false; 61 | } 62 | 63 | if (!$result) { 64 | $this->markTestSkipped('Unable to run gearman tasks. Check if gearman server is running.'); 65 | } 66 | 67 | $this->assertEquals($queueSize, count($poppedItems)); 68 | $this->assertGreaterThan(1, count($workerIds), 'Not enough workers to test concurrency.'); 69 | } 70 | 71 | protected function getConcurrencyQueueSize() 72 | { 73 | return (int) getenv('PHIVE_CONCUR_QUEUE_SIZE'); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /tests/Queue/ExceptionalQueueTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Phive\Queue\Tests\Queue; 13 | 14 | use Phive\Queue\ExceptionalQueue; 15 | use Phive\Queue\QueueException; 16 | 17 | class ExceptionalQueueTest extends \PHPUnit_Framework_TestCase 18 | { 19 | use Util; 20 | 21 | protected $innerQueue; 22 | protected $queue; 23 | 24 | /** 25 | * {@inheritdoc} 26 | */ 27 | protected function setUp() 28 | { 29 | $this->innerQueue = $this->getQueueMock(); 30 | $this->queue = new ExceptionalQueue($this->innerQueue); 31 | } 32 | 33 | public function testPush() 34 | { 35 | $item = 'foo'; 36 | 37 | $this->innerQueue->expects($this->once())->method('push') 38 | ->with($this->equalTo($item)); 39 | 40 | $this->queue->push($item); 41 | } 42 | 43 | public function testPop() 44 | { 45 | $this->innerQueue->expects($this->once())->method('pop'); 46 | $this->queue->pop(); 47 | } 48 | 49 | public function testCount() 50 | { 51 | $this->innerQueue->expects($this->once())->method('count') 52 | ->will($this->returnValue(42)); 53 | 54 | $this->assertSame(42, $this->queue->count()); 55 | } 56 | 57 | public function testClear() 58 | { 59 | $this->innerQueue->expects($this->once())->method('clear'); 60 | $this->queue->clear(); 61 | } 62 | 63 | /** 64 | * @dataProvider provideQueueInterfaceMethods 65 | * @expectedException \Phive\Queue\QueueException 66 | */ 67 | public function testThrowOriginalQueueException($method) 68 | { 69 | $this->innerQueue->expects($this->once())->method($method) 70 | ->will($this->throwException(new QueueException($this->innerQueue))); 71 | 72 | $this->callQueueMethod($this->queue, $method); 73 | } 74 | 75 | /** 76 | * @dataProvider provideQueueInterfaceMethods 77 | * @expectedException \Phive\Queue\QueueException 78 | */ 79 | public function testThrowWrappedQueueException($method) 80 | { 81 | $this->innerQueue->expects($this->once())->method($method) 82 | ->will($this->throwException(new \Exception())); 83 | 84 | $this->callQueueMethod($this->queue, $method); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /tests/Queue/InMemoryQueueTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Phive\Queue\Tests\Queue; 13 | 14 | use Phive\Queue\InMemoryQueue; 15 | 16 | class InMemoryQueueTest extends QueueTest 17 | { 18 | use Performance; 19 | 20 | public function createQueue() 21 | { 22 | return new InMemoryQueue(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tests/Queue/MongoQueueTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Phive\Queue\Tests\Queue; 13 | 14 | use Phive\Queue\Tests\Handler\MongoHandler; 15 | 16 | /** 17 | * @requires function MongoClient::connect 18 | */ 19 | class MongoQueueTest extends QueueTest 20 | { 21 | use Performance; 22 | use Concurrency; 23 | 24 | protected function getUnsupportedItemTypes() 25 | { 26 | return [Types::TYPE_BINARY_STRING, Types::TYPE_OBJECT]; 27 | } 28 | 29 | /** 30 | * @dataProvider provideItemsOfUnsupportedTypes 31 | * @expectedException Exception 32 | * @expectedExceptionMessageRegExp /zero-length keys are not allowed|non-utf8 string|Objects are not identical/ 33 | */ 34 | public function testUnsupportedItemType($item, $type) 35 | { 36 | $this->queue->push($item); 37 | 38 | if (Types::TYPE_OBJECT === $type && $item !== $this->queue->pop()) { 39 | throw new \Exception('Objects are not identical'); 40 | } 41 | } 42 | 43 | public static function createHandler(array $config) 44 | { 45 | return new MongoHandler([ 46 | 'server' => $config['PHIVE_MONGO_SERVER'], 47 | 'db_name' => $config['PHIVE_MONGO_DB_NAME'], 48 | 'coll_name' => $config['PHIVE_MONGO_COLL_NAME'], 49 | ]); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /tests/Queue/Pdo/MockPdo.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Phive\Queue\Tests\Queue\Pdo; 13 | 14 | class MockPdo extends \PDO 15 | { 16 | public $driverName; 17 | 18 | public function __construct() 19 | { 20 | } 21 | 22 | public function getAttribute($attribute) 23 | { 24 | if (self::ATTR_DRIVER_NAME === $attribute) { 25 | return $this->driverName; 26 | } 27 | 28 | return parent::getAttribute($attribute); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/Queue/Pdo/MysqlPdoQueueTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Phive\Queue\Tests\Queue\Pdo; 13 | 14 | use Phive\Queue\Tests\Handler\PdoHandler; 15 | 16 | /** 17 | * @requires extension pdo_mysql 18 | */ 19 | class MysqlPdoQueueTest extends PdoQueueTest 20 | { 21 | public static function createHandler(array $config) 22 | { 23 | return new PdoHandler([ 24 | 'dsn' => $config['PHIVE_PDO_MYSQL_DSN'], 25 | 'username' => $config['PHIVE_PDO_MYSQL_USERNAME'], 26 | 'password' => $config['PHIVE_PDO_MYSQL_PASSWORD'], 27 | 'table_name' => $config['PHIVE_PDO_MYSQL_TABLE_NAME'], 28 | ]); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/Queue/Pdo/PdoQueueTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Phive\Queue\Tests\Queue\Pdo; 13 | 14 | use Phive\Queue\NoItemAvailableException; 15 | use Phive\Queue\Tests\Handler\PdoHandler; 16 | use Phive\Queue\Tests\Queue\Concurrency; 17 | use Phive\Queue\Tests\Queue\Performance; 18 | use Phive\Queue\Tests\Queue\QueueTest; 19 | use Phive\Queue\Tests\Queue\Types; 20 | use Phive\Queue\Tests\Queue\Util; 21 | 22 | abstract class PdoQueueTest extends QueueTest 23 | { 24 | use Concurrency; 25 | use Performance; 26 | use Util; 27 | 28 | public function getUnsupportedItemTypes() 29 | { 30 | return [Types::TYPE_BINARY_STRING, Types::TYPE_ARRAY, Types::TYPE_OBJECT]; 31 | } 32 | 33 | /** 34 | * @dataProvider provideItemsOfUnsupportedTypes 35 | * @expectedException PHPUnit_Framework_Exception 36 | * @expectedExceptionMessageRegExp /expects parameter 1 to be string|Binary strings are not identical/ 37 | */ 38 | public function testUnsupportedItemType($item, $type) 39 | { 40 | $this->queue->push($item); 41 | 42 | if (Types::TYPE_BINARY_STRING === $type && $item !== $this->queue->pop()) { 43 | $this->fail('Binary strings are not identical'); 44 | } 45 | } 46 | 47 | /** 48 | * @dataProvider provideQueueInterfaceMethods 49 | */ 50 | public function testThrowExceptionOnMalformedSql($method) 51 | { 52 | $options = self::getHandler()->getOptions(); 53 | $options['table_name'] = uniqid('non_existing_table_name_'); 54 | 55 | $handler = new PdoHandler($options); 56 | $queue = $handler->createQueue(); 57 | 58 | try { 59 | $this->callQueueMethod($queue, $method); 60 | } catch (NoItemAvailableException $e) { 61 | } catch (\PDOException $e) { 62 | return; 63 | } 64 | 65 | $this->fail(); 66 | } 67 | 68 | /** 69 | * @expectedException InvalidArgumentException 70 | * @expectedExceptionMessage PDO driver "foobar" is unsupported 71 | */ 72 | public function testThrowExceptionOnUnsupportedDriver() 73 | { 74 | $pdo = new MockPdo(); 75 | $pdo->driverName = 'foobar'; 76 | 77 | $handler = self::getHandler(); 78 | $class = $handler->getQueueClass(); 79 | 80 | new $class($pdo, $handler->getOption('table_name')); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /tests/Queue/Pdo/PgsqlPdoQueueTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Phive\Queue\Tests\Queue\Pdo; 13 | 14 | use Phive\Queue\Tests\Handler\PdoHandler; 15 | 16 | /** 17 | * @requires extension pdo_pgsql 18 | */ 19 | class PgsqlPdoQueueTest extends PdoQueueTest 20 | { 21 | public static function createHandler(array $config) 22 | { 23 | return new PdoHandler([ 24 | 'dsn' => $config['PHIVE_PDO_PGSQL_DSN'], 25 | 'username' => $config['PHIVE_PDO_PGSQL_USERNAME'], 26 | 'password' => $config['PHIVE_PDO_PGSQL_PASSWORD'], 27 | 'table_name' => $config['PHIVE_PDO_PGSQL_TABLE_NAME'], 28 | ]); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/Queue/Pdo/SqlitePdoQueueTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Phive\Queue\Tests\Queue\Pdo; 13 | 14 | use Phive\Queue\Tests\Handler\PdoHandler; 15 | 16 | /** 17 | * @requires extension pdo_sqlite 18 | */ 19 | class SqlitePdoQueueTest extends PdoQueueTest 20 | { 21 | public static function createHandler(array $config) 22 | { 23 | // Generate a new db file on every method call to prevent 24 | // a "Database schema has changed" error which occurs if any 25 | // other process (e.g. worker) is still using the old db file. 26 | // We also can't use the shared cache mode due to 27 | // @link http://stackoverflow.com/questions/9150319/enable-shared-pager-cache-in-sqlite-using-php-pdo 28 | 29 | return new PdoHandler([ 30 | 'dsn' => sprintf('sqlite:%s/%s.sq3', sys_get_temp_dir(), uniqid('phive_tests_')), 31 | 'username' => null, 32 | 'password' => null, 33 | 'table_name' => 'queue', 34 | ]); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tests/Queue/Performance.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Phive\Queue\Tests\Queue; 13 | 14 | trait Performance 15 | { 16 | /** 17 | * @group performance 18 | * @dataProvider providePerformancePopDelay 19 | */ 20 | public function testPushPopPerformance($delay) 21 | { 22 | $queueSize = static::getPerformanceQueueSize(); 23 | $queueName = preg_replace('~^'.preg_quote(__NAMESPACE__).'\\\|Test$~', '', get_class($this)); 24 | $item = str_repeat('x', static::getPerformanceItemLength()); 25 | 26 | printf("\n%s::push()%s\n", $queueName, $delay ? ' (delayed)' : ''); 27 | 28 | $runtime = $this->benchmarkPush($queueSize, $item, $delay); 29 | $this->printPerformanceResult($queueSize, $runtime); 30 | 31 | if ($delay) { 32 | sleep($delay); 33 | } 34 | 35 | printf("\n%s::pop()%s\n", $queueName, $delay ? ' (delayed)' : ''); 36 | 37 | $start = microtime(true); 38 | for ($i = $queueSize; $i; $i--) { 39 | $this->queue->pop(); 40 | } 41 | 42 | $this->printPerformanceResult($queueSize, microtime(true) - $start); 43 | } 44 | 45 | public function providePerformancePopDelay() 46 | { 47 | return [[0], [1]]; 48 | } 49 | 50 | protected function benchmarkPush($queueSize, $item, $delay) 51 | { 52 | $eta = $delay ? time() + $delay : null; 53 | 54 | $start = microtime(true); 55 | for ($i = $queueSize; $i; $i--) { 56 | $this->queue->push($item, $eta); 57 | } 58 | 59 | return microtime(true) - $start; 60 | } 61 | 62 | protected function printPerformanceResult($total, $runtime) 63 | { 64 | printf(" Total operations: %d\n", $total); 65 | printf(" Operations per second: %01.3f [#/sec]\n", $total / $runtime); 66 | printf(" Time per operation: %01.3f [ms]\n", ($runtime / $total) * 1000000); 67 | printf(" Time taken for test: %01.3f [sec]\n", $runtime); 68 | } 69 | 70 | protected static function getPerformanceQueueSize() 71 | { 72 | return (int) getenv('PHIVE_PERF_QUEUE_SIZE'); 73 | } 74 | 75 | protected static function getPerformanceItemLength() 76 | { 77 | return (int) getenv('PHIVE_PERF_ITEM_LENGTH'); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /tests/Queue/Persistence.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Phive\Queue\Tests\Queue; 13 | 14 | trait Persistence 15 | { 16 | /** 17 | * @var \Phive\Queue\Tests\Handler\Handler 18 | */ 19 | private static $handler; 20 | 21 | protected function setUp() 22 | { 23 | parent::setUp(); 24 | 25 | self::getHandler()->clear(); 26 | } 27 | 28 | /** 29 | * @return \Phive\Queue\Queue 30 | */ 31 | public function createQueue() 32 | { 33 | return self::getHandler()->createQueue(); 34 | } 35 | 36 | /** 37 | * Abstract static class functions are not supported since v5.2. 38 | * 39 | * @param array $config 40 | * 41 | * @return \Phive\Queue\Tests\Handler\Handler 42 | * 43 | * @throws \BadMethodCallException 44 | */ 45 | public static function createHandler(array $config) 46 | { 47 | throw new \BadMethodCallException( 48 | sprintf('Method %s:%s is not implemented.', get_called_class(), __FUNCTION__) 49 | ); 50 | } 51 | 52 | public static function getHandler() 53 | { 54 | if (!self::$handler) { 55 | self::$handler = static::createHandler($_ENV); 56 | } 57 | 58 | return self::$handler; 59 | } 60 | 61 | public static function setUpBeforeClass() 62 | { 63 | parent::setUpBeforeClass(); 64 | 65 | self::getHandler()->reset(); 66 | } 67 | 68 | public static function tearDownAfterClass() 69 | { 70 | parent::tearDownAfterClass(); 71 | 72 | self::$handler = null; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /tests/Queue/PheanstalkQueueTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Phive\Queue\Tests\Queue; 13 | 14 | use Phive\Queue\Tests\Handler\PheanstalkHandler; 15 | 16 | class PheanstalkQueueTest extends QueueTest 17 | { 18 | use Performance; 19 | use Concurrency; 20 | 21 | protected $supportsExpiredEta = false; 22 | 23 | protected function getUnsupportedItemTypes() 24 | { 25 | return [Types::TYPE_ARRAY, Types::TYPE_OBJECT]; 26 | } 27 | 28 | /** 29 | * @dataProvider provideItemsOfUnsupportedTypes 30 | * @expectedException PHPUnit_Framework_Error_Warning 31 | * @expectedExceptionMessage expects parameter 1 to be string 32 | */ 33 | public function testUnsupportedItemType($item) 34 | { 35 | $this->queue->push($item); 36 | } 37 | 38 | public static function createHandler(array $config) 39 | { 40 | return new PheanstalkHandler([ 41 | 'host' => $config['PHIVE_BEANSTALK_HOST'], 42 | 'port' => $config['PHIVE_BEANSTALK_PORT'], 43 | 'tube_name' => $config['PHIVE_BEANSTALK_TUBE_NAME'], 44 | ]); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/Queue/QueueExceptionTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Phive\Queue\Tests\Queue; 13 | 14 | use Phive\Queue\QueueException; 15 | 16 | class QueueExceptionTest extends \PHPUnit_Framework_TestCase 17 | { 18 | use Util; 19 | 20 | /** 21 | * @var \Phive\Queue\Queue 22 | */ 23 | protected $queue; 24 | 25 | /** 26 | * {@inheritdoc} 27 | */ 28 | protected function setUp() 29 | { 30 | $this->queue = $this->getQueueMock(); 31 | } 32 | 33 | public function testQueueExceptionExtendsBaseException() 34 | { 35 | $this->assertInstanceOf('Exception', new QueueException($this->queue)); 36 | } 37 | 38 | public function testGetQueue() 39 | { 40 | $e = new QueueException($this->queue); 41 | 42 | $this->assertEquals($this->queue, $e->getQueue()); 43 | } 44 | 45 | public function testGetMessage() 46 | { 47 | $message = 'Error message'; 48 | $e = new QueueException($this->queue, $message); 49 | 50 | $this->assertEquals($message, $e->getMessage()); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /tests/Queue/QueueTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Phive\Queue\Tests\Queue; 13 | 14 | use Phive\Queue\NoItemAvailableException; 15 | use Phive\Queue\Queue; 16 | use Phive\Queue\Tests\TimeUtils; 17 | 18 | abstract class QueueTest extends \PHPUnit_Framework_TestCase 19 | { 20 | use Util; 21 | 22 | /** 23 | * @var Queue 24 | */ 25 | protected $queue; 26 | 27 | /** 28 | * Whether the queue supports an expired ETA or not. 29 | * 30 | * @var bool 31 | */ 32 | protected $supportsExpiredEta = true; 33 | 34 | /** 35 | * {@inheritdoc} 36 | */ 37 | protected function setUp() 38 | { 39 | $this->queue = $this->createQueue(); 40 | } 41 | 42 | public function testImplementQueueInterface() 43 | { 44 | $this->assertInstanceOf('Phive\Queue\Queue', $this->queue); 45 | } 46 | 47 | public function testPushPop() 48 | { 49 | $this->queue->push('item'); 50 | 51 | $this->assertEquals('item', $this->queue->pop()); 52 | $this->assertNoItemIsAvailable($this->queue); 53 | } 54 | 55 | public function testPopOrder() 56 | { 57 | if ($this->supportsExpiredEta) { 58 | $this->queue->push('item1'); 59 | $this->queue->push('item2', '-1 hour'); 60 | } else { 61 | $this->queue->push('item1', '+3 seconds'); 62 | $this->queue->push('item2'); 63 | } 64 | 65 | $this->assertEquals('item2', $this->queue->pop()); 66 | if (!$this->supportsExpiredEta) { 67 | sleep(3); 68 | } 69 | $this->assertEquals('item1', $this->queue->pop()); 70 | } 71 | 72 | public function testPopDelay() 73 | { 74 | $eta = time() + 3; 75 | 76 | $this->queue->push('item', $eta); 77 | $this->assertNoItemIsAvailable($this->queue); 78 | 79 | TimeUtils::callAt($eta, function () { 80 | $this->assertEquals('item', $this->queue->pop()); 81 | }, !$this->supportsExpiredEta); 82 | } 83 | 84 | public function testPushWithExpiredEta() 85 | { 86 | $this->queue->push('item', time() - 1); 87 | $this->assertEquals('item', $this->queue->pop()); 88 | } 89 | 90 | public function testPushEqualItems() 91 | { 92 | $this->queue->push('item'); 93 | $this->queue->push('item'); 94 | 95 | $this->assertEquals('item', $this->queue->pop()); 96 | $this->assertEquals('item', $this->queue->pop()); 97 | } 98 | 99 | public function testCountAndClear() 100 | { 101 | $this->assertEquals(0, $this->queue->count()); 102 | 103 | for ($i = $count = 5; $i; $i--) { 104 | $this->queue->push('item'.$i); 105 | } 106 | 107 | $this->assertEquals($count, $this->queue->count()); 108 | 109 | $this->queue->clear(); 110 | $this->assertEquals(0, $this->queue->count()); 111 | } 112 | 113 | /** 114 | * @dataProvider provideItemsOfSupportedTypes 115 | */ 116 | public function testSupportItemType($item, $type) 117 | { 118 | $this->queue->push($item); 119 | 120 | if (Types::TYPE_BINARY_STRING === $type) { 121 | // strict comparison 122 | $this->assertSame($item, $this->queue->pop()); 123 | } else { 124 | // loose comparison 125 | $this->assertEquals($item, $this->queue->pop()); 126 | } 127 | } 128 | 129 | protected function assertNoItemIsAvailable(Queue $queue) 130 | { 131 | try { 132 | $queue->pop(); 133 | } catch (NoItemAvailableException $e) { 134 | return; 135 | } 136 | 137 | $this->fail('An expected NoItemAvailableException has not been raised.'); 138 | } 139 | 140 | /** 141 | * @return Queue 142 | */ 143 | abstract public function createQueue(); 144 | } 145 | -------------------------------------------------------------------------------- /tests/Queue/QueueUtilsTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Phive\Queue\Tests\Queue; 13 | 14 | use Phive\Queue\QueueUtils; 15 | 16 | class QueueUtilsTest extends \PHPUnit_Framework_TestCase 17 | { 18 | /** 19 | * @dataProvider provideValidEtas 20 | */ 21 | public function testNormalizeEta($eta, $timestamp) 22 | { 23 | if (is_callable($timestamp)) { 24 | $timestamp = $timestamp(); 25 | } 26 | 27 | $this->assertEquals($timestamp, QueueUtils::normalizeEta($eta)); 28 | } 29 | 30 | /** 31 | * @dataProvider provideInvalidEtas 32 | * @expectedException InvalidArgumentException 33 | * @expectedExceptionMessage The eta parameter is not valid. 34 | */ 35 | public function testNormalizeEtaThrowsException($eta) 36 | { 37 | QueueUtils::normalizeEta($eta); 38 | } 39 | 40 | /** 41 | * @dataProvider provideValidEtas 42 | */ 43 | public function testCalcDelay($eta, $_, $delay) 44 | { 45 | $this->assertEquals($delay, QueueUtils::calculateDelay($eta)); 46 | } 47 | 48 | public function provideValidEtas() 49 | { 50 | $date = new \DateTime(); 51 | $now = $date->getTimestamp(); 52 | 53 | return [ 54 | [0, 0, 0], 55 | [-1, -1, 0], 56 | [null, function () { return time(); }, 0], 57 | [$now, $now, 0], 58 | ['@'.$now, $now, 0], 59 | [$date->format(\DateTime::ISO8601), $now, 0], 60 | ['+1 hour', function () { return time() + 3600; }, 3600], 61 | [$date, $now, 0], 62 | ]; 63 | } 64 | 65 | public function provideInvalidEtas() 66 | { 67 | return [ 68 | [new \stdClass()], 69 | ['invalid eta string'], 70 | [[]], 71 | ]; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /tests/Queue/RedisQueueTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Phive\Queue\Tests\Queue; 13 | 14 | use Phive\Queue\NoItemAvailableException; 15 | use Phive\Queue\QueueException; 16 | use Phive\Queue\RedisQueue; 17 | use Phive\Queue\Tests\Handler\RedisHandler; 18 | 19 | /** 20 | * @requires function Redis::connect 21 | */ 22 | class RedisQueueTest extends QueueTest 23 | { 24 | use Performance; 25 | use Concurrency; 26 | 27 | protected function getUnsupportedItemTypes() 28 | { 29 | return [Types::TYPE_ARRAY, Types::TYPE_OBJECT]; 30 | } 31 | 32 | /** 33 | * @dataProvider provideItemsOfUnsupportedTypes 34 | * @expectedException PHPUnit_Framework_Exception 35 | * @expectedExceptionMessageRegExp /could not be converted to string|Array to string conversion/ 36 | */ 37 | public function testUnsupportedItemType($item) 38 | { 39 | $this->queue->push($item); 40 | } 41 | 42 | /** 43 | * @requires function Redis::_serialize 44 | * @dataProvider provideItemsOfVariousTypes 45 | */ 46 | public function testSupportItemTypeWithSerializerLoose($item) 47 | { 48 | $redis = self::getHandler()->createRedis(); 49 | $queue = new RedisQueue($redis); 50 | 51 | $serializers = [\Redis::SERIALIZER_PHP]; 52 | if (defined('Redis::SERIALIZER_IGBINARY')) { 53 | $serializers[] = \Redis::SERIALIZER_IGBINARY; 54 | } 55 | 56 | foreach ($serializers as $serializer) { 57 | $redis->setOption(\Redis::OPT_SERIALIZER, $serializer); 58 | 59 | $queue->push($item); 60 | $this->assertEquals($item, $queue->pop()); 61 | } 62 | } 63 | 64 | /** 65 | * @dataProvider provideQueueInterfaceMethods 66 | */ 67 | public function testThrowExceptionOnErrorResponse($method) 68 | { 69 | $mock = $this->getMock('Redis'); 70 | 71 | $redisMethods = get_class_methods('Redis'); 72 | foreach ($redisMethods as $redisMethod) { 73 | $mock->expects($this->any())->method($redisMethod)->will($this->returnValue(false)); 74 | } 75 | 76 | $queue = new RedisQueue($mock); 77 | 78 | try { 79 | $this->callQueueMethod($queue, $method); 80 | } catch (NoItemAvailableException $e) { 81 | } catch (QueueException $e) { 82 | return; 83 | } 84 | 85 | $this->fail(); 86 | } 87 | 88 | public static function createHandler(array $config) 89 | { 90 | return new RedisHandler([ 91 | 'host' => $config['PHIVE_REDIS_HOST'], 92 | 'port' => $config['PHIVE_REDIS_PORT'], 93 | 'prefix' => $config['PHIVE_REDIS_PREFIX'], 94 | ]); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /tests/Queue/SysVQueueTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Phive\Queue\Tests\Queue; 13 | 14 | use Phive\Queue\NoItemAvailableException; 15 | use Phive\Queue\QueueException; 16 | use Phive\Queue\SysVQueue; 17 | use Phive\Queue\Tests\Handler\SysVHandler; 18 | 19 | /** 20 | * @requires extension sysvmsg 21 | */ 22 | class SysVQueueTest extends QueueTest 23 | { 24 | use Performance { 25 | Performance::testPushPopPerformance as baseTestPushPopPerformance; 26 | } 27 | use Concurrency; 28 | 29 | protected function getUnsupportedItemTypes() 30 | { 31 | return [Types::TYPE_NULL, Types::TYPE_ARRAY, Types::TYPE_OBJECT]; 32 | } 33 | 34 | /** 35 | * @dataProvider provideItemsOfUnsupportedTypes 36 | * @expectedException Phive\Queue\QueueException 37 | * @expectedExceptionMessageRegExp /^Message parameter must be either a string or a number\./ 38 | */ 39 | public function testUnsupportedItemType($item) 40 | { 41 | @$this->queue->push($item); 42 | } 43 | 44 | /** 45 | * @dataProvider provideItemsOfVariousTypes 46 | */ 47 | public function testSupportItemTypeWithSerializerLoose($item) 48 | { 49 | $handler = self::getHandler(); 50 | $key = $handler->getOption('key'); 51 | 52 | $queue = new SysVQueue($key, true); 53 | 54 | $queue->push($item); 55 | $this->assertEquals($item, $queue->pop()); 56 | } 57 | 58 | /** 59 | * @dataProvider provideQueueInterfaceMethods 60 | */ 61 | public function testThrowExceptionOnMissingResource($method) 62 | { 63 | // force a resource creation 64 | $this->queue->count(); 65 | 66 | self::getHandler()->clear(); 67 | 68 | try { 69 | // suppress notices/warnings triggered by msg_* functions 70 | // to avoid a PHPUnit_Framework_Error_Notice to be thrown 71 | @$this->callQueueMethod($this->queue, $method); 72 | } catch (NoItemAvailableException $e) { 73 | } catch (QueueException $e) { 74 | return; 75 | } 76 | 77 | $this->fail(); 78 | } 79 | 80 | /** 81 | * @requires extension uopz 82 | * @dataProvider provideQueueInterfaceMethods 83 | */ 84 | public function testThrowExceptionOnInabilityToCreateResource($method) 85 | { 86 | uopz_backup('msg_get_queue'); 87 | uopz_function('msg_get_queue', function () { return false; }); 88 | 89 | $passed = false; 90 | 91 | try { 92 | // suppress notices/warnings triggered by msg_* functions 93 | // to avoid a PHPUnit_Framework_Error_Notice to be thrown 94 | @$this->callQueueMethod($this->queue, $method); 95 | } catch (NoItemAvailableException $e) { 96 | } catch (QueueException $e) { 97 | $this->assertSame('Failed to create/attach to the queue.', $e->getMessage()); 98 | $passed = true; 99 | } 100 | 101 | uopz_restore('msg_get_queue'); 102 | 103 | if (!$passed) { 104 | $this->fail(); 105 | } 106 | } 107 | 108 | public function testSetPermissions() 109 | { 110 | $handler = self::getHandler(); 111 | $key = $handler->getOption('key'); 112 | 113 | $queue = new SysVQueue($key, null, 0606); 114 | 115 | // force a resource creation 116 | $queue->count(); 117 | 118 | $meta = $handler->getMeta(); 119 | 120 | $this->assertEquals(0606, $meta['msg_perm.mode']); 121 | } 122 | 123 | public function testSetItemMaxLength() 124 | { 125 | $this->queue->push('xx'); 126 | $this->queue->setItemMaxLength(1); 127 | 128 | try { 129 | $this->queue->pop(); 130 | } catch (\Exception $e) { 131 | if (7 === $e->getCode() && 'Argument list too long.' === $e->getMessage()) { 132 | return; 133 | } 134 | } 135 | 136 | $this->fail(); 137 | } 138 | 139 | /** 140 | * @group performance 141 | * @dataProvider providePerformancePopDelay 142 | */ 143 | public function testPushPopPerformance($delay) 144 | { 145 | exec('sysctl kernel.msgmnb 2> /dev/null', $output); 146 | 147 | if (!$output) { 148 | $this->markTestSkipped('Unable to determine the maximum size of the System V queue.'); 149 | } 150 | 151 | $maxSizeInBytes = (int) str_replace('kernel.msgmnb = ', '', $output[0]); 152 | $queueSize = static::getPerformanceQueueSize(); 153 | $itemLength = static::getPerformanceItemLength(); 154 | 155 | if ($itemLength * $queueSize > $maxSizeInBytes) { 156 | $this->markTestSkipped(sprintf( 157 | 'The System V queue size is too small (%d bytes) to run this test. '. 158 | 'Try to decrease the "PHIVE_PERF_QUEUE_SIZE" environment variable to %d.', 159 | $maxSizeInBytes, 160 | floor($maxSizeInBytes / $itemLength) 161 | )); 162 | } 163 | 164 | self::baseTestPushPopPerformance($delay); 165 | } 166 | 167 | public static function createHandler(array $config) 168 | { 169 | return new SysVHandler([ 170 | 'key' => $config['PHIVE_SYSV_KEY'], 171 | ]); 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /tests/Queue/TarantoolQueueTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Phive\Queue\Tests\Queue; 13 | 14 | use Phive\Queue\Tests\Handler\TarantoolHandler; 15 | 16 | /** 17 | * @requires extension tarantool 18 | */ 19 | class TarantoolQueueTest extends QueueTest 20 | { 21 | use Performance; 22 | use Concurrency; 23 | 24 | protected $supportsExpiredEta = false; 25 | 26 | protected function getUnsupportedItemTypes() 27 | { 28 | return [Types::TYPE_ARRAY, Types::TYPE_OBJECT]; 29 | } 30 | 31 | /** 32 | * @dataProvider provideItemsOfUnsupportedTypes 33 | * @expectedException PHPUnit_Framework_Exception 34 | * @expectedExceptionMessageRegExp /could not be converted to string|Array to string conversion|unsupported field type/ 35 | */ 36 | public function testUnsupportedItemType($item) 37 | { 38 | $this->queue->push($item); 39 | } 40 | 41 | /** 42 | * @see https://github.com/tarantool/tarantool/issues/336 43 | */ 44 | public function testItemsOfDifferentLength() 45 | { 46 | for ($item = 'x'; strlen($item) < 9; $item .= 'x') { 47 | $this->queue->push($item); 48 | $this->assertEquals($item, $this->queue->pop()); 49 | } 50 | } 51 | 52 | public static function createHandler(array $config) 53 | { 54 | return new TarantoolHandler([ 55 | 'host' => $config['PHIVE_TARANTOOL_HOST'], 56 | 'port' => $config['PHIVE_TARANTOOL_PORT'], 57 | 'space' => $config['PHIVE_TARANTOOL_SPACE'], 58 | 'tube_name' => $config['PHIVE_TARANTOOL_TUBE_NAME'], 59 | ]); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /tests/Queue/TypeSafeQueueTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Phive\Queue\Tests\Queue; 13 | 14 | use Phive\Queue\TypeSafeQueue; 15 | 16 | class TypeSafeQueueTest extends \PHPUnit_Framework_TestCase 17 | { 18 | use Util; 19 | 20 | protected $innerQueue; 21 | protected $queue; 22 | 23 | /** 24 | * {@inheritdoc} 25 | */ 26 | protected function setUp() 27 | { 28 | $this->innerQueue = $this->getQueueMock(); 29 | $this->queue = new TypeSafeQueue($this->innerQueue); 30 | } 31 | 32 | /** 33 | * @dataProvider provideItemsOfSupportedTypes 34 | */ 35 | public function testPush($item) 36 | { 37 | $serializedItem = null; 38 | 39 | $this->innerQueue->expects($this->once())->method('push') 40 | ->with($this->callback(function ($subject) use (&$serializedItem) { 41 | $serializedItem = $subject; 42 | 43 | return is_string($subject) && ctype_print($subject); 44 | })); 45 | 46 | $this->queue->push($item); 47 | 48 | return ['original' => $item, 'serialized' => $serializedItem]; 49 | } 50 | 51 | /** 52 | * @depends testPush 53 | */ 54 | public function testPop($data) 55 | { 56 | $this->innerQueue->expects($this->once())->method('pop') 57 | ->will($this->returnValue($data['serialized'])); 58 | 59 | $this->assertEquals($data['original'], $this->queue->pop()); 60 | } 61 | 62 | public function testCount() 63 | { 64 | $this->innerQueue->expects($this->once())->method('count') 65 | ->will($this->returnValue(42)); 66 | 67 | $this->assertSame(42, $this->queue->count()); 68 | } 69 | 70 | public function testClear() 71 | { 72 | $this->innerQueue->expects($this->once())->method('clear'); 73 | $this->queue->clear(); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /tests/Queue/Types.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Phive\Queue\Tests\Queue; 13 | 14 | class Types 15 | { 16 | const TYPE_NULL = 1; 17 | const TYPE_BOOL = 2; 18 | const TYPE_INT = 3; 19 | const TYPE_FLOAT = 4; 20 | const TYPE_STRING = 5; 21 | const TYPE_BINARY_STRING = 6; 22 | const TYPE_ARRAY = 7; 23 | const TYPE_OBJECT = 8; 24 | 25 | public static function getAll() 26 | { 27 | return [ 28 | self::TYPE_NULL => null, 29 | self::TYPE_BOOL => true, 30 | self::TYPE_INT => 42, 31 | self::TYPE_FLOAT => 1.5, 32 | self::TYPE_STRING => 'string', 33 | self::TYPE_BINARY_STRING => "\x04\x00\xa0\x00\x00", 34 | self::TYPE_ARRAY => ['a', 'r', 'r', 'a', 'y'], 35 | self::TYPE_OBJECT => new self(), 36 | ]; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tests/Queue/Util.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Phive\Queue\Tests\Queue; 13 | 14 | use Phive\Queue\Queue; 15 | 16 | trait Util 17 | { 18 | /** 19 | * @return \PHPUnit_Framework_MockObject_MockObject 20 | */ 21 | public function getQueueMock() 22 | { 23 | return $this->getMock('Phive\Queue\Queue'); 24 | } 25 | 26 | public function provideQueueInterfaceMethods() 27 | { 28 | return array_chunk(get_class_methods('Phive\Queue\Queue'), 1); 29 | } 30 | 31 | public function callQueueMethod(Queue $queue, $method) 32 | { 33 | $r = new \ReflectionMethod($queue, $method); 34 | 35 | if ($num = $r->getNumberOfRequiredParameters()) { 36 | return call_user_func_array([$queue, $method], array_fill(0, $num, 'foo')); 37 | } 38 | 39 | return $queue->$method(); 40 | } 41 | 42 | public function provideItemsOfVariousTypes() 43 | { 44 | $data = []; 45 | 46 | foreach (Types::getAll() as $type => $item) { 47 | $data[$type] = [$item, $type]; 48 | } 49 | 50 | return $data; 51 | } 52 | 53 | public function provideItemsOfSupportedTypes() 54 | { 55 | return array_diff_key( 56 | $this->provideItemsOfVariousTypes(), 57 | array_fill_keys($this->getUnsupportedItemTypes(), false) 58 | ); 59 | } 60 | 61 | public function provideItemsOfUnsupportedTypes() 62 | { 63 | return array_intersect_key( 64 | $this->provideItemsOfVariousTypes(), 65 | array_fill_keys($this->getUnsupportedItemTypes(), false) 66 | ); 67 | } 68 | 69 | protected function getUnsupportedItemTypes() 70 | { 71 | return []; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /tests/TimeUtils.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Phive\Queue\Tests; 13 | 14 | abstract class TimeUtils 15 | { 16 | public static function callAt($timestamp, \Closure $func, $forceSleep = null) 17 | { 18 | if (!function_exists('uopz_function')) { 19 | $forceSleep = true; 20 | } 21 | 22 | if ($forceSleep) { 23 | sleep(-time() + $timestamp); 24 | 25 | return $func(); 26 | } 27 | 28 | self::setTime($timestamp); 29 | 30 | try { 31 | $result = $func(); 32 | } catch (\Exception $e) { 33 | self::unsetTime(); 34 | throw $e; 35 | } 36 | 37 | self::unsetTime(); 38 | 39 | return $result; 40 | } 41 | 42 | private static function setTime($timestamp) 43 | { 44 | $handler = function () use ($timestamp) { 45 | return $timestamp; 46 | }; 47 | 48 | uopz_function('time', $handler); 49 | uopz_function('DateTime', 'getTimestamp', $handler); 50 | } 51 | 52 | private static function unsetTime() 53 | { 54 | uopz_restore('time'); 55 | uopz_restore('DateTime', 'getTimestamp'); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /tests/worker.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | require __DIR__.'/../vendor/autoload.php'; 13 | 14 | $worker = new \GearmanWorker(); 15 | $worker->addServer(); 16 | 17 | $workerId = uniqid(getmypid().'_', true); 18 | $worker->addFunction('pop', function (\GearmanJob $job) use ($workerId) { 19 | static $i = 0; 20 | 21 | $handler = unserialize($job->workload()); 22 | $queue = $handler->createQueue(); 23 | $queueName = $handler->getQueueName($queue); 24 | 25 | $item = $queue->pop(); 26 | 27 | printf("%s: %s item #%s\n", 28 | str_pad(++$i, 4, ' ', STR_PAD_LEFT), 29 | str_pad($queueName.' ', 50, '.'), 30 | $item 31 | ); 32 | 33 | return $workerId.':'.$item; 34 | }); 35 | 36 | echo "Waiting for a job...\n"; 37 | while ($worker->work()) { 38 | if (GEARMAN_SUCCESS !== $worker->returnCode()) { 39 | echo $worker->error()."\n"; 40 | exit(1); 41 | } 42 | } 43 | --------------------------------------------------------------------------------