Skip to content

Commit

Permalink
bug #45896 [Cache] Optimize caching of tags (sbelyshkin)
Browse files Browse the repository at this point in the history
This PR was merged into the 6.1 branch.

Discussion
----------

[Cache] Optimize caching of tags

| Q             | A
| ------------- | ---
| Branch?       | 6.1
| Bug fix?      | no
| New feature?  | no <!-- please update src/**/CHANGELOG.md files -->
| Deprecations? | no <!-- please update UPGRADE-*.md and src/**/CHANGELOG.md files -->
| Tickets       | - <!-- prefix each issue number with "Fix #", no need to create an issue if none exist, explain below instead -->
| License       | MIT
| Doc PR        | - <!-- required for new features -->

It's the follow-up to #42997.

1. Forcing the adapter to fetch tags on item commits makes the algorithm straightforward and a bit more optimized.
2. Caching tag versions only when they are retrieved from or being persisted to the pool ensures that any tagged item will be rejected when there is no related tags in the pool.
3. Using FIFO instead of LRU for known tag versions allows to use all cached tags until expiration and still be able to prevent memory leak.

<!--
Replace this notice by a short README for your feature/bugfix.
This will help reviewers and should be a good start for the documentation.

Additionally (see https://symfony.com/releases):
 - Always add tests and ensure they pass.
 - Bug fixes must be submitted against the lowest maintained branch where they apply
   (lowest branches are regularly merged to upper ones so they get the fixes too.)
 - Features and deprecations must be submitted against the latest branch.
 - Changelog entry should follow https://symfony.com/doc/current/contributing/code/conventions.html#writing-a-changelog-entry
 - Never break backward compatibility (see https://symfony.com/bc).
-->

Commits
-------

68f309b [Cache] Optimize caching of tags
  • Loading branch information
nicolas-grekas committed Apr 25, 2022
2 parents 6bc0882 + 68f309b commit fa636c0
Show file tree
Hide file tree
Showing 2 changed files with 26 additions and 24 deletions.
36 changes: 18 additions & 18 deletions src/Symfony/Component/Cache/Adapter/TagAwareAdapter.php
Expand Up @@ -39,7 +39,6 @@ class TagAwareAdapter implements TagAwareAdapterInterface, TagAwareCacheInterfac
use LoggerAwareTrait;

public const TAGS_PREFIX = "\0tags\0";
private const MAX_NUMBER_OF_KNOWN_TAG_VERSIONS = 1000;

private array $deferred = [];
private AdapterInterface $pool;
Expand Down Expand Up @@ -79,8 +78,7 @@ static function (array $items, array $itemTags) {
);
self::$setTagVersions ??= \Closure::bind(
static function (array $items, array $tagVersions) {
$now = null;
foreach ($items as $key => $item) {
foreach ($items as $item) {
$item->newMetadata[CacheItem::METADATA_TAGS] = array_intersect_key($tagVersions, $item->newMetadata[CacheItem::METADATA_TAGS] ?? []);
}
},
Expand Down Expand Up @@ -343,14 +341,16 @@ public function __destruct()
private function getTagVersions(array $tagsByKey, bool $persistTags): array
{
$tagVersions = [];
$fetchTagVersions = false;
$fetchTagVersions = $persistTags;

foreach ($tagsByKey as $tags) {
$tagVersions += $tags;

if ($fetchTagVersions) {
continue;
}
foreach ($tags as $tag => $version) {
if ($tagVersions[$tag] !== $version) {
unset($this->knownTagVersions[$tag]);
$fetchTagVersions = true;
}
}
}
Expand All @@ -364,14 +364,10 @@ private function getTagVersions(array $tagsByKey, bool $persistTags): array
foreach ($tagVersions as $tag => $version) {
$tags[$tag.static::TAGS_PREFIX] = $tag;
$knownTagVersion = $this->knownTagVersions[$tag] ?? [0, null];
if ($fetchTagVersions || $knownTagVersion[1] !== $version || $now - $knownTagVersion[0] >= $this->knownTagVersionsTtl) {
// reuse previously fetched tag versions up to the ttl
if ($fetchTagVersions || $now > $knownTagVersion[0] || $knownTagVersion[1] !== $version) {
// reuse previously fetched tag versions until the expiration
$fetchTagVersions = true;
}
unset($this->knownTagVersions[$tag]); // For LRU tracking
if ([0, null] !== $knownTagVersion) {
$this->knownTagVersions[$tag] = $knownTagVersion;
}
}

if (!$fetchTagVersions) {
Expand All @@ -380,20 +376,24 @@ private function getTagVersions(array $tagsByKey, bool $persistTags): array

$newTags = [];
$newVersion = null;
$expiration = $now + $this->knownTagVersionsTtl;
foreach ($this->tags->getItems(array_keys($tags)) as $tag => $version) {
if (!$version->isHit()) {
unset($this->knownTagVersions[$tag = $tags[$tag]]); // update FIFO
if (null !== $tagVersions[$tag] = $version->get()) {
$this->knownTagVersions[$tag] = [$expiration, $tagVersions[$tag]];
} elseif ($persistTags) {
$newTags[$tag] = $version->set($newVersion ??= random_bytes(6));
$tagVersions[$tag] = $newVersion;
$this->knownTagVersions[$tag] = [$expiration, $newVersion];
}
$tagVersions[$tag = $tags[$tag]] = $version->get();
$this->knownTagVersions[$tag] = [$now, $tagVersions[$tag]];
}

if ($newTags && $persistTags) {
if ($newTags) {
(self::$saveTags)($this->tags, $newTags);
}

if (\count($this->knownTagVersions) > $maxTags = max(self::MAX_NUMBER_OF_KNOWN_TAG_VERSIONS, \count($newTags) << 1)) {
array_splice($this->knownTagVersions, 0, $maxTags >> 1);
while ($now > ($this->knownTagVersions[$tag = array_key_first($this->knownTagVersions)][0] ?? \INF)) {
unset($this->knownTagVersions[$tag]);
}

return $tagVersions;
Expand Down
Expand Up @@ -12,7 +12,6 @@
namespace Symfony\Component\Cache\Tests\Adapter;

use PHPUnit\Framework\MockObject\MockObject;
use Psr\Cache\CacheItemInterface;
use Psr\Cache\CacheItemPoolInterface;
use Symfony\Component\Cache\Adapter\AdapterInterface;
use Symfony\Component\Cache\Adapter\ArrayAdapter;
Expand Down Expand Up @@ -67,15 +66,18 @@ public function testKnownTagVersionsTtl()

$pool->save($item);
$this->assertTrue($pool->getItem('foo')->isHit());
$this->assertTrue($pool->getItem('foo')->isHit());

sleep(20);
$tagsPool->deleteItem('baz'.TagAwareAdapter::TAGS_PREFIX); // tag invalidation

$this->assertTrue($pool->getItem('foo')->isHit());
$this->assertTrue($pool->getItem('foo')->isHit()); // known tag version is used

sleep(5);
sleep(10);

$this->assertTrue($pool->getItem('foo')->isHit());
$this->assertTrue($pool->getItem('foo')->isHit()); // known tag version is still used

sleep(1);

$this->assertFalse($pool->getItem('foo')->isHit()); // known tag version has expired
}

public function testInvalidateTagsWithArrayAdapter()
Expand Down

0 comments on commit fa636c0

Please sign in to comment.