diff --git a/src/Symfony/Component/Cache/Adapter/AbstractAdapter.php b/src/Symfony/Component/Cache/Adapter/AbstractAdapter.php index 9bc05fd2b3..6b38991b77 100644 --- a/src/Symfony/Component/Cache/Adapter/AbstractAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/AbstractAdapter.php @@ -38,7 +38,7 @@ abstract class AbstractAdapter implements AdapterInterface, LoggerAwareInterface */ protected function __construct($namespace = '', $defaultLifetime = 0) { - $this->namespace = '' === $namespace ? '' : $this->getId($namespace).':'; + $this->namespace = '' === $namespace ? '' : CacheItem::validateKey($namespace).':'; if (null !== $this->maxIdLength && strlen($namespace) > $this->maxIdLength - 24) { throw new InvalidArgumentException(sprintf('Namespace must be %d chars max, %d given ("%s")', $this->maxIdLength - 24, strlen($namespace), $namespace)); } diff --git a/src/Symfony/Component/Cache/Adapter/ProxyAdapter.php b/src/Symfony/Component/Cache/Adapter/ProxyAdapter.php index 4f37ffd731..cd310be062 100644 --- a/src/Symfony/Component/Cache/Adapter/ProxyAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/ProxyAdapter.php @@ -35,7 +35,7 @@ class ProxyAdapter implements AdapterInterface { $this->pool = $pool; $this->poolHash = $poolHash = spl_object_hash($pool); - $this->namespace = '' === $namespace ? '' : $this->getId($namespace); + $this->namespace = '' === $namespace ? '' : CacheItem::validateKey($namespace); $this->namespaceLen = strlen($namespace); $this->createCacheItem = \Closure::bind( function ($key, $innerItem) use ($defaultLifetime, $poolHash) { diff --git a/src/Symfony/Component/Cache/CacheItem.php b/src/Symfony/Component/Cache/CacheItem.php index 55e25de9a9..d6f629b2ab 100644 --- a/src/Symfony/Component/Cache/CacheItem.php +++ b/src/Symfony/Component/Cache/CacheItem.php @@ -148,6 +148,8 @@ final class CacheItem implements CacheItemInterface * * @param string $key The key to validate * + * @return string + * * @throws InvalidArgumentException When $key is not valid */ public static function validateKey($key) @@ -161,6 +163,8 @@ final class CacheItem implements CacheItemInterface if (false !== strpbrk($key, '{}()/\@:')) { throw new InvalidArgumentException(sprintf('Cache key "%s" contains reserved characters {}()/\@:', $key)); } + + return $key; } /** diff --git a/src/Symfony/Component/Cache/Simple/AbstractCache.php b/src/Symfony/Component/Cache/Simple/AbstractCache.php index e4046463f1..264eb60653 100644 --- a/src/Symfony/Component/Cache/Simple/AbstractCache.php +++ b/src/Symfony/Component/Cache/Simple/AbstractCache.php @@ -37,7 +37,7 @@ abstract class AbstractCache implements CacheInterface, LoggerAwareInterface protected function __construct($namespace = '', $defaultLifetime = 0) { $this->defaultLifetime = max(0, (int) $defaultLifetime); - $this->namespace = '' === $namespace ? '' : $this->getId($namespace).':'; + $this->namespace = '' === $namespace ? '' : CacheItem::validateKey($namespace).':'; if (null !== $this->maxIdLength && strlen($namespace) > $this->maxIdLength - 24) { throw new InvalidArgumentException(sprintf('Namespace must be %d chars max, %d given ("%s")', $this->maxIdLength - 24, strlen($namespace), $namespace)); } diff --git a/src/Symfony/Component/Cache/Tests/CacheItemTest.php b/src/Symfony/Component/Cache/Tests/CacheItemTest.php index 4e455c64a4..daca925fd5 100644 --- a/src/Symfony/Component/Cache/Tests/CacheItemTest.php +++ b/src/Symfony/Component/Cache/Tests/CacheItemTest.php @@ -18,7 +18,7 @@ class CacheItemTest extends TestCase { public function testValidKey() { - $this->assertNull(CacheItem::validateKey('foo')); + $this->assertSame('foo', CacheItem::validateKey('foo')); } /** diff --git a/src/Symfony/Component/Cache/Traits/AbstractTrait.php b/src/Symfony/Component/Cache/Traits/AbstractTrait.php index 375ccf7620..108ac67c8e 100644 --- a/src/Symfony/Component/Cache/Traits/AbstractTrait.php +++ b/src/Symfony/Component/Cache/Traits/AbstractTrait.php @@ -24,6 +24,8 @@ trait AbstractTrait use LoggerAwareTrait; private $namespace; + private $namespaceVersion = ''; + private $versioningIsEnabled = false; private $deferred = array(); /** @@ -102,10 +104,18 @@ trait AbstractTrait */ public function clear() { + if ($cleared = $this->versioningIsEnabled) { + $this->namespaceVersion = 2; + foreach ($this->doFetch(array('@'.$this->namespace)) as $v) { + $this->namespaceVersion = 1 + (int) $v; + } + $this->namespaceVersion .= ':'; + $cleared = $this->doSave(array('@'.$this->namespace => $this->namespaceVersion), 0); + } $this->deferred = array(); try { - return $this->doClear($this->namespace); + return $this->doClear($this->namespace) || $cleared; } catch (\Exception $e) { CacheItem::log($this->logger, 'Failed to clear the cache', array('exception' => $e)); @@ -158,6 +168,27 @@ trait AbstractTrait return $ok; } + /** + * Enables/disables versioning of items. + * + * When versioning is enabled, clearing the cache is atomic and doesn't require listing existing keys to proceed, + * but old keys may need garbage collection and extra round-trips to the back-end are required. + * + * Calling this method also clears the memoized namespace version and thus forces a resynchonization of it. + * + * @param bool $enable + * + * @return bool the previous state of versioning + */ + public function enableVersioning($enable = true) + { + $wasEnabled = $this->versioningIsEnabled; + $this->versioningIsEnabled = (bool) $enable; + $this->namespaceVersion = ''; + + return $wasEnabled; + } + /** * Like the native unserialize() function but throws an exception if anything goes wrong. * @@ -189,11 +220,18 @@ trait AbstractTrait { CacheItem::validateKey($key); - if (null === $this->maxIdLength) { - return $this->namespace.$key; + if ($this->versioningIsEnabled && '' === $this->namespaceVersion) { + $this->namespaceVersion = '1:'; + foreach ($this->doFetch(array('@'.$this->namespace)) as $v) { + $this->namespaceVersion = $v; + } } - if (strlen($id = $this->namespace.$key) > $this->maxIdLength) { - $id = $this->namespace.substr_replace(base64_encode(hash('sha256', $key, true)), ':', -22); + + if (null === $this->maxIdLength) { + return $this->namespace.$this->namespaceVersion.$key; + } + if (strlen($id = $this->namespace.$this->namespaceVersion.$key) > $this->maxIdLength) { + $id = $this->namespace.$this->namespaceVersion.substr_replace(base64_encode(hash('sha256', $key, true)), ':', -22); } return $id; diff --git a/src/Symfony/Component/Cache/Traits/MemcachedTrait.php b/src/Symfony/Component/Cache/Traits/MemcachedTrait.php index 8a836bac18..6dc46e9dd8 100644 --- a/src/Symfony/Component/Cache/Traits/MemcachedTrait.php +++ b/src/Symfony/Component/Cache/Traits/MemcachedTrait.php @@ -54,6 +54,7 @@ trait MemcachedTrait } parent::__construct($namespace, $defaultLifetime); + $this->enableVersioning(); } /** @@ -242,7 +243,7 @@ trait MemcachedTrait */ protected function doClear($namespace) { - return $this->checkResultCode($this->getClient()->flush()); + return false; } private function checkResultCode($result) diff --git a/src/Symfony/Component/Cache/Traits/RedisTrait.php b/src/Symfony/Component/Cache/Traits/RedisTrait.php index b45d65adc8..6202051a8d 100644 --- a/src/Symfony/Component/Cache/Traits/RedisTrait.php +++ b/src/Symfony/Component/Cache/Traits/RedisTrait.php @@ -46,7 +46,9 @@ trait RedisTrait if (preg_match('#[^-+_.A-Za-z0-9]#', $namespace, $match)) { throw new InvalidArgumentException(sprintf('RedisAdapter namespace contains "%s" but only characters in [-+_.A-Za-z0-9] are allowed.', $match[0])); } - if (!$redisClient instanceof \Redis && !$redisClient instanceof \RedisArray && !$redisClient instanceof \RedisCluster && !$redisClient instanceof \Predis\Client) { + if ($redisClient instanceof \RedisCluster) { + $this->enableversioning(); + } elseif (!$redisClient instanceof \Redis && !$redisClient instanceof \RedisArray && !$redisClient instanceof \Predis\Client) { 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))); } $this->redis = $redisClient; @@ -171,8 +173,8 @@ trait RedisTrait */ protected function doClear($namespace) { - // When using a native Redis cluster, clearing the cache cannot work and always returns false. - // Clearing the cache should then be done by any other means (e.g. by restarting the cluster). + // When using a native Redis cluster, clearing the cache is done by versioning in AbstractTrait::clear(). + // This means old keys are not really removed until they expire and may need gargage collection. $cleared = true; $hosts = array($this->redis);