From 52fdf7004c616f7f365167eb8547b4d43ba05ad7 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Mon, 8 Apr 2013 19:44:29 -0500 Subject: [PATCH] Switch to Predis backend for Redis. Supports pipelining, sharing, etc. --- composer.json | 1 + src/Illuminate/Redis/CommandException.php | 3 - src/Illuminate/Redis/ConnectionException.php | 3 - src/Illuminate/Redis/Database.php | 291 ++---------------- src/Illuminate/Redis/RedisManager.php | 115 ------- src/Illuminate/Redis/RedisServiceProvider.php | 2 +- src/Illuminate/Redis/composer.json | 3 +- tests/Redis/RedisDatabaseTest.php | 94 ------ 8 files changed, 36 insertions(+), 476 deletions(-) delete mode 100644 src/Illuminate/Redis/CommandException.php delete mode 100644 src/Illuminate/Redis/ConnectionException.php delete mode 100644 src/Illuminate/Redis/RedisManager.php delete mode 100644 tests/Redis/RedisDatabaseTest.php diff --git a/composer.json b/composer.json index c08a7b817bbf..7ab9aacf9c7d 100644 --- a/composer.json +++ b/composer.json @@ -15,6 +15,7 @@ "ircmaxell/password-compat": "1.0.*", "monolog/monolog": "1.4.*", "patchwork/utf8": "1.0.*", + "predis/predis": "0.*", "swiftmailer/swiftmailer": "4.3.*", "symfony/browser-kit": "2.3.*", "symfony/console": "2.3.*", diff --git a/src/Illuminate/Redis/CommandException.php b/src/Illuminate/Redis/CommandException.php deleted file mode 100644 index f530068f4351..000000000000 --- a/src/Illuminate/Redis/CommandException.php +++ /dev/null @@ -1,3 +0,0 @@ -host = $host; - $this->port = $port; - $this->database = $database; - $this->password = $password; - } - - /** - * Connect to the Redis database. - * + * @param array $servers * @return void */ - public function connect() + public function __construct(array $servers = array()) { - if ( ! is_null($this->connection)) return; - - $this->connection = $this->openSocket(); - - if ( ! is_null($this->password)) + if (isset($servers['cluster']) and $servers['cluster']) { - $this->command('auth', array($this->password)); + $this->clients = $this->createAggregateClient($servers); } - - $this->command('select', array($this->database)); - } - - /** - * Run a command against the Redis database. - * - * @param string $method - * @param array $parameters - * @return mixed - */ - public function command($method, array $parameters = array()) - { - $this->fileWrite($this->buildCommand($method, $parameters)); - - return $this->parseResponse($this->fileGet(512)); - } - - /** - * Build the Redis command syntax. - * - * Redis protocol states that a command should conform to the following format: - * - * * CR LF - * $ CR LF - * CR LF - * ... - * $ CR LF - * CR LF - * - * More information regarding the Redis protocol: http://redis.io/topics/protocol - * - * @param string $method - * @param array $parameters - * @return string - */ - public function buildCommand($method, array $parameters) - { - $command = '*'.(count($parameters) + 1)."\r\n"; - - // Before each parameter, we must send the number of bytes in the upcoming - // value that we are sending. So, we'll just take the sting length of a - // parameter and add it to the command. Then we'll add the parameter. - $command .= '$'.strlen($method)."\r\n"; - - $command .= strtoupper($method)."\r\n"; - - foreach ($parameters as $parameter) + else { - $command .= '$'.strlen($parameter)."\r\n".$parameter."\r\n"; + $this->clients = $this->createSingleClients($servers); } - - return $command; } /** - * Parse the Redis database response. + * Create a new aggregate client supporting sharding. * - * @param string $response - * @return mixed - */ - public function parseResponse($response) - { - switch (substr($response, 0, 1)) - { - // The first character of the response tells us what type of response we - // are dealing with. So we will process the response according to the - // the type of response we've received back from the Redis command. - case '+': - return $this->parseInlineResponse($response); - - case ':': - return (int) $this->parseInlineResponse($response); - - case '$': - return $this->parseBulkResponse($response); - - // "Multi-bulk" responses are used to handle responses that contain many - // values such as the response for a "lrange" command to the database - // and these will be handled similarly to regular "bulk" responses. - case '*': - return $this->parseMultiResponse($response); - } - - throw new CommandException(trim($response)); - } - - /** - * Parse an inline response from the database. - * - * @param string $response - * @return string - */ - protected function parseInlineResponse($response) - { - return substr(trim($response), 1); - } - - /** - * Parse a bulk response from the database. - * - * @param string $response - * @return string - */ - protected function parseBulkResponse($response) - { - // If the response size is listed as a negative one it means that there is - // no data for the given keys, and we should return "null" according to - // the Redis protocol documentation for every client Redis libraries. - if (trim($response) == '$-1') - { - return null; - } - - $total = substr($response, 1); - - list($read, $data) = array(0, ''); - - // If we have bytes to read, we will read off the data in 1024 byte chunks - // and then return the response. Once we've read the data, we will also - // need to read the final two byte new line feed off the file stream. - if ($total > 0) - { - do - { - $data .= $this->readBulkBlock($total, $read); - - } while ($read < $total); - } - - // After every response there is a final two byte new line feed that we'll - // need to read off this stream to get it out of the way for subsequent - // command responses that we may retrieve from the Redis connections. - $this->fileRead(2); - - return $data; - } - - /** - * Read the next block of bytes for a bulk response. - * - * @param int $total - * @param int $read - * @return string - */ - protected function readBulkBlock($total, &$read) - { - $block = (($remaining = $total - $read) < 1024) ? $remaining : 1024; - - $read += $block; - - return $this->fileRead($block); - } - - /** - * Parse a multi-bulk response from the database. - * - * @param string $response + * @param array $servers * @return array */ - protected function parseMultiResponse($response) + protected function createAggregateClient(array $servers) { - if (($total = substr($response, 1)) == '-1') return; - - $data = array(); + $servers = array_except($servers, array('cluster')); - // When reading off multi-bulk responses, we can just send the response back - // through the typical parse routines since a multi-bulk is simply a list - // of plain bulk responses. We'll just iterate over the response count. - for ($i = 0; $i < $total; $i++) - { - $data[] = $this->parseResponse($this->fileGet(512)); - } - - return $data; + return array('default' => new \Predis\Client(array_values($servers))); } /** - * Get the socket connection to the database. + * Create an array of single connection clients. * - * @return resource + * @param array $servers + * @return array */ - protected function openSocket() + protected function createSingleClients(array $servers) { - $connection = @fsockopen($this->host, $this->port, $error, $message); + $clients = array(); - if ($connection === false) + foreach ($servers as $key => $server) { - throw new ConnectionException("{$error} - {$message}"); + $clients[$key] = new \Predis\Client($server); } - return $connection; + return $clients; } /** - * Read the specified number of bytes from the file resource. + * Get a specific Redis connection instance. * - * @param int $bytes - * @return string + * @param string $name + * @return \Predis\Connection\SingleConnectionInterface */ - public function fileRead($bytes) + public function connection($name = 'default') { - return fread($this->getConnection(), $bytes); + return $this->clients[$name]; } /** - * Get the specified number of bytes from a file line. - * - * @param int $bytes - * @return string - */ - public function fileGet($bytes) - { - return fgets($this->getConnection(), $bytes); - } - - /** - * Write the given command to the file resource. - * - * @param string $command - * @return void - */ - public function fileWrite($command) - { - fwrite($this->getConnection(), $command); - } - - /** - * Get the Redis socket connection. + * Run a command against the Redis database. * - * @return resource + * @param string $method + * @param array $parameters + * @return mixed */ - public function getConnection() + public function command($method, array $parameters = array()) { - $this->connect(); - - return $this->connection; + return call_user_func_array(array($this->clients['default'], $method), $parameters); } /** diff --git a/src/Illuminate/Redis/RedisManager.php b/src/Illuminate/Redis/RedisManager.php deleted file mode 100644 index de0aabb71876..000000000000 --- a/src/Illuminate/Redis/RedisManager.php +++ /dev/null @@ -1,115 +0,0 @@ -app = $app; - } - - /** - * Get a Redis connection instance. - * - * @param string $name - * @return \Illuminate\Redis\Database - */ - public function connection($name = null) - { - if ( ! isset($this->connections[$name])) - { - $this->connections[$name] = $this->createConnection($name); - } - - return $this->connections[$name]; - } - - /** - * Create the given connection by name. - * - * @param string $name - * @return \Illuminate\Redis\Database - */ - protected function createConnection($name) - { - $config = $this->getConfig($name); - - // Redis may optionally have a password. So, we will attempt to extract out - // the password from the configuration. But one is not required so we'll - // just use array_get to return null if one hasn't been set in config. - $password = array_get($config, 'password'); - - $connection = new Database($config['host'], $config['port'], $config['database'], $password); - - $connection->connect(); - - return $connection; - } - - /** - * Get the configuration for a connection. - * - * @param string $name - * @return array - */ - protected function getConfig($name) - { - $name = $name ?: $this->getDefaultConnection(); - - // To get the database connection configuration, we will just pull each of the - // connection configurations and get the configurations for the given name. - // If the configuration doesn't exist, we'll throw an exception and bail. - $connections = $this->app['config']['database.redis']; - - if (is_null($config = array_get($connections, $name))) - { - throw new \InvalidArgumentException("Redis [$name] not configured."); - } - - return $config; - } - - /** - * Get the default connection name. - * - * @return string - */ - protected function getDefaultConnection() - { - return 'default'; - } - - /** - * Dynamically pass methods to the default connection. - * - * @param string $method - * @param array $parameters - * @return mixed - */ - public function __call($method, $parameters) - { - return call_user_func_array(array($this->connection(), $method), $parameters); - } - -} \ No newline at end of file diff --git a/src/Illuminate/Redis/RedisServiceProvider.php b/src/Illuminate/Redis/RedisServiceProvider.php index f05e38c501e0..55e79851527b 100644 --- a/src/Illuminate/Redis/RedisServiceProvider.php +++ b/src/Illuminate/Redis/RedisServiceProvider.php @@ -20,7 +20,7 @@ public function register() { $this->app['redis'] = $this->app->share(function($app) { - return new RedisManager($app); + return new Database($app['config']['database.redis']); }); } diff --git a/src/Illuminate/Redis/composer.json b/src/Illuminate/Redis/composer.json index cafbdc02e6b3..ca4949ad801c 100644 --- a/src/Illuminate/Redis/composer.json +++ b/src/Illuminate/Redis/composer.json @@ -9,7 +9,8 @@ ], "require": { "php": ">=5.3.0", - "illuminate/support": "4.0.x" + "illuminate/support": "4.0.x", + "predis/predis": "0.*" }, "require-dev": { "mockery/mockery": "0.7.2", diff --git a/tests/Redis/RedisDatabaseTest.php b/tests/Redis/RedisDatabaseTest.php deleted file mode 100644 index d862b3fbb081..000000000000 --- a/tests/Redis/RedisDatabaseTest.php +++ /dev/null @@ -1,94 +0,0 @@ -getMock('Illuminate\Redis\Database', array('openSocket', 'command'), array('127.0.0.1', 100)); - $redis->expects($this->once())->method('openSocket'); - $redis->expects($this->once())->method('command')->with($this->equalTo('select'), $this->equalTo(array(0))); - - $redis->connect(); - } - - - public function testCommandMethodIssuesCommand() - { - $redis = $this->getMock('Illuminate\Redis\Database', array('fileWrite', 'fileGet', 'buildCommand', 'parseResponse'), array('127.0.0.1', 100)); - $redis->expects($this->once())->method('fileWrite')->with($this->equalTo('built')); - $redis->expects($this->once())->method('buildCommand')->with($this->equalTo('foo'), $this->equalTo(array('bar')))->will($this->returnValue('built')); - $redis->expects($this->once())->method('fileGet')->with($this->equalTo(512))->will($this->returnValue('results')); - $redis->expects($this->once())->method('parseResponse')->with($this->equalTo('results'))->will($this->returnValue('parsed')); - - $this->assertEquals('parsed', $redis->command('foo', array('bar'))); - } - - - public function testInlineParsing() - { - $redis = new Database('127.0.0.1', 100); - $response = $redis->parseResponse('+OK'); - - $this->assertEquals('OK', $response); - } - - - public function testIntegerInlineResponse() - { - $redis = new Database('127.0.0.1', 100); - $response = $redis->parseResponse(":1\r\n"); - - $this->assertEquals(1, $response); - } - - - public function testBulkResponse() - { - $redis = m::mock('Illuminate\Redis\Database[fileRead]'); - $redis->shouldReceive('fileRead')->once()->with(3)->andReturn('foo'); - $redis->shouldReceive('fileRead')->once()->with(2); - - $this->assertEquals('foo', $redis->parseResponse("$3\r\nfoo\r\n")); - } - - - public function testLongBulkResponse() - { - $redis = m::mock('Illuminate\Redis\Database[fileRead]'); - $redis->shouldReceive('fileRead')->once()->with(1024)->andReturn('foo'); - $redis->shouldReceive('fileRead')->once()->with(10)->andReturn('bar'); - $redis->shouldReceive('fileRead')->once()->with(2); - - $this->assertEquals('foobar', $redis->parseResponse("$1034\r\nfoo\r\n")); - } - - - public function testMultiBulkResponse() - { - $redis = m::mock('Illuminate\Redis\Database[fileGet,fileRead]'); - $redis->shouldReceive('fileGet')->twice()->with(512)->andReturn('$3'); - $redis->shouldReceive('fileRead')->twice()->with(3)->andReturn('foo', 'bar'); - $redis->shouldReceive('fileRead')->twice()->with(2); - - $this->assertEquals(array('foo', 'bar'), $redis->parseResponse("*2\r\n$3\r\nfoo\r\n$3\r\nbar\r\n")); - } - - - public function testCommandsAreBuiltProperly() - { - $redis = new Database('127.0.0.1', 100); - $command = $redis->buildCommand('lpush', array('list', 'taylor')); - - $this->assertEquals("*3\r\n$5\r\nLPUSH\r\n$4\r\nlist\r\n$6\r\ntaylor\r\n", $command); - } - -} \ No newline at end of file