├── .github └── workflows │ ├── main.yml │ └── phpcs.yml ├── LICENSE ├── Module.php ├── README.md ├── UPGRADE.md ├── composer.json ├── config ├── module.config.php └── slm_queue.global.php.dist ├── docs ├── 1.Introduction.md ├── 2.Configuration.md ├── 3.Jobs.md ├── 4.QueueAware.md ├── 5.Workers.md ├── 6.Events.md └── 7.WorkerManagement.md ├── phpcs.xml └── src ├── Command └── StartWorkerCommand.php ├── ConfigProvider.php ├── Controller ├── Exception │ ├── QueueNotFoundException.php │ └── WorkerProcessException.php └── Plugin │ └── QueuePlugin.php ├── Exception ├── BadMethodCallException.php ├── ExceptionInterface.php └── RuntimeException.php ├── Factory ├── JobPluginManagerFactory.php ├── QueueControllerPluginFactory.php ├── QueuePluginManagerFactory.php ├── StrategyPluginManagerFactory.php ├── WorkerAbstractFactory.php └── WorkerPluginManagerFactory.php ├── Job ├── AbstractJob.php ├── Exception │ └── RuntimeException.php ├── JobInterface.php └── JobPluginManager.php ├── Module.php ├── Queue ├── AbstractQueue.php ├── BinaryMessageInterface.php ├── Exception │ ├── RuntimeException.php │ └── UnsupportedOperationException.php ├── QueueAwareInterface.php ├── QueueAwareTrait.php ├── QueueInterface.php └── QueuePluginManager.php ├── Strategy ├── AbstractStrategy.php ├── AttachQueueListenersStrategy.php ├── Factory │ └── AttachQueueListenersStrategyFactory.php ├── FileWatchStrategy.php ├── InterruptStrategy.php ├── MaxMemoryStrategy.php ├── MaxPollingFrequencyStrategy.php ├── MaxRunsStrategy.php ├── ProcessQueueStrategy.php ├── StrategyPluginManager.php └── WorkerLifetimeStrategy.php └── Worker ├── AbstractWorker.php ├── Event ├── AbstractWorkerEvent.php ├── BootstrapEvent.php ├── FinishEvent.php ├── ProcessIdleEvent.php ├── ProcessJobEvent.php ├── ProcessQueueEvent.php ├── ProcessStateEvent.php └── WorkerEventInterface.php ├── Result ├── ExitWorkerLoopResult.php └── ProcessStateResult.php ├── WorkerInterface.php └── WorkerPluginManager.php /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: "Unit tests" 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - "master" 7 | push: 8 | branches: 9 | - "master" 10 | 11 | jobs: 12 | unit-test: 13 | name: "Unit tests" 14 | runs-on: "ubuntu-20.04" 15 | strategy: 16 | matrix: 17 | php-version: 18 | - "7.4" 19 | - "8.0" 20 | - "8.1" 21 | - "8.2" 22 | - "8.3" 23 | dependencies: 24 | - "highest" 25 | - "lowest" 26 | steps: 27 | - name: "Checkout" 28 | uses: "actions/checkout@v2" 29 | 30 | - name: "Install PHP" 31 | uses: "shivammathur/setup-php@v2" 32 | with: 33 | php-version: "${{ matrix.php-version }}" 34 | env: 35 | COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }} 36 | 37 | - name: "Install dependencies with Composer" 38 | uses: "ramsey/composer-install@v1" 39 | with: 40 | dependency-versions: "${{ matrix.dependencies }}" 41 | env: 42 | COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }} 43 | 44 | - name: "Run PHPUnit" 45 | run: "composer test" 46 | env: 47 | COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }} 48 | -------------------------------------------------------------------------------- /.github/workflows/phpcs.yml: -------------------------------------------------------------------------------- 1 | name: "PHPCS" 2 | 3 | on: 4 | pull_request: 5 | paths: 6 | - "**.php" 7 | - "phpcs.xml" 8 | - ".github/workflows/phpcs.yml" 9 | 10 | jobs: 11 | phpcs: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | with: 16 | fetch-depth: 0 # important! 17 | 18 | # we may use whatever way to install phpcs, just specify the path on the next step 19 | # however, curl seems to be the fastest 20 | - name: Install PHP_CodeSniffer 21 | run: | 22 | curl -OL https://squizlabs.github.io/PHP_CodeSniffer/phpcs.phar 23 | php phpcs.phar --version 24 | 25 | - uses: tinovyatkin/action-php-codesniffer@v1 26 | with: 27 | files: "**.php" # you may customize glob as needed 28 | phpcs_path: php phpcs.phar 29 | standard: phpcs.xml 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | New BSD License 2 | =============== 3 | 4 | Copyright (c) 2012-2014, Jurian Sluiman 5 | All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without 8 | modification, are permitted provided that the following conditions are met: 9 | 10 | * Redistributions of source code must retain the above copyright notice, 11 | this list of conditions and the following disclaimer. 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | * Neither the names of the copyright holders nor the names of its 16 | contributors may be used to endorse or promote products derived from this 17 | software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 22 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 23 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 24 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 25 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 26 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 27 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 28 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 29 | POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /Module.php: -------------------------------------------------------------------------------- 1 | = 7.4 58 | * [laminas-servicemanager >= 3.3.1](https://github.com/laminas/laminas-servicemanager) 59 | 60 | 61 | Code samples 62 | ------------ 63 | Below are a few snippets which show the power of SlmQueue in your application. The full documentation is available in 64 | [docs/](/docs) directory. 65 | 66 | A sample job to send an email with php's `mail()` might look like this: 67 | 68 | ```php 69 | namespace MyModule\Job; 70 | 71 | use SlmQueue\Job\AbstractJob; 72 | 73 | class EmailJob extends AbstractJob 74 | { 75 | public static function create(string $to, string $subject, string $message): self 76 | { 77 | // This will bypass the constructor, and thus load a job without 78 | // having to load the dependencies. 79 | $job = self::createEmptyJob([ 80 | 'subject' => $subject, 81 | 'to' => $to, 82 | 'message' => $message, 83 | ]); 84 | 85 | // Add some metadata, so we see what is going on. 86 | $job->setMetadata('to', $to); 87 | 88 | return $job; 89 | } 90 | 91 | private SomeMailService $mailService; 92 | 93 | public function __construct(SomeMailService $mailService) 94 | { 95 | $this->mailService = $mailService; 96 | } 97 | 98 | public function execute() 99 | { 100 | $payload = $this->getContent(); 101 | 102 | $to = $payload['to']; 103 | $subject = $payload['subject']; 104 | $message = $payload['message']; 105 | 106 | $this->mailService->send($to, $subject, $message); 107 | } 108 | } 109 | ``` 110 | 111 | If you want to inject this job into a queue, you can do this for instance in your controller: 112 | 113 | ```php 114 | namespace MyModule\Controller; 115 | 116 | use MyModule\Job\Email as EmailJob; 117 | use SlmQueue\Queue\QueueInterface; 118 | use Laminas\Mvc\Controller\AbstractActionController; 119 | 120 | class MyController extends AbstractActionController 121 | { 122 | protected $queue; 123 | 124 | public function __construct(QueueInterface $queue) 125 | { 126 | $this->queue = $queue; 127 | } 128 | 129 | public function fooAction() 130 | { 131 | // Do some work 132 | 133 | $this->queue->push( 134 | EmailJob::create('john@doe.com', 'Just hi', 'Hi, I want to say hi!'), 135 | ['delay' => 60] 136 | ); 137 | } 138 | } 139 | ``` 140 | 141 | Now the above code lets you insert jobs in a queue, but then you need to spin up a worker which can process these jobs. 142 | 143 | SlmQueue integrates with [`laminas-cli`](https://github.com/laminas/laminas-servicemanager) for command line usage. You can start up a worker for queue "default" with the following command: 144 | 145 | ```sh 146 | $ vendor/bin/laminas slm-queue:start default 147 | ``` 148 | 149 | Contributing 150 | ------------ 151 | 152 | SlmQueue is developed by various fanatic Laminas users. The code is written to be as generic as possible for 153 | Laminas applications. If you want to contribute to SlmQueue, fork this repository and start hacking! 154 | 155 | Any bugs can be reported as an [issue](https://github.com/Webador/SlmQueue/issues) at GitHub. If you want to 156 | contribute, please be aware of the following guidelines: 157 | 158 | 1. Fork the project to your own repository 159 | 2. Use branches to work on your own part 160 | 3. Create a Pull Request at the canonical SlmQueue repository 161 | 4. Make sure to cover changes with the right amount of unit tests 162 | 5. If you add a new feature, please work on some documentation as well 163 | 164 | For long-term contributors, push access to this repository is granted. 165 | 166 | Who to thank? 167 | ------------- 168 | 169 | [Jurian Sluiman](https://github.com/juriansluiman) and [Michaël Gallego](https://github.com/bakura10) did the initial work on creating this repo, and maintained it for a long time. 170 | 171 | Currently it is maintained by: 172 | 173 | * [Bas Kamer](https://github.com/basz) 174 | * [Roel van Duijnhoven](https://github.com/roelvanduijnhoven) 175 | -------------------------------------------------------------------------------- /UPGRADE.md: -------------------------------------------------------------------------------- 1 | # Upgrade to 3.0 2 | 3 | This release adds support for PHP 8.0. 4 | 5 | ## BC BREAK: `laminas-cli` replaces `laminas-mvc-console` 6 | 7 | The CLI can now be invoked with the following command: 8 | 9 | ```sh 10 | vendor/bin/laminas slm-queue:start 11 | ``` 12 | 13 | ## BC BREAK: Dropped support for PHP 7.3 14 | 15 | SlmQueue now requires at least PHP 7.4. 16 | 17 | ## BC BREAK: Added `QueueInterface::getWorkerName()` 18 | 19 | Classes implementing `QueueInterface` are now required to indicate which worker should be used to process the queue. This change will likely only affect you if you use a custom delegator for your queue. 20 | 21 | ## BC BREAK: Workers are now managed by `WorkerPluginManager` 22 | 23 | Workers (and their aliases and delegators) should now be configured under `slm_queue.worker_manager` instead of `service_manager` (Laminas) or `dependencies` (Mezzio). 24 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "slm/queue", 3 | "description": "Laminas module that integrates with various queue management systems", 4 | "license": "BSD-3-Clause", 5 | "type": "library", 6 | "keywords": [ 7 | "laminas", 8 | "mezzio", 9 | "queue", 10 | "job" 11 | ], 12 | "homepage": "https://github.com/Webador/SlmQueue", 13 | "authors": [ 14 | { 15 | "name": "Jurian Sluiman", 16 | "email": "jurian@juriansluiman.nl", 17 | "homepage": "http://juriansluiman.nl" 18 | }, 19 | { 20 | "name": "Michaël Gallego", 21 | "email": "mic.gallego@gmail.com", 22 | "homepage": "http://www.michaelgallego.fr" 23 | }, 24 | { 25 | "name": "Bas Kamer", 26 | "email": "baskamer@gmail.com" 27 | }, 28 | { 29 | "name": "Roel van Duijnhoven", 30 | "email": "roel.duijnhoven@gmail.com", 31 | "homepage": "http://www.roelvanduijnhoven.nl" 32 | } 33 | ], 34 | "require": { 35 | "php": "~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0", 36 | "ext-json": "*", 37 | "laminas/laminas-eventmanager": "^3.4", 38 | "laminas/laminas-servicemanager": "^3.11", 39 | "laminas/laminas-stdlib": "^3.7.1", 40 | "laminas/laminas-cli": "^1.2" 41 | }, 42 | "require-dev": { 43 | "laminas/laminas-mvc": "^3.3", 44 | "laminas/laminas-modulemanager": "^2.11", 45 | "laminas/laminas-view": "^2.13", 46 | "laminas/laminas-serializer": "^2.11", 47 | "laminas/laminas-log": "^2.15", 48 | "laminas/laminas-i18n": "^2.12", 49 | "laminas/laminas-config": "^3.7", 50 | "phpunit/phpunit": "^9.3", 51 | "squizlabs/php_codesniffer": "^3.6.2" 52 | }, 53 | "suggest": { 54 | "slm/queue-sqs": "If you are using Amazon SQS", 55 | "slm/queue-beanstalkd": "If you are using Beanstalk", 56 | "slm/queue-doctrine": "If you are using Doctrine ORM", 57 | "rnd-cosoft/slm-queue-rabbitmq": "If you are using RabbitMQ" 58 | }, 59 | "extra": { 60 | "branch-alias": { 61 | "dev-master": "1.1.x-dev" 62 | }, 63 | "laminas": { 64 | "module": "SlmQueue", 65 | "config-provider": "SlmQueue\\ConfigProvider" 66 | } 67 | }, 68 | "autoload": { 69 | "psr-4": { 70 | "SlmQueue\\": "src/" 71 | } 72 | }, 73 | "autoload-dev": { 74 | "psr-4": { 75 | "SlmQueueTest\\": "tests/src" 76 | } 77 | }, 78 | "scripts": { 79 | "cs-check": "phpcs", 80 | "cs-fix": "phpcbf", 81 | "test": [ 82 | "phpunit", 83 | "@composer test --working-dir=tests/integration/laminas", 84 | "@composer test --working-dir=tests/integration/mezzio" 85 | ] 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /config/module.config.php: -------------------------------------------------------------------------------- 1 | [ 26 | 'factories' => [ 27 | JobPluginManager::class => JobPluginManagerFactory::class, 28 | StrategyPluginManager::class => StrategyPluginManagerFactory::class, 29 | QueuePluginManager::class => QueuePluginManagerFactory::class, 30 | WorkerPluginManager::class => WorkerPluginManagerFactory::class, 31 | 32 | StartWorkerCommand::class => ReflectionBasedAbstractFactory::class, 33 | ], 34 | ], 35 | 36 | 'laminas-cli' => [ 37 | 'commands' => [ 38 | 'slm-queue:start' => StartWorkerCommand::class, 39 | ], 40 | ], 41 | 42 | 'controller_plugins' => [ 43 | 'factories' => [ 44 | 'queue' => QueueControllerPluginFactory::class, 45 | ], 46 | ], 47 | 48 | 'slm_queue' => [ 49 | /** 50 | * Worker Strategies 51 | */ 52 | 'worker_strategies' => [ 53 | 'default' => [ // per worker 54 | AttachQueueListenersStrategy::class, // attaches strategies per queue 55 | MaxRunsStrategy::class => ['max_runs' => 100000], 56 | MaxMemoryStrategy::class => ['max_memory' => 100 * 1024 * 1024], 57 | InterruptStrategy::class, 58 | ], 59 | 'queues' => [ // per queue 60 | 'default' => [ 61 | ProcessQueueStrategy::class, 62 | ], 63 | ], 64 | ], 65 | 66 | /** 67 | * Queue configuration 68 | */ 69 | 'queues' => [], 70 | 71 | /** 72 | * Job manager configuration 73 | */ 74 | 'job_manager' => [], 75 | 76 | /** 77 | * Queue manager configuration 78 | */ 79 | 'queue_manager' => [], 80 | 81 | /** 82 | * Worker manager configuration 83 | */ 84 | 'worker_manager' => [], 85 | 86 | /** 87 | * Strategy manager configuration 88 | */ 89 | 'strategy_manager' => [ 90 | 'invokables' => [ 91 | ProcessQueueStrategy::class => ProcessQueueStrategy::class, 92 | InterruptStrategy::class => InterruptStrategy::class, 93 | MaxRunsStrategy::class => MaxRunsStrategy::class, 94 | WorkerLifetimeStrategy::class => WorkerLifetimeStrategy::class, 95 | MaxMemoryStrategy::class => MaxMemoryStrategy::class, 96 | FileWatchStrategy::class => FileWatchStrategy::class, 97 | MaxPollingFrequencyStrategy::class => MaxPollingFrequencyStrategy::class, 98 | ], 99 | 'factories' => [ 100 | AttachQueueListenersStrategy::class => AttachQueueListenersStrategyFactory::class, 101 | ], 102 | ], 103 | ], 104 | ]; 105 | -------------------------------------------------------------------------------- /config/slm_queue.global.php.dist: -------------------------------------------------------------------------------- 1 | [ 13 | /** 14 | * Allow to configure a specific queue. 15 | * 16 | * Available options depends on the queue factory 17 | */ 18 | 'queues' => [], 19 | 20 | /** 21 | * This block is use to register and configure strategies to the worker event manager. The default key holds any 22 | * configuration for all instanciated workers. The ones configured within the 'queues' keys are specific to 23 | * specific queues only. 24 | * 25 | * Note that module.config.php defines a few defaults and that configuration where the value is not an array 26 | * will be ignored (thus allows you to disable preconfigured strategies). 27 | * 28 | * 'worker_strategies' => [ 29 | * 'default' => [ // per worker 30 | * // Would disable the pre configured max memory strategy 31 | * MaxMemoryStrategy::class => null 32 | * // Reconfigure the pre configured max memory strategy to use 250Mb max 33 | * MaxMemoryStrategy::class => ['max_memory' => 250 * 1024 * 1024] 34 | * ], 35 | * ], 36 | * 37 | * As queue processing is handled by strategies it is important that for each queue a ProcessQueueStrategy 38 | * (a strategy that listens to WorkerEventInterface::EVENT_PROCESS) is registered. By default SlmQueue does handles that 39 | * for the queue called 'default'. 40 | * 41 | * 'worker_strategies' => [ 42 | * 'queues' => [ 43 | * 'my-queue' => [ 44 | * ProcessQueueStrategy::class, 45 | * ] 46 | * ], 47 | * ], 48 | */ 49 | 'worker_strategies' => [ 50 | 'default' => [ // per worker 51 | ], 52 | 'queues' => [ // per queue 53 | 'default' => [ 54 | ], 55 | ], 56 | ], 57 | 58 | /** 59 | * Allow to configure the plugin manager that manages strategies. This works like any other 60 | * PluginManager in Zend Framework. 61 | * 62 | * Add you own or override existing factories 63 | * 64 | * 'strategy_manager' => [ 65 | * 'factories' => [ 66 | * MyVeryOwn\LogJobStrategy::class => 'MyVeryOwn\LogJobStrategyFactory', 67 | * ] 68 | * ], 69 | */ 70 | 'strategy_manager' => [], 71 | 72 | /** 73 | * Allow to configure dependencies for jobs that are pulled from any queue. This works like any other 74 | * PluginManager in Zend Framework. For instance, if you want to inject something into every job using 75 | * a factory, just adds an element into the "factories" array, with the key being the FQCN of the job, 76 | * and the value the factory: 77 | * 78 | * 'job_manager' => [ 79 | * 'factories' => [ 80 | * 'Application\Job\UserJob' => 'Application\Factory\UserJobFactory' 81 | * ] 82 | * ] 83 | * 84 | * Therefore, the job will be created through the factory (the identifier and content of the job will be 85 | * automatically set after creation). Note that this plugin manager is configured as such it automatically 86 | * add any unknown classes to the invokables list. This means you should only add factories and/or abstract 87 | * factories here. 88 | */ 89 | 'job_manager' => [], 90 | 91 | /** 92 | * Allow to add queues. You need to have at least one queue. This works like any other PluginManager in 93 | * Zend Framework. For instance, if you have a queue whose name is "email", you can add it as an 94 | * invokable this way: 95 | * 96 | * 'queue_manager' => [ 97 | * 'invokables' => [ 98 | * 'email' => 'Application\Queue\MyQueue' 99 | * ] 100 | * ] 101 | * 102 | * Please note that you can find built-in factories for several queue systems (Beanstalk, Amazon Sqs...) 103 | * in SlmQueueSqs and SlmQueueBeanstalk 104 | */ 105 | 'queue_manager' => [] 106 | ], 107 | ]; 108 | -------------------------------------------------------------------------------- /docs/1.Introduction.md: -------------------------------------------------------------------------------- 1 | Documentation - introduction 2 | ============================ 3 | 4 | SlmQueue is a job queue abstraction layer. It allows you to easily use job queue systems in a Laminas 5 | application. Thereby it does not enforce you to specifically use one type of job queue. You can write your code and jobs 6 | independent of the underlying system. This enables great flexibility and decoupling of the systems. 7 | 8 | Why a queue system? 9 | ------------------- 10 | 11 | In PHP applications, the request/response cycle is vital to have a fast response time. The earlier you can return a 12 | response, the better it is for your visitors. However, if you need to perform a lot of processing, the response time will 13 | be extended. If these processing tasks contain pieces of code which can be executed at a later moment (say, 5 minutes 14 | later) they are an excellent candidate for a job queue system. 15 | 16 | A job queue system allows you to perform some work in the background, independent from the request/response cycle in your 17 | MVC application. If a client visits a page, the controller will tell the queue to perform a job at a later moment. Next, 18 | the controller can return a response very fast, without the waiting on this job. Some examples of a job are: 19 | 20 | 1. Send an email 21 | 2. Create a PDF file 22 | 3. Connect to a third party server over HTTP 23 | 24 | A typical sketch of a job queue system is the following: 25 | 26 | ``` 27 | Request __________ _______ ________ 28 | ------> | | | | | | 29 | | Server | ---> | Queue | -----> | Worker | 30 | Response | | | | ^--| | | 31 | <------ |_________| |_______| |________| 32 | ``` 33 | 34 | The server sends jobs to the queue. There is a worker which waits until a job arrives. If it got one, it executes the 35 | job and waits again until the next job arrives. The worker is in most cases a long running process, processing jobs 36 | one by one. If the workload is too much for one worker, you can let two workers process the same queue. 37 | 38 | A queue abstraction layer 39 | ------------------------- 40 | 41 | SlmQueue is called a job queue *abstraction* layer. This layer sits in between your application and the queue system. 42 | The benefit of such layer, is your application is indepedent from the queue system. 43 | 44 | Currently, SlmQueue supports beanstalkd, Amazon SQS and Doctrine. A layer like this makes it possible to use Doctrine 45 | first as your application starts small. If the system grows, you might want to migrate something more mature like 46 | beanstalkd or SQS. Without an abstraction layer, you have to rewrite your complete code to support the new system. With 47 | SlmQueue in place, you simply switch a configuration parameter and all your code still works. 48 | 49 | Another typical example are your different application environments. On your local machine, you just run SlmQueue on top 50 | of Doctrine or beanstalkd. With your application hosted at Amazon's EC2, your production environment might use the SQS 51 | adapter (although it is not required to use SQS with EC2 or vice versa). All these different adapters are irrelevant for 52 | your application, making it very flexible to switch, test and try all implementations. 53 | 54 | The SlmQueue architecture 55 | ------------------------- 56 | 57 | SlmQueue consists of three main components: a Job, a Queue and a Worker. Besides these, there is a controller, a CLI 58 | command and several factory classes. 59 | 60 | The `Job` class represents a single job. An example from above is sending an email. The job describes this specific task 61 | by wrapping the code to send an email inside a `Job` object. You create your own class (for instance, `EmailJob`) which 62 | has to implement the interface `SlmQueue\Job\JobInterface`. Inside the `execute()` method, all logic is placed to send 63 | this email. If you need to perform various tasks through a job queue, you write for every job a separate class. 64 | 65 | The `Queue` class is a direct representation of a queue implementation. All different queues implement the same 66 | `SlmQueue\Queue\QueueInterface`. For every implementation (beanstalkd, SQS or Doctrine, at this moment) there is a class 67 | implementing this interface. The object is the main point of entry to insert jobs in the queue and retrieve them back 68 | from it. 69 | 70 | The `Worker` class consumes a `Queue` object and interacts with it to extract jobs from the queue and execute the job. 71 | The worker represents the long running process Every implementation has its own worker, accepting a queue from a certain 72 | type. This way, a worker can handle accordingly to the general flow of jobs from the specific implementation. 73 | 74 | Navigation 75 | ---------- 76 | 77 | Next page: [Configuration](2.Configuration.md) 78 | 79 | 1. [Introduction](1.Introduction.md) 80 | 2. [Configuration](2.Configuration.md) 81 | 3. [Jobs](3.Jobs.md) 82 | 4. [QueueAware](4.QueueAware.md) 83 | 5. [Workers](5.Workers.md) 84 | 6. [Events](6.Events.md) 85 | 7. [Worker management](7.WorkerManagement.md) 86 | -------------------------------------------------------------------------------- /docs/2.Configuration.md: -------------------------------------------------------------------------------- 1 | Documentation - configuration 2 | ============================= 3 | 4 | For queues and jobs, SlmQueue uses a plugin structure. Both the queue and a job are a plugin in the queue or job 5 | manager. You can pull the queue or job from this manager and base your logic around this structure. In essence, the 6 | plugin structure is similar to Laminas controllers or view helpers. 7 | 8 | Queues 9 | ------ 10 | 11 | SlmQueue supports at this moment three types of queues: Beanstalkd, Doctrine and Amazon SQS. You can specify this type 12 | by using a factory from one of these systems. A queue is defined in the `queue_manager` key. In the following example, 13 | a queue named "default" is defined. 14 | 15 | ```php 16 | 'slm_queue' => [ 17 | 'queue_manager' => [ 18 | 'factories' => [ 19 | 'default' => 'SlmQueueBeanstalkd\Factory\BeanstalkdQueueFactory' 20 | ], 21 | ], 22 | ], 23 | ``` 24 | 25 | If you get this queue from the queue manager, SlmQueue will configure the instance completely for you and you are ready 26 | to push jobs into the queue. In the following example, access to the queue in your own controller factory is demonstrated. 27 | 28 | ```php 29 | namespace MyModule\Factory; 30 | 31 | use MyModule\Controller\MyController; 32 | use Laminas\ServiceManager\Factory\FactoryInterface; 33 | use Laminas\ServiceManager\ServiceLocatorInterface; 34 | 35 | class MyControllerFactory implements FactoryInterface 36 | { 37 | public function __invoke(ContainerInterface $container, $requestedName, array $options = null) 38 | { 39 | $queueManager = $container->get('SlmQueue\Queue\QueuePluginManager'); 40 | $queue = $queueManager->get('default'); 41 | 42 | $controller = new MyController($queue); 43 | return $controller; 44 | } 45 | } 46 | ``` 47 | 48 | With a plugin structure, it allows you to create multiple queues. For larger applications, it might be useful to define 49 | different types of queues for different types of tasks. Simple jobs can be pushed into a default queue, but difficult 50 | jobs can be pushed into another queue. The workers of that second queue will run on a larger separate machine, so they 51 | are processed more efficiently. 52 | 53 | Jobs 54 | ---- 55 | 56 | Jobs are defined in a similar fashion. The `job_manager` key is available for this configuration. 57 | 58 | ```php 59 | 'slm_queue' => [ 60 | 'job_manager' => [ 61 | 'factories' => [ 62 | 'MyModule\Job\SendEmailJob' => 'MyModule\Factory\SendEmailJobFactory', 63 | ], 64 | 'invokables' => [ 65 | 'MyModule\Job\PrintHelloWorldJob' => 'MyModule\Job\PrintHelloWorldJob', 66 | ], 67 | ], 68 | ], 69 | ``` 70 | 71 | It is not required to use factories for all jobs. If a job does not need any dependency, you can define the job as an 72 | invokable. You can get the job via the job plugin manager, as shown below. 73 | 74 | ```php 75 | namespace MyModule\Controller; 76 | 77 | use SlmQueue\Queue\QueueInterface; 78 | use SlmQueue\Job\JobPluginManager; 79 | use Laminas\Mvc\Controller\AbstractActionController; 80 | 81 | class MyController extends AbstractActionController 82 | { 83 | protected $queue; 84 | protected $jobManager; 85 | 86 | public function __construct(QueueInterface $queue, JobPluginManager $jobManager) 87 | { 88 | $this->queue = $queue; 89 | $this->jobManager = $jobManager; 90 | } 91 | 92 | public function fooAction() 93 | { 94 | // Do some work 95 | 96 | $job = $this->jobManager->get('MyModule\Job\PrintHelloWorldJob'); 97 | $this->queue->push($job); 98 | } 99 | } 100 | ``` 101 | 102 | Jobs without job plugin manager 103 | ------------------------------- 104 | 105 | There is no need to define all the jobs you use in the job plugin manager. If the job can be invoked (no factory is 106 | defined) you can use the FQCN for the job in the job plugin manager to access it as well. 107 | 108 | ```php 109 | $job = $jobPluginManager->get('MyModule\Job\PrintHelloWorldJob'); 110 | ``` 111 | 112 | Alternatively, in many cases the job manager is not even required to instantiate the job. You can push jobs into queues 113 | where you get the job by using the `new` keyword: 114 | 115 | ```php 116 | $job = new PrintHelloWorldJob; 117 | $queue->push($job); 118 | ``` 119 | 120 | Navigation 121 | ---------- 122 | 123 | Previous page: [Introduction](1.Introduction.md) 124 | Next page: [Jobs](3.Jobs.md) 125 | 126 | 1. [Introduction](1.Introduction.md) 127 | 2. [Configuration](2.Configuration.md) 128 | 3. [Jobs](3.Jobs.md) 129 | 4. [QueueAware](4.QueueAware.md) 130 | 5. [Workers](5.Workers.md) 131 | 6. [Events](6.Events.md) 132 | 7. [Worker management](7.WorkerManagement.md) 133 | -------------------------------------------------------------------------------- /docs/3.Jobs.md: -------------------------------------------------------------------------------- 1 | Documentation - Jobs 2 | ==================== 3 | 4 | Every job must implement the interface `SlmQueue\Job\JobInterface`. For ease of use, an abstract class 5 | `SlmQueue\Job\AbstractJob` is provided to implement most of the interface methods. The method, `execute()`, must be 6 | implemented in userland code. 7 | 8 | An example job would look like the following: 9 | 10 | ```php 11 | namespace MyModule\Job; 12 | 13 | use SlmQueue\Job\AbstractJob; 14 | 15 | class PrintHelloWorldJob extends AbstractJob 16 | { 17 | public function execute() 18 | { 19 | echo 'Hello World'; 20 | } 21 | } 22 | ``` 23 | 24 | Job payload 25 | ----------- 26 | The job often needs some data to work with, commonly called a payload. For an email job, the contents of the email (to 27 | address, subject and message) are the payload. If you generate an invoice based on an order in your database, you set 28 | the id of this order as the payload. 29 | 30 | Internally, a `SlmQueue\Job\AbstractJob` extends the `Laminas\Stdlib\Message` class which allows it to set and get content. 31 | These methods are used to set the payload upon pushing and retrieve the payload when the job is executed. 32 | 33 | To set the payload in a controller, below is an example: 34 | 35 | ```php 36 | namespace MyModule\Controller; 37 | 38 | use Laminas\Mvc\Controller\AbstractActionController; 39 | 40 | class MyController extends AbstractActionController 41 | { 42 | public function fooAction() 43 | { 44 | // Do some work 45 | 46 | $job = $this->jobManager->get('MyModule\Job\SendEmailJob'); 47 | $job->setContent([ 48 | 'to' => 'bob@acme.com', 49 | 'subject' => 'Registration completed', 50 | 'message' => 'Hi bob, you just registered for our website! Welcome!' 51 | ]); 52 | 53 | $this->queue->push($job); 54 | } 55 | } 56 | ``` 57 | 58 | Then you can fetch this payload from the job itself: 59 | 60 | ```php 61 | namespace MyModule\Job; 62 | 63 | use SlmQueue\Job\AbstractJob; 64 | 65 | class SendEmailJob extends AbstractJob 66 | { 67 | public function execute() 68 | { 69 | $payload = $this->getContent(); 70 | $to = $payload['to']; 71 | $subject = $payload['subject']; 72 | $message = $payload['message']; 73 | 74 | // Do something with $to, $subjet, $message 75 | } 76 | } 77 | ``` 78 | 79 | Job payload are a flexible array structure and will be automatically serialized by SlmQueue. This means you can have 80 | values like a `DateTime` object as payload for jobs which will be serialized and deserialized in the background. However, 81 | this also give you restrictions than payload data *must* be serializable. Doctrine 2 entities, references to 82 | `Laminas\Mvc\Application` or other unserializable instances should not be set as payload. 83 | 84 | Job dependencies 85 | ---------------- 86 | 87 | Because of the configuration via a job plugin manager, you can inject dependencies you need into the constructor of the 88 | job class. This will require you to define a job factory for that job as well. Assume here we send an email in a job and 89 | this job requires the email transport class as a dependency. 90 | 91 | ```php 92 | namespace MyModule\Job; 93 | 94 | use SlmQueue\Job\AbstractJob; 95 | use Laminas\Mail\Transport\TransportInterface; 96 | use Laminas\Mail\Message; 97 | 98 | class SendEmailJob extends AbstractJob 99 | { 100 | protected $transport; 101 | 102 | public function __construct(TransportInterface $transport) 103 | { 104 | $this->transport = $transport; 105 | } 106 | 107 | public function execute() 108 | { 109 | $message = new Message; 110 | $payload = $this->getContent(); 111 | 112 | $message->setTo($payload['to']); 113 | $message->setSubject($payload['subject']); 114 | $message->setContent($payload['content']); 115 | 116 | $this->transport->send($message); 117 | } 118 | } 119 | ``` 120 | 121 | To inject the email transport instance, a factory must be created to instantiate the job: 122 | 123 | ```php 124 | namespace MyModule\Factory; 125 | 126 | use Laminas\ServiceManager\Factory\FactoryInterface; 127 | use Laminas\ServiceManager\ServiceLocatorInterface; 128 | 129 | use MyModule\Job\SendEmailJob; 130 | 131 | class SendEmailJobFactory implements FactoryInterface 132 | { 133 | public function __invoke(ContainerInterface $container, $requestedName, array $options = null) 134 | { 135 | $transport = $container->getServiceLocator()->get('my-transport-service'); 136 | 137 | $job = new SendEmailJob($transport); 138 | return $job; 139 | } 140 | } 141 | ``` 142 | 143 | The last step is to register this factory for the above job. Note that in order for this to work the service should be registered with the FQCN of the job class as its identifier. 144 | 145 | ```php 146 | 'slm_queue' => [ 147 | 'job_manager' => [ 148 | 'factories' => [ 149 | 'MyModule\Job\SendEmailJob' => 'MyModule\Factory\SendEmailJobFactory', 150 | ], 151 | ], 152 | ] 153 | ``` 154 | 155 | Jobs with binary payload 156 | ------------------------ 157 | 158 | By default, `SlmQueue` will fail if your job contains a binary payload. If you want to include a binary payload for your job, your job can implement the `SlmQueue\Queue\BinaryMessageInterface`. 159 | 160 | For example, when you want to send an email with a binary attachment: 161 | 162 | ```php 163 | namespace MyModule\Job; 164 | 165 | use SlmQueue\Job\AbstractJob; 166 | use SlmQueue\Queue\BinaryMessageInterface 167 | use Laminas\Mail\Transport\TransportInterface; 168 | use Laminas\Mail\Message; 169 | use Laminas\Mime; 170 | 171 | class SendEmailWithAttachmentJob extends AbstractJob implements BinaryMessageInterface 172 | { 173 | protected $transport; 174 | 175 | public function __construct(TransportInterface $transport) 176 | { 177 | $this->transport = $transport; 178 | } 179 | 180 | public function execute() 181 | { 182 | $message = new Message; 183 | $payload = $this->getContent(); 184 | 185 | $message->setTo($payload['to']); 186 | $message->setSubject($payload['subject']); 187 | 188 | $text = new Mime\Part($payload['content']); 189 | $text->type = 'text/plain'; 190 | 191 | $attachment = new Mime\Part($payload['attachment']); 192 | $attachment->filename = $payload['attachment_filename']; 193 | $attachment->type = $payload['attachment_type']; 194 | 195 | $mimeMessage = new Mime\Message([$text, $attachment]); 196 | 197 | $message->setBody($mimeMessage); 198 | 199 | $this->transport->send($message); 200 | } 201 | } 202 | ``` 203 | 204 | If you have configured the job with a factory, you will now be able to queue jobs with binary contents: 205 | 206 | ```php 207 | $job = $jobManager->get('MyModule\Job\SendEmailWithAttachmentJob'); 208 | 209 | $job->setContent([ 210 | 'to' => 'info@example.com', 211 | 'subject' => 'Email with attachment', 212 | 'content' => 'Hello, this is an email with an attachment', 213 | 'attachment' => file_get_contents('some/image/file.png'), 214 | 'attachment_type' => 'image/png', 215 | 'attachment_filename' => 'cat.png' 216 | ]); 217 | 218 | $queue->push($job); 219 | ``` 220 | 221 | Push jobs via the controller plugin 222 | ----------------------------------- 223 | 224 | Above examples all showed how to inject jobs using the queue directly. This requires SlmQueue users to inject the queue, 225 | or queue plugin manager, in the controller. To avoid duplicate code about queue injection and job retrieval, a queue controller 226 | plugin is created. The controller plugins allows users to push jobs by its service name. 227 | 228 | ```php 229 | namespace MyModule\Controller; 230 | 231 | use Laminas\Mvc\Controller\AbstractActionController; 232 | 233 | class MyController extends AbstractActionController 234 | { 235 | public function fooAction() 236 | { 237 | // Do some work 238 | 239 | $this->queue('default') 240 | ->push('MyModule\Job\SendEmailJob', [ 241 | 'to' => 'bob@acme.com', 242 | 'subject' => 'Registration completed', 243 | 'message' => 'Hi bob, you just registered for our website! Welcome!' 244 | ]); 245 | } 246 | } 247 | ``` 248 | 249 | The second parameter from the plugin's `push()` is optional. You can omit the array completely. Also, it is possible to 250 | inject multiple jobs into the same queue, or inject a job in another queue: 251 | 252 | ```php 253 | namespace MyModule\Controller; 254 | 255 | use Laminas\Mvc\Controller\AbstractActionController; 256 | 257 | class MyController extends AbstractActionController 258 | { 259 | public function fooAction() 260 | { 261 | // Do some work 262 | 263 | // Push two jobs into the same queue 264 | $this->queue('default') 265 | ->push('MyModule\Job\SendEmailJob', [ 266 | 'to' => 'bob@acme.com', 267 | 'subject' => 'Registration completed', 268 | 'message' => 'Hi bob, you just registered for our website! Welcome!' 269 | ]) 270 | ->push('MyModule\Job\TweetJob', [ 271 | 'message' => 'We have a new user registered, his name is bob!' 272 | ]); 273 | 274 | // Push into anther queue 275 | $this->queue('background') 276 | ->push('OtherModule\Job\BussinessReportJob'); 277 | } 278 | } 279 | ``` 280 | 281 | Job status codes 282 | ---------------- 283 | 284 | When using [events](6.Events.md) you might want to hook in the status process of a job. Has 285 | a job successfully been executed or were there errors? The result of a job is expressed in 286 | its status code. SlmQueue defines the following default status codes: 287 | 288 | 0. `JOB_STATUS_UNKNOWN` 289 | 1. `JOB_STATUS_SUCCESS` 290 | 2. `JOB_STATUS_FAILURE` 291 | 3. `JOB_STATUS_FAILURE_RECOVERABLE` 292 | 293 | The status codes are stored in the ProcessJobEvent object (more on that at the 294 | [event section](6.Events.md)). Normally when jobs are completely executed, the status is 295 | success. If any exception is thrown, the status is set to failure. 296 | 297 | ```php 298 | use SlmQueue\Job\AbstractJob; 299 | 300 | class SuccessfulJob extends AbstractJob 301 | { 302 | public function execute() 303 | { 304 | // all is OK 305 | } 306 | } 307 | ``` 308 | 309 | ```php 310 | use RuntimeException 311 | use SlmQueue\Job\AbstractJob; 312 | 313 | class FailingJob extends AbstractJob 314 | { 315 | public function execute() 316 | { 317 | throw new RuntimeException('Not going well'); 318 | } 319 | } 320 | ``` 321 | 322 | Troubleshooting 323 | ---------------- 324 | Issue: My job is failing but the worker keeps on going. How can I get more information? 325 | 326 | Potential exception: JOB_SIZE_TOO_LARGE 327 | 328 | Cause: your job is passing in a large object or collection of results. E.g., Beanstalkd has an upper limit on the size of the job. 329 | 330 | Diagnostic process: Add try / catch handling in your job around the area where your job is leveraging the QueueAware items and pushing out a new job. It is at this point that you will discover that an exception is thrown but not displayed anywhere in the worker. 331 | 332 | Solution: Change your thinking about what can / cannot be pushed in via a job based on the middleware stack that you are using with SlmQueue. If you need to push large byte streams around (e.g., massive collection of results) around then using a caching layer like Redis may suit your purposes. Otherwise something more persistent like AWS S3 or clustered storage may be a good candidate for persisting the large data. Your job should then pass a reference to that byte stream that can then be fetched and utilized rather than the actual byte stream. 333 | 334 | Side note: a recurring clean-up job may need to be written if the byte stream is intended to be ephemeral and the receiving job fails for some reason. 335 | 336 | 337 | 338 | 339 | 340 | 341 | Navigation 342 | ---------- 343 | 344 | Previous page: [Configuration](2.Configuration.md) 345 | Next page: [Queue Aware](4.QueueAware.md) 346 | 347 | 1. [Introduction](1.Introduction.md) 348 | 2. [Configuration](2.Configuration.md) 349 | 3. [Jobs](3.Jobs.md) 350 | 4. [QueueAware](4.QueueAware.md) 351 | 5. [Workers](5.Workers.md) 352 | 6. [Events](6.Events.md) 353 | 7. [Worker management](7.WorkerManagement.md) 354 | -------------------------------------------------------------------------------- /docs/4.QueueAware.md: -------------------------------------------------------------------------------- 1 | Documentation - Queue Aware 2 | ==================== 3 | 4 | It is good practice to keep jobs small and simple. Use jobs for a single task and do not create a single job for multiple 5 | tasks. When multiple tasks should be executed, create a chain of small jobs which all are executed after each other. In 6 | this chapter we will use example of a chain of jobs. First, a PDF file must be generated and after this the PDF must 7 | send via email. The jobs look like this: 8 | 9 | ```php 10 | namespace MyModule\Job; 11 | 12 | use SlmQueue\Job\AbstractJob; 13 | 14 | class GeneratePdfJob extends AbstractJob 15 | { 16 | public function execute() 17 | { 18 | // Generate PDF file 19 | } 20 | } 21 | 22 | class SendEmailJob extends AbstractJob 23 | { 24 | public function execute() 25 | { 26 | // Send the email 27 | } 28 | } 29 | ``` 30 | 31 | SlmQueue solves the job chain by making the `GeneratePdfJob` responsible to start the `SendEmailJob`. The job to 32 | email the file is instantiated within the pdf job. Then, it is pushed into the queue. The queue to inject the new 33 | job into can be automatically injected by SlmQueue. In order to do this, an interface `QueueAwareInterface` must 34 | be implemented: 35 | 36 | ```php 37 | namespace MyModule\Job; 38 | 39 | use SlmQueue\Job\AbstractJob; 40 | use SlmQueue\Queue\QueueAwareInterface; 41 | use SlmQueue\Queue\QueueInterface; 42 | 43 | class GeneratePdfJob extends AbstractJob implements QueueAwareInterface 44 | { 45 | /** 46 | * {@inheritDoc} 47 | */ 48 | protected $queue; 49 | 50 | /** 51 | * {@inheritDoc} 52 | */ 53 | public function getQueue() 54 | { 55 | return $this->queue; 56 | } 57 | 58 | /** 59 | * {@inheritDoc} 60 | */ 61 | public function setQueue(QueueInterface $queue) 62 | { 63 | $this->queue = $queue; 64 | } 65 | 66 | public function execute() 67 | { 68 | // Generate PDF file 69 | 70 | $job = new SendEmailJob; 71 | $this->getQueue()->push($job); 72 | } 73 | } 74 | ``` 75 | 76 | To avoid repeating the interface methods, a trait is provided to ease writing queue aware jobs. 77 | 78 | ```php 79 | namespace MyModule\Job; 80 | 81 | use SlmQueue\Job\AbstractJob; 82 | use SlmQueue\Queue\QueueAwareInterface; 83 | use SlmQueue\Queue\QueueAwareTrait; 84 | 85 | class GeneratePdfJob extends AbstractJob implements QueueAwareInterface 86 | { 87 | use QueueAwareTrait; 88 | 89 | public function execute() 90 | { 91 | // Generate PDF file 92 | 93 | $job = new SendEmailJob; 94 | $this->getQueue()->push($job); 95 | } 96 | } 97 | ``` 98 | 99 | Injecting dependencies into jobs 100 | -------------------------------- 101 | 102 | In the case jobs have dependencies, the job cannot be instantiated with the `new` keyword. The job will have a factory 103 | and the [job manager](3.Jobs.md) must be used to fetch the job from. In this case, retrieve the job manager from the queue first: 104 | 105 | ```php 106 | namespace MyModule\Job; 107 | 108 | use SlmQueue\Job\AbstractJob; 109 | use SlmQueue\Queue\QueueAwareInterface; 110 | use SlmQueue\Queue\QueueAwareTrait; 111 | 112 | use MyModule\Service\PdfGenerator; 113 | use Laminas\Mail\Transport\TransportInterface; 114 | 115 | class GeneratePdfJob extends AbstractJob implements QueueAwareInterface 116 | { 117 | use QueueAwareTrait; 118 | 119 | protected $generator; 120 | 121 | public function __construct(PdfGenerator $generator) 122 | { 123 | $this->generator = $generator; 124 | } 125 | 126 | public function execute() 127 | { 128 | // Generate PDF file using $this->generator 129 | 130 | $queue = $this->getQueue(); 131 | $manager = $queue->getJobPluginManager(); 132 | $job = $manager->get('MyModule\Job\PrintHelloWorldJob') 133 | $queue->push($job); 134 | } 135 | } 136 | 137 | class SendEmailJob extends AbstractJob 138 | { 139 | protected $transport; 140 | 141 | public function __construct(TransportInterface $transport) 142 | { 143 | $this->transport = $transport; 144 | } 145 | 146 | public function execute() 147 | { 148 | // Send the email using $this->transport 149 | } 150 | } 151 | ``` 152 | 153 | Navigation 154 | ---------- 155 | 156 | Previous page: [Jobs](3.Jobs.md) 157 | Next page: [Workers](5.Workers.md) 158 | 159 | 1. [Introduction](1.Introduction.md) 160 | 2. [Configuration](2.Configuration.md) 161 | 3. [Jobs](3.Jobs.md) 162 | 4. [QueueAware](4.QueueAware.md) 163 | 5. [Workers](5.Workers.md) 164 | 6. [Events](6.Events.md) 165 | 7. [Worker management](7.WorkerManagement.md) 166 | -------------------------------------------------------------------------------- /docs/5.Workers.md: -------------------------------------------------------------------------------- 1 | Documentation - Workers 2 | ==================== 3 | 4 | After [queues are configured](2.Configuration.md), [jobs classes are written](3.Jobs.md) and pushed into a queue, they 5 | must be popped out by a worker to be executed. This is done, as explained in the [introduction](1.Introduction.md), by 6 | workers. A worker for SlmQueue is a class implementing the `SlmQueue\Worker\WorkerInterface`. 7 | 8 | A worker is started by by calling its `processQueue($queue, array $options = [])` method. The `$queue` parameter is 9 | a string and should be the service name of the queue as registered in the queue plugin manager. The `$options` parameter 10 | is an optional array which is directly passed on to the `pop()` method of the queue. This can contain flags specifically 11 | for the different queue implementations. 12 | 13 | Worker stop conditions 14 | ---------------------- 15 | 16 | The worker will be a long running call, due to the `while(...){ /*...*/ }` loop it 17 | contains inside the `processQueue()` method. However, there are reasons to cancel the loop and stop the worker to 18 | continue. PHP is not the best language for creating infinite running scripts. 19 | 20 | It is wise to abort the script frequently, for example after x number of cycles in the `while` loop. 21 | 22 | Various build-in strategies (['see 6.Events'](6.Events.md)) are used to decide if the worker should exit. These 23 | strategies are aggregate listeners that hook into events the worker dispatches. A listener (to 'process.queue' or 'process.idle') may shortcut the event flow simply be returning a `ExitWorkerLoopResult::withReason('a reason')`. The 24 | worker will inspect the response collection. 25 | 26 | Navigation 27 | ---------- 28 | 29 | Previous page: [Queue Aware](4.QueueAware.md) 30 | Next page: [Events](6.Events.md) 31 | 32 | 1. [Introduction](1.Introduction.md) 33 | 2. [Configuration](2.Configuration.md) 34 | 3. [Jobs](3.Jobs.md) 35 | 4. [QueueAware](4.QueueAware.md) 36 | 5. [Workers](5.Workers.md) 37 | 6. [Events](6.Events.md) 38 | 7. [Worker management](7.WorkerManagement.md) 39 | -------------------------------------------------------------------------------- /docs/6.Events.md: -------------------------------------------------------------------------------- 1 | Documentation - Events 2 | ====================== 3 | 4 | As of version 0.4.0 the worker has been rewritten to a flexible event driven approach. The processing logic is now a 5 | very minimalistic method. In pseudocode it looks like this; 6 | 7 | ``` 8 | processQueue 9 | trigger event 'bootstrap' 10 | 11 | while event says continue processing 12 | trigger event 'process.queue' 13 | trigger event 'process.job' 14 | 15 | trigger event 'finish' 16 | 17 | trigger event 'state' 18 | 19 | ``` 20 | 21 | Worker Strategies 22 | ----------------- 23 | 24 | To get some useful results it is required to register so called 'worker strategies' to the worker. SlmQueue makes this 25 | trivial via configuration. 26 | 27 | Worker strategies are aggregate listeners which are created via a plugin manager. 28 | 29 | At least one worker strategy listening to the bootstrap event must be registered to the worker. The Worker Factory will 30 | throw an exception if its not. SlmQueue attaches the provided `AttachQueueListenersStrategy` to do just that. 31 | 32 | It is worth noting that events will be dispatched from the worker (obviously) but can also be dispatch from within 33 | worker strategies. 34 | 35 | The plugin manager ensures they extend `SlmQueue\Listener\Strategy\AbstractStrategy` and each worker strategy therefore 36 | gains the following capabilities; 37 | 38 | ### Accept options 39 | 40 | Configuration options are passed by the plugin manager to the constructor of an worker strategy. Setter methods will be 41 | called for each option. If a setter does not exist an exception will be thrown. 42 | 43 | ```php 44 | 'SlmQueue\Strategy\MaxRunsStrategy' => ['max_runs' => 10]; 45 | ``` 46 | Such a config will result in an MaxRunsStrategy instance of which the setMaxRuns method is called with '10'. 47 | 48 | *The optional 'priority' option is used when the aggregates listeners are are registered with event manager and is 49 | thereafter removed from the passed options. This means a Worker Strategy cannot have this option.* 50 | 51 | ### Request to stop processing the queue 52 | 53 | Worker strategies may inform the worker to stop processing the queue. Or more concrete; invalidate the condition of 54 | the while loop. 55 | 56 | ```php 57 | public function onSomeListener(WorkerEvent $event) 58 | { 59 | return ExitWorkerLoopResult::withReason('an exit reason'); 60 | ... 61 | } 62 | ``` 63 | 64 | ### Do something before or after the processing of a queue 65 | 66 | While processing a queue it might be required to execute some setup- or teardown logic. A worker strategy may listen to 67 | the `bootstrap` and/or `finish` event to do just this. 68 | 69 | ```php 70 | /** 71 | * @param EventManagerInterface $events 72 | */ 73 | public function attach(EventManagerInterface $events) 74 | { 75 | $this->listeners[] = $events->attach( 76 | WorkerEventInterface::EVENT_BOOTSTRAP, 77 | [$this, 'onBootstrap'] 78 | ); 79 | $this->listeners[] = $events->attach( 80 | WorkerEventInterface::EVENT_FINISH, 81 | [$this, 'onFinish'] 82 | ); 83 | } 84 | 85 | /** 86 | * @param BootstrapEvent $e 87 | */ 88 | public function onBootstrap(BootstrapEvent $e) 89 | { 90 | // setup code 91 | } 92 | 93 | /** 94 | * @param FinishEvent $e 95 | */ 96 | public function onFinish(FinishEvent $e) 97 | { 98 | // teardown code 99 | } 100 | ``` 101 | 102 | ### Do something before or after the processing of a job 103 | 104 | For some types of jobs it might be required to do something before or after the execution of an individual job. 105 | 106 | This can be done by listening to the `process` event at different priorities. 107 | 108 | ```php 109 | /** 110 | * @param EventManagerInterface $events 111 | */ 112 | public function attach(EventManagerInterface $events) 113 | { 114 | $this->listeners[] = $events->attach( 115 | WorkerEventInterface::EVENT_PROCESS_JOB, 116 | [$this, 'onPreProcess'], 117 | 100 118 | ); 119 | $this->listeners[] = $events->attach( 120 | WorkerEventInterface::EVENT_PROCESS_JOB, 121 | [$this, 'onPostProcess'], 122 | -100 123 | ); 124 | } 125 | 126 | /** 127 | * @param ProcessJobEvent $e 128 | */ 129 | public function onPreProcess(ProcessJobEvent $e) 130 | { 131 | // pre job execution code 132 | } 133 | 134 | /** 135 | * @param ProcessJobEvent $e 136 | */ 137 | public function onPostProcess(ProcessJobEvent $e) 138 | { 139 | // post job execution code 140 | } 141 | ``` 142 | 143 | ### Report on 'the thing' a strategy is tasked with. 144 | 145 | A worker strategy may report a state when th eworker exits. The strategy need to listen to 'WorkerEventInterface::EVENT_PROCESS_STATE' event. The AbstractStrategy implements a `onReportQueueState` method that takes the `$this->state` and returns it as appropiate. 146 | 147 | From the MaxRunStrategy; 148 | 149 | ```php 150 | public function attach(EventManagerInterface $events, $priority = 1) 151 | { 152 | $this->listeners[] = $events->attach( 153 | WorkerEventInterface::EVENT_PROCESS_QUEUE, 154 | [$this, 'onStopConditionCheck'], 155 | -1000 156 | ); 157 | $this->listeners[] = $events->attach( 158 | WorkerEventInterface::EVENT_PROCESS_STATE, 159 | [$this, 'onReportQueueState'], 160 | $priority 161 | ); 162 | } 163 | 164 | 165 | public function onStopConditionCheck(WorkerEvent $event) 166 | { 167 | $this->runCount++; 168 | 169 | $this->state = sprintf('%s jobs processed', $this->runCount); 170 | } 171 | ``` 172 | 173 | ### Dispatch WorkerEvents 174 | 175 | A worker strategy may ask the worker to dispatch events. 176 | 177 | From the ProcessQueueStrategy 178 | 179 | ```php 180 | public function onJobPop(ProcessQueueEvent $processQueueEvent) 181 | { 182 | /** @var AbstractWorker $worker */ 183 | $worker = $processQueueEvent->getTarget(); 184 | $queue = $processQueueEvent->getQueue(); 185 | $options = $processQueueEvent->getOptions(); 186 | $eventManager = $worker->getEventManager(); 187 | 188 | $job = $queue->pop($options); 189 | 190 | // The queue may return null, for instance if a timeout was set 191 | if (!$job instanceof JobInterface) { 192 | /** @var ResponseCollection $results */ 193 | $results = $eventManager->triggerEventUntil( 194 | function ($response) { 195 | return $response instanceof ExitWorkerLoopResult; 196 | }, 197 | new ProcessIdleEvent($worker, $queue) 198 | ); 199 | 200 | $processQueueEvent->stopPropagation(); 201 | 202 | if ($results->stopped()) { 203 | return $results->last(); 204 | } 205 | 206 | return; 207 | } 208 | 209 | $eventManager->triggerEvent(new ProcessJobEvent($job, $worker, $queue)); 210 | } 211 | ``` 212 | 213 | Configuration 214 | ------------- 215 | 216 | 217 | Services 218 | -------- 219 | 220 | Worker strategies are regular ZF2 services that are instanciated via a plugin manager. If a worker strategy has 221 | dependancies on other services it should be created it via service factory. 222 | 223 | **The plugin manager is configured to *not* share services.** 224 | 225 | WorkerEvent 226 | ----------- 227 | 228 | Events the worker *and* worker strategies may dispatch; 229 | 230 | * `WorkerEventInterface::EVENT_BOOTSTRAP` just before loop is entered 231 | * `WorkerEventInterface::EVENT_FINISH` just after the loop has exited 232 | * `WorkerEventInterface::EVENT_PROCESS_QUEUE` fetch job(s) 233 | * `WorkerEventInterface::EVENT_PROCESS_JOB` processes job(s) 234 | * `WorkerEventInterface::EVENT_PROCESS_IDLE` when the queue is empty 235 | * `WorkerEventInterface::EVENT_PROCESS_STATE` collects 'states' from strategies. 236 | 237 | A listener waiting for above events will be passed a an event class extending `WorkerEvent`. Depending on the type it might contain additional methods, such as getJob or getQueue. 238 | 239 | ```php 240 | $em->attach(WorkerEventInterface::EVENT_PROCESS_JOB, function(ProcessJobEvent $e) { 241 | $queue = $e->getQueue(); 242 | $job = $e->getJob(); 243 | }); 244 | ``` 245 | 246 | In above example, `$em` refers to the event manager inside the worker object: `$em = $worker->getEventManager();`. 247 | 248 | Job status codes 249 | ---------------- 250 | 251 | When a job is processed, the [job or worker returns a status code](3.Jobs.md#job-status-codes). You 252 | can use a listener to act upon this status, for example to log any failed jobs: 253 | 254 | ```php 255 | $logger = $sm->get('logger'); 256 | $em->attach(WorkerEventInterface::EVENT_PROCESS_JOB, function(ProcessJobEvent $e) use ($logger) { 257 | $result = $e->getResult(); 258 | if (ProcessJobEvent::JOB_STATUS_FAILURE === $result) { 259 | $job = $e->getJob(); 260 | $logger->warn(sprintf( 261 | 'Job #%s (%s) failed executing', $job->getId(), get_class($job) 262 | )); 263 | } 264 | }, -1000); 265 | ``` 266 | 267 | Provided Worker Strategies 268 | -------------------------- 269 | 270 | #### AttachQueueListenersStrategy 271 | 272 | The purpose of this strategy is to register additional strategies that are specific to the queue that is being 273 | processed. 274 | 275 | After registering any additional worker strategies it will unregister itself as a listener. Finally it halts the event 276 | propagation and re-triggers the `bootstrap` event. 277 | 278 | A new cycle of bootstraping will occure but now with additional queue specific strategies. 279 | 280 | listens to: 281 | 282 | - `bootstrap` event at priority PHP_MAX_INT 283 | 284 | triggers: 285 | 286 | - `bootstrap` 287 | 288 | throws: 289 | 290 | - RunTimeException if the `process.queue` event isn't listened to by any registered strategy. 291 | 292 | This strategy is enabled by default for all queue's. 293 | 294 | #### FileWatchStrategy 295 | 296 | This strategy is able to 'watch' files by creating a hash of their contents. If it detects a change it will request to 297 | stop processing the queue. This is useful if you have something like [supervisor](7.WorkerManagement.md) automatically 298 | restarting the worker process. 299 | 300 | The strategy builds a list of files it needs to watch via a preg_match on the filenames within the application. 301 | 302 | listens to: 303 | 304 | - `process.idle` event at priority 1 305 | - `process.job` event at priority 1000 306 | - `process.state` event at priority 1 307 | 308 | options: 309 | 310 | - pattern defaults to `'/^\.\/(config|module).*\.(php|phtml)$/'` 311 | - idle_throttle_time defaults to 300 seconds 312 | 313 | This strategy is not enabled by default. It can be slow and is recommended for development only. In production you may 314 | watch a single file. It will run the check before each job and while idling after `idle_throttle_time` seconds 315 | have passed. 316 | 317 | #### InterruptStrategy 318 | 319 | The InterruptStrategy is able to catch a stop condition under Linux-like systems (as well as OS X). If a worker is 320 | started from the command line interface (CLI), it is possible to send a SIGTERM or SiGINT call to the worker. SlmQueue 321 | is smart enough not to quit the script directly, but let the job finish its work first and then break out of the loop. 322 | On Windows systems this strategy does nothing. 323 | 324 | listens to: 325 | 326 | - `process.idle` event at priority 1 327 | - `process.queue` event at priority -1000 328 | - `process.state` event at priority 1 329 | 330 | This strategy is enabled by default for all queue's. 331 | 332 | #### MaxMemoryStrategy 333 | 334 | The MaxMemoryStrategy will measure the amount of memory allocated to PHP after each processed job. It will request to 335 | exit when a threshold is exceeded. 336 | 337 | Note that an individual job may exceed this threshold during it's live time. But if you have a memory leak this strategy 338 | can make sure the script aborts eventually. 339 | 340 | listens to: 341 | 342 | - `process.idle` event at priority 1 343 | - `process.queue` event at priority -1000 344 | - `process.state` event at priority 1 345 | 346 | options: 347 | 348 | - max_memory defaults to 100\*1024\*1024 349 | 350 | This strategy is enabled by default for all queue's. 351 | 352 | #### MaxRunsStrategy 353 | 354 | The MaxRunsStrategy will request to exit after a set number of jobs have been processed. 355 | 356 | listens to: 357 | 358 | - `process.idle` event at priority 1 359 | - `process.job` event at priority -1000 360 | - `process.state` event at priority 1 361 | 362 | options: 363 | 364 | - max_runs defaults to 100000 365 | 366 | This strategy is enabled by default for all queue's. 367 | 368 | #### WorkerLifetimeStrategy 369 | 370 | The `WorkerLifetimeStrategy` will request to exit the worker if a given lifetime was reached or exceeded. 371 | 372 | The configured lifetime is NOT a hard-cap for the actual runtime of the worker, because no jobs will be killed during 373 | their execution. It is more like a soft-cap because the check for exiting is only between jobs 374 | (while idling and at the start of a new job). 375 | 376 | So if a worker with a short lifetime (e. g. 1 hour) starts a long running job (e. g. 2 hours) it will 377 | exit after the execution of the job. 378 | 379 | listens to: 380 | 381 | - `bootstrap` event at priority 1: sets the start time 382 | - `process.queue` event at priority -1000: exits if the lifetime was exceeded 383 | - `process.idle` event at priority -1000: exits if the lifetime was exceeded 384 | - `process.state` event at priority 1: returns the current state of the strategy 385 | 386 | options: 387 | 388 | - `lifetime`: the softcap of the worker lifetime in seconds, defaults to 3600 seconds (1 hour) 389 | 390 | #### ProcessQueueStrategy 391 | 392 | Responsible for quering the queue for jobs and executing them. 393 | 394 | listens to: 395 | 396 | - `process.queue` event at priority 1 397 | - `process.job` event at priority 1 398 | 399 | triggers: 400 | 401 | - `process.job` for each job pop'ed from the queue 402 | - `process.idle` if the queue returns null (it might be empty or timed out) 403 | 404 | #### MaxPollingFrequencyStrategy 405 | 406 | The MaxPollingFrequencyStrategy ensure the polling frequency don't exceed a 407 | defined value. This can be useful in the case where you are using a system like 408 | SQS which makes you pay the service per request. 409 | 410 | listens to: 411 | 412 | - `process.queue` event at priority 1000 413 | 414 | options: 415 | 416 | - max_frequency 417 | 418 | This strategy is not enabled by default. 419 | 420 | ##### Frequency - Time per unit table 421 | 422 | | Frequency | x / sec | x / min | x / hour | x / day | x / month | 423 | | --------- | ------- | ------- | -------- | ------- | --------- | 424 | | 0.1 | 0.1 | 6 | 360 | 8640 | 259200 | 425 | | 0.2 | 0.2 | 12 | 720 | 17280 | 518400 | 426 | | 0.5 | 0.5 | 30 | 1800 | 43200 | 1296000 | 427 | | 1 | 1 | 60 | 3600 | 86400 | 2592000 | 428 | | 2 | 2 | 120 | 7200 | 172800 | 5184000 | 429 | | 5 | 5 | 300 | 18000 | 432000 | 12960000 | 430 | | 10 | 10 | 600 | 36000 | 864000 | 25920000 | 431 | 432 | ##### Frequency - Delay table 433 | 434 | | Frequency | Delay | 435 | | --------- | ------ | 436 | | 0.000278 | 1 hour | 437 | | 0.0167 | 1 min | 438 | | 0.1 | 10 s | 439 | | 0.2 | 5 s | 440 | | 0.5 | 2 s | 441 | | 1 | 1 s | 442 | | 2 | 500 ms | 443 | | 5 | 200 ms | 444 | | 10 | 100 ms | 445 | 446 | ##### Add the strategy to you worker 447 | 448 | In the SlmQueue config, find the part named `worker_strategies` and add the 449 | following line: 450 | 451 | ```php 452 | 'SlmQueue\Strategy\MaxPollingFrequencyStrategy' => ['max_frequency' => 1] 453 | ``` 454 | 455 | Replace the `max_frequency` value helping you with the tables above. 456 | 457 | Using the shared event manager 458 | ------------------------------ 459 | 460 | Instead of direct access to the worker's event manager, the shared manager is available to register events too: 461 | 462 | ```php 463 | getApplication()->getEventManager(); 474 | $sharedEm = $em->getSharedManager(); 475 | 476 | $sharedEm->attach('SlmQueue\Worker\WorkerInterface', WorkerEvent::EVENT_PROCESS_JOB, function() { 477 | // some thing just before a job starts. 478 | }, 1000); 479 | } 480 | } 481 | ``` 482 | 483 | Note: since v1.0.1 we have decoupled from laminas/laminas-mvc and as such the shared event manager isn't available in the service container. If you are not using laminas-mvc you should not use the shared event manager. 484 | 485 | Note: we will probably move away from the shared event manager for a next major release. We recommend that if you need to subscribe to events to use the worker's event manager `SlmQueue\Worker\AbstractWorker::getEventManager()`. 486 | 487 | ```php 488 | getApplication()->getServiceLocator(); 499 | $worker = $sm->get('\SlmQueueDoctrine\Worker\DoctrineWorker'); 500 | $em = $worker->getEventManager(); 501 | 502 | $em->attach(WorkerEvent::EVENT_PROCESS_JOB, function() { 503 | // some thing just before a job starts. 504 | }, 1000); 505 | } 506 | } 507 | ``` 508 | 509 | An example 510 | ---------- 511 | 512 | A good example is i18n: a job is given a locale if the job performs localized actions. This locale is set to the 513 | translator just before processing starts. The original locale is reverted when the job has finished processing. 514 | 515 | In this case, all jobs which require a locale set are implementing a `LocaleAwareInterface`: 516 | 517 | ```php 518 | translator = $translator; 566 | } 567 | 568 | /** 569 | * {@inheritDoc} 570 | */ 571 | public function attach(EventManagerInterface $events) 572 | { 573 | $this->listeners[] = $events->attach(WorkerEventInterface::EVENT_PROCESS_JOB, [$this, 'onPreJobProc'], 1000); 574 | $this->listeners[] = $events->attach(WorkerEventInterface::EVENT_PROCESS_JOB, [$this, 'onPostJobProc'], -1000); 575 | } 576 | 577 | public function onPreJobProcessing(ProcessJobEvent $e) 578 | { 579 | $job = $e->getJob(); 580 | 581 | if (!$job instanceof LocaleAwareInterface) { 582 | return; 583 | } 584 | 585 | $this->locale = $this->translator->getLocale(); 586 | $this->translator->setLocale($job->getLocale()); 587 | } 588 | 589 | public function onPostJobProcessing(ProcessJobEvent $e) 590 | { 591 | $job = $e->getJob(); 592 | 593 | if (!$job instanceof LocaleAwareInterface) { 594 | return; 595 | } 596 | 597 | $this->translator->setLocale($this->locale); 598 | } 599 | } 600 | ``` 601 | 602 | Since this worker strategy has a dependency that needs to be injected we should create a factory for it. 603 | 604 | ```php 605 | getServiceLocator(); 617 | 618 | /** @var $sm \Laminas\Mvc\I18n\Translator */ 619 | $translator = $sm->get('MvcTranslator'); 620 | 621 | $strategy = new JobTranslatorStrategy($translator); 622 | 623 | return $strategy; 624 | } 625 | } 626 | ``` 627 | 628 | Finally add two configuration settings; 629 | 630 | 1. Register the factory to the plugin manager to the Strategy Manager. 631 | 2. Add the strategy by name to the worker strategies. Note we can do this for all queue's or for specific ones. 632 | 633 | ```php 634 | [ 637 | /** 638 | * Worker Strategies 639 | */ 640 | 'worker_strategies' => [ 641 | 'default' => [ // per worker 642 | // add it here to enable the 643 | ], 644 | 'queues' => [ // per queue 645 | 'my-queue' => [ 646 | 'MyModule\Strategy\JobTranslatorStrategy', 647 | ] 648 | ], 649 | ], 650 | 651 | /** 652 | * Strategy manager 653 | */ 654 | 'strategy_manager' => [ 655 | 'factories' => [ 656 | 'MyModule\Strategy\JobTranslatorStrategy' => 'MyModule\Strategy\Factory\JobTranslatorStrategyFactory', 657 | ] 658 | ], 659 | ] 660 | ]; 661 | 662 | ``` 663 | 664 | Navigation 665 | ---------- 666 | 667 | Previous page: [Workers](5.Worker.md) 668 | Next page: [Worker Management](7.WorkerManagement.md) 669 | 670 | 1. [Introduction](1.Introduction.md) 671 | 2. [Configuration](2.Configuration.md) 672 | 3. [Jobs](3.Jobs.md) 673 | 4. [QueueAware](4.QueueAware.md) 674 | 5. [Workers](5.Workers.md) 675 | 6. [Events](6.Events.md) 676 | 7. [Worker management](7.WorkerManagement.md) 677 | -------------------------------------------------------------------------------- /docs/7.WorkerManagement.md: -------------------------------------------------------------------------------- 1 | Documentation - Worker Management 2 | ==================== 3 | 4 | Workers are spawned by the command line but will also quit themselves automaticaly. This introduces the need to manage 5 | these workers in an automated way. 6 | 7 | For Linux there is a system called [supervisord](http://supervisord.org). Supervisord controls processes, will start 8 | them automatically and restarts them when they are stopped. This part of the SlmQueue documentation does not fully 9 | explain all features of supervisord, but it gives a kickstart for users who are in need of worker management. 10 | 11 | Basic configuration 12 | ------------------- 13 | 14 | Supervisord has a configuration file under `/etc/supervisord/supervisord.conf`. There you define some basic settings, 15 | for example logging. Under `[unix_http_server]` and `[supervisorctl]` you have to define some required fields as well: 16 | 17 | ```ini 18 | [supervisord] 19 | logfile = /var/log/supervisord/supervisord.log 20 | logfile_maxbytes = 50MB 21 | logfile_backups = 10 22 | loglevel = error 23 | 24 | [unix_http_server] 25 | file = /tmp/supervisor.sock 26 | 27 | [supervisorctl] 28 | serverurl = unix:///tmp/supervisor.sock 29 | ``` 30 | 31 | Worker configuration 32 | -------------------- 33 | 34 | In supervisord, every process "group" it manages is sectioned under a `[program:x]` key. Here, `x` is the name of the 35 | program you want to manage. For using SlmQueue in your application, you might want to choose `my-app` as an appropriate 36 | name. 37 | 38 | ```ini 39 | [program:my-app] 40 | command = php /var/www/mysite/vendor/bin/laminas slm-queue:start default 41 | user = www-data 42 | autorestart = true 43 | ``` 44 | 45 | For every program, at least the `command` line must be set, as supervisord must know which process it should manage. 46 | Secondly, there are options to set e.g. the user under which the process will run, but there much more options available. All 47 | options can be found at the [manual](http://supervisord.org/configuration.html) of supervisord. 48 | 49 | When a process is killed, for example because the number of maximum runs is reached, it will exit. By default, the exit 50 | code 0 is used. In this case, supervisord registers the killed process and starts a new process. Every time the PHP 51 | script is stopped, supervisord makes sure a new process spawns again. 52 | 53 | In case of an error or exception, the worker will probably be killed very soon. Supervisord checks for stop conditions 54 | which occur within 1 second after the process started. If the process is killed within 1 second for 3 consequetive times, 55 | supervisord stops respwaning the process. This event will be registered in the log of supervisord. 56 | 57 | Multiple workers 58 | ---------------- 59 | 60 | When a large number of jobs are inserted into the queue, you might want to spin up more than one worker. Supervisord is 61 | capable of managing more processes of one program, under the key `numprocs`. Because be default, the process name is the 62 | program name, you have to define the `process_name` as well, to distinguish the different processes. 63 | 64 | ```ini 65 | [program:my-app] 66 | user = www-data 67 | command = php /var/www/mysite/vendor/bin/laminas slm-queue:start default 68 | numprocs = 3 69 | process_name = my-app-worker-%(process_num) 70 | autorestart = true 71 | ``` 72 | 73 | Navigation 74 | ---------- 75 | 76 | Previous page: [Events](6.Events.md) 77 | 78 | 1. [Introduction](1.Introduction.md) 79 | 2. [Configuration](2.Configuration.md) 80 | 3. [Jobs](3.Jobs.md) 81 | 4. [QueueAware](4.QueueAware.md) 82 | 5. [Workers](5.Workers.md) 83 | 6. [Events](6.Events.md) 84 | 7. [Worker management](7.WorkerManagement.md) 85 | -------------------------------------------------------------------------------- /phpcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | config 9 | src 10 | docs 11 | tests 12 | 13 | tests/integration/*/vendor/*$ 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/Command/StartWorkerCommand.php: -------------------------------------------------------------------------------- 1 | queuePluginManager = $queuePluginManager; 27 | $this->workerPluginManager = $workerPluginManager; 28 | } 29 | 30 | protected function configure(): void 31 | { 32 | $this->addArgument('queue', InputArgument::REQUIRED); 33 | } 34 | 35 | protected function execute(InputInterface $input, OutputInterface $output): int 36 | { 37 | $queueName = $input->getArgument('queue'); 38 | $queue = $this->queuePluginManager->get($queueName); 39 | $worker = $this->workerPluginManager->get($queue->getWorkerName()); 40 | 41 | try { 42 | $messages = $worker->processQueue($queue, $input->getArguments()); 43 | } catch (ExceptionInterface $e) { 44 | throw new WorkerProcessException( 45 | 'Caught exception while processing queue', 46 | $e->getCode(), 47 | $e 48 | ); 49 | } 50 | 51 | $messages = implode("\n", array_map(function (string $message): string { 52 | return sprintf(' - %s', $message); 53 | }, $messages)); 54 | 55 | $output->writeln(sprintf( 56 | "Finished worker for queue '%s':\n%s\n", 57 | $queueName, 58 | $messages 59 | )); 60 | 61 | return 0; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/ConfigProvider.php: -------------------------------------------------------------------------------- 1 | getConfig(); 11 | 12 | return [ 13 | 'dependencies' => $config['service_manager'], 14 | 'slm_queue' => $config['slm_queue'], 15 | 'laminas-cli' => $config['laminas-cli'], 16 | ]; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Controller/Exception/QueueNotFoundException.php: -------------------------------------------------------------------------------- 1 | queuePluginManager = $queuePluginManager; 44 | $this->jobPluginManager = $jobPluginManager; 45 | } 46 | 47 | /** 48 | * Invoke plugin and optionally set queue 49 | */ 50 | public function __invoke(string $name = null): self 51 | { 52 | if (null !== $name) { 53 | if (! $this->queuePluginManager->has($name)) { 54 | throw new QueueNotFoundException( 55 | sprintf("Queue '%s' does not exist", $name) 56 | ); 57 | } 58 | 59 | $this->queue = $this->queuePluginManager->get($name); 60 | } 61 | 62 | return $this; 63 | } 64 | 65 | /** 66 | * Push a job by its name onto the selected queue 67 | * 68 | * @param string $name Name of the job to create 69 | * @param mixed $payload Payload of the job set as content 70 | * @param array $options Push job options 71 | * @return JobInterface Created job by the job plugin manager 72 | * @throws QueueNotFoundException If the method is called without a queue set 73 | */ 74 | public function push(string $name, $payload = null, array $options = []): JobInterface 75 | { 76 | $this->assertQueueIsSet(); 77 | 78 | $job = $this->jobPluginManager->get($name); 79 | if (null !== $payload) { 80 | $job->setContent($payload); 81 | } 82 | 83 | $this->queue->push($job, $options); 84 | 85 | return $job; 86 | } 87 | 88 | /** 89 | * Push a job on the selected queue 90 | * 91 | * @param JobInterface $job 92 | * @param array $options Push job options 93 | * @throws QueueNotFoundException If the method is called without a queue set 94 | */ 95 | public function pushJob(JobInterface $job, array $options = []): void 96 | { 97 | $this->assertQueueIsSet(); 98 | 99 | $this->queue->push($job, $options); 100 | } 101 | 102 | /** 103 | * @throws QueueNotFoundException 104 | */ 105 | protected function assertQueueIsSet(): void 106 | { 107 | if (null === $this->queue) { 108 | throw new QueueNotFoundException( 109 | 'You cannot push a job without a queue selected' 110 | ); 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/Exception/BadMethodCallException.php: -------------------------------------------------------------------------------- 1 | get('config'); 17 | $config = $config['slm_queue']['job_manager']; 18 | 19 | return new JobPluginManager($container, $config); 20 | } 21 | 22 | public function createService(ServiceLocatorInterface $serviceLocator): JobPluginManager 23 | { 24 | return $this($serviceLocator, JobPluginManager::class); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Factory/QueueControllerPluginFactory.php: -------------------------------------------------------------------------------- 1 | get(QueuePluginManager::class); 16 | $jobPluginManager = $container->get(JobPluginManager::class); 17 | 18 | return new QueuePlugin($queuePluginManager, $jobPluginManager); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Factory/QueuePluginManagerFactory.php: -------------------------------------------------------------------------------- 1 | get('config'); 14 | $config = $config['slm_queue']['queue_manager']; 15 | 16 | return new QueuePluginManager($container, $config); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Factory/StrategyPluginManagerFactory.php: -------------------------------------------------------------------------------- 1 | get('config'); 17 | $config = $config['slm_queue']['strategy_manager']; 18 | 19 | return new StrategyPluginManager($container, $config); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Factory/WorkerAbstractFactory.php: -------------------------------------------------------------------------------- 1 | get('config'); 22 | $strategies = $config['slm_queue']['worker_strategies']['default']; 23 | 24 | $eventManager = $container->has('EventManager') ? $container->get('EventManager') : new EventManager(); 25 | $listenerPluginManager = $container->get(StrategyPluginManager::class); 26 | $this->attachWorkerListeners($eventManager, $listenerPluginManager, $strategies); 27 | 28 | /** @var WorkerInterface $worker */ 29 | $worker = new $requestedName($eventManager); 30 | 31 | return $worker; 32 | } 33 | 34 | protected function attachWorkerListeners( 35 | EventManagerInterface $eventManager, 36 | StrategyPluginManager $listenerPluginManager, 37 | array $strategyConfig = [] 38 | ): void { 39 | foreach ($strategyConfig as $strategy => $options) { 40 | // no options given, name stored as value 41 | if (is_numeric($strategy) && is_string($options)) { 42 | $strategy = $options; 43 | $options = []; 44 | } 45 | 46 | if (! is_string($strategy) || ! is_array($options)) { 47 | continue; 48 | } 49 | 50 | $priority = null; 51 | if (isset($options['priority'])) { 52 | $priority = $options['priority']; 53 | unset($options['priority']); 54 | } 55 | 56 | $listener = $listenerPluginManager->get($strategy, $options); 57 | 58 | if ($priority !== null) { 59 | $listener->attach($eventManager, $priority); 60 | } else { 61 | $listener->attach($eventManager); 62 | } 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Factory/WorkerPluginManagerFactory.php: -------------------------------------------------------------------------------- 1 | get('config'); 14 | $config = $config['slm_queue']['worker_manager']; 15 | 16 | return new WorkerPluginManager($container, $config); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Job/AbstractJob.php: -------------------------------------------------------------------------------- 1 | $this->queue('default')->push(SomeJob::create($someData)); 20 | * 21 | * And that static method can use `self::createEmptyJob` internally. 22 | * 23 | * @param mixed $content 24 | * @return static 25 | * 26 | * TODO Add static as return type, as soon as we only support PHP8. 27 | * TODO Make sure $content is always an array? 28 | */ 29 | protected static function createEmptyJob($content = null) 30 | { 31 | $job = (new \ReflectionClass(get_called_class()))->newInstanceWithoutConstructor(); 32 | $job->setContent($content); 33 | return $job; 34 | } 35 | 36 | /** 37 | * @var string|array|null 38 | */ 39 | protected $content = null; 40 | 41 | public function setId($id): JobInterface 42 | { 43 | $this->setMetadata('__id__', $id); 44 | 45 | return $this; 46 | } 47 | 48 | /** 49 | * {@inheritDoc} 50 | */ 51 | public function getId() 52 | { 53 | return $this->getMetadata('__id__'); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Job/Exception/RuntimeException.php: -------------------------------------------------------------------------------- 1 | setMetadata('__name__', $name); 36 | 37 | return $instance; 38 | } 39 | 40 | /** 41 | * {@inheritDoc} 42 | */ 43 | public function validate($instance) 44 | { 45 | if ($instance instanceof JobInterface) { 46 | return; // we're okay 47 | } 48 | 49 | throw new Exception\RuntimeException(sprintf( 50 | 'Plugin of type %s is invalid; must implement SlmQueue\Job\JobInterface', 51 | (is_object($instance) ? get_class($instance) : gettype($instance)) 52 | )); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Module.php: -------------------------------------------------------------------------------- 1 | name = $name; 20 | $this->jobPluginManager = $jobPluginManager; 21 | } 22 | 23 | public function getName(): string 24 | { 25 | return $this->name; 26 | } 27 | 28 | public function getWorkerName(): string 29 | { 30 | return static::$defaultWorkerName; 31 | } 32 | 33 | public function getJobPluginManager(): JobPluginManager 34 | { 35 | return $this->jobPluginManager; 36 | } 37 | 38 | /** 39 | * Create a job instance based on serialized input 40 | * 41 | * Instantiate a job based on a serialized data string. The string 42 | * is a JSON string containing job name, content and metadata. Use 43 | * the decoded JSON value to create a job instance, configure it 44 | * and return it. 45 | */ 46 | public function unserializeJob($string, array $metadata = []): JobInterface 47 | { 48 | $data = json_decode($string, true); 49 | $name = $data['metadata']['__name__']; 50 | $metadata += $data['metadata']; 51 | $content = $data['content']; 52 | 53 | /** @var $job JobInterface */ 54 | $job = $this->getJobPluginManager()->get($name); 55 | 56 | if ($job instanceof BinaryMessageInterface) { 57 | $content = base64_decode($content); 58 | } 59 | 60 | $content = unserialize($content); 61 | 62 | $job->setContent($content); 63 | $job->setMetadata($metadata); 64 | 65 | if ($job instanceof QueueAwareInterface) { 66 | $job->setQueue($this); 67 | } 68 | 69 | return $job; 70 | } 71 | 72 | /** 73 | * Serialize job to allow persistence 74 | * 75 | * The serialization format is a JSON object with keys "content", 76 | * "metadata" and "__name__". When a job is fetched from the SL, a job name 77 | * will be set and be available as metadata. An invokable job has no service 78 | * name and therefore the FQCN will be used. 79 | */ 80 | public function serializeJob(JobInterface $job): string 81 | { 82 | $job->setMetadata('__name__', $job->getMetadata('__name__', get_class($job))); 83 | 84 | $data = [ 85 | 'content' => serialize($job->getContent()), 86 | 'metadata' => $job->getMetadata(), 87 | ]; 88 | 89 | if ($job instanceof BinaryMessageInterface) { 90 | $data['content'] = base64_encode($data['content']); 91 | } 92 | 93 | return json_encode($data, JSON_THROW_ON_ERROR); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/Queue/BinaryMessageInterface.php: -------------------------------------------------------------------------------- 1 | queue; 15 | } 16 | 17 | public function setQueue(QueueInterface $queue): void 18 | { 19 | $this->queue = $queue; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Queue/QueueInterface.php: -------------------------------------------------------------------------------- 1 | setOptions($options); 23 | } 24 | } 25 | 26 | public function setOptions(array $options): void 27 | { 28 | foreach ($options as $key => $value) { 29 | $setter = 'set' . str_replace(' ', '', ucwords(str_replace('_', ' ', $key))); 30 | if (! method_exists($this, $setter)) { 31 | throw new Exception\BadMethodCallException( 32 | 'The option "' . $key . '" does not ' 33 | . 'have a matching ' . $setter . ' setter method ' 34 | . 'which must be defined' 35 | ); 36 | } 37 | $this->{$setter}($value); 38 | } 39 | } 40 | 41 | /** 42 | * Event listener which returns the state of the queue 43 | * 44 | * @param ProcessStateEvent $event 45 | * @return bool|string 46 | */ 47 | public function onReportQueueState(ProcessStateEvent $event) 48 | { 49 | return is_string($this->state) ? ProcessStateResult::withState($this->state) : false; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Strategy/AttachQueueListenersStrategy.php: -------------------------------------------------------------------------------- 1 | pluginManager = $pluginManager; 26 | $this->strategyConfig = $strategyConfig; 27 | } 28 | 29 | public function attach(EventManagerInterface $events, $priority = 1): void 30 | { 31 | $this->listeners[] = $events->attach( 32 | WorkerEventInterface::EVENT_BOOTSTRAP, 33 | [$this, 'attachQueueListeners'], 34 | PHP_INT_MAX 35 | ); 36 | } 37 | 38 | public function attachQueueListeners(BootstrapEvent $bootstrapEvent): void 39 | { 40 | /** @var AbstractWorker $worker */ 41 | $worker = $bootstrapEvent->getWorker(); 42 | $name = $bootstrapEvent->getQueue()->getName(); 43 | $eventManager = $worker->getEventManager(); 44 | 45 | $this->detach($eventManager); 46 | 47 | if (! isset($this->strategyConfig[$name])) { 48 | $name = 'default'; // We want to make sure the default process queue is always attached 49 | } 50 | 51 | $strategies = $this->strategyConfig[$name]; 52 | 53 | foreach ($strategies as $strategy => $options) { 54 | // no options given, name stored as value 55 | if (is_numeric($strategy) && is_string($options)) { 56 | $strategy = $options; 57 | $options = []; 58 | } 59 | 60 | if (! is_string($strategy) || ! is_array($options)) { 61 | continue; 62 | } 63 | 64 | $priority = null; 65 | if (isset($options['priority'])) { 66 | $priority = $options['priority']; 67 | unset($options['priority']); 68 | } 69 | 70 | /** @var ListenerAggregateInterface $listener */ 71 | $listener = $this->pluginManager->get($strategy, $options); 72 | 73 | if ($priority !== null) { 74 | $listener->attach($eventManager, $priority); 75 | } else { 76 | $listener->attach($eventManager); 77 | } 78 | } 79 | 80 | $bootstrapEvent->stopPropagation(); 81 | $eventManager->triggerEvent(new BootstrapEvent($worker, $bootstrapEvent->getQueue())); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Strategy/Factory/AttachQueueListenersStrategyFactory.php: -------------------------------------------------------------------------------- 1 | get(StrategyPluginManager::class); 19 | $config = $container->get('config'); 20 | $strategyConfig = $config['slm_queue']['worker_strategies']['queues']; 21 | 22 | return new AttachQueueListenersStrategy($pluginManager, $strategyConfig); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Strategy/FileWatchStrategy.php: -------------------------------------------------------------------------------- 1 | pattern = $pattern; 46 | $this->files = []; 47 | } 48 | 49 | public function getPattern(): string 50 | { 51 | return $this->pattern; 52 | } 53 | 54 | public function setIdleThrottleTime(int $idleThrottleTime): void 55 | { 56 | $this->idleThrottleTime = $idleThrottleTime; 57 | } 58 | 59 | /** 60 | * Files being watched 61 | */ 62 | public function getFiles(): ?array 63 | { 64 | return $this->files; 65 | } 66 | 67 | public function attach(EventManagerInterface $events, $priority = 1): void 68 | { 69 | $this->listeners[] = $events->attach( 70 | WorkerEventInterface::EVENT_PROCESS_IDLE, 71 | [$this, 'onStopConditionCheck'], 72 | $priority 73 | ); 74 | $this->listeners[] = $events->attach( 75 | WorkerEventInterface::EVENT_PROCESS_QUEUE, 76 | [$this, 'onStopConditionCheck'], 77 | 1000 78 | ); 79 | $this->listeners[] = $events->attach( 80 | WorkerEventInterface::EVENT_PROCESS_STATE, 81 | [$this, 'onReportQueueState'], 82 | $priority 83 | ); 84 | } 85 | 86 | public function onStopConditionCheck(WorkerEventInterface $event): ?ExitWorkerLoopResult 87 | { 88 | if ($event->getName() == WorkerEventInterface::EVENT_PROCESS_IDLE) { 89 | if ($this->previousIdlingTime + $this->idleThrottleTime > microtime(true)) { 90 | return null; 91 | } else { 92 | $this->previousIdlingTime = microtime(true); 93 | } 94 | } 95 | 96 | if (! count($this->files)) { 97 | $this->constructFileList(); 98 | 99 | $this->state = sprintf("watching %s files for modifications", count($this->files)); 100 | } 101 | 102 | foreach ($this->files as $checksum => $file) { 103 | if (! file_exists($file) || ! is_readable($file) || (string) $checksum !== hash_file('crc32', $file)) { 104 | $reason = sprintf("file modification detected for '%s'", $file); 105 | 106 | return ExitWorkerLoopResult::withReason($reason); 107 | } 108 | } 109 | 110 | return null; 111 | } 112 | 113 | protected function constructFileList(): void 114 | { 115 | $iterator = new RecursiveDirectoryIterator('.', RecursiveDirectoryIterator::FOLLOW_SYMLINKS); 116 | $files = new RecursiveIteratorIterator($iterator); 117 | 118 | /** @var $file SplFileInfo */ 119 | foreach ($files as $file) { 120 | if ($file->isDir()) { 121 | continue; 122 | } 123 | 124 | if (! preg_match($this->pattern, $file)) { 125 | continue; 126 | } 127 | 128 | $this->files[hash_file('crc32', $file)] = (string) $file; 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/Strategy/InterruptStrategy.php: -------------------------------------------------------------------------------- 1 | listeners[] = $events->attach( 30 | WorkerEventInterface::EVENT_PROCESS_IDLE, 31 | [$this, 'onStopConditionCheck'], 32 | $priority 33 | ); 34 | $this->listeners[] = $events->attach( 35 | WorkerEventInterface::EVENT_PROCESS_QUEUE, 36 | [$this, 'onStopConditionCheck'], 37 | -1000 38 | ); 39 | $this->listeners[] = $events->attach( 40 | WorkerEventInterface::EVENT_PROCESS_STATE, 41 | [$this, 'onReportQueueState'], 42 | $priority 43 | ); 44 | } 45 | 46 | public function onStopConditionCheck(WorkerEventInterface $event): ?ExitWorkerLoopResult 47 | { 48 | declare(ticks=1); 49 | 50 | if ($this->interrupted) { 51 | $reason = sprintf("interrupt by an external signal on '%s'", $event->getName()); 52 | 53 | return ExitWorkerLoopResult::withReason($reason); 54 | } 55 | 56 | return null; 57 | } 58 | 59 | public function onPCNTLSignal(int $signal): void 60 | { 61 | switch ($signal) { 62 | case SIGTERM: 63 | case SIGINT: 64 | $this->interrupted = true; 65 | break; 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Strategy/MaxMemoryStrategy.php: -------------------------------------------------------------------------------- 1 | maxMemory = $maxMemory; 19 | } 20 | 21 | public function getMaxMemory(): int 22 | { 23 | return $this->maxMemory; 24 | } 25 | 26 | public function attach(EventManagerInterface $events, $priority = 1): void 27 | { 28 | $this->listeners[] = $events->attach( 29 | WorkerEventInterface::EVENT_PROCESS_IDLE, 30 | [$this, 'onStopConditionCheck'], 31 | $priority 32 | ); 33 | $this->listeners[] = $events->attach( 34 | WorkerEventInterface::EVENT_PROCESS_QUEUE, 35 | [$this, 'onStopConditionCheck'], 36 | -1000 37 | ); 38 | $this->listeners[] = $events->attach( 39 | WorkerEventInterface::EVENT_PROCESS_STATE, 40 | [$this, 'onReportQueueState'], 41 | $priority 42 | ); 43 | } 44 | 45 | public function onStopConditionCheck(WorkerEventInterface $event): ?ExitWorkerLoopResult 46 | { 47 | // @see http://php.net/manual/en/features.gc.collecting-cycles.php 48 | if (gc_enabled()) { 49 | gc_collect_cycles(); 50 | } 51 | 52 | $usage = memory_get_usage(); 53 | $this->state = sprintf('%s memory usage', $this->humanFormat($usage)); 54 | 55 | if ($this->maxMemory && $usage > $this->maxMemory) { 56 | $reason = sprintf( 57 | "memory threshold of %s exceeded (usage: %s)", 58 | $this->humanFormat($this->maxMemory), 59 | $this->humanFormat($usage) 60 | ); 61 | 62 | return ExitWorkerLoopResult::withReason($reason); 63 | } 64 | 65 | return null; 66 | } 67 | 68 | private function humanFormat(int $bytes): string 69 | { 70 | $units = ['b', 'kB', 'MB', 'GB', 'TB', 'PB']; 71 | 72 | return @round($bytes / (1024 ** ($i = floor(log($bytes, 1024)))), 2) . $units[$i]; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Strategy/MaxPollingFrequencyStrategy.php: -------------------------------------------------------------------------------- 1 | listeners[] = $events->attach( 27 | WorkerEventInterface::EVENT_PROCESS_QUEUE, 28 | [$this, 'onQueueProcessFinish'], 29 | 1000 30 | ); 31 | } 32 | 33 | public function onQueueProcessFinish(ProcessQueueEvent $event): void 34 | { 35 | $startTime = microtime(true); 36 | $time = ($startTime - $this->lastTime); 37 | 38 | $minTime = 1.0 / $this->maxFrequency; 39 | 40 | if ($time < $minTime) { 41 | $waitTime = $minTime - $time; 42 | usleep(round($waitTime * 1000000)); 43 | } 44 | 45 | $this->lastTime = microtime(true); 46 | } 47 | 48 | public function setMaxFrequency(float $maxFrequency): void 49 | { 50 | $this->maxFrequency = $maxFrequency; 51 | } 52 | 53 | public function getMaxFrequency(): float 54 | { 55 | return $this->maxFrequency; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Strategy/MaxRunsStrategy.php: -------------------------------------------------------------------------------- 1 | maxRuns = $maxRuns; 29 | } 30 | 31 | public function getMaxRuns(): int 32 | { 33 | return $this->maxRuns; 34 | } 35 | 36 | public function attach(EventManagerInterface $events, $priority = 1): void 37 | { 38 | $this->listeners[] = $events->attach( 39 | WorkerEventInterface::EVENT_PROCESS_QUEUE, 40 | [$this, 'onStopConditionCheck'], 41 | -1000 42 | ); 43 | $this->listeners[] = $events->attach( 44 | WorkerEventInterface::EVENT_PROCESS_STATE, 45 | [$this, 'onReportQueueState'], 46 | $priority 47 | ); 48 | } 49 | 50 | public function onStopConditionCheck(WorkerEventInterface $event): ?ExitWorkerLoopResult 51 | { 52 | $this->runCount++; 53 | 54 | $this->state = sprintf('%s jobs processed', $this->runCount); 55 | 56 | if ($this->maxRuns && $this->runCount >= $this->maxRuns) { 57 | $reason = sprintf('maximum of %s jobs processed', $this->runCount); 58 | 59 | return ExitWorkerLoopResult::withReason($reason); 60 | } 61 | 62 | return null; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Strategy/ProcessQueueStrategy.php: -------------------------------------------------------------------------------- 1 | listeners[] = $events->attach( 23 | WorkerEventInterface::EVENT_PROCESS_QUEUE, 24 | [$this, 'onJobPop'], 25 | $priority 26 | ); 27 | $this->listeners[] = $events->attach( 28 | WorkerEventInterface::EVENT_PROCESS_JOB, 29 | [$this, 'onJobProcess'], 30 | $priority 31 | ); 32 | } 33 | 34 | public function onJobPop(ProcessQueueEvent $processQueueEvent): ?ExitWorkerLoopResult 35 | { 36 | /** @var AbstractWorker $worker */ 37 | $worker = $processQueueEvent->getWorker(); 38 | $queue = $processQueueEvent->getQueue(); 39 | $options = $processQueueEvent->getOptions(); 40 | $eventManager = $worker->getEventManager(); 41 | 42 | $job = $queue->pop($options); 43 | 44 | // The queue may return null, for instance if a timeout was set 45 | if (! $job instanceof JobInterface) { 46 | /** @var ResponseCollection $results */ 47 | $results = $eventManager->triggerEventUntil( 48 | function ($response) { 49 | return $response instanceof ExitWorkerLoopResult; 50 | }, 51 | new ProcessIdleEvent($worker, $queue) 52 | ); 53 | 54 | $processQueueEvent->stopPropagation(); 55 | 56 | if ($results->stopped()) { 57 | return $results->last(); 58 | } 59 | 60 | return null; 61 | } 62 | 63 | $eventManager->triggerEvent(new ProcessJobEvent($job, $worker, $queue)); 64 | 65 | return null; 66 | } 67 | 68 | public function onJobProcess(ProcessJobEvent $processJobEvent): void 69 | { 70 | $job = $processJobEvent->getJob(); 71 | $queue = $processJobEvent->getQueue(); 72 | 73 | /** @var AbstractWorker $worker */ 74 | $worker = $processJobEvent->getWorker(); 75 | 76 | $result = $worker->processJob($job, $queue); 77 | $processJobEvent->setResult($result); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Strategy/StrategyPluginManager.php: -------------------------------------------------------------------------------- 1 | lifetime = (int) $lifetime; 34 | } 35 | 36 | public function getLifetime(): int 37 | { 38 | return $this->lifetime; 39 | } 40 | 41 | /** 42 | * {@inheritDoc} 43 | */ 44 | public function attach(EventManagerInterface $events, $priority = 1): void 45 | { 46 | $this->listeners[] = $events->attach( 47 | WorkerEventInterface::EVENT_BOOTSTRAP, 48 | [$this, 'onBootstrap'], 49 | $priority 50 | ); 51 | 52 | $this->listeners[] = $events->attach( 53 | WorkerEventInterface::EVENT_PROCESS_QUEUE, 54 | [$this, 'checkRuntime'], 55 | -1000 56 | ); 57 | 58 | $this->listeners[] = $events->attach( 59 | WorkerEventInterface::EVENT_PROCESS_IDLE, 60 | [$this, 'checkRuntime'], 61 | -1000 62 | ); 63 | 64 | $this->listeners[] = $events->attach( 65 | WorkerEventInterface::EVENT_PROCESS_STATE, 66 | [$this, 'onReportQueueState'], 67 | $priority 68 | ); 69 | } 70 | 71 | /** 72 | * @param BootstrapEvent $event 73 | */ 74 | public function onBootstrap(BootstrapEvent $event) 75 | { 76 | $this->startTime = time(); 77 | } 78 | 79 | /** 80 | * @param WorkerEventInterface $event 81 | * 82 | * @return ExitWorkerLoopResult|null 83 | */ 84 | public function checkRuntime(WorkerEventInterface $event) 85 | { 86 | $now = time(); 87 | $runtime = $now - $this->startTime; 88 | $this->state = sprintf('%d seconds passed', $runtime); 89 | 90 | if ($runtime >= $this->lifetime) { 91 | $reason = sprintf('lifetime of %d seconds reached', $this->lifetime); 92 | 93 | return ExitWorkerLoopResult::withReason($reason); 94 | } 95 | 96 | return null; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/Worker/AbstractWorker.php: -------------------------------------------------------------------------------- 1 | setIdentifiers([ 27 | __CLASS__, 28 | get_called_class(), 29 | 'SlmQueue\Worker\WorkerInterface', 30 | ]); 31 | 32 | $this->eventManager = $eventManager; 33 | } 34 | 35 | public function processQueue(QueueInterface $queue, array $options = []): array 36 | { 37 | $this->eventManager->triggerEvent(new BootstrapEvent($this, $queue)); 38 | 39 | $shouldExitWorkerLoop = false; 40 | while (! $shouldExitWorkerLoop) { 41 | /** @var ResponseCollection $exitReasons */ 42 | $exitReasons = $this->eventManager->triggerEventUntil( 43 | function ($response) { 44 | return $response instanceof ExitWorkerLoopResult; 45 | }, 46 | new ProcessQueueEvent($this, $queue, $options) 47 | ); 48 | 49 | if ($exitReasons->stopped() && $exitReasons->last()) { 50 | $shouldExitWorkerLoop = true; 51 | } 52 | } 53 | 54 | $this->eventManager->triggerEvent(new FinishEvent($this, $queue)); 55 | 56 | $queueState = $this->eventManager->triggerEvent(new ProcessStateEvent($this)); 57 | $queueState = array_filter(iterator_to_array($queueState)); 58 | 59 | if ($exitReasons->last()) { 60 | $queueState[] = $exitReasons->last(); 61 | } 62 | 63 | // cast to string 64 | $queueState = array_map('strval', $queueState); 65 | 66 | return $queueState; 67 | } 68 | 69 | public function getEventManager(): EventManagerInterface 70 | { 71 | return $this->eventManager; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Worker/Event/AbstractWorkerEvent.php: -------------------------------------------------------------------------------- 1 | getTarget(); 23 | 24 | return $worker; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Worker/Event/BootstrapEvent.php: -------------------------------------------------------------------------------- 1 | queue = $queue; 20 | } 21 | 22 | public function getQueue(): QueueInterface 23 | { 24 | return $this->queue; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Worker/Event/FinishEvent.php: -------------------------------------------------------------------------------- 1 | queue = $queue; 20 | } 21 | 22 | public function getQueue(): QueueInterface 23 | { 24 | return $this->queue; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Worker/Event/ProcessIdleEvent.php: -------------------------------------------------------------------------------- 1 | queue = $queue; 20 | } 21 | 22 | public function getQueue(): QueueInterface 23 | { 24 | return $this->queue; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Worker/Event/ProcessJobEvent.php: -------------------------------------------------------------------------------- 1 | queue = $queue; 58 | $this->setJob($job); 59 | } 60 | 61 | public function getQueue(): QueueInterface 62 | { 63 | return $this->queue; 64 | } 65 | 66 | private function setJob(JobInterface $job): void 67 | { 68 | $this->job = $job; 69 | $this->setResult(self::JOB_STATUS_UNKNOWN); 70 | } 71 | 72 | public function getJob(): JobInterface 73 | { 74 | return $this->job; 75 | } 76 | 77 | public function setResult(int $result): void 78 | { 79 | $this->result = $result; 80 | } 81 | 82 | public function getResult(): int 83 | { 84 | return $this->result; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/Worker/Event/ProcessQueueEvent.php: -------------------------------------------------------------------------------- 1 | queue = $queue; 25 | $this->options = $options; 26 | } 27 | 28 | public function getQueue(): QueueInterface 29 | { 30 | return $this->queue; 31 | } 32 | 33 | public function getOptions(): array 34 | { 35 | return $this->options; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Worker/Event/ProcessStateEvent.php: -------------------------------------------------------------------------------- 1 | reason = $reason; 15 | } 16 | 17 | public static function withReason(string $reason): ExitWorkerLoopResult 18 | { 19 | return new static($reason); 20 | } 21 | 22 | public function getReason(): string 23 | { 24 | return $this->reason; 25 | } 26 | 27 | public function __toString(): string 28 | { 29 | return (string) $this->reason; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Worker/Result/ProcessStateResult.php: -------------------------------------------------------------------------------- 1 | state = $state; 15 | } 16 | 17 | public static function withState(string $state): ProcessStateResult 18 | { 19 | return new static($state); 20 | } 21 | 22 | public function getState(): string 23 | { 24 | return $this->state; 25 | } 26 | 27 | public function __toString(): string 28 | { 29 | return (string) $this->state; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Worker/WorkerInterface.php: -------------------------------------------------------------------------------- 1 | addAbstractFactory(new WorkerAbstractFactory()); 21 | } 22 | } 23 | --------------------------------------------------------------------------------