├── .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 | [](https://gitter.im/php-enqueue/Lobby)
13 | [](https://github.com/php-enqueue/redis/actions?query=workflow%3ACI)
14 | [](https://packagist.org/packages/enqueue/redis)
15 | [](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 |