mirror of
https://github.com/predis/predis.git
synced 2026-09-12 19:37:05 +00:00
Implement role-specific default parameters.
Until now users could specify a set of default parameters applied to all
connections created by the connection factory when not explicitly set in
the user-supplied set of parameters of each single node connection. This
is definitely handy, but it has some limits especially when dealing with
sentinel nodes since there are times when it is better to use different
defaults (e.g. "timeout") and they do not support certain parameters.
This commit adds the ability to specify role-specific default parameters
that gets applied only to connections targeting specific roles. This is
mostly useful for sentinels as they usually require lower timeouts than
normal Redis nodes and may also have a different password.
Role-specific parameters are passed as part of the "parameters" client
option in the form of named sub-arrays and take precedence over global
parameters, but they still do not override parameters explicitly set by
the user for single nodes.
Supported keys are "role.sentinel", "role.master" and "role.slave". In
regards to "role.sentinel", please note that:
- sentinels do not support ACL authentication or database selection so
so "username" and "database" are always stripped off.
- "password" is never inherited from global defaults because users can
have password-protected Redis nodes but unprotected Redis sentinels.
In such cases, users must explicitly set a password either for each
sentinel connection or just once in "role.sentinel".
Here is a brief example showing how to configure Predis for replication
supervised by redis-sentinel using different timeout and password values
for sentinel nodes compared to normal master and replica nodes.
$client = new Predis\Client($arrayOfSentinels, [
'replication' => 'sentinel',
'service' => $sentinelService,
'parameters' => [
// Set of global default parameters, applied to *any* connection:
'scheme' => true,
'tcp_nodelay' => true,
'timeout' => 5,
'username' => $redisUsername, // Won't be inherited by sentinels.
'password' => $redisPassword, // Won't be inherited by sentinels.
// Set of sentinels-specific default parameters:
'role.sentinel' => [
// For sentinels, "scheme" and "tcp_nodelay" are inherited from
// default parameters and "timeout" is overridden. On the other
// hand both "username" and "password" are never inherited but
// still explicitly set a password for sentinels because, in our
// example, sentinels are indeed password-protected.
'timeout' => 0.200,
'password' => $sentinelPassword,
],
]);
This commit is contained in:
@@ -11,6 +11,7 @@
|
||||
|
||||
namespace Predis\Connection;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use Predis\Command\RawCommand;
|
||||
|
||||
/**
|
||||
@@ -119,6 +120,28 @@ class Factory implements FactoryInterface
|
||||
*/
|
||||
public function setDefaultParameters(array $parameters)
|
||||
{
|
||||
if (isset($parameters['role.master']) && !is_array($parameters['role.master'])) {
|
||||
throw new InvalidArgumentException('Default parameters for `role.master` must be passed as a named array');
|
||||
}
|
||||
|
||||
if (isset($parameters['role.slave']) && !is_array($parameters['role.slave'])) {
|
||||
throw new InvalidArgumentException('Default parameters for `role.slave` must be passed as a named array');
|
||||
}
|
||||
|
||||
if (isset($parameters['role.sentinel'])) {
|
||||
if (!is_array($parameters['role.sentinel'])) {
|
||||
throw new InvalidArgumentException('Default parameters for `role.sentinel` must be passed as a named array');
|
||||
}
|
||||
|
||||
// NOTE: sentinels do not support "SELECT" and ACL "AUTH" commands
|
||||
// so we must strip "database" and "username" from "role.sentinel"
|
||||
// to prevent spurious commands from being sent to sentinel nodes.
|
||||
unset(
|
||||
$parameters['role.sentinel']['username'],
|
||||
$parameters['role.sentinel']['database']
|
||||
);
|
||||
}
|
||||
|
||||
$this->defaults = $parameters;
|
||||
}
|
||||
|
||||
@@ -132,6 +155,37 @@ class Factory implements FactoryInterface
|
||||
return $this->defaults;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies default connection parameters to the user supplied parameters.
|
||||
*
|
||||
* @param array $parameters Input connection parameters
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function applyDefaultParameters(array $parameters)
|
||||
{
|
||||
static $stripInternal = ['role.sentinel' => null, 'role.master' => null, 'role.slave' => null];
|
||||
|
||||
$stripAdditional = [];
|
||||
|
||||
if (isset($parameters['role'])) {
|
||||
switch ($role = $parameters['role']) {
|
||||
case 'sentinel':
|
||||
// NOTE: we strip these from global defaults when dealing with sentinel nodes.
|
||||
$stripAdditional = ['username' => null, 'password' => null, 'database' => null];
|
||||
case 'master':
|
||||
case 'slave':
|
||||
if (isset($this->defaults["role.$role"])) {
|
||||
$parameters += $this->defaults["role.$role"];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$parameters += array_diff_key($this->defaults, $stripInternal, $stripAdditional);
|
||||
|
||||
return $parameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a connection parameters instance from the supplied argument.
|
||||
*
|
||||
@@ -144,11 +198,19 @@ class Factory implements FactoryInterface
|
||||
if (is_string($parameters)) {
|
||||
$parameters = Parameters::parse($parameters);
|
||||
} else {
|
||||
$parameters = $parameters ?: array();
|
||||
$parameters = $parameters ?? [];
|
||||
}
|
||||
|
||||
if (isset($parameters['role']) && $parameters['role'] === 'sentinel') {
|
||||
// NOTE: sentinels do not support "SELECT" and ACL "AUTH" commands so we must strip
|
||||
// "database" and "username" from input parameters to prevent spurious commands from
|
||||
// being sent to sentinel nodes but they can still accept "password" when explicitly
|
||||
// set (password-based authentication for sentinels is supported on Redis >= 5.0).
|
||||
unset($parameters['username'], $parameters['database']);
|
||||
}
|
||||
|
||||
if ($this->defaults) {
|
||||
$parameters += $this->defaults;
|
||||
$parameters = $this->applyDefaultParameters($parameters);
|
||||
}
|
||||
|
||||
return new Parameters($parameters);
|
||||
|
||||
@@ -241,6 +241,8 @@ class SentinelReplication implements ReplicationInterface
|
||||
/**
|
||||
* Creates a new connection to a sentinel server.
|
||||
*
|
||||
* @param mixed $parameters Connection parameters or connection instance
|
||||
*
|
||||
* @return NodeConnectionInterface
|
||||
*/
|
||||
protected function createSentinelConnection($parameters)
|
||||
@@ -254,12 +256,10 @@ class SentinelReplication implements ReplicationInterface
|
||||
}
|
||||
|
||||
if (is_array($parameters)) {
|
||||
// Password authentication is fine now that Redis Sentinel supports
|
||||
// password-protected sentinel instances, but we must explicitly set
|
||||
// "database" and "username" to NULL so that no augmented AUTH (ACL)
|
||||
// and SELECT command are sent by accident to the sentinels.
|
||||
$parameters['database'] = null;
|
||||
$parameters['username'] = null;
|
||||
// NOTE: we enforce the "sentinel" role so that appropriate default
|
||||
// parameters are applied when creating the new connection instance
|
||||
// and blacklisted ones are stripped off from input parameters.
|
||||
$parameters['role'] = 'sentinel';
|
||||
|
||||
if (!isset($parameters['timeout'])) {
|
||||
$parameters['timeout'] = $this->sentinelTimeout;
|
||||
|
||||
@@ -44,8 +44,108 @@ class FactoryTest extends PredisTestCase
|
||||
));
|
||||
|
||||
$this->assertSame($defaults, $factory->getDefaultParameters());
|
||||
}
|
||||
|
||||
$parameters = array('database' => 10, 'persistent' => true);
|
||||
/**
|
||||
* @group disconnected
|
||||
*/
|
||||
public function testSettingDefaultParametersForMasterRole(): void
|
||||
{
|
||||
$factory = new Factory();
|
||||
|
||||
$factory->setDefaultParameters($expected = array(
|
||||
'role.master' => [
|
||||
'username' => 'myusername',
|
||||
'password' => 'secret',
|
||||
'database' => 10,
|
||||
]
|
||||
));
|
||||
|
||||
$this->assertSame($expected, $factory->getDefaultParameters());
|
||||
}
|
||||
|
||||
/**
|
||||
* @group disconnected
|
||||
*/
|
||||
public function testSettingDefaultParametersForMasterRoleAcceptsArrayOnly(): void
|
||||
{
|
||||
$this->expectException('InvalidArgumentException');
|
||||
$this->expectExceptionMessage('Default parameters for `role.master` must be passed as a named array');
|
||||
|
||||
$factory = new Factory();
|
||||
$factory->setDefaultParameters(array(
|
||||
'role.master' => 'invalid value',
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* @group disconnected
|
||||
*/
|
||||
public function testSettingDefaultParametersForSlaveRole(): void
|
||||
{
|
||||
$factory = new Factory();
|
||||
|
||||
$factory->setDefaultParameters($expected = array(
|
||||
'role.slave' => [
|
||||
'username' => 'myusername',
|
||||
'password' => 'secret',
|
||||
'database' => 10,
|
||||
]
|
||||
));
|
||||
|
||||
$this->assertSame($expected, $factory->getDefaultParameters());
|
||||
}
|
||||
|
||||
/**
|
||||
* @group disconnected
|
||||
*/
|
||||
public function testSettingDefaultParametersForSlaveRoleAcceptsArrayOnly(): void
|
||||
{
|
||||
$this->expectException('InvalidArgumentException');
|
||||
$this->expectExceptionMessage('Default parameters for `role.slave` must be passed as a named array');
|
||||
|
||||
$factory = new Factory();
|
||||
$factory->setDefaultParameters(array(
|
||||
'role.slave' => 'invalid value',
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* @group disconnected
|
||||
*/
|
||||
public function testSettingDefaultParametersForSentinelRoleIgnoresUsernameAndPassword(): void
|
||||
{
|
||||
$factory = new Factory();
|
||||
|
||||
$factory->setDefaultParameters(array(
|
||||
'role.sentinel' => [
|
||||
'username' => 'myusername',
|
||||
'password' => 'secret',
|
||||
'database' => 10,
|
||||
]
|
||||
));
|
||||
|
||||
$expected = array(
|
||||
'role.sentinel' => [
|
||||
'password' => 'secret',
|
||||
]
|
||||
);
|
||||
|
||||
$this->assertSame($expected, $factory->getDefaultParameters());
|
||||
}
|
||||
|
||||
/**
|
||||
* @group disconnected
|
||||
*/
|
||||
public function testSettingDefaultParametersForSentinelRoleAcceptsArrayOnly(): void
|
||||
{
|
||||
$this->expectException('InvalidArgumentException');
|
||||
$this->expectExceptionMessage('Default parameters for `role.sentinel` must be passed as a named array');
|
||||
|
||||
$factory = new Factory();
|
||||
$factory->setDefaultParameters(array(
|
||||
'role.sentinel' => 'invalid value',
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -195,7 +295,7 @@ class FactoryTest extends PredisTestCase
|
||||
/**
|
||||
* @group disconnected
|
||||
*/
|
||||
public function testCreateConnectionWithArrayParametersAndDefaults(): void
|
||||
public function testCreateConnectionWithDefaultParametersDoNotOverrideExplicitInputParameters(): void
|
||||
{
|
||||
$factory = new Factory();
|
||||
|
||||
@@ -223,6 +323,218 @@ class FactoryTest extends PredisTestCase
|
||||
$this->assertNull($parameters->path);
|
||||
}
|
||||
|
||||
/**
|
||||
* @group disconnected
|
||||
*/
|
||||
public function testCreateConnectionForSentinelRoleIgnoresUsernameAndDatabase(): void
|
||||
{
|
||||
$factory = new Factory();
|
||||
|
||||
$connection = $factory->create($inputParams = array(
|
||||
'role' => 'sentinel',
|
||||
'username' => 'myusername',
|
||||
'password' => 'mypassword',
|
||||
'database' => 10,
|
||||
));
|
||||
|
||||
$parameters = $connection->getParameters();
|
||||
|
||||
$this->assertInstanceOf('Predis\Connection\NodeConnectionInterface', $connection);
|
||||
|
||||
$this->assertEquals($inputParams['role'], $parameters->role);
|
||||
$this->assertEquals($inputParams['password'], $parameters->password);
|
||||
$this->assertNull($parameters->username);
|
||||
$this->assertNull($parameters->database);
|
||||
}
|
||||
|
||||
/**
|
||||
* @group disconnected
|
||||
*/
|
||||
public function testCreateConnectionForSentinelRoleDoesNotInheritPasswordFromGlobalDefaultParameters(): void
|
||||
{
|
||||
$factory = new Factory();
|
||||
|
||||
$factory->setDefaultParameters($defaultParams = array(
|
||||
'password' => 'pwd.default',
|
||||
));
|
||||
|
||||
$connectionSentinelRole = $factory->create($inputParamsSentinelRole = array(
|
||||
'role' => 'sentinel',
|
||||
));
|
||||
|
||||
$parameters = $connectionSentinelRole->getParameters();
|
||||
$this->assertNull($parameters->password);
|
||||
}
|
||||
|
||||
/**
|
||||
* @group disconnected
|
||||
*/
|
||||
public function testCreateConnectionForSentinelRoleDoesNotInheritUsernameFromGlobalDefaultParameters(): void
|
||||
{
|
||||
$factory = new Factory();
|
||||
|
||||
$factory->setDefaultParameters($defaultParams = array(
|
||||
'username' => 'usr.default',
|
||||
));
|
||||
|
||||
$connectionSentinelRole = $factory->create($inputParamsSentinelRole = array(
|
||||
'role' => 'sentinel',
|
||||
));
|
||||
|
||||
$parameters = $connectionSentinelRole->getParameters();
|
||||
$this->assertNull($parameters->username);
|
||||
}
|
||||
|
||||
/**
|
||||
* @group disconnected
|
||||
*/
|
||||
public function testCreateConnectionForSentinelRoleDoesNotInheritDatabaseFromGlobalDefaultParameters(): void
|
||||
{
|
||||
$factory = new Factory();
|
||||
|
||||
$factory->setDefaultParameters($defaultParams = array(
|
||||
'database' => 15,
|
||||
));
|
||||
|
||||
$connectionSentinelRole = $factory->create($inputParamsSentinelRole = array(
|
||||
'role' => 'sentinel',
|
||||
));
|
||||
|
||||
$parameters = $connectionSentinelRole->getParameters();
|
||||
$this->assertNull($parameters->database);
|
||||
}
|
||||
|
||||
/**
|
||||
* @group disconnected
|
||||
*/
|
||||
public function testCreateConnectionWithDefaultRoleParametersDoNotOverrideExplicitInputParameters(): void
|
||||
{
|
||||
$factory = new Factory();
|
||||
|
||||
$factory->setDefaultParameters($defaultParams = array(
|
||||
'timeout' => 20,
|
||||
'password' => 'pwd.default.norole',
|
||||
|
||||
'role.master' => [
|
||||
'password' => 'pwd.role.master',
|
||||
'timeout' => 10,
|
||||
],
|
||||
'role.slave' => [
|
||||
'password' => 'pwd.role.slave',
|
||||
'timeout' => 5,
|
||||
],
|
||||
'role.sentinel' => [
|
||||
'password' => 'pwd.role.sentinel',
|
||||
'timeout' => 1,
|
||||
],
|
||||
));
|
||||
|
||||
// NO ROLE
|
||||
$connectionNoRole = $factory->create($inputParamsNoRole = array(
|
||||
'password' => 'pwd.local.norole',
|
||||
'timeout' => 30,
|
||||
));
|
||||
|
||||
$parameters = $connectionNoRole->getParameters();
|
||||
$this->assertEquals('pwd.local.norole', $parameters->password);
|
||||
$this->assertEquals(30, $parameters->timeout);
|
||||
|
||||
// ROLE MASTER
|
||||
$connectionMasterRole = $factory->create($inputParamsMasterRole = array(
|
||||
'role' => 'master',
|
||||
'password' => 'pwd.local.master',
|
||||
'timeout' => 30,
|
||||
));
|
||||
|
||||
$parameters = $connectionMasterRole->getParameters();
|
||||
$this->assertEquals('pwd.local.master', $parameters->password);
|
||||
$this->assertEquals(30, $parameters->timeout);
|
||||
|
||||
// ROLE SLAVE
|
||||
$connectionSlaveRole = $factory->create($inputParamsSlaveRole = array(
|
||||
'role' => 'slave',
|
||||
'password' => 'pwd.local.slave',
|
||||
'timeout' => 30,
|
||||
));
|
||||
|
||||
$parameters = $connectionSlaveRole->getParameters();
|
||||
$this->assertEquals('pwd.local.slave', $parameters->password);
|
||||
$this->assertEquals(30, $parameters->timeout);
|
||||
|
||||
// ROLE SENTINEL
|
||||
$connectionSentinelRole = $factory->create($inputParamsSentinelRole = array(
|
||||
'role' => 'slave',
|
||||
'password' => 'pwd.local.sentinel',
|
||||
'timeout' => 30,
|
||||
));
|
||||
|
||||
$parameters = $connectionSentinelRole->getParameters();
|
||||
$this->assertEquals('pwd.local.sentinel', $parameters->password);
|
||||
$this->assertEquals(30, $parameters->timeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* @group disconnected
|
||||
*/
|
||||
public function testCreateConnectionWithDefaultRoleParametersOverridesDefaultGlobalParameters(): void
|
||||
{
|
||||
$factory = new Factory();
|
||||
|
||||
$factory->setDefaultParameters($defaultParams = array(
|
||||
'timeout' => 20,
|
||||
'password' => 'pwd.default.norole',
|
||||
|
||||
'role.master' => [
|
||||
'password' => 'pwd.role.master',
|
||||
'timeout' => 10,
|
||||
],
|
||||
'role.slave' => [
|
||||
'password' => 'pwd.role.slave',
|
||||
'timeout' => 5,
|
||||
],
|
||||
'role.sentinel' => [
|
||||
'password' => 'pwd.role.sentinel',
|
||||
'timeout' => 1,
|
||||
],
|
||||
));
|
||||
|
||||
// NO ROLE
|
||||
$connectionNoRole = $factory->create($inputParamsNoRole = array(
|
||||
// EMPTY
|
||||
));
|
||||
|
||||
$parameters = $connectionNoRole->getParameters();
|
||||
$this->assertEquals('pwd.default.norole', $parameters->password);
|
||||
$this->assertEquals(20, $parameters->timeout);
|
||||
|
||||
// ROLE MASTER
|
||||
$connectionMasterRole = $factory->create($inputParamsMasterRole = array(
|
||||
'role' => 'master',
|
||||
));
|
||||
|
||||
$parameters = $connectionMasterRole->getParameters();
|
||||
$this->assertEquals('pwd.role.master', $parameters->password);
|
||||
$this->assertEquals(10, $parameters->timeout);
|
||||
|
||||
// ROLE SLAVE
|
||||
$connectionSlaveRole = $factory->create($inputParamsSlaveRole = array(
|
||||
'role' => 'slave',
|
||||
));
|
||||
|
||||
$parameters = $connectionSlaveRole->getParameters();
|
||||
$this->assertEquals('pwd.role.slave', $parameters->password);
|
||||
$this->assertEquals(5, $parameters->timeout);
|
||||
|
||||
// ROLE SENTINEL
|
||||
$connectionSentinelRole = $factory->create($inputParamsSentinelRole = array(
|
||||
'role' => 'sentinel',
|
||||
));
|
||||
|
||||
$parameters = $connectionSentinelRole->getParameters();
|
||||
$this->assertEquals('pwd.role.sentinel', $parameters->password);
|
||||
$this->assertEquals(1, $parameters->timeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* @group disconnected
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user