├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── composer.json ├── php-extensions.ini ├── phpunit.xml ├── src └── Simpleue │ ├── Job │ └── Job.php │ ├── Locker │ ├── BaseLocker.php │ ├── Locker.php │ ├── MemcachedLocker.php │ └── RedisLocker.php │ ├── Queue │ ├── BeanStalkdQueue.php │ ├── Queue.php │ ├── RedisQueue.php │ └── SqsQueue.php │ └── Worker │ └── QueueWorker.php └── tests └── Simpleue ├── Mocks ├── JobSpy.php ├── LoggerSpy.php ├── QueueSpy.php └── QueueWorkerSpy.php └── Unitary ├── Locker ├── MemcachedLockerTest.php └── RedisLockerTest.php ├── Queue ├── BeanStalkdQueueTest.php ├── RedisQueueTest.php └── SqsQueueTest.php └── Worker └── QueueWorkerTest.php /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | composer.lock 3 | composer.phar 4 | vendor/ 5 | logs/ 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | 3 | language: php 4 | 5 | cache: 6 | apt: true 7 | directories: 8 | - vendor 9 | - $HOME/.composer/cache 10 | 11 | matrix: 12 | include: 13 | # Use the newer stack for HHVM as HHVM does not support Precise anymore since a long time and so Precise has an outdated version 14 | - php: hhvm-3.18 15 | sudo: required 16 | dist: trusty 17 | group: edge 18 | - php: 5.5 19 | - php: 5.6 20 | - php: 7.0 21 | - php: 7.1 22 | - php: nightly 23 | fast_finish: true 24 | allow_failures: 25 | - php: nightly 26 | 27 | before_script: 28 | - phpenv config-add php-extensions.ini 29 | - composer install 30 | 31 | script: ./vendor/bin/phpunit 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016-2017 Javier Bravo 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 | Simpleue - Simple Queue Worker for PHP 2 | ====================================== 3 | 4 | [![Build Status](https://travis-ci.org/javibravo/simpleue.svg?branch=master)](https://travis-ci.org/javibravo/simpleue) 5 | [![Total Downloads](https://img.shields.io/packagist/dt/javibravo/simpleue.svg)](https://packagist.org/packages/javibravo/simpleue) 6 | [![Latest Stable Version](https://img.shields.io/packagist/v/javibravo/simpleue.svg)](https://packagist.org/packages/javibravo/simpleue) 7 | 8 | Simpleue provide a very simple way to run workers to consume queues (consumers) in PHP. 9 | The library have been developed to be easily extended to work with different queue servers and 10 | open to manage any kind of job. 11 | 12 | Current implementations: 13 | 14 | - **Redis** queue adapter. 15 | - **AWS SQS** queue adapter. 16 | - **Beanstalkd** queue adapter. 17 | 18 | You can find an example of use in [simpleue-example](https://github.com/javibravo/simpleue-example) 19 | 20 | Worker 21 | ------ 22 | 23 | The lib has a worker class that run and infinite loop (can be stopped with some 24 | conditions) and manage all the stages to process jobs: 25 | 26 | - Get next job. 27 | - Execute job. 28 | - job success then do ... 29 | - job failed then do ... 30 | - Execution error then do ... 31 | - No jobs then do ... 32 | 33 | The loop can be **stopped** under control using the following methods: 34 | 35 | - **STOP Job** : The job handler allow to define a STOP job. 36 | - **Max iterations** : It can be specified when the object is declared. 37 | 38 | Each worker has one queue source and manage one type of jobs. Many workers 39 | can be working concurrently using the same queue source. 40 | 41 | ### Graceful Exit 42 | 43 | The worker is also capable for handling some posix signals, *viz.* `SIGINT` and `SIGTERM` so 44 | that it exits gracefully (waits for the current queue job to complete) when someone tries to 45 | manually stop it (usually with a `C-c` keystroke in a shell). 46 | 47 | This behaviour is disabled by default. To enable it, you need to pass an extra parameter 48 | in the constructor of the worker class. See Usage below for an example. 49 | 50 | **Note: This feature is not tested in HHVM, thus might not work as expected if you are running 51 | it on HHVM** 52 | 53 | Queue 54 | ----- 55 | 56 | The lib provide an interface which allow to implement a queue connection for different queue 57 | servers. Currently the lib provide following implementations: 58 | 59 | - **Redis** queue adapter. 60 | - **AWS SQS** queue adapter. 61 | - **Beanstalkd** queue adapter. 62 | 63 | The queue interface manage all related with the queue system and abstract the job about that. 64 | 65 | It require the queue system client: 66 | 67 | - Redis : Predis\Client 68 | - AWS SQS : Aws\Sqs\SqsClient 69 | - Beanstalkd : Pheanstalk\Pheanstalk; 70 | 71 | And was well the source *queue name*. The consumer will need additional queues to manage the process: 72 | 73 | - **Processing queue** (only for Redis): It will store the item popped from source queue while it is being processed. 74 | - **Failed queue**: All Jobs that fail (according the Job definition) will be add in this queue. 75 | - **Error queue**: All Jobs that throw and exception in the management process will be add to this queue. 76 | 77 | **Important** 78 | 79 | For AWS SQS Queue all the queues must exist before start working. 80 | 81 | Jobs 82 | ---- 83 | 84 | The job interface is used to manage the job received in the queue. It must manage the domain 85 | business logic and **define the STOP job**. 86 | 87 | The job is abstracted form the queue system, so the same job definition is able to work with 88 | different queues interfaces. The job always receive the message body from the queue. 89 | 90 | If you have different job types ( send mail, crop images, etc. ) and you use one queue, you can define **isMyJob**. 91 | If job is not expected type, you can send back job to queue. 92 | 93 | Install 94 | ------- 95 | 96 | Require the package in your composer json file: 97 | 98 | ```json 99 | { 100 | 101 | "require": { 102 | "javibravo/simpleue" : "dev-master", 103 | }, 104 | 105 | } 106 | ``` 107 | 108 | Usage 109 | ----- 110 | 111 | The first step is to define and implement the **Job** to be managed. 112 | 113 | ```php 114 | 'localhost', 'port' => 6379, 'schema' => 'tcp')), 165 | 'my_queue_name' 166 | ); 167 | $myNewConsumer = new QueueWorker($redisQueue, new MyJob()); 168 | $myNewConsumer->start(); 169 | ``` 170 | 171 | **AWS SQS Consumer** 172 | 173 | ```php 174 | 'aws-profile', 183 | 'region' => 'eu-west-1', 184 | 'version' => 'latest' 185 | ]); 186 | 187 | $sqsQueue = new SqsQueue($sqsClient, 'my_queue_name'); 188 | 189 | $myNewConsumer = new QueueWorker($sqsQueue, new MyJob()); 190 | $myNewConsumer->start(); 191 | ``` 192 | 193 | **Beanstalkd Consumer** 194 | 195 | ```php 196 | start(); 209 | ``` 210 | 211 | **Using maxIterations** 212 | 213 | There are two ways you can set maximum iterations, both are shown below: 214 | 215 | Using Setter Method 216 | ```php 217 | $myConsumer = new QueueWorker($myQueue, new MyJob()); 218 | $myConsumer->setMaxIterations(10); //any number 219 | $myConsumer->start(); 220 | ``` 221 | 222 | Using Constructor Parameter 223 | ```php 224 | $myConsumer = new QueueWorker($myQueue, new MyJob(), 10); 225 | $myConsumer->start(); 226 | ``` 227 | 228 | **Enabling Graceful Exit** 229 | 230 | To enable graceful exit, pass in an extra parameter in the constructor. 231 | 232 | ```php 233 | $myConsumer = new QueueWorker($myQueue, new MyJob(), 10, true); 234 | $myConsumer->start(); 235 | ``` 236 | 237 | **AWS SQS Job Locking to Prevent Duplication** 238 | 239 | When using AWS SQS Standart Queue, sometimes workers can get duplicated messages even if MessageVisibilityTimeout given. 240 | To prevent this duplication, you can give Redis or Memcached Locker to SqsQueue object. If you proived locker object and lock failed, job sent to error queue. 241 | Locker provider does not remove/unlock job. If required, you should unlock manually. You can get job key with **getJobUniqId** method. 242 | 243 | ```php 244 | addServer('localhost', 11211); 254 | $memcachedLocker = new MemcachedLocker($memcached); 255 | 256 | $sqsClient = new SqsClient([ 257 | 'profile' => 'aws-profile', 258 | 'region' => 'eu-west-1', 259 | 'version' => 'latest' 260 | ]); 261 | 262 | $sqsQueue = new SqsQueue($sqsClient, 'my_queue_name', 20, 30); 263 | $sqsQueue->setLocker($memcachedLocker); 264 | 265 | $myNewConsumer = new QueueWorker($sqsQueue, new MyJob()); 266 | $myNewConsumer->start(); 267 | ``` 268 | 269 | See http://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/standard-queues.html#standard-queues-at-least-once-delivery for more info 270 | 271 | 272 | (*) The idea is to support any queue system, so it is open for that. Contributions are welcome. 273 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "javibravo/simpleue", 3 | "description": "Php package to manage queue tasks in a simple way", 4 | "keywords": ["queue", "task", "job", "redis", "sqs"], 5 | "homepage": "http://github.com/javibravo/simpleue", 6 | "type": "library", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "Javier Bravo", 11 | "email": "javibravo85@gmail.com" 12 | } 13 | ], 14 | "require": { 15 | "php": ">=5.5", 16 | "psr/log": "~1.0" 17 | }, 18 | "autoload": { 19 | "psr-4": {"Simpleue\\": "src/Simpleue"} 20 | }, 21 | "require-dev": { 22 | "phpunit/phpunit": "4.0.*", 23 | "predis/predis": "^1.0", 24 | "aws/aws-sdk-php": "^3.9", 25 | "pda/pheanstalk": "^3.1" 26 | }, 27 | "autoload-dev": { 28 | "psr-4": {"Simpleue\\": "tests/Simpleue"} 29 | }, 30 | "suggest": { 31 | "predis/predis": "Allow work with Redis queues", 32 | "ext-redis": "Allow work with Redis Locker", 33 | "aws/aws-sdk-php": "Allow work with AWS Simple Queue Service (SQS) queues", 34 | "pda/pheanstalk": "Allow work with Beanstalkd queues" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /php-extensions.ini: -------------------------------------------------------------------------------- 1 | extension = "memcached.so" -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | tests/ 7 | 8 | 9 | 10 | 11 | 12 | src/SimplePhpQueue/ 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/Simpleue/Job/Job.php: -------------------------------------------------------------------------------- 1 | uniqIdFunction = function ($job) { 13 | return md5(strtolower($job)); 14 | }; 15 | } 16 | 17 | public function setJobUniqIdFunction(\Closure $function) 18 | { 19 | $this->uniqIdFunction = $function; 20 | } 21 | 22 | public function getJobUniqId($job) 23 | { 24 | if ($this->uniqIdFunction) { 25 | $func = $this->uniqIdFunction; 26 | return $this->keyPrefix . $func($job); 27 | } else { 28 | throw new \InvalidArgumentException('Locker::uniqIdFunction not defined!'); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Simpleue/Locker/Locker.php: -------------------------------------------------------------------------------- 1 | memcached = $memcached; 17 | $this->memcached->setOptions([ 18 | \Memcached::OPT_TCP_NODELAY => true, 19 | \Memcached::OPT_NO_BLOCK => true, 20 | \Memcached::OPT_CONNECT_TIMEOUT => 60 21 | ]); 22 | } 23 | 24 | public function getLockerInfo() 25 | { 26 | return 'Memcached ( ' . json_encode($this->memcached->getServerList()) . ' )'; 27 | } 28 | 29 | public function lock($job, $timeout = 30) 30 | { 31 | if (!$job) { 32 | throw new \RuntimeException('Job for lock is invalid!'); 33 | } 34 | return $this->memcached->add( 35 | $this->getJobUniqId($job), 36 | time() + $timeout + 1, 37 | $timeout 38 | ); 39 | } 40 | 41 | public function disconnect() 42 | { 43 | $this->memcached->quit(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Simpleue/Locker/RedisLocker.php: -------------------------------------------------------------------------------- 1 | redis = $redisClient; 17 | if ($this->redis->isConnected()===false) { 18 | throw new \RuntimeException('Redis Client not connected!'); 19 | } 20 | } 21 | 22 | public function getLockerInfo() 23 | { 24 | return 'Redis ( ' 25 | . $this->redis->getHost() 26 | . ':' . $this->redis->getPort() 27 | . ' -> ' . $this->redis->getDbNum() 28 | . ' )'; 29 | } 30 | 31 | public function lock($job, $timeout = 40) 32 | { 33 | if (!$job) { 34 | throw new \RuntimeException('Job for lock is invalid!'); 35 | } 36 | $key = $this->getJobUniqId($job); 37 | $status = $this->redis->set( 38 | $key, 39 | time() + $timeout + 1, 40 | array('nx', 'ex' => $timeout) 41 | ); 42 | if ($status) { 43 | return true; 44 | } 45 | 46 | $currentLockTimestamp = $this->redis->get($key); 47 | if ($currentLockTimestamp > time()) { 48 | return false; 49 | } 50 | $oldLockTimestamp = $this->redis->getSet($key, (time() + $timeout + 1)); 51 | if ($oldLockTimestamp > time()) { 52 | return false; 53 | } 54 | return true; 55 | } 56 | 57 | public function disconnect() 58 | { 59 | $this->redis->close(); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Simpleue/Queue/BeanStalkdQueue.php: -------------------------------------------------------------------------------- 1 | 11 | * @package Simpleue\Queue 12 | */ 13 | class BeanStalkdQueue implements Queue 14 | { 15 | /** @var Pheanstalk */ 16 | private $beanStalkdClient; 17 | private $sourceQueue; 18 | private $failedQueue; 19 | private $errorQueue; 20 | 21 | public function __construct($beanStalkdClient, $queueName) 22 | { 23 | $this->beanStalkdClient = $beanStalkdClient; 24 | $this->setQueues($queueName); 25 | } 26 | 27 | 28 | protected function setQueues($queueName) 29 | { 30 | $this->sourceQueue = $queueName; 31 | $this->failedQueue = $queueName . '-failed'; 32 | $this->errorQueue = $queueName . '-error'; 33 | } 34 | 35 | public function getNext() 36 | { 37 | $this->beanStalkdClient->watch($this->sourceQueue); 38 | return $this->beanStalkdClient->reserve(0); 39 | } 40 | 41 | public function successful($job) 42 | { 43 | return $this->beanStalkdClient->delete($job); 44 | } 45 | 46 | /** 47 | * @param $job Job 48 | * @return int 49 | */ 50 | public function failed($job) 51 | { 52 | $this->beanStalkdClient->putInTube($this->failedQueue, $job->getData()); 53 | $this->beanStalkdClient->delete($job); 54 | return; 55 | } 56 | 57 | /** 58 | * @param $job Job 59 | * @return int 60 | */ 61 | public function error($job) 62 | { 63 | $this->beanStalkdClient->putInTube($this->errorQueue, $job->getData()); 64 | $this->beanStalkdClient->delete($job); 65 | return; 66 | } 67 | 68 | public function nothingToDo() 69 | { 70 | return; 71 | } 72 | 73 | public function stopped($job) 74 | { 75 | return $this->beanStalkdClient->delete($job); 76 | } 77 | 78 | /** 79 | * @param $job Job 80 | * @return string 81 | */ 82 | public function getMessageBody($job) 83 | { 84 | return $job->getData(); 85 | } 86 | 87 | /** 88 | * @param $job Job 89 | * @return string 90 | */ 91 | public function toString($job) 92 | { 93 | return json_encode(['id' => $job->getId(), 'data' => $job->getData()]); 94 | } 95 | 96 | /** 97 | * @param $job string 98 | * @return int 99 | */ 100 | public function sendJob($job) 101 | { 102 | return $this->beanStalkdClient->putInTube($this->sourceQueue, $job); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/Simpleue/Queue/Queue.php: -------------------------------------------------------------------------------- 1 | redisClient = $redisClient; 19 | $this->sourceQueue = $queueName; 20 | $this->maxWaitingSeconds = $maxWaitingSeconds; 21 | } 22 | 23 | public function setRedisClient(Client $redisClient) { 24 | $this->redisClient = $redisClient; 25 | return $this; 26 | } 27 | 28 | public function setQueueName($queueName) { 29 | $this->sourceQueue = $queueName; 30 | return $this; 31 | } 32 | 33 | public function setMaxWaitingSeconds($maxWaitingSeconds) { 34 | $this->maxWaitingSeconds = $maxWaitingSeconds; 35 | return $this; 36 | } 37 | 38 | public function getNext() { 39 | $queueItem = $this->redisClient->brpoplpush($this->getSourceQueue(), $this->getProcessingQueue(), $this->maxWaitingSeconds); 40 | return ($queueItem !== null) ? $queueItem : false; 41 | } 42 | 43 | public function successful($job) { 44 | $this->redisClient->lrem($this->getProcessingQueue(), 1, $job); 45 | return; 46 | } 47 | 48 | public function failed($job) { 49 | $this->redisClient->lpush($this->getFailedQueue(), $job); 50 | $this->redisClient->lrem($this->getProcessingQueue(), 1, $job); 51 | return; 52 | } 53 | 54 | public function error($job) { 55 | $this->redisClient->lpush($this->getErrorQueue(), $job); 56 | $this->redisClient->lrem($this->getProcessingQueue(), 1, $job); 57 | return; 58 | } 59 | 60 | public function nothingToDo() { 61 | $this->redisClient->ping(); 62 | } 63 | 64 | public function stopped($job) { 65 | $this->redisClient->lrem($this->getProcessingQueue(), 1, $job); 66 | return; 67 | } 68 | 69 | public function getMessageBody($job) { 70 | return $job; 71 | } 72 | 73 | protected function getSourceQueue() { 74 | return $this->sourceQueue; 75 | } 76 | 77 | protected function getProcessingQueue() { 78 | return $this->sourceQueue . "-processing"; 79 | } 80 | 81 | protected function getFailedQueue() { 82 | return $this->sourceQueue . "-failed"; 83 | } 84 | 85 | protected function getErrorQueue() { 86 | return $this->sourceQueue . "-error"; 87 | } 88 | 89 | public function toString($job) { 90 | return $job; 91 | } 92 | 93 | public function sendJob($job) { 94 | $this->redisClient->lpush($this->getSourceQueue(), $job); 95 | } 96 | 97 | } -------------------------------------------------------------------------------- /src/Simpleue/Queue/SqsQueue.php: -------------------------------------------------------------------------------- 1 | sqsClient = $sqsClient; 31 | $this->maxWaitingSeconds = $maxWaitingSeconds; 32 | $this->visibilityTimeout = $visibilityTimeout; 33 | $this->setQueues($queueName); 34 | } 35 | 36 | public function setVisibilityTimeout($visibilityTimeout) { 37 | $this->visibilityTimeout = $visibilityTimeout; 38 | return $this; 39 | } 40 | 41 | public function setMaxWaitingSeconds($maxWaitingSeconds) { 42 | $this->maxWaitingSeconds = $maxWaitingSeconds; 43 | return $this; 44 | } 45 | 46 | public function setSourceQueueUrl($queueUrl) { 47 | $this->sourceQueueUrl = $queueUrl; 48 | return $this; 49 | } 50 | 51 | public function setFailedQueueUrl($queueUrl) { 52 | $this->failedQueueUrl = $queueUrl; 53 | return $this; 54 | } 55 | 56 | public function setErrorQueueUrl($queueUrl) { 57 | $this->errorQueueUrl = $queueUrl; 58 | return $this; 59 | } 60 | 61 | protected function setQueues($queueName) { 62 | $this->sourceQueueUrl = $this->getQueueUrl($queueName); 63 | $this->failedQueueUrl = $this->getQueueUrl($queueName.'-failed'); 64 | $this->errorQueueUrl = $this->getQueueUrl($queueName.'-error'); 65 | } 66 | 67 | protected function getQueueUrl($queueName) { 68 | try { 69 | $queueData = $this->sqsClient->getQueueUrl(['QueueName' => $queueName]); 70 | } catch (SqsException $ex) { 71 | throw $ex; 72 | } 73 | return $queueData->get('QueueUrl'); 74 | } 75 | 76 | public function setSqsClient(SqsClient $sqsClient) { 77 | $this->sqsClient = $sqsClient; 78 | return $this; 79 | } 80 | 81 | /** 82 | * @param BaseLocker $locker 83 | */ 84 | public function setLocker($locker) 85 | { 86 | $this->locker = $locker; 87 | } 88 | 89 | public function getNext() { 90 | $queueItem = $this->sqsClient->receiveMessage([ 91 | 'QueueUrl' => $this->sourceQueueUrl, 92 | 'MaxNumberOfMessages' => 1, 93 | 'WaitTimeSeconds' => $this->maxWaitingSeconds, 94 | 'VisibilityTimeout' => $this->visibilityTimeout 95 | ]); 96 | if ($queueItem->hasKey('Messages')) { 97 | $msg = $queueItem->get('Messages')[0]; 98 | if ($this->locker && $this->locker->lock($this->getMessageBody($msg), $this->visibilityTimeout)===false) { 99 | $this->error($msg); 100 | throw new \RuntimeException( 101 | 'Sqs msg lock cannot acquired!' 102 | .' LockId: ' . $this->locker->getJobUniqId($this->getMessageBody($msg)) 103 | .' LockerInfo: ' . $this->locker->getLockerInfo() 104 | ); 105 | } 106 | return $msg; 107 | } 108 | return false; 109 | } 110 | 111 | public function successful($job) { 112 | $this->deleteMessage($this->sourceQueueUrl, $job['ReceiptHandle']); 113 | } 114 | 115 | protected function deleteMessage($queueUrl, $messageReceiptHandle) { 116 | $this->sqsClient->deleteMessage([ 117 | 'QueueUrl' => $queueUrl, 118 | 'ReceiptHandle' => $messageReceiptHandle 119 | ]); 120 | } 121 | 122 | public function failed($job) { 123 | $this->sendMessage($this->failedQueueUrl, $job['Body']); 124 | $this->deleteMessage($this->sourceQueueUrl, $job['ReceiptHandle']); 125 | return; 126 | } 127 | 128 | private function sendMessage($queueUrl, $messageBody) { 129 | $this->sqsClient->sendMessage([ 130 | 'QueueUrl' => $queueUrl, 131 | 'MessageBody' => $messageBody 132 | ]); 133 | } 134 | 135 | public function error($job) { 136 | $this->sendMessage($this->errorQueueUrl, $job['Body']); 137 | $this->deleteMessage($this->sourceQueueUrl, $job['ReceiptHandle']); 138 | return; 139 | } 140 | 141 | public function nothingToDo() { 142 | return; 143 | } 144 | 145 | public function stopped($job) { 146 | $this->deleteMessage($this->sourceQueueUrl, $job['ReceiptHandle']); 147 | return; 148 | } 149 | 150 | public function getMessageBody($job) { 151 | return $job['Body']; 152 | } 153 | 154 | public function toString($job) { 155 | return json_encode($job); 156 | } 157 | 158 | public function sendJob($job) { 159 | $this->sendMessage($this->sourceQueueUrl, $job); 160 | } 161 | } -------------------------------------------------------------------------------- /src/Simpleue/Worker/QueueWorker.php: -------------------------------------------------------------------------------- 1 | queueHandler = $queueHandler; 24 | $this->jobHandler = $jobHandler; 25 | $this->maxIterations = (int) $maxIterations; 26 | $this->iterations = 0; 27 | $this->logger = false; 28 | $this->terminated = false; 29 | 30 | if ($handleSignals) { 31 | $this->handleSignals(); 32 | } 33 | } 34 | 35 | public function setQueueHandler(Queue $queueHandler) 36 | { 37 | $this->queueHandler = $queueHandler; 38 | 39 | return $this; 40 | } 41 | 42 | public function setJobHandler(Job $jobHandler) 43 | { 44 | $this->jobHandler = $jobHandler; 45 | 46 | return $this; 47 | } 48 | 49 | public function setMaxIterations($maxIterations) 50 | { 51 | $this->maxIterations = (int) $maxIterations; 52 | 53 | return $this; 54 | } 55 | 56 | public function setLogger(LoggerInterface $logger) 57 | { 58 | $this->logger = $logger; 59 | 60 | return $this; 61 | } 62 | 63 | public function start() 64 | { 65 | $this->log('debug', 'Starting Queue Worker!'); 66 | $this->iterations = 0; 67 | $this->starting(); 68 | while ($this->isRunning()) { 69 | ++$this->iterations; 70 | try { 71 | $job = $this->queueHandler->getNext(); 72 | } catch (\Exception $exception) { 73 | $this->log('error', 'Error getting data. Message: '.$exception->getMessage()); 74 | continue; 75 | } 76 | if ($this->isValidJob($job) && $this->jobHandler->isMyJob($this->queueHandler->getMessageBody($job))) { 77 | if ($this->jobHandler->isStopJob($this->queueHandler->getMessageBody($job))) { 78 | $this->queueHandler->stopped($job); 79 | $this->log('debug', 'STOP instruction received.'); 80 | break; 81 | } 82 | $this->manageJob($job); 83 | } else { 84 | $this->log('debug', 'Nothing to do.'); 85 | $this->queueHandler->nothingToDo(); 86 | } 87 | } 88 | $this->log('debug', 'Queue Worker finished.'); 89 | $this->finished(); 90 | } 91 | 92 | protected function log($type, $message) 93 | { 94 | if ($this->logger) { 95 | $this->logger->$type($message); 96 | } 97 | } 98 | 99 | protected function starting() 100 | { 101 | return true; 102 | } 103 | 104 | protected function isRunning() 105 | { 106 | if ($this->terminated) { 107 | return false; 108 | } 109 | 110 | if ($this->maxIterations > 0) { 111 | return $this->iterations < $this->maxIterations; 112 | } 113 | 114 | return true; 115 | } 116 | 117 | protected function isValidJob($job) 118 | { 119 | return $job !== false; 120 | } 121 | 122 | protected function handleSignals() 123 | { 124 | if (!function_exists('pcntl_signal')) { 125 | $this->log( 126 | 'error', 127 | 'Please make sure that \'pcntl\' is enabled if you want us to handle signals' 128 | ); 129 | 130 | throw new \Exception('Please make sure that \'pcntl\' is enabled if you want us to handle signals'); 131 | } 132 | 133 | declare(ticks = 1); 134 | pcntl_signal(SIGTERM, [$this, 'terminate']); 135 | pcntl_signal(SIGINT, [$this, 'terminate']); 136 | 137 | $this->log('debug', 'Finished Setting up Handler for signals SIGTERM and SIGINT'); 138 | } 139 | 140 | protected function terminate() 141 | { 142 | $this->log('debug', 'Caught signals. Trying a Graceful Exit'); 143 | $this->terminated = true; 144 | } 145 | 146 | private function manageJob($job) 147 | { 148 | try { 149 | $jobDone = $this->jobHandler->manage($this->queueHandler->getMessageBody($job)); 150 | if ($jobDone) { 151 | $this->log('debug', 'Successful Job: '.$this->queueHandler->toString($job)); 152 | $this->queueHandler->successful($job); 153 | } else { 154 | $this->log('debug', 'Failed Job:'.$this->queueHandler->toString($job)); 155 | $this->queueHandler->failed($job); 156 | } 157 | } catch (\Exception $exception) { 158 | $this->log('error', 'Error Managing data. Data :'.$this->queueHandler->toString($job).'. Message: '.$exception->getMessage()); 159 | $this->queueHandler->error($job); 160 | } 161 | } 162 | 163 | protected function finished() 164 | { 165 | return true; 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /tests/Simpleue/Mocks/JobSpy.php: -------------------------------------------------------------------------------- 1 | manageCounter = 0; 19 | } 20 | 21 | public function manage($job) { 22 | if ($this->quitCount && (($this->manageCounter+1) === $this->quitCount)) { 23 | posix_kill(posix_getpid(), $this->testSignal); 24 | } 25 | 26 | $this->manageCounter++; 27 | return true; 28 | } 29 | 30 | public function isStopJob($job) { 31 | return ($job === 'STOP'); 32 | } 33 | 34 | public function isMyJob($job) { 35 | return ($job !== false); 36 | } 37 | 38 | public function setQuitCount($num) { 39 | $this->quitCount = $num; 40 | } 41 | 42 | public function setSignalToTest($sig) { 43 | $this->testSignal = $sig; 44 | } 45 | 46 | public function getManageCounter() { 47 | return $this->manageCounter; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /tests/Simpleue/Mocks/LoggerSpy.php: -------------------------------------------------------------------------------- 1 | errorMessages = array(); 18 | $this->debugMessages = array(); 19 | } 20 | 21 | public function error($message, array $context = array()) { 22 | $this->errorMessages[] = $message; 23 | } 24 | 25 | public function debug($message, array $context = array()) { 26 | $this->debugMessages[] = $message; 27 | } 28 | 29 | public function emergency($message, array $context = array()) {} 30 | public function alert($message, array $context = array()) {} 31 | public function critical($message, array $context = array()) {} 32 | public function warning($message, array $context = array()) {} 33 | public function notice($message, array $context = array()) {} 34 | public function info($message, array $context = array()) {} 35 | public function log($level, $message, array $context = array()) {} 36 | 37 | } -------------------------------------------------------------------------------- /tests/Simpleue/Mocks/QueueSpy.php: -------------------------------------------------------------------------------- 1 | getNextCounter = 0; 23 | $this->successfulCounter = 0; 24 | $this->failedCounter = 0; 25 | $this->errorCounter = 0; 26 | $this->nothingToDoCounter = 0; 27 | $this->stoppedCounter = 0; 28 | } 29 | 30 | public function getNext() { 31 | $this->getNextCounter++; 32 | return rand(0,1000); 33 | } 34 | 35 | public function successful($job) { 36 | $this->successfulCounter++; 37 | return; 38 | } 39 | 40 | public function failed($job) { 41 | $this->failedCounter++; 42 | return $job; 43 | } 44 | 45 | public function error($job) { 46 | $this->errorCounter++; 47 | return $job; 48 | } 49 | 50 | public function nothingToDo() { 51 | $this->nothingToDoCounter++; 52 | return; 53 | } 54 | 55 | public function stopped($job) { 56 | $this->stoppedCounter++; 57 | return; 58 | } 59 | 60 | public function getMessageBody($job) { 61 | $this->getMessageBodyCounter++; 62 | return $job; 63 | } 64 | 65 | public function toString($job) { 66 | return $job; 67 | } 68 | 69 | public function sendJob($job) { 70 | return; 71 | } 72 | 73 | } -------------------------------------------------------------------------------- /tests/Simpleue/Mocks/QueueWorkerSpy.php: -------------------------------------------------------------------------------- 1 | iterations; 15 | } 16 | 17 | } -------------------------------------------------------------------------------- /tests/Simpleue/Unitary/Locker/MemcachedLockerTest.php: -------------------------------------------------------------------------------- 1 | memcachedClientMock = $this->getMock( 18 | '\Memcached', 19 | array('addServer', 'setOptions', 'getServerList', 'add', 'quit') 20 | ); 21 | $this->memcachedClientMock->expects($this->any())->method('getServerList')->willReturn( 22 | [['host' => 'localhost', 'port' => 11211, 'weight'=>10]] 23 | ); 24 | 25 | $this->memcachedLocker = new MemcachedLocker($this->memcachedClientMock); 26 | } 27 | 28 | public function testGetJobUniqId() 29 | { 30 | $job = '{"string": "example", "uniqid":"123"}'; 31 | $this->assertEquals( 32 | 'sqslocker-'.md5(strtolower($job)), 33 | $this->memcachedLocker->getJobUniqId($job) 34 | ); 35 | $this->memcachedLocker->setJobUniqIdFunction(function ($job) { 36 | return json_decode($job, true)['uniqid']; 37 | }); 38 | $this->assertEquals( 39 | 'sqslocker-123', 40 | $this->memcachedLocker->getJobUniqId($job) 41 | ); 42 | } 43 | 44 | public function testGetLockerInfo() 45 | { 46 | $this->assertEquals( 47 | 'Memcached ( ' . json_encode([['host' => 'localhost', 'port' => 11211, 'weight'=>10]]) . ' )', 48 | $this->memcachedLocker->getLockerInfo() 49 | ); 50 | } 51 | 52 | public function testLock() 53 | { 54 | $job = '{"string": "example", "uniqid":"123"}'; 55 | $this->memcachedClientMock->expects($this->at(0))->method('add')->willReturn(true); 56 | $this->memcachedClientMock->expects($this->at(1))->method('add')->willReturn(false); 57 | $this->assertTrue($this->memcachedLocker->lock($job, 60)); 58 | $this->assertFalse($this->memcachedLocker->lock($job, 60)); 59 | 60 | $this->setExpectedException('RuntimeException', 'Job for lock is invalid!'); 61 | $this->memcachedLocker->lock(false, 60); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /tests/Simpleue/Unitary/Locker/RedisLockerTest.php: -------------------------------------------------------------------------------- 1 | redisClientMock = $this->getMock( 18 | '\Redis', 19 | array('isConnected', 'getHost', 'getPort', 'getDbNum', 'set', 'get', 'getSet') 20 | ); 21 | 22 | $this->redisLocker = new RedisLocker($this->redisClientMock); 23 | } 24 | 25 | public function testIsConnected() 26 | { 27 | $this->redisClientMock->expects($this->at(0))->method('isConnected')->willReturn(false); 28 | $this->setExpectedException('RuntimeException', 'Redis Client not connected!'); 29 | new RedisLocker($this->redisClientMock); 30 | } 31 | 32 | public function testGetJobUniqId() 33 | { 34 | $job = '{"string": "example", "uniqid":"123"}'; 35 | $this->assertEquals( 36 | 'sqslocker-' . md5(strtolower($job)), 37 | $this->redisLocker->getJobUniqId($job) 38 | ); 39 | $this->redisLocker->setJobUniqIdFunction(function ($job) { 40 | return json_decode($job, true)['uniqid']; 41 | }); 42 | $this->assertEquals( 43 | 'sqslocker-123', 44 | $this->redisLocker->getJobUniqId($job) 45 | ); 46 | } 47 | 48 | public function testGetLockerInfo() 49 | { 50 | $this->redisClientMock->expects($this->at(0))->method('getHost')->willReturn('localhost'); 51 | $this->redisClientMock->expects($this->at(1))->method('getPort')->willReturn('6379'); 52 | $this->redisClientMock->expects($this->at(2))->method('getDbNum')->willReturn('0'); 53 | $this->assertEquals( 54 | 'Redis ( localhost:6379 -> 0 )', 55 | $this->redisLocker->getLockerInfo() 56 | ); 57 | } 58 | 59 | public function testLock() 60 | { 61 | $job = '{"string": "example", "uniqid":"123"}'; 62 | $this->redisClientMock->expects($this->at(0))->method('set')->willReturn(true); 63 | $this->assertTrue($this->redisLocker->lock($job, 60)); 64 | 65 | $this->redisClientMock->expects($this->at(0))->method('set')->willReturn(false); 66 | $this->redisClientMock->expects($this->at(1))->method('get')->willReturn(time()+20); 67 | $this->assertFalse($this->redisLocker->lock($job, 60)); 68 | 69 | $this->redisClientMock->expects($this->at(0))->method('set')->willReturn(false); 70 | $this->redisClientMock->expects($this->at(1))->method('get')->willReturn(time()-20); 71 | $this->redisClientMock->expects($this->at(2))->method('getSet')->willReturn(time()+20); 72 | $this->assertFalse($this->redisLocker->lock($job, 60)); 73 | 74 | $this->redisClientMock->expects($this->at(0))->method('set')->willReturn(false); 75 | $this->redisClientMock->expects($this->at(1))->method('get')->willReturn(time()-20); 76 | $this->redisClientMock->expects($this->at(2))->method('getSet')->willReturn(time()-20); 77 | $this->assertTrue($this->redisLocker->lock($job, 60)); 78 | 79 | $this->setExpectedException('RuntimeException', 'Job for lock is invalid!'); 80 | $this->redisLocker->lock(false, 60); 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /tests/Simpleue/Unitary/Queue/BeanStalkdQueueTest.php: -------------------------------------------------------------------------------- 1 | 11 | * @package Simpleue\Unitary\Queue 12 | */ 13 | class BeanStalkdQueueTest extends \PHPUnit_Framework_TestCase 14 | { 15 | private $beanStalkdQueue; 16 | /** @var \PHPUnit_Framework_MockObject_MockObject */ 17 | private $beanStalkdClientMock; 18 | private $testQueueName; 19 | 20 | protected function setUp() 21 | { 22 | $this->beanStalkdClientMock = $this->getMockBuilder('Pheanstalk\Pheanstalk')->disableOriginalConstructor() 23 | ->setMethods(['put', 'delete', 'useTube', 'reserve', 'putInTube', 'watch'])->getMock(); 24 | $this->testQueueName = 'queue-test'; 25 | $this->beanStalkdQueue = new BeanStalkdQueue($this->beanStalkdClientMock, $this->testQueueName); 26 | } 27 | 28 | public function testGetNext() 29 | { 30 | $returnExample = new Job(1, '{string: example}'); 31 | $this->beanStalkdClientMock->expects($this->once())->method('watch')->willReturnSelf(); 32 | $this->beanStalkdClientMock->expects($this->once())->method('reserve')->willReturn($returnExample); 33 | $this->assertEquals($returnExample->getData(), $this->beanStalkdQueue->getNext()->getData()); 34 | } 35 | 36 | public function testGetNextMaxWaitReached() 37 | { 38 | $this->beanStalkdClientMock->expects($this->once())->method('reserve')->willReturn(false); 39 | $this->assertTrue(false === $this->beanStalkdQueue->getNext()); 40 | } 41 | 42 | public function testSuccess() 43 | { 44 | $job = new Job(1, '{data:sample}'); 45 | $this->beanStalkdClientMock->expects($this->once())->method('delete')->with($job); 46 | $this->beanStalkdQueue->successful($job); 47 | } 48 | 49 | public function testFailed() 50 | { 51 | $job = new Job(1, '{data:sample}'); 52 | $this->beanStalkdClientMock->expects($this->once())->method('delete')->with($job); 53 | $this->beanStalkdClientMock->expects($this->once())->method('putInTube') 54 | ->with($this->testQueueName . '-failed', $job->getData()); 55 | $this->beanStalkdQueue->failed($job); 56 | } 57 | 58 | public function testError() 59 | { 60 | $job = new Job(1, '{data:sample}'); 61 | $this->beanStalkdClientMock->expects($this->once())->method('delete')->with($job); 62 | $this->beanStalkdClientMock->expects($this->once())->method('putInTube') 63 | ->with($this->testQueueName . '-error', $job->getData()); 64 | $this->beanStalkdQueue->error($job); 65 | } 66 | 67 | 68 | public function testStopped() 69 | { 70 | $job = new Job(1, '{data:sample}'); 71 | $this->beanStalkdClientMock->expects($this->once())->method('delete')->with($job); 72 | $this->beanStalkdQueue->stopped($job); 73 | } 74 | 75 | public function testSendJob() 76 | { 77 | $job = '{data:sample}'; 78 | $this->beanStalkdClientMock->expects($this->once())->method('putInTube')->with($this->testQueueName, $job); 79 | $this->beanStalkdQueue->sendJob($job); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /tests/Simpleue/Unitary/Queue/RedisQueueTest.php: -------------------------------------------------------------------------------- 1 | redisClientMock = $this->getMock('Predis\Client', array('brpoplpush', 'lrem', 'lpush', 'ping')); 18 | $this->redisQueue = new RedisQueue($this->redisClientMock, 'queue.test', 20); 19 | } 20 | 21 | public function testGetNext() { 22 | $returnExample = "{string: example}"; 23 | $this->redisClientMock->expects($this->once())->method('brpoplpush') 24 | ->with('queue.test', 'queue.test-processing', 20)->willReturn($returnExample); 25 | $this->assertEquals($returnExample, $this->redisQueue->getNext()); 26 | } 27 | 28 | public function testGetNextMaxWaitReached() { 29 | $this->redisClientMock->expects($this->once())->method('brpoplpush') 30 | ->with('queue.test', 'queue.test-processing', 20)->willReturn(null); 31 | $this->assertTrue(false === $this->redisQueue->getNext()); 32 | } 33 | 34 | public function testSuccess() { 35 | $job = '{data:sample}'; 36 | $this->redisClientMock->expects($this->once())->method('lrem')->with('queue.test-processing', 1, $job); 37 | $this->redisQueue->successful($job); 38 | } 39 | 40 | public function testFailed() { 41 | $data = '{data:sample}'; 42 | $this->redisClientMock->expects($this->once())->method('lpush')->with('queue.test-failed', $data); 43 | $this->redisClientMock->expects($this->once())->method('lrem')->with('queue.test-processing', 1, $data); 44 | $this->redisQueue->failed($data); 45 | } 46 | 47 | public function testError() { 48 | $data = '{data:sample}'; 49 | $this->redisClientMock->expects($this->once())->method('lpush')->with('queue.test-error', $data); 50 | $this->redisClientMock->expects($this->once())->method('lrem')->with('queue.test-processing', 1, $data); 51 | $this->redisQueue->error($data); 52 | } 53 | 54 | public function testNothingToDo() { 55 | $this->redisClientMock->expects($this->once())->method('ping'); 56 | $this->redisQueue->nothingToDo(); 57 | } 58 | 59 | public function testStopped() { 60 | $data = '{data:sample}'; 61 | $this->redisClientMock->expects($this->once())->method('lrem')->with('queue.test-processing', 1, $data); 62 | $this->redisQueue->stopped($data); 63 | } 64 | 65 | } -------------------------------------------------------------------------------- /tests/Simpleue/Unitary/Queue/SqsQueueTest.php: -------------------------------------------------------------------------------- 1 | sqsClientMock = $this->getMockBuilder('Aws\Sqs\SqsClient')->disableOriginalConstructor() 19 | ->setMethods(array('receiveMessage', 'getQueueUrl', 'deleteMessage', 'sendMessage'))->getMock(); 20 | $this->sqsClientMock->expects($this->any())->method('getQueueUrl')->willReturn(new Result(['QueueUrl' => 'queue-url-test'])); 21 | $this->sqsQueue = new SqsQueue($this->sqsClientMock, 'queue-test', 20); 22 | $this->sqsQueue->setSourceQueueUrl('queue-url-source'); 23 | $this->sqsQueue->setErrorQueueUrl('queue-url-error'); 24 | $this->sqsQueue->setFailedQueueUrl('queue-url-failed'); 25 | } 26 | 27 | public function testGetNext() { 28 | $messageContent = '{string: example}'; 29 | $result = new Result(['Messages' => [$messageContent]]); 30 | $this->sqsClientMock->expects($this->once())->method('receiveMessage')->willReturn($result); 31 | $this->assertEquals($messageContent, $this->sqsQueue->getNext()); 32 | } 33 | 34 | public function testGetNextMaxWaitReached() { 35 | $this->sqsClientMock->expects($this->once())->method('receiveMessage')->willReturn(new Result()); 36 | $this->assertEquals(false, $this->sqsQueue->getNext()); 37 | } 38 | 39 | public function testSuccess() { 40 | $data = '{data:sample}'; 41 | $ReceipHandle = 'MyReceiptHandler'; 42 | $job = ['Body' => $data, 'ReceiptHandle' => $ReceipHandle]; 43 | $this->sqsClientMock->expects($this->once())->method('deleteMessage')->with(['QueueUrl' => 'queue-url-source', 'ReceiptHandle' => $ReceipHandle]); 44 | $this->sqsQueue->successful($job); 45 | } 46 | 47 | public function testFailed() { 48 | $data = '{data:sample}'; 49 | $ReceipHandle = 'MyReceiptHandler'; 50 | $job = ['Body' => $data, 'ReceiptHandle' => $ReceipHandle]; 51 | $this->sqsClientMock->expects($this->once())->method('sendMessage')->with(['QueueUrl' => 'queue-url-failed', 'MessageBody' => $data]); 52 | $this->sqsClientMock->expects($this->once())->method('deleteMessage')->with(['QueueUrl' => 'queue-url-source', 'ReceiptHandle' => $ReceipHandle]); 53 | $this->sqsQueue->failed($job); 54 | } 55 | 56 | public function testError() { 57 | $data = '{data:sample}'; 58 | $ReceipHandle = 'MyReceiptHandler'; 59 | $job = ['Body' => $data, 'ReceiptHandle' => $ReceipHandle]; 60 | $this->sqsClientMock->expects($this->once())->method('sendMessage')->with(['QueueUrl' => 'queue-url-error', 'MessageBody' => $data]); 61 | $this->sqsClientMock->expects($this->once())->method('deleteMessage')->with(['QueueUrl' => 'queue-url-source', 'ReceiptHandle' => $ReceipHandle]); 62 | $this->sqsQueue->error($job); 63 | } 64 | 65 | public function testStopped() { 66 | $data = '{data:sample}'; 67 | $ReceipHandle = 'MyReceiptHandler'; 68 | $job = ['Body' => $data, 'ReceiptHandle' => $ReceipHandle]; 69 | $this->sqsClientMock->expects($this->once())->method('deleteMessage')->with(['QueueUrl' => 'queue-url-source', 'ReceiptHandle' => $ReceipHandle]); 70 | $this->sqsQueue->stopped($job); 71 | } 72 | 73 | public function testGetNextLocked() 74 | { 75 | $data = '{data:sample}'; 76 | $ReceipHandle = 'MyReceiptHandler'; 77 | $job = ['Body' => $data, 'ReceiptHandle' => $ReceipHandle]; 78 | 79 | $redisLockerMock = $this->getMock( 80 | 'Simpleue\Locker\RedisLocker', 81 | array('lock', 'getJobUniqId', 'getLockerInfo'), 82 | array(), 83 | '', 84 | false 85 | ); 86 | $redisLockerMock->expects($this->at(0))->method('lock')->willReturn(false); 87 | $redisLockerMock->expects($this->at(1))->method('getJobUniqId')->willReturn('uniqid'); 88 | $redisLockerMock->expects($this->at(2))->method('getLockerInfo')->willReturn('info'); 89 | $this->sqsQueue->setLocker($redisLockerMock); 90 | 91 | $result = new Result(['Messages' => [$job]]); 92 | $this->sqsClientMock->expects($this->once())->method('receiveMessage')->willReturn($result); 93 | $this->sqsClientMock->expects($this->once())->method('sendMessage')->with(['QueueUrl' => 'queue-url-error', 'MessageBody' => $data]); 94 | $this->sqsClientMock->expects($this->once())->method('deleteMessage')->with(['QueueUrl' => 'queue-url-source', 'ReceiptHandle' => $ReceipHandle]); 95 | $this->setExpectedException( 96 | 'RuntimeException', 97 | 'Sqs msg lock cannot acquired!' 98 | .' LockId: uniqid' 99 | .' LockerInfo: info' 100 | ); 101 | $this->sqsQueue->getNext($job); 102 | } 103 | 104 | } -------------------------------------------------------------------------------- /tests/Simpleue/Unitary/Worker/QueueWorkerTest.php: -------------------------------------------------------------------------------- 1 | sourceQueueMock = new QueueSpy(); 24 | $this->jobHandlerMock = new JobSpy(); 25 | $this->queueWorkerSpy = new QueueWorkerSpy($this->sourceQueueMock, $this->jobHandlerMock); 26 | } 27 | 28 | public function testRunMaxIterations() { 29 | $this->queueWorkerSpy->setMaxIterations(3); 30 | $this->queueWorkerSpy->start(); 31 | $this->assertEquals(3, $this->queueWorkerSpy->getIterations()); 32 | 33 | $this->queueWorkerSpy->setMaxIterations(10); 34 | $this->queueWorkerSpy->start(); 35 | $this->assertEquals(10, $this->queueWorkerSpy->getIterations()); 36 | } 37 | 38 | public function testRunManageSuccessfulJob() { 39 | $this->queueWorkerSpy->setMaxIterations(10); 40 | $this->queueWorkerSpy->start(); 41 | $this->assertEquals(10, $this->sourceQueueMock->getNextCounter, 'Get Next counter'); 42 | $this->assertEquals(10, $this->sourceQueueMock->successfulCounter, 'Successful counter'); 43 | $this->assertEquals(0, $this->sourceQueueMock->failedCounter, 'Failed counter'); 44 | $this->assertEquals(0, $this->sourceQueueMock->errorCounter, 'Error counter'); 45 | $this->assertEquals(0, $this->sourceQueueMock->nothingToDoCounter, 'Nothing to do counter'); 46 | $this->assertEquals(0, $this->sourceQueueMock->stoppedCounter, 'Stop inst. management counter'); 47 | $this->assertEquals(30, $this->sourceQueueMock->getMessageBodyCounter, 'Message body counter'); 48 | } 49 | 50 | public function testStopInstruction() { 51 | $this->sourceQueueMock = $this->getMock('Simpleue\Mocks\QueueSpy', array('getNext')); 52 | $this->sourceQueueMock->expects($this->at(0))->method('getNext')->willReturn(1); 53 | $this->sourceQueueMock->expects($this->at(1))->method('getNext')->willReturn(1); 54 | $this->sourceQueueMock->expects($this->at(2))->method('getNext')->willReturn('STOP'); 55 | 56 | $this->queueWorkerSpy = new QueueWorkerSpy($this->sourceQueueMock, $this->jobHandlerMock); 57 | $this->queueWorkerSpy->setMaxIterations(10); 58 | $this->queueWorkerSpy->start(); 59 | $this->assertEquals(3, $this->queueWorkerSpy->getIterations()); 60 | $this->assertEquals(2, $this->sourceQueueMock->successfulCounter, 'Successful counter'); 61 | $this->assertEquals(0, $this->sourceQueueMock->failedCounter, 'Failed counter'); 62 | $this->assertEquals(0, $this->sourceQueueMock->errorCounter, 'Error counter'); 63 | $this->assertEquals(0, $this->sourceQueueMock->nothingToDoCounter, 'Nothing to do counter'); 64 | $this->assertEquals(1, $this->sourceQueueMock->stoppedCounter, 'Stop inst. management counter'); 65 | $this->assertEquals(8, $this->sourceQueueMock->getMessageBodyCounter, 'Message body counter'); 66 | } 67 | 68 | public function testNothingToDo() { 69 | $this->sourceQueueMock = $this->getMock('Simpleue\Mocks\QueueSpy', array('getNext')); 70 | $this->sourceQueueMock->expects($this->at(0))->method('getNext')->willReturn(false); 71 | $this->sourceQueueMock->expects($this->at(1))->method('getNext')->willReturn(1); 72 | $this->sourceQueueMock->expects($this->at(2))->method('getNext')->willReturn(false); 73 | $this->sourceQueueMock->expects($this->at(3))->method('getNext')->willReturn(0); 74 | $this->sourceQueueMock->expects($this->at(4))->method('getNext')->willReturn(''); 75 | 76 | $this->jobHandlerMock = $this->getMock('Simpleue\Mocks\JobSpy', array('isMyJob')); 77 | $this->jobHandlerMock->expects($this->at(0))->method('isMyJob')->willReturn(true); 78 | $this->jobHandlerMock->expects($this->at(1))->method('isMyJob')->willReturn(false); 79 | $this->jobHandlerMock->expects($this->at(2))->method('isMyJob')->willReturn(true); 80 | 81 | $this->queueWorkerSpy = new QueueWorkerSpy($this->sourceQueueMock, $this->jobHandlerMock); 82 | $this->queueWorkerSpy->setMaxIterations(5); 83 | $this->queueWorkerSpy->start(); 84 | $this->assertEquals(5, $this->queueWorkerSpy->getIterations()); 85 | $this->assertEquals(2, $this->sourceQueueMock->successfulCounter, 'Successful counter'); 86 | $this->assertEquals(0, $this->sourceQueueMock->failedCounter, 'Failed counter'); 87 | $this->assertEquals(0, $this->sourceQueueMock->errorCounter, 'Error counter'); 88 | $this->assertEquals(3, $this->sourceQueueMock->nothingToDoCounter, 'Nothing to do counter'); 89 | $this->assertEquals(0, $this->sourceQueueMock->stoppedCounter, 'Stop inst. management counter'); 90 | $this->assertEquals(7, $this->sourceQueueMock->getMessageBodyCounter, 'Message body counter'); 91 | } 92 | 93 | public function testRunManagedFailedJobs() { 94 | $this->jobHandlerMock = $this->getMock('Simpleue\Mocks\JobSpy', array('manage')); 95 | $this->jobHandlerMock->expects($this->at(0))->method('manage')->willReturn(true); 96 | $this->jobHandlerMock->expects($this->at(1))->method('manage')->willReturn(false); 97 | $this->jobHandlerMock->expects($this->at(2))->method('manage')->willReturn(true); 98 | $this->jobHandlerMock->expects($this->at(3))->method('manage')->willReturn(true); 99 | $this->jobHandlerMock->expects($this->at(4))->method('manage')->willReturn(false); 100 | 101 | $this->queueWorkerSpy = new QueueWorkerSpy($this->sourceQueueMock, $this->jobHandlerMock); 102 | $this->queueWorkerSpy->setMaxIterations(5); 103 | $this->queueWorkerSpy->start(); 104 | $this->assertEquals(5, $this->queueWorkerSpy->getIterations()); 105 | $this->assertEquals(3, $this->sourceQueueMock->successfulCounter, 'Successful counter'); 106 | $this->assertEquals(2, $this->sourceQueueMock->failedCounter, 'Failed counter'); 107 | $this->assertEquals(0, $this->sourceQueueMock->errorCounter, 'Error counter'); 108 | $this->assertEquals(0, $this->sourceQueueMock->nothingToDoCounter, 'Nothing to do counter'); 109 | $this->assertEquals(0, $this->sourceQueueMock->stoppedCounter, 'Stop inst. management counter'); 110 | $this->assertEquals(15, $this->sourceQueueMock->getMessageBodyCounter, 'Message body counter'); 111 | } 112 | 113 | public function testHandlerManageExceptions() { 114 | $this->jobHandlerMock = $this->getMock('Simpleue\Mocks\JobSpy', array('manage')); 115 | $this->jobHandlerMock->expects($this->at(0))->method('manage')->willReturn(true); 116 | $this->jobHandlerMock->expects($this->at(1))->method('manage')->willThrowException(new \Exception('Testing exceptions')); 117 | $this->jobHandlerMock->expects($this->at(2))->method('manage')->willReturn(false); 118 | $this->jobHandlerMock->expects($this->at(3))->method('manage')->willThrowException(new \Exception('Testing exceptions')); 119 | 120 | $this->queueWorkerSpy = new QueueWorkerSpy($this->sourceQueueMock, $this->jobHandlerMock); 121 | $this->queueWorkerSpy->setMaxIterations(4); 122 | $this->queueWorkerSpy->start(); 123 | $this->assertEquals(4, $this->queueWorkerSpy->getIterations()); 124 | $this->assertEquals(1, $this->sourceQueueMock->successfulCounter, 'Successful counter'); 125 | $this->assertEquals(1, $this->sourceQueueMock->failedCounter, 'Failed counter'); 126 | $this->assertEquals(2, $this->sourceQueueMock->errorCounter, 'Error counter'); 127 | $this->assertEquals(0, $this->sourceQueueMock->nothingToDoCounter, 'Nothing to do counter'); 128 | $this->assertEquals(0, $this->sourceQueueMock->stoppedCounter, 'Stop inst. management counter'); 129 | $this->assertEquals(12, $this->sourceQueueMock->getMessageBodyCounter, 'Message body counter'); 130 | } 131 | 132 | public function testSourceQueueGetNextExceptions() { 133 | $this->sourceQueueMock = $this->getMock('Simpleue\Mocks\QueueSpy', array('getNext')); 134 | $this->sourceQueueMock->expects($this->at(0))->method('getNext')->willThrowException(new \Exception('Testing exceptions')); 135 | $this->sourceQueueMock->expects($this->at(1))->method('getNext')->willReturn(1); 136 | $this->sourceQueueMock->expects($this->at(2))->method('getNext')->willThrowException(new \Exception('Testing exceptions')); 137 | 138 | $this->queueWorkerSpy = new QueueWorkerSpy($this->sourceQueueMock, $this->jobHandlerMock); 139 | $this->queueWorkerSpy->setMaxIterations(3); 140 | $this->queueWorkerSpy->start(); 141 | $this->assertEquals(3, $this->queueWorkerSpy->getIterations()); 142 | $this->assertEquals(1, $this->sourceQueueMock->successfulCounter, 'Successful counter'); 143 | $this->assertEquals(0, $this->sourceQueueMock->failedCounter, 'Failed counter'); 144 | $this->assertEquals(0, $this->sourceQueueMock->nothingToDoCounter, 'Nothing to do counter'); 145 | $this->assertEquals(0, $this->sourceQueueMock->stoppedCounter, 'Stop inst. management'); 146 | $this->assertEquals(3, $this->sourceQueueMock->getMessageBodyCounter, 'Message body counter'); 147 | } 148 | 149 | public function testSourceQueueSuccessfulAndFailedExceptions() { 150 | $this->jobHandlerMock = $this->getMock('Simpleue\Mocks\JobSpy', array('manage')); 151 | $this->sourceQueueMock = $this->getMock('Simpleue\Mocks\QueueSpy', array('successful', 'failed')); 152 | $this->jobHandlerMock->expects($this->at(0))->method('manage')->willReturn(true); 153 | $this->sourceQueueMock->expects($this->at(0))->method('successful')->willThrowException(new \Exception('Testing exceptions')); 154 | $this->jobHandlerMock->expects($this->at(1))->method('manage')->willReturn(true); 155 | $this->sourceQueueMock->expects($this->at(1))->method('successful')->willReturn(1); 156 | $this->jobHandlerMock->expects($this->at(2))->method('manage')->willReturn(false); 157 | $this->sourceQueueMock->expects($this->at(2))->method('failed')->willThrowException(new \Exception('Testing exceptions')); 158 | $this->jobHandlerMock->expects($this->at(3))->method('manage')->willReturn(false); 159 | $this->sourceQueueMock->expects($this->at(3))->method('failed')->willReturn(1); 160 | 161 | $this->queueWorkerSpy = new QueueWorkerSpy($this->sourceQueueMock, $this->jobHandlerMock); 162 | $this->queueWorkerSpy->setMaxIterations(4); 163 | $this->queueWorkerSpy->start(); 164 | $this->assertEquals(4, $this->queueWorkerSpy->getIterations()); 165 | $this->assertEquals(2, $this->sourceQueueMock->errorCounter, 'Error counter'); 166 | $this->assertEquals(0, $this->sourceQueueMock->nothingToDoCounter, 'Nothing to do counter'); 167 | $this->assertEquals(0, $this->sourceQueueMock->stoppedCounter, 'Stop inst. management counter'); 168 | $this->assertEquals(12, $this->sourceQueueMock->getMessageBodyCounter, 'Message body counter'); 169 | } 170 | 171 | public function testWorkerExitsGracefullyOnSigINT() { 172 | if (!function_exists('pcntl_signal')) { 173 | $this->markTestSkipped('Enable pcntl_* extension to run this test'); 174 | } 175 | 176 | if (defined('HHVM_VERSION')) { 177 | $this->markTestSkipped('The Graceful Exit feature does not work for HHVM'); 178 | } 179 | 180 | $this->jobHandlerMock->setQuitCount(3); 181 | $this->jobHandlerMock->setSignalToTest(SIGINT); 182 | $this->queueWorkerSpy = new QueueWorkerSpy($this->sourceQueueMock, $this->jobHandlerMock, 10, true); 183 | $this->queueWorkerSpy->start(); 184 | $this->assertEquals(3, $this->queueWorkerSpy->getIterations()); 185 | $this->assertEquals(3, $this->jobHandlerMock->getmanageCounter()); 186 | } 187 | 188 | public function testWorkerExitsGracefullyOnSigTERM() { 189 | if (!function_exists('pcntl_signal')) { 190 | $this->markTestSkipped('Enable pcntl_* extension to run this test'); 191 | } 192 | 193 | if (defined('HHVM_VERSION')) { 194 | $this->markTestSkipped('The Graceful Exit feature does not work for HHVM'); 195 | } 196 | 197 | $this->jobHandlerMock->setQuitCount(8); 198 | $this->jobHandlerMock->setSignalToTest(SIGTERM); 199 | $this->queueWorkerSpy = new QueueWorkerSpy($this->sourceQueueMock, $this->jobHandlerMock, 10, true); 200 | $this->queueWorkerSpy->start(); 201 | $this->assertEquals(8, $this->queueWorkerSpy->getIterations()); 202 | $this->assertEquals(8, $this->jobHandlerMock->getmanageCounter()); 203 | } 204 | 205 | public function testLoggerDebug() { 206 | $loggerSpy = new LoggerSpy(); 207 | $this->queueWorkerSpy->setMaxIterations(1); 208 | $this->queueWorkerSpy->setLogger($loggerSpy); 209 | $this->queueWorkerSpy->start(); 210 | $this->assertEquals(3, count($loggerSpy->debugMessages)); 211 | 212 | $loggerSpy = new LoggerSpy(); 213 | $this->queueWorkerSpy->setMaxIterations(3); 214 | $this->queueWorkerSpy->setLogger($loggerSpy); 215 | $this->queueWorkerSpy->start(); 216 | $this->assertEquals(5, count($loggerSpy->debugMessages)); 217 | } 218 | 219 | public function testLoggerError() { 220 | $loggerSpy = new LoggerSpy(); 221 | $this->jobHandlerMock = $this->getMock('Simpleue\Mocks\JobSpy', array('manage')); 222 | $this->jobHandlerMock->expects($this->at(0))->method('manage')->willReturn(true); 223 | $this->jobHandlerMock->expects($this->at(1))->method('manage')->willThrowException(new \Exception('Testing exceptions')); 224 | $this->jobHandlerMock->expects($this->at(2))->method('manage')->willReturn(false); 225 | $this->jobHandlerMock->expects($this->at(3))->method('manage')->willThrowException(new \Exception('Testing exceptions')); 226 | 227 | $this->queueWorkerSpy = new QueueWorkerSpy($this->sourceQueueMock, $this->jobHandlerMock); 228 | $this->queueWorkerSpy->setMaxIterations(4); 229 | $this->queueWorkerSpy->setLogger($loggerSpy); 230 | $this->queueWorkerSpy->start(); 231 | $this->assertEquals(2, count($loggerSpy->errorMessages)); 232 | } 233 | } 234 | --------------------------------------------------------------------------------