diff --git a/bin/resque b/bin/resque index b8089ac..4cb50da 100755 --- a/bin/resque +++ b/bin/resque @@ -30,7 +30,15 @@ if(empty($QUEUE)) { die("Set QUEUE env var containing the list of queues to work.\n"); } +/** + * REDIS_BACKEND can have simple 'host:port' format or use a DSN-style format like this: + * - redis://user:pass@host:port + * + * Note: the 'user' part of the DSN URI is required but is not used. + */ $REDIS_BACKEND = getenv('REDIS_BACKEND'); + +// A redis database number $REDIS_BACKEND_DB = getenv('REDIS_BACKEND_DB'); if(!empty($REDIS_BACKEND)) { if (empty($REDIS_BACKEND_DB)) diff --git a/demo/check_status.php b/demo/check_status.php index 645bf6d..871daba 100644 --- a/demo/check_status.php +++ b/demo/check_status.php @@ -7,6 +7,9 @@ require __DIR__ . '/init.php'; date_default_timezone_set('GMT'); Resque::setBackend('127.0.0.1:6379'); +// You can also use a DSN-style format: +//Resque::setBackend('redis://user:pass@127.0.0.1:6379'); +//Resque::setBackend('redis://user:pass@a.host.name:3432/2'); $status = new Resque_Job_Status($argv[1]); if(!$status->isTracking()) { diff --git a/demo/queue.php b/demo/queue.php index 52f2f0b..1eca124 100644 --- a/demo/queue.php +++ b/demo/queue.php @@ -7,6 +7,10 @@ require __DIR__ . '/init.php'; date_default_timezone_set('GMT'); Resque::setBackend('127.0.0.1:6379'); +// You can also use a DSN-style format: +//Resque::setBackend('redis://user:pass@127.0.0.1:6379'); +//Resque::setBackend('redis://user:pass@a.host.name:3432/2'); + $args = array( 'time' => time(), 'array' => array( diff --git a/lib/Resque.php b/lib/Resque.php index 2b47084..0f4b94e 100644 --- a/lib/Resque.php +++ b/lib/Resque.php @@ -32,7 +32,7 @@ class Resque * Given a host/port combination separated by a colon, set it as * the redis server that Resque will talk to. * - * @param mixed $server Host/port combination separated by a colon, or + * @param mixed $server Host/port combination separated by a colon, DSN-formatted URI, or * a nested array of servers with host/port pairs. * @param int $database */ @@ -54,12 +54,7 @@ class Resque return self::$redis; } - $server = self::$redisServer; - if (empty($server)) { - $server = 'localhost:6379'; - } - - self::$redis = new Resque_Redis($server, self::$redisDatabase); + self::$redis = new Resque_Redis(self::$redisServer, self::$redisDatabase); return self::$redis; } diff --git a/lib/Resque/Redis.php b/lib/Resque/Redis.php index 6cef3d2..a946b14 100644 --- a/lib/Resque/Redis.php +++ b/lib/Resque/Redis.php @@ -8,14 +8,26 @@ */ class Resque_Redis { - /** - * Redis namespace - * @var string - */ - private static $defaultNamespace = 'resque:'; + /** + * Redis namespace + * @var string + */ + 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; /** * @var array List of all commands in Redis that supply a key as their @@ -92,47 +104,106 @@ class Resque_Redis 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. However, if we find a valid database number in the DSN the + * DSN-supplied value will be used instead and this parameter is ignored. + */ + public function __construct($server, $database = null) { - $this->server = $server; - $this->database = $database; - - if (is_array($this->server)) { + if (is_array($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 - // assume host:port combination. - if (strpos($server, '/') === false) { - $parts = explode(':', $server); - if (isset($parts[1])) { - $port = $parts[1]; - } - $host = $parts[0]; - }else if (strpos($server, 'redis://') !== false){ - // Redis format is: - // redis://[user]:[password]@[host]:[port] - 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)){ + list($host, $port, $dsnDatabase, $user, $password, $options) = self::parseDsn($server); + // $user is not used, only $password + + // Look for known Credis_Client options + $timeout = isset($options['timeout']) ? intval($options['timeout']) : null; + $persistent = isset($options['persistent']) ? $options['persistent'] : ''; + + $this->driver = new Credis_Client($host, $port, $timeout, $persistent); + if ($password){ $this->driver->auth($password); } + + // If we have found a database in our DSN, use it instead of the `$database` + // value passed into the constructor. + if ($dsnDatabase !== false) { + $database = $dsnDatabase; + } } - if ($this->database !== null) { + if ($database !== null) { $this->driver->select($database); } } + /** + * Parse a DSN string, which can have one of the following formats: + * + * - host:port + * - redis://user:pass@host:port/db?option1=val1&option2=val2 + * - tcp://user:pass@host:port/db?option1=val1&option2=val2 + * + * Note: the 'user' part of the DSN is not used. + * + * @param string $dsn A DSN string + * @return array An array of DSN compotnents, with 'false' values for any unknown components. e.g. + * [host, port, db, user, pass, options] + */ + public static function parseDsn($dsn) + { + if ($dsn == '') { + // Use a sensible default for an empty DNS string + $dsn = 'redis://' . self::DEFAULT_HOST; + } + $parts = parse_url($dsn); + + // Check the URI scheme + $validSchemes = array('redis', 'tcp'); + 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']) && isset($parts['path'])) { + $parts['host'] = $parts['path']; + unset($parts['path']); + } + + // Extract the port number as an integer + $port = isset($parts['port']) ? intval($parts['port']) : self::DEFAULT_PORT; + + // Get the database from the 'path' part of the URI + $database = false; + if (isset($parts['path'])) { + // Strip non-digit chars from path + $database = intval(preg_replace('/[^0-9]/', '', $parts['path'])); + } + + // Extract any 'user' and 'pass' values + $user = isset($parts['user']) ? $parts['user'] : false; + $pass = isset($parts['pass']) ? $parts['pass'] : false; + + // Convert the query string into an associative array + $options = array(); + if (isset($parts['query'])) { + // Parse the query string into an array + parse_str($parts['query'], $options); + } + + return array( + $parts['host'], + $port, + $database, + $user, + $pass, + $options, + ); + } + /** * Magic method to handle all function requests and prefix key based * operations with the {self::$defaultNamespace} key prefix. @@ -141,37 +212,38 @@ class Resque_Redis * @param array $args Array of supplied arguments to the method. * @return mixed Return value from Resident::call() based on the command. */ - public function __call($name, $args) { - if(in_array($name, $this->keyCommands)) { - if(is_array($args[0])) { - foreach($args[0] AS $i => $v) { - $args[0][$i] = self::$defaultNamespace . $v; - } - } else { - $args[0] = self::$defaultNamespace . $args[0]; - } + public function __call($name, $args) + { + if (in_array($name, $this->keyCommands)) { + if (is_array($args[0])) { + foreach ($args[0] AS $i => $v) { + $args[0][$i] = self::$defaultNamespace . $v; + } + } + else { + $args[0] = self::$defaultNamespace . $args[0]; + } } try { return $this->driver->__call($name, $args); } - catch(CredisException $e) { + catch (CredisException $e) { return false; } } - public static function getPrefix() - { - return self::$defaultNamespace; - } + public static function getPrefix() + { + return self::$defaultNamespace; + } - public static function removePrefix($string) - { - $prefix=self::getPrefix(); + public static function removePrefix($string) + { + $prefix=self::getPrefix(); - if (substr($string, 0, strlen($prefix)) == $prefix) { - $string = substr($string, strlen($prefix), strlen($string) ); - } - return $string; - } + if (substr($string, 0, strlen($prefix)) == $prefix) { + $string = substr($string, strlen($prefix), strlen($string) ); + } + return $string; + } } -?> \ No newline at end of file diff --git a/test/Resque/Tests/DsnTest.php b/test/Resque/Tests/DsnTest.php new file mode 100644 index 0000000..086db1e --- /dev/null +++ b/test/Resque/Tests/DsnTest.php @@ -0,0 +1,182 @@ + + * @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, + false, + false, false, + array(), + )), + array('localhost', array( + 'localhost', + Resque_Redis::DEFAULT_PORT, + false, + false, false, + array(), + )), + array('localhost:1234', array( + 'localhost', + 1234, + false, + false, false, + array(), + )), + array('localhost:1234/2', array( + 'localhost', + 1234, + 2, + false, false, + array(), + )), + array('redis://foobar', array( + 'foobar', + Resque_Redis::DEFAULT_PORT, + false, + false, false, + array(), + )), + array('redis://foobar/', array( + 'foobar', + Resque_Redis::DEFAULT_PORT, + false, + false, false, + array(), + )), + array('redis://foobar:1234', array( + 'foobar', + 1234, + false, + false, false, + array(), + )), + array('redis://foobar:1234/15', array( + 'foobar', + 1234, + 15, + false, false, + array(), + )), + array('redis://foobar:1234/0', array( + 'foobar', + 1234, + 0, + false, false, + array(), + )), + array('redis://user@foobar:1234', array( + 'foobar', + 1234, + false, + 'user', false, + array(), + )), + array('redis://user@foobar:1234/15', array( + 'foobar', + 1234, + 15, + 'user', false, + array(), + )), + array('redis://user:pass@foobar:1234', array( + 'foobar', + 1234, + false, + 'user', 'pass', + array(), + )), + array('redis://user:pass@foobar:1234?x=y&a=b', array( + 'foobar', + 1234, + false, + 'user', 'pass', + array('x' => 'y', 'a' => 'b'), + )), + array('redis://:pass@foobar:1234?x=y&a=b', array( + 'foobar', + 1234, + false, + false, 'pass', + array('x' => 'y', 'a' => 'b'), + )), + array('redis://user@foobar:1234?x=y&a=b', array( + 'foobar', + 1234, + false, + 'user', false, + array('x' => 'y', 'a' => 'b'), + )), + array('redis://foobar:1234?x=y&a=b', array( + 'foobar', + 1234, + false, + 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( + array('http://foo.bar/'), + array('user:@foobar:1234?x=y&a=b'), + array('foobar:1234?x=y&a=b'), + ); + } + + /** + * @dataProvider validDsnStringProvider + */ + public function testParsingValidDsnString($dsn, $expected) + { + $result = Resque_Redis::parseDsn($dsn); + $this->assertEquals($expected, $result); + } + + /** + * @dataProvider bogusDsnStringProvider + * @expectedException InvalidArgumentException + */ + public function testParsingBogusDsnStringThrowsException($dsn) + { + // The next line should throw an InvalidArgumentException + $result = Resque_Redis::parseDsn($dsn); + } + +}