diff --git a/core/Cache/BaseAggregateCache.class.php b/core/Cache/BaseAggregateCache.class.php index 50ea2d956c..e9f31d6875 100644 --- a/core/Cache/BaseAggregateCache.class.php +++ b/core/Cache/BaseAggregateCache.class.php @@ -118,12 +118,17 @@ public function getList($indexes) foreach ($indexes as $index) $labels[$this->guessLabel($index)][] = $index; - foreach ($labels as $label => $indexList) - if ($this->peers[$label]['object']->isAlive()) { - if ($list = $this->peers[$label]['object']->getList($indexList)) + foreach ($labels as $label => $indexList) { + + /** @var CachePeer $peer **/ + $peer = $this->peers[$label]['object']; + + if ($peer->isAlive()) { + if ($list = $peer->getList($indexList)) $out = array_merge($out, $list); } else $this->checkAlive(); + } return $out; } diff --git a/core/Cache/Memcached.class.php b/core/Cache/Memcached.class.php index f05fe55acb..d414f8134a 100644 --- a/core/Cache/Memcached.class.php +++ b/core/Cache/Memcached.class.php @@ -177,8 +177,11 @@ public function delete($index, $time = null) public function append($key, $data) { - $packed = serialize($data); - +// WHY? +// see $this->store() check type need +// $packed = serialize($data); + $packed = $data; + $length = strlen($packed); // flags and exptime are ignored diff --git a/core/Cache/Redis.class.php b/core/Cache/Redis.class.php new file mode 100644 index 0000000000..d6f4c578e3 --- /dev/null +++ b/core/Cache/Redis.class.php @@ -0,0 +1,374 @@ +link = fsockopen($host, $port, $errno, $errstr, 1)) { + $this->alive = true; + + $this->buffer = $buffer; + + stream_set_blocking($this->link, true); + } + } catch (BaseException $e) {/*_*/} + } + + public function __destruct() + { + try { + fclose($this->link); + } catch (BaseException $e) {/*_*/} + } + + /** + * @return Redis + */ + public function setTimeout($microseconds) + { + Assert::isGreater($microseconds, 0); + + $this->timeout = $microseconds; + + if ($this->alive) { + $seconds = floor($microseconds / 1000); + $fraction = $microseconds - ($seconds * 1000); + + stream_set_timeout($this->link, $seconds, $fraction); + } + + return $this; + } + + /** + * @return Redis + **/ + public function clean() + { + if (!$this->link) { + $this->alive = false; + return null; + } + + $this->sendRequest(array('flushall')); + $this->getResponse(); + + return parent::clean(); + } + + public function getList($indexes) + { + if (!$this->link) { + $this->alive = false; + return null; + } + + $command = array_merge(array('mget'), $indexes); + + if (!$this->sendRequest($command)) + return null; + + $response = $this->getResponse(); + if (is_array($response)) { + $response = array_combine($indexes, $response); + $response = array_filter($response, array('Redis', 'emptyFilter')); + } + + return $response; + } + + public static function emptyFilter($var) + { + return ($var !== null); + } + + public function increment($key, $value) + { + return $this->changeInteger('incrby', $key, $value); + } + + public function decrement($key, $value) + { + return $this->changeInteger('decrby', $key, $value); + } + + public function get($index) + { + if (!$this->link) { + $this->alive = false; + return null; + } + + if (!$this->sendRequest(array('get', $index))) + return null; + + return $this->getResponse(); + } + + public function delete($index, $time = null) + { + if (!$this->sendRequest(array('del', $index))) + return false; + + try { + $response = $this->getResponse(); + } catch (BaseException $e) { + return false; + } + + if ($this->isTimeout()) + return false; + + return ($response == '1'); + } + + public function append($key, $data) + { + $value = $this->get($key); + if (!$value) + $value = ''; + + if (!$this->sendRequest(array('append', $key, $value))) + return false; + + $response = $this->getResponse(); + + if ($this->isTimeout()) + return false; + + if ($response == "OK") + return true; + + return false; + } + + protected function store( + $method, $index, $value, $expires = Cache::EXPIRES_MINIMUM + ) + { + if ($expires === Cache::DO_NOT_CACHE) + return false; + +// incrby decrby append not work properly with expire +// $methodMap = array( +// 'add' => 'setex', +// 'replace' => 'setex', +// 'set' =>' setex' +// ); + + $methodMap = array( + 'add' => 'set', + 'replace' => 'set', + 'set' => 'set' + ); + + $method = $methodMap[$method]; + +// incrby decrby append not work properly with expire +// if (!$this->sendRequest(array($method, $index, $expires, $value))) + if (!$this->sendRequest(array($method, $index, $value))) + return false; + + $response = $this->getResponse(); + + if ($this->isTimeout()) + return false; + + if ($response == "OK") + return true; + + return false; + } + + private function changeInteger($command, $key, $value) + { + if (!$this->link) + return null; + + if (!$this->sendRequest(array($command, $key, $value))) + return null; + + try { + $response = $this->getResponse(); + } catch (BaseException $e) { + return null; + } + + if ($this->isTimeout()) + return null; + + if (is_numeric($response)) + return (int) $response; + + return null; + } + + private function getResponse() + { + $parserMap = array( + '+' => 'singleLine', + '-' => 'error', + ':' => 'integer', + '$' => 'bulk', + '*' => 'multiBulk', + ); + + $responseType = null; + $buffer = fgets($this->link, 8192); + if ($buffer) { + $responseType = substr($buffer, 0, 1); + $response = $buffer; + } + + if ($this->isTimeout()) + return null; + + if (key_exists($responseType, $parserMap)) { + $parseMethod = 'parse'.ucfirst($parserMap[$responseType]); + + $response = rtrim($response, "\r\n "); + $response = mb_substr($response, 1, mb_strlen($response)-1); + return $this->$parseMethod(trim($response, "\r\n ")); + } else { +// throw new WrongArgumentException('unknown response type in '.$response); + return $response; + } + } + + private function parseMultiBulk($response) + { + $result = array(); + for ($i=0; $i < $response; $i++) { + $buffer = fgets($this->link, 8192); + $buffer = rtrim($buffer, "\r\n "); + $buffer = mb_substr($buffer, 1, mb_strlen($buffer)-1); + $result[] = $this->parseBulk($buffer); + } + return $result; + } + + private function parseBulk($response) + { + if ($response == -1) { + return null; + } + $buffer = fgets($this->link, 8192); + $result = rtrim($buffer, "\r\n "); + + if (strlen($result) != $response) { + return null; + } + + return $result; + } + + private function parseError($response) + { + return null; + } + + private function parseInteger($response) + { + return $response; + } + + private function parseSingleLine($response) + { + return $response; + } + + private function sendRequest(array $command) + { + $commandString = '*'.count($command)."\r\n"; + foreach ($command as $item) { + $commandString .= '$'.strlen($item)."\r\n".$item."\r\n"; + } + + $commandLenght = strlen($commandString); + + if ($commandLenght > $this->buffer) { + $offset = 0; + while ($offset < $commandLenght) { + try { + $result = fwrite( + $this->link, + substr($commandString, $offset, $this->buffer) + ); + } catch (BaseException $e) { + return $this->alive = false; + } + + if ($result !== false) + $offset += $result; + else + return false; + } + } else { + try { + return + fwrite($this->link, $commandString, $commandLenght) !== false; + } catch (BaseException $e) { + return $this->alive = false; + } + } + + if ($this->isTimeout()) + return false; + + return true; + } + + private function isTimeout() + { + if (!$this->timeout) + return false; + + $meta = stream_get_meta_data($this->link); + + return $meta['timed_out']; + } + } +?> diff --git a/test/core/MemcachedTest.class.php b/test/core/MemcachedTest.class.php index 79f6c9641e..7796b2382a 100644 --- a/test/core/MemcachedTest.class.php +++ b/test/core/MemcachedTest.class.php @@ -5,21 +5,27 @@ final class MemcachedTest extends TestCase { public function testClients() { - $this->clientTest('PeclMemcached'); - $this->clientTest('Memcached'); + $this->clientTest(new PeclMemcached('localhost')); + $this->clientTest(new Memcached('localhost')); + $this->clientTest(Redis::create()); } public function testWrongKeys() { - $this->doTestWrongKeys('Memcached'); - $this->doTestWrongKeys('PeclMemcached'); + $this->doTestWrongKeys(new Memcached('localhost')); + $this->doTestWrongKeys(new PeclMemcached('localhost')); + $this->doTestWrongKeys(Redis::create()); } public function testWithTimeout() { - $cache = - Memcached::create('localhost')-> - setTimeout(200); + $this->withTimeout(Memcached::create('localhost')); +// $this->withTimeout(new Redis()); + } + + protected function withTimeout(CachePeer $cache) + { + $cache->setTimeout(200); $cache->add('a', 'b'); @@ -27,19 +33,17 @@ public function testWithTimeout() $cache->clean(); } - - protected function clientTest($className) + + protected function clientTest(CachePeer $cache) { - $this->clientTestSingleGet($className); - $this->clientTestMultiGet($className); + $this->clientTestSingleGet($cache); + $this->clientTestMultiGet($cache); } - protected function clientTestSingleGet($className) + protected function clientTestSingleGet(CachePeer $cache) { - $cache = new $className('localhost'); - if (!$cache->isAlive()) { - return $this->markTestSkipped('memcached not available'); + return $this->markTestSkipped('cache not available'); } $cache->clean(); @@ -47,16 +51,35 @@ protected function clientTestSingleGet($className) $value = 'a'; $cache->set('a', $value, Cache::EXPIRES_MEDIUM); - - $this->assertEquals($cache->get('a'), 'a'); - + $this->assertEquals($cache->get('a'), $value); + + $cache->append('a', $value); + $this->assertEquals($cache->get('a'), $value.$value); + + $value = 'L'.str_repeat('o', 256).'ng string'; + $cache->set('a', $value, Cache::EXPIRES_MEDIUM); + $this->assertEquals($cache->get('a'), $value); + + $cache->delete('a'); + + $cache->set('a', 1, Cache::EXPIRES_MEDIUM); + $this->assertEquals($cache->get('a'), 1); + $cache->increment('a', 1); + $this->assertEquals($cache->get('a'), 2); + $cache->decrement('a', 2); + $this->assertEquals($cache->get('a'), 0); + + $cache->set('c', 42.28, Cache::EXPIRES_MEDIUM); + $this->assertEquals($cache->get('c'), 42.28); + + $cache->replace('c', 42.297, Cache::EXPIRES_MEDIUM); + $this->assertEquals($cache->get('c'), 42.297); + $cache->clean(); } - protected function clientTestMultiGet($className) + protected function clientTestMultiGet(CachePeer $cache) { - $cache = new $className('localhost'); - if (!$cache->isAlive()) { return $this->markTestSkipped('memcached not available'); } @@ -100,12 +123,11 @@ protected function clientTestMultiGet($className) $cache->clean(); } - private function doTestWrongKeys($classname) + private function doTestWrongKeys(CachePeer $cache) { - $peer = new $classname('localhost'); - $peer->get(null); - - $this->assertTrue($peer->isAlive()); + $this->assertNull($cache->get(null)); + + $this->assertTrue($cache->isAlive()); } } ?> \ No newline at end of file diff --git a/test/core/WatermarkedPeerTest.class.php b/test/core/WatermarkedPeerTest.class.php index 4d9e8eb0a8..1a4ed0b4ca 100644 --- a/test/core/WatermarkedPeerTest.class.php +++ b/test/core/WatermarkedPeerTest.class.php @@ -3,10 +3,14 @@ final class WatermarkedPeerTest extends TestCase { - public function testMultiGet() - { + public function testMultiGet() { + $this->multiGet(new RuntimeMemory()); + $this->multiGet(new Redis()); + } - $cache = new WatermarkedPeer(new RuntimeMemory()); + protected function multiGet(CachePeer $peer) + { + $cache = new WatermarkedPeer($peer); $cache->clean();