Skip to content

Commit

Permalink
[Cache] Add optimized FileSystem & Redis TagAware Adapters
Browse files Browse the repository at this point in the history
Reduces cache lookups by 50% by changing logic of how tag information is
stored to avoid having to look it up on getItem(s) calls.

For Filesystem symlinks are used, for Redis "Set" datatype is used.
  • Loading branch information
andrerom committed Feb 24, 2019
1 parent 3895acd commit eda0753
Show file tree
Hide file tree
Showing 3 changed files with 558 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,264 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace Symfony\Component\Cache\Adapter\TagAware;

use Psr\Cache\CacheItemInterface;
use Psr\Log\LoggerAwareInterface;
use Symfony\Component\Cache\Adapter\AdapterInterface;
use Symfony\Component\Cache\CacheItem;
use Symfony\Component\Cache\Exception\InvalidArgumentException;
use Symfony\Component\Cache\ResettableInterface;
use Symfony\Component\Cache\Traits\AbstractTrait;
use Symfony\Component\Cache\Traits\ContractsTrait;
use Symfony\Contracts\Cache\CacheInterface;

/**
* @author Nicolas Grekas <p@tchwork.com>
*/
abstract class AbstractTagAwareAdapter implements AdapterInterface, CacheInterface, LoggerAwareInterface, ResettableInterface
{
use AbstractTrait { getId as protected; }
use ContractsTrait;

protected const TAGS_PREFIX = "\0tags\0";

private $createCacheItem;
private $mergeByLifetime;

protected function __construct(string $namespace = '', int $defaultLifetime = 0)
{
$this->namespace = '' === $namespace ? '' : CacheItem::validateKey($namespace).':';
if (null !== $this->maxIdLength && \strlen($namespace) > $this->maxIdLength - 24) {
throw new InvalidArgumentException(sprintf('Namespace must be %d chars max, %d given ("%s")', $this->maxIdLength - 24, \strlen($namespace), $namespace));
}
$this->createCacheItem = \Closure::bind(
function ($key, $value, $isHit) use ($defaultLifetime) {
$item = new CacheItem();
$item->key = $key;
$item->isHit = $isHit;
$item->defaultLifetime = $defaultLifetime;
//<diff:AbstractAdapter> extract Value and Tags from the cache value
$item->value = $v = $value['value'];
$item->metadata[CacheItem::METADATA_TAGS] = $value['tags'] ?? [];
// Detect wrapped values that encode for their expiry and creation duration
// For compactness, these values are packed
if (isset($value['meta'])) {
$v = \unpack('Ve/Nc', $value['meta']);
$item->metadata[CacheItem::METADATA_EXPIRY] = $v['e'] + CacheItem::METADATA_EXPIRY_OFFSET;
$item->metadata[CacheItem::METADATA_CTIME] = $v['c'];
}
//</diff:AbstractAdapter>

return $item;
},
null,
CacheItem::class
);
$getId = \Closure::fromCallable([$this, 'getId']);
$this->mergeByLifetime = \Closure::bind(
function ($deferred, $namespace, &$expiredIds) use ($getId) {
$byLifetime = [];
$now = microtime(true);
$expiredIds = [];

foreach ($deferred as $key => $item) {
$key = (string) $key;
if (null === $item->expiry) {
$ttl = 0 < $item->defaultLifetime ? $item->defaultLifetime : 0;
} elseif (0 >= $ttl = (int) ($item->expiry - $now)) {
$expiredIds[] = $getId($key);
continue;
}
//<diff:AbstractAdapter> store Value and Tags on the cache value
if (isset(($metadata = $item->newMetadata)[CacheItem::METADATA_TAGS])) {
$value = ['value' => $item->value, 'tags' => $metadata[CacheItem::METADATA_TAGS]];
unset($metadata[CacheItem::METADATA_TAGS]);
} else {
$value = ['value' => $item->value, 'tags' => []];
}

if ($metadata) {
// For compactness, expiry and creation duration are packed, using magic numbers as separators
$value['meta'] = pack('VN', (int) $metadata[CacheItem::METADATA_EXPIRY] - CacheItem::METADATA_EXPIRY_OFFSET, $metadata[CacheItem::METADATA_CTIME]);
}
$byLifetime[$ttl][$getId($key)] = $value;
//</diff:AbstractAdapter>
}

return $byLifetime;
},
null,
CacheItem::class
);
}

/**
* {@inheritdoc}
*/
public function getItem($key)
{
if ($this->deferred) {
$this->commit();
}
$id = $this->getId($key);

$f = $this->createCacheItem;
$isHit = false;
$value = null;

try {
foreach ($this->doFetch([$id]) as $value) {
$isHit = true;
}
} catch (\Exception $e) {
CacheItem::log($this->logger, 'Failed to fetch key "{key}"', ['key' => $key, 'exception' => $e]);
}

return $f($key, $value, $isHit);
}

/**
* {@inheritdoc}
*/
public function getItems(array $keys = [])
{
if ($this->deferred) {
$this->commit();
}
$ids = [];

foreach ($keys as $key) {
$ids[] = $this->getId($key);
}
try {
$items = $this->doFetch($ids);
} catch (\Exception $e) {
CacheItem::log($this->logger, 'Failed to fetch requested items', ['keys' => $keys, 'exception' => $e]);
$items = [];
}
$ids = array_combine($ids, $keys);

return $this->generateItems($items, $ids);
}

/**
* {@inheritdoc}
*/
public function save(CacheItemInterface $item)
{
if (!$item instanceof CacheItem) {
return false;
}
$this->deferred[$item->getKey()] = $item;

return $this->commit();
}

/**
* {@inheritdoc}
*/
public function saveDeferred(CacheItemInterface $item)
{
if (!$item instanceof CacheItem) {
return false;
}
$this->deferred[$item->getKey()] = $item;

return true;
}

/**
* {@inheritdoc}
*/
public function commit()
{
$ok = true;
$byLifetime = $this->mergeByLifetime;
$byLifetime = $byLifetime($this->deferred, $this->namespace, $expiredIds);
$retry = $this->deferred = [];

if ($expiredIds) {
$this->doDelete($expiredIds);
}
foreach ($byLifetime as $lifetime => $values) {
try {
$e = $this->doSave($values, $lifetime);
} catch (\Exception $e) {
}
if (true === $e || [] === $e) {
continue;
}
if (\is_array($e) || 1 === \count($values)) {
foreach (\is_array($e) ? $e : array_keys($values) as $id) {
$ok = false;
$v = $values[$id];
$type = \is_object($v) ? \get_class($v) : \gettype($v);
CacheItem::log($this->logger, 'Failed to save key "{key}" ({type})', ['key' => substr($id, \strlen($this->namespace)), 'type' => $type, 'exception' => $e instanceof \Exception ? $e : null]);
}
} else {
foreach ($values as $id => $v) {
$retry[$lifetime][] = $id;
}
}
}

// When bulk-save failed, retry each item individually
foreach ($retry as $lifetime => $ids) {
foreach ($ids as $id) {
try {
$v = $byLifetime[$lifetime][$id];
$e = $this->doSave([$id => $v], $lifetime);
} catch (\Exception $e) {
}
if (true === $e || [] === $e) {
continue;
}
$ok = false;
$type = \is_object($v) ? \get_class($v) : \gettype($v);
CacheItem::log($this->logger, 'Failed to save key "{key}" ({type})', ['key' => substr($id, \strlen($this->namespace)), 'type' => $type, 'exception' => $e instanceof \Exception ? $e : null]);
}
}

return $ok;
}

public function __destruct()
{
if ($this->deferred) {
$this->commit();
}
}

private function generateItems($items, &$keys)
{
$f = $this->createCacheItem;

try {
foreach ($items as $id => $value) {
if (!isset($keys[$id])) {
$id = key($keys);
}
$key = $keys[$id];
unset($keys[$id]);
yield $key => $f($key, $value, true);
}
} catch (\Exception $e) {
CacheItem::log($this->logger, 'Failed to fetch requested items', ['keys' => array_values($keys), 'exception' => $e]);
}

foreach ($keys as $key) {
yield $key => $f($key, null, false);
}
}
}
Loading

0 comments on commit eda0753

Please sign in to comment.