mirror of
https://github.com/predis/predis.git
synced 2026-09-19 14:56:57 +00:00
Push notifications support (#1316)
* Added new push notifications consumer * Added push notifications dispatcher loop, intoduced consumer and dispatcher interfaces * Static analysis fixes * Codestyle fix * Codestyle fixes * Moved RelayConsumer in appropriate directory, fixed access modifiers
This commit is contained in:
committed by
GitHub
parent
f239e989fe
commit
6a1a2c4734
@@ -0,0 +1,479 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Predis package.
|
||||
*
|
||||
* (c) 2009-2020 Daniele Alessandri
|
||||
* (c) 2021-2023 Till Krüss
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Predis\Consumer\PubSub;
|
||||
|
||||
use Predis\Client;
|
||||
use Predis\Consumer\PubSub\Consumer as PubSubConsumer;
|
||||
use PredisTestCase;
|
||||
|
||||
/**
|
||||
* @group realm-pubsub
|
||||
*/
|
||||
class ConsumerTest extends PredisTestCase
|
||||
{
|
||||
/**
|
||||
* @group disconnected
|
||||
*/
|
||||
public function testPubSubConsumerRequirePubSubRelatedCommand(): void
|
||||
{
|
||||
$this->expectException('Predis\NotSupportedException');
|
||||
$this->expectExceptionMessage('PUB/SUB commands are not supported by the current command factory.');
|
||||
|
||||
$commands = $this->getMockBuilder('Predis\Command\FactoryInterface')->getMock();
|
||||
$commands
|
||||
->expects($this->any())
|
||||
->method('supports')
|
||||
->willReturn(false);
|
||||
|
||||
$client = new Client(null, ['commands' => $commands]);
|
||||
|
||||
new PubSubConsumer($client);
|
||||
}
|
||||
|
||||
/**
|
||||
* @group disconnected
|
||||
*/
|
||||
public function testPubSubConsumerDoesNotWorkOnClusters(): void
|
||||
{
|
||||
$this->expectException('Predis\NotSupportedException');
|
||||
$this->expectExceptionMessage('Cannot initialize a PUB/SUB consumer over cluster connections');
|
||||
|
||||
$cluster = $this->getMockBuilder('Predis\Connection\Cluster\ClusterInterface')->getMock();
|
||||
$client = new Client($cluster);
|
||||
|
||||
new PubSubConsumer($client);
|
||||
}
|
||||
|
||||
/**
|
||||
* @group disconnected
|
||||
*/
|
||||
public function testConstructorWithoutSubscriptionsDoesNotStartConsumer(): void
|
||||
{
|
||||
$connection = $this->getMockBuilder('Predis\Connection\NodeConnectionInterface')->getMock();
|
||||
|
||||
/** @var Client */
|
||||
$client = $this->getMockBuilder('Predis\Client')
|
||||
->onlyMethods(['executeCommand'])
|
||||
->setConstructorArgs([$connection])
|
||||
->getMock();
|
||||
|
||||
$client->expects($this->never())
|
||||
->method('executeCommand');
|
||||
|
||||
new PubSubConsumer($client);
|
||||
}
|
||||
|
||||
/**
|
||||
* @group disconnected
|
||||
*/
|
||||
public function testConstructorWithSubscriptionsStartsConsumer(): void
|
||||
{
|
||||
$commands = $this->getCommandFactory();
|
||||
|
||||
$connection = $this->getMockBuilder('Predis\Connection\NodeConnectionInterface')->getMock();
|
||||
$connection->expects($this->exactly(2))->method('writeRequest');
|
||||
|
||||
/** @var Client */
|
||||
$client = $this->getMockBuilder('Predis\Client')
|
||||
->onlyMethods(['createCommand'])
|
||||
->addMethods(['writeRequest'])
|
||||
->setConstructorArgs([$connection])
|
||||
->getMock();
|
||||
$client
|
||||
->expects($this->exactly(2))
|
||||
->method('createCommand')
|
||||
->with($this->logicalOr($this->equalTo('subscribe'), $this->equalTo('psubscribe')))
|
||||
->willReturnCallback(function ($id, $args) use ($commands) {
|
||||
return $commands->create($id, $args);
|
||||
});
|
||||
|
||||
$options = ['subscribe' => 'channel:foo', 'psubscribe' => 'channels:*'];
|
||||
|
||||
new PubSubConsumer($client, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @group disconnected
|
||||
*/
|
||||
public function testStoppingConsumerWithTrueClosesConnection(): void
|
||||
{
|
||||
$connection = $this->getMockBuilder('Predis\Connection\NodeConnectionInterface')->getMock();
|
||||
|
||||
/** @var Client */
|
||||
$client = $this->getMockBuilder('Predis\Client')
|
||||
->onlyMethods(['disconnect'])
|
||||
->setConstructorArgs([$connection])
|
||||
->getMock();
|
||||
$client
|
||||
->expects($this->once())
|
||||
->method('disconnect');
|
||||
|
||||
$pubsub = new PubSubConsumer($client, ['subscribe' => 'channel:foo']);
|
||||
|
||||
$connection->expects($this->never())->method('writeRequest');
|
||||
|
||||
$pubsub->stop(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @group disconnected
|
||||
*/
|
||||
public function testStoppingConsumerWithFalseSendsUnsubscriptions(): void
|
||||
{
|
||||
$commands = $this->getCommandFactory();
|
||||
$classUnsubscribe = $commands->getCommandClass('unsubscribe');
|
||||
$classPunsubscribe = $commands->getCommandClass('punsubscribe');
|
||||
|
||||
$connection = $this->getMockBuilder('Predis\Connection\NodeConnectionInterface')->getMock();
|
||||
|
||||
/** @var Client */
|
||||
$client = $this->getMockBuilder('Predis\Client')
|
||||
->onlyMethods(['disconnect'])
|
||||
->setConstructorArgs([$connection])
|
||||
->getMock();
|
||||
|
||||
$options = ['subscribe' => 'channel:foo', 'psubscribe' => 'channels:*'];
|
||||
$pubsub = new PubSubConsumer($client, $options);
|
||||
|
||||
$connection
|
||||
->expects($this->exactly(2))
|
||||
->method('writeRequest')
|
||||
->with($this->logicalOr(
|
||||
$this->isInstanceOf($classUnsubscribe),
|
||||
$this->isInstanceOf($classPunsubscribe)
|
||||
));
|
||||
|
||||
$pubsub->stop(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @group disconnected
|
||||
*/
|
||||
public function testIsNotValidWhenNotSubscribed(): void
|
||||
{
|
||||
$connection = $this->getMockBuilder('Predis\Connection\NodeConnectionInterface')->getMock();
|
||||
|
||||
/** @var Client */
|
||||
$client = $this->getMockBuilder('Predis\Client')
|
||||
->onlyMethods(['disconnect'])
|
||||
->setConstructorArgs([$connection])
|
||||
->getMock();
|
||||
|
||||
$pubsub = new PubSubConsumer($client);
|
||||
|
||||
$this->assertFalse($pubsub->valid());
|
||||
}
|
||||
|
||||
/**
|
||||
* @group disconnected
|
||||
*/
|
||||
public function testHandlesPongMessages(): void
|
||||
{
|
||||
$rawmessage = ['pong', ''];
|
||||
|
||||
$connection = $this->getMockBuilder('Predis\Connection\NodeConnectionInterface')->getMock();
|
||||
$connection
|
||||
->expects($this->once())
|
||||
->method('read')
|
||||
->willReturn($rawmessage);
|
||||
|
||||
$client = new Client($connection);
|
||||
$pubsub = new PubSubConsumer($client, ['subscribe' => 'channel:foo']);
|
||||
|
||||
$message = $pubsub->current();
|
||||
$this->assertSame('pong', $message->kind);
|
||||
$this->assertSame('', $message->payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* @group disconnected
|
||||
*/
|
||||
public function testHandlesPongMessagesWithPayload(): void
|
||||
{
|
||||
$rawmessage = ['pong', 'foobar'];
|
||||
|
||||
$connection = $this->getMockBuilder('Predis\Connection\NodeConnectionInterface')->getMock();
|
||||
$connection
|
||||
->expects($this->once())
|
||||
->method('read')
|
||||
->willReturn($rawmessage);
|
||||
|
||||
$client = new Client($connection);
|
||||
$pubsub = new PubSubConsumer($client, ['subscribe' => 'channel:foo']);
|
||||
|
||||
$message = $pubsub->current();
|
||||
$this->assertSame('pong', $message->kind);
|
||||
$this->assertSame('foobar', $message->payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* @group disconnected
|
||||
*/
|
||||
public function testReadsMessageFromConnection(): void
|
||||
{
|
||||
$rawmessage = ['message', 'channel:foo', 'message from channel'];
|
||||
|
||||
$connection = $this->getMockBuilder('Predis\Connection\NodeConnectionInterface')->getMock();
|
||||
$connection
|
||||
->expects($this->once())
|
||||
->method('read')
|
||||
->willReturn($rawmessage);
|
||||
|
||||
$client = new Client($connection);
|
||||
$pubsub = new PubSubConsumer($client, ['subscribe' => 'channel:foo']);
|
||||
|
||||
$message = $pubsub->current();
|
||||
$this->assertSame('message', $message->kind);
|
||||
$this->assertSame('channel:foo', $message->channel);
|
||||
$this->assertSame('message from channel', $message->payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* @group disconnected
|
||||
*/
|
||||
public function testReadsPmessageFromConnection(): void
|
||||
{
|
||||
$rawmessage = ['pmessage', 'channel:*', 'channel:foo', 'message from channel'];
|
||||
|
||||
$connection = $this->getMockBuilder('Predis\Connection\NodeConnectionInterface')->getMock();
|
||||
$connection
|
||||
->expects($this->once())
|
||||
->method('read')
|
||||
->willReturn($rawmessage);
|
||||
|
||||
$client = new Client($connection);
|
||||
$pubsub = new PubSubConsumer($client, ['psubscribe' => 'channel:*']);
|
||||
|
||||
$message = $pubsub->current();
|
||||
$this->assertSame('pmessage', $message->kind);
|
||||
$this->assertSame('channel:*', $message->pattern);
|
||||
$this->assertSame('channel:foo', $message->channel);
|
||||
$this->assertSame('message from channel', $message->payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* @group disconnected
|
||||
*/
|
||||
public function testReadsSubscriptionMessageFromConnection(): void
|
||||
{
|
||||
$rawmessage = ['subscribe', 'channel:foo', 1];
|
||||
|
||||
$connection = $this->getMockBuilder('Predis\Connection\NodeConnectionInterface')->getMock();
|
||||
$connection
|
||||
->expects($this->once())
|
||||
->method('read')
|
||||
->willReturn($rawmessage);
|
||||
|
||||
$client = new Client($connection);
|
||||
$pubsub = new PubSubConsumer($client, ['subscribe' => 'channel:foo']);
|
||||
|
||||
$message = $pubsub->current();
|
||||
$this->assertSame('subscribe', $message->kind);
|
||||
$this->assertSame('channel:foo', $message->channel);
|
||||
$this->assertSame(1, $message->payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* @group disconnected
|
||||
*/
|
||||
public function testReadsUnsubscriptionMessageFromConnection(): void
|
||||
{
|
||||
$rawmessage = ['unsubscribe', 'channel:foo', 1];
|
||||
|
||||
$connection = $this->getMockBuilder('Predis\Connection\NodeConnectionInterface')->getMock();
|
||||
$connection
|
||||
->expects($this->once())
|
||||
->method('read')
|
||||
->willReturn($rawmessage);
|
||||
|
||||
$client = new Client($connection);
|
||||
$pubsub = new PubSubConsumer($client, ['subscribe' => 'channel:foo']);
|
||||
|
||||
$message = $pubsub->current();
|
||||
$this->assertSame('unsubscribe', $message->kind);
|
||||
$this->assertSame('channel:foo', $message->channel);
|
||||
$this->assertSame(1, $message->payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* @group disconnected
|
||||
*/
|
||||
public function testUnsubscriptionMessageWithZeroChannelCountInvalidatesConsumer(): void
|
||||
{
|
||||
$rawmessage = ['unsubscribe', 'channel:foo', 0];
|
||||
|
||||
$connection = $this->getMockBuilder('Predis\Connection\NodeConnectionInterface')->getMock();
|
||||
$connection
|
||||
->expects($this->once())
|
||||
->method('read')
|
||||
->willReturn($rawmessage);
|
||||
|
||||
$client = new Client($connection);
|
||||
$pubsub = new PubSubConsumer($client, ['subscribe' => 'channel:foo']);
|
||||
|
||||
$this->assertTrue($pubsub->valid());
|
||||
|
||||
$message = $pubsub->current();
|
||||
$this->assertSame('unsubscribe', $message->kind);
|
||||
$this->assertSame('channel:foo', $message->channel);
|
||||
$this->assertSame(0, $message->payload);
|
||||
|
||||
$this->assertFalse($pubsub->valid());
|
||||
}
|
||||
|
||||
/**
|
||||
* @group disconnected
|
||||
*/
|
||||
public function testGetUnderlyingClientInstance(): void
|
||||
{
|
||||
$connection = $this->getMockBuilder('Predis\Connection\NodeConnectionInterface')->getMock();
|
||||
|
||||
$client = new Client($connection);
|
||||
$pubsub = new PubSubConsumer($client);
|
||||
|
||||
$this->assertSame($client, $pubsub->getClient());
|
||||
}
|
||||
|
||||
// ******************************************************************** //
|
||||
// ---- INTEGRATION TESTS --------------------------------------------- //
|
||||
// ******************************************************************** //
|
||||
|
||||
// NOTE: the following 2 tests fail at random without any apparent reason
|
||||
// when executed on our CI environments and these failures are not tied
|
||||
// to a particular version of PHP or Redis. It is most likely some weird
|
||||
// timing issue on busy systems as it is really rare to get it triggered
|
||||
// locally. The chances it is a bug in the library are pretty low so for
|
||||
// now we just mark this test skipped on our CI environments (but still
|
||||
// enabled for local test runs) and "debug" this issue using a separate
|
||||
// branch to avoid having spurious failures on main development branches
|
||||
// which is utterly annoying.
|
||||
|
||||
/**
|
||||
* @group connected
|
||||
* @requiresRedisVersion >= 2.0.0
|
||||
*/
|
||||
public function testPubSubAgainstRedisServer(): void
|
||||
{
|
||||
$this->markTestSkippedOnCIEnvironment(
|
||||
'Test temporarily skipped on CI environments, see note in the body of the test' // TODO
|
||||
);
|
||||
|
||||
$parameters = [
|
||||
'host' => constant('REDIS_SERVER_HOST'),
|
||||
'port' => constant('REDIS_SERVER_PORT'),
|
||||
'database' => constant('REDIS_SERVER_DBNUM'),
|
||||
// Prevents suite from handing on broken test
|
||||
'read_write_timeout' => 2,
|
||||
];
|
||||
|
||||
$messages = [];
|
||||
|
||||
$producer = new Client($parameters);
|
||||
$producer->connect();
|
||||
|
||||
$consumer = new Client($parameters);
|
||||
$consumer->connect();
|
||||
|
||||
$pubsub = new PubSubConsumer($consumer);
|
||||
$pubsub->subscribe('channel:foo');
|
||||
|
||||
$producer->publish('channel:foo', 'message1');
|
||||
$producer->publish('channel:foo', 'message2');
|
||||
$producer->publish('channel:foo', 'QUIT');
|
||||
|
||||
foreach ($pubsub as $message) {
|
||||
if ($message->kind !== 'message') {
|
||||
continue;
|
||||
}
|
||||
$messages[] = ($payload = $message->payload);
|
||||
if ($payload === 'QUIT') {
|
||||
$pubsub->stop();
|
||||
}
|
||||
}
|
||||
|
||||
$this->assertSame(['message1', 'message2', 'QUIT'], $messages);
|
||||
$this->assertFalse($pubsub->valid());
|
||||
$this->assertEquals('ECHO', $consumer->echo('ECHO'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @group connected
|
||||
* @requiresRedisVersion >= 2.0.0
|
||||
* @requires extension pcntl
|
||||
*/
|
||||
public function testPubSubAgainstRedisServerBlocking(): void
|
||||
{
|
||||
$this->markTestSkippedOnCIEnvironment(
|
||||
'Test temporarily skipped on CI environments, see note in the body of the test' // TODO
|
||||
);
|
||||
|
||||
$parameters = [
|
||||
'host' => constant('REDIS_SERVER_HOST'),
|
||||
'port' => constant('REDIS_SERVER_PORT'),
|
||||
'database' => constant('REDIS_SERVER_DBNUM'),
|
||||
'read_write_timeout' => -1, // -1 to set blocking reads
|
||||
];
|
||||
|
||||
// create consumer before forking so the child can disconnect it
|
||||
$consumer = new Client($parameters);
|
||||
$consumer->connect();
|
||||
|
||||
/*
|
||||
* fork
|
||||
* parent: consumer
|
||||
* child: producer
|
||||
*/
|
||||
if ($childPID = pcntl_fork()) {
|
||||
$messages = [];
|
||||
|
||||
$pubsub = new PubSubConsumer($consumer);
|
||||
$pubsub->subscribe('channel:foo');
|
||||
|
||||
foreach ($pubsub as $message) {
|
||||
if ($message->kind !== 'message') {
|
||||
continue;
|
||||
}
|
||||
$messages[] = ($payload = $message->payload);
|
||||
if ($payload === 'QUIT') {
|
||||
$pubsub->stop();
|
||||
}
|
||||
}
|
||||
|
||||
$this->assertSame(['message1', 'message2', 'QUIT'], $messages);
|
||||
$this->assertFalse($pubsub->valid());
|
||||
$this->assertEquals('ECHO', $consumer->echo('ECHO'));
|
||||
|
||||
// kill the child
|
||||
posix_kill($childPID, SIGKILL);
|
||||
} else {
|
||||
// create producer, read_write_timeout = 2 because it doesn't do blocking reads anyway
|
||||
$producer = new Client(array_replace($parameters, ['read_write_timeout' => 2]));
|
||||
$producer->connect();
|
||||
|
||||
$producer->publish('channel:foo', 'message1');
|
||||
$producer->publish('channel:foo', 'message2');
|
||||
|
||||
$producer->publish('channel:foo', 'QUIT');
|
||||
|
||||
// sleep, giving the consumer a chance to respond to the QUIT message
|
||||
sleep(1);
|
||||
|
||||
// disconnect the consumer because otherwise it could remain stuck in blocking read
|
||||
// if it failed to respond to the QUIT message
|
||||
$consumer->disconnect();
|
||||
|
||||
// exit child
|
||||
exit(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user