Improve Resque_Redis DSN parsing.

- Allow for DSN URIs to work as expected.
- Backward-compatible with simple 'host:port' format.
- Does not parse DSNs provided in array format for Credis_Cluster.
This commit is contained in:
Iskandar Najmuddin 2014-05-05 13:02:16 +00:00
parent 610c4dcdbf
commit ad33efbc67
2 changed files with 246 additions and 32 deletions

121
lib/Resque/Redis.php Normal file → Executable file
View File

@ -8,14 +8,29 @@
*/ */
class Resque_Redis class Resque_Redis
{ {
/** /**
* Redis namespace * Redis namespace
* @var string * @var string
*/ */
private static $defaultNamespace = 'resque:'; private static $defaultNamespace = 'resque:';
private $server; /**
private $database; * A default host to connect to
*/
const DEFAULT_HOST = 'localhost';
/**
* The default Redis port
*/
const DEFAULT_PORT = 6379;
/**
* The default Redis Database number
*/
const DEFAULT_DATABASE = 0;
private $server;
private $database;
/** /**
* @var array List of all commands in Redis that supply a key as their * @var array List of all commands in Redis that supply a key as their
@ -92,40 +107,36 @@ class Resque_Redis
self::$defaultNamespace = $namespace; self::$defaultNamespace = $namespace;
} }
public function __construct($server, $database = null) /**
* @param string|array $server A DSN or array
* @param int $database A database number to select
*/
public function __construct($server, $database = null)
{ {
$this->server = $server; $this->server = $server;
$this->database = $database; $this->database = $database;
if (is_array($this->server)) { if (is_array($this->server)) {
$this->driver = new Credis_Cluster($server); $this->driver = new Credis_Cluster($server);
}
else {
$port = null;
$password = null;
$host = $server;
// If not a UNIX socket path or tcp:// formatted connections string } else {
// assume host:port combination.
if (strpos($server, '/') === false) { list($host, $port, $dsnDatabase, $user, $password, $options) = $this->parseDsn($server);
$parts = explode(':', $server); // $user is are unused here
if (isset($parts[1])) {
$port = $parts[1]; // Look for known Credis_Client options
} $timeout = isset($options['timeout']) ? intval($options['timeout']) : null;
$host = $parts[0]; $persistent = isset($options['persistent']) ? $options['persistent'] : '';
}else if (strpos($server, 'redis://') !== false){
// Redis format is: $this->driver = new Credis_Client($host, $port, $timeout, $persistent);
// redis://[user]:[password]@[host]:[port] if ($password){
list($userpwd,$hostport) = explode('@', $server);
$userpwd = substr($userpwd, strpos($userpwd, 'redis://')+8);
list($host, $port) = explode(':', $hostport);
list($user, $password) = explode(':', $userpwd);
}
$this->driver = new Credis_Client($host, $port);
if (isset($password)){
$this->driver->auth($password); $this->driver->auth($password);
} }
// If the `$database` constructor argument is not set, use the value from the DSN.
if (is_null($database)) {
$database = $dsnDatabase;
}
} }
if ($this->database !== null) { if ($this->database !== null) {
@ -133,6 +144,52 @@ class Resque_Redis
} }
} }
/**
* Parse a DSN string
* @param string $dsn
* @return array [host, port, db, user, pass, options]
*/
public function parseDsn($dsn)
{
$validSchemes = array('redis', 'tcp');
if ($dsn == '') {
// Use a sensible default for an empty DNS string
$dsn = 'redis://' . self::DEFAULT_HOST;
}
$parts = parse_url($dsn);
if (isset($parts['scheme']) && ! in_array($parts['scheme'], $validSchemes)) {
throw new \InvalidArgumentException("Invalid DSN. Supported schemes are " . implode(', ', $validSchemes));
}
// Allow simple 'hostname' format, which parse_url treats as a path, not host.
if ( ! isset($parts['host'])) {
$parts = array('host' => $parts['path']);
}
$port = isset($parts['port']) ? intval($parts['port']) : self::DEFAULT_PORT;
$database = self::DEFAULT_DATABASE;
if (isset($parts['path'])) {
// Strip non-digit chars from path
$database = intval(preg_replace('/[^0-9]/', '', $parts['path']));
}
$options = array();
if (isset($parts['query'])) {
// Parse the query string into an array
parse_str($parts['query'], $options);
}
return array(
$parts['host'],
$port,
$database,
isset($parts['user']) ? $parts['user'] : false,
isset($parts['pass']) ? $parts['pass'] : false,
$options,
);
}
/** /**
* Magic method to handle all function requests and prefix key based * Magic method to handle all function requests and prefix key based
* operations with the {self::$defaultNamespace} key prefix. * operations with the {self::$defaultNamespace} key prefix.

157
test/Resque/Tests/DsnTest.php Executable file
View File

@ -0,0 +1,157 @@
<?php
/**
* Resque_Redis DSN tests.
*
* @package Resque/Tests
* @author Iskandar Najmuddin <github@iskandar.co.uk>
* @license http://www.opensource.org/licenses/mit-license.php
*/
class Resque_Tests_DsnTest extends Resque_Tests_TestCase
{
/**
* These DNS strings are considered valid.
*
* @return array
*/
public function validDsnStringProvider()
{
return array(
// Input , Expected output
array('', array(
'localhost',
Resque_Redis::DEFAULT_PORT,
Resque_Redis::DEFAULT_DATABASE,
false, false,
array(),
)),
array('localhost', array(
'localhost',
Resque_Redis::DEFAULT_PORT,
Resque_Redis::DEFAULT_DATABASE,
false, false,
array(),
)),
array('localhost:1234', array(
'localhost',
1234,
Resque_Redis::DEFAULT_DATABASE,
false, false,
array(),
)),
array('localhost:1234/2', array(
'localhost',
1234,
2,
false, false,
array(),
)),
array('redis://foobar', array(
'foobar',
Resque_Redis::DEFAULT_PORT,
Resque_Redis::DEFAULT_DATABASE,
false, false,
array(),
)),
array('redis://foobar:1234', array(
'foobar',
1234,
Resque_Redis::DEFAULT_DATABASE,
false, false,
array(),
)),
array('redis://user@foobar:1234', array(
'foobar',
1234,
Resque_Redis::DEFAULT_DATABASE,
'user', false,
array(),
)),
array('redis://user:pass@foobar:1234', array(
'foobar',
1234,
Resque_Redis::DEFAULT_DATABASE,
'user', 'pass',
array(),
)),
array('redis://user:pass@foobar:1234?x=y&a=b', array(
'foobar',
1234,
Resque_Redis::DEFAULT_DATABASE,
'user', 'pass',
array('x' => 'y', 'a' => 'b'),
)),
array('redis://:pass@foobar:1234?x=y&a=b', array(
'foobar',
1234,
Resque_Redis::DEFAULT_DATABASE,
false, 'pass',
array('x' => 'y', 'a' => 'b'),
)),
array('redis://user@foobar:1234?x=y&a=b', array(
'foobar',
1234,
Resque_Redis::DEFAULT_DATABASE,
'user', false,
array('x' => 'y', 'a' => 'b'),
)),
array('redis://foobar:1234?x=y&a=b', array(
'foobar',
1234,
Resque_Redis::DEFAULT_DATABASE,
false, false,
array('x' => 'y', 'a' => 'b'),
)),
array('redis://user@foobar:1234/12?x=y&a=b', array(
'foobar',
1234,
12,
'user', false,
array('x' => 'y', 'a' => 'b'),
)),
array('tcp://user@foobar:1234/12?x=y&a=b', array(
'foobar',
1234,
12,
'user', false,
array('x' => 'y', 'a' => 'b'),
)),
);
}
/**
* These DSN values should throw exceptions
* @return array
*/
public function bogusDsnStringProvider()
{
return array(
'http://foo.bar/',
'://foo.bar/',
'user:@foobar:1234?x=y&a=b',
'foobar:1234?x=y&a=b',
);
}
/**
* @dataProvider validDsnStringProvider
*/
public function testParsingValidDsnString($dsn, $expected)
{
$resqueRedis = new Resque_Redis('localhost');
$result = $resqueRedis->parseDsn($dsn);
$this->assertEquals($expected, $result);
}
/**
* @dataProvider bogusDsnStringProvider
* @expectedException InvalidArgumentException
*/
public function testParsingBogusDsnStringThrowsException($dsn)
{
$resqueRedis = new Resque_Redis('localhost');
// The next line should throw an InvalidArgumentException
$result = $resqueRedis->parseDsn($dsn);
}
}