From ba6e5bef94cd6e4ae32b3d26672af079ea194851 Mon Sep 17 00:00:00 2001 From: "Igor V. Gulyaev" Date: Thu, 12 Jan 2012 15:11:29 +0400 Subject: [PATCH 1/5] * redis cache peer simple implementation, based on Memcached CachePeer --- core/Cache/CachePeer.class.php | 2 +- core/Cache/Memcached.class.php | 7 +- core/Cache/Redis.class.php | 374 ++++++++++++++++++++++++ core/Cache/WatermarkedPeer.class.php | 21 +- test/core/MemcachedTest.class.php | 68 +++-- test/core/WatermarkedPeerTest.class.php | 54 ++++ 6 files changed, 493 insertions(+), 33 deletions(-) create mode 100644 core/Cache/Redis.class.php create mode 100644 test/core/WatermarkedPeerTest.class.php diff --git a/core/Cache/CachePeer.class.php b/core/Cache/CachePeer.class.php index 1fe10efe2c..35ea662b27 100644 --- a/core/Cache/CachePeer.class.php +++ b/core/Cache/CachePeer.class.php @@ -150,7 +150,7 @@ public function getList($indexes) foreach ($indexes as $key) if (null !== ($value = $this->get($key))) - $out[] = $value; + $out[$key] = $value; 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/core/Cache/WatermarkedPeer.class.php b/core/Cache/WatermarkedPeer.class.php index c696b68e9a..3319db692d 100644 --- a/core/Cache/WatermarkedPeer.class.php +++ b/core/Cache/WatermarkedPeer.class.php @@ -110,10 +110,23 @@ public function decrement($key, $value) public function getList($indexes) { - foreach ($indexes as &$index) - $index = $this->getActualWatermark().$index; - - return $this->peer->getList($indexes); + $peerIndexMap = array(); + foreach ($indexes as $index) + $peerIndexMap[$this->getActualWatermark().$index] = $index; + + $peerIndexes = array_keys($peerIndexMap); + $peerResult = $this->peer->getList($peerIndexes); + + $result = array(); + if (!empty($peerResult)) { + foreach ($peerResult as $key => $value) { + $result[$peerIndexMap[$key]] = $value; + } + } else { + $result = $peerResult; + } + + return $result; } public function get($key) diff --git a/test/core/MemcachedTest.class.php b/test/core/MemcachedTest.class.php index 79f6c9641e..13016378e7 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,29 @@ 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 = 'Loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong 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->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 +117,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 new file mode 100644 index 0000000000..1a4ed0b4ca --- /dev/null +++ b/test/core/WatermarkedPeerTest.class.php @@ -0,0 +1,54 @@ +multiGet(new RuntimeMemory()); + $this->multiGet(new Redis()); + } + + protected function multiGet(CachePeer $peer) + { + $cache = new WatermarkedPeer($peer); + + $cache->clean(); + + $cache->set('a', 'a', Cache::EXPIRES_MEDIUM); + $cache->set('b', 2, Cache::EXPIRES_MEDIUM); + $cache->set('c', 42.28, Cache::EXPIRES_MEDIUM); + + $this->assertEquals($cache->get('a'), 'a'); + $this->assertEquals($cache->get('b'), 2); + $this->assertEquals($cache->get('c'), 42.28); + + $list = $cache->getList(array('a', 'b', 'c')); + + $this->assertEquals(count($list), 3); + + $this->assertEquals($list['a'], 'a'); + $this->assertEquals($list['b'], 2); + $this->assertEquals($list['c'], 42.28); + + $list = $cache->getList(array('a')); + + $this->assertEquals(count($list), 1); + + $this->assertEquals($list['a'], 'a'); + + $list = $cache->getList(array('a', 'b', 'c', 'd')); + + $this->assertEquals(count($list), 3); + + $this->assertEquals($list['a'], 'a'); + $this->assertEquals($list['b'], 2); + $this->assertEquals($list['c'], 42.28); + + $list = $cache->getList(array('d')); + + $this->assertEquals(count($list), 0); + + $cache->clean(); + } + } +?> \ No newline at end of file From ff4754eafc01dc47169202909cea65682c445336 Mon Sep 17 00:00:00 2001 From: "Evgeniy V. Kokovikhin" Date: Mon, 26 Dec 2011 18:04:08 +0800 Subject: [PATCH 2/5] throw exception for null values --- core/DB/Dialect.class.php | 21 ++++++++++++--------- test/core/LogicTest.class.php | 8 ++++++++ 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/core/DB/Dialect.class.php b/core/DB/Dialect.class.php index 5c6b9e4641..1323e24b9f 100644 --- a/core/DB/Dialect.class.php +++ b/core/DB/Dialect.class.php @@ -100,17 +100,20 @@ public function toValueString($expression) private function toNeededString($expression, $method) { + if (null === $expression) + throw new WrongArgumentException( + 'not null expression expected' + ); + $string = null; - if (null !== $expression) { - if ($expression instanceof DialectString) { - if ($expression instanceof Query) - $string .= '('.$expression->toDialectString($this).')'; - else - $string .= $expression->toDialectString($this); - } else { - $string .= $this->$method($expression); - } + if ($expression instanceof DialectString) { + if ($expression instanceof Query) + $string .= '('.$expression->toDialectString($this).')'; + else + $string .= $expression->toDialectString($this); + } else { + $string .= $this->$method($expression); } return $string; diff --git a/test/core/LogicTest.class.php b/test/core/LogicTest.class.php index bfaa3834e7..83b20e5c4c 100644 --- a/test/core/LogicTest.class.php +++ b/test/core/LogicTest.class.php @@ -210,6 +210,14 @@ public function testBaseSqlGeneration() '(- a)', Expression::minus('a')->toDialectString($dialect) ); + + try { + Expression::eq('id', null)->toDialectString($dialect); + + $this->fail(); + } catch (WrongArgumentException $e) { + //it's Ok + } } public function testPgGeneration() From c0a2e174224e1176aa14b77b2036905d35b446de Mon Sep 17 00:00:00 2001 From: "Evgeniy V. Kokovikhin" Date: Wed, 11 Jan 2012 15:07:41 +0800 Subject: [PATCH 3/5] + changelog --- doc/ChangeLog | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/doc/ChangeLog b/doc/ChangeLog index 5407f828ee..c19bd04dbc 100644 --- a/doc/ChangeLog +++ b/doc/ChangeLog @@ -1,3 +1,8 @@ +2012-01-11 Evgeny V. Kokovikhin + + * core/DB/Dialect.class.php: throw exception for null values. Thanks to + Nikita V. Konstantinov. + 2011-11-21 Alexey S. Denisov, Evgeny V. Kokovikhin * meta/types/ObjectType.class.php, test/misc/DAOTest.class.php: changed logic From e2a15d27526a0c47a4ef5ad6abb53def4c690b95 Mon Sep 17 00:00:00 2001 From: "Igor V. Gulyaev" Date: Fri, 13 Jan 2012 09:55:49 +0400 Subject: [PATCH 4/5] * phpdoc type hint --- core/Cache/BaseAggregateCache.class.php | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) 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; } From f6f19af96d85818f7185869dd554af26c35a2f38 Mon Sep 17 00:00:00 2001 From: "Igor V. Gulyaev" Date: Fri, 13 Jan 2012 09:56:43 +0400 Subject: [PATCH 5/5] * bit more cache test cases --- test/core/MemcachedTest.class.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/test/core/MemcachedTest.class.php b/test/core/MemcachedTest.class.php index 13016378e7..7796b2382a 100644 --- a/test/core/MemcachedTest.class.php +++ b/test/core/MemcachedTest.class.php @@ -56,7 +56,7 @@ protected function clientTestSingleGet(CachePeer $cache) $cache->append('a', $value); $this->assertEquals($cache->get('a'), $value.$value); - $value = 'Loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong string'; + $value = 'L'.str_repeat('o', 256).'ng string'; $cache->set('a', $value, Cache::EXPIRES_MEDIUM); $this->assertEquals($cache->get('a'), $value); @@ -69,6 +69,12 @@ protected function clientTestSingleGet(CachePeer $cache) $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(); }