8000 [Cache] Improve reliability and performance of `TagAwareAdapter` by making tag versions an integral part of item value by sbelyshkin · Pull Request #42997 · symfony/symfony · GitHub
[go: up one dir, main page]

Skip to content

[Cache] Improve reliability and performance of TagAwareAdapter by making tag versions an integral part of item value #42997

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Mar 28, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,9 @@
"files": [
"src/Symfony/Component/String/Resources/functions.php"
],
"classmap": [
"src/Symfony/Component/Cache/Traits/ValueWrapper.php"
],
"exclude-from-classmap": [
"**/Tests/"
]
Expand Down
16 changes: 2 additions & 14 deletions src/Symfony/Component/Cache/Adapter/AbstractAdapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,15 +49,7 @@ static function ($key, $value, $isHit) {
$item->key = $key;
$item->value = $v = $value;
$item->isHit = $isHit;
// Detect wrapped values that encode for their expiry and creation duration
// For compactness, these values are packed in the key of an array using
// magic numbers in the form 9D-..-..-..-..-00-..-..-..-5F
if (\is_array($v) && 1 === \count($v) && 10 === \strlen($k = (string) array_key_first($v)) && "\x9D" === $k[0] && "\0" === $k[5] && "\x5F" === $k[9]) {
$item->value = $v[$k];
$v = unpack('Ve/Nc', substr($k, 1, -1));
$item->metadata[CacheItem::METADATA_EXPIRY] = $v['e'] + CacheItem::METADATA_EXPIRY_OFFSET;
$item->metadata[CacheItem::METADATA_CTIME] = $v['c'];
}
$item->unpack();

return $item;
},
Expand All @@ -80,11 +72,7 @@ static function ($deferred, $namespace, &$expiredIds, $getId, $defaultLifetime)
$expiredIds[] = $getId($key);
continue;
}
if (isset(($metadata = $item->newMetadata)[CacheItem::METADATA_TAGS])) {
unset($metadata[CacheItem::METADATA_TAGS]);
}
// For compactness, expiry and creation duration are packed in the key of an array, using magic numbers as separators
$byLifetime[$ttl][$getId($key)] = $metadata ? ["\x9D".pack('VN', (int) (0.1 + $metadata[self::METADATA_EXPIRY] - self::METADATA_EXPIRY_OFFSET), $metadata[self::METADATA_CTIME])."\x5F" => $item->value] : $item->value;
$byLifetime[$ttl][$getId($key)] = $item->pack();
}

return $byLifetime;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ static function ($key, $value, $isHit) {
$item->isHit = $isHit;
// Extract value, tags and meta data from the cache value
$item->value = $value['value'];
$item->metadata[CacheItem::METADATA_TAGS] = $value['tags'] ?? [];
$item->metadata[CacheItem::METADATA_TAGS] = isset($value['tags']) ? array_combine($value['tags'], $value['tags']) : [];
if (isset($value['meta'])) {
// For compactness these values are packed, & expiry is offset to reduce size
$v = unpack('Ve/Nc', $value['meta']);
Expand Down Expand Up @@ -95,18 +95,19 @@ static function ($deferred, &$expiredIds, $getId, $tagPrefix, $defaultLifetime)

if ($metadata) {
// For compactness, expiry and creation duration are packed, using magic numbers as separators
$value['meta'] = pack('VN', (int) (0.1 + $metadata[self::METADATA_EXPIRY] - self::METADATA_EXPIRY_OFFSET), $metadata[self::METADATA_CTIME]);
$value['meta'] = pack('VN', (int) (0.1 + $metadata[CacheItem::METADATA_EXPIRY] - CacheItem::METADATA_EXPIRY_OFFSET), $metadata[CacheItem::METADATA_CTIME]);
}

// Extract tag changes, these should be removed from values in doSave()
$value['tag-operations'] = ['add' => [], 'remove' => []];
$oldTags = $item->metadata[CacheItem::METADATA_TAGS] ?? [];
foreach (array_diff($value['tags'], $oldTags) as $addedTag) {
foreach (array_diff_key($value['tags'], $oldTags) as $addedTag) {
$value['tag-operations']['add'][] = $getId($tagPrefix.$addedTag);
}
foreach (array_diff($oldTags, $value['tags']) as $removedTag) {
foreach (array_diff_key($oldTags, $value['tags']) as $removedTag) {
$value['tag-operations']['remove'][] = $getId($tagPrefix.$removedTag);
}
$value['tags'] = array_keys($value['tags']);

$byLifetime[$ttl][$getId($key)] = $value;
$item->metadata = $item->newMetadata;
Expand Down
26 changes: 17 additions & 9 deletions src/Symfony/Component/Cache/Adapter/ArrayAdapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ class ArrayAdapter implements AdapterInterface, CacheInterface, LoggerAwareInter

private bool $storeSerialized;
private array $values = [];
private array $tags = [];
private array $expiries = [];
private int $defaultLifetime;
private float $maxLifetime;
Expand All @@ -57,11 +58,14 @@ public function __construct(int $defaultLifetime = 0, bool $storeSerialized = tr
$this->maxLifetime = $maxLifetime;
$this->maxItems = $maxItems;
self::$createCacheItem ?? self::$createCacheItem = \Closure::bind(
static function ($key, $value, $isHit) {
static function ($key, $value, $isHit, $tags) {
$item = new CacheItem();
$item->key = $key;
$item->value = $value;
$item->isHit = $isHit;
if (null !== $tags) {
$item->metadata[CacheItem::METADATA_TAGS] = $tags;
}

return $item;
},
Expand Down Expand Up @@ -131,7 +135,7 @@ public function getItem(mixed $key): CacheItem
$value = $this->storeSerialized ? $this->unfreeze($key, $isHit) : $this->values[$key];
}

return (self::$createCacheItem)($key, $value, $isHit);
return (self::$createCacheItem)($key, $value, $isHit, $this->tags[$key] ?? null);
}

/**
Expand All @@ -150,7 +154,7 @@ public function getItems(array $keys = []): iterable
public function deleteItem(mixed $key): bool
{
\assert('' !== CacheItem::validateKey($key));
unset($this->values[$key], $this->expiries[$key]);
unset($this->values[$key], $this->tags[$key], $this->expiries[$key]);

return true;
}
Expand Down Expand Up @@ -202,21 +206,25 @@ public function save(CacheItemInterface $item): bool
}

if ($this->maxItems) {
unset($this->values[$key]);
unset($this->values[$key], $this->tags[$key]);

// Iterate items and vacuum expired ones while we are at it
foreach ($this->values as $k => $v) {
if ($this->expiries[$k] > $now && \count($this->values) < $this->maxItems) {
break;
}

unset($this->values[$k], $this->expiries[$k]);
unset($this->values[$k], $this->tags[$k], $this->expiries[$k]);
}
}

$this->values[$key] = $value;
$this->expiries[$key] = $expiry ?? \PHP_INT_MAX;

if (null === $this->tags[$key] = $item["\0*\0newMetadata"][CacheItem::METADATA_TAGS] ?? null) {
unset($this->tags[$key]);
}

return true;
}

Expand Down Expand Up @@ -246,7 +254,7 @@ public function clear(string $prefix = ''): bool

foreach ($this->values as $key => $value) {
if (!isset($this->expiries[$key]) || $this->expiries[$key] <= $now || str_starts_with($key, $prefix)) {
unset($this->values[$key], $this->expiries[$key]);
unset($this->values[$key], $this->tags[$key], $this->expiries[$key]);
}
}

Expand All @@ -255,7 +263,7 @@ public function clear(string $prefix = ''): bool
}
}

$this->values = $this->expiries = [];
$this->values = $this->tags = $this->expiries = [];

return true;
}
Expand Down Expand Up @@ -312,7 +320,7 @@ private function generateItems(array $keys, float $now, \Closure $f): \Generator
}
unset($keys[$i]);

yield $key => $f($key, $value, $isHit);
yield $key => $f($key, $value, $isHit, $this->tags[$key] ?? null);
}

foreach ($keys as $key) {
Expand All @@ -334,7 +342,7 @@ private function freeze($value, string $key)
try {
$serialized = serialize($value);
} catch (\Exception $e) {
unset($this->values[$key]);
unset($this->values[$key], $this->tags[$key]);
$type = get_debug_type($value);
$message = sprintf('Failed to save key "{key}" of type %s: %s', $type, $e->getMessage());
CacheItem::log($this->logger, $message, ['key' => $key, 'exception' => $e, 'cache-adapter' => get_debug_type($this)]);
Expand Down
1 change: 0 additions & 1 deletion src/Symfony/Component/Cache/Adapter/ChainAdapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,6 @@ public function __construct(array $adapters, int $defaultLifetime = 0)
static function ($sourceItem, $item, $defaultLifetime, $sourceMetadata = null) {
$sourceItem->isTaggable = false;
$sourceMetadata ??= $sourceItem->metadata;
unset($sourceMetadata[CacheItem::METADATA_TAGS]);

$item->value = $sourceItem->value;
$item->isHit = $sourceItem->isHit;
Expand Down
52 changes: 17 additions & 35 deletions src/Symfony/Component/Cache/Adapter/ProxyAdapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public function __construct(CacheItemPoolInterface $pool, string $namespace = ''
}
$this->namespaceLen = \strlen($namespace);
$this->defaultLifetime = $defaultLifetime;
self::$createCacheItem ?? self::$createCacheItem = \Closure::bind(
self::$createCacheItem ??= \Closure::bind(
static function ($key, $innerItem, $poolHash) {
$item = new CacheItem();
$item->key = $key;
Expand All @@ -55,20 +55,12 @@ static function ($key, $innerItem, $poolHash) {
return $item;
}

$item->value = $v = $innerItem->get();
$item->value = $innerItem->get();
$item->isHit = $innerItem->isHit();
$item->innerItem = $innerItem;
$item->poolHash = $poolHash;

// Detect wrapped values that encode for their expiry and creation duration
// For compactness, these values are packed in the key of an array using
// magic numbers in the form 9D-..-..-..-..-00-..-..-..-5F
if (\is_array($v) && 1 === \count($v) && 10 === \strlen($k = (string) array_key_first($v)) && "\x9D" === $k[0] && "\0" === $k[5] && "\x5F" === $k[9]) {
$item->value = $v[$k];
$v = unpack('Ve/Nc', substr($k, 1, -1));
$item->metadata[CacheItem::METADATA_EXPIRY] = $v['e'] + CacheItem::METADATA_EXPIRY_OFFSET;
$item->metadata[CacheItem::METADATA_CTIME] = $v['c'];
} elseif ($innerItem instanceof CacheItem) {
if (!$item->unpack() && $innerItem instanceof CacheItem) {
$item->metadata = $innerItem->metadata;
}
$innerItem->set(null);
Expand All @@ -78,21 +70,10 @@ static function ($key, $innerItem, $poolHash) {
null,
CacheItem::class
);
self::$setInnerItem ?? self::$setInnerItem = \Closure::bind(
/**
* @param array $item A CacheItem cast to (array); accessing protected properties requires adding the "\0*\0" PHP prefix
*/
static function (CacheItemInterface $innerItem, array $item) {
// Tags are stored separately, no need to account for them when considering this item's newly set metadata
if (isset(($metadata = $item["\0*\0newMetadata"])[CacheItem::METADATA_TAGS])) {
unset($metadata[CacheItem::METADATA_TAGS]);
}
if ($metadata) {
// For compactness, expiry and creation duration are packed in the key of an array, using magic numbers as separators
$item["\0*\0value"] = ["\x9D".pack('VN', (int) (0.1 + $metadata[self::METADATA_EXPIRY] - self::METADATA_EXPIRY_OFFSET), $metadata[self::METADATA_CTIME])."\x5F" => $item["\0*\0value"]];
}
$innerItem->set($item["\0*\0value"]);
$innerItem->expiresAt(null !== $item["\0*\0expiry"] ? \DateTime::createFromFormat('U.u', sprintf('%.6F', $item["\0*\0expiry"])) : null);
self::$setInnerItem ??= \Closure::bind(
static function (CacheItemInterface $innerItem, CacheItem $item, $expiry = null) {
$innerItem->set($item->pack());
$innerItem->expiresAt(($expiry ?? $item->expiry) ? \DateTime::createFromFormat('U.u', sprintf('%.6F', $expiry ?? $item->expiry)) : null);
},
null,
CacheItem::class
Expand All @@ -111,7 +92,7 @@ public function get(string $key, callable $callback, float $beta = null, array &
return $this->pool->get($this->getId($key), function ($innerItem, bool &$save) use ($key, $callback) {
$item = (self::$createCacheItem)($key, $innerItem, $this->poolHash);
$item->set($value = $callback($item, $save));
(self::$setInnerItem)($innerItem, (array) $item);
(self::$setInnerItem)($innerItem, $item);

return $value;
}, $beta, $metadata);
Expand Down Expand Up @@ -212,22 +193,23 @@ private function doSave(CacheItemInterface $item, string $method): bool
if (!$item instanceof CacheItem) {
return false;
}
$item = (array) $item;
if (null === $item["\0*\0expiry"] && 0 < $this->defaultLifetime) {
$item["\0*\0expiry"] = microtime(true) + $this->defaultLifetime;
$castItem = (array) $item;

if (null === $castItem["\0*\0expiry"] && 0 < $this->defaultLifetime) {
$castItem["\0*\0expiry"] = microtime(true) + $this->defaultLifetime;
}

if ($item["\0*\0poolHash"] === $this->poolHash && $item["\0*\0innerItem"]) {
$innerItem = $item["\0*\0innerItem"];
if ($castItem["\0*\0poolHash"] === $this->poolHash && $castItem["\0*\0innerItem"]) {
$innerItem = $castItem["\0*\0innerItem"];
} elseif ($this->pool instanceof AdapterInterface) {
// this is an optimization specific for AdapterInterface implementations
// so we can save a round-trip to the backend by just creating a new item
$innerItem = (self::$createCacheItem)($this->namespace.$item["\0*\0key"], null, $this->poolHash);
$innerItem = (self::$createCacheItem)($this->namespace.$castItem["\0*\0key"], null, $this->poolHash);
} else {
$innerItem = $this->pool->getItem($this->namespace.$item["\0*\0key"]);
$innerItem = $this->pool->getItem($this->namespace.$castItem["\0*\0key"]);
}

(self::$setInnerItem)($innerItem, $item);
(self::$setInnerItem)($innerItem, $item, $castItem["\0*\0expiry"]);

return $this->pool->$method($innerItem);
}
Expand Down
Loading
0