diff --git a/composer.json b/composer.json index 4a78e2e..0e58e7b 100644 --- a/composer.json +++ b/composer.json @@ -49,7 +49,8 @@ "latte/latte": "~2.3@dev", "tracy/tracy": "~2.3@dev", - "nette/tester": "~1.3@rc" + "nette/tester": "~1.3@rc", + "mockery/mockery": "~0.9" }, "autoload": { "psr-0": { diff --git a/docs/en/index.md b/docs/en/index.md index 406e278..8439863 100644 --- a/docs/en/index.md +++ b/docs/en/index.md @@ -124,3 +124,41 @@ You can disable the native driver by this option (and the emulated will take con redis: session: {native: off} ``` + +## Key namespace + +Key namespace gives you availability to create prefixed keys in Redis storage. + +You can define namespace for: journal, session, storage. + +To use key prefixed with namespace, add `namespace` option in configuration, for example: + +Instance 1 +```yml +redis: + host: 127.0.0.1 + port: 6379 + journal: + namespace: "instance1" + session: on + storage: on + debugger: off +``` + +Instance 2 +```yml +redis: + host: 127.0.0.1 + port: 6379 + journal: + namespace: "instance2" + session: on + storage: on + debugger: off +``` + +After configuration all keys will be prefixed "namespace:key" + +Example use case: +When you use two instances of one application with one Redis server, it is possible, that your data can be overwritten by second application instance. +To avoid this problem you can define key namespaces in configuration. diff --git a/src/Kdyby/Redis/DI/RedisExtension.php b/src/Kdyby/Redis/DI/RedisExtension.php index c507567..e513b3f 100644 --- a/src/Kdyby/Redis/DI/RedisExtension.php +++ b/src/Kdyby/Redis/DI/RedisExtension.php @@ -17,7 +17,6 @@ use Nette\DI\Config; - /** * @author Filip Procházka */ @@ -52,6 +51,7 @@ class RedisExtension extends Nette\DI\CompilerExtension 'lockAcquireTimeout' => FALSE, 'debugger' => '%debugMode%', 'versionCheck' => TRUE, + 'namespace' => NULL, ); /** @@ -155,8 +155,17 @@ protected function loadJournal(array $config) $builder = $this->getContainerBuilder(); + $journalConfig = Nette\DI\Config\Helpers::merge(is_array($config['journal']) ? $config['journal'] : array(), array( + 'namespace' => NULL, + )); + + $constructParams = array( + $this->prefix('@client'), + $journalConfig['namespace'], + ); + $builder->addDefinition($this->prefix('cacheJournal')) - ->setClass('Kdyby\Redis\RedisLuaJournal'); + ->setClass('Kdyby\Redis\RedisLuaJournal', $constructParams); // overwrite $journalService = $builder->getByType('Nette\Caching\Storages\IJournal') ?: 'nette.cacheJournal'; @@ -176,10 +185,17 @@ protected function loadStorage(array $config) $storageConfig = Nette\DI\Config\Helpers::merge(is_array($config['storage']) ? $config['storage'] : array(), array( 'locks' => TRUE, + 'namespace' => NULL, )); + $constructParams = array( + $this->prefix('@client'), + $this->prefix('@cacheJournal'), + $storageConfig['namespace'], + ); + $cacheStorage = $builder->addDefinition($this->prefix('cacheStorage')) - ->setClass('Kdyby\Redis\RedisStorage'); + ->setClass('Kdyby\Redis\RedisStorage', $constructParams); if (!$storageConfig['locks']) { $cacheStorage->addSetup('disableLocking'); @@ -206,7 +222,7 @@ protected function loadSession(array $config) 'weight' => 1, 'timeout' => $config['timeout'], 'database' => $config['database'], - 'prefix' => self::DEFAULT_SESSION_PREFIX, + 'prefix' => isset($config['namespace']) ? $config['namespace'] : self::DEFAULT_SESSION_PREFIX, 'auth' => $config['auth'], 'native' => TRUE, 'lockDuration' => $config['lockDuration'], @@ -220,15 +236,20 @@ protected function loadSession(array $config) if ($sessionConfig['native']) { $this->loadNativeSessionHandler($sessionConfig); + return; + } - } else { - $builder->addDefinition($this->prefix('sessionHandler')) - ->setClass('Kdyby\Redis\RedisSessionHandler', array($this->prefix('@sessionHandler_client'))); + $constructParams = array( + $this->prefix('@sessionHandler_client'), + $sessionConfig['namespace'], + ); - $sessionService = $builder->getByType('Nette\Http\Session') ?: 'session'; - $builder->getDefinition($sessionService) - ->addSetup('?->bind(?)', array($this->prefix('@sessionHandler'), '@self')); - } + $builder->addDefinition($this->prefix('sessionHandler')) + ->setClass('Kdyby\Redis\RedisSessionHandler', $constructParams); + + $sessionService = $builder->getByType('Nette\Http\Session') ?: 'session'; + $builder->getDefinition($sessionService) + ->addSetup('?->bind(?)', array($this->prefix('@sessionHandler'), '@self')); } diff --git a/src/Kdyby/Redis/ExclusiveLock.php b/src/Kdyby/Redis/ExclusiveLock.php index c0705d0..5e633ac 100644 --- a/src/Kdyby/Redis/ExclusiveLock.php +++ b/src/Kdyby/Redis/ExclusiveLock.php @@ -48,7 +48,6 @@ class ExclusiveLock extends Nette\Object public $acquireTimeout = FALSE; - /** * @param RedisClient $redisClient */ diff --git a/src/Kdyby/Redis/RedisJournal.php b/src/Kdyby/Redis/RedisJournal.php index 28f5210..4dd1499 100644 --- a/src/Kdyby/Redis/RedisJournal.php +++ b/src/Kdyby/Redis/RedisJournal.php @@ -14,7 +14,6 @@ use Nette\Caching\Cache; - /** * Redis journal for tags and priorities of cached values. * @@ -36,14 +35,22 @@ class RedisJournal extends Nette\Object implements Nette\Caching\Storages\IJourn */ protected $client; + /** + * @var string + */ + protected $namespace = ''; /** * @param RedisClient $client + * @param string|NULL $namespace */ - public function __construct(RedisClient $client) + public function __construct(RedisClient $client, $namespace = NULL) { $this->client = $client; + if (!empty($namespace)) { + $this->namespace = $namespace . ':'; + } } @@ -183,7 +190,7 @@ private function tagEntries($tag) */ protected function formatKey($key, $suffix = NULL) { - return self::NS_NETTE . ':' . $key . ($suffix ? ':' . $suffix : NULL); + return self::NS_NETTE . ':' . $this->namespace . $key . ($suffix ? ':' . $suffix : NULL); } } diff --git a/src/Kdyby/Redis/RedisLuaJournal.php b/src/Kdyby/Redis/RedisLuaJournal.php index 642d28d..14a0d61 100644 --- a/src/Kdyby/Redis/RedisLuaJournal.php +++ b/src/Kdyby/Redis/RedisLuaJournal.php @@ -49,7 +49,7 @@ public function clean(array $conds, Nette\Caching\IStorage $storage = NULL) $args = self::flattenDp($conds); - $result = $this->client->evalScript($this->getScript('clean'), array(), array($args)); + $result = $this->client->evalScript($this->getScript('clean'), array(), array($args, $this->namespace)); if (!is_array($result) && $result !== TRUE) { throw new RedisClientException("Failed to successfully execute lua script journal.clean(): " . $this->client->getDriver()->getLastError()); } diff --git a/src/Kdyby/Redis/RedisSessionHandler.php b/src/Kdyby/Redis/RedisSessionHandler.php index fb4ffea..6518104 100644 --- a/src/Kdyby/Redis/RedisSessionHandler.php +++ b/src/Kdyby/Redis/RedisSessionHandler.php @@ -40,6 +40,11 @@ class RedisSessionHandler extends Nette\Object implements \SessionHandlerInterfa */ private $client; + /** + * @var string + */ + private $namespace = ''; + /** * @var Nette\Http\Session */ @@ -51,13 +56,16 @@ class RedisSessionHandler extends Nette\Object implements \SessionHandlerInterfa private $ttl; - /** * @param RedisClient $redisClient + * @param string|NULL $namespace */ - public function __construct(RedisClient $redisClient) + public function __construct(RedisClient $redisClient, $namespace = NULL) { $this->client = $redisClient; + if (!empty($namespace)) { + $this->namespace = $namespace . ':'; + } } @@ -228,7 +236,8 @@ protected function lock($id) */ private function formatKey($id) { - return self::NS_NETTE . $id; + $key = $this->namespace . self::NS_NETTE . $id; + return $key; } } diff --git a/src/Kdyby/Redis/RedisStorage.php b/src/Kdyby/Redis/RedisStorage.php index 3c0999a..8655cb7 100644 --- a/src/Kdyby/Redis/RedisStorage.php +++ b/src/Kdyby/Redis/RedisStorage.php @@ -49,21 +49,29 @@ class RedisStorage extends Nette\Object implements IMultiReadStorage */ private $journal; + /** + * @var string + */ + private $namespace = ''; + /** * @var bool */ private $useLocks = TRUE; - /** * @param RedisClient $client * @param \Nette\Caching\Storages\IJournal $journal + * @param string|NULL $namespace */ - public function __construct(RedisClient $client, IJournal $journal = NULL) + public function __construct(RedisClient $client, IJournal $journal = NULL, $namespace = NULL) { $this->client = $client; $this->journal = $journal; + if (!empty($namespace)) { + $this->namespace = $namespace . ':'; + } } @@ -275,7 +283,7 @@ public function clean(array $conds) { // cleaning using file iterator if (!empty($conds[Cache::ALL])) { - if ($keys = $this->client->send('keys', array(self::NS_NETTE . ':*'))) { + if ($keys = $this->client->send('keys', array(self::NS_NETTE . ':' . $this->namespace . '*'))) { $this->client->send('del', $keys); } @@ -301,7 +309,7 @@ public function clean(array $conds) */ protected function formatEntryKey($key) { - return self::NS_NETTE . ':' . str_replace(Cache::NAMESPACE_SEPARATOR, ':', $key); + return self::NS_NETTE . ':' . $this->namespace . str_replace(Cache::NAMESPACE_SEPARATOR, ':', $key); } diff --git a/src/Kdyby/Redis/scripts/common.lua b/src/Kdyby/Redis/scripts/common.lua index a1a3b22..c704859 100644 --- a/src/Kdyby/Redis/scripts/common.lua +++ b/src/Kdyby/Redis/scripts/common.lua @@ -1,6 +1,13 @@ +local namespace = ARGV[2] +if namespace == nil then + namespace = "" +end + +rawset(_G, "namespace", namespace) + local formatKey = function (key, suffix) - local res = "Nette.Journal:" .. key + local res = "Nette.Journal:" .. namespace .. key if suffix ~= nil then res = res .. ":" .. suffix end @@ -9,7 +16,7 @@ local formatKey = function (key, suffix) end local formatStorageKey = function(key, suffix) - local res = "Nette.Storage:" .. key + local res = "Nette.Storage:" .. namespace .. key if suffix ~= nil then res = res .. ":" .. suffix end @@ -45,3 +52,4 @@ local cleanEntry = function (keys) -- redis.call('exec') end end + diff --git a/src/Kdyby/Redis/scripts/journal.clean.lua b/src/Kdyby/Redis/scripts/journal.clean.lua index abe74c1..0ab30ee 100644 --- a/src/Kdyby/Redis/scripts/journal.clean.lua +++ b/src/Kdyby/Redis/scripts/journal.clean.lua @@ -3,7 +3,7 @@ local conds = cjson.decode(ARGV[1]) if conds["all"] ~= nil then -- redis.call('multi') - for i, value in pairs(redis.call('keys', "Nette.Journal:*")) do + for i, value in pairs(redis.call('keys', "Nette.Journal:".. namespace .."*")) do redis.call('del', value) end -- redis.call('exec') diff --git a/tests/KdybyTests/Redis/ExclusiveLock.phpt b/tests/KdybyTests/Redis/ExclusiveLock.phpt index a8d0f46..e4ddbd3 100644 --- a/tests/KdybyTests/Redis/ExclusiveLock.phpt +++ b/tests/KdybyTests/Redis/ExclusiveLock.phpt @@ -41,6 +41,21 @@ class ExclusiveLockTest extends AbstractRedisTestCase }, 'Kdyby\Redis\LockException', 'Process ran too long. Increase lock duration, or extend lock regularly.'); } + public function testLockExpiredWithNamespace() + { + $client = $this->client; + Assert::exception(function () use ($client) { + $first = new ExclusiveLock($client, 'foo'); + $first->duration = 1; + + Assert::true($first->acquireLock('foo:bar')); + sleep(3); + + $first->increaseLockTimeout('foo:bar'); + + }, 'Kdyby\Redis\LockException', 'Process ran too long. Increase lock duration, or extend lock regularly.'); + } + public function testDeadlockHandling() @@ -56,6 +71,49 @@ class ExclusiveLockTest extends AbstractRedisTestCase Assert::true($second->acquireLock('foo:bar')); } + public function testDeadlockHandlingWithNamespace() + { + $first = new ExclusiveLock($this->client, 'foo'); + $first->duration = 1; + $second = new ExclusiveLock(new RedisClient()); + $second->duration = 1; + + Assert::true($first->acquireLock('foo:bar')); + sleep(3); // first died? + + Assert::true($second->acquireLock('foo:bar')); + } + + /** + * @return array + */ + public function getLockKeyTestData() + { + return array( + array('bar'), + array('foo:bar'), + array('xxx:foo:bar'), + ); + } + + /** + * @dataProvider getLockKeyTestData + * @param $key + */ + public function testLockKey($key) + { + $client = \Mockery::mock('Kdyby\Redis\RedisClient'); + $client->shouldReceive('setNX')->once()->andReturn(false); + $client->shouldReceive('get')->once()->with($key . ':lock')->andReturn(10); + $client->shouldReceive('getSet')->once()->andReturn(10); + $client->shouldReceive('del')->with($key . ':lock'); + $client->shouldReceive('close')->with(); + $lock = new ExclusiveLock($client); + $lock->duration = 5; + $lock->acquireLock($key); + Assert::true((0 < $lock->getLockTimeout($key))); + \Mockery::close(); + } } \run(new ExclusiveLockTest()); diff --git a/tests/KdybyTests/Redis/Extension.phpt b/tests/KdybyTests/Redis/Extension.phpt index d661288..50e67cc 100644 --- a/tests/KdybyTests/Redis/Extension.phpt +++ b/tests/KdybyTests/Redis/Extension.phpt @@ -26,23 +26,45 @@ class ExtensionTest extends Tester\TestCase { /** - * @return \SystemContainer|\Nette\DI\Container + * @param string $path + * @return Nette\DI\Container|\SystemContainer */ - protected function createContainer() + protected function createContainer($path) { $config = new Nette\Configurator(); $config->setTempDirectory(TEMP_DIR); Kdyby\Redis\DI\RedisExtension::register($config); - $config->addConfig(__DIR__ . '/files/config.neon', $config::NONE); + $config->addConfig($path, $config::NONE); return $config->createContainer(); } + /** + * @return array + */ + public function getConfigs() + { + $directory = __DIR__ . '/files'; + $files = array_diff(scandir($directory), array('..', '.')); + $configs = array(); + foreach ($files as $file) { + if (!preg_match('/config/', $file)) { + continue; + } + $path = $directory . '/' . $file; + $configs[] = array($path); + } + return $configs; + } - public function testFunctional() + /** + * @dataProvider getConfigs + * @param string $path Path to neon config + */ + public function testFunctional($path) { - $dic = $this->createContainer(); + $dic = $this->createContainer($path); Assert::true($dic->getService('redis.client') instanceof Kdyby\Redis\RedisClient); Assert::true($dic->getService('redis.cacheJournal') instanceof Kdyby\Redis\RedisLuaJournal); Assert::true($dic->getService('nette.cacheJournal') instanceof Kdyby\Redis\RedisLuaJournal); diff --git a/tests/KdybyTests/Redis/RedisNamespaceJournal.phpt b/tests/KdybyTests/Redis/RedisNamespaceJournal.phpt new file mode 100644 index 0000000..60b4d8e --- /dev/null +++ b/tests/KdybyTests/Redis/RedisNamespaceJournal.phpt @@ -0,0 +1,406 @@ + + * @package Kdyby\Redis + */ + +namespace KdybyTests\Redis; + +use Kdyby\Redis\RedisLuaJournal; +use Nette; +use Nette\Caching\Cache; +use Tester; +use Tester\Assert; + +require_once __DIR__ . '/../bootstrap.php'; + + +/** + * @author Filip Procházka + */ +class RedisNamespaceJournalTest extends AbstractRedisTestCase +{ + + /** + * @var RedisLuaJournal|Nette\Caching\Storages\IJournal + */ + private $journal; + + + + protected function setUp() + { + parent::setUp(); + + $this->journal = new RedisLuaJournal($this->getClient(), 'foo'); + } + + + + public function testRemoveByTag() + { + // Assert::same(0, count($this->getClient()->keys('*'))); + $this->assertKeysInDatabase(0); + + $this->journal->write('ok_test1', array( + Cache::TAGS => array('test:homepage'), + )); + + // Assert::same(2, count($this->getClient()->keys('*'))); + $this->assertKeysInDatabase(2); + + $result = $this->journal->clean(array(Cache::TAGS => array('test:homepage'))); + Assert::same(1, count($result)); + Assert::same('ok_test1', $result[0]); + } + + + + public function testRemovingByMultipleTags_OneIsNotDefined() + { + $this->journal->write('ok_test2', array( + Cache::TAGS => array('test:homepage', 'test:homepage2'), + )); + + $result = $this->journal->clean(array(Cache::TAGS => array('test:homepage2'))); + Assert::same(1, count($result)); + Assert::same('ok_test2', $result[0]); + } + + + + public function testRemovingByMultipleTags_BothAreOnOneEntry() + { + $this->journal->write('ok_test2b', array( + Cache::TAGS => array('test:homepage', 'test:homepage2'), + )); + + $result = $this->journal->clean(array(Cache::TAGS => array('test:homepage', 'test:homepage2'))); + Assert::same(1, count($result)); + Assert::same('ok_test2b', $result[0]); + } + + + + public function testRemoveByMultipleTags_TwoSameTags() + { + $this->journal->write('ok_test2c', array( + Cache::TAGS => array('test:homepage', 'test:homepage'), + )); + + $result = $this->journal->clean(array(Cache::TAGS => array('test:homepage', 'test:homepage'))); + Assert::same(1, count($result)); + Assert::same('ok_test2c', $result[0]); + } + + + + public function testRemoveByTagAndPriority() + { + $this->journal->write('ok_test2d', array( + Cache::TAGS => array('test:homepage'), + Cache::PRIORITY => 15, + )); + + $result = $this->journal->clean(array(Cache::TAGS => array('test:homepage'), Cache::PRIORITY => 20)); + Assert::same(1, count($result)); + Assert::same('ok_test2d', $result[0]); + } + + + + public function testRemoveByPriority() + { + $this->journal->write('ok_test3', array( + Cache::PRIORITY => 10, + )); + + $result = $this->journal->clean(array(Cache::PRIORITY => 10)); + Assert::same(1, count($result)); + Assert::same('ok_test3', $result[0]); + } + + + + public function testPriorityAndTag_CleanByTag() + { + $this->journal->write('ok_test4', array( + Cache::TAGS => array('test:homepage'), + Cache::PRIORITY => 10, + )); + + $result = $this->journal->clean(array(Cache::TAGS => array('test:homepage'))); + Assert::same(1, count($result)); + Assert::same('ok_test4', $result[0]); + } + + + + public function testPriorityAndTag_CleanByPriority() + { + $this->journal->write('ok_test5', array( + Cache::TAGS => array('test:homepage'), + Cache::PRIORITY => 10, + )); + + $result = $this->journal->clean(array(Cache::PRIORITY => 10)); + Assert::same(1, count($result)); + Assert::same('ok_test5', $result[0]); + } + + + + public function testMultipleWritesAndMultipleClean() + { + for ($i = 1; $i <= 10; $i++) { + $this->journal->write('ok_test6_' . $i, array( + Cache::TAGS => array('test:homepage', 'test:homepage/' . $i), + Cache::PRIORITY => $i, + )); + } + + $result = $this->journal->clean(array(Cache::PRIORITY => 5)); + Assert::same(5, count($result), "clean priority lower then 5"); + Assert::same('ok_test6_1', $result[0], "clean priority lower then 5"); + + $result = $this->journal->clean(array(Cache::TAGS => array('test:homepage/7'))); + Assert::same(1, count($result), "clean tag homepage/7"); + Assert::same('ok_test6_7', $result[0], "clean tag homepage/7"); + + $result = $this->journal->clean(array(Cache::TAGS => array('test:homepage/4'))); + Assert::same(0, count($result), "clean non exists tag"); + + $result = $this->journal->clean(array(Cache::PRIORITY => 4)); + Assert::same(0, count($result), "clean non exists priority"); + + $result = $this->journal->clean(array(Cache::TAGS => array('test:homepage'))); + Assert::same(4, count($result), "clean other"); + sort($result); + Assert::same(array('ok_test6_10', 'ok_test6_6', 'ok_test6_8', 'ok_test6_9'), $result, "clean other"); + } + + + + public function testSpecialChars() + { + $this->journal->write('ok_test7ščřžýáíé', array( + Cache::TAGS => array('čšřýýá', 'ýřžčýž/10') + )); + + $result = $this->journal->clean(array(Cache::TAGS => array('čšřýýá'))); + Assert::same(1, count($result)); + Assert::same('ok_test7ščřžýáíé', $result[0]); + } + + + + public function testDuplicates_SameTag() + { + $this->journal->write('ok_test_a', array( + Cache::TAGS => array('homepage') + )); + + $this->journal->write('ok_test_a', array( + Cache::TAGS => array('homepage') + )); + + $result = $this->journal->clean(array(Cache::TAGS => array('homepage'))); + Assert::same(1, count($result)); + Assert::same('ok_test_a', $result[0]); + } + + + + public function testDuplicates_SamePriority() + { + $this->journal->write('ok_test_b', array( + Cache::PRIORITY => 12 + )); + + $this->journal->write('ok_test_b', array( + Cache::PRIORITY => 12 + )); + + $result = $this->journal->clean(array(Cache::PRIORITY => 12)); + Assert::same(1, count($result)); + Assert::same('ok_test_b', $result[0]); + } + + + + public function testDuplicates_DifferentTags() + { + $this->journal->write('ok_test_ba', array( + Cache::TAGS => array('homepage') + )); + + $this->journal->write('ok_test_ba', array( + Cache::TAGS => array('homepage2') + )); + + $result = $this->journal->clean(array(Cache::TAGS => array('homepage'))); + Assert::same(0, count($result)); + + $result2 = $this->journal->clean(array(Cache::TAGS => array('homepage2'))); + Assert::same(1, count($result2)); + Assert::same('ok_test_ba', $result2[0]); + } + + + + public function testDuplicates_DifferentPriorities() + { + $this->journal->write('ok_test_bb', array( + Cache::PRIORITY => 15 + )); + + $this->journal->write('ok_test_bb', array( + Cache::PRIORITY => 20 + )); + + $result = $this->journal->clean(array(Cache::PRIORITY => 30)); + Assert::same(1, count($result)); + Assert::same('ok_test_bb', $result[0]); + } + + + + public function testCleanAll() + { + $this->journal->write('ok_test_all_tags', array( + Cache::TAGS => array('test:all', 'test:all') + )); + + $this->journal->write('ok_test_all_priority', array( + Cache::PRIORITY => 5, + )); + + $result = $this->journal->clean(array(Cache::ALL => TRUE)); + Assert::null($result); + + $result2 = $this->journal->clean(array(Cache::TAGS => 'test:all')); + Assert::true(empty($result2)); + } + + + + public function testBigCache() + { + if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') { + Tester\Helpers::skip("Linux only"); + } + + $script = $this->cacheGeneratorScripts(); + $script .= <<getClient()->evalScript($script)); + $this->assertKeysInDatabase(5100); + + $this->journal->clean(array(Cache::TAGS => 'test.4356')); + $this->assertKeysInDatabase(0); + } + + + + public function testBigCache_ShitloadOfEntries() + { + if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') { + Tester\Helpers::skip("Linux only"); + } + + $script = $this->cacheGeneratorScripts(); + $script .= <<getClient()->evalScript($script)); + $this->assertKeysInDatabase(100001); + + $this->journal->clean(array(Cache::TAGS => 'kdyby')); + $this->assertKeysInDatabase(0); + } + + + + protected function assertKeysInDatabase($number) + { + $dbNum = $this->getClient()->getDriver()->getDBNum(); + $dbInfo = $this->getClient()->info('db' . $dbNum); + + if ($number > 0 && !$dbInfo) { + Assert::fail("Number of keys in database couldn't be determined"); + } + + Assert::equal($number, $dbInfo ? (int) $dbInfo['keys'] : 0); + } + + + + private function cacheGeneratorScripts() + { + $script = "local ARGV = {}\n"; + $script .= 'ARGV[2] = "foo:"'. "\n"; + $script .= file_get_contents(__DIR__ . '/../../../src/Kdyby/Redis/scripts/common.lua'); + $script .= << 0 and + function(_, lastvalue) + local nextvalue = lastvalue + step + if nextvalue <= to then return nextvalue end + end or + step < 0 and + function(_, lastvalue) + local nextvalue = lastvalue + step + if nextvalue >= to then return nextvalue end + end or + function(_, lastvalue) return lastvalue end + return f, nil, from - step +end + + +LUA; + return $script; + } + + + + public function testNullByte() + { + $key = "prefix\x00test:\\2"; + $this->journal->write($key, array( + Cache::TAGS => array("test:nullByte") + )); + + $result = $this->journal->clean(array( + Cache::TAGS => array("test:nullByte") + )); + Assert::same(array($key), $result); + } + +} + +\run(new RedisNamespaceJournalTest()); diff --git a/tests/KdybyTests/Redis/RedisNamespaceStorage.phpt b/tests/KdybyTests/Redis/RedisNamespaceStorage.phpt new file mode 100644 index 0000000..aec086d --- /dev/null +++ b/tests/KdybyTests/Redis/RedisNamespaceStorage.phpt @@ -0,0 +1,407 @@ + + * @package Kdyby\Redis + */ + +namespace KdybyTests\Redis; + +use Kdyby\Redis\RedisLuaJournal; +use Kdyby\Redis\RedisStorage; +use Nette; +use Nette\Caching\Cache; +use Tester; +use Tester\Assert; + +require_once __DIR__ . '/../bootstrap.php'; + + +/** + * @author Vladimir Bosiak + */ +class RedisNamespaceStorageTest extends AbstractRedisTestCase +{ + + /** + * @var \Kdyby\Redis\RedisStorage + */ + private $storage; + + public function setUp() + { + parent::setUp(); + $this->storage = new RedisStorage($this->client, NULL, 'foo'); + } + + public function testBasics() + { + list($key, $value) = $this->basicData(); + + $cache = new Cache($this->storage); + Assert::null($cache->load($key), "Cache content"); + + // Writing cache... + $cache->save($key, $value); + Assert::same($value, $cache->load($key), "Is cache ok?"); + + // Removing from cache using unset()... + $cache->remove($key); + Assert::false($cache->load($key) !== null, "Is cached?"); + + // Removing from cache using set NULL... + $cache->save($key, $value); + $cache->save($key, null); + Assert::false($cache->load($key) !== null, "Is cached?"); + } + + /** + * key and data with special chars + * + * @return array + */ + public function basicData() + { + return array( + $key = array(1, true), + $value = range("\x00", "\xFF"), + ); + } + + /** + * @param mixed $val + * @return mixed + */ + public static function dependency($val) + { + return $val; + } + + + public function testCallbacks() + { + $key = 'nette'; + $value = 'rulez'; + + $cache = new Cache($this->storage); + $cb = get_called_class() . '::dependency'; + + // Writing cache... + $cache->save($key, $value, array( + Cache::CALLBACKS => array(array($cb, 1)), + )); + + Assert::true($cache->load($key) !== null, 'Is cached?'); + + // Writing cache... + $cache->save($key, $value, array( + Cache::CALLBACKS => array(array($cb, 0)), + )); + + Assert::false($cache->load($key) !== null, 'Is cached?'); + } + + + public function testCleanAll() + { + $cacheA = new Cache($this->storage); + $cacheB = new Cache($this->storage, 'B'); + + $cacheA->save('test1', 'David'); + $cacheA->save('test2', 'Grudl'); + $cacheB->save('test1', 'divaD'); + $cacheB->save('test2', 'ldurG'); + + Assert::same('David Grudl divaD ldurG', implode(' ', array( + $cacheA->load('test1'), + $cacheA->load('test2'), + $cacheB->load('test1'), + $cacheB->load('test2'), + ))); + + $this->storage->clean(array(Cache::ALL => true)); + + Assert::null($cacheA->load('test1')); + Assert::null($cacheA->load('test2')); + Assert::null($cacheB->load('test1')); + Assert::null($cacheB->load('test2')); + } + + + public function testExpiration() + { + $key = 'nette'; + $value = 'rulez'; + + $cache = new Cache($this->storage); + + // Writing cache... + $cache->save($key, $value, array( + Cache::EXPIRATION => time() + 3, + )); + + // Sleeping 1 second + sleep(1); + Assert::true($cache->load($key) !== null, 'Is cached?'); + + // Sleeping 3 seconds + sleep(3); + Assert::false($cache->load($key) !== null, 'Is cached?'); + } + + + public function testIntKeys() + { + // key and data with special chars + $key = 0; + $value = range("\x00", "\xFF"); + + $cache = new Cache($this->storage); + Assert::false($cache->load($key) !== null, 'Is cached?'); + Assert::null($cache->load($key), 'Cache content'); + + // Writing cache... + $cache->save($key, $value); + Assert::true($cache->load($key) !== null, 'Is cached?'); + Assert::same($value, $cache->load($key), 'Is cache ok?'); + + // Removing from cache using unset()... + $cache->remove($key); + Assert::false($cache->load($key) !== null, 'Is cached?'); + + // Removing from cache using set NULL... + $cache->save($key, $value); + $cache->save($key, null); + Assert::false($cache->load($key) !== null, 'Is cached?'); + } + + + public function testDependentItems() + { + $key = 'nette'; + $value = 'rulez'; + + $cache = new Cache($this->storage); + + // Writing cache... + $cache->save($key, $value, array( + Cache::ITEMS => array('dependent'), + )); + Assert::true($cache->load($key) !== null, 'Is cached?'); + + // Modifing dependent cached item + $cache->save('dependent', 'hello world'); + Assert::false($cache->load($key) !== null, 'Is cached?'); + + // Writing cache... + $cache->save($key, $value, array( + Cache::ITEMS => 'dependent', + )); + Assert::true($cache->load($key) !== null, 'Is cached?'); + + // Modifing dependent cached item + sleep(2); + $cache->save('dependent', 'hello europe'); + Assert::false($cache->load($key) !== null, 'Is cached?'); + + // Writing cache... + $cache->save($key, $value, array( + Cache::ITEMS => 'dependent', + )); + Assert::true($cache->load($key) !== null, 'Is cached?'); + + // Deleting dependent cached item + $cache->save('dependent', null); + Assert::false($cache->load($key) !== null, 'Is cached?'); + } + + + /** + */ + public function testLoadOrSave() + { + // key and data with special chars + $key = '../' . implode('', range("\x00", "\x1F")); + $value = range("\x00", "\xFF"); + + $cache = new Cache($this->storage); + Assert::false($cache->load($key) !== null, 'Is cached?'); + + // Writing cache using Closure... + $res = $cache->load($key, function (& $dp) use ($value) { + $dp = array( + Cache::EXPIRATION => time() + 2, + ); + + return $value; + }); + + Assert::same($value, $res, 'Is result ok?'); + Assert::same($value, $cache->load($key), 'Is cache ok?'); + + // Sleeping 3 seconds + sleep(3); + Assert::false($cache->load($key) !== null, 'Is cached?'); + } + + + public function testNamespace() + { + $cacheA = new Cache($this->storage, 'a'); + $cacheB = new Cache($this->storage, 'b'); + + // Writing cache... + $cacheA->save('key', 'hello'); + $cacheB->save('key', 'world'); + + Assert::true($cacheA->load('key') !== null, 'Is cached #1?'); + Assert::true($cacheB->load('key') !== null, 'Is cached #2?'); + Assert::same('hello', $cacheA->load('key'), 'Is cache ok #1?'); + Assert::same('world', $cacheB->load('key'), 'Is cache ok #2?'); + + // Removing from cache #2 using unset()... + $cacheB->remove('key'); + Assert::true($cacheA->load('key') !== null, 'Is cached #1?'); + Assert::false($cacheB->load('key') !== null, 'Is cached #2?'); + } + + + public function testPriority() + { + $storage = new RedisStorage($this->client, new Nette\Caching\Storages\FileJournal(TEMP_DIR)); + $cache = new Cache($storage); + + // Writing cache... + $cache->save('key1', 'value1', array( + Cache::PRIORITY => 100, + )); + $cache->save('key2', 'value2', array( + Cache::PRIORITY => 200, + )); + $cache->save('key3', 'value3', array( + Cache::PRIORITY => 300, + )); + $cache->save('key4', 'value4'); + + // Cleaning by priority... + $cache->clean(array( + Cache::PRIORITY => '200', + )); + + Assert::false($cache->load('key1') !== null, 'Is cached key1?'); + Assert::false($cache->load('key2') !== null, 'Is cached key2?'); + Assert::true($cache->load('key3') !== null, 'Is cached key3?'); + Assert::true($cache->load('key4') !== null, 'Is cached key4?'); + } + + + public function testPriority_Optimized() + { + $storage = new RedisStorage($this->client, new RedisLuaJournal($this->client)); + $cache = new Cache($storage); + + // Writing cache... + $cache->save('key1', 'value1', array( + Cache::PRIORITY => 100, + )); + $cache->save('key2', 'value2', array( + Cache::PRIORITY => 200, + )); + $cache->save('key3', 'value3', array( + Cache::PRIORITY => 300, + )); + $cache->save('key4', 'value4'); + + // Cleaning by priority... + $cache->clean(array( + Cache::PRIORITY => '200', + )); + + Assert::false($cache->load('key1') !== null, 'Is cached key1?'); + Assert::false($cache->load('key2') !== null, 'Is cached key2?'); + Assert::true($cache->load('key3') !== null, 'Is cached key3?'); + Assert::true($cache->load('key4') !== null, 'Is cached key4?'); + } + + + public function testTags() + { + $storage = new RedisStorage($this->client, new Nette\Caching\Storages\FileJournal(TEMP_DIR)); + $cache = new Cache($storage); + + // Writing cache... + $cache->save('key1', 'value1', array( + Cache::TAGS => array('one', 'two'), + )); + $cache->save('key2', 'value2', array( + Cache::TAGS => array('one', 'three'), + )); + $cache->save('key3', 'value3', array( + Cache::TAGS => array('two', 'three'), + )); + $cache->save('key4', 'value4'); + + // Cleaning by tags... + $cache->clean(array( + Cache::TAGS => 'one', + )); + + Assert::false($cache->load('key1') !== null, 'Is cached key1?'); + Assert::false($cache->load('key2') !== null, 'Is cached key2?'); + Assert::true($cache->load('key3') !== null, 'Is cached key3?'); + Assert::true($cache->load('key4') !== null, 'Is cached key4?'); + } + + + public function testTags_Optimized() + { + $storage = new RedisStorage($this->client, new RedisLuaJournal($this->client)); + $cache = new Cache($storage); + + // Writing cache... + $cache->save('key1', 'value1', array( + Cache::TAGS => array('one', 'two'), + )); + $cache->save('key2', 'value2', array( + Cache::TAGS => array('one', 'three'), + )); + $cache->save('key3', 'value3', array( + Cache::TAGS => array('two', 'three'), + )); + $cache->save('key4', 'value4'); + + // Cleaning by tags... + $cache->clean(array( + Cache::TAGS => 'one', + )); + + Assert::false($cache->load('key1') !== null, 'Is cached key1?'); + Assert::false($cache->load('key2') !== null, 'Is cached key2?'); + Assert::true($cache->load('key3') !== null, 'Is cached key3?'); + Assert::true($cache->load('key4') !== null, 'Is cached key4?'); + } + + + public function testMultiRead() + { + $storage = $this->storage; + + $storage->write('A', 1, array()); + $storage->write('B', 2, array()); + $storage->write('C', false, array()); + $storage->write('E', null, array()); + + Assert::equal(array( + 'A' => 1, + 'B' => 2, + 'C' => false, + 'D' => null, + 'E' => null, + ), $storage->multiRead(array('A', 'B', 'C', 'D', 'E'))); + } +} + +\run(new RedisNamespaceStorageTest()); diff --git a/tests/KdybyTests/Redis/files/keyNamespace.config.neon b/tests/KdybyTests/Redis/files/keyNamespace.config.neon new file mode 100644 index 0000000..03d0a72 --- /dev/null +++ b/tests/KdybyTests/Redis/files/keyNamespace.config.neon @@ -0,0 +1,7 @@ +redis: + journal: + namespace: "instance1" + storage: + namespace: "instance2" + session: + namespace: "instance3"