8000 [Cache] Add hierachical tags based invalidation · symfony/symfony@d032e6d · GitHub
[go: up one dir, main page]

Skip to content

Commit d032e6d

Browse files
[Cache] Add hierachical tags based invalidation
1 parent 0469c4a commit d032e6d

11 files changed

+566
-21
lines changed

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

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

5353
foreach ($deferred as $key => $item) {
54+
$id = $namespace.$key;
55+
5456
if (null === $item->expiry) {
55-
$byLifetime[0][$namespace.$key] = $item->value;
57+
$byLifetime['value'][0][$id] = $item->value;
58+
$byLifetime['tags'][0][$id] = $item->tags;
5659
} elseif ($item->expiry > $now) {
57-
$byLifetime[$item->expiry - $now][$namespace.$key] = $item->value;
60+
$byLifetime['value'][$item->expiry - $now][$id] = $item->value;
61+
$byLifetime['tags'][$item->expiry - $now][$id] = $item->tags;
5862
} else {
59-
$expiredIds[] = $namespace.$key;
63+
$expiredIds[] = $id;
64+
continue;
6065
}
6166
}
6267

@@ -285,9 +290,13 @@ public function commit()
285290
if ($expiredIds) {
286291
$this->doDelete($expiredIds);
287292
}
288-
foreach ($byLifetime as $lifetime => $values) {
293+
foreach ($byLifetime['value'] as $lifetime => $values) {
289294
try {
290-
$e = $this->doSave($values, $lifetime);
295+
if ($this instanceof AbstractInvalidatingAdapter) {
296+
$e = $this->doSaveWithTags($values, $lifetime, $byLifetime['tags'][$lifetime]);
297+
} else {
298+
$e = $this->doSave($values, $lifetime);
299+
}
291300
} catch (\Exception $e) {
292301
}
293302
if (true === $e || array() === $e) {
@@ -311,8 +320,12 @@ public function commit()
311320
foreach ($retry as $lifetime => $ids) {
312321
foreach ($ids as $id) {
313322
try {
314-
$v = $byLifetime[$lifetime][$id];
315-
$e = $this->doSave(array($id => $v), $lifetime);
323+
$v = $byLifetime['value'][$lifetime][$id];
324+
if ($this instanceof AbstractInvalidatingAdapter) {
325+
$e = $this->doSaveWithTags(array($id => $v), $lifetime, array($id => $byLifetime['tags'][$lifetime][$id]));
326+
} else {
327+
$e = $this->doSave(array($id => $v), $lifetime);
328+
}
316329
} catch (\Exception $e) {
317330
}
318331
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 <fabien@symfony.com>
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 <p@tchwork.com>
16+
*/
17+
abstract class AbstractInvalidatingAdapter extends AbstractAdapter implements InvalidatingAdapterInterface
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: 123 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,13 @@
1111

1212
namespace Symfony\Component\Cache\Adapter;
1313

14+
use Symfony\Component\Cache\CacheItem;
1415
use Symfony\Component\Cache\Exception\InvalidArgumentException;
1516

1617
/**
1718
* @author Nicolas Grekas <p@tchwork.com>
1819
*/
19-
class FilesystemAdapter extends AbstractAdapter
20+
class FilesystemAdapter extends AbstractInvalidatingAdapter
2021
{
2122
private $directory;
2223

@@ -50,6 +51,20 @@ public function __construct($namespace = '', $defaultLifetime = 0, $directory =
5051
$this->directory = $dir;
5152
}
5253

54+
/**
55+
* {@inheritdoc}
56+
*/
57+
public function invalidate($tags)
58+
{
59+
$ok = true;
60+
61+
foreach (CacheItem::normalizeTags($tags) as $tag) {
62+
$ok = $this->doInvalidate($tag) && $ok;
63+
}
64+
65+
return $ok;
66+
}
67+
5368
/**
5469
* {@inheritdoc}
5570
*/
@@ -123,14 +138,37 @@ protected function doDelete(array $ids)
123138
/**
124139
* {@inheritdoc}
125140
*/
126-
protected function doSave(array $values, $lifetime)
141+
protected function doSaveWithTags(array $values, $lifetime, array $tags)
127142
{
128143
$ok = true;
129144
$expiresAt = $lifetime ? time() + $lifetime : PHP_INT_MAX;
145+
$newTags = $oldTags = array();
130146
$tmp = $this->directory.uniqid('', true);
131147

132148
foreach ($values as $id => $value) {
149+
$newIdTags = $tags[$id];
133150
$file = $this->getFile($id, true);
151+
$tagFile = $this->getFile($id.':tag', $newIdTags);
152+
$hasFile = file_exists($file);
153+
154+
if ($hadTags = file_exists($tagFile)) {
155+
foreach (file($tagFile, FILE_IGNORE_NEW_LINES) as $tag) {
156+
if (isset($newIdTags[$tag = rawurldecode($tag)])) {
157+
if ($hasFile) {
158+
unset($newIdTags[$tag]);
159+
}
160+
} else {
161+
$oldTags[] = $tag;
162+
}
163+
}
164+
if ($oldTags) {
165+
$this->removeTags($id, $oldTags);
166+
$oldTags = array();
167+
}
168+
}
169+
foreach ($newIdTags as $tag) {
170+
$newTags[$tag][] = $id;
171+
}
134172

135173
$value = $expiresAt."\n".rawurlencode($id)."\n".serialize($value);
136174
if (false !== @file_put_contents($tmp, $value)) {
@@ -139,11 +177,94 @@ protected function doSave(array $values, $lifetime)
139177
} else {
140178
$ok = false;
141179
}
180+
181+
if ($tags[$id]) {
182+
$ok = false !== @file_put_contents($tmp, implode("\n", array_map('rawurlencode', $tags[$id]))."\n") && @rename($tmp, $tagFile) && $ok;
183+
} elseif ($hadTags) {
184+
@unlink($tagFile);
185+
}
186+
}
187+
if ($newTags) {
188+
$ok = $this->doTag($newTags) && $ok;
142189
}
143190

144191
return $ok;
145192
}
146193

194+
private function doTag(array $tags)
195+
{
196+
$ok = true;
197+
$linkedTags = array();
198+
199+
foreach ($tags as $tag => $ids) {
200+
$file = $this->getFile($tag, true);
201+
$linkedTags[$tag] = file_exists($file) ?: null;
202+
$h = fopen($file, 'ab');
203+
204+
foreach ($ids as $id) {
205+
$ok = fwrite($h, rawurlencode($id)."\n") && $ok;
206+
}
207+
fclose($h);
208+
209+
while (!isset($linkedTags[$tag]) && 0 < $r = strrpos($tag, '/')) {
210+
$linkedTags[$tag] = true;
211+
$parent = substr($tag, 0, $r);
212+
$file = $this->getFile($parent, true);
213+
$linkedTags[$parent] = file_exists($file) ?: null;
214+
$ok = file_put_contents($file, rawurlencode($tag)."\n", FILE_APPEND) && $ok;
215+
$tag = $parent;
216+
}
217+
}
218+
219+
return $ok;
220+
}
221+
222+
private function doInvalidate($tag)
223+
{
224+
if (!$h = @fopen($this->getFile($tag), 'r+b')) {
225+
return true;
226+
}
227+
$ok = true;
228+
229+
while (false !== $id = fgets($h)) {
230+
if ('!' === $id[0]) {
231+
continue;
232+
}
233+
$id = rawurldecode(substr($id, 0, -1));
234+
235+
if ('/' === $id[0]) {
236+
$ok = $this->doInvalidate($id) && $ok;
237+
} else {
238+
$file = $this->getFile($id);
239+
$ok = (!file_exists($file) || @unlink($file) || !file_exists($file)) && $ok;
240+
}
241+
}
242+
243+
ftruncate($h, 0);
244+
fclose($h);
245+
246+
return $ok;
247+
}
248+
249+
private function removeTags($id, $tags)
250+
{
251+
$idLine = rawurlencode($id)."\n";
252+
$idSeek = -strlen($idLine);
253+
254+
foreach ($tags as $tag) {
255+
if (!$h = @fopen($this->getFile($tag), 'r+b')) {
256+
continue;
257+
}
258+
while (false !== $line = fgets($h)) {
259+
if ($line === $idLine) {
260+
fseek($h, $idSeek, SEEK_CUR);
261+
fwrite($h, '!');
262+
}
263+
}
264+
fclose($h);
265+
}
266+
}
267+
147268
private function getFile($id, $mkdir = false)
148269
{
149270
$hash = str_replace('/', '-', base64_encode(md5($id, true)));
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
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+
use Psr\Cache\CacheItemPoolInterface;
15+
use Psr\Cache\InvalidArgumentException;
16+
17+
/**
18+
* Interface for invalidating cached items using tag hierarchies.
19+
*
20+
* @author Nicolas Grekas <p@tchwork.com>
21+
*/
22+
interface InvalidatingAdapterInterface extends CacheItemPoolInterface
23+
{
24+
/**
25+
* Invalidates cached items using tag hierarchies.
26+
*
27+
* @param string|string[] $tags A tag or an array of tag hierarchies to invalidate.
28+
*
29+
* @return bool True on success.
30+
*
31+
* @throws InvalidArgumentException When $tags is not valid.
32+
*/
33+
public function invalidate($tags);
34+
}

0 commit comments

Comments
 (0)
0