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

Skip to content

Commit f37bcd9

Browse files
[Cache] Add hierachical tags based invalidation
1 parent d9e0eab commit f37bcd9

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 <[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 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 <[email protected]>
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
*/
@@ -126,25 +141,131 @@ protected function doDelete(array $ids)
126141
/**
127142
* {@inheritdoc}
128143
*/
129-
protected function doSave(array $values, $lifetime)
144+
protected function doSaveWithTags(array $values, $lifetime, array $tags)
130145
{
131146
$ok = true;
132147
$expiresAt = $lifetime ? time() + $lifetime : PHP_INT_MAX;
148+
$newTags = $oldTags = array();
133149

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

137175
$value = $expiresAt."\n".rawurlencode($id)."\n".serialize($value);
138176
if (false !== @file_put_contents($file, $value, LOCK_EX)) {
139177
@touch($file, $expiresAt);
140178
} else {
141179
$ok = false;
142180
}
181+
182+
if ($tags[$id]) {
183+
$ok = file_put_contents($tagFile, implode("\n", array_map('rawurlencode', $tags[$id]))."\n") && $ok;
184+
} elseif ($hadTags) {
185+
@unlink($tagFile);
186+
}
187+
}
188+
if ($newTags) {
189+
$ok = $this->doTag($newTags) && $ok;
143190
}
144191

145192
return $ok;
146193
}
147194

195+
private function doTag(array $tags)
196+
{
197+
$ok = true;
198+
$linkedTags = array();
199+
200+
foreach ($tags as $tag => $ids) {
201+
$file = $this->getFile($tag, true);
202+
$linkedTags[$tag] = file_exists($file) ?: null;
203+
$h = fopen($file, 'ab');
204+
205+
foreach ($ids as $id) {
206+
$ok = fwrite($h, rawurlencode($id)."\n") && $ok;
207+
}
208+
fclose($h);
209+
210+
while (!isset($linkedTags[$tag]) && 0 < $r = strrpos($tag, '/')) {
211+
$linkedTags[$tag] = true;
212+
$parent = substr($tag, 0, $r);
213+
$file = $this->getFile($parent, true);
214+
$linkedTags[$parent] = file_exists($file) ?: null;
215+
$ok = file_put_contents($file, rawurlencode($tag)."\n", FILE_APPEND) && $ok;
216+
$tag = $parent;
217+
}
218+
}
219+
220+
return $ok;
221+
}
222+
223+
private function doInvalidate($tag)
224+
{
225+
if (!$h = @fopen($this->getFile($tag, false), 'r+b')) {
226+
return true;
227+
}
228+
$ok = true;
229+
230+
while (false !== $id = fgets($h)) {
231+
if ('!' === $id[0]) {
232+
continue;
233+
}
234+
$id = rawurldecode(substr($id, 0, -1));
235+
236+
if ('/' === $id[0]) {
237+
$ok = $this->doInvalidate($id) && $ok;
238+
} else {
239+
$file = $this->getFile($id, false);
240+
$ok = (!file_exists($file) || @unlink($file) || !file_exists($file)) && $ok;
241+
}
242+
}
243+
244+
ftruncate($h, 0);
245+
fclose($h);
246+
247+
return $ok;
248+
}
249+
250+
private function removeTags($id, $tags)
251+
{
252+
$idLine = rawurlencode($id)."\n";
253+
$idSeek = -strlen($idLine);
254+
255+
foreach ($tags as $tag) {
256+
if (!$h = @fopen($this->getFile($tag, false), 'r+b')) {
257+
continue;
258+
}
259+
while (false !== $line = fgets($h)) {
260+
if ($line === $idLine) {
261+
fseek($h, $idSeek, SEEK_CUR);
262+
fwrite($h, '!');
263+
}
264+
}
265+
fclose($h);
266+
}
267+
}
268+
148269
private function getFile($id, $mkdir)
149270
{
150271
$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 <[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+
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 <[email protected]>
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)