Commit 503e46bb authored by catch's avatar catch

Issue #918538 by Berdir, slashrsm, damiankloip, sun, tobiasb: Decouple cache tags from cache bins

parent 370d6fda
......@@ -32,6 +32,18 @@ services:
class: Drupal\Core\Cache\TimeZoneCacheContext
tags:
- { name: cache.context}
cache_tags.invalidator:
parent: container.trait
class: Drupal\Core\Cache\CacheTagsInvalidator
calls:
- [setContainer, ['@service_container']]
tags:
- { name: service_collector, call: addInvalidator, tag: cache_tags_invalidator }
cache_tags.invalidator.checksum:
class: Drupal\Core\Cache\DatabaseCacheTagsChecksum
arguments: ['@database']
tags:
- { name: cache_tags_invalidator}
cache.backend.chainedfast:
class: Drupal\Core\Cache\ChainedFastBackendFactory
arguments: ['@settings']
......@@ -39,12 +51,13 @@ services:
- [setContainer, ['@service_container']]
cache.backend.database:
class: Drupal\Core\Cache\DatabaseBackendFactory
arguments: ['@database']
arguments: ['@database', '@cache_tags.invalidator.checksum']
cache.backend.apcu:
class: Drupal\Core\Cache\ApcuBackendFactory
arguments: ['@app.root']
arguments: ['@app.root', '@cache_tags.invalidator.checksum']
cache.backend.php:
class: Drupal\Core\Cache\PhpBackendFactory
arguments: ['@cache_tags.invalidator.checksum']
cache.bootstrap:
class: Drupal\Core\Cache\CacheBackendInterface
tags:
......
......@@ -36,29 +36,11 @@ class ApcuBackend implements CacheBackendInterface {
protected $binPrefix;
/**
* Prefix for keys holding invalidation cache tags.
* The cache tags checksum provider.
*
* Includes the site-specific prefix in $sitePrefix.
*
* @var string
*/
protected $invalidationsTagsPrefix;
/**
* Prefix for keys holding invalidation cache tags.
*
* Includes the site-specific prefix in $sitePrefix.
*
* @var string
* @var \Drupal\Core\Cache\CacheTagsChecksumInterface
*/
protected $deletionsTagsPrefix;
/**
* A static cache of all tags checked during the request.
*
* @var array
*/
protected static $tagCache = array('deletions' => array(), 'invalidations' => array());
protected $checksumProvider;
/**
* Constructs a new ApcuBackend instance.
......@@ -67,13 +49,14 @@ class ApcuBackend implements CacheBackendInterface {
* The name of the cache bin.
* @param string $site_prefix
* The prefix to use for all keys in the storage that belong to this site.
* @param \Drupal\Core\Cache\CacheTagsChecksumInterface $checksum_provider
* The cache tags checksum provider.
*/
public function __construct($bin, $site_prefix) {
public function __construct($bin, $site_prefix, CacheTagsChecksumInterface $checksum_provider) {
$this->bin = $bin;
$this->sitePrefix = $site_prefix;
$this->checksumProvider = $checksum_provider;
$this->binPrefix = $this->sitePrefix . '::' . $this->bin . '::';
$this->invalidationsTagsPrefix = $this->sitePrefix . '::itags::';
$this->deletionsTagsPrefix = $this->sitePrefix . '::dtags::';
}
/**
......@@ -163,18 +146,12 @@ protected function prepareItem($cache, $allow_invalid) {
}
$cache->tags = $cache->tags ? explode(' ', $cache->tags) : array();
$checksum = $this->checksumTags($cache->tags);
// Check if deleteTags() has been called with any of the entry's tags.
if ($cache->checksum_deletions != $checksum['deletions']) {
return FALSE;
}
// Check expire time.
$cache->valid = $cache->expire == Cache::PERMANENT || $cache->expire >= REQUEST_TIME;
// Check if invalidateTags() has been called with any of the entry's tags.
if ($cache->checksum_invalidations != $checksum['invalidations']) {
if (!$this->checksumProvider->isValid($cache->checksum, $cache->tags)) {
$cache->valid = FALSE;
}
......@@ -196,9 +173,7 @@ public function set($cid, $data, $expire = CacheBackendInterface::CACHE_PERMANEN
$cache->created = round(microtime(TRUE), 3);
$cache->expire = $expire;
$cache->tags = implode(' ', $tags);
$checksum = $this->checksumTags($tags);
$cache->checksum_invalidations = $checksum['invalidations'];
$cache->checksum_deletions = $checksum['deletions'];
$cache->checksum = $this->checksumProvider->getCurrentChecksum($tags);
// APC serializes/unserializes any structure itself.
$cache->serialized = 0;
$cache->data = $data;
......@@ -283,65 +258,4 @@ public function invalidateAll() {
}
}
/**
* {@inheritdoc}
*/
public function deleteTags(array $tags) {
foreach ($tags as $tag) {
apc_inc($this->deletionsTagsPrefix . $tag, 1, $success);
if (!$success) {
apc_store($this->deletionsTagsPrefix . $tag, 1);
}
}
}
/**
* {@inheritdoc}
*/
public function invalidateTags(array $tags) {
foreach ($tags as $tag) {
apc_inc($this->invalidationsTagsPrefix . $tag, 1, $success);
if (!$success) {
apc_store($this->invalidationsTagsPrefix . $tag, 1);
}
}
}
/**
* Returns the sum total of validations for a given set of tags.
*
* @param array $tags
* Associative array of tags.
*
* @return int
* Sum of all invalidations.
*/
protected function checksumTags(array $tags) {
$checksum = array('invalidations' => 0, 'deletions' => 0);
$query_tags = array('invalidations' => array(), 'deletions' => array());
foreach ($tags as $tag) {
foreach (array('deletions', 'invalidations') as $type) {
if (isset(static::$tagCache[$type][$tag])) {
$checksum[$type] += static::$tagCache[$type][$tag];
}
else {
$query_tags[$type][] = $this->{$type . 'TagsPrefix'} . $tag;
}
}
}
foreach (array('deletions', 'invalidations') as $type) {
if ($query_tags[$type]) {
$result = apc_fetch($query_tags[$type]);
if ($result) {
static::$tagCache[$type] = array_merge(static::$tagCache[$type], $result);
$checksum[$type] += array_sum($result);
}
}
}
return $checksum;
}
}
......@@ -18,14 +18,24 @@ class ApcuBackendFactory implements CacheFactoryInterface {
*/
protected $sitePrefix;
/**
* The cache tags checksum provider.
*
* @var \Drupal\Core\Cache\CacheTagsChecksumInterface
*/
protected $checksumProvider;
/**
* Constructs an ApcuBackendFactory object.
*
* @param string $root
* The app root.
* @param \Drupal\Core\Cache\CacheTagsChecksumInterface $checksum_provider
* The cache tags checksum provider.
*/
public function __construct($root) {
public function __construct($root, CacheTagsChecksumInterface $checksum_provider) {
$this->sitePrefix = Crypt::hashBase64($root . '/' . conf_path());
$this->checksumProvider = $checksum_provider;
}
/**
......@@ -38,7 +48,7 @@ public function __construct($root) {
* The cache backend object for the specified cache bin.
*/
public function get($bin) {
return new ApcuBackend($bin, $this->sitePrefix);
return new ApcuBackend($bin, $this->sitePrefix, $this->checksumProvider);
}
}
......@@ -23,7 +23,7 @@
* @ingroup cache
*/
class BackendChain implements CacheBackendInterface {
class BackendChain implements CacheBackendInterface, CacheTagsInvalidatorInterface {
/**
* Ordered list of CacheBackendInterface instances.
......@@ -158,15 +158,6 @@ public function deleteMultiple(array $cids) {
}
}
/**
* Implements Drupal\Core\Cache\CacheBackendInterface::deleteTags().
*/
public function deleteTags(array $tags) {
foreach ($this->backends as $backend) {
$backend->deleteTags($tags);
}
}
/**
* Implements Drupal\Core\Cache\CacheBackendInterface::deleteAll().
*/
......@@ -199,7 +190,9 @@ public function invalidateMultiple(array $cids) {
*/
public function invalidateTags(array $tags) {
foreach ($this->backends as $backend) {
$backend->invalidateTags($tags);
if ($backend instanceof CacheTagsInvalidatorInterface) {
$backend->invalidateTags($tags);
}
}
}
......
......@@ -93,44 +93,14 @@ public static function buildTags($prefix, array $suffixes) {
return $tags;
}
/**
* Deletes items from all bins with any of the specified tags.
*
* Many sites have more than one active cache backend, and each backend may
* use a different strategy for storing tags against cache items, and
* deleting cache items associated with a given tag.
*
* When deleting a given list of tags, we iterate over each cache backend, and
* and call deleteTags() on each.
*
* @param string[] $tags
* The list of tags to delete cache items for.
*/
public static function deleteTags(array $tags) {
static::validateTags($tags);
foreach (static::getBins() as $cache_backend) {
$cache_backend->deleteTags($tags);
}
}
/**
* Marks cache items from all bins with any of the specified tags as invalid.
*
* Many sites have more than one active cache backend, and each backend my use
* a different strategy for storing tags against cache items, and invalidating
* cache items associated with a given tag.
*
* When invalidating a given list of tags, we iterate over each cache backend,
* and call invalidateTags() on each.
*
* @param string[] $tags
* The list of tags to invalidate cache items for.
*/
public static function invalidateTags(array $tags) {
static::validateTags($tags);
foreach (static::getBins() as $cache_backend) {
$cache_backend->invalidateTags($tags);
}
\Drupal::service('cache_tags.invalidator')->invalidateTags($tags);
}
/**
......
......@@ -136,7 +136,6 @@ public function setMultiple(array $items);
*
* @see \Drupal\Core\Cache\CacheBackendInterface::invalidate()
* @see \Drupal\Core\Cache\CacheBackendInterface::deleteMultiple()
* @see \Drupal\Core\Cache\CacheBackendInterface::deleteTags()
* @see \Drupal\Core\Cache\CacheBackendInterface::deleteAll()
*/
public function delete($cid);
......@@ -155,39 +154,16 @@ public function delete($cid);
*
* @see \Drupal\Core\Cache\CacheBackendInterface::invalidateMultiple()
* @see \Drupal\Core\Cache\CacheBackendInterface::delete()
* @see \Drupal\Core\Cache\CacheBackendInterface::deleteTags()
* @see \Drupal\Core\Cache\CacheBackendInterface::deleteAll()
*/
public function deleteMultiple(array $cids);
/**
* Deletes items with any of the specified tags.
*
* If the cache items are being deleted because they are no longer "fresh",
* you may consider using invalidateTags() instead. This allows callers to
* retrieve the invalid items by calling get() with $allow_invalid set to TRUE.
* In some cases an invalid item may be acceptable rather than having to
* rebuild the cache.
*
* @param array $tags
* Associative array of tags, in the same format that is passed to
* CacheBackendInterface::set().
*
* @see \Drupal\Core\Cache\CacheBackendInterface::set()
* @see \Drupal\Core\Cache\CacheBackendInterface::invalidateTags()
* @see \Drupal\Core\Cache\CacheBackendInterface::delete()
* @see \Drupal\Core\Cache\CacheBackendInterface::deleteMultiple()
* @see \Drupal\Core\Cache\CacheBackendInterface::deleteAll()
*/
public function deleteTags(array $tags);
/**
* Deletes all cache items in a bin.
*
* @see \Drupal\Core\Cache\CacheBackendInterface::invalidateAll()
* @see \Drupal\Core\Cache\CacheBackendInterface::delete()
* @see \Drupal\Core\Cache\CacheBackendInterface::deleteMultiple()
* @see \Drupal\Core\Cache\CacheBackendInterface::deleteTags()
*/
public function deleteAll();
......@@ -202,7 +178,6 @@ public function deleteAll();
*
* @see \Drupal\Core\Cache\CacheBackendInterface::delete()
* @see \Drupal\Core\Cache\CacheBackendInterface::invalidateMultiple()
* @see \Drupal\Core\Cache\CacheBackendInterface::invalidateTags()
* @see \Drupal\Core\Cache\CacheBackendInterface::invalidateAll()
*/
public function invalidate($cid);
......@@ -213,44 +188,24 @@ public function invalidate($cid);
* Invalid items may be returned in later calls to get(), if the $allow_invalid
* argument is TRUE.
*
* @param string $cids
* @param string[] $cids
* An array of cache IDs to invalidate.
*
* @see \Drupal\Core\Cache\CacheBackendInterface::deleteMultiple()
* @see \Drupal\Core\Cache\CacheBackendInterface::invalidate()
* @see \Drupal\Core\Cache\CacheBackendInterface::invalidateTags()
* @see \Drupal\Core\Cache\CacheBackendInterface::invalidateAll()
*/
public function invalidateMultiple(array $cids);
/**
* Marks cache items with any of the specified tags as invalid.
*
* @param array $tags
* Associative array of tags, in the same format that is passed to
* CacheBackendInterface::set().
*
* @see \Drupal\Core\Cache\CacheBackendInterface::set()
* @see \Drupal\Core\Cache\CacheBackendInterface::deleteTags()
* @see \Drupal\Core\Cache\CacheBackendInterface::invalidate()
* @see \Drupal\Core\Cache\CacheBackendInterface::invalidateMultiple()
* @see \Drupal\Core\Cache\CacheBackendInterface::invalidateAll()
*/
public function invalidateTags(array $tags);
/**
* Marks all cache items as invalid.
*
* Invalid items may be returned in later calls to get(), if the $allow_invalid
* argument is TRUE.
*
* @param string $cids
* An array of cache IDs to invalidate.
*
* @see \Drupal\Core\Cache\CacheBackendInterface::deleteAll()
* @see \Drupal\Core\Cache\CacheBackendInterface::invalidate()
* @see \Drupal\Core\Cache\CacheBackendInterface::invalidateMultiple()
* @see \Drupal\Core\Cache\CacheBackendInterface::invalidateTags()
*/
public function invalidateAll();
......
......@@ -280,7 +280,7 @@ public function reset() {
public function clear() {
$this->reset();
if ($this->tags) {
Cache::deleteTags($this->tags);
Cache::invalidateTags($this->tags);
}
else {
$this->cache->delete($this->getCid());
......
<?php
/**
* @file
* Contains \Drupal\Core\Cache\CacheTagsChecksumInterface.
*/
namespace Drupal\Core\Cache;
/**
* Provides checksums for cache tag invalidations.
*
* Cache backends can use this to check if any cache tag invalidations happened
* for a stored cache item.
*
* To do so, they can inject the cache_tags.invalidator.checksum service, and
* when a cache item is written, store cache tags together with the current
* checksum, calculated by getCurrentChecksum(). When a cache item is fetched,
* the checksum can be validated with isValid(). The service will return FALSE
* if any of those cache tags were invalidated in the meantime.
*
* @ingroup cache
*/
interface CacheTagsChecksumInterface {
/**
* Returns the sum total of validations for a given set of tags.
*
* Called by a backend when storing a cache item.
*
* @param string[] $tags
* Array of cache tags.
*
* @return string
* Cache tag invalidations checksum.
*/
public function getCurrentChecksum(array $tags);
/**
* Returns whether the checksum is valid for the given cache tags.
*
* Used when retrieving a cache item in a cache backend, to verify that no
* cache tag based invalidation happened.
*
* @param int $checksum
* The checksum that was stored together with the cache item.
* @param string[] $tags
* The cache tags that were stored together with the cache item.
*
* @return bool
* FALSE if cache tag invalidations happened for the passed in tags since
* the cache item was stored, TRUE otherwise.
*/
public function isValid($checksum, array $tags);
/**
* Reset statically cached tags.
*
* This is only used by tests.
*/
public function reset();
}
<?php
/**
* @file
* Contains \Drupal\Core\Cache\CacheTagsInvalidator.
*/
namespace Drupal\Core\Cache;
use Symfony\Component\DependencyInjection\ContainerAwareTrait;
/**
* Passes cache tag events to classes that wish to respond to them.
*/
class CacheTagsInvalidator implements CacheTagsInvalidatorInterface {
use ContainerAwareTrait;
/**
* Holds an array of cache tags invalidators.
*
* @var \Drupal\Core\Cache\CacheTagsInvalidatorInterface[]
*/
protected $invalidators = array();
/**
* {@inheritdoc}
*/
public function invalidateTags(array $tags) {
// Validate the tags.
Cache::validateTags($tags);
// Notify all added cache tags invalidators.
foreach ($this->invalidators as $invalidator) {
$invalidator->invalidateTags($tags);
}
// Additionally, notify each cache bin if it implements the service.
foreach ($this->getInvalidatorCacheBins() as $bin) {
$bin->invalidateTags($tags);
}
}
/**
* Adds a cache tags invalidator.
*
* @param \Drupal\Core\Cache\CacheTagsInvalidatorInterface $invalidator
* A cache invalidator.
*/
public function addInvalidator(CacheTagsInvalidatorInterface $invalidator) {
$this->invalidators[] = $invalidator;
}
/**
* Returns all cache bins that need to be notified about invalidations.
*
* @return \Drupal\Core\Cache\CacheTagsInvalidatorInterface[]
* An array of cache backend objects that implement the invalidator
* interface, keyed by their cache bin.
*/
protected function getInvalidatorCacheBins() {
$bins = array();
foreach ($this->container->getParameter('cache_bins') as $service_id => $bin) {
$service = $this->container->get($service_id);
if ($service instanceof CacheTagsInvalidatorInterface) {
$bins[$bin] = $service;
}
}
return $bins;
}
}
<?php
/**
* @file
* Contains \Drupal\Core\Cache\CacheTagsInvalidatorInterface
*/
namespace Drupal\Core\Cache;
/**
* Defines required methods for classes wanting to handle cache tag changes.
*
* Services that implement this interface must add the cache_tags_invalidator
* tag to be notified. Cache backends may implement this interface as well, they
* will be notified automatically.
*
* @ingroup cache
*/
interface CacheTagsInvalidatorInterface {
/**
* Marks cache items with any of the specified tags as invalid.
*
* @param string[] $tags
* The list of tags for which to invalidate cache items.
*/
public function invalidateTags(array $tags);
}
......@@ -41,7 +41,7 @@
*
* @ingroup cache
*/
class ChainedFastBackend implements CacheBackendInterface {
class ChainedFastBackend implements CacheBackendInterface, CacheTagsInvalidatorInterface {
/**
* Cache key prefix for the bin-specific entry to track the last write.
......@@ -210,14 +210,6 @@ public function deleteMultiple(array $cids) {
$this->markAsOutdated();
}
/**
* {@inheritdoc}
*/
public function deleteTags(array $tags) {
$this->markAsOutdated();
$this->consistentBackend->deleteTags($tags);
}
/**
* {@inheritdoc}
*/
......@@ -245,7 +237,9 @@ public function invalidateMultiple(array $cids) {
* {@inheritdoc}
*/
public function invalidateTags(array $tags) {
$this->consistentBackend->invalidateTags($tags);
if ($this->consistentBackend instanceof CacheTagsInvalidatorInterface) {
$this->consistentBackend->invalidateTags($tags);
}
$this->markAsOutdated();
}
......
......@@ -18,13 +18,24 @@ class DatabaseBackendFactory implements CacheFactoryInterface {
*/
protected $connection;
/**
* The cache tags checksum provider.
*
* @var \Drupal\Core\Cache\CacheTagsChecksumInterface
*/
protected $checksumProvider;
/**
* Constructs the DatabaseBackendFactory object.
*
* @param \Drupal\Core\Database\Connection $connection
* Database connection
* @param \Drupal\Core\Cache\CacheTagsChecksumInterface $checksum_provider
* The cache tags checksum provider.
*/
function __construct(Connection $connection) {
function __construct(Connection $connection, CacheTagsChecksumInterface $checksum_provider) {
$this->connection = $connection;
$this->checksumProvider = $checksum_provider;
}
/**
......@@ -37,7 +48,7 @@ function __construct(Connection $connection) {
* The cache backend object for the specified cache bin.
*/
function get($bin) {
return new DatabaseBackend($this->connection, $bin);
return new DatabaseBackend($this->connection, $this->checksumProvider, $bin);
}
}
<?php
/**
* @file
* Contains \Drupal\Core\Cache\DatabaseCacheTagsChecksum.
*/
namespace Drupal\Core\Cache;
use Drupal\Core\Database\Connection;
use Drupal\Core\Database\SchemaObjectExistsException;
/**
* Cache tags invalidations checksum implementation that uses the database.
*/
class DatabaseCacheTagsChecksum implements CacheTagsChecksumInterface, CacheTagsInvalidatorInterface {
/**
* The database connection.
*
* @var \Drupal\Core\Database\Connection
*/
protected $connection;
/**
* Contains already loaded cache invalidations from the database.
*
* @var array
*/
protected $tagCache = array();
/**
* A list of tags that have already been invalidated in this request.
*
* Used to prevent the invalidation of the same cache tag multiple times.
*
* @var array
*/
protected $invalidatedTags = array();
/**
* Constructs a DatabaseCacheTagsChecksum object.
*
* @param \Drupal\Core\Database\Connection $connection
* The database connection.
*/
public function __construct(Connection $connection) {
$this->connection = $connection;
}
/**
* {@inheritdoc}
*/
public function invalidateTags(array $tags) {
try {
foreach ($tags as $tag) {
// Only invalidate tags once per request unless they are written again.
if (isset($this->invalidatedTags[$tag])) {
continue;
}
$this->invalidatedTags[$tag] = TRUE;
unset($this->tagCache[$tag]);
$this->connection->merge('cachetags')