├── .github └── workflows │ └── ci.yml ├── JsonSerializer.php ├── LICENSE ├── LuaScripts.php ├── PRedis.php ├── PhpRedis.php ├── README.md ├── Redis.php ├── RedisConnectionFactory.php ├── RedisConsumer.php ├── RedisConsumerHelperTrait.php ├── RedisContext.php ├── RedisDestination.php ├── RedisMessage.php ├── RedisProducer.php ├── RedisResult.php ├── RedisSubscriptionConsumer.php ├── Serializer.php ├── SerializerAwareTrait.php ├── ServerException.php └── composer.json /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | pull_request: 4 | push: 5 | branches: 6 | - master 7 | jobs: 8 | tests: 9 | runs-on: ubuntu-latest 10 | strategy: 11 | fail-fast: false 12 | matrix: 13 | php: ['7.4', '8.0', '8.1', '8.2'] 14 | 15 | name: PHP ${{ matrix.php }} tests 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | 20 | - uses: shivammathur/setup-php@v2 21 | with: 22 | php-version: ${{ matrix.php }} 23 | coverage: none 24 | extensions: redis 25 | 26 | - uses: "ramsey/composer-install@v1" 27 | with: 28 | composer-options: "--prefer-source" 29 | 30 | - run: vendor/bin/phpunit --exclude-group=functional 31 | -------------------------------------------------------------------------------- /JsonSerializer.php: -------------------------------------------------------------------------------- 1 | $message->getBody(), 13 | 'properties' => $message->getProperties(), 14 | 'headers' => $message->getHeaders(), 15 | ]); 16 | 17 | if (\JSON_ERROR_NONE !== json_last_error()) { 18 | throw new \InvalidArgumentException(sprintf('The malformed json given. Error %s and message %s', json_last_error(), json_last_error_msg())); 19 | } 20 | 21 | return $json; 22 | } 23 | 24 | public function toMessage(string $string): RedisMessage 25 | { 26 | $data = json_decode($string, true); 27 | if (\JSON_ERROR_NONE !== json_last_error()) { 28 | throw new \InvalidArgumentException(sprintf('The malformed json given. Error %s and message %s', json_last_error(), json_last_error_msg())); 29 | } 30 | 31 | return new RedisMessage($data['body'], $data['properties'], $data['headers']); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2017 Forma-Pro 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is furnished 9 | to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /LuaScripts.php: -------------------------------------------------------------------------------- 1 | options = $config['predis_options']; 38 | 39 | $this->parameters = [ 40 | 'scheme' => $config['scheme'], 41 | 'host' => $config['host'], 42 | 'port' => $config['port'], 43 | 'password' => $config['password'], 44 | 'database' => $config['database'], 45 | 'path' => $config['path'], 46 | 'async' => $config['async'], 47 | 'persistent' => $config['persistent'], 48 | 'timeout' => $config['timeout'], 49 | 'read_write_timeout' => $config['read_write_timeout'], 50 | ]; 51 | 52 | if ($config['ssl']) { 53 | $this->parameters['ssl'] = $config['ssl']; 54 | } 55 | } 56 | 57 | public function eval(string $script, array $keys = [], array $args = []) 58 | { 59 | try { 60 | // mixed eval($script, $numkeys, $keyOrArg1 = null, $keyOrArgN = null) 61 | return call_user_func_array([$this->redis, 'eval'], array_merge([$script, count($keys)], $keys, $args)); 62 | } catch (PRedisServerException $e) { 63 | throw new ServerException('eval command has failed', 0, $e); 64 | } 65 | } 66 | 67 | public function zadd(string $key, string $value, float $score): int 68 | { 69 | try { 70 | return $this->redis->zadd($key, [$value => $score]); 71 | } catch (PRedisServerException $e) { 72 | throw new ServerException('zadd command has failed', 0, $e); 73 | } 74 | } 75 | 76 | public function zrem(string $key, string $value): int 77 | { 78 | try { 79 | return $this->redis->zrem($key, [$value]); 80 | } catch (PRedisServerException $e) { 81 | throw new ServerException('zrem command has failed', 0, $e); 82 | } 83 | } 84 | 85 | public function lpush(string $key, string $value): int 86 | { 87 | try { 88 | return $this->redis->lpush($key, [$value]); 89 | } catch (PRedisServerException $e) { 90 | throw new ServerException('lpush command has failed', 0, $e); 91 | } 92 | } 93 | 94 | public function brpop(array $keys, int $timeout): ?RedisResult 95 | { 96 | try { 97 | if ($result = $this->redis->brpop($keys, $timeout)) { 98 | return new RedisResult($result[0], $result[1]); 99 | } 100 | 101 | return null; 102 | } catch (PRedisServerException $e) { 103 | throw new ServerException('brpop command has failed', 0, $e); 104 | } 105 | } 106 | 107 | public function rpop(string $key): ?RedisResult 108 | { 109 | try { 110 | if ($message = $this->redis->rpop($key)) { 111 | return new RedisResult($key, $message); 112 | } 113 | 114 | return null; 115 | } catch (PRedisServerException $e) { 116 | throw new ServerException('rpop command has failed', 0, $e); 117 | } 118 | } 119 | 120 | public function connect(): void 121 | { 122 | if ($this->redis) { 123 | return; 124 | } 125 | 126 | $this->redis = new Client($this->parameters, $this->options); 127 | 128 | // No need to pass "auth" here because Predis already handles this internally 129 | 130 | $this->redis->connect(); 131 | } 132 | 133 | public function disconnect(): void 134 | { 135 | $this->redis->disconnect(); 136 | } 137 | 138 | public function del(string $key): void 139 | { 140 | $this->redis->del([$key]); 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /PhpRedis.php: -------------------------------------------------------------------------------- 1 | config = $config; 27 | } 28 | 29 | public function eval(string $script, array $keys = [], array $args = []) 30 | { 31 | try { 32 | return $this->redis->eval($script, array_merge($keys, $args), count($keys)); 33 | } catch (\RedisException $e) { 34 | throw new ServerException('eval command has failed', 0, $e); 35 | } 36 | } 37 | 38 | public function zadd(string $key, string $value, float $score): int 39 | { 40 | try { 41 | return $this->redis->zAdd($key, $score, $value); 42 | } catch (\RedisException $e) { 43 | throw new ServerException('zadd command has failed', 0, $e); 44 | } 45 | } 46 | 47 | public function zrem(string $key, string $value): int 48 | { 49 | try { 50 | return $this->redis->zRem($key, $value); 51 | } catch (\RedisException $e) { 52 | throw new ServerException('zrem command has failed', 0, $e); 53 | } 54 | } 55 | 56 | public function lpush(string $key, string $value): int 57 | { 58 | try { 59 | return $this->redis->lPush($key, $value); 60 | } catch (\RedisException $e) { 61 | throw new ServerException('lpush command has failed', 0, $e); 62 | } 63 | } 64 | 65 | public function brpop(array $keys, int $timeout): ?RedisResult 66 | { 67 | try { 68 | if ($result = $this->redis->brPop($keys, $timeout)) { 69 | return new RedisResult($result[0], $result[1]); 70 | } 71 | 72 | return null; 73 | } catch (\RedisException $e) { 74 | throw new ServerException('brpop command has failed', 0, $e); 75 | } 76 | } 77 | 78 | public function rpop(string $key): ?RedisResult 79 | { 80 | try { 81 | if ($message = $this->redis->rPop($key)) { 82 | return new RedisResult($key, $message); 83 | } 84 | 85 | return null; 86 | } catch (\RedisException $e) { 87 | throw new ServerException('rpop command has failed', 0, $e); 88 | } 89 | } 90 | 91 | public function connect(): void 92 | { 93 | if ($this->redis) { 94 | return; 95 | } 96 | 97 | $supportedSchemes = ['redis', 'rediss', 'tcp', 'unix']; 98 | if (false == in_array($this->config['scheme'], $supportedSchemes, true)) { 99 | throw new \LogicException(sprintf('The given scheme protocol "%s" is not supported by php extension. It must be one of "%s"', $this->config['scheme'], implode('", "', $supportedSchemes))); 100 | } 101 | 102 | $this->redis = new \Redis(); 103 | 104 | $connectionMethod = $this->config['persistent'] ? 'pconnect' : 'connect'; 105 | 106 | $host = 'rediss' === $this->config['scheme'] ? 'tls://'.$this->config['host'] : $this->config['host']; 107 | 108 | $result = call_user_func( 109 | [$this->redis, $connectionMethod], 110 | 'unix' === $this->config['scheme'] ? $this->config['path'] : $host, 111 | $this->config['port'], 112 | $this->config['timeout'], 113 | $this->config['persistent'] ? ($this->config['phpredis_persistent_id'] ?? null) : null, 114 | (int) ($this->config['phpredis_retry_interval'] ?? 0), 115 | (float) $this->config['read_write_timeout'] ?? 0 116 | ); 117 | 118 | if (false == $result) { 119 | throw new ServerException('Failed to connect.'); 120 | } 121 | 122 | if ($this->config['password']) { 123 | $this->redis->auth($this->config['password']); 124 | } 125 | 126 | if (null !== $this->config['database']) { 127 | $this->redis->select($this->config['database']); 128 | } 129 | } 130 | 131 | public function disconnect(): void 132 | { 133 | if ($this->redis) { 134 | $this->redis->close(); 135 | } 136 | } 137 | 138 | public function del(string $key): void 139 | { 140 | $this->redis->del($key); 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Supporting Enqueue

2 | 3 | Enqueue is an MIT-licensed open source project with its ongoing development made possible entirely by the support of community and our customers. If you'd like to join them, please consider: 4 | 5 | - [Become a sponsor](https://www.patreon.com/makasim) 6 | - [Become our client](http://forma-pro.com/) 7 | 8 | --- 9 | 10 | # Redis Transport 11 | 12 | [![Gitter](https://badges.gitter.im/php-enqueue/Lobby.svg)](https://gitter.im/php-enqueue/Lobby) 13 | [![Build Status](https://img.shields.io/github/actions/workflow/status/php-enqueue/redis/ci.yml?branch=master)](https://github.com/php-enqueue/redis/actions?query=workflow%3ACI) 14 | [![Total Downloads](https://poser.pugx.org/enqueue/redis/d/total.png)](https://packagist.org/packages/enqueue/redis) 15 | [![Latest Stable Version](https://poser.pugx.org/enqueue/redis/version.png)](https://packagist.org/packages/enqueue/redis) 16 | 17 | This is an implementation of Queue Interop specification. It allows you to send and consume message with Redis store as a broker. 18 | 19 | ## Resources 20 | 21 | * [Site](https://enqueue.forma-pro.com/) 22 | * [Documentation](https://php-enqueue.github.io/transport/redis/) 23 | * [Questions](https://gitter.im/php-enqueue/Lobby) 24 | * [Issue Tracker](https://github.com/php-enqueue/enqueue-dev/issues) 25 | 26 | ## Developed by Forma-Pro 27 | 28 | Forma-Pro is a full stack development company which interests also spread to open source development. 29 | Being a team of strong professionals we have an aim an ability to help community by developing cutting edge solutions in the areas of e-commerce, docker & microservice oriented architecture where we have accumulated a huge many-years experience. 30 | Our main specialization is Symfony framework based solution, but we are always looking to the technologies that allow us to do our job the best way. We are committed to creating solutions that revolutionize the way how things are developed in aspects of architecture & scalability. 31 | 32 | If you have any questions and inquires about our open source development, this product particularly or any other matter feel free to contact at opensource@forma-pro.com 33 | 34 | ## License 35 | 36 | It is released under the [MIT License](LICENSE). 37 | -------------------------------------------------------------------------------- /Redis.php: -------------------------------------------------------------------------------- 1 | A redis DSN string. 28 | * 'scheme' => Specifies the protocol used to communicate with an instance of Redis. 29 | * 'host' => IP or hostname of the target server. 30 | * 'port' => TCP/IP port of the target server. 31 | * 'path' => Path of the UNIX domain socket file used when connecting to Redis using UNIX domain sockets. 32 | * 'database' => Accepts a numeric value that is used by Predis to automatically select a logical database with the SELECT command. 33 | * 'password' => Accepts a value used to authenticate with a Redis server protected by password with the AUTH command. 34 | * 'async' => Specifies if connections to the server is estabilished in a non-blocking way (that is, the client is not blocked while the underlying resource performs the actual connection). 35 | * 'persistent' => Specifies if the underlying connection resource should be left open when a script ends its lifecycle. 36 | * 'lazy' => The connection will be performed as later as possible, if the option set to true 37 | * 'timeout' => Timeout (expressed in seconds) used to connect to a Redis server after which an exception is thrown. 38 | * 'read_write_timeout' => Timeout (expressed in seconds) used when performing read or write operations on the underlying network resource after which an exception is thrown. 39 | * 'predis_options' => An array of predis specific options. 40 | * 'ssl' => could be any of http://fi2.php.net/manual/en/context.ssl.php#refsect1-context.ssl-options 41 | * 'redelivery_delay' => Default 300 sec. Returns back message into the queue if message was not acknowledged or rejected after this delay. 42 | * It could happen if consumer has failed with fatal error or even if message processing is slow and takes more than this time. 43 | * ]. 44 | * 45 | * or 46 | * 47 | * redis://h:asdfqwer1234asdf@ec2-111-1-1-1.compute-1.amazonaws.com:111 48 | * tls://127.0.0.1?ssl[cafile]=private.pem&ssl[verify_peer]=1 49 | * 50 | * or 51 | * 52 | * instance of Enqueue\Redis 53 | * 54 | * @param array|string|Redis|null $config 55 | */ 56 | public function __construct($config = 'redis:') 57 | { 58 | if ($config instanceof Redis) { 59 | $this->redis = $config; 60 | $this->config = $this->defaultConfig(); 61 | 62 | return; 63 | } 64 | 65 | if (empty($config)) { 66 | $config = []; 67 | } elseif (is_string($config)) { 68 | $config = $this->parseDsn($config); 69 | } elseif (is_array($config)) { 70 | if (array_key_exists('dsn', $config)) { 71 | $config = array_replace_recursive($config, $this->parseDsn($config['dsn'])); 72 | 73 | unset($config['dsn']); 74 | } 75 | } else { 76 | throw new \LogicException(sprintf('The config must be either an array of options, a DSN string, null or instance of %s', Redis::class)); 77 | } 78 | 79 | $this->config = array_replace($this->defaultConfig(), $config); 80 | } 81 | 82 | /** 83 | * @return RedisContext 84 | */ 85 | public function createContext(): Context 86 | { 87 | if ($this->config['lazy']) { 88 | return new RedisContext(function () { 89 | return $this->createRedis(); 90 | }, (int) $this->config['redelivery_delay']); 91 | } 92 | 93 | return new RedisContext($this->createRedis(), (int) $this->config['redelivery_delay']); 94 | } 95 | 96 | private function createRedis(): Redis 97 | { 98 | if (false == $this->redis) { 99 | if (in_array('phpredis', $this->config['scheme_extensions'], true)) { 100 | $this->redis = new PhpRedis($this->config); 101 | } else { 102 | $this->redis = new PRedis($this->config); 103 | } 104 | 105 | $this->redis->connect(); 106 | } 107 | 108 | return $this->redis; 109 | } 110 | 111 | private function parseDsn(string $dsn): array 112 | { 113 | $dsn = Dsn::parseFirst($dsn); 114 | 115 | $supportedSchemes = ['redis', 'rediss', 'tcp', 'tls', 'unix']; 116 | if (false == in_array($dsn->getSchemeProtocol(), $supportedSchemes, true)) { 117 | throw new \LogicException(sprintf('The given scheme protocol "%s" is not supported. It must be one of "%s"', $dsn->getSchemeProtocol(), implode('", "', $supportedSchemes))); 118 | } 119 | 120 | $database = $dsn->getDecimal('database'); 121 | 122 | // try use path as database name if not set. 123 | if (null === $database && 'unix' !== $dsn->getSchemeProtocol() && null !== $dsn->getPath()) { 124 | $database = (int) ltrim($dsn->getPath(), '/'); 125 | } 126 | 127 | return array_filter(array_replace($dsn->getQuery(), [ 128 | 'scheme' => $dsn->getSchemeProtocol(), 129 | 'scheme_extensions' => $dsn->getSchemeExtensions(), 130 | 'host' => $dsn->getHost(), 131 | 'port' => $dsn->getPort(), 132 | 'path' => $dsn->getPath(), 133 | 'database' => $database, 134 | 'password' => $dsn->getPassword() ?: $dsn->getUser() ?: $dsn->getString('password'), 135 | 'async' => $dsn->getBool('async'), 136 | 'persistent' => $dsn->getBool('persistent'), 137 | 'timeout' => $dsn->getFloat('timeout'), 138 | 'read_write_timeout' => $dsn->getFloat('read_write_timeout'), 139 | ]), function ($value) { return null !== $value; }); 140 | } 141 | 142 | private function defaultConfig(): array 143 | { 144 | return [ 145 | 'scheme' => 'redis', 146 | 'scheme_extensions' => [], 147 | 'host' => '127.0.0.1', 148 | 'port' => 6379, 149 | 'path' => null, 150 | 'database' => null, 151 | 'password' => null, 152 | 'async' => false, 153 | 'persistent' => false, 154 | 'lazy' => true, 155 | 'timeout' => 5.0, 156 | 'read_write_timeout' => null, 157 | 'predis_options' => null, 158 | 'ssl' => null, 159 | 'redelivery_delay' => 300, 160 | ]; 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /RedisConsumer.php: -------------------------------------------------------------------------------- 1 | context = $context; 34 | $this->queue = $queue; 35 | } 36 | 37 | public function getRedeliveryDelay(): ?int 38 | { 39 | return $this->redeliveryDelay; 40 | } 41 | 42 | public function setRedeliveryDelay(int $delay): void 43 | { 44 | $this->redeliveryDelay = $delay; 45 | } 46 | 47 | /** 48 | * @return RedisDestination 49 | */ 50 | public function getQueue(): Queue 51 | { 52 | return $this->queue; 53 | } 54 | 55 | /** 56 | * @return RedisMessage 57 | */ 58 | public function receive(int $timeout = 0): ?Message 59 | { 60 | $timeout = (int) ceil($timeout / 1000); 61 | 62 | if ($timeout <= 0) { 63 | while (true) { 64 | if ($message = $this->receive(5000)) { 65 | return $message; 66 | } 67 | } 68 | } 69 | 70 | return $this->receiveMessage([$this->queue], $timeout, $this->redeliveryDelay); 71 | } 72 | 73 | /** 74 | * @return RedisMessage 75 | */ 76 | public function receiveNoWait(): ?Message 77 | { 78 | return $this->receiveMessageNoWait($this->queue, $this->redeliveryDelay); 79 | } 80 | 81 | /** 82 | * @param RedisMessage $message 83 | */ 84 | public function acknowledge(Message $message): void 85 | { 86 | $this->getRedis()->zrem($this->queue->getName().':reserved', $message->getReservedKey()); 87 | } 88 | 89 | /** 90 | * @param RedisMessage $message 91 | */ 92 | public function reject(Message $message, bool $requeue = false): void 93 | { 94 | InvalidMessageException::assertMessageInstanceOf($message, RedisMessage::class); 95 | 96 | $this->acknowledge($message); 97 | 98 | if ($requeue) { 99 | $message = $this->getContext()->getSerializer()->toMessage($message->getReservedKey()); 100 | $message->setRedelivered(true); 101 | 102 | if ($message->getTimeToLive()) { 103 | $message->setHeader('expires_at', time() + $message->getTimeToLive()); 104 | } 105 | 106 | $payload = $this->getContext()->getSerializer()->toString($message); 107 | 108 | $this->getRedis()->lpush($this->queue->getName(), $payload); 109 | } 110 | } 111 | 112 | private function getContext(): RedisContext 113 | { 114 | return $this->context; 115 | } 116 | 117 | private function getRedis(): Redis 118 | { 119 | return $this->context->getRedis(); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /RedisConsumerHelperTrait.php: -------------------------------------------------------------------------------- 1 | queueNames) { 25 | $this->queueNames = []; 26 | foreach ($queues as $queue) { 27 | $this->queueNames[] = $queue->getName(); 28 | } 29 | } 30 | 31 | while ($thisTimeout > 0) { 32 | $this->migrateExpiredMessages($this->queueNames); 33 | 34 | if (false == $result = $this->getContext()->getRedis()->brpop($this->queueNames, $thisTimeout)) { 35 | return null; 36 | } 37 | 38 | $this->pushQueueNameBack($result->getKey()); 39 | 40 | if ($message = $this->processResult($result, $redeliveryDelay)) { 41 | return $message; 42 | } 43 | 44 | $thisTimeout -= time() - $startAt; 45 | } 46 | 47 | return null; 48 | } 49 | 50 | protected function receiveMessageNoWait(RedisDestination $destination, int $redeliveryDelay): ?RedisMessage 51 | { 52 | $this->migrateExpiredMessages([$destination->getName()]); 53 | 54 | if ($result = $this->getContext()->getRedis()->rpop($destination->getName())) { 55 | return $this->processResult($result, $redeliveryDelay); 56 | } 57 | 58 | return null; 59 | } 60 | 61 | protected function processResult(RedisResult $result, int $redeliveryDelay): ?RedisMessage 62 | { 63 | $message = $this->getContext()->getSerializer()->toMessage($result->getMessage()); 64 | 65 | $now = time(); 66 | 67 | if (0 === $message->getAttempts() && $expiresAt = $message->getHeader('expires_at')) { 68 | if ($now > $expiresAt) { 69 | return null; 70 | } 71 | } 72 | 73 | $message->setHeader('attempts', $message->getAttempts() + 1); 74 | $message->setRedelivered($message->getAttempts() > 1); 75 | $message->setKey($result->getKey()); 76 | $message->setReservedKey($this->getContext()->getSerializer()->toString($message)); 77 | 78 | $reservedQueue = $result->getKey().':reserved'; 79 | $redeliveryAt = $now + $redeliveryDelay; 80 | 81 | $this->getContext()->getRedis()->zadd($reservedQueue, $message->getReservedKey(), $redeliveryAt); 82 | 83 | return $message; 84 | } 85 | 86 | protected function pushQueueNameBack(string $queueName): void 87 | { 88 | if (count($this->queueNames) <= 1) { 89 | return; 90 | } 91 | 92 | if (false === $from = array_search($queueName, $this->queueNames, true)) { 93 | throw new \LogicException(sprintf('Queue name was not found: "%s"', $queueName)); 94 | } 95 | 96 | $to = count($this->queueNames) - 1; 97 | 98 | $out = array_splice($this->queueNames, $from, 1); 99 | array_splice($this->queueNames, $to, 0, $out); 100 | } 101 | 102 | protected function migrateExpiredMessages(array $queueNames): void 103 | { 104 | $now = time(); 105 | 106 | foreach ($queueNames as $queueName) { 107 | $this->getContext()->getRedis() 108 | ->eval(LuaScripts::migrateExpired(), [$queueName.':delayed', $queueName], [$now]); 109 | 110 | $this->getContext()->getRedis() 111 | ->eval(LuaScripts::migrateExpired(), [$queueName.':reserved', $queueName], [$now]); 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /RedisContext.php: -------------------------------------------------------------------------------- 1 | redis = $redis; 46 | } elseif (is_callable($redis)) { 47 | $this->redisFactory = $redis; 48 | } else { 49 | throw new \InvalidArgumentException(sprintf('The $redis argument must be either %s or callable that returns %s once called.', Redis::class, Redis::class)); 50 | } 51 | 52 | $this->redeliveryDelay = $redeliveryDelay; 53 | $this->setSerializer(new JsonSerializer()); 54 | } 55 | 56 | /** 57 | * @return RedisMessage 58 | */ 59 | public function createMessage(string $body = '', array $properties = [], array $headers = []): Message 60 | { 61 | return new RedisMessage($body, $properties, $headers); 62 | } 63 | 64 | /** 65 | * @return RedisDestination 66 | */ 67 | public function createTopic(string $topicName): Topic 68 | { 69 | return new RedisDestination($topicName); 70 | } 71 | 72 | /** 73 | * @return RedisDestination 74 | */ 75 | public function createQueue(string $queueName): Queue 76 | { 77 | return new RedisDestination($queueName); 78 | } 79 | 80 | /** 81 | * @param RedisDestination $queue 82 | */ 83 | public function deleteQueue(Queue $queue): void 84 | { 85 | InvalidDestinationException::assertDestinationInstanceOf($queue, RedisDestination::class); 86 | 87 | $this->deleteDestination($queue); 88 | } 89 | 90 | /** 91 | * @param RedisDestination $topic 92 | */ 93 | public function deleteTopic(Topic $topic): void 94 | { 95 | InvalidDestinationException::assertDestinationInstanceOf($topic, RedisDestination::class); 96 | 97 | $this->deleteDestination($topic); 98 | } 99 | 100 | public function createTemporaryQueue(): Queue 101 | { 102 | throw TemporaryQueueNotSupportedException::providerDoestNotSupportIt(); 103 | } 104 | 105 | /** 106 | * @return RedisProducer 107 | */ 108 | public function createProducer(): Producer 109 | { 110 | return new RedisProducer($this); 111 | } 112 | 113 | /** 114 | * @param RedisDestination $destination 115 | * 116 | * @return RedisConsumer 117 | */ 118 | public function createConsumer(Destination $destination): Consumer 119 | { 120 | InvalidDestinationException::assertDestinationInstanceOf($destination, RedisDestination::class); 121 | 122 | $consumer = new RedisConsumer($this, $destination); 123 | $consumer->setRedeliveryDelay($this->redeliveryDelay); 124 | 125 | return $consumer; 126 | } 127 | 128 | /** 129 | * @return RedisSubscriptionConsumer 130 | */ 131 | public function createSubscriptionConsumer(): SubscriptionConsumer 132 | { 133 | $consumer = new RedisSubscriptionConsumer($this); 134 | $consumer->setRedeliveryDelay($this->redeliveryDelay); 135 | 136 | return $consumer; 137 | } 138 | 139 | /** 140 | * @param RedisDestination $queue 141 | */ 142 | public function purgeQueue(Queue $queue): void 143 | { 144 | $this->deleteDestination($queue); 145 | } 146 | 147 | public function close(): void 148 | { 149 | $this->getRedis()->disconnect(); 150 | } 151 | 152 | public function getRedis(): Redis 153 | { 154 | if (false == $this->redis) { 155 | $redis = call_user_func($this->redisFactory); 156 | if (false == $redis instanceof Redis) { 157 | throw new \LogicException(sprintf('The factory must return instance of %s. It returned %s', Redis::class, is_object($redis) ? $redis::class : gettype($redis))); 158 | } 159 | 160 | $this->redis = $redis; 161 | } 162 | 163 | return $this->redis; 164 | } 165 | 166 | private function deleteDestination(RedisDestination $destination): void 167 | { 168 | $this->getRedis()->del($destination->getName()); 169 | $this->getRedis()->del($destination->getName().':delayed'); 170 | $this->getRedis()->del($destination->getName().':reserved'); 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /RedisDestination.php: -------------------------------------------------------------------------------- 1 | name = $name; 20 | } 21 | 22 | public function getName(): string 23 | { 24 | return $this->name; 25 | } 26 | 27 | public function getQueueName(): string 28 | { 29 | return $this->getName(); 30 | } 31 | 32 | public function getTopicName(): string 33 | { 34 | return $this->getName(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /RedisMessage.php: -------------------------------------------------------------------------------- 1 | body = $body; 44 | $this->properties = $properties; 45 | $this->headers = $headers; 46 | 47 | $this->redelivered = false; 48 | } 49 | 50 | public function getBody(): string 51 | { 52 | return $this->body; 53 | } 54 | 55 | public function setBody(string $body): void 56 | { 57 | $this->body = $body; 58 | } 59 | 60 | public function setProperties(array $properties): void 61 | { 62 | $this->properties = $properties; 63 | } 64 | 65 | public function getProperties(): array 66 | { 67 | return $this->properties; 68 | } 69 | 70 | public function setProperty(string $name, $value): void 71 | { 72 | $this->properties[$name] = $value; 73 | } 74 | 75 | public function getProperty(string $name, $default = null) 76 | { 77 | return array_key_exists($name, $this->properties) ? $this->properties[$name] : $default; 78 | } 79 | 80 | public function setHeaders(array $headers): void 81 | { 82 | $this->headers = $headers; 83 | } 84 | 85 | public function getHeaders(): array 86 | { 87 | return $this->headers; 88 | } 89 | 90 | public function setHeader(string $name, $value): void 91 | { 92 | $this->headers[$name] = $value; 93 | } 94 | 95 | public function getHeader(string $name, $default = null) 96 | { 97 | return array_key_exists($name, $this->headers) ? $this->headers[$name] : $default; 98 | } 99 | 100 | public function setRedelivered(bool $redelivered): void 101 | { 102 | $this->redelivered = (bool) $redelivered; 103 | } 104 | 105 | public function isRedelivered(): bool 106 | { 107 | return $this->redelivered; 108 | } 109 | 110 | public function setCorrelationId(?string $correlationId = null): void 111 | { 112 | $this->setHeader('correlation_id', $correlationId); 113 | } 114 | 115 | public function getCorrelationId(): ?string 116 | { 117 | return $this->getHeader('correlation_id'); 118 | } 119 | 120 | public function setMessageId(?string $messageId = null): void 121 | { 122 | $this->setHeader('message_id', $messageId); 123 | } 124 | 125 | public function getMessageId(): ?string 126 | { 127 | return $this->getHeader('message_id'); 128 | } 129 | 130 | public function getTimestamp(): ?int 131 | { 132 | $value = $this->getHeader('timestamp'); 133 | 134 | return null === $value ? null : (int) $value; 135 | } 136 | 137 | public function setTimestamp(?int $timestamp = null): void 138 | { 139 | $this->setHeader('timestamp', $timestamp); 140 | } 141 | 142 | public function setReplyTo(?string $replyTo = null): void 143 | { 144 | $this->setHeader('reply_to', $replyTo); 145 | } 146 | 147 | public function getReplyTo(): ?string 148 | { 149 | return $this->getHeader('reply_to'); 150 | } 151 | 152 | public function getAttempts(): int 153 | { 154 | return (int) $this->getHeader('attempts', 0); 155 | } 156 | 157 | public function getTimeToLive(): ?int 158 | { 159 | return $this->getHeader('time_to_live'); 160 | } 161 | 162 | /** 163 | * Set time to live in milliseconds. 164 | */ 165 | public function setTimeToLive(?int $timeToLive = null): void 166 | { 167 | $this->setHeader('time_to_live', $timeToLive); 168 | } 169 | 170 | public function getDeliveryDelay(): ?int 171 | { 172 | return $this->getHeader('delivery_delay'); 173 | } 174 | 175 | /** 176 | * Set delay in milliseconds. 177 | */ 178 | public function setDeliveryDelay(?int $deliveryDelay = null): void 179 | { 180 | $this->setHeader('delivery_delay', $deliveryDelay); 181 | } 182 | 183 | public function getReservedKey(): ?string 184 | { 185 | return $this->reservedKey; 186 | } 187 | 188 | public function setReservedKey(string $reservedKey) 189 | { 190 | $this->reservedKey = $reservedKey; 191 | } 192 | 193 | public function getKey(): ?string 194 | { 195 | return $this->key; 196 | } 197 | 198 | public function setKey(string $key): void 199 | { 200 | $this->key = $key; 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /RedisProducer.php: -------------------------------------------------------------------------------- 1 | context = $context; 35 | } 36 | 37 | /** 38 | * @param RedisDestination $destination 39 | * @param RedisMessage $message 40 | */ 41 | public function send(Destination $destination, Message $message): void 42 | { 43 | InvalidDestinationException::assertDestinationInstanceOf($destination, RedisDestination::class); 44 | InvalidMessageException::assertMessageInstanceOf($message, RedisMessage::class); 45 | 46 | $message->setMessageId(Uuid::uuid4()->toString()); 47 | $message->setHeader('attempts', 0); 48 | 49 | if (null !== $this->timeToLive && null === $message->getTimeToLive()) { 50 | $message->setTimeToLive($this->timeToLive); 51 | } 52 | 53 | if (null !== $this->deliveryDelay && null === $message->getDeliveryDelay()) { 54 | $message->setDeliveryDelay($this->deliveryDelay); 55 | } 56 | 57 | if ($message->getTimeToLive()) { 58 | $message->setHeader('expires_at', time() + $message->getTimeToLive()); 59 | } 60 | 61 | $payload = $this->context->getSerializer()->toString($message); 62 | 63 | if ($message->getDeliveryDelay()) { 64 | $deliveryAt = time() + $message->getDeliveryDelay() / 1000; 65 | $this->context->getRedis()->zadd($destination->getName().':delayed', $payload, $deliveryAt); 66 | } else { 67 | $this->context->getRedis()->lpush($destination->getName(), $payload); 68 | } 69 | } 70 | 71 | /** 72 | * @return self 73 | */ 74 | public function setDeliveryDelay(?int $deliveryDelay = null): Producer 75 | { 76 | $this->deliveryDelay = $deliveryDelay; 77 | 78 | return $this; 79 | } 80 | 81 | public function getDeliveryDelay(): ?int 82 | { 83 | return $this->deliveryDelay; 84 | } 85 | 86 | /** 87 | * @return RedisProducer 88 | */ 89 | public function setPriority(?int $priority = null): Producer 90 | { 91 | if (null === $priority) { 92 | return $this; 93 | } 94 | 95 | throw PriorityNotSupportedException::providerDoestNotSupportIt(); 96 | } 97 | 98 | public function getPriority(): ?int 99 | { 100 | return null; 101 | } 102 | 103 | /** 104 | * @return self 105 | */ 106 | public function setTimeToLive(?int $timeToLive = null): Producer 107 | { 108 | $this->timeToLive = $timeToLive; 109 | 110 | return $this; 111 | } 112 | 113 | public function getTimeToLive(): ?int 114 | { 115 | return $this->timeToLive; 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /RedisResult.php: -------------------------------------------------------------------------------- 1 | key = $key; 22 | $this->message = $message; 23 | } 24 | 25 | public function getKey(): string 26 | { 27 | return $this->key; 28 | } 29 | 30 | public function getMessage(): string 31 | { 32 | return $this->message; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /RedisSubscriptionConsumer.php: -------------------------------------------------------------------------------- 1 | context = $context; 34 | $this->subscribers = []; 35 | } 36 | 37 | public function getRedeliveryDelay(): ?int 38 | { 39 | return $this->redeliveryDelay; 40 | } 41 | 42 | public function setRedeliveryDelay(int $delay): void 43 | { 44 | $this->redeliveryDelay = $delay; 45 | } 46 | 47 | public function consume(int $timeout = 0): void 48 | { 49 | if (empty($this->subscribers)) { 50 | throw new \LogicException('No subscribers'); 51 | } 52 | 53 | $timeout = (int) ceil($timeout / 1000); 54 | $endAt = time() + $timeout; 55 | 56 | $queues = []; 57 | /** @var Consumer $consumer */ 58 | foreach ($this->subscribers as list($consumer)) { 59 | $queues[] = $consumer->getQueue(); 60 | } 61 | 62 | while (true) { 63 | if ($message = $this->receiveMessage($queues, $timeout ?: 5, $this->redeliveryDelay)) { 64 | list($consumer, $callback) = $this->subscribers[$message->getKey()]; 65 | 66 | if (false === call_user_func($callback, $message, $consumer)) { 67 | return; 68 | } 69 | } 70 | 71 | if ($timeout && microtime(true) >= $endAt) { 72 | return; 73 | } 74 | } 75 | } 76 | 77 | /** 78 | * @param RedisConsumer $consumer 79 | */ 80 | public function subscribe(Consumer $consumer, callable $callback): void 81 | { 82 | if (false == $consumer instanceof RedisConsumer) { 83 | throw new \InvalidArgumentException(sprintf('The consumer must be instance of "%s" got "%s"', RedisConsumer::class, $consumer::class)); 84 | } 85 | 86 | $queueName = $consumer->getQueue()->getQueueName(); 87 | if (array_key_exists($queueName, $this->subscribers)) { 88 | if ($this->subscribers[$queueName][0] === $consumer && $this->subscribers[$queueName][1] === $callback) { 89 | return; 90 | } 91 | 92 | throw new \InvalidArgumentException(sprintf('There is a consumer subscribed to queue: "%s"', $queueName)); 93 | } 94 | 95 | $this->subscribers[$queueName] = [$consumer, $callback]; 96 | $this->queueNames = null; 97 | } 98 | 99 | /** 100 | * @param RedisConsumer $consumer 101 | */ 102 | public function unsubscribe(Consumer $consumer): void 103 | { 104 | if (false == $consumer instanceof RedisConsumer) { 105 | throw new \InvalidArgumentException(sprintf('The consumer must be instance of "%s" got "%s"', RedisConsumer::class, $consumer::class)); 106 | } 107 | 108 | $queueName = $consumer->getQueue()->getQueueName(); 109 | 110 | if (false == array_key_exists($queueName, $this->subscribers)) { 111 | return; 112 | } 113 | 114 | if ($this->subscribers[$queueName][0] !== $consumer) { 115 | return; 116 | } 117 | 118 | unset($this->subscribers[$queueName]); 119 | $this->queueNames = null; 120 | } 121 | 122 | public function unsubscribeAll(): void 123 | { 124 | $this->subscribers = []; 125 | $this->queueNames = null; 126 | } 127 | 128 | private function getContext(): RedisContext 129 | { 130 | return $this->context; 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /Serializer.php: -------------------------------------------------------------------------------- 1 | serializer = $serializer; 17 | } 18 | 19 | /** 20 | * @return Serializer 21 | */ 22 | public function getSerializer() 23 | { 24 | return $this->serializer; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /ServerException.php: -------------------------------------------------------------------------------- 1 |