├── .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 | [](http://travis-ci.org/rybakit/phive-queue)
4 | [](https://scrutinizer-ci.com/g/rybakit/phive-queue/?branch=master)
5 | [](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 |
--------------------------------------------------------------------------------