Skip to content

Commit

Permalink
Bulk write implementation (#73)
Browse files Browse the repository at this point in the history
  • Loading branch information
KminekMatej authored and dg committed Mar 10, 2024
1 parent d2bb3f3 commit 0f37f75
Show file tree
Hide file tree
Showing 6 changed files with 260 additions and 1 deletion.
30 changes: 30 additions & 0 deletions src/Caching/BulkWriter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/

declare(strict_types=1);

namespace Nette\Caching;

use Nette;


/**
* Cache storage with a bulk write support.
*/
interface BulkWriter
{
/**
* Writes to cache in bulk.
* @param array{string, mixed} $items
*/
function bulkWrite(array $items, array $dependencies): void;

/**
* Removes multiple items from cache
*/
function bulkRemove(array $keys): void;
}
50 changes: 50 additions & 0 deletions src/Caching/Cache.php
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,56 @@ public function bulkLoad(array $keys, ?callable $generator = null): array
}


/**
* Writes multiple items into cache
*
* @param array $items
* @param array|null $dependencies
* @return array Stored items
*
* @throws InvalidArgumentException
*/
public function bulkSave(array $items, ?array $dependencies = null): array
{
$storedItems = [];

if (!$this->storage instanceof BulkWriter) {

foreach ($items as $key => $data) {
$storedItems[$key] = $this->save($key, $data, $dependencies);
}
return $storedItems;
}

$dependencies = $this->completeDependencies($dependencies);

if (isset($dependencies[self::Expire]) && $dependencies[self::Expire] <= 0) {
$this->storage->bulkRemove(array_map(fn($key): string => $this->generateKey($key), array_keys($items)));
return [];
}

$removals = [];
$toCache = [];
foreach ($items as $key => $data) {
$cKey = $this->generateKey($key);

if ($data === null) {
$removals[] = $cKey;
} else {
$storedItems[$key] = $toCache[$cKey] = $data;
}
}

if (!empty($removals)) {
$this->storage->bulkRemove($removals);
}

$this->storage->bulkWrite($toCache, $dependencies);

return $storedItems;
}


/**
* Writes item into the cache.
* Dependencies are:
Expand Down
44 changes: 43 additions & 1 deletion src/Caching/Storages/MemcachedStorage.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
/**
* Memcached storage using memcached extension.
*/
class MemcachedStorage implements Nette\Caching\Storage, Nette\Caching\BulkReader
class MemcachedStorage implements Nette\Caching\Storage, Nette\Caching\BulkReader, Nette\Caching\BulkWriter
{
/** @internal cache structure */
private const
Expand Down Expand Up @@ -168,12 +168,54 @@ public function write(string $key, $data, array $dp): void
}


public function bulkWrite(array $items, array $dp): void
{
if (isset($dp[Cache::Items])) {
throw new Nette\NotSupportedException('Dependent items are not supported by MemcachedStorage.');
}

$meta = $records = [];
$expire = 0;
if (isset($dp[Cache::Expire])) {
$expire = (int) $dp[Cache::Expire];
if (!empty($dp[Cache::Sliding])) {
$meta[self::MetaDelta] = $expire; // sliding time
}
}

if (isset($dp[Cache::Callbacks])) {
$meta[self::MetaCallbacks] = $dp[Cache::Callbacks];
}

foreach ($items as $key => $meta[self::MetaData]) {
$key = urlencode($this->prefix . $key);
$records[$key] = $meta;

if (isset($dp[Cache::Tags]) || isset($dp[Cache::Priority])) {
if (!$this->journal) {
throw new Nette\InvalidStateException('CacheJournal has not been provided.');
}

$this->journal->write($key, $dp);
}
}

$this->memcached->setMulti($records, $expire);
}


public function remove(string $key): void
{
$this->memcached->delete(urlencode($this->prefix . $key), 0);
}


public function bulkRemove(array $keys): void
{
$this->memcached->deleteMulti(array_map(fn($key) => urlencode($this->prefix . $key), $keys), 0);
}


public function clean(array $conditions): void
{
if (!empty($conditions[Cache::All])) {
Expand Down
51 changes: 51 additions & 0 deletions tests/Caching/Cache.bulkSave.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php

/**
* Test: Nette\Caching\Cache save().
*/

declare(strict_types=1);

use Nette\Caching\Cache;
use Tester\Assert;

require __DIR__ . '/../bootstrap.php';
require __DIR__ . '/Cache.php';


test('storage without bulk write support', function () {
$storage = new TestStorage;
$cache = new Cache($storage, 'ns');
Assert::same([1, 2], $cache->bulkSave([1, 2]), 'data');
Assert::same([1 => 'value1', 2 => 'value2'], $cache->bulkSave([1 => 'value1', 2 => 'value2']), 'data');

$data = $cache->bulkLoad([1, 2]);
Assert::same('value1', $data[1]['data']);
Assert::same('value2', $data[2]['data']);
});

test('storage with bulk write support', function () {
$storage = new BulkWriteTestStorage;
$cache = new Cache($storage, 'ns');
Assert::same([1, 2], $cache->bulkSave([1, 2]), 'data');
Assert::same([1 => 'value1', 2 => 'value2'], $cache->bulkSave([1 => 'value1', 2 => 'value2']), 'data');

$data = $cache->bulkLoad([1, 2]);
Assert::same('value1', $data[1]['data']);
Assert::same('value2', $data[2]['data']);
});

test('dependencies', function () {
$storage = new BulkWriteTestStorage;
$cache = new Cache($storage, 'ns');
$dependencies = [Cache::Tags => ['tag']];
$cache->bulkSave([1 => 'value1', 2 => 'value2'], $dependencies);

$data = $cache->bulkLoad([1, 2]);
Assert::same($dependencies, $data[1]['dependencies']);
Assert::same($dependencies, $data[2]['dependencies']);

$cache->clean($dependencies);

Assert::same([1 => null, 2 => null], $cache->bulkLoad([1, 2]));
});
50 changes: 50 additions & 0 deletions tests/Caching/Cache.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

declare(strict_types=1);

use Nette\Caching\BulkWriter;
use Nette\Caching\IBulkReader;
use Nette\Caching\IStorage;

Expand Down Expand Up @@ -37,6 +38,25 @@ public function remove(string $key): void

public function clean(array $conditions): void
{
if (!empty($conditions[Nette\Caching\Cache::All])) {
$this->data = [];
return;
}

//unset based by tags
if (!empty($conditions[Nette\Caching\Cache::Tags])) {
$unsets = [];
foreach ($this->data as $key => $data) {
$tags = $data['dependencies'][Nette\Caching\Cache::Tags] ?? null;
if (array_intersect($conditions[Nette\Caching\Cache::Tags], $tags)) {
$unsets[$key] = $key;
}
}

foreach ($unsets as $unsetKey) {
unset($this->data[$unsetKey]);
}
}
}
}

Expand All @@ -55,3 +75,33 @@ public function bulkRead(array $keys): array
return $result;
}
}

class BulkWriteTestStorage extends TestStorage implements BulkWriter
{
public function bulkRead(array $keys): array
{
$result = [];
foreach ($keys as $key) {
$data = $this->read($key);
if ($data !== null) {
$result[$key] = $data;
}
}

return $result;
}


public function bulkRemove(array $keys): void
{

}


public function bulkWrite($items, array $dp): void
{
foreach ($items as $key => $data) {
$this->write($key, $data, $dp);
}
}
}
36 changes: 36 additions & 0 deletions tests/Storages/Memcached.bulkWrite.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

/**
* Test: Nette\Caching\Storages\MemcachedStorage and bulkWrite
*/

declare(strict_types=1);

use Nette\Caching\Cache;
use Nette\Caching\Storages\MemcachedStorage;
use Nette\Caching\Storages\SQLiteJournal;
use Tester\Assert;

require __DIR__ . '/../bootstrap.php';


if (!MemcachedStorage::isAvailable()) {
Tester\Environment::skip('Requires PHP extension Memcached.');
}

Tester\Environment::lock('memcached-files', getTempDir());


$storage = new MemcachedStorage('localhost', 11211, '', new SQLiteJournal(getTempDir() . '/journal-memcached.s3db'));
$cache = new Cache($storage);

//standard
$cache->bulkSave(['foo' => 'bar']);
Assert::same(['foo' => 'bar', 'lorem' => null], $cache->bulkLoad(['foo', 'lorem']));

//tags
$dependencies = [Cache::Tags => ['tag']];
$cache->bulkSave(['foo' => 'bar'], $dependencies);
Assert::same(['foo' => 'bar'], $cache->bulkLoad(['foo']));
$cache->clean($dependencies);
Assert::same(['foo' => null], $cache->bulkLoad(['foo']));

0 comments on commit 0f37f75

Please sign in to comment.