diff --git a/src/Caching/Cache.php b/src/Caching/Cache.php index 07e8b491..b30c4708 100644 --- a/src/Caching/Cache.php +++ b/src/Caching/Cache.php @@ -97,6 +97,55 @@ public function load($key, $fallback = NULL) } + /** + * Reads multiple items from the cache. + * @param array + * @param callable + * @return array + */ + public function bulkLoad(array $keys, $fallback = NULL) + { + if (count($keys) === 0) { + return []; + } + foreach ($keys as $key) { + if (!is_scalar($key)) { + throw new Nette\InvalidArgumentException('Only scalar keys are allowed in bulkLoad()'); + } + } + $storageKeys = array_map([$this, 'generateKey'], $keys); + if (!$this->storage instanceof IBulkReader) { + $result = array_combine($keys, array_map([$this->storage, 'read'], $storageKeys)); + if ($fallback !== NULL) { + foreach ($result as $key => $value) { + if ($value === NULL) { + $result[$key] = $this->save($key, function (& $dependencies) use ($key, $fallback) { + return call_user_func_array($fallback, [$key, & $dependencies]); + }); + } + } + } + return $result; + } + + $cacheData = $this->storage->bulkRead($storageKeys); + $result = []; + foreach ($keys as $i => $key) { + $storageKey = $storageKeys[$i]; + if (isset($cacheData[$storageKey])) { + $result[$key] = $cacheData[$storageKey]; + } elseif ($fallback) { + $result[$key] = $this->save($key, function (& $dependencies) use ($key, $fallback) { + return call_user_func_array($fallback, [$key, & $dependencies]); + }); + } else { + $result[$key] = NULL; + } + } + return $result; + } + + /** * Writes item into the cache. * Dependencies are: diff --git a/src/Caching/IBulkReader.php b/src/Caching/IBulkReader.php new file mode 100644 index 00000000..1f04d647 --- /dev/null +++ b/src/Caching/IBulkReader.php @@ -0,0 +1,24 @@ + value pairs, missing items are omitted + */ + function bulkRead(array $keys); + +} diff --git a/src/Caching/Storages/NewMemcachedStorage.php b/src/Caching/Storages/NewMemcachedStorage.php index 2854e15d..8e299b87 100644 --- a/src/Caching/Storages/NewMemcachedStorage.php +++ b/src/Caching/Storages/NewMemcachedStorage.php @@ -14,7 +14,7 @@ /** * Memcached storage using memcached extension. */ -class NewMemcachedStorage implements Nette\Caching\IStorage +class NewMemcachedStorage implements Nette\Caching\IStorage, Nette\Caching\IBulkReader { use Nette\SmartObject; @@ -110,6 +110,39 @@ public function read($key) } + /** + * Reads from cache in bulk. + * @param string key + * @return array key => value pairs, missing items are omitted + */ + public function bulkRead(array $keys) + { + $prefixedKeys = array_map(function ($key) { + return urlencode($this->prefix . $key); + }, $keys); + $keys = array_combine($prefixedKeys, $keys); + $metas = $this->memcached->getMulti($prefixedKeys); + $result = []; + $deleteKeys = []; + foreach ($metas as $prefixedKey => $meta) { + if (!empty($meta[self::META_CALLBACKS]) && !Cache::checkCallbacks($meta[self::META_CALLBACKS])) { + $deleteKeys[] = $prefixedKey; + } else { + $result[$keys[$prefixedKey]] = $meta[self::META_DATA]; + } + + if (!empty($meta[self::META_DELTA])) { + $this->memcached->replace($prefixedKey, $meta, $meta[self::META_DELTA] + time()); + } + } + if (!empty($deleteKeys)) { + $this->memcached->deleteMulti($deleteKeys, 0); + } + + return $result; + } + + /** * Prevents item reading and writing. Lock is released by write() or remove(). * @param string key diff --git a/src/Caching/Storages/SQLiteStorage.php b/src/Caching/Storages/SQLiteStorage.php index 0d97c62f..38611449 100644 --- a/src/Caching/Storages/SQLiteStorage.php +++ b/src/Caching/Storages/SQLiteStorage.php @@ -14,7 +14,7 @@ /** * SQLite storage. */ -class SQLiteStorage implements Nette\Caching\IStorage +class SQLiteStorage implements Nette\Caching\IStorage, Nette\Caching\IBulkReader { use Nette\SmartObject; @@ -64,6 +64,31 @@ public function read($key) } + /** + * Reads from cache in bulk. + * @param string key + * @return array key => value pairs, missing items are omitted + */ + public function bulkRead(array $keys) + { + $stmt = $this->pdo->prepare('SELECT key, data, slide FROM cache WHERE key IN (?' . str_repeat(',?', count($keys) - 1) . ') AND (expire IS NULL OR expire >= ?)'); + $stmt->execute(array_merge($keys, [time()])); + $result = []; + $updateSlide = []; + foreach ($stmt->fetchAll(\PDO::FETCH_ASSOC) as $row) { + if ($row['slide'] !== NULL) { + $updateSlide[] = $row['key']; + } + $result[$row['key']] = unserialize($row['data']); + } + if (!empty($updateSlide)) { + $stmt = $this->pdo->prepare('UPDATE cache SET expire = ? + slide WHERE key IN(?' . str_repeat(',?', count($updateSlide) - 1) . ')'); + $stmt->execute(array_merge([time()], $updateSlide)); + } + return $result; + } + + /** * Prevents item reading and writing. Lock is released by write() or remove(). * @param string key diff --git a/tests/Caching/Cache.bulkLoad.phpt b/tests/Caching/Cache.bulkLoad.phpt new file mode 100644 index 00000000..10fe2b74 --- /dev/null +++ b/tests/Caching/Cache.bulkLoad.phpt @@ -0,0 +1,64 @@ + NULL, 2 => NULL], $cache->bulkLoad([1, 2]), 'data'); + + Assert::same([1 => 1, 2 => 2], $cache->bulkLoad([1, 2], function ($key) { + return $key; + })); + + $data = $cache->bulkLoad([1, 2]); + Assert::same(1, $data[1]['data']); + Assert::same(2, $data[2]['data']); +}); + +// storage with bulk load support +test(function () { + $storage = new BulkReadTestStorage; + $cache = new Cache($storage, 'ns'); + Assert::same([1 => NULL, 2 => NULL], $cache->bulkLoad([1, 2])); + + Assert::same([1 => 1, 2 => 2], $cache->bulkLoad([1, 2], function ($key) { + return $key; + })); + + $data = $cache->bulkLoad([1, 2]); + Assert::same(1, $data[1]['data']); + Assert::same(2, $data[2]['data']); +}); + +// dependencies +test(function () { + $storage = new BulkReadTestStorage; + $cache = new Cache($storage, 'ns'); + $dependencies = [Cache::TAGS => 'tag']; + $cache->bulkLoad([1], function ($key, & $deps) use ($dependencies) { + $deps = $dependencies; + return $key; + }); + + $data = $cache->bulkLoad([1, 2]); + Assert::same($dependencies, $data[1]['dependencies']); +}); + +test(function () { + Assert::exception(function () { + $cache = new Cache(new BulkReadTestStorage()); + $cache->bulkLoad([[1]]); + }, Nette\InvalidArgumentException::class, 'Only scalar keys are allowed in bulkLoad()'); +}); diff --git a/tests/Caching/Cache.php b/tests/Caching/Cache.php index d6f3d666..c41bf794 100644 --- a/tests/Caching/Cache.php +++ b/tests/Caching/Cache.php @@ -1,5 +1,6 @@ read($key); + if ($data !== NULL) { + $result[$key] = $data; + } + } + + return $result; + } + +} diff --git a/tests/Storages/NewMemcached.bulkRead.phpt b/tests/Storages/NewMemcached.bulkRead.phpt new file mode 100644 index 00000000..1b87a4fa --- /dev/null +++ b/tests/Storages/NewMemcached.bulkRead.phpt @@ -0,0 +1,27 @@ +save('foo', 'bar'); + +Assert::same(['foo' => 'bar', 'lorem' => NULL], $cache->bulkLoad(['foo', 'lorem'])); diff --git a/tests/Storages/NewMemcached.sliding.phpt b/tests/Storages/NewMemcached.sliding.phpt index bcbe9903..604a88c0 100644 --- a/tests/Storages/NewMemcached.sliding.phpt +++ b/tests/Storages/NewMemcached.sliding.phpt @@ -37,10 +37,31 @@ for ($i = 0; $i < 5; $i++) { sleep(1); Assert::truthy($cache->load($key)); - } // Sleeping few seconds... sleep(5); Assert::null($cache->load($key)); + + +// Bulk + +// Writing cache... +$cache->save($key, $value, [ + Cache::EXPIRATION => time() + 3, + Cache::SLIDING => TRUE, +]); + + +for ($i = 0; $i < 5; $i++) { + // Sleeping 1 second + sleep(1); + + Assert::truthy($cache->bulkLoad([$key])[$key]); +} + +// Sleeping few seconds... +sleep(5); + +Assert::null($cache->load([$key])[$key]); diff --git a/tests/Storages/SQLiteStorage.bulkRead.phpt b/tests/Storages/SQLiteStorage.bulkRead.phpt new file mode 100644 index 00000000..3568e71c --- /dev/null +++ b/tests/Storages/SQLiteStorage.bulkRead.phpt @@ -0,0 +1,23 @@ +save('foo', 'bar'); + +Assert::same(['foo' => 'bar', 'lorem' => NULL], $cache->bulkLoad(['foo', 'lorem'])); diff --git a/tests/Storages/SQLiteStorage.sliding.phpt b/tests/Storages/SQLiteStorage.sliding.phpt index 5e5da02b..43d2c349 100644 --- a/tests/Storages/SQLiteStorage.sliding.phpt +++ b/tests/Storages/SQLiteStorage.sliding.phpt @@ -41,3 +41,23 @@ for ($i = 0; $i < 5; $i++) { sleep(5); Assert::null($cache->load($key)); + + +// Writing cache... +$cache->save($key, $value, [ + Cache::EXPIRATION => time() + 3, + Cache::SLIDING => TRUE, +]); + + +for ($i = 0; $i < 5; $i++) { + // Sleeping 1 second + sleep(1); + + Assert::truthy($cache->bulkLoad([$key])[$key]); +} + +// Sleeping few seconds... +sleep(5); + +Assert::null($cache->bulkLoad([$key])[$key]);