Skip to content

Commit

Permalink
Bulk write implementation (#73)
Browse files Browse the repository at this point in the history
  • Loading branch information
KminekMatej committed Mar 10, 2024
1 parent 31a50f4 commit 40be743
Show file tree
Hide file tree
Showing 6 changed files with 279 additions and 8 deletions.
36 changes: 36 additions & 0 deletions src/Caching/BulkWriter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?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\InvalidStateException;
use Nette\NotSupportedException;


/**
* Cache storage with a bulk write support.
*/
interface BulkWriter
{
/**
* Writes to cache in bulk.
* <p>Similar to <code>write()</code>, but instead of a single key/value item, it works on multiple items specified in <code>items</code></p>
*
* @param array{string, mixed} $items <p>An array of key/data pairs to store on the server</p>
* @param array $dp Global dependencies of each stored value
* @return bool <p>Returns <b><code>true</code></b> on success or <b><code>false</code></b> on failure</p>
* @throws NotSupportedException
* @throws InvalidStateException
*/
function bulkWrite(array $items, array $dp): bool;

/**
* 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 @@ -173,6 +173,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

Check failure on line 185 in src/Caching/Cache.php

View workflow job for this annotation

GitHub Actions / PHPStan

PHPDoc tag @throws with type Nette\Caching\InvalidArgumentException is not subtype of Throwable
{
$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);

Check failure on line 220 in src/Caching/Cache.php

View workflow job for this annotation

GitHub Actions / PHPStan

Parameter #1 $items of method Nette\Caching\BulkWriter::bulkWrite() expects array{string, mixed}, array<string, mixed> given.

return $storedItems;
}


/**
* Writes item into the cache.
* Dependencies are:
Expand Down
59 changes: 51 additions & 8 deletions 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 All @@ -25,8 +25,6 @@ class MemcachedStorage implements Nette\Caching\Storage, Nette\Caching\BulkReade
MetaDelta = 'delta';

private \Memcached $memcached;
private string $prefix;
private ?Journal $journal;


/**
Expand All @@ -41,15 +39,12 @@ public static function isAvailable(): bool
public function __construct(
string $host = 'localhost',
int $port = 11211,
string $prefix = '',
?Journal $journal = null,
private string $prefix = '',
private ?Journal $journal = null,
) {
if (!static::isAvailable()) {
throw new Nette\NotSupportedException("PHP extension 'memcached' is not loaded.");
}

$this->prefix = $prefix;
$this->journal = $journal;
$this->memcached = new \Memcached;
if ($host) {
$this->addServer($host, $port);
Expand Down Expand Up @@ -168,12 +163,60 @@ public function write(string $key, $data, array $dp): void
}


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

$records = [];

$expire = 0;
if (isset($dp[Cache::Expire])) {
$expire = (int) $dp[Cache::Expire];
}

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

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

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

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);
}

$records[$key] = $meta;
}

return $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
53 changes: 53 additions & 0 deletions tests/Caching/Cache.bulkSave.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?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]));
});
52 changes: 52 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,35 @@ 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): bool
{
foreach ($items as $key => $data) {
$this->write($key, $data, $dp);
}

return true;
}
}
37 changes: 37 additions & 0 deletions tests/Storages/Memcached.bulkWrite.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?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 40be743

Please sign in to comment.