Skip to content

Implementing PhpRedis client #36

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 18 commits into
base: 2.x
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -125,6 +125,7 @@ before_install:
- if [ -n "$LUMEN_VERSION" ]; then composer remove --dev --no-update "laravel/framework"; fi
- if [ -n "$LUMEN_VERSION" ]; then composer require --no-update "laravel/lumen-framework:$LUMEN_VERSION"; fi
- if [ -z "$HORIZON" ]; then composer remove --dev --no-update "laravel/horizon"; fi
- if [[ ${TRAVIS_PHP_VERSION:0:1} == "7" ]]; then printf "\n" | pecl install -f redis; fi

install: travis_retry composer install --prefer-dist --no-interaction --no-suggest

30 changes: 29 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -59,10 +59,13 @@ Requirements

- PHP 5.4 or greater
- [Redis][redis] 2.8 or greater (for Sentinel support)
- [Predis][predis] 1.1 or greater (for Sentinel client support)
- [Laravel][laravel] or [Lumen][lumen] 5.0 or greater (4.x doesn't support the
required Predis version)

Driver options:
- [Predis][predis] 1.1 or greater (for Sentinel client support)
- [PhpRedis][php-redis] 5.3.4 or greater (for Sentinel client support)

**Note:** Laravel 5.4 introduced the ability to use the [PhpRedis][php-redis]
extension as a Redis client for the framework. This package does not yet
support the PhpRedis option.
@@ -132,6 +135,7 @@ QUEUE_CONNECTION=redis-sentinel # Laravel >= 5.7
QUEUE_DRIVER=redis-sentinel # Laravel <= 5.6
REDIS_DRIVER=redis-sentinel

REDIS_CLIENT=predis
REDIS_HOST=sentinel1.example.com, sentinel2.example.com, 10.0.0.1, etc.
REDIS_PORT=26379
REDIS_SENTINEL_SERVICE=mymaster # or your Redis master group name
@@ -409,6 +413,30 @@ for a single connection. The default values are shown below:
],
```

The PhpRedis client supports extra options. The default values are shown below:

```php
'options' => [
...

// The default number of attempts to retry the connection if the inititial
// connection has failed. A value of 0 instructs the
// client to throw an exception after the first failed attempt, while a
// value of -1 causes the client to continue to retry commands indefinitely.
'connector_retry_limit' => 20,

// The default amount of time (in milliseconds) that the client waits before
// retrying the connection attempt.
'connector_retry_wait' => 1000,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do these configuration paramters play together with retry_limit and retry_wait? Would it be possible to allow the PhpRedisConnection to reuse the connector logic (and retry configuration) of PhpRedisConnector?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These settings allow for a different configuration on first connection. So the normal retry_limit and retry_wait are used for reconnecting after a failure. The connector_ options are used when creating the connection for the first time.

I've combined some of the retry logic, but I can't seem to find a way to simplify this. Maybe you have some ideas about it?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the settings are fine, just a bit hard to understand in combination with the other ones (which have quite similar names too). Explaining their difference from the similar named settings is probably all you can do at this point.


// Sets the persistent option in the RedisSentinel class.
'sentinel_persistent' => null,

// Sets the read timeout option in the RedisSentinel class. 0 means unlimited.
'sentinel_read_timeout' => 0,
],
```

### Broadcasting, Cache, Session, and Queue Drivers

After configuring the Sentinel database connections, we can instruct Laravel to
27 changes: 16 additions & 11 deletions composer.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
{
"name": "monospice/laravel-redis-sentinel-drivers",
"description": "Redis Sentinel integration for Laravel and Lumen.",
"keywords": ["redis", "sentinel", "laravel", "lumen"],
"keywords": [
"redis",
"sentinel",
"laravel",
"lumen"
],
"type": "library",
"license": "MIT",
"authors": [
@@ -12,21 +17,21 @@
],
"require": {
"php": ">=5.6.4",
"illuminate/cache": "^5.4 || ^6.0 || ^7.0 || ^8.0",
"illuminate/contracts": "^5.4 || ^6.0 || ^7.0 || ^8.0",
"illuminate/queue": "^5.4 || ^6.0 || ^7.0 || ^8.0",
"illuminate/redis": "^5.4 || ^6.0 || ^7.0 || ^8.0",
"illuminate/session": "^5.4 || ^6.0 || ^7.0 || ^8.0",
"illuminate/support": "^5.4 || ^6.0 || ^7.0 || ^8.0",
"illuminate/cache": "^5.4 || ^6.0 || ^7.0 || ^8.0 || ^9.0",
"illuminate/contracts": "^5.4 || ^6.0 || ^7.0 || ^8.0 || ^9.0",
"illuminate/queue": "^5.4 || ^6.0 || ^7.0 || ^8.0 || ^9.0",
"illuminate/redis": "^5.4 || ^6.0 || ^7.0 || ^8.0 || ^9.0",
"illuminate/session": "^5.4 || ^6.0 || ^7.0 || ^8.0 || ^9.0",
"illuminate/support": "^5.4 || ^6.0 || ^7.0 || ^8.0 || ^9.0",
"monospice/spicy-identifiers": "^0.1",
"predis/predis": "^1.1"
},
"require-dev": {
"laravel/framework": "^5.4 || ^6.0 || ^7.0 || ^8.0",
"laravel/horizon": "^1.0 || ^2.0 || ^3.0 || ^4.0",
"laravel/lumen-framework": "^5.4 || ^6.0 || ^7.0 || ^8.0",
"laravel/framework": "^5.4 || ^6.0 || ^7.0 || ^8.0 || ^9.0",
"laravel/horizon": "^1.0 || ^2.0 || ^3.0 || ^4.0 || ^5.8",
"laravel/lumen-framework": "^5.4 || ^6.0 || ^7.0 || ^8.0 || ^9.0",
"mockery/mockery": "^1.3",
"phpunit/phpunit": "^5.0"
"phpunit/phpunit": "^5.0 || ^9.5.10"
},
"autoload": {
"psr-4": {
2 changes: 1 addition & 1 deletion src/Configuration/HostNormalizer.php
Original file line number Diff line number Diff line change
@@ -62,7 +62,7 @@ class HostNormalizer
public static function normalizeConnections(array $connections)
{
foreach ($connections as $name => $connection) {
if ($name === 'options' || $name === 'clusters') {
if (in_array($name, ['client', 'options', 'clusters'])) {
continue;
}

227 changes: 227 additions & 0 deletions src/Connections/PhpRedisConnection.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
<?php

namespace Monospice\LaravelRedisSentinel\Connections;

use Closure;
use Illuminate\Redis\Connections\PhpRedisConnection as LaravelPhpRedisConnection;
use Monospice\LaravelRedisSentinel\Connectors\PhpRedisConnector;
use Redis;
use RedisException;

/**
* Executes Redis commands using the PhpRedis client.
*
* This package extends Laravel's PhpRedisConnection class to wrap all command
* methods with a retryOnFailure method.
*
* @category Package
* @package Monospice\LaravelRedisSentinel
* @author Jeffrey Zant <j.zant@slash2.nl>
* @license See LICENSE file
* @link https://github.com/monospice/laravel-redis-sentinel-drivers
*/
class PhpRedisConnection extends LaravelPhpRedisConnection
{
/**
* The connection creation callback.
*
* Laravel 5 does not store the connector by default.
*
* @var callable|null
*/
protected $connector;

/**
* The number of times the client attempts to retry a command when it fails
* to connect to a Redis instance behind Sentinel.
*
* @var int
*/
protected $retryLimit = 20;

/**
* The time in milliseconds to wait before the client retries a failed
* command.
*
* @var int
*/
protected $retryWait = 1000;

/**
* Create a new PhpRedis connection.
*
* @param \Redis $client
* @param callable|null $connector
* @param array $sentinelOptions
* @return void
*/
public function __construct($client, callable $connector = null, array $sentinelOptions = [])
{
parent::__construct($client, $connector);

// Set the connector when it is not set. Used for Laravel 5.
if (! $this->connector) {
$this->connector = $connector;
}

// Set the retry limit.
if (isset($sentinelOptions['retry_limit']) && is_numeric($sentinelOptions['retry_limit'])) {
$this->retryLimit = (int) $sentinelOptions['retry_limit'];
}

// Set the retry wait.
if (isset($sentinelOptions['retry_wait']) && is_numeric($sentinelOptions['retry_wait'])) {
$this->retryWait = (int) $sentinelOptions['retry_wait'];
}
}

/**
* {@inheritdoc} in addition retry on client failure.
*
* @param mixed $cursor
* @param array $options
* @return mixed
*/
public function scan($cursor, $options = [])
{
return $this->retryOnFailure(function () use ($cursor, $options) {
return parent::scan($cursor, $options);
});
}

/**
* {@inheritdoc} in addition retry on client failure.
*
* @param string $key
* @param mixed $cursor
* @param array $options
* @return mixed
*/
public function zscan($key, $cursor, $options = [])
{
return $this->retryOnFailure(function () use ($key, $cursor, $options) {
parent::zscan($key, $cursor, $options);
});
}

/**
* {@inheritdoc} in addition retry on client failure.
*
* @param string $key
* @param mixed $cursor
* @param array $options
* @return mixed
*/
public function hscan($key, $cursor, $options = [])
{
return $this->retryOnFailure(function () use ($key, $cursor, $options) {
parent::hscan($key, $cursor, $options);
});
}

/**
* {@inheritdoc} in addition retry on client failure.
*
* @param string $key
* @param mixed $cursor
* @param array $options
* @return mixed
*/
public function sscan($key, $cursor, $options = [])
{
return $this->retryOnFailure(function () use ($key, $cursor, $options) {
parent::sscan($key, $cursor, $options);
});
}

/**
* {@inheritdoc} in addition retry on client failure.
*
* @param callable|null $callback
* @return \Redis|array
*/
public function pipeline(callable $callback = null)
{
return $this->retryOnFailure(function () use ($callback) {
return parent::pipeline($callback);
});
}

/**
* {@inheritdoc} in addition retry on client failure.
*
* @param callable|null $callback
* @return \Redis|array
*/
public function transaction(callable $callback = null)
{
return $this->retryOnFailure(function () use ($callback) {
return parent::transaction($callback);
});
}

/**
* {@inheritdoc} in addition retry on client failure.
*
* @param array|string $channels
* @param \Closure $callback
* @return void
*/
public function subscribe($channels, Closure $callback)
{
return $this->retryOnFailure(function () use ($channels, $callback) {
return parent::subscribe($channels, $callback);
});
}

/**
* {@inheritdoc} in addition retry on client failure.
*
* @param array|string $channels
* @param \Closure $callback
* @return void
*/
public function psubscribe($channels, Closure $callback)
{
return $this->retryOnFailure(function () use ($channels, $callback) {
return parent::psubscribe($channels, $callback);
});
}

/**
* {@inheritdoc} in addition retry on client failure.
*
* @param string $method
* @param array $parameters
* @return mixed
*/
public function command($method, array $parameters = [])
{
return $this->retryOnFailure(function () use ($method, $parameters) {
return parent::command($method, $parameters);
});
}

/**
* Attempt to retry the provided operation when the client fails to connect
* to a Redis server.
*
* @param callable $callback The operation to execute.
* @return mixed The result of the first successful attempt.
*/
protected function retryOnFailure(callable $callback)
{
return PhpRedisConnector::retryOnFailure($callback, $this->retryLimit, $this->retryWait, function () {
$this->client->close();

try {
if ($this->connector) {
$this->client = call_user_func($this->connector);
}
} catch (RedisException $e) {
// Ignore when the creation of a new client gets an exception.
// If this exception isn't caught the retry will stop.
}
});
}
}
279 changes: 279 additions & 0 deletions src/Connectors/PhpRedisConnector.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,279 @@
<?php

namespace Monospice\LaravelRedisSentinel\Connectors;

use Illuminate\Support\Arr;
use Illuminate\Redis\Connectors\PhpRedisConnector as LaravelPhpRedisConnector;
use LogicException;
use Monospice\LaravelRedisSentinel\Connections\PhpRedisConnection;
use Monospice\LaravelRedisSentinel\Exceptions\RedisRetryException;
use Redis;
use RedisSentinel;
use RedisException;

/**
* Initializes PhpRedis Client instances for Redis Sentinel connections
*
* @category Package
* @package Monospice\LaravelRedisSentinel
* @author Jeffrey Zant <j.zant@slash2.nl>
* @license See LICENSE file
* @link http://github.com/monospice/laravel-redis-sentinel-drivers
*/
class PhpRedisConnector extends LaravelPhpRedisConnector
{
/**
* Holds the current sentinel servers.
*
* @var array
*/
protected $servers;

/**
* The number of times the client attempts to retry a command when it fails
* to connect to a Redis instance behind Sentinel.
*
* @var int
*/
protected $connectorRetryLimit = 20;

/**
* The time in milliseconds to wait before the client retries a failed
* command.
*
* @var int
*/
protected $connectorRetryWait = 1000;

/**
* Configuration options specific to Sentinel connection operation
*
* Some of the Sentinel configuration options can be entered in this class.
* The retry_wait and retry_limit values are passed to the connection.
*
* @var array
*/
protected $sentinelKeys = [
'sentinel_timeout' => null,
'retry_wait' => null,
'retry_limit' => null,
'update_sentinels' => null,

'sentinel_persistent' => null,
'sentinel_read_timeout' => null,
];

/**
* Instantiate the connector and check if the required extension is available.
*/
public function __construct()
{
if (! extension_loaded('redis')) {
throw new LogicException('Please make sure the PHP Redis extension is installed and enabled.');
}

if (! class_exists(RedisSentinel::class)) {
throw new LogicException('Please make sure the PHP Redis extension is up to date (5.3.4 or greater).');
}
}

/**
* Create a new Redis Sentinel connection from the provided configuration
* options
*
* @param array $server The client configuration for the connection
* @param array $options The global client options shared by all Sentinel
* connections
*
* @return PhpRedisConnection The Sentinel connection containing a configured
* PhpRedis Client
*/
public function connect(array $servers, array $options = [ ])
{
// Set the initial Sentinel servers.
$this->servers = array_map(function ($server) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should extract the options key before setting the servers or it can result trying to connect to the wrong hostname here: https://github.com/monospice/laravel-redis-sentinel-drivers/pull/36/files#diff-3bf85f59585710cebd9ae7787d8b7ffb7df273ce8ffe2e72053ef96b4bb81ccaR157

return $this->formatServer($server);
}, $servers);

// Set the connector retry limit.
if (isset($options['connector_retry_limit']) && is_numeric($options['connector_retry_limit'])) {
$this->connectorRetryLimit = (int) $options['connector_retry_limit'];
}

// Set the connector retry wait.
if (isset($options['connector_retry_wait']) && is_numeric($options['connector_retry_wait'])) {
$this->connectorRetryWait = (int) $options['connector_retry_wait'];
}

// Merge the global options shared by all Sentinel connections with
// connection-specific options
$clientOpts = array_merge($options, Arr::pull($servers, 'options', [ ]));

// Extract the array of Sentinel connection options from the rest of
// the client options
$sentinelOpts = array_intersect_key($clientOpts, $this->sentinelKeys);

// Filter the Sentinel connection options elements from the client
// options array
$clientOpts = array_diff_key($clientOpts, $this->sentinelKeys);

// Create a client by calling the Sentinel servers
$connector = function () use ($options) {
return $this->createClientWithSentinel($options);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you are using $options instead of $clientOpts

};

// Create a connection and retry if this fails.
$connection = self::retryOnFailure(function () use ($connector) {
return $connector();
}, $this->connectorRetryLimit, $this->connectorRetryWait);

return new PhpRedisConnection($connection, $connector, $sentinelOpts);
}

/**
* Create the Redis client instance
*
* @param array $options
* @return Redis
*
* @throws LogicException
*/
protected function createClientWithSentinel(array $options)
{
$serverConfigurations = $this->servers;
$clientConfiguration = isset($options['parameters']) ? $options['parameters'] : [];

$updateSentinels = isset($options['update_sentinels']) ? $options['update_sentinels'] : false;
$sentinelService = isset($options['service']) ? $options['service'] : 'mymaster';
$sentinelTimeout = isset($options['sentinel_timeout']) ? $options['sentinel_timeout'] : 0;
$sentinelPersistent = isset($options['sentinel_persistent']) ? $options['sentinel_persistent'] : null;
$sentinelReadTimeout = isset($options['sentinel_read_timeout']) ? $options['sentinel_read_timeout'] : 0;

// Shuffle the server configurations to perform some loadbalancing.
shuffle($serverConfigurations);

// Try to connect to any of the servers.
foreach ($serverConfigurations as $idx => $serverConfiguration) {
$host = isset($serverConfiguration['host']) ? $serverConfiguration['host'] : 'localhost';
$port = isset($serverConfiguration['port']) ? $serverConfiguration['port'] : 26379;

// Create a connection to the Sentinel instance. Using a retry_interval of 0, retrying
// is done inside the PhpRedisConnection. Cannot seem to get the retry_interval to work:
// https://github.com/phpredis/phpredis/blob/37a90257d09b4efa75230769cf535484116b2b67/library.c#L343
$sentinel = new RedisSentinel($host, $port, $sentinelTimeout, $sentinelPersistent, 0, $sentinelReadTimeout);

try {
// Check if the Sentinel server list needs to be updated.
// Put the current server and the other sentinels in the server list.
if ($updateSentinels === true) {
$this->updateSentinels($sentinel, $host, $port, $sentinelService);
}

// Lookup the master node.
$master = $sentinel->getMasterAddrByName($sentinelService);
if (is_array($master) && ! count($master)) {
throw new RedisException(sprintf('No master found for service "%s".', $sentinelService));
}

// Create a PhpRedis client for the selected master node.
return $this->createClient(array_merge($clientConfiguration, $serverConfiguration, [
'host' => $master[0],
'port' => $master[1],
]));
} catch (RedisException $e) {
// Rethrow the expection when the last server is reached.
if ($idx === count($serverConfigurations) - 1) {
throw $e;
}
}
}
}

/**
* Update the list With sentinel servers.
*
* @param RedisSentinel $sentinel
* @param string $currentHost
* @param int $currentPort
* @param string $service
* @return void
*/
protected function updateSentinels(RedisSentinel $sentinel, string $currentHost, int $currentPort, string $service)
{
$this->servers = array_merge(
[
[
'host' => $currentHost,
'port' => $currentPort,
]
], array_map(function ($sentinel) {
return [
'host' => $sentinel['ip'],
'port' => $sentinel['port'],
];
}, $sentinel->sentinels($service))
);
}

/**
* Format a server.
*
* @param mixed $server
* @return array
*
* @throws RedisException
*/
protected function formatServer($server)
{
if (is_string($server)) {
list($host, $port) = explode(':', $server);
if (! $host || ! $port) {
throw new RedisException('Could not format the server definition.');
}

return ['host' => $host, 'port' => (int) $port];
}

if (! is_array($server)) {
throw new RedisException('Could not format the server definition.');
}

return $server;
}

/**
* Retry the callback when a RedisException is catched.
*
* @param callable $callback The operation to execute.
* @param int $retryLimit The number of times the retry is performed.
* @param int $retryWait The time in milliseconds to wait before retrying again.
* @param callable $failureCallback The operation to execute when a failure happens.
* @return mixed The result of the first successful attempt.
*
* @throws RedisRetryException After exhausting the allowed number of
* attempts to connect.
*/
public static function retryOnFailure(callable $callback, int $retryLimit, int $retryWait, callable $failureCallback = null)
{
$attempts = 0;
$previousException = null;

while ($attempts < $retryLimit) {
try {
return $callback();
} catch (RedisException $exception) {
$previousException = $exception;

if ($failureCallback) {
call_user_func($failureCallback);
}

usleep($retryWait * 1000);

$attempts++;
}
}

throw new RedisRetryException(sprintf('Reached the (re)connect limit of %d attempts', $attempts), 0, $previousException);
}
}
10 changes: 10 additions & 0 deletions src/Exceptions/RedisRetryException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

namespace Monospice\LaravelRedisSentinel\Exceptions;

use RedisException as PhpRedisException;

class RedisRetryException extends PhpRedisException
{
//
}
2 changes: 2 additions & 0 deletions src/Manager/VersionedRedisSentinelManager.php
Original file line number Diff line number Diff line change
@@ -70,6 +70,8 @@ protected function connector()
switch ($this->driver) {
case 'predis':
return new Connectors\PredisConnector();
case 'phpredis':
return new Connectors\PhpRedisConnector();
}

throw new InvalidArgumentException(
178 changes: 178 additions & 0 deletions tests/Integration/Connections/PhpRedisConnectionTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
<?php

namespace Monospice\LaravelRedisSentinel\Tests\Integration\Connections;

use Monospice\LaravelRedisSentinel\Connections\PhpRedisConnection;
use Monospice\LaravelRedisSentinel\Connectors\PhpRedisConnector;
use Monospice\LaravelRedisSentinel\Tests\Support\DummyException;
use Monospice\LaravelRedisSentinel\Tests\Support\IntegrationTestCase;
use Monospice\LaravelRedisSentinel\Exceptions\RedisRetryException;
use Redis;
use RedisException;

class PhpRedisConnectionTest extends IntegrationTestCase
{
/**
* The instance of the PhpRedis client wrapper under test.
*
* @var PhpRedisConnection
*/
protected $subject;

/**
* Run this setup before each test
*
* @return void
*/
public function setUp()
{
parent::setUp();

if (! extension_loaded('redis')) {
return;
}

$this->subject = $this->makeConnection();
}

public function testAllowsTransactionsOnAggregateConnection()
{
if (! extension_loaded('redis')) {
$this->markTestSkipped('The redis extension is not installed. Please install the extension to enable '.__CLASS__);

return;
}

$transaction = $this->subject->transaction();

$this->assertInstanceOf(Redis::class, $transaction);
}

public function testExecutesCommandsInTransaction()
{
if (! extension_loaded('redis')) {
$this->markTestSkipped('The redis extension is not installed. Please install the extension to enable '.__CLASS__);

return;
}

$result = $this->subject->transaction(function ($trans) {
$trans->set('test-key', 'test value');
$trans->get('test-key');
});

$this->assertCount(2, $result);
$this->assertTrue($result[0]);
$this->assertEquals('test value', $result[1]);
$this->assertRedisKeyEquals('test-key', 'test value');
}

public function testExecutesTransactionsOnMaster()
{
if (! extension_loaded('redis')) {
$this->markTestSkipped('The redis extension is not installed. Please install the extension to enable '.__CLASS__);

return;
}

$expectedSubset = ['role' => 'master'];

$info = $this->subject->transaction(function ($transaction) {
$transaction->info();
});

$this->assertArraySubset($expectedSubset, $info[0]);
}

public function testAbortsTransactionOnException()
{
if (! extension_loaded('redis')) {
$this->markTestSkipped('The redis extension is not installed. Please install the extension to enable '.__CLASS__);

return;
}

$exception = null;

try {
$this->subject->transaction(function ($trans) {
$trans->set('test-key', 'test value');
throw new DummyException();
});
} catch (DummyException $exception) {
// With PHPUnit, we need to wrap the throwing block to perform
// assertions afterward.
}

$this->assertNotNull($exception);
$this->assertRedisKeyEquals('test-key', null);
}

public function testRetriesTransactionWhenConnectionFails()
{
if (! extension_loaded('redis')) {
$this->markTestSkipped('The redis extension is not installed. Please install the extension to enable '.__CLASS__);

return;
}

$this->expectException(RedisRetryException::class);

$this->subject = $this->makeConnection(1, 0); // retry once and immediately

$this->subject->transaction(function () {
throw new RedisException();
});
}

public function testCanReconnectWhenConnectionFails()
{
if (! extension_loaded('redis')) {
$this->markTestSkipped('The redis extension is not installed. Please install the extension to enable '.__CLASS__);

return;
}

$retries = 3;
$attempts = 0;

$this->subject = $this->makeConnection($retries, 0); // retry immediately

$this->subject->transaction(function ($trans) use (&$attempts, $retries) {
$attempts++;

if ($attempts < $retries) {
throw new RedisException();
} else {
$trans->set('test-key', 'test value');
}
});

$this->assertGreaterThan(1, $attempts, 'First try does not count.');
$this->assertRedisKeyEquals('test-key', 'test value');
}

/**
* Initialize a PhpRedis client using the test connection configuration
* that can verify connectivity failure handling.
*
* @param int|null $retryLimit
* @param int|null $retryWait
* @return PhpRedisConnection A client instance for the subject under test.
*/
protected function makeConnection(int $retryLimit = null, int $retryWait = null)
{
$connector = new PhpRedisConnector();

$options = $this->config['options'];
if ($retryLimit !== null) {
$options['retry_limit'] = $retryLimit;
}

if ($retryWait !== null) {
$options['retry_wait'] = $retryWait;
}

return $connector->connect($this->config['default'], $options);
}
}
67 changes: 67 additions & 0 deletions tests/Integration/Connectors/PhpRedisConnectorTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?php

namespace Monospice\LaravelRedisSentinel\Tests\Integration\Connections;

use Monospice\LaravelRedisSentinel\Connections\PhpRedisConnection;
use Monospice\LaravelRedisSentinel\Connectors\PhpRedisConnector;
use Monospice\LaravelRedisSentinel\Tests\Support\IntegrationTestCase;
use Monospice\LaravelRedisSentinel\Exceptions\RedisRetryException;

class PhpRedisConnectorTest extends IntegrationTestCase
{
/**
* Run this setup before each test
*
* @return void
*/
public function setUp()
{
parent::setUp();

if (! extension_loaded('redis')) {
return;
}
}

public function testCanConnect()
{
if (! extension_loaded('redis')) {
$this->markTestSkipped('The redis extension is not installed. Please install the extension to enable '.__CLASS__);

return;
}

$connector = new PhpRedisConnector();
$client = $connector->connect($this->config['default'], $this->config['options']);

$this->assertInstanceOf(PhpRedisConnection::class, $client);
}

public function testRetriesTransactionWhenConnectionFails()
{
if (! extension_loaded('redis')) {
$this->markTestSkipped('The redis extension is not installed. Please install the extension to enable '.__CLASS__);

return;
}

$this->expectException(RedisRetryException::class);

$connector = new PhpRedisConnector();

$servers = [
[
'host' => '127.0.0.1',
'port' => 1111,
],
];

$options = array_merge([
'connector_retry_limit' => 3,
'connector_retry_wait' => 0,
]);

$connector = new PhpRedisConnector();
$connector->connect($servers, $options);
}
}
4 changes: 2 additions & 2 deletions tests/Unit/Configuration/LoaderTest.php
Original file line number Diff line number Diff line change
@@ -499,9 +499,9 @@ public function testSkipsLoadingHorizonConfigurationIfCached()

public function testDisallowsUsingNonexistantConnectionForHorizon()
{
$this->config->set('horizon.use', 'not-a-connection');
$this->expectException(UnexpectedValueException::class);

$this->setExpectedException(UnexpectedValueException::class);
$this->config->set('horizon.use', 'not-a-connection');

$this->loader->loadHorizonConfiguration();
}
2 changes: 1 addition & 1 deletion tests/Unit/Connections/PredisConnectionTest.php
Original file line number Diff line number Diff line change
@@ -102,7 +102,7 @@ public function testSetsSentinelConnectionOptionsFromConfig()

public function testDisallowsInvalidSentinelOptions()
{
$this->setExpectedException(BadMethodCallException::class);
$this->expectException(BadMethodCallException::class);

new PredisConnection($this->clientMock, [ 'not_an_option' => null ]);
}
9 changes: 4 additions & 5 deletions tests/Unit/Manager/VersionedRedisSentinelManagerTest.php
Original file line number Diff line number Diff line change
@@ -2,7 +2,6 @@

namespace Monospice\LaravelRedisSentinel\Tests\Unit;

use Closure;
use Illuminate\Contracts\Container\Container;
use Illuminate\Contracts\Redis\Factory as RedisFactory;
use Illuminate\Redis\RedisManager;
@@ -118,23 +117,23 @@ public function testCreatesSingleClientsWithIndividualConfig()

public function testDisallowsRedisClusterConnections()
{
$this->setExpectedException(InvalidArgumentException::class);
$this->expectException(InvalidArgumentException::class);

$this->subject->connection('clustered_connection');
}

public function testFailsOnUndefinedConnection()
{
$this->setExpectedException(InvalidArgumentException::class);
$this->expectException(InvalidArgumentException::class);

$this->subject->connection('nonexistant_connection');
}

public function testFailsOnUnsupportedClientDriver()
{
$this->setExpectedException(InvalidArgumentException::class);
$this->expectException(InvalidArgumentException::class);

$manager = $this->makeSubject('phpredis', [
$manager = $this->makeSubject('fakeredis', [
'test_connection' => [ ],
]);

3 changes: 2 additions & 1 deletion tests/Unit/RedisSentinelServiceProviderTest.php
Original file line number Diff line number Diff line change
@@ -200,10 +200,11 @@ public function testBootExtendsSessionHandlers()

public function testWaitsForBoot()
{
$this->expectException(\InvalidArgumentException::class);

$this->app->config->set('redis-sentinel.auto_boot', false);
$this->provider->register();

$this->setExpectedException(\InvalidArgumentException::class);

// It didn't auto boot
$this->assertNull($this->app->cache->store('redis-sentinel'));