├── .github └── workflows │ ├── linter.yml │ ├── phpcs.yml │ ├── phpstan.yml │ └── phpunit.yml ├── CHANGELOG.md ├── CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── composer.json ├── examples ├── predis │ ├── emitter.php │ ├── processor.php │ └── shutdown.php ├── rabbitmq │ ├── emitter.php │ └── processor.php ├── redis │ ├── emitter.php │ ├── processor.php │ └── shutdown.php └── sqs │ ├── emitter.php │ └── processor.php ├── phpstan.neon ├── phpunit.xml └── src ├── Dispatcher.php ├── DispatcherInterface.php ├── Driver ├── AmazonSqsDriver.php ├── DriverInterface.php ├── LazyRabbitMqDriver.php ├── MaxItemsTrait.php ├── NotSupportedException.php ├── PredisSetDriver.php ├── RedisSetDriver.php ├── SerializerAwareTrait.php ├── ShutdownTrait.php └── UnknownPriorityException.php ├── Emitter.php ├── EmitterInterface.php ├── Handler ├── EchoHandler.php ├── HandlerInterface.php └── RetryTrait.php ├── HermesException.php ├── Message.php ├── MessageInterface.php ├── MessageSerializer.php ├── SerializeException.php ├── SerializerInterface.php └── Shutdown ├── PredisShutdown.php ├── RedisShutdown.php ├── SharedFileShutdown.php ├── ShutdownException.php └── ShutdownInterface.php /.github/workflows/linter.yml: -------------------------------------------------------------------------------- 1 | name: PHP linter 2 | 3 | on: [push] 4 | 5 | jobs: 6 | phpcs: 7 | name: Linter 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout code 11 | uses: actions/checkout@v2 12 | 13 | - name: lint 14 | run: php -l src tests examples 15 | -------------------------------------------------------------------------------- /.github/workflows/phpcs.yml: -------------------------------------------------------------------------------- 1 | name: PHPCS check 2 | 3 | on: [push] 4 | 5 | jobs: 6 | phpcs: 7 | name: PHPCS 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout code 11 | uses: actions/checkout@v2 12 | 13 | - name: Composer update 14 | run: composer install --prefer-dist --no-progress --no-suggest 15 | 16 | - name: phpcs 17 | run: vendor/bin/phpcs --standard=PSR2 src tests examples -n 18 | -------------------------------------------------------------------------------- /.github/workflows/phpstan.yml: -------------------------------------------------------------------------------- 1 | name: phpstan 2 | 3 | on: [push] 4 | 5 | jobs: 6 | phpcs: 7 | name: phpstan 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout code 11 | uses: actions/checkout@v2 12 | 13 | - name: Composer update 14 | run: composer install --prefer-dist --no-progress --no-suggest 15 | 16 | - name: phpstan 17 | run: vendor/bin/phpstan analyse 18 | -------------------------------------------------------------------------------- /.github/workflows/phpunit.yml: -------------------------------------------------------------------------------- 1 | name: Phpunit 2 | 3 | on: [push] 4 | 5 | jobs: 6 | run: 7 | runs-on: ${{ matrix.operating-system }} 8 | strategy: 9 | matrix: 10 | operating-system: [ubuntu-latest] 11 | php-versions: ['7.2', '7.3', '7.4', '8.0'] 12 | name: PHP ${{ matrix.php-versions }} Test on ${{ matrix.operating-system }} 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v2 16 | 17 | - name: Setup PHP 18 | uses: shivammathur/setup-php@v2 19 | with: 20 | php-version: ${{ matrix.php-versions }} 21 | ini-values: post_max_size=256M, max_execution_time=180 22 | coverage: xdebug 23 | 24 | - name: Composer install 25 | run: composer install --prefer-dist --no-progress --no-suggest 26 | 27 | - name: phpunit 28 | run: vendor/bin/phpunit --coverage-clover build/logs/clover.xml 29 | 30 | - if: | 31 | matrix.php-versions == '7.4' && 32 | matrix.operating-system == 'ubuntu-latest' 33 | name: Check test coverage 34 | uses: johanvanhelden/gha-clover-test-coverage-check@v1 35 | with: 36 | percentage: "70" 37 | filename: "build/logs/clover.xml" 38 | 39 | - if: | 40 | matrix.php-versions == '7.4' && 41 | matrix.operating-system == 'ubuntu-latest' 42 | name: Codeclimate 43 | uses: paambaati/codeclimate-action@v2.7.4 44 | env: 45 | CC_TEST_REPORTER_ID: ${{secrets.CC_TEST_REPORTER_ID}} 46 | with: 47 | coverageLocations: ${{github.workspace}}/build/logs/clover.xml:clover 48 | 49 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file. 3 | Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) principles. 4 | 5 | ## [Unreleased][unreleased] 6 | 7 | 8 | ## 4.2.0 9 | 10 | ### Added 11 | 12 | * **BREAKING CHANGE**: changed interface for Dispatcher - added ability to unregister handlers 13 | 14 | 15 | ## 4.1.0 - 2023-12-17 16 | 17 | ### Changed 18 | 19 | - RedisSetDriver - add atomicity to scheduled set 20 | 21 | 22 | ## 4.0.1 - 2021-11-23 23 | 24 | ### Changed 25 | 26 | * Fixed Predis driver when retry is being scheduled [#48](https://github.com/tomaj/hermes/issues/48) 27 | 28 | 29 | ## 4.0.0 - 2021-02-02 30 | 31 | ### Changed 32 | 33 | * **BREAKING CHANGE**: Renamed all **Restart** to **Shutdown** 34 | * **BREAKING CHANGE**: Removed deprecated *RabbitMqDriver**. You can use LazyRabbitMqDriver 35 | * **BREAKING CHANGE**: Splitted **RedisSetDriver** to two implementations based on how it is interacting with redis. For using Redis php extension you can use old **RedisSetDriver**, for using predis package you have to use **PredisSetDriver** 36 | * Added clearstatcache() into SharedFileShutdown 37 | 38 | 39 | 40 | ## 3.1.0 - 2020-10-22 41 | 42 | ### Changed 43 | 44 | * Added support for *soft restart* to all drivers 45 | * Added `consumer tag` to LazyRabbitMq driver for consumer 46 | * Added support for _max items_ and _restart_ for LazyRabbitMq Driver 47 | * updated restart policy for AmazonSQS Driver 48 | 49 | 50 | ## 3.0.1 - 2020-10-16 51 | 52 | ### Changed 53 | 54 | * Fixed `RedisRestart::restart()` response for Predis instance. `\Predis\Client::set()` returns object _(with 'OK' payload)_ instead of bool. 55 | * Deprecated `RabbitMqDriver` (will be removed in 4.0.0) - use `LazyRabbitMqDriver` instead 56 | * Fixed error while parsing message with invalid UTF8 character 57 | 58 | ## 3.0.0 - 2020-10-13 59 | 60 | ### Added 61 | 62 | * `RedisRestart` - implementation of `RestartInterface` allowing graceful shutdown of Hermes through Redis entry. 63 | * **BREAKING CHANGE**: Added `RestartInterface::restart()` method to initiate Hermes restart without knowing the requirements of used `RestartInterface` implementation. _Updated all related tests._ 64 | * **BREAKING CHANGE**: Removed support for ZeroMQ - driver moved into [separated package](https://github.com/tomaj/hermes-zmq-driver) 65 | * Upgraded phpunit and tests 66 | * **BREAKING CHANGE** Drop support for php 7.1 67 | 68 | ## 2.2.0 - 2019-07-12 69 | 70 | ### Added 71 | 72 | * Ability to register multiple handlers at once for one key (`registerHandlers` in `DispatcherInterface`) 73 | * Fixed loss of messages when the handler crashes and mechanism of retries for RabbitMQ Drivers 74 | 75 | ## 2.1.0 - 2019-07-06 76 | 77 | ### Added 78 | 79 | * Added retry to handlers 80 | 81 | #### Added 82 | 83 | * Added missing handle() method to DispatcherInterface 84 | 85 | ## 2.0.0 - 2018-08-14 86 | 87 | ### Added 88 | 89 | * Message now support scheduled parameter - Driver needs to support this behaviour. 90 | * Type hints 91 | 92 | ### Changed 93 | 94 | * Dropped support for php 5.4 95 | * Deprecated emit() in Disapatcher - introduced Emitter 96 | 97 | ## 1.2.0 - 2016-09-26 98 | 99 | ### Updated 100 | 101 | * Amazon aws library updated to version 3 in composer - still works with v2 but you have to initialize Sqs client in v2 style 102 | 103 | ## 1.1.0 - 2016-09-05 104 | 105 | ### Added 106 | 107 | * Amazon SQS driver 108 | 109 | ## 1.0.0 - 2016-09-02 110 | 111 | ### Added 112 | 113 | * First stable version 114 | * Added ACK to rabbitmq driver 115 | 116 | ## 0.4.0 - 2016-04-26 117 | 118 | ### Added 119 | 120 | * Added RabbitMQ Lazy driver 121 | 122 | ## 0.3.0 - 2016-03-23 123 | 124 | ### Added 125 | 126 | * Added possibility to gracefull restart worker with RestartInterface 127 | * Added Tracy debugger log when error occured 128 | 129 | ## 0.2.0 - 2015-10-30 130 | 131 | ### Changed 132 | 133 | * Handling responses from handlers. 134 | * Tests structure refactored 135 | 136 | ## 0.1.0 - 2015-10-28 137 | 138 | ### Added 139 | 140 | * initial version with 2 drivers 141 | -------------------------------------------------------------------------------- /CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | As contributors and maintainers of this project, and in the interest of fostering an open and welcoming community, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities. 4 | 5 | We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, religion, or nationality. 6 | 7 | Examples of unacceptable behavior by participants include: 8 | 9 | * The use of sexualized language or imagery 10 | * Personal attacks 11 | * Trolling or insulting/derogatory comments 12 | * Public or private harassment 13 | * Publishing other's private information, such as physical or electronic addresses, without explicit permission 14 | * Other unethical or unprofessional conduct. 15 | 16 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. By adopting this Code of Conduct, project maintainers commit themselves to fairly and consistently applying these principles to every aspect of managing this project. Project maintainers who do not follow or enforce the Code of Conduct may be permanently removed from the project team. 17 | 18 | This code of conduct applies both within project spaces and in public spaces when an individual is representing the project or its community in a direct capacity. Personal views, beliefs and values of individuals do not necessarily reflect those of the organisation or affiliated individuals and organisations. 19 | 20 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers. 21 | 22 | This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.2.0, available at [http://contributor-covenant.org/version/1/2/0/](http://contributor-covenant.org/version/1/2/0/) 23 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are **welcome** and will be fully **credited**. 4 | 5 | We accept contributions via Pull Requests on [Github](https://github.com/thephpleague/:package_name). 6 | 7 | 8 | ## Pull Requests 9 | 10 | - **[PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)** - The easiest way to apply the conventions is to install [PHP Code Sniffer](http://pear.php.net/package/PHP_CodeSniffer). 11 | 12 | - **Add tests!** - Your patch won't be accepted if it doesn't have tests. 13 | 14 | - **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date. 15 | 16 | - **Consider our release cycle** - We try to follow [SemVer v2.0.0](http://semver.org/). Randomly breaking public APIs is not an option. 17 | 18 | - **Create feature branches** - Don't ask us to pull from your master branch. 19 | 20 | - **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. 21 | 22 | - **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](http://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting. 23 | 24 | 25 | ## Running Tests 26 | 27 | ``` bash 28 | $ composer test 29 | ``` 30 | 31 | 32 | **Happy coding**! 33 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Tomas Majer 4 | 5 | > Permission is hereby granted, free of charge, to any person obtaining a copy 6 | > of this software and associated documentation files (the "Software"), to deal 7 | > in the Software without restriction, including without limitation the rights 8 | > to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | > copies of the Software, and to permit persons to whom the Software is 10 | > furnished to do so, subject to the following conditions: 11 | > 12 | > The above copyright notice and this permission notice shall be included in 13 | > all copies or substantial portions of the Software. 14 | > 15 | > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | > IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | > FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | > AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | > LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | > OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | > THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hermes 2 | 3 | **Background job processing PHP library** 4 | 5 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/tomaj/hermes/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/tomaj/hermes/?branch=master) 6 | [![Latest Stable Version](https://img.shields.io/packagist/v/tomaj/hermes.svg)](https://packagist.org/packages/tomaj/hermes) 7 | [![Phpstan](https://img.shields.io/badge/PHPStan-level%208-brightgreen.svg)](https://phpstan.org/) 8 | 9 | ## What is Hermes? 10 | 11 | If you need to process some task outside of HTTP request in your web app, you can utilize Hermes. Hermes provides message broker for sending messages from HTTP thread to offline processing jobs. Recommended use for sending emails, call other API or other time-consuming operations. 12 | 13 | Another goal for Hermes is variability to use various message brokers like Redis, rabbit, database, and ability to easily create new drivers for other messaging solutions. And also the simple creation of workers to perform tasks on specified events. 14 | 15 | 16 | ## Installation 17 | 18 | This library requires PHP 7.2 or later. 19 | 20 | The recommended installation method is via Composer: 21 | 22 | ```bash 23 | $ composer require tomaj/hermes 24 | ``` 25 | 26 | Library is compliant with [PSR-1][], [PSR-2][], [PSR-3][] and [PSR-4][]. 27 | 28 | [PSR-1]: https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-1-basic-coding-standard.md 29 | [PSR-2]: https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md 30 | [PSR-3]: https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md 31 | [PSR-4]: https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-4-autoloader.md 32 | 33 | 34 | ## Optional dependencies 35 | 36 | Hermes is able to log activity with a logger that is compatible with `psr/log` interface. For more information take a look at [psr/log][]. 37 | 38 | The library works without logger, but maintainer recommends installing [monolog][] for logging. 39 | 40 | [psr/log]: https://github.com/php-fig/log 41 | [monolog]: https://github.com/Seldaek/monolog 42 | 43 | ## Supported drivers 44 | 45 | Right now Hermes library is distributed with 3 drivers and one driver in a separate package: 46 | 47 | * [Redis][] driver (two different implementations [phpredis][] or [Predis][]) 48 | * [Amazon SQS][] driverDispatcherRestartTest.php 49 | * [RabbitMQ][] driver 50 | * [ZeroMQ][] drivver (via [php-zmq][] extension) availabe as [tomaj/hermes-zmq-driver](https://github.com/tomaj/hermes-zmq-driver) 51 | 52 | **Note:** You have to install all 3rd party libraries for initializing connections to these drivers. For example, you have to add `nrk/predis` to your *composer.json* and create a connection to your Redis instance. 53 | 54 | [Amazon SQS]: https://aws.amazon.com/sqs/ 55 | [php-zmq]: https://zeromq.org/ 56 | [phpredis]: https://github.com/phpredis/phpredis 57 | [Redis]: https://redis.io/ 58 | [RabbitMQ]: https://www.rabbitmq.com/ 59 | [Predis]: https://github.com/nrk/predis 60 | [ZeroMQ]: https://zeromq.org/ 61 | 62 | 63 | ## Concept - How Hermes works? 64 | 65 | Hermes works as an emitter and Dispatcher for events from your PHP requests on the webserver to particular handler running on CLI. Basically like this: 66 | 67 | ``` 68 | --> HTTP request to /file.php -> emit(Message) -> Hermes Emitter 69 | \ 70 | Queue (Redis, rabbit etc.) 71 | / 72 | --> running PHP CLI file waiting for new Message-s from Queue 73 | when received a new message it calls registered handler to process it. 74 | ``` 75 | 76 | You have to implement these four steps in your application: 77 | 78 | 1. select driver that you would like to use and register it to Dispatcher and Emitter 79 | 2. emit events when you need to process something in the background 80 | 3. write a handler class that will process your message from 2. 81 | 4. create a PHP file that will run on your server "forever" and run Dispatcher there 82 | 83 | 84 | ## How to use 85 | 86 | This simple example demonstrates using Redis driver and is an example of how to send email in the background. 87 | 88 | 89 | ### Emitting event 90 | 91 | Emitting messages (anywhere in the application, easy and quick). 92 | 93 | ```php 94 | use Redis; 95 | use Tomaj\Hermes\Message; 96 | use Tomaj\Hermes\Emitter; 97 | use Tomaj\Hermes\Driver\RedisSetDriver; 98 | 99 | $redis = new Redis(); 100 | $redis->connect('127.0.0.1', 6379); 101 | $driver = new RedisSetDriver($redis); 102 | $emitter = new Emitter($driver); 103 | 104 | $message = new Message('send-email', [ 105 | 'to' => 'test@test.com', 106 | 'subject' => 'Testing hermes email', 107 | 'message' => 'Hello from hermes!' 108 | ]); 109 | 110 | $emitter->emit($message); 111 | ``` 112 | 113 | ### Processing event 114 | 115 | For processing an event, we need to create some PHP file that will be running in CLI. We can make this simple implementation and register this simple handler. 116 | 117 | 118 | ```php 119 | # file handler.php 120 | use Redis; 121 | use Tomaj\Hermes\Driver\RedisSetDriver; 122 | use Tomaj\Hermes\Dispatcher; 123 | use Tomaj\Hermes\Handler\HandlerInterface; 124 | 125 | class SendEmailHandler implements HandlerInterface 126 | { 127 | // here you will receive message that was emitted from web application 128 | public function handle(MessageInterface $message) 129 | { 130 | $payload = $message->getPayload(); 131 | mail($payload['to'], $payload['subject'], $payload['message']); 132 | return true; 133 | } 134 | } 135 | 136 | 137 | // create dispatcher like in the first snippet 138 | $redis = new Redis(); 139 | $redis->connect('127.0.0.1', 6379); 140 | $driver = new RedisSetDriver($redis); 141 | $dispatcher = new Dispatcher($driver); 142 | 143 | // register handler for event 144 | $dispatcher->registerHandler('send-email', new SendEmailHandler()); 145 | 146 | // at this point this script will wait for new message 147 | $dispatcher->handle(); 148 | ``` 149 | 150 | For running *handler.php* on your server you can use tools like [upstart][], [supervisord][], [monit][], [god][], or any other alternative. 151 | 152 | [upstart]: http://upstart.ubuntu.com/ 153 | [supervisord]: http://supervisord.org 154 | [monit]: https://mmonit.com/monit/ 155 | [god]: http://godrb.com/ 156 | 157 | ## Logging 158 | 159 | Hermes can use any [psr/log][] logger. You can set logger for Dispatcher or Emitter and see what type of messages come to Dispatcher or Emitter and when a handler processed a message. If you add trait `Psr\Log\LoggerAwareTrait` (or implement `Psr\Log\LoggerAwareInterface`) to your handler, you can use logger also in your handler (Dispatcher and Emitter injects it automatically). 160 | 161 | Basic example with [monolog][]: 162 | 163 | ```php 164 | 165 | use Monolog\Logger; 166 | use Monolog\Handler\StreamHandler; 167 | 168 | // create a log channel 169 | $log = new Logger('hermes'); 170 | $log->pushHandler(new StreamHandler('hermes.log')); 171 | 172 | // $driver = .... 173 | 174 | $dispatcher = new Dispatcher($driver, $log); 175 | ``` 176 | 177 | and if you want to log also some information in handlers: 178 | 179 | ```php 180 | use Redis; 181 | use Tomaj\Hermes\Driver\RedisSetDriver; 182 | use Tomaj\Hermes\Dispatcher; 183 | use Tomaj\Hermes\Handler\HandlerInterface; 184 | use Psr\Log\LoggerAwareTrait; 185 | 186 | class SendEmailHandlerWithLogger implements HandlerInterface 187 | { 188 | // enable logger 189 | use LoggerAwareTrait; 190 | 191 | public function handle(MessageInterface $message) 192 | { 193 | $payload = $message->getPayload(); 194 | 195 | // log info message 196 | $this->logger->info("Trying to send email to {$payload['to']}"); 197 | 198 | mail($payload['to'], $payload['subject'], $payload['message']); 199 | return true; 200 | } 201 | } 202 | 203 | ``` 204 | 205 | ## Retry 206 | 207 | If you need to retry, you handle() method when they fail for some reason you can add `RetryTrait` to the handler. 208 | If you want, you can override the `maxRetry()` method from this trait to specify how many times Hermes will try to run your handle(). 209 | **Warning:** if you want to use retry you have to use a driver that supports delayed execution (`$executeAt` message parameter) 210 | 211 | ```php 212 | declare(strict_types=1); 213 | 214 | namespace Tomaj\Hermes\Handler; 215 | 216 | use Tomaj\Hermes\MessageInterface; 217 | 218 | class EchoHandler implements HandlerInterface 219 | { 220 | use RetryTrait; 221 | 222 | public function handle(MessageInterface $message): bool 223 | { 224 | throw new \Exception('this will always fail'); 225 | } 226 | 227 | // optional - default is 25 228 | public function maxRetry(): int 229 | { 230 | return 10; 231 | } 232 | } 233 | ``` 234 | 235 | ## Priorities 236 | 237 | There is a possibility to declare multiple queues with different priority and ensure that messages in the high priority queue will be processed first. 238 | 239 | Example with Redis driver: 240 | ```php 241 | use Tomaj\Hermes\Driver\RedisSetDriver; 242 | use Tomaj\Hermes\Emitter; 243 | use Tomaj\Hermes\Message; 244 | use Tomaj\Hermes\Dispatcher; 245 | 246 | $redis = new Redis(); 247 | $redis->connect('127.0.0.1', 6379); 248 | $driver = new RedisSetDriver($redis); 249 | $driver->setupPriorityQueue('hermes_low', Dispatcher::DEFAULT_PRIORITY - 10); 250 | $driver->setupPriorityQueue('hermes_high', Dispatcher::DEFAULT_PRIORITY + 10); 251 | 252 | $emitter = new Emitter($driver); 253 | $emitter->emit(new Message('type1', ['a' => 'b'], Dispatcher::DEFAULT_PRIORITY - 10)); 254 | $emitter->emit(new Message('type1', ['c' => 'd'], Dispatcher::DEFAULT_PRIORITY + 10)); 255 | ``` 256 | 257 | Few details: 258 | - you can use priority constants from `Dispatcher` class, but you can also use any number 259 | - high number priority queue messages will be handled first 260 | - in `Dispatcher::handle()` method you can provide an array of queue names and create a worker that will handle only one or multiple selected queues 261 | 262 | ## Graceful shutdown 263 | 264 | Hermes worker can be gracefully stopped. 265 | 266 | If implementation of `Tomaj\Hermes\Shutdoown\ShutdownInteface` is provided when initiating `Dispatcher`, Hermes will check `ShutdwnInterface::shouldShutdown()` after each processed message. If it returns `true`, Hermes will shutdown _(notice is logged)_. 267 | 268 | **WARNING:** relaunch is not provided by this library, and it should be handled by process controller you use to keep Hermes running _(e.g. launchd, daemontools, supervisord, etc.)_. 269 | 270 | Currently, two methods are implemented. 271 | 272 | ### SharedFileShutdown 273 | 274 | Shutdown initiated by touching predefined file. 275 | 276 | ```php 277 | $shutdownFile = '/tmp/hermes_shutdown'; 278 | $shutdown = Tomaj\Hermes\Shutdown\SharedFileShutdown($shutdownFile); 279 | 280 | // $log = ... 281 | // $driver = .... 282 | $dispatcher = new Dispatcher($driver, $log, $shutdown); 283 | 284 | // ... 285 | 286 | // shutdown can be triggered be calling `ShutdownInterface::shutdown()` 287 | $shutdown->shutdown(); 288 | ``` 289 | 290 | ### RedisShutdown 291 | 292 | Shutdown initiated by storing timestamp to Redis to predefined shutdown key. 293 | 294 | ```php 295 | $redisClient = new Predis\Client(); 296 | $redisShutdownKey = 'hermes_shutdown'; // can be omitted; default value is `hermes_shutdown` 297 | $shutdown = Tomaj\Hermes\Shutdown\RedisShutdown($redisClient, $redisShutdownKey); 298 | 299 | // $log = ... 300 | // $driver = .... 301 | $dispatcher = new Dispatcher($driver, $log, $shutdown); 302 | 303 | // ... 304 | 305 | // shutdown can be triggered be calling `ShutdownInteface::shutdown()` 306 | $shutdown->shutdown(); 307 | ``` 308 | 309 | ## Scaling Hermes 310 | 311 | If you have many messages that you need to process, you can scale your Hermes workers very quickly. You just run multiple instances of handlers - CLI files that will register handlers to Dispatcher and then run `$dispatcher->handle()`. You can also put your source codes to multiple machines and scale it out to as many nodes as you want. But it would help if you had a driver that supports these 2 things: 312 | 313 | 1. driver needs to be able to work over the network 314 | 2. one message must be delivered to only one worker 315 | 316 | If you ensure this, Hermes will work correctly. Rabbit driver or Redis driver can handle this stuff, and these products are made for big loads, too. 317 | 318 | ## Extending Hermes 319 | 320 | Hermes is written as separate classes that depend on each other via interfaces. You can easily change the implementation of classes. For example, you can create a new driver, use another logger. Or if you really want, you can create the format of your messages that will be sent to your driver serialized via your custom serializer. 321 | 322 | ### How to write your driver 323 | 324 | Each driver has to implement `Tomaj\Hermes\Driver\DriverInterface` with 2 methods (**send** and **wait**). A simple driver that will use [Gearman][] as a driver 325 | 326 | ```PHP 327 | namespace My\Custom\Driver; 328 | 329 | use Tomaj\Hermes\Driver\DriverInterface; 330 | use Tomaj\Hermes\Message; 331 | use Closure; 332 | 333 | class GearmanDriver implements DriverInterface 334 | { 335 | private $client; 336 | 337 | private $worker; 338 | 339 | private $channel; 340 | 341 | private $serializer; 342 | 343 | public function __construct(GearmanClient $client, GearmanWorker $worker, $channel = 'hermes') 344 | { 345 | $this->client = $client; 346 | $this->worker = $worker; 347 | $this->channel = $channel; 348 | $this->serializer = $serialier; 349 | } 350 | 351 | public function send(Message $message) 352 | { 353 | $this->client->do($this->channel, $this->serializer->serialize($message)); 354 | } 355 | 356 | public function wait(Closure $callback) 357 | { 358 | $worker->addFunction($this->channel, function ($gearmanMessage) use ($callback) { 359 | $message = $this->serializer->unserialize($gearmanMessage); 360 | $callback($message); 361 | }); 362 | while ($this->worker->work()); 363 | } 364 | } 365 | ``` 366 | 367 | [Gearman]: http://gearman.org/ 368 | 369 | ### How to write your own serializer 370 | 371 | If you want o use your own serializer in your drivers, you have to create a new class that implements `Tomaj\Hermes\MessageSerializer`, and you need a driver that will support it. You can add the trait `Tomaj\Hermes\Driver\SerializerAwareTrait` to your driver that will add method `setSerializer` to your driver. 372 | 373 | Simple serializer that will use library [jms/serializer][]: 374 | 375 | ```php 376 | namespace My\Custom\Serializer; 377 | 378 | use Tomaj\Hermes\SerializerInterface; 379 | use Tomaj\Hermes\MessageInterface; 380 | 381 | class JmsSerializer implements SerializerInterface 382 | { 383 | public function serialize(MessageInterface $message) 384 | { 385 | $serializer = JMS\Serializer\SerializerBuilder::create()->build(); 386 | return $serializer->serialize($message, 'json'); 387 | } 388 | 389 | public function unserialize($string) 390 | { 391 | $serializer = JMS\Serializer\SerializerBuilder::create()->build(); 392 | return $serializer->deserialize($message, 'json'); 393 | } 394 | } 395 | ``` 396 | 397 | [jms/serializer]: http://jmsyst.com/libs/serializer 398 | 399 | 400 | 401 | ### Scheduled execution 402 | 403 | From version 2.0 you can add the 4th parameter to Message as a timestamp in the future. This message will be processed after this time. This functionality is supported in RedisSetDriver and PredisSetDriver right now. 404 | 405 | ### Upgrade 406 | 407 | #### From v3 to v4 408 | 409 | - Renamed Restart to Shutdown 410 | * Naming changed to reflect the functionality of Hermes. It can gracefully stop own process, but restart (relaunch) of Hermes has to be handled by external process/library. And therefore this is shutdown and not restart. 411 | * RestartInterface to ShutdownInterface 412 | * also all implementations changed namespace name and class name 413 | 414 | ## Changelog 415 | 416 | Please see [CHANGELOG](CHANGELOG.md) for more information what has changed recently. 417 | 418 | ## Testing 419 | 420 | ``` bash 421 | $ composer test 422 | ``` 423 | 424 | ## Contributing 425 | 426 | Please see [CONTRIBUTING](CONTRIBUTING.md) and [CONDUCT](CONDUCT.md) for details. 427 | 428 | ## Security 429 | 430 | If you discover any security-related issues, please email tomasmajer@gmail.com instead of using the issue tracker. 431 | 432 | ## License 433 | 434 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 435 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tomaj/hermes", 3 | "description": "Simple php background processing library", 4 | "type": "library", 5 | "license": "MIT", 6 | "keywords": ["event", "background", "workers"], 7 | "homepage": "https://github.com/tomaj/hermes", 8 | "authors": [ 9 | { 10 | "name": "Tomas Majer", 11 | "email": "tomasmajer@gmail.com", 12 | "homepage": "http://www.tomaj.sk/" 13 | } 14 | ], 15 | "support": { 16 | "issues": "https://github.com/tomaj/hermes/issues", 17 | "source": "https://github.com/tomaj/hermes" 18 | }, 19 | "require": { 20 | "php": ">= 7.2.0", 21 | "ext-json": "*", 22 | "ramsey/uuid": "^3 || ^4", 23 | "psr/log": "^1 || ^2 || ^3", 24 | "tracy/tracy": "^2.0" 25 | }, 26 | "require-dev": { 27 | "ext-redis": "*", 28 | "predis/predis": "^1.1", 29 | "phpunit/phpunit": "^8 || ^9", 30 | "squizlabs/php_codesniffer": "~3.5", 31 | "php-amqplib/php-amqplib": "^2.6.3", 32 | "scrutinizer/ocular": "^1.6.0", 33 | "aws/aws-sdk-php": "3.*", 34 | "phpstan/phpstan": "^0.12.65", 35 | "pepakriz/phpstan-exception-rules": "^0.11.3" 36 | }, 37 | "suggest": { 38 | "monolog/monolog": "Basic logger implements psr/logger", 39 | "ext-redis": "Allow use for redis driver with native redis php extension", 40 | "predis/predis": "Allow use for redis driver with php package Predis", 41 | "aws/aws-sdk-php": "Allow use Amazon SQS driver", 42 | "php-amqplib/php-amqplib": "Allow using rabbimq as driver" 43 | }, 44 | "autoload": { 45 | "psr-4": { 46 | "Tomaj\\Hermes\\": "src" 47 | } 48 | }, 49 | "autoload-dev": { 50 | "psr-4": { 51 | "Tomaj\\Hermes\\Test\\": "tests" 52 | } 53 | }, 54 | "scripts": { 55 | "phpunit": "phpunit", 56 | "phpstan": "phpstan analyse", 57 | "phpcs": "phpcs --standard=PSR2 src tests examples -n" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /examples/predis/emitter.php: -------------------------------------------------------------------------------- 1 | 'tcp', 13 | 'host' => '127.0.0.1', 14 | 'port' => 6379, 15 | ]); 16 | $driver = new PredisSetDriver($redis); 17 | $driver->setupPriorityQueue('hermes_low', \Tomaj\Hermes\Dispatcher::DEFAULT_PRIORITY - 10); 18 | $driver->setupPriorityQueue('hermes_high', \Tomaj\Hermes\Dispatcher::DEFAULT_PRIORITY + 10); 19 | 20 | $emitter = new Emitter($driver); 21 | 22 | $counter = 1; 23 | $priorities = [\Tomaj\Hermes\Dispatcher::DEFAULT_PRIORITY, \Tomaj\Hermes\Dispatcher::DEFAULT_PRIORITY - 10, \Tomaj\Hermes\Dispatcher::DEFAULT_PRIORITY + 10]; 24 | while (true) { 25 | $emitter->emit(new Message('type1', ['message' => $counter]), $priorities[rand(0, count($priorities) - 1)]); 26 | echo "Emited message $counter\n"; 27 | $counter++; 28 | sleep(1); 29 | } 30 | -------------------------------------------------------------------------------- /examples/predis/processor.php: -------------------------------------------------------------------------------- 1 | 'tcp', 14 | 'host' => '127.0.0.1', 15 | 'port' => 6379, 16 | ]); 17 | $driver = new PredisSetDriver($redis, 'hermes', 1); 18 | $driver->setShutdown(new PredisShutdown($redis)); 19 | $driver->setupPriorityQueue('hermes_low', \Tomaj\Hermes\Dispatcher::DEFAULT_PRIORITY - 10); 20 | $driver->setupPriorityQueue('hermes_high', \Tomaj\Hermes\Dispatcher::DEFAULT_PRIORITY + 10); 21 | 22 | $dispatcher = new Dispatcher($driver); 23 | 24 | $dispatcher->registerHandler('type1', new EchoHandler()); 25 | 26 | $dispatcher->handle(); 27 | //$dispatcher->handle([Dispatcher::PRIORITY_HIGH]); 28 | -------------------------------------------------------------------------------- /examples/predis/shutdown.php: -------------------------------------------------------------------------------- 1 | 'tcp', 12 | 'host' => '127.0.0.1', 13 | 'port' => 6379, 14 | ]); 15 | $driver = new PredisSetDriver($redis); 16 | (new PredisShutdown($redis))->shutdown(); 17 | -------------------------------------------------------------------------------- /examples/rabbitmq/emitter.php: -------------------------------------------------------------------------------- 1 | emit(new Message('type1', ['message' => $counter])); 21 | echo "Emited message $counter\n"; 22 | $counter++; 23 | sleep(1); 24 | } 25 | -------------------------------------------------------------------------------- /examples/rabbitmq/processor.php: -------------------------------------------------------------------------------- 1 | registerHandler('type1', new EchoHandler()); 18 | 19 | $dispatcher->handle(); 20 | -------------------------------------------------------------------------------- /examples/redis/emitter.php: -------------------------------------------------------------------------------- 1 | connect('127.0.0.1', 6379); 12 | $driver = new RedisSetDriver($redis); 13 | $driver->setupPriorityQueue('hermes_low', \Tomaj\Hermes\Dispatcher::DEFAULT_PRIORITY - 10); 14 | $driver->setupPriorityQueue('hermes_high', \Tomaj\Hermes\Dispatcher::DEFAULT_PRIORITY + 10); 15 | 16 | $emitter = new Emitter($driver); 17 | 18 | $counter = 1; 19 | $priorities = [\Tomaj\Hermes\Dispatcher::DEFAULT_PRIORITY, \Tomaj\Hermes\Dispatcher::DEFAULT_PRIORITY - 10, \Tomaj\Hermes\Dispatcher::DEFAULT_PRIORITY + 10]; 20 | while (true) { 21 | $emitter->emit(new Message('type1', ['message' => $counter]), $priorities[rand(0, count($priorities) - 1)]); 22 | echo "Emited message $counter\n"; 23 | $counter++; 24 | sleep(1); 25 | } 26 | -------------------------------------------------------------------------------- /examples/redis/processor.php: -------------------------------------------------------------------------------- 1 | connect('127.0.0.1', 6379); 13 | $driver = new RedisSetDriver($redis, 'hermes', 1); 14 | $driver->setShutdown(new RedisShutdown($redis)); 15 | $driver->setupPriorityQueue('hermes_low', \Tomaj\Hermes\Dispatcher::DEFAULT_PRIORITY - 10); 16 | $driver->setupPriorityQueue('hermes_high', \Tomaj\Hermes\Dispatcher::DEFAULT_PRIORITY + 10); 17 | 18 | $dispatcher = new Dispatcher($driver); 19 | 20 | $dispatcher->registerHandler('type1', new EchoHandler()); 21 | 22 | $dispatcher->handle(); 23 | //$dispatcher->handle([Dispatcher::PRIORITY_HIGH]); 24 | -------------------------------------------------------------------------------- /examples/redis/shutdown.php: -------------------------------------------------------------------------------- 1 | connect('127.0.0.1', 6379); 10 | (new RedisShutdown($redis))->shutdown(); 11 | -------------------------------------------------------------------------------- /examples/sqs/emitter.php: -------------------------------------------------------------------------------- 1 | 'latest', 13 | 'region' => '*region*', 14 | 'credentials' => [ 15 | 'key' => '*key*', 16 | 'secret' => '*secret*', 17 | ] 18 | ]); 19 | 20 | $driver = new AmazonSqsDriver($client, '*queueName*'); 21 | $emitter = new Emitter($driver); 22 | $counter = 1; 23 | while (true) { 24 | $emitter->emit(new Message('type1', ['message' => $counter])); 25 | echo "Emited message $counter\n"; 26 | $counter++; 27 | sleep(1); 28 | } 29 | -------------------------------------------------------------------------------- /examples/sqs/processor.php: -------------------------------------------------------------------------------- 1 | 'latest', 13 | 'region' => '*region*', 14 | 'credentials' => [ 15 | 'key' => '*key*', 16 | 'secret' => '*secret*', 17 | ] 18 | ]); 19 | 20 | $driver = new AmazonSqsDriver($client, '*queueName*'); 21 | $dispatcher = new Dispatcher($driver); 22 | 23 | $dispatcher->registerHandler('type1', new EchoHandler()); 24 | 25 | $dispatcher->handle(); 26 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | includes: 2 | - vendor/pepakriz/phpstan-exception-rules/extension.neon 3 | 4 | parameters: 5 | level: max 6 | paths: 7 | - src 8 | - examples 9 | # - tests 10 | 11 | 12 | exceptionRules: 13 | reportUnusedCatchesOfUncheckedExceptions: true 14 | reportUnusedCheckedThrowsInSubtypes: false 15 | reportCheckedThrowsInGlobalScope: true 16 | checkedExceptions: 17 | - RuntimeException 18 | 19 | treatPhpDocTypesAsCertain: false 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 14 | 15 | 16 | tests 17 | 18 | 19 | 20 | 22 | 23 | src 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/Dispatcher.php: -------------------------------------------------------------------------------- 1 | driver = $driver; 63 | $this->logger = $logger; 64 | $this->shutdown = $shutdown; 65 | $this->startTime = new DateTime(); 66 | 67 | // check if driver use ShutdownTrait 68 | if ($shutdown && method_exists($this->driver, 'setShutdown')) { 69 | $this->driver->setShutdown($shutdown); 70 | } 71 | } 72 | 73 | /** 74 | * @param MessageInterface $message 75 | * @param int $priority 76 | * @return DispatcherInterface 77 | * @deprecated - use Emitter::emit method instead 78 | */ 79 | public function emit(MessageInterface $message, int $priority = self::DEFAULT_PRIORITY): DispatcherInterface 80 | { 81 | $this->driver->send($message, $priority); 82 | 83 | $this->log( 84 | LogLevel::INFO, 85 | "Dispatcher send message #{$message->getId()} with priority {$priority} to driver " . get_class($this->driver), 86 | $this->messageLoggerContext($message) 87 | ); 88 | return $this; 89 | } 90 | 91 | /** 92 | * Basic method for background job to star listening. 93 | * 94 | * This method hook to driver wait() method and start listening events. 95 | * Method is blocking, so when you call it all processing will stop. 96 | * WARNING! Don't use it on web server calls. Run it only with cli. 97 | * 98 | * @param int[] $priorities 99 | * 100 | * @return void 101 | */ 102 | public function handle(array $priorities = []): void 103 | { 104 | try { 105 | $this->driver->wait(function (MessageInterface $message, int $priority = Dispatcher::DEFAULT_PRIORITY) { 106 | $this->log( 107 | LogLevel::INFO, 108 | "Start handle message #{$message->getId()} ({$message->getType()}) priority:{$priority}", 109 | $this->messageLoggerContext($message) 110 | ); 111 | 112 | $result = $this->dispatch($message); 113 | 114 | if ($this->shutdown !== null && $this->shutdown->shouldShutdown($this->startTime)) { 115 | throw new ShutdownException('Shutdown'); 116 | } 117 | 118 | return $result; 119 | }, $priorities); 120 | } catch (ShutdownException $e) { 121 | $this->log(LogLevel::NOTICE, 'Exiting hermes dispatcher - shutdown'); 122 | } catch (Exception $exception) { 123 | if (Debugger::isEnabled()) { 124 | Debugger::log($exception, Debugger::EXCEPTION); 125 | } 126 | } 127 | } 128 | 129 | /** 130 | * Dispatch message 131 | * 132 | * @param MessageInterface $message 133 | * 134 | * @return bool 135 | */ 136 | private function dispatch(MessageInterface $message): bool 137 | { 138 | $type = $message->getType(); 139 | 140 | if (!$this->hasHandlers($type)) { 141 | return true; 142 | } 143 | 144 | $result = true; 145 | 146 | foreach ($this->handlers[$type] as $handler) { 147 | $handlerResult = $this->handleMessage($handler, $message); 148 | 149 | if ($result && !$handlerResult) { 150 | $result = false; 151 | } 152 | } 153 | 154 | return $result; 155 | } 156 | 157 | /** 158 | * Handle given message with given handler 159 | * 160 | * @param HandlerInterface $handler 161 | * @param MessageInterface $message 162 | * 163 | * @return bool 164 | */ 165 | private function handleMessage(HandlerInterface $handler, MessageInterface $message): bool 166 | { 167 | // check if handler implements Psr\Log\LoggerAwareInterface (you can use \Psr\Log\LoggerAwareTrait) 168 | if ($this->logger !== null && method_exists($handler, 'setLogger')) { 169 | $handler->setLogger($this->logger); 170 | } 171 | 172 | try { 173 | $result = $handler->handle($message); 174 | 175 | $this->log( 176 | LogLevel::INFO, 177 | "End handle message #{$message->getId()} ({$message->getType()})", 178 | $this->messageLoggerContext($message) 179 | ); 180 | } catch (Exception $e) { 181 | $this->log( 182 | LogLevel::ERROR, 183 | "Handler " . get_class($handler) . " throws exception - {$e->getMessage()}", 184 | ['error' => $e, 'message' => $this->messageLoggerContext($message), 'exception' => $e] 185 | ); 186 | if (Debugger::isEnabled()) { 187 | Debugger::log($e, Debugger::EXCEPTION); 188 | } 189 | 190 | $this->retryMessage($message, $handler); 191 | 192 | $result = false; 193 | } 194 | return $result; 195 | } 196 | 197 | /** 198 | * Helper function for sending retrying message back to driver 199 | * 200 | * @param MessageInterface $message 201 | * @param HandlerInterface $handler 202 | */ 203 | private function retryMessage(MessageInterface $message, HandlerInterface $handler): void 204 | { 205 | if (method_exists($handler, 'canRetry') && method_exists($handler, 'maxRetry')) { 206 | if ($message->getRetries() < $handler->maxRetry()) { 207 | $executeAt = $this->nextRetry($message); 208 | $newMessage = new Message($message->getType(), $message->getPayload(), $message->getId(), $message->getCreated(), $executeAt, $message->getRetries() + 1); 209 | $this->driver->send($newMessage); 210 | } 211 | } 212 | } 213 | 214 | /** 215 | * Calculate next retry 216 | * 217 | * Inspired by ruby sidekiq (https://github.com/mperham/sidekiq/wiki/Error-Handling#automatic-job-retry) 218 | * 219 | * @param MessageInterface $message 220 | * @return float 221 | */ 222 | private function nextRetry(MessageInterface $message): float 223 | { 224 | return microtime(true) + pow($message->getRetries(), 4) + 15 + (rand(1, 30) * ($message->getRetries() + 1)); 225 | } 226 | 227 | /** 228 | * Check if actual dispatcher has handler for given type 229 | * 230 | * @param string $type 231 | * 232 | * @return bool 233 | */ 234 | private function hasHandlers(string $type): bool 235 | { 236 | return isset($this->handlers[$type]) && count($this->handlers[$type]) > 0; 237 | } 238 | 239 | /** 240 | * {@inheritdoc} 241 | */ 242 | public function registerHandler(string $type, HandlerInterface $handler): DispatcherInterface 243 | { 244 | if (!isset($this->handlers[$type])) { 245 | $this->handlers[$type] = []; 246 | } 247 | 248 | $this->handlers[$type][] = $handler; 249 | return $this; 250 | } 251 | 252 | /** 253 | * {@inheritdoc} 254 | */ 255 | public function registerHandlers(string $type, array $handlers): DispatcherInterface 256 | { 257 | foreach ($handlers as $handler) { 258 | $this->registerHandler($type, $handler); 259 | } 260 | return $this; 261 | } 262 | 263 | /** 264 | * {@inheritdoc} 265 | */ 266 | public function unregisterAllHandlers(): DispatcherInterface 267 | { 268 | $this->handlers = []; 269 | return $this; 270 | } 271 | 272 | /** 273 | * {@inheritdoc} 274 | */ 275 | public function unregisterHandler(string $type, HandlerInterface $handler): DispatcherInterface 276 | { 277 | if (!isset($this->handlers[$type])) { 278 | return $this; 279 | } 280 | $this->handlers[$type] = array_filter( 281 | $this->handlers[$type], 282 | function (HandlerInterface $registeredHandler) use ($handler) { 283 | return $registeredHandler !== $handler; 284 | } 285 | ); 286 | 287 | return $this; 288 | } 289 | 290 | /** 291 | * Serialize message to logger context 292 | * 293 | * @param MessageInterface $message 294 | * 295 | * @return array 296 | */ 297 | private function messageLoggerContext(MessageInterface $message): array 298 | { 299 | return [ 300 | 'id' => $message->getId(), 301 | 'created' => $message->getCreated(), 302 | 'type' => $message->getType(), 303 | 'payload' => $message->getPayload(), 304 | 'retries' => $message->getRetries(), 305 | 'execute_at' => $message->getExecuteAt(), 306 | ]; 307 | } 308 | 309 | /** 310 | * Interal log method wrapper 311 | * 312 | * @param mixed $level 313 | * @param string $message 314 | * @param array $context 315 | * 316 | * @return void 317 | */ 318 | private function log($level, string $message, array $context = []): void 319 | { 320 | if ($this->logger !== null) { 321 | $this->logger->log($level, $message, $context); 322 | } 323 | } 324 | } 325 | -------------------------------------------------------------------------------- /src/DispatcherInterface.php: -------------------------------------------------------------------------------- 1 | 45 | * use Aws\Sqs\SqsClient; 46 | * 47 | * $client = SqsClient::factory(array( 48 | * 'profile' => '', 49 | * 'region' => '' 50 | * )); 51 | * 52 | * 53 | * or 54 | * 55 | * 56 | * use Aws\Common\Aws; 57 | * 58 | * // Create a service builder using a configuration file 59 | * $aws = Aws::factory('/path/to/my_config.json'); 60 | * 61 | * // Get the client from the builder by namespace 62 | * $client = $aws->get('Sqs'); 63 | * 64 | * 65 | * More examples see: https://docs.aws.amazon.com/aws-sdk-php/v2/guide/service-sqs.html 66 | * 67 | * 68 | * @see examples/sqs folder 69 | * 70 | * @param SqsClient $client 71 | * @param string $queueName 72 | * @param array $queueAttributes 73 | */ 74 | public function __construct(SqsClient $client, string $queueName, array $queueAttributes = []) 75 | { 76 | $this->client = $client; 77 | $this->queueName = $queueName; 78 | $this->serializer = new MessageSerializer(); 79 | 80 | $result = $client->createQueue([ 81 | 'QueueName' => $queueName, 82 | 'Attributes' => $queueAttributes, 83 | ]); 84 | 85 | $this->queueUrl = $result->get('QueueUrl'); 86 | } 87 | 88 | /** 89 | * {@inheritdoc} 90 | */ 91 | public function send(MessageInterface $message, int $priority = Dispatcher::DEFAULT_PRIORITY): bool 92 | { 93 | $this->client->sendMessage([ 94 | 'QueueUrl' => $this->queueUrl, 95 | 'MessageBody' => $this->serializer->serialize($message), 96 | ]); 97 | return true; 98 | } 99 | 100 | /** 101 | * @param string $name 102 | * @param int $priority 103 | * 104 | * @throws NotSupportedException 105 | */ 106 | public function setupPriorityQueue(string $name, int $priority): void 107 | { 108 | throw new NotSupportedException("AmazonSQS is not supporting priority queues now"); 109 | } 110 | 111 | /** 112 | * {@inheritdoc} 113 | */ 114 | public function wait(Closure $callback, array $priorities = []): void 115 | { 116 | while (true) { 117 | $this->checkShutdown(); 118 | if (!$this->shouldProcessNext()) { 119 | break; 120 | } 121 | 122 | $result = $this->client->receiveMessage([ 123 | 'QueueUrl' => $this->queueUrl, 124 | 'WaitTimeSeconds' => 1, 125 | ]); 126 | 127 | $messages = $result['Messages']; 128 | 129 | if ($messages) { 130 | $hermesMessages = []; 131 | foreach ($messages as $message) { 132 | $this->client->deleteMessage([ 133 | 'QueueUrl' => $this->queueUrl, 134 | 'ReceiptHandle' => $message['ReceiptHandle'], 135 | ]); 136 | $hermesMessages[] = $this->serializer->unserialize($message['Body']); 137 | } 138 | foreach ($hermesMessages as $hermesMessage) { 139 | $callback($hermesMessage); 140 | $this->incrementProcessedItems(); 141 | } 142 | } else { 143 | if ($this->sleepInterval) { 144 | $this->checkShutdown(); 145 | sleep($this->sleepInterval); 146 | } 147 | } 148 | } 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/Driver/DriverInterface.php: -------------------------------------------------------------------------------- 1 | */ 31 | private $amqpMessageProperties = []; 32 | 33 | /** @var integer */ 34 | private $refreshInterval; 35 | 36 | /** @var string */ 37 | private $consumerTag; 38 | 39 | /** 40 | * @param AMQPLazyConnection $connection 41 | * @param string $queue 42 | * @param array $amqpMessageProperties 43 | * @param int $refreshInterval 44 | * @param string $consumerTag 45 | */ 46 | public function __construct(AMQPLazyConnection $connection, string $queue, array $amqpMessageProperties = [], int $refreshInterval = 0, string $consumerTag = 'hermes') 47 | { 48 | $this->connection = $connection; 49 | $this->queue = $queue; 50 | $this->amqpMessageProperties = $amqpMessageProperties; 51 | $this->refreshInterval = $refreshInterval; 52 | $this->consumerTag = $consumerTag; 53 | $this->serializer = new MessageSerializer(); 54 | } 55 | 56 | /** 57 | * {@inheritdoc} 58 | * 59 | * @throws \PhpAmqpLib\Exception\AMQPChannelClosedException 60 | * @throws \PhpAmqpLib\Exception\AMQPConnectionBlockedException 61 | * @throws \PhpAmqpLib\Exception\AMQPConnectionClosedException 62 | * @throws \PhpAmqpLib\Exception\AMQPTimeoutException 63 | */ 64 | public function send(MessageInterface $message, int $priority = Dispatcher::DEFAULT_PRIORITY): bool 65 | { 66 | $rabbitMessage = new AMQPMessage($this->serializer->serialize($message), $this->amqpMessageProperties); 67 | $this->getChannel()->basic_publish($rabbitMessage, '', $this->queue); 68 | return true; 69 | } 70 | 71 | /** 72 | * @param string $name 73 | * @param int $priority 74 | * 75 | * @throws NotSupportedException 76 | */ 77 | public function setupPriorityQueue(string $name, int $priority): void 78 | { 79 | throw new NotSupportedException("LazyRabbitMqDriver is not supporting priority queues now"); 80 | } 81 | 82 | /** 83 | * {@inheritdoc} 84 | * 85 | * @throws ShutdownException 86 | * @throws \PhpAmqpLib\Exception\AMQPOutOfBoundsException 87 | * @throws \PhpAmqpLib\Exception\AMQPRuntimeException 88 | * @throws \PhpAmqpLib\Exception\AMQPTimeoutException 89 | * @throws \ErrorException 90 | */ 91 | public function wait(Closure $callback, array $priorities = []): void 92 | { 93 | while (true) { 94 | $this->getChannel()->basic_consume( 95 | $this->queue, 96 | $this->consumerTag, 97 | false, 98 | true, 99 | false, 100 | false, 101 | function ($rabbitMessage) use ($callback) { 102 | $message = $this->serializer->unserialize($rabbitMessage->body); 103 | $callback($message); 104 | } 105 | ); 106 | 107 | while (count($this->getChannel()->callbacks)) { 108 | $this->getChannel()->wait(null, true); 109 | $this->checkShutdown(); 110 | if (!$this->shouldProcessNext()) { 111 | break 2; 112 | } 113 | if ($this->refreshInterval) { 114 | sleep($this->refreshInterval); 115 | } 116 | } 117 | } 118 | 119 | $this->getChannel()->close(); 120 | $this->connection->close(); 121 | } 122 | 123 | /** 124 | * @throws \PhpAmqpLib\Exception\AMQPTimeoutException 125 | * @return AMQPChannel 126 | */ 127 | private function getChannel(): AMQPChannel 128 | { 129 | if ($this->channel !== null) { 130 | return $this->channel; 131 | } 132 | $this->channel = $this->connection->channel(); 133 | $this->channel->queue_declare($this->queue, false, false, false, false); 134 | return $this->channel; 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/Driver/MaxItemsTrait.php: -------------------------------------------------------------------------------- 1 | maxProcessItems = $count; 21 | } 22 | 23 | public function incrementProcessedItems(): int 24 | { 25 | $this->processed++; 26 | return $this->processed; 27 | } 28 | 29 | public function processed(): int 30 | { 31 | return $this->processed; 32 | } 33 | 34 | public function shouldProcessNext(): bool 35 | { 36 | if ($this->maxProcessItems == 0) { 37 | return true; 38 | } 39 | if ($this->processed >= $this->maxProcessItems) { 40 | return false; 41 | } 42 | return true; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Driver/NotSupportedException.php: -------------------------------------------------------------------------------- 1 | */ 21 | private $queues = []; 22 | 23 | /** 24 | * @var string 25 | */ 26 | private $scheduleKey; 27 | 28 | /** 29 | * @var Client 30 | */ 31 | private $redis; 32 | 33 | /** 34 | * @var integer 35 | */ 36 | private $refreshInterval; 37 | 38 | /** 39 | * Create new PredisSetDriver 40 | * 41 | * This driver is using redis set. With send message it add new item to set 42 | * and in wait() command it is reading new items in this set. 43 | * This driver doesn't use redis pubsub functionality, only redis sets. 44 | * 45 | * Managing connection to redis is up to you and you have to create it outsite 46 | * of this class. You will need to install predis php package. 47 | * 48 | * @param Client $redis 49 | * @param string $key 50 | * @param integer $refreshInterval 51 | * @param string $scheduleKey 52 | * @see examples/redis 53 | * 54 | * @throws NotSupportedException 55 | */ 56 | public function __construct(Client $redis, string $key = 'hermes', int $refreshInterval = 1, string $scheduleKey = 'hermes_schedule') 57 | { 58 | $this->setupPriorityQueue($key, Dispatcher::DEFAULT_PRIORITY); 59 | 60 | $this->scheduleKey = $scheduleKey; 61 | $this->redis = $redis; 62 | $this->refreshInterval = $refreshInterval; 63 | $this->serializer = new MessageSerializer(); 64 | } 65 | 66 | /** 67 | * {@inheritdoc} 68 | * 69 | * @throws UnknownPriorityException 70 | * @throws SerializeException 71 | */ 72 | public function send(MessageInterface $message, int $priority = Dispatcher::DEFAULT_PRIORITY): bool 73 | { 74 | if ($message->getExecuteAt() && $message->getExecuteAt() > microtime(true)) { 75 | $this->redis->zadd($this->scheduleKey, [$this->serializer->serialize($message) => $message->getExecuteAt()]); 76 | } else { 77 | $key = $this->getKey($priority); 78 | $this->redis->sadd($key, [$this->serializer->serialize($message)]); 79 | } 80 | return true; 81 | } 82 | 83 | /** 84 | * {@inheritdoc} 85 | */ 86 | public function setupPriorityQueue(string $name, int $priority): void 87 | { 88 | $this->queues[$priority] = $name; 89 | } 90 | 91 | /** 92 | * @param int $priority 93 | * @return string 94 | * 95 | * @throws UnknownPriorityException 96 | */ 97 | private function getKey(int $priority): string 98 | { 99 | if (!isset($this->queues[$priority])) { 100 | throw new UnknownPriorityException("Unknown priority {$priority}"); 101 | } 102 | return $this->queues[$priority]; 103 | } 104 | 105 | /**s 106 | * {@inheritdoc} 107 | * 108 | * @throws ShutdownException 109 | * @throws UnknownPriorityException 110 | * @throws SerializeException 111 | */ 112 | public function wait(Closure $callback, array $priorities = []): void 113 | { 114 | $queues = array_reverse($this->queues, true); 115 | while (true) { 116 | $this->checkShutdown(); 117 | if (!$this->shouldProcessNext()) { 118 | break; 119 | } 120 | 121 | // check schedule 122 | $messagesString = $this->redis->zrangebyscore($this->scheduleKey, '-inf', microtime(true), ['LIMIT' => ['OFFSET' => 0, 'COUNT' => 1]]); 123 | if (count($messagesString)) { 124 | foreach ($messagesString as $messageString) { 125 | $this->redis->zrem($this->scheduleKey, $messageString); 126 | $this->send($this->serializer->unserialize($messageString)); 127 | } 128 | } 129 | 130 | $messageString = null; 131 | $foundPriority = null; 132 | 133 | foreach ($queues as $priority => $name) { 134 | if (count($priorities) > 0 && !in_array($priority, $priorities)) { 135 | continue; 136 | } 137 | if ($messageString !== null) { 138 | break; 139 | } 140 | 141 | $key = $this->getKey($priority); 142 | 143 | $messageString = $this->redis->spop($key); 144 | $foundPriority = $priority; 145 | } 146 | 147 | if ($messageString !== null) { 148 | $message = $this->serializer->unserialize($messageString); 149 | $callback($message, $foundPriority); 150 | $this->incrementProcessedItems(); 151 | } else { 152 | if ($this->refreshInterval) { 153 | $this->checkShutdown(); 154 | sleep($this->refreshInterval); 155 | } 156 | } 157 | } 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/Driver/RedisSetDriver.php: -------------------------------------------------------------------------------- 1 | */ 21 | private $queues = []; 22 | 23 | /** 24 | * @var string 25 | */ 26 | private $scheduleKey; 27 | 28 | /** 29 | * @var Redis 30 | */ 31 | private $redis; 32 | 33 | /** 34 | * @var integer 35 | */ 36 | private $refreshInterval; 37 | 38 | /** 39 | * Create new RedisSetDriver 40 | * 41 | * This driver is using redis set. With send message it add new item to set 42 | * and in wait() command it is reading new items in this set. 43 | * This driver doesn't use redis pubsub functionality, only redis sets. 44 | * 45 | * Managing connection to redis is up to you and you have to create it outsite 46 | * of this class. You have to have enabled native Redis php extension. 47 | * 48 | * @see examples/redis 49 | * 50 | * @param Redis $redis 51 | * @param string $key 52 | * @param integer $refreshInterval 53 | * @param string $scheduleKey 54 | */ 55 | public function __construct(Redis $redis, string $key = 'hermes', int $refreshInterval = 1, string $scheduleKey = 'hermes_schedule') 56 | { 57 | $this->setupPriorityQueue($key, Dispatcher::DEFAULT_PRIORITY); 58 | 59 | $this->scheduleKey = $scheduleKey; 60 | $this->redis = $redis; 61 | $this->refreshInterval = $refreshInterval; 62 | $this->serializer = new MessageSerializer(); 63 | } 64 | 65 | /** 66 | * {@inheritdoc} 67 | * 68 | * @throws UnknownPriorityException 69 | */ 70 | public function send(MessageInterface $message, int $priority = Dispatcher::DEFAULT_PRIORITY): bool 71 | { 72 | if ($message->getExecuteAt() !== null && $message->getExecuteAt() > microtime(true)) { 73 | $this->redis->zAdd($this->scheduleKey, $message->getExecuteAt(), $this->serializer->serialize($message)); 74 | } else { 75 | $key = $this->getKey($priority); 76 | $this->redis->sAdd($key, $this->serializer->serialize($message)); 77 | } 78 | return true; 79 | } 80 | 81 | /** 82 | * {@inheritdoc} 83 | */ 84 | public function setupPriorityQueue(string $name, int $priority): void 85 | { 86 | $this->queues[$priority] = $name; 87 | } 88 | 89 | /** 90 | * @param int $priority 91 | * @return string 92 | * 93 | * @throws UnknownPriorityException 94 | */ 95 | private function getKey(int $priority): string 96 | { 97 | if (!isset($this->queues[$priority])) { 98 | throw new UnknownPriorityException("Unknown priority {$priority}"); 99 | } 100 | return $this->queues[$priority]; 101 | } 102 | 103 | /** 104 | * {@inheritdoc} 105 | * 106 | * @throws ShutdownException 107 | * @throws UnknownPriorityException 108 | * @throws SerializeException 109 | */ 110 | public function wait(Closure $callback, array $priorities = []): void 111 | { 112 | $queues = array_reverse($this->queues, true); 113 | while (true) { 114 | $this->checkShutdown(); 115 | if (!$this->shouldProcessNext()) { 116 | break; 117 | } 118 | 119 | // check schedule 120 | $microTime = microtime(true); 121 | $messageStrings = $this->redis->zRangeByScore($this->scheduleKey, '-inf', (string) $microTime, ['limit' => [0, 1]]); 122 | for ($i = 1; $i <= count($messageStrings); $i++) { 123 | $messageString = $this->pop($this->scheduleKey); 124 | if (!$messageString) { 125 | break; 126 | } 127 | $scheduledMessage = $this->serializer->unserialize($messageString); 128 | $this->send($scheduledMessage); 129 | 130 | if ($scheduledMessage->getExecuteAt() > $microTime) { 131 | break; 132 | } 133 | } 134 | 135 | $messageString = null; 136 | $foundPriority = null; 137 | 138 | foreach ($queues as $priority => $name) { 139 | if (count($priorities) > 0 && !in_array($priority, $priorities)) { 140 | continue; 141 | } 142 | if ($messageString !== null) { 143 | break; 144 | } 145 | 146 | $messageString = $this->pop($this->getKey($priority)); 147 | $foundPriority = $priority; 148 | } 149 | 150 | if ($messageString !== null) { 151 | $message = $this->serializer->unserialize($messageString); 152 | $callback($message, $foundPriority); 153 | $this->incrementProcessedItems(); 154 | } else { 155 | if ($this->refreshInterval) { 156 | $this->checkShutdown(); 157 | sleep($this->refreshInterval); 158 | } 159 | } 160 | } 161 | } 162 | 163 | private function pop(string $key): ?string 164 | { 165 | $messageString = $this->redis->sPop($key); 166 | if (is_string($messageString) && $messageString !== "") { 167 | return $messageString; 168 | } 169 | 170 | return null; 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /src/Driver/SerializerAwareTrait.php: -------------------------------------------------------------------------------- 1 | serializer = $serializer; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Driver/ShutdownTrait.php: -------------------------------------------------------------------------------- 1 | shutdown = $shutdown; 21 | $this->startTime = new DateTime(); 22 | } 23 | 24 | private function shouldShutdown(): bool 25 | { 26 | return $this->shutdown !== null && $this->shutdown->shouldShutdown($this->startTime); 27 | } 28 | 29 | /** 30 | * @throws ShutdownException 31 | */ 32 | private function checkShutdown(): void 33 | { 34 | if ($this->shouldShutdown()) { 35 | throw new ShutdownException(); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Driver/UnknownPriorityException.php: -------------------------------------------------------------------------------- 1 | driver = $driver; 35 | $this->logger = $logger; 36 | } 37 | 38 | /** 39 | * {@inheritdoc} 40 | * 41 | * @throws Driver\UnknownPriorityException 42 | */ 43 | public function emit(MessageInterface $message, int $priority = Dispatcher::DEFAULT_PRIORITY): EmitterInterface 44 | { 45 | $this->driver->send($message, $priority); 46 | 47 | $this->log( 48 | LogLevel::INFO, 49 | "Dispatcher send message #{$message->getId()} to driver " . get_class($this->driver), 50 | $this->messageLoggerContext($message) 51 | ); 52 | return $this; 53 | } 54 | 55 | /** 56 | * Serialize message to logger context 57 | * 58 | * @param MessageInterface $message 59 | * 60 | * @return array 61 | */ 62 | private function messageLoggerContext(MessageInterface $message): array 63 | { 64 | return [ 65 | 'id' => $message->getId(), 66 | 'created' => $message->getCreated(), 67 | 'type' => $message->getType(), 68 | 'payload' => $message->getPayload(), 69 | ]; 70 | } 71 | 72 | /** 73 | * Internal log method wrapper 74 | * 75 | * @param mixed $level 76 | * @param string $message 77 | * @param array $context 78 | * 79 | * @return void 80 | */ 81 | private function log($level, string $message, array $context = []): void 82 | { 83 | if ($this->logger !== null) { 84 | $this->logger->log($level, $message, $context); 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/EmitterInterface.php: -------------------------------------------------------------------------------- 1 | getId()} (type {$message->getType()})\n"; 18 | $payload = json_encode($message->getPayload()); 19 | echo "Payload: {$payload}\n"; 20 | return true; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Handler/HandlerInterface.php: -------------------------------------------------------------------------------- 1 | |null 17 | */ 18 | private $payload; 19 | 20 | /** 21 | * @var string 22 | */ 23 | private $messageId; 24 | 25 | /** 26 | * @var float 27 | */ 28 | private $created; 29 | 30 | /** 31 | * @var float|null 32 | */ 33 | private $executeAt; 34 | 35 | /** 36 | * @var int 37 | */ 38 | private $retries; 39 | 40 | /** 41 | * Native implementation of message. 42 | * 43 | * @param string $type 44 | * @param array|null $payload 45 | * @param string|null $messageId 46 | * @param float|null $created timestamp (microtime(true)) 47 | * @param float|null $executeAt timestamp (microtime(true)) 48 | * @param int $retries 49 | */ 50 | public function __construct(string $type, ?array $payload = null, ?string $messageId = null, ?float $created = null, ?float $executeAt = null, int $retries = 0) 51 | { 52 | if ($messageId === null || $messageId == "") { 53 | $messageId = Uuid::uuid4()->toString(); 54 | } 55 | $this->messageId = $messageId; 56 | 57 | if (!$created) { 58 | $created = microtime(true); 59 | } 60 | $this->created = $created; 61 | 62 | $this->type = $type; 63 | $this->payload = $payload; 64 | $this->executeAt = $executeAt; 65 | $this->retries = $retries; 66 | } 67 | 68 | /** 69 | * {@inheritdoc} 70 | */ 71 | public function getId(): string 72 | { 73 | return $this->messageId; 74 | } 75 | 76 | /** 77 | * {@inheritdoc} 78 | */ 79 | public function getCreated(): float 80 | { 81 | return $this->created; 82 | } 83 | 84 | /** 85 | * {@inheritdoc} 86 | */ 87 | public function getExecuteAt(): ?float 88 | { 89 | return $this->executeAt; 90 | } 91 | 92 | /** 93 | * {@inheritdoc} 94 | */ 95 | public function getType(): string 96 | { 97 | return $this->type; 98 | } 99 | 100 | /** 101 | * {@inheritdoc} 102 | */ 103 | public function getPayload(): ?array 104 | { 105 | return $this->payload; 106 | } 107 | 108 | /** 109 | * {@inheritdoc} 110 | */ 111 | public function getRetries(): int 112 | { 113 | return $this->retries; 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/MessageInterface.php: -------------------------------------------------------------------------------- 1 | 51 | */ 52 | public function getPayload(): ?array; 53 | 54 | /** 55 | * Total retries for message 56 | * 57 | * @return int 58 | */ 59 | public function getRetries(): int; 60 | } 61 | -------------------------------------------------------------------------------- /src/MessageSerializer.php: -------------------------------------------------------------------------------- 1 | [ 15 | 'id' => $message->getId(), 16 | 'type' => $message->getType(), 17 | 'created' => $message->getCreated(), 18 | 'payload' => $message->getPayload(), 19 | 'execute_at' => $message->getExecuteAt(), 20 | 'retries' => $message->getRetries(), 21 | ] 22 | ], JSON_INVALID_UTF8_IGNORE); 23 | if ($result === false) { 24 | throw new SerializeException("Cannot serialize message {$message->getId()}"); 25 | } 26 | return $result; 27 | } 28 | 29 | /** 30 | * {@inheritdoc} 31 | */ 32 | public function unserialize(string $string): MessageInterface 33 | { 34 | $data = json_decode($string, true); 35 | if ($data === null || $data === false) { 36 | throw new SerializeException("Cannot unserialize message from '{$string}'"); 37 | } 38 | $message = $data['message']; 39 | $executeAt = null; 40 | if (isset($message['execute_at'])) { 41 | $executeAt = floatval($message['execute_at']); 42 | } 43 | $retries = 0; 44 | if (isset($message['retries'])) { 45 | $retries = intval($message['retries']); 46 | } 47 | return new Message($message['type'], $message['payload'], $message['id'], $message['created'], $executeAt, $retries); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/SerializeException.php: -------------------------------------------------------------------------------- 1 | redis = $redis; 25 | $this->key = $key; 26 | } 27 | 28 | /** 29 | * {@inheritdoc} 30 | * 31 | * Returns true: 32 | * 33 | * - if shutdown timestamp is set, 34 | * - and timestamp is not in future, 35 | * - and hermes was started ($startTime) before timestamp 36 | */ 37 | public function shouldShutdown(DateTime $startTime): bool 38 | { 39 | // load UNIX timestamp from redis 40 | $shutdownTime = $this->redis->get($this->key); 41 | if ($shutdownTime === null) { 42 | return false; 43 | } 44 | $shutdownTime = (int) $shutdownTime; 45 | 46 | // do not shutdown if shutdown time is in future 47 | if ($shutdownTime > time()) { 48 | return false; 49 | } 50 | 51 | // do not shutdown if hermes started after shutdown time 52 | if ($shutdownTime < $startTime->getTimestamp()) { 53 | return false; 54 | } 55 | 56 | return true; 57 | } 58 | 59 | /** 60 | * {@inheritdoc} 61 | * 62 | * Sets to Redis value `$shutdownTime` (or current DateTime) to `$key` defined in constructor. 63 | */ 64 | public function shutdown(?DateTime $shutdownTime = null): bool 65 | { 66 | if ($shutdownTime === null) { 67 | $shutdownTime = new DateTime(); 68 | } 69 | 70 | /** @var \Predis\Response\Status $response */ 71 | $response = $this->redis->set($this->key, $shutdownTime->format('U')); 72 | 73 | return $response->getPayload() === 'OK'; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Shutdown/RedisShutdown.php: -------------------------------------------------------------------------------- 1 | redis = $redis; 25 | $this->key = $key; 26 | } 27 | 28 | /** 29 | * {@inheritdoc} 30 | * 31 | * Returns true: 32 | * - if shutdown timestamp is set, 33 | * - and timestamp is not in future, 34 | * - and hermes was started ($startTime) before timestamp 35 | */ 36 | public function shouldShutdown(DateTime $startTime): bool 37 | { 38 | // load UNIX timestamp from redis 39 | $shutdownTime = $this->redis->get($this->key); 40 | if ($shutdownTime == null) { 41 | return false; 42 | } 43 | $shutdownTime = (int) $shutdownTime; 44 | 45 | // do not shutdown if shutdown time is in future 46 | if ($shutdownTime > time()) { 47 | return false; 48 | } 49 | 50 | // do not shutdown if hermes started after shutdown time 51 | if ($shutdownTime < $startTime->getTimestamp()) { 52 | return false; 53 | } 54 | 55 | return true; 56 | } 57 | 58 | /** 59 | * {@inheritdoc} 60 | * 61 | * Sets to Redis value `$shutdownTime` (or current DateTime) to `$key` defined in constructor. 62 | */ 63 | public function shutdown(?DateTime $shutdownTime = null): bool 64 | { 65 | if ($shutdownTime === null) { 66 | $shutdownTime = new DateTime(); 67 | } 68 | 69 | return $this->redis->set($this->key, $shutdownTime->format('U')); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Shutdown/SharedFileShutdown.php: -------------------------------------------------------------------------------- 1 | filePath = $filePath; 16 | } 17 | 18 | /** 19 | * {@inheritdoc} 20 | */ 21 | public function shouldShutdown(DateTime $startTime): bool 22 | { 23 | clearstatcache(false, $this->filePath); 24 | 25 | if (!file_exists($this->filePath)) { 26 | return false; 27 | } 28 | 29 | $time = filemtime($this->filePath); 30 | if ($time >= $startTime->getTimestamp()) { 31 | return true; 32 | } 33 | 34 | return false; 35 | } 36 | 37 | /** 38 | * {@inheritdoc} 39 | * 40 | * Creates file defined in constructor with modification time `$shutdownTime` (or current DateTime). 41 | */ 42 | public function shutdown(?DateTime $shutdownTime = null): bool 43 | { 44 | if ($shutdownTime === null) { 45 | $shutdownTime = new DateTime(); 46 | } 47 | 48 | return touch($this->filePath, (int) $shutdownTime->format('U')); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Shutdown/ShutdownException.php: -------------------------------------------------------------------------------- 1 |