Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 21381f1

Browse files
[Cache] Add hierachical tags based invalidation
1 parent 6f83328 commit 21381f1

11 files changed

+568
-21
lines changed

src/Symfony/Component/Cache/Adapter/AbstractAdapter.php

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -47,17 +47,22 @@ function ($key, $value, $isHit) use ($defaultLifetime) {
4747
);
4848
$this->mergeByLifetime = \Closure::bind(
4949
function ($deferred, $namespace, &$expiredIds) {
50-
$byLifetime = array();
50+
$byLifetime = array('value' => array(), 'tags' => array());
5151
$now = time();
5252
$expiredIds = array();
5353

5454
foreach ($deferred as $key => $item) {
55+
$id = $namespace.$key;
56+
5557
if (null === $item->expiry) {
56-
$byLifetime[0][$namespace.$key] = $item->value;
58+
$byLifetime['value'][0][$id] = $item->value;
59+
$byLifetime['tags'][0][$id] = $item->tags;
5760
} elseif ($item->expiry > $now) {
58-
$byLifetime[$item->expiry - $now][$namespace.$key] = $item->value;
61+
$byLifetime['value'][$item->expiry - $now][$id] = $item->value;
62+
$byLifetime['tags'][$item->expiry - $now][$id] = $item->tags;
5963
} else {
60-
$expiredIds[] = $namespace.$key;
64+
$expiredIds[] = $id;
65+
continue;
6166
}
6267
}
6368

@@ -313,9 +318,13 @@ public function commit()
313318
if ($expiredIds) {
314319
$this->doDelete($expiredIds);
315320
}
316-
foreach ($byLifetime as $lifetime => $values) {
321+
foreach ($byLifetime['value'] as $lifetime => $values) {
317322
try {
318-
$e = $this->doSave($values, $lifetime);
323+
if ($this instanceof AbstractTagsInvalidatingAdapter) {
324+
$e = $this->doSaveWithTags($values, $lifetime, $byLifetime['tags'][$lifetime]);
325+
} else {
326+
$e = $this->doSave($values, $lifetime);
327+
}
319328
} catch (\Exception $e) {
320329
}
321330
if (true === $e || array() === $e) {
@@ -339,8 +348,12 @@ public function commit()
339348
foreach ($retry as $lifetime => $ids) {
340349
foreach ($ids as $id) {
341350
try {
342-
$v = $byLifetime[$lifetime][$id];
343-
$e = $this->doSave(array($id => $v), $lifetime);
351+
$v = $byLifetime['value'][$lifetime][$id];
352+
if ($this instanceof AbstractTagsInvalidatingAdapter) {
353+
$e = $this->doSaveWithTags(array($id => $v), $lifetime, array($id => $byLifetime['tags'][$lifetime][$id]));
354+
} else {
355+
$e = $this->doSave(array($id => $v), $lifetime);
356+
}
344357
} catch (\Exception $e) {
345358
}
346359
if (true === $e || array() === $e) {
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Cache\Adapter;
13+
14+
/**
15+
* @author Nicolas Grekas <[email protected]>
16+
*/
17+
abstract class AbstractTagsInvalidatingAdapter extends AbstractAdapter implements TagsInvalidatingAdapterInterface
18+
{
19+
/**
20+
* Persists several cache items immediately.
21+
*
22+
* @param array $values The values to cache, indexed by their cache identifier.
23+
* @param int $lifetime The lifetime of the cached values, 0 for persisting until manual cleaning.
24+
* @param array $tags The tags corresponding to each value identifiers.
25+
*
26+
* @return array|bool The identifiers that failed to be cached or a boolean stating if caching succeeded or not.
27+
*/
28+
abstract protected function doSaveWithTags(array $values, $lifetime, array $tags);
29+
30+
/**
31+
* @internal
32+
*/
33+
protected function doSave(array $values, $lifetime)
34+
{
35+
throw new \BadMethodCallException('Use doSaveWithTags() instead.');
36+
}
37+
}

src/Symfony/Component/Cache/Adapter/FilesystemAdapter.php

Lines changed: 126 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@
1111

1212
namespace Symfony\Component\Cache\Adapter;
1313

14+
use Symfony\Component\Cache\CacheItem;
15+
1416
/**
1517
* @author Nicolas Grekas <[email protected]>
1618
*/
17-
class FilesystemAdapter extends AbstractAdapter
19+
class FilesystemAdapter extends AbstractTagsInvalidatingAdapter
1820
{
1921
use FilesystemAdapterTrait;
2022

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

29+
/**
30+
* {@inheritdoc}
31+
*/
32+
public function invalidateTags($tags)
33+
{
34+
$ok = true;
35+
36+
foreach (CacheItem::normalizeTags($tags) as $tag) {
37+
$ok = $this->doInvalidateTag($tag) && $ok;
38+
}
39+
40+
return $ok;
41+
}
42+
2743
/**
2844
* {@inheritdoc}
2945
*/
@@ -68,15 +84,123 @@ protected function doHave($id)
6884
/**
6985
* {@inheritdoc}
7086
*/
71-
protected function doSave(array $values, $lifetime)
87+
protected function doSaveWithTags(array $values, $lifetime, array $tags)
7288
{
7389
$ok = true;
7490
$expiresAt = $lifetime ? time() + $lifetime : PHP_INT_MAX;
91+
$newTags = $oldTags = array();
7592

7693
foreach ($values as $id => $value) {
94+
$newIdTags = $tags[$id];
95+
$file = $this->getFile($id, true);
96+
$tagFile = $this->getFile($id.':tag', $newIdTags);
97+
$hasFile = file_exists($file);
98+
99+
if ($hadTags = file_exists($tagFile)) {
100+
foreach (file($tagFile, FILE_IGNORE_NEW_LINES) as $tag) {
101+
if (isset($newIdTags[$tag = rawurldecode($tag)])) {
102+
if ($hasFile) {
103+
unset($newIdTags[$tag]);
104+
}
105+
} else {
106+
$oldTags[] = $tag;
107+
}
108+
}
109+
if ($oldTags) {
110+
$this->removeTags($id, $oldTags);
111+
$oldTags = array();
112+
}
113+
}
114+
foreach ($newIdTags as $tag) {
115+
$newTags[$tag][] = $id;
116+
}
117+
77118
$ok = $this->write($this->getFile($id, true), $expiresAt."\n".rawurlencode($id)."\n".serialize($value), $expiresAt) && $ok;
119+
120+
if ($tags[$id]) {
121+
$ok = $this->write($tagFile, implode("\n", array_map('rawurlencode', $tags[$id]))."\n") && $ok;
122+
} elseif ($hadTags) {
123+
@unlink($tagFile);
124+
}
125+
}
126+
if ($newTags) {
127+
$ok = $this->doTag($newTags) && $ok;
78128
}
79129

80130
return $ok;
81131
}
132+
133+
private function doTag(array $tags)
134+
{
135+
$ok = true;
136+
$linkedTags = array();
137+
138+
foreach ($tags as $tag => $ids) {
139+
$file = $this->getFile($tag, true);
140+
$linkedTags[$tag] = file_exists($file) ?: null;
141+
$h = fopen($file, 'ab');
142+
143+
foreach ($ids as $id) {
144+
$ok = fwrite($h, rawurlencode($id)."\n") && $ok;
145+
}
146+
fclose($h);
147+
148+
while (!isset($linkedTags[$tag]) && 0 < $r = strrpos($tag, '/')) {
149+
$linkedTags[$tag] = true;
150+
$parent = substr($tag, 0, $r);
151+
$file = $this->getFile($parent, true);
152+
$linkedTags[$parent] = file_exists($file) ?: null;
153+
$ok = file_put_contents($file, rawurlencode($tag)."\n", FILE_APPEND) && $ok;
154+
$tag = $parent;
155+
}
156+
}
157+
158+
return $ok;
159+
}
160+
161+
private function doInvalidateTag($tag)
162+
{
163+
if (!$h = @fopen($this->getFile($tag), 'r+b')) {
164+
return true;
165+
}
166+
$ok = true;
167+
168+
while (false !== $id = fgets($h)) {
169+
if ('!' === $id[0]) {
170+
continue;
171+
}
172+
$id = rawurldecode(substr($id, 0, -1));
173+
174+
if ('/' === $id[0]) {
175+
$ok = $this->doInvalidateTag($id) && $ok;
176+
} else {
177+
$file = $this->getFile($id);
178+
$ok = (!file_exists($file) || @unlink($file) || !file_exists($file)) && $ok;
179+
}
180+
}
181+
182+
ftruncate($h, 0);
183+
fclose($h);
184+
185+
return $ok;
186+
}
187+
188+
private function removeTags($id, $tags)
189+
{
190+
$idLine = rawurlencode($id)."\n";
191+
$idSeek = -strlen($idLine);
192+
193+
foreach ($tags as $tag) {
194+
if (!$h = @fopen($this->getFile($tag), 'r+b')) {
195+
continue;
196+
}
197+
while (false !== $line = fgets($h)) {
198+
if ($line === $idLine) {
199+
fseek($h, $idSeek, SEEK_CUR);
200+
fwrite($h, '!');
201+
}
202+
}
203+
fclose($h);
204+
}
205+
}
82206
}

0 commit comments

Comments
 (0)