Skip to content

Commit 025be26

Browse files
[Cache] Use namespace versioning for backends that dont support clearing by keys
1 parent 420f089 commit 025be26

File tree

10 files changed

+42
-12
lines changed

10 files changed

+42
-12
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ abstract class AbstractAdapter implements AdapterInterface, LoggerAwareInterface
3838
*/
3939
protected function __construct($namespace = '', $defaultLifetime = 0)
4040
{
41-
$this->namespace = '' === $namespace ? '' : $this->getId($namespace).':';
41+
$this->namespace = '' === $namespace ? '' : CacheItem::validateKey($namespace).':';
4242
if (null !== $this->maxIdLength && strlen($namespace) > $this->maxIdLength - 24) {
4343
throw new InvalidArgumentException(sprintf('Namespace must be %d chars max, %d given ("%s")', $this->maxIdLength - 24, strlen($namespace), $namespace));
4444
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ class MemcachedAdapter extends AbstractAdapter
1818
use MemcachedTrait;
1919

2020
protected $maxIdLength = 250;
21+
protected $versioningIsEnabled = true;
2122

2223
/**
2324
* Constructor.

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ public function __construct(CacheItemPoolInterface $pool, $namespace = '', $defa
3535
{
3636
$this->pool = $pool;
3737
$this->poolHash = $poolHash = spl_object_hash($pool);
38-
$this->namespace = '' === $namespace ? '' : $this->getId($namespace);
38+
$this->namespace = '' === $namespace ? '' : CacheItem::validateKey($namespace);
3939
$this->namespaceLen = strlen($namespace);
4040
$this->createCacheItem = \Closure::bind(
4141
function ($key, $innerItem) use ($defaultLifetime, $poolHash) {

src/Symfony/Component/Cache/CacheItem.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,8 @@ public function getPreviousTags()
148148
*
149149
* @param string $key The key to validate
150150
*
151+
* @return string
152+
*
151153
* @throws InvalidArgumentException When $key is not valid
152154
*/
153155
public static function validateKey($key)
@@ -161,6 +163,8 @@ public static function validateKey($key)
161163
if (false !== strpbrk($key, '{}()/\@:')) {
162164
throw new InvalidArgumentException(sprintf('Cache key "%s" contains reserved characters {}()/\@:', $key));
163165
}
166+
167+
return $key;
164168
}
165169

166170
/**

src/Symfony/Component/Cache/Simple/AbstractCache.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ abstract class AbstractCache implements CacheInterface, LoggerAwareInterface
3737
protected function __construct($namespace = '', $defaultLifetime = 0)
3838
{
3939
$this->defaultLifetime = max(0, (int) $defaultLifetime);
40-
$this->namespace = '' === $namespace ? '' : $this->getId($namespace).':';
40+
$this->namespace = '' === $namespace ? '' : CacheItem::validateKey($namespace).':';
4141
if (null !== $this->maxIdLength && strlen($namespace) > $this->maxIdLength - 24) {
4242
throw new InvalidArgumentException(sprintf('Namespace must be %d chars max, %d given ("%s")', $this->maxIdLength - 24, strlen($namespace), $namespace));
4343
}

src/Symfony/Component/Cache/Simple/MemcachedCache.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ class MemcachedCache extends AbstractCache
1818
use MemcachedTrait;
1919

2020
protected $maxIdLength = 250;
21+
protected $versioningIsEnabled = true;
2122

2223
/**
2324
* @param \Memcached $client

src/Symfony/Component/Cache/Tests/CacheItemTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ class CacheItemTest extends TestCase
1818
{
1919
public function testValidKey()
2020
{
21-
$this->assertNull(CacheItem::validateKey('foo'));
21+
$this->assertSame('foo', CacheItem::validateKey('foo'));
2222
}
2323

2424
/**

src/Symfony/Component/Cache/Traits/AbstractTrait.php

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,20 @@ trait AbstractTrait
2424
use LoggerAwareTrait;
2525

2626
private $namespace;
27+
private $namespaceVersion = '';
2728
private $deferred = array();
2829

2930
/**
3031
* @var int|null The maximum length to enforce for identifiers or null when no limit applies
3132
*/
3233
protected $maxIdLength;
3334

35+
/**
36+
* @var bool When versioning is enabled, listing existing key for clearing the cache is not required,
37+
* but old keys may need garbage collection and extra round-trips to the back-end are needed.
38+
*/
39+
protected $versioningIsEnabled = false;
40+
3441
/**
3542
* Fetches several cache items.
3643
*
@@ -102,10 +109,18 @@ public function hasItem($key)
102109
*/
103110
public function clear()
104111
{
112+
if ($cleared = $this->versioningIsEnabled) {
113+
$this->namespaceVersion = 2;
114+
foreach ($this->doFetch(array('@'.$this->namespace)) as $v) {
115+
$this->namespaceVersion = 1 + (int) $v;
116+
}
117+
$this->namespaceVersion .= ':';
118+
$cleared = $this->doSave(array('@'.$this->namespace => $this->namespaceVersion), 0);
119+
}
105120
$this->deferred = array();
106121

107122
try {
108-
return $this->doClear($this->namespace);
123+
return $this->doClear($this->namespace) || $cleared;
109124
} catch (\Exception $e) {
110125
CacheItem::log($this->logger, 'Failed to clear the cache', array('exception' => $e));
111126

@@ -189,11 +204,18 @@ private function getId($key)
189204
{
190205
CacheItem::validateKey($key);
191206

207+
if ($this->versioningIsEnabled && '' === $this->namespaceVersion) {
208+
$this->namespaceVersion = '1:';
209+
foreach ($this->doFetch(array('@'.$this->namespace)) as $v) {
210+
$this->namespaceVersion = $v;
211+
}
212+
}
213+
192214
if (null === $this->maxIdLength) {
193-
return $this->namespace.$key;
215+
return $this->namespace.$this->namespaceVersion.$key;
194216
}
195-
if (strlen($id = $this->namespace.$key) > $this->maxIdLength) {
196-
$id = $this->namespace.substr_replace(base64_encode(hash('sha256', $key, true)), ':', -22);
217+
if (strlen($id = $this->namespace.$this->namespaceVersion.$key) > $this->maxIdLength) {
218+
$id = $this->namespace.$this->namespaceVersion.substr_replace(base64_encode(hash('sha256', $key, true)), ':', -22);
197219
}
198220

199221
return $id;

src/Symfony/Component/Cache/Traits/MemcachedTrait.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@ protected function doDelete(array $ids)
233233
*/
234234
protected function doClear($namespace)
235235
{
236-
return $this->checkResultCode($this->client->flush());
236+
return false;
237237
}
238238

239239
private function checkResultCode($result)

src/Symfony/Component/Cache/Traits/RedisTrait.php

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,9 @@ public function init($redisClient, $namespace = '', $defaultLifetime = 0)
4646
if (preg_match('#[^-+_.A-Za-z0-9]#', $namespace, $match)) {
4747
throw new InvalidArgumentException(sprintf('RedisAdapter namespace contains "%s" but only characters in [-+_.A-Za-z0-9] are allowed.', $match[0]));
4848
}
49-
if (!$redisClient instanceof \Redis && !$redisClient instanceof \RedisArray && !$redisClient instanceof \RedisCluster && !$redisClient instanceof \Predis\Client) {
49+
if ($redisClient instanceof \RedisCluster) {
50+
$this->versioningIsEnabled = true;
51+
} elseif (!$redisClient instanceof \Redis && !$redisClient instanceof \RedisArray && !$redisClient instanceof \Predis\Client) {
5052
throw new InvalidArgumentException(sprintf('%s() expects parameter 1 to be Redis, RedisArray, RedisCluster or Predis\Client, %s given', __METHOD__, is_object($redisClient) ? get_class($redisClient) : gettype($redisClient)));
5153
}
5254
$this->redis = $redisClient;
@@ -171,8 +173,8 @@ protected function doHave($id)
171173
*/
172174
protected function doClear($namespace)
173175
{
174-
// When using a native Redis cluster, clearing the cache cannot work and always returns false.
175-
// Clearing the cache should then be done by any other means (e.g. by restarting the cluster).
176+
// When using a native Redis cluster, clearing the cache is done by versioning in AbstractTrait::clear().
177+
// This means old keys are not really removed until they expire and may need gargage collection.
176178

177179
$cleared = true;
178180
$hosts = array($this->redis);

0 commit comments

Comments
 (0)