Skip to content

Commit 2cd2524

Browse files
committed
[redis] Add subscription consumer
1 parent bc28f1e commit 2cd2524

20 files changed

+556
-6
lines changed

Diff for: pkg/redis/PhpRedis.php

-2
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,6 @@ public function __construct(array $config)
3030
'persisted' => false,
3131
'database' => 0,
3232
], $config);
33-
34-
var_dump($this->config);
3533
}
3634

3735
/**

Diff for: pkg/redis/RedisContext.php

+12-1
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@
66
use Interop\Queue\PsrContext;
77
use Interop\Queue\PsrDestination;
88
use Interop\Queue\PsrQueue;
9+
use Interop\Queue\PsrSubscriptionConsumerAwareContext;
910
use Interop\Queue\PsrTopic;
1011

11-
class RedisContext implements PsrContext
12+
class RedisContext implements PsrContext, PsrSubscriptionConsumerAwareContext
1213
{
1314
/**
1415
* @var Redis
@@ -122,6 +123,16 @@ public function createConsumer(PsrDestination $destination)
122123
return new RedisConsumer($this, $destination);
123124
}
124125

126+
/**
127+
* {@inheritdoc}
128+
*
129+
* @return RedisSubscriptionConsumer
130+
*/
131+
public function createSubscriptionConsumer()
132+
{
133+
return new RedisSubscriptionConsumer($this);
134+
}
135+
125136
public function close()
126137
{
127138
$this->getRedis()->disconnect();

Diff for: pkg/redis/RedisSubscriptionConsumer.php

+15-2
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ class RedisSubscriptionConsumer implements PsrSubscriptionConsumer
2525
public function __construct(RedisContext $context)
2626
{
2727
$this->context = $context;
28+
$this->subscribers = [];
2829
}
2930

3031
/**
@@ -58,7 +59,7 @@ public function consume($timeout = 0)
5859
$result = $this->context->getRedis()->brpop($currentQueueNames, $timeout || 5000);
5960
if ($result) {
6061
$message = RedisMessage::jsonUnserialize($result->getMessage());
61-
$callback = $this->subscribers[$result->getKey()];
62+
list($consumer, $callback) = $this->subscribers[$result->getKey()];
6263
if (false === call_user_func($callback, $message, $consumer)) {
6364
return;
6465
}
@@ -91,7 +92,11 @@ public function subscribe(PsrConsumer $consumer, callable $callback)
9192

9293
$queueName = $consumer->getQueue()->getQueueName();
9394
if (array_key_exists($queueName, $this->subscribers)) {
94-
return;
95+
if ($this->subscribers[$queueName][0] === $consumer && $this->subscribers[$queueName][1] === $callback) {
96+
return;
97+
}
98+
99+
throw new \InvalidArgumentException(sprintf('There is a consumer subscribed to queue: "%s"', $queueName));
95100
}
96101

97102
$this->subscribers[$queueName] = [$consumer, $callback];
@@ -110,6 +115,14 @@ public function unsubscribe(PsrConsumer $consumer)
110115

111116
$queueName = $consumer->getQueue()->getQueueName();
112117

118+
if (false == array_key_exists($queueName, $this->subscribers)) {
119+
return;
120+
}
121+
122+
if ($this->subscribers[$queueName][0] !== $consumer) {
123+
return;
124+
}
125+
113126
unset($this->subscribers[$queueName]);
114127
}
115128

Diff for: pkg/redis/Tests/RedisContextTest.php

+16
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,11 @@
1010
use Enqueue\Redis\RedisDestination;
1111
use Enqueue\Redis\RedisMessage;
1212
use Enqueue\Redis\RedisProducer;
13+
use Enqueue\Redis\RedisSubscriptionConsumer;
1314
use Enqueue\Test\ClassExtensionTrait;
1415
use Interop\Queue\InvalidDestinationException;
1516
use Interop\Queue\PsrContext;
17+
use Interop\Queue\PsrSubscriptionConsumerAwareContext;
1618

1719
class RedisContextTest extends \PHPUnit\Framework\TestCase
1820
{
@@ -201,6 +203,20 @@ public function testShouldAllowDeleteTopic()
201203
$context->deleteQueue($topic);
202204
}
203205

206+
public function testShouldImplementPsrSubscriptionConsumerAwareInterface()
207+
{
208+
$rc = new \ReflectionClass(RedisContext::class);
209+
210+
$this->assertTrue($rc->implementsInterface(PsrSubscriptionConsumerAwareContext::class));
211+
}
212+
213+
public function testShouldReturnExpectedSubscriptionConsumerInstance()
214+
{
215+
$context = new RedisContext($this->createRedisMock());
216+
217+
$this->assertInstanceOf(RedisSubscriptionConsumer::class, $context->createSubscriptionConsumer());
218+
}
219+
204220
/**
205221
* @return \PHPUnit_Framework_MockObject_MockObject|Redis
206222
*/

Diff for: pkg/redis/Tests/RedisSubscriptionConsumerTest.php

+174
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
<?php
2+
3+
namespace Enqueue\Redis\Tests;
4+
5+
use Enqueue\Redis\RedisConsumer;
6+
use Enqueue\Redis\RedisContext;
7+
use Enqueue\Redis\RedisSubscriptionConsumer;
8+
use Interop\Queue\PsrConsumer;
9+
use Interop\Queue\PsrQueue;
10+
use Interop\Queue\PsrSubscriptionConsumer;
11+
use PHPUnit\Framework\TestCase;
12+
13+
class RedisSubscriptionConsumerTest extends TestCase
14+
{
15+
public function testShouldImplementPsrSubscriptionConsumerInterface()
16+
{
17+
$rc = new \ReflectionClass(RedisSubscriptionConsumer::class);
18+
19+
$this->assertTrue($rc->implementsInterface(PsrSubscriptionConsumer::class));
20+
}
21+
22+
public function testCouldBeConstructedWithAmqpContextAsFirstArgument()
23+
{
24+
new RedisSubscriptionConsumer($this->createRedisContextMock());
25+
}
26+
27+
public function testShouldAddConsumerAndCallbackToSubscribersPropertyOnSubscribe()
28+
{
29+
$subscriptionConsumer = new RedisSubscriptionConsumer($this->createRedisContextMock());
30+
31+
$fooCallback = function () {};
32+
$fooConsumer = $this->createConsumerStub('foo_queue');
33+
34+
$barCallback = function () {};
35+
$barConsumer = $this->createConsumerStub('bar_queue');
36+
37+
$subscriptionConsumer->subscribe($fooConsumer, $fooCallback);
38+
$subscriptionConsumer->subscribe($barConsumer, $barCallback);
39+
40+
$this->assertAttributeSame([
41+
'foo_queue' => [$fooConsumer, $fooCallback],
42+
'bar_queue' => [$barConsumer, $barCallback],
43+
], 'subscribers', $subscriptionConsumer);
44+
}
45+
46+
public function testThrowsIfTrySubscribeAnotherConsumerToAlreadySubscribedQueue()
47+
{
48+
$subscriptionConsumer = new RedisSubscriptionConsumer($this->createRedisContextMock());
49+
50+
$fooCallback = function () {};
51+
$fooConsumer = $this->createConsumerStub('foo_queue');
52+
53+
$barCallback = function () {};
54+
$barConsumer = $this->createConsumerStub('foo_queue');
55+
56+
$subscriptionConsumer->subscribe($fooConsumer, $fooCallback);
57+
58+
$this->expectException(\InvalidArgumentException::class);
59+
$this->expectExceptionMessage('There is a consumer subscribed to queue: "foo_queue"');
60+
$subscriptionConsumer->subscribe($barConsumer, $barCallback);
61+
}
62+
63+
public function testShouldAllowSubscribeSameConsumerAndCallbackSecondTime()
64+
{
65+
$subscriptionConsumer = new RedisSubscriptionConsumer($this->createRedisContextMock());
66+
67+
$fooCallback = function () {};
68+
$fooConsumer = $this->createConsumerStub('foo_queue');
69+
70+
$subscriptionConsumer->subscribe($fooConsumer, $fooCallback);
71+
$subscriptionConsumer->subscribe($fooConsumer, $fooCallback);
72+
}
73+
74+
public function testShouldRemoveSubscribedConsumerOnUnsubscribeCall()
75+
{
76+
$subscriptionConsumer = new RedisSubscriptionConsumer($this->createRedisContextMock());
77+
78+
$fooConsumer = $this->createConsumerStub('foo_queue');
79+
$barConsumer = $this->createConsumerStub('bar_queue');
80+
81+
$subscriptionConsumer->subscribe($fooConsumer, function () {});
82+
$subscriptionConsumer->subscribe($barConsumer, function () {});
83+
84+
// guard
85+
$this->assertAttributeCount(2, 'subscribers', $subscriptionConsumer);
86+
87+
$subscriptionConsumer->unsubscribe($fooConsumer);
88+
89+
$this->assertAttributeCount(1, 'subscribers', $subscriptionConsumer);
90+
}
91+
92+
public function testShouldDoNothingIfTryUnsubscribeNotSubscribedQueueName()
93+
{
94+
$subscriptionConsumer = new RedisSubscriptionConsumer($this->createRedisContextMock());
95+
96+
$subscriptionConsumer->subscribe($this->createConsumerStub('foo_queue'), function () {});
97+
98+
// guard
99+
$this->assertAttributeCount(1, 'subscribers', $subscriptionConsumer);
100+
101+
$subscriptionConsumer->unsubscribe($this->createConsumerStub('bar_queue'));
102+
103+
$this->assertAttributeCount(1, 'subscribers', $subscriptionConsumer);
104+
}
105+
106+
public function testShouldDoNothingIfTryUnsubscribeNotSubscribedConsumer()
107+
{
108+
$subscriptionConsumer = new RedisSubscriptionConsumer($this->createRedisContextMock());
109+
110+
$subscriptionConsumer->subscribe($this->createConsumerStub('foo_queue'), function () {});
111+
112+
// guard
113+
$this->assertAttributeCount(1, 'subscribers', $subscriptionConsumer);
114+
115+
$subscriptionConsumer->unsubscribe($this->createConsumerStub('foo_queue'));
116+
117+
$this->assertAttributeCount(1, 'subscribers', $subscriptionConsumer);
118+
}
119+
120+
public function testShouldRemoveAllSubscriberOnUnsubscribeAllCall()
121+
{
122+
$subscriptionConsumer = new RedisSubscriptionConsumer($this->createRedisContextMock());
123+
124+
$subscriptionConsumer->subscribe($this->createConsumerStub('foo_queue'), function () {});
125+
$subscriptionConsumer->subscribe($this->createConsumerStub('bar_queue'), function () {});
126+
127+
// guard
128+
$this->assertAttributeCount(2, 'subscribers', $subscriptionConsumer);
129+
130+
$subscriptionConsumer->unsubscribeAll();
131+
132+
$this->assertAttributeCount(0, 'subscribers', $subscriptionConsumer);
133+
}
134+
135+
public function testThrowsIfTryConsumeWithoutSubscribers()
136+
{
137+
$subscriptionConsumer = new RedisSubscriptionConsumer($this->createRedisContextMock());
138+
139+
$this->expectException(\LogicException::class);
140+
$this->expectExceptionMessage('No subscribers');
141+
$subscriptionConsumer->consume();
142+
}
143+
144+
/**
145+
* @return RedisContext|\PHPUnit_Framework_MockObject_MockObject
146+
*/
147+
private function createRedisContextMock()
148+
{
149+
return $this->createMock(RedisContext::class);
150+
}
151+
152+
/**
153+
* @param null|mixed $queueName
154+
*
155+
* @return PsrConsumer|\PHPUnit_Framework_MockObject_MockObject
156+
*/
157+
private function createConsumerStub($queueName = null)
158+
{
159+
$queueMock = $this->createMock(PsrQueue::class);
160+
$queueMock
161+
->expects($this->any())
162+
->method('getQueueName')
163+
->willReturn($queueName);
164+
165+
$consumerMock = $this->createMock(RedisConsumer::class);
166+
$consumerMock
167+
->expects($this->any())
168+
->method('getQueue')
169+
->willReturn($queueMock)
170+
;
171+
172+
return $consumerMock;
173+
}
174+
}

Diff for: pkg/redis/Tests/Spec/RedisConnectionFactoryTest.php

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
namespace Enqueue\Redis\Tests\Spec;
4+
5+
use Enqueue\Redis\RedisConnectionFactory;
6+
use Interop\Queue\Spec\PsrConnectionFactorySpec;
7+
8+
/**
9+
* @group Redis
10+
*/
11+
class RedisConnectionFactoryTest extends PsrConnectionFactorySpec
12+
{
13+
/**
14+
* {@inheritdoc}
15+
*/
16+
protected function createConnectionFactory()
17+
{
18+
return new RedisConnectionFactory();
19+
}
20+
}

Diff for: pkg/redis/Tests/Spec/RedisContextTest.php

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
namespace Enqueue\Redis\Tests\Spec;
4+
5+
use Enqueue\Test\RedisExtension;
6+
use Interop\Queue\Spec\PsrContextSpec;
7+
8+
/**
9+
* @group functional
10+
* @group Redis
11+
*/
12+
class RedisContextTest extends PsrContextSpec
13+
{
14+
use RedisExtension;
15+
16+
/**
17+
* {@inheritdoc}
18+
*/
19+
protected function createContext()
20+
{
21+
return $this->buildPhpRedisContext();
22+
}
23+
}

Diff for: pkg/redis/Tests/Spec/RedisMessageTest.php

+3
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
use Enqueue\Redis\RedisMessage;
66
use Interop\Queue\Spec\PsrMessageSpec;
77

8+
/**
9+
* @group Redis
10+
*/
811
class RedisMessageTest extends PsrMessageSpec
912
{
1013
/**

Diff for: pkg/redis/Tests/Spec/RedisProducerTest.php

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
namespace Enqueue\Redis\Tests\Spec;
4+
5+
use Enqueue\Test\RedisExtension;
6+
use Interop\Queue\Spec\PsrProducerSpec;
7+
8+
/**
9+
* @group functional
10+
* @group Redis
11+
*/
12+
class RedisProducerTest extends PsrProducerSpec
13+
{
14+
use RedisExtension;
15+
16+
/**
17+
* {@inheritdoc}
18+
*/
19+
protected function createProducer()
20+
{
21+
return $this->buildPhpRedisContext()->createProducer();
22+
}
23+
}

Diff for: pkg/redis/Tests/Spec/RedisQueueTest.php

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
namespace Enqueue\Redis\Tests\Spec;
4+
5+
use Enqueue\Redis\RedisDestination;
6+
use Interop\Queue\Spec\PsrQueueSpec;
7+
8+
/**
9+
* @group Redis
10+
*/
11+
class RedisQueueTest extends PsrQueueSpec
12+
{
13+
/**
14+
* {@inheritdoc}
15+
*/
16+
protected function createQueue()
17+
{
18+
return new RedisDestination(self::EXPECTED_QUEUE_NAME);
19+
}
20+
}

0 commit comments

Comments
 (0)