diff --git a/lib/Cake/Cache/Engine/RedisEngine.php b/lib/Cake/Cache/Engine/RedisEngine.php new file mode 100644 index 000000000..0ec2270cd --- /dev/null +++ b/lib/Cake/Cache/Engine/RedisEngine.php @@ -0,0 +1,216 @@ + 'Redis', + 'prefix' => null, + 'server' => '127.0.0.1', + 'port' => null, + 'persistent' => true, + 'serialize' => true + ), $settings) + ); + + return $this->_connect(); + } + +/** + * Connects to a Redis server + * + * @return boolean True if Redis server was connected + */ + protected function _connect() { + $return = false; + try { + $this->_Redis = new Redis(); + if (empty($this->settings['persistent'])) { + $return = $this->_Redis->connect($this->settings['server']); + } else { + $return = $this->_Redis->pconnect($this->settings['server']); + } + } catch (RedisException $e) { + return false; + } + return $return; + } + +/** + * Write data for key into cache. + * + * @param string $key Identifier for the data + * @param mixed $value Data to be cached + * @param integer $duration How long to cache the data, in seconds + * @return boolean True if the data was successfully cached, false on failure + */ + public function write($key, $value, $duration) { + if (!is_int($value)) { + $value = serialize($value); + } + if ($duration === 0) { + return $this->_Redis->set($key, $value); + } + + return $this->_Redis->setex($key, $duration, $value); + } + +/** + * Read a key from the cache + * + * @param string $key Identifier for the data + * @return mixed The cached data, or false if the data doesn't exist, has expired, or if there was an error fetching it + */ + public function read($key) { + $value = $this->_Redis->get($key); + if (ctype_digit($value)) { + $value = (int) $value; + } + if ($value !== false && is_string($value)) { + $value = unserialize($value); + } + return $value; + } + +/** + * Increments the value of an integer cached key + * + * @param string $key Identifier for the data + * @param integer $offset How much to increment + * @return New incremented value, false otherwise + * @throws CacheException when you try to increment with compress = true + */ + public function increment($key, $offset = 1) { + return (int) $this->_Redis->incrBy($key, $offset); + } + +/** + * Decrements the value of an integer cached key + * + * @param string $key Identifier for the data + * @param integer $offset How much to subtract + * @return New decremented value, false otherwise + * @throws CacheException when you try to decrement with compress = true + */ + public function decrement($key, $offset = 1) { + return (int) $this->_Redis->decrBy($key, $offset); + } + +/** + * Delete a key from the cache + * + * @param string $key Identifier for the data + * @return boolean True if the value was successfully deleted, false if it didn't exist or couldn't be removed + */ + public function delete($key) { + return $this->_Redis->delete($key) > 0; + } + +/** + * Delete all keys from the cache + * + * @param boolean $check + * @return boolean True if the cache was successfully cleared, false otherwise + */ + public function clear($check) { + if ($check) { + return true; + } + $keys = $this->_Redis->getKeys($this->settings['prefix'] . '*'); + $this->_Redis->del($keys); + + return true; + } + +/** + * Returns the `group value` for each of the configured groups + * If the group initial value was not found, then it initializes + * the group accordingly. + * + * @return array + **/ + public function groups() { + $result = array(); + foreach ($this->settings['groups'] as $group) { + $value = $this->_Redis->get($this->settings['prefix'] . $group); + if (!$value) { + $value = 1; + $this->_Redis->set($this->settings['prefix'] . $group, $value); + } + $result[] = $group . $value; + } + return $result; + } + +/** + * Increments the group value to simulate deletion of all keys under a group + * old values will remain in storage until they expire. + * + * @return boolean success + **/ + public function clearGroup($group) { + return (bool)$this->_Redis->incr($this->settings['prefix'] . $group); + } + +/** + * Disconnects from the redis server + * + * @return voind + **/ + public function __destruct() { + if (!$this->settings['persistent']) { + $this->_Redis->close(); + } + } +} diff --git a/lib/Cake/Test/Case/Cache/Engine/RedisEngineTest.php b/lib/Cake/Test/Case/Cache/Engine/RedisEngineTest.php new file mode 100644 index 000000000..bf0d5b7de --- /dev/null +++ b/lib/Cake/Test/Case/Cache/Engine/RedisEngineTest.php @@ -0,0 +1,336 @@ + + * Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org) + * + * Licensed under The MIT License + * Redistributions of files must retain the above copyright notice + * + * @copyright Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org) + * @link http://book.cakephp.org/view/1196/Testing CakePHP(tm) Tests + * @package Cake.Test.Case.Cache.Engine + * @since CakePHP(tm) v 2.2 + * @license MIT License (http://www.opensource.org/licenses/mit-license.php) + */ + +App::uses('Cache', 'Cache'); +App::uses('RedisEngine', 'Cache/Engine'); + +/** + * RedisEngineTest class + * + * @package Cake.Test.Case.Cache.Engine + */ +class RegisEngineTest extends CakeTestCase { + +/** + * setUp method + * + * @return void + */ + public function setUp() { + $this->skipIf(!class_exists('Redis'), 'Redis is not installed or configured properly.'); + + $this->_cacheDisable = Configure::read('Cache.disable'); + Configure::write('Cache.disable', false); + Cache::config('redis', array( + 'engine' => 'Redis', + 'prefix' => 'cake_', + 'duration' => 3600 + )); + } + +/** + * tearDown method + * + * @return void + */ + public function tearDown() { + Configure::write('Cache.disable', $this->_cacheDisable); + Cache::drop(''); + Cache::drop('redis_groups'); + Cache::drop('redis_helper'); + Cache::config('default'); + } + +/** + * testSettings method + * + * @return void + */ + public function testSettings() { + $settings = Cache::settings('redis'); + unset($settings['serialize'], $settings['path']); + $expecting = array( + 'port' => null, + 'prefix' => 'cake_', + 'duration' => 3600, + 'probability' => 100, + 'groups' => array(), + 'engine' => 'Redis', + 'server' => '127.0.0.1', + 'persistent' => true + ); + $this->assertEquals($expecting, $settings); + } + +/** + * testConnect method + * + * @return void + */ + public function testConnect() { + $Redis = new RedisEngine(); + $this->assertTrue($Redis->init(Cache::settings('redis'))); + } + +/** + * testReadAndWriteCache method + * + * @return void + */ + public function testReadAndWriteCache() { + Cache::set(array('duration' => 1), null, 'redis'); + + $result = Cache::read('test', 'redis'); + $expecting = ''; + $this->assertEquals($expecting, $result); + + $data = 'this is a test of the emergency broadcasting system'; + $result = Cache::write('test', $data, 'redis'); + $this->assertTrue($result); + + $result = Cache::read('test', 'redis'); + $expecting = $data; + $this->assertEquals($expecting, $result); + + $data = array(1, 2, 3); + $this->assertTrue(Cache::write('array_data', $data, 'redis')); + $this->assertEquals($data, Cache::read('array_data', 'redis')); + + Cache::delete('test', 'redis'); + } + +/** + * testExpiry method + * + * @return void + */ + public function testExpiry() { + Cache::set(array('duration' => 1), 'redis'); + + $result = Cache::read('test', 'redis'); + $this->assertFalse($result); + + $data = 'this is a test of the emergency broadcasting system'; + $result = Cache::write('other_test', $data, 'redis'); + $this->assertTrue($result); + + sleep(2); + $result = Cache::read('other_test', 'redis'); + $this->assertFalse($result); + + Cache::set(array('duration' => "+1 second"), 'redis'); + + $data = 'this is a test of the emergency broadcasting system'; + $result = Cache::write('other_test', $data, 'redis'); + $this->assertTrue($result); + + sleep(2); + $result = Cache::read('other_test', 'redis'); + $this->assertFalse($result); + + Cache::config('redis', array('duration' => '+1 second')); + sleep(2); + + $result = Cache::read('other_test', 'redis'); + $this->assertFalse($result); + + Cache::config('redis', array('duration' => '+29 days')); + $data = 'this is a test of the emergency broadcasting system'; + $result = Cache::write('long_expiry_test', $data, 'redis'); + $this->assertTrue($result); + + sleep(2); + $result = Cache::read('long_expiry_test', 'redis'); + $expecting = $data; + $this->assertEquals($expecting, $result); + + Cache::config('redis', array('duration' => 3600)); + } + +/** + * testDeleteCache method + * + * @return void + */ + public function testDeleteCache() { + $data = 'this is a test of the emergency broadcasting system'; + $result = Cache::write('delete_test', $data, 'redis'); + $this->assertTrue($result); + + $result = Cache::delete('delete_test', 'redis'); + $this->assertTrue($result); + } + +/** + * testDecrement method + * + * @return void + */ + public function testDecrement() { + Cache::delete('test_decrement', 'redis'); + $result = Cache::write('test_decrement', 5, 'redis'); + $this->assertTrue($result); + + $result = Cache::decrement('test_decrement', 1, 'redis'); + $this->assertEquals(4, $result); + + $result = Cache::read('test_decrement', 'redis'); + $this->assertEquals(4, $result); + + $result = Cache::decrement('test_decrement', 2, 'redis'); + $this->assertEquals(2, $result); + + $result = Cache::read('test_decrement', 'redis'); + $this->assertEquals(2, $result); + } + +/** + * testIncrement method + * + * @return void + */ + public function testIncrement() { + Cache::delete('test_increment', 'redis'); + $result = Cache::increment('test_increment', 1, 'redis'); + $this->assertEquals(1, $result); + + $result = Cache::read('test_increment', 'redis'); + $this->assertEquals(1, $result); + + $result = Cache::increment('test_increment', 2, 'redis'); + $this->assertEquals(3, $result); + + $result = Cache::read('test_increment', 'redis'); + $this->assertEquals(3, $result); + } + + +/** + * test clearing redis. + * + * @return void + */ + public function testClear() { + Cache::config('redis2', array( + 'engine' => 'Redis', + 'prefix' => 'cake2_', + 'duration' => 3600 + )); + + Cache::write('some_value', 'cache1', 'redis'); + $result = Cache::clear(true, 'redis'); + $this->assertTrue($result); + $this->assertEquals('cache1', Cache::read('some_value', 'redis')); + + Cache::write('some_value', 'cache2', 'redis2'); + $result = Cache::clear(false, 'redis'); + $this->assertTrue($result); + $this->assertFalse(Cache::read('some_value', 'redis')); + $this->assertEquals('cache2', Cache::read('some_value', 'redis2')); + + Cache::clear(false, 'redis2'); + } + +/** + * test that a 0 duration can successfully write. + * + * @return void + */ + public function testZeroDuration() { + Cache::config('redis', array('duration' => 0)); + $result = Cache::write('test_key', 'written!', 'redis'); + + $this->assertTrue($result); + $result = Cache::read('test_key', 'redis'); + $this->assertEquals('written!', $result); + } + +/** + * Tests that configuring groups for stored keys return the correct values when read/written + * Shows that altering the group value is equivalent to deleting all keys under the same + * group + * + * @return void + */ + public function testGroupReadWrite() { + Cache::config('redis_groups', array( + 'engine' => 'Redis', + 'duration' => 3600, + 'groups' => array('group_a', 'group_b'), + 'prefix' => 'test_' + )); + Cache::config('redis_helper', array( + 'engine' => 'Redis', + 'duration' => 3600, + 'prefix' => 'test_' + )); + $this->assertTrue(Cache::write('test_groups', 'value', 'redis_groups')); + $this->assertEquals('value', Cache::read('test_groups', 'redis_groups')); + + Cache::increment('group_a', 1, 'redis_helper'); + $this->assertFalse(Cache::read('test_groups', 'redis_groups')); + $this->assertTrue(Cache::write('test_groups', 'value2', 'redis_groups')); + $this->assertEquals('value2', Cache::read('test_groups', 'redis_groups')); + + Cache::increment('group_b', 1, 'redis_helper'); + $this->assertFalse(Cache::read('test_groups', 'redis_groups')); + $this->assertTrue(Cache::write('test_groups', 'value3', 'redis_groups')); + $this->assertEquals('value3', Cache::read('test_groups', 'redis_groups')); + } + +/** + * Tests that deleteing from a groups-enabled config is possible + * + * @return void + */ + public function testGroupDelete() { + Cache::config('redis_groups', array( + 'engine' => 'Redis', + 'duration' => 3600, + 'groups' => array('group_a', 'group_b') + )); + $this->assertTrue(Cache::write('test_groups', 'value', 'redis_groups')); + $this->assertEquals('value', Cache::read('test_groups', 'redis_groups')); + $this->assertTrue(Cache::delete('test_groups', 'redis_groups')); + + $this->assertFalse(Cache::read('test_groups', 'redis_groups')); + } + +/** + * Test clearing a cache group + * + * @return void + **/ + public function testGroupClear() { + Cache::config('redis_groups', array( + 'engine' => 'Redis', + 'duration' => 3600, + 'groups' => array('group_a', 'group_b') + )); + + $this->assertTrue(Cache::write('test_groups', 'value', 'redis_groups')); + $this->assertTrue(Cache::clearGroup('group_a', 'redis_groups')); + $this->assertFalse(Cache::read('test_groups', 'redis_groups')); + + $this->assertTrue(Cache::write('test_groups', 'value2', 'redis_groups')); + $this->assertTrue(Cache::clearGroup('group_b', 'redis_groups')); + $this->assertFalse(Cache::read('test_groups', 'redis_groups')); + } + +} \ No newline at end of file