10000 [Cache] Add hierachical tags based invalidation by nicolas-grekas · Pull Request #18646 · symfony/symfony · GitHub
[go: up one dir, main page]

Skip to content

[Cache] Add hierachical tags based invalidation #18646

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

Closed
Closed
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
29 changes: 21 additions & 8 deletions src/Symfony/Component/Cache/Adapter/AbstractAdapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,17 +47,22 @@ function ($key, $value, $isHit) use ($defaultLifetime) {
);
$this->mergeByLifetime = \Closure::bind(
function ($deferred, $namespace, &$expiredIds) {
$byLifetime = array();
$byLifetime = array('value' => array(), 'tags' => array());
$now = time();
$expiredIds = array();

foreach ($deferred as $key => $item) {
$id = $namespace.$key;

if (null === $item->expiry) {
$byLifetime[0][$namespace.$key] = $item->value;
$byLifetime['value'][0][$id] = $item->value;
$byLifetime['tags'][0][$id] = $item->tags;
} elseif ($item->expiry > $now) {
$byLifetime[$item->expiry - $now][$namespace.$key] = $item->value;
$byLifetime['value'][$item->expiry - $now][$id] = $item->value;
$byLifetime['tags'][$item->expiry - $now][$id] = $item->tags;
} else {
$expiredIds[] = $namespace.$key;
$expiredIds[] = $id;
continue;
}
}

Expand Down Expand Up @@ -313,9 +318,13 @@ public function commit()
if ($expiredIds) {
$this->doDelete($expiredIds);
}
foreach ($byLifetime as $lifetime => $values) {
foreach ($byLifetime['value'] as $lifetime => $values) {
try {
$e = $this->doSave($values, $lifetime);
if ($this instanceof AbstractTagsInvalidatingAdapter) {
$e = $this->doSaveWithTags($values, $lifetime, $byLifetime['tags'][$lifetime]);
} else {
$e = $this->doSave($values, $lifetime);
}
} catch (\Exception $e) {
}
if (true === $e || array() === $e) {
Expand All @@ -339,8 +348,12 @@ public function commit()
foreach ($retry as $lifetime => $ids) {
foreach ($ids as $id) {
try {
$v = $byLifetime[$lifetime][$id];
$e = $this->doSave(array($id => $v), $lifetime);
$v = $byLifetime['value'][$lifetime][$id];
if ($this instanceof AbstractTagsInvalidatingAdapter) {
$e = $this->doSaveWithTags(array($id => $v), $lifetime, array($id => $byLifetime['tags'][$lifetime][$id]));
} else {
$e = $this->doSave(array($id => $v), $lifetime);
}
} catch (\Exception $e) {
}
if (true === $e || array() === $e) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?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.
*/

namespace Symfony\Component\Cache\Adapter;

/**
* @author Nicolas Grekas <p@tchwork.com>
*/
abstract class AbstractTagsInvalidatingAdapter extends AbstractAdapter implements TagsInvalidatingAdapterInterface
{
/**
* Persists several cache items immediately.
*
* @param array $values The values to cache, indexed by their cache identifier.
* @param int $lifetime The lifetime of the cached values, 0 for persisting until manual cleaning.
* @param array $tags The tags corresponding to each value identifiers.
*
* @return array|bool The identifiers that failed to be cached or a boolean stating if caching succeeded or not.
*/
abstract protected function doSaveWithTags(array $values, $lifetime, array $tags);

/**
* @internal
*/
protected function doSave(array $values, $lifetime)
{
throw new \BadMethodCallException('Use doSaveWithTags() instead.');
}
}
130 changes: 128 additions & 2 deletions src/Symfony/Component/Cache/Adapter/FilesystemAdapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@

namespace Symfony\Component\Cache\Adapter;

use Symfony\Component\Cache\CacheItem;

/**
* @author Nicolas Grekas <p@tchwork.com>
*/
class FilesystemAdapter extends AbstractAdapter
class FilesystemAdapter extends AbstractTagsInvalidatingAdapter
{
use FilesystemAdapterTrait;

Expand All @@ -24,6 +26,20 @@ public function __construct($namespace = '', $defaultLifetime = 0, $directory =
$this->init($namespace, $directory);
}

/**
* {@inheritdoc}
*/
public function invalidateTags($tags)
{
$ok = true;

foreach (CacheItem::normalizeTags($tags) as $tag) {
$ok = $this->doInvalidateTag($tag) && $ok;
}

return $ok;
}

/**
* {@inheritdoc}
*/
Expand Down Expand Up @@ -68,15 +84,125 @@ protected function doHave($id)
/**
* {@inheritdoc}
*/
protected function doSave(array $values, $lifetime)
protected function doSaveWithTags(array $values, $lifetime, array $tags)
{
$ok = true;
$expiresAt = $lifetime ? time() + $lifetime : PHP_INT_MAX;
$newTags = $oldTags = array();

foreach ($values as $id => $value) {
$newIdTags = $tags[$id];
$file = $this->getFile($id, true);
$tagFile = $this->getFile($id.':tag', $newIdTags);
$hasFile = file_exists($file);

if ($hadTags = file_exists($tagFile)) {
foreach (file($tagFile, FILE_IGNORE_NEW_LINES) as $tag) {
if (isset($newIdTags[$tag = rawurldecode($tag)])) {
if ($hasFile) {
unset($newIdTags[$tag]);
}
} else {
$oldTags[] = $tag;
}
}
if ($oldTags) {
$this->removeTags($id, $oldTags);
$oldTags = array();
}
}
foreach ($newIdTags as $tag) {
$newTags[$tag][] = $id;
}

$ok = $this->write($this->getFile($id, true), $expiresAt."\n".rawurlencode($id)."\n".serialize($value), $expiresAt) && $ok;

if ($tags[$id]) {
$ok = $this->write($tagFile, implode("\n", array_map('rawurlencode', $tags[$id]))."\n") && $ok;
} elseif ($hadTags) {
@unlink($tagFile);
}
}
if ($newTags) {
$ok = $this->doTag($newTags) && $ok;
}

return $ok;
}

private function doTag(array $tags)
{
$ok = true;
$linkedTags = array();

foreach ($tags as $tag => $ids) {
$file = $this->getFile($tag, true);
$linkedTags[$tag] = file_exists($file) ?: null;
$h = fopen($file, 'ab');

foreach ($ids as $id) {
$ok = fwrite($h, rawurlencode($id)."\n") && $ok;
}
fclose($h);

while (!isset($linkedTags[$tag]) && 0 < $r = strrpos($tag, '/')) {
$linkedTags[$tag] = true;
$parent = substr($tag, 0, $r);
$file = $this->getFile($parent, true);
$linkedTags[$parent] = file_exists($file) ?: null;
$ok = file_put_contents($file, rawurlencode($tag)."\n", FILE_APPEND) && $ok;
$tag = $parent;
}
}

return $ok;
}

private function doInvalidateTag($tag)
{
if (!$h = @fopen($this->getFile($tag), 'r+b')) {
return true;
}
$ok = true;
$count = 0;

while (false !== $id = fgets($h)) {
if ('!' === $id[0]) {
continue;
}
$id = rawurldecode(substr($id, 0, -1));

if ('/' === $id[0]) {
$ok = $this->doInvalidateTag($id) && $ok;
} elseif (file_exists($file = $this->getFile($id))) {
$count += $unlink = @unlink($file);
$ok = ($unlink || !file_exists($file)) && $ok;
}
}

ftruncate($h, 0);
fclose($h);
CacheItem::log($this->logger, 'Invalidating {count} items tagged as "{tag}"', array('tag' => $tag, 'count' => $count));

return $ok;
}

private function removeTags($id, $tags)
{
$idLine = rawurlencode($id)."\n";
$idSeek = -strlen($idLine);

foreach ($tags as $tag) {
if (!$h = @fopen($this->getFile($tag), 'r+b')) {
continue;
}
while (false !== $line = fgets($h)) {
if ($line === $idLine) {
fseek($h, $idSeek, SEEK_CUR);
fwrite($h, '!');
}
}
fclose($h);
}
}
}
Loading
0