-
-
Notifications
You must be signed in to change notification settings - Fork 9.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[Cache] Add optimized FileSystem & Redis TagAware Adapters
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
Showing
3 changed files
with
558 additions
and
0 deletions.
There are no files selected for viewing
264 changes: 264 additions & 0 deletions
264
src/Symfony/Component/Cache/Adapter/TagAware/AbstractTagAwareAdapter.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} | ||
} |
Oops, something went wrong.