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

Skip to content

Commit 3a3527b

Browse files
[Cache] Add hierachical tags based invalidation
1 parent 84b48de commit 3a3527b

10 files changed

+512
-30
lines changed

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

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,18 +45,25 @@ function ($key, $value, $isHit) use ($defaultLifetime) {
4545
CacheItem::class
4646
);
4747
$this->mergeByLifetime = \Closure::bind(
48-
function ($deferred, $namespace, &$expiredIds) {
48+
function ($deferred, $namespace, &$expiredIds, &$tags) {
4949
$byLifetime = array();
5050
$now = time();
5151
$expiredIds = array();
52+
$tags = array();
5253

5354
foreach ($deferred as $key => $item) {
55+
$id = $namespace.$key;
56+
5457
if (null === $item->expiry) {
55-
$byLifetime[0][$namespace.$key] = $item->value;
58+
$byLifetime[0][$id] = $item->value;
5659
} elseif ($item->expiry > $now) {
57-
$byLifetime[$item->expiry - $now][$namespace.$key] = $item->value;
60+
$ ED4F byLifetime[$item->expiry - $now][$id] = $item->value;
5861
} else {
59-
$expiredIds[] = $namespace.$key;
62+
$expiredIds[] = $id;
63+
continue;
64+
}
65+
foreach ($item->tags as $tag) {
66+
$tags[$namespace.'/'.$tag][$id] = $id;
6067
}
6168
}
6269

@@ -113,6 +120,23 @@ abstract protected function doDelete(array $ids);
113120
*/
114121
abstract protected function doSave(array $values, $lifetime);
115122

123+
/**
124+
* Adds hierarchical tags to a set of cache keys.
125+
*
126+
* @param array $tags The tags as keys, the set of corresponding identifiers as values.
127+
*
128+
* @return bool True if the tags were successfully stored, false otherwise.
129+
*/
130+
protected function doTag(array $tags)
131+
{
132+
if ($this instanceof TagInvalidationInterface) {
133+
throw new \LogicException(sprintf('Class "%s" must overwrite the AbstractAdapter::doTag() method to implement TagInvalidationInterface', get_class($this)));
134+
}
135+
CacheItem::log($this->logger, 'Failed to commit cache tags: {adapter} does not implement TagInvalidationInterface', array('adapter' => get_class($this)));
136+
137+
return false;
138+
}
139+
116140
/**
117141
* {@inheritdoc}
118142
*/
@@ -279,7 +303,7 @@ public function commit()
279303
{
280304
$ok = true;
281305
$byLifetime = F438 $this->mergeByLifetime;
282-
$byLifetime = $byLifetime($this->deferred, $this->namespace, $expiredIds);
306+
$byLifetime = $byLifetime($this->deferred, $this->namespace, $expiredIds, $tags);
283307
$retry = $this->deferred = array();
284308

285309
if ($expiredIds) {
@@ -321,6 +345,22 @@ public function commit()
321345
$ok = false;
322346
$type = is_object($v) ? get_class($v) : gettype($v);
323347
CacheItem::log($this->logger, 'Failed to save key "{key}" ({type})', array('key' => substr($id, strlen($this->namespace)), 'type' => $type, 'exception' => $e instanceof \Exception ? $e : null));
348+
349+
foreach ($tags as $tag => $ids) {
350+
unset($tags[$tag][$id]);
351+
}
352+
}
353+
}
354+
355+
if ($tags) {
356+
try {
357+
$ok = $this->doTag($tags) && $ok;
358+
} catch (\Exception $e) {
359+
if (!$this instanceof TagInvalidationInterface) {
360+
throw $e;
361+
}
362+
$ok = false;
363+
CacheItem::log($this->logger, 'Failed to commit cache tags', array('exception' => $e));
324364
}
325365
}
326366
$this->deferred = array();

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

Lines changed: 119 additions & 11 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 AbstractAdapter implements TagInvalidationInterface
2021
{
2122
private $directory;
2223

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

54+
/**
55+
* {@inheritdoc}
56+
*/
57+
public function invalidate($tag)
58+
{
59+
$tag = '/'.CacheItem::normalizeTag($tag);
60+
$ok = true;
61+
62+
foreach ($this->getInvalidatedIds($tag) as $id) {
63+
$file = $this->getFile($id, false);
64+
$ok = (!file_exists($file) || @unlink($file) || !file_exists($file)) && $ok;
65+
}
66+
67+
return $ok;
68+
}
69+
5370
/**
5471
* {@inheritdoc}
5572
*/
@@ -59,7 +76,7 @@ protected function doFetch(array $ids)
5976
$now = time();
6077

6178
foreach ($ids as $id) {
62-
$file = $this->getFile($id);
79+
$file = $this->getFile($id, false);
6380
if (!$h = @fopen($file, 'rb')) {
6481
continue;
6582
}
@@ -89,7 +106,7 @@ protected function doFetch(array $ids)
89106
*/
90107
protected function doHave($id)
91108
{
92-
$file = $this->getFile($id);
109+
$file = $this->getFile($id, false);
93110

94111
return file_exists($file) && (@filemtime($file) > time() || $this->doFetch(array($id)));
95112
}
@@ -116,7 +133,8 @@ protected function doDelete(array $ids)
116133
$ok = true;
117134

118135
foreach ($ids as $id) {
119-
$file = $this->getFile($id);
136+
$this->removeTags($id);
137+
$file = $this->getFile($id, false);
120138
$ok = (!file_exists($file) || @unlink($file) || !file_exists($file)) && $ok;
121139
}
122140

@@ -132,14 +150,11 @@ protected function doSave(array $values, $lifetime)
132150
$expiresAt = $lifetime ? time() + $lifetime : PHP_INT_MAX;
133151

134152
foreach ($values as $id => $value) {
135-
$file = $this->getFile($id);
136-
$dir = dirname($file);
137-
if (!file_exists($dir)) {
138-
@mkdir($dir, 0777, true);
139-
}
153+
$file = $this->getFile($id, true);
140154
$value = $expiresAt."\n".rawurlencode($id)."\n".serialize($value);
141155
if (false !== @file_put_contents($file, $value, LOCK_EX)) {
142156
@touch($file, $expiresAt);
157+
$this->removeTags($id);
143158
} else {
144159
$ok = false;
145160
}
@@ -148,10 +163,103 @@ protected function doSave(array $values, $lifetime)
148163
return $ok;
149164
}
150165

151-
private function getFile($id)
166+
/**
167+
* {@inheritdoc}
168+
*/
169+
protected function doTag(array $tags)
170+
{
171+
$ok = true;
172+
$byIds = array();
173+
174+
foreach ($tags as $tag => $ids) {
175+
$file = $this->getFile($tag, true);
176+
$h = fopen($file, 'ab');
177+
178+
foreach ($ids as $id) {
179+
$ok = fwrite($h, rawurlencode($id)."\n") && $ok;
180+
$byIds[$id][] = $tag;
181+
}
182+
fclose($h);
183+
}
184+
foreach ($byIds as $id => $tags) {
185+
$file = $this->getFile($id.':tag', true);
186+
$h = fopen($file, 'ab');
187+
188+
foreach ($tags as $tag) {
189+
fwrite($h, rawurlencode($tag)."\n");
190+
}
191+
fclose($h);
192+
}
193+
$s = strpos($tag, '/');
194+
$r = strrpos($tag, '/');
195+
while ($r > $s) {
196+
$parent = substr($tag, 0, $r);
197+
$ok = file_put_contents($this->getFile($parent, true), rawurlencode($tag)."\n", FILE_APPEND) && $ok;
198+
$r = strrpos($tag = $parent, '/');
199+
}
200+
201+
return $ok;
202+
}
203+
204+
private function getInvalidatedIds($tag)
205+
{
206+
$file = $this->getFile($tag, false);
207+
208+
if ($h = @fopen($file, 'rb')) {
209+
while (false !== $id = fgets($h)) {
210+
if ('!' === $id[0]) {
211+
continue;
212+
}
213+
$id = rawurldecode(substr($id, 0, -1));
214+
215+
if ('/' === $id[0]) {
216+
foreach ($this->getInvalidatedIds($id) as $id) {
217+
yield $id;
218+
}
219+
} else {
220+
yield $id;
221+
}
222+
}
223+
224+
fclose($h);
225+
@unlink($file);
226+
}
227+
}
228+
229+
public function removeTags($id)
230+
{
231+
if (!file_exists($file = $this->getFile($id.':tag', false))) {
232+
return;
233+
}
234+
$idLine = rawurlencode($id)."\n";
235+
$idSeek = -strlen($idLine);
236+
237+
foreach (file($file, FILE_IGNORE_NEW_LINES) as $tag) {
238+
if (!file_exists($tagFile = $this->getFile(rawurldecode($tag), false))) {
239+
continue;
240+
}
241+
$h = fopen($tagFile, 'r+b');
242+
243+
while (false !== $line = fgets($h)) {
244+
if ($line === $idLine) {
245+
fseek($h, $idSeek, SEEK_CUR);
246+
fwrite($h, '!');
247+
}
248+
}
249+
fclose($h);
250+
}
251+
@unlink($file);
252+
}
253+
254+
private function getFile($id, $mkdir)
152255
{
153256
$hash = str_replace('/', '-', base64_encode(md5($id, true)));
257+
$dir = $this->directory.$hash[0].DIRECTORY_SEPARATOR.$hash[1].DIRECTORY_SEPARATOR;
258+
259+
if ($mkdir && !file_exists($dir)) {
260+
@mkdir($dir, 0777, true);
261+
}
154262

155-
return $this->directory.$hash[0].DIRECTORY_SEPARATOR.$hash[1].DIRECTORY_SEPARATOR.substr($hash, 2, -2);
263+
return $dir.substr($hash, 2, -2);
156264
}
157265
}

0 commit comments

Comments
 (0)
0