Commit b0964231 authored by catch's avatar catch
Browse files

Issue #1774332 by c960657: Better handling of invalid/expired cache entries.

parent a49654af
......@@ -44,19 +44,38 @@ function cache($bin = 'cache') {
}
/**
* Invalidates the items associated with given list of 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 array $tags
* The list of tags to delete cache items for.
*/
function cache_delete_tags(array $tags) {
foreach (CacheFactory::getBackends() as $bin => $class) {
cache($bin)->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 invalidate on each.
* and call invalidateTags() on each.
*
* @param array $tags
* The list of tags to invalidate cache items for.
*/
function cache_invalidate(array $tags) {
function cache_invalidate_tags(array $tags) {
foreach (CacheFactory::getBackends() as $bin => $class) {
cache($bin)->invalidateTags($tags);
}
......
......@@ -6623,7 +6623,7 @@ function drupal_flush_all_caches() {
// This is executed based on old/previously known information, which is
// sufficient, since new extensions cannot have any primed caches yet.
foreach (module_invoke_all('cache_flush') as $bin) {
cache($bin)->flush();
cache($bin)->deleteAll();
}
// Flush asset file caches.
......
......@@ -50,7 +50,7 @@ function entity_get_info($entity_type = NULL) {
function entity_info_cache_clear() {
drupal_static_reset('entity_get_info');
// Clear all languages.
cache()->invalidateTags(array('entity_info' => TRUE));
cache()->deleteTags(array('entity_info' => TRUE));
}
/**
......
......@@ -2617,7 +2617,7 @@ function menu_link_load($mlid) {
* Clears the cached cached data for a single named menu.
*/
function menu_cache_clear($menu_name = 'tools') {
cache('menu')->invalidateTags(array('menu' => $menu_name));
cache('menu')->deleteTags(array('menu' => $menu_name));
// Also clear the menu system static caches.
menu_reset_static_cache();
}
......@@ -2629,7 +2629,7 @@ function menu_cache_clear($menu_name = 'tools') {
* might have been made to the router items or menu links.
*/
function menu_cache_clear_all() {
cache('menu')->flush();
cache('menu')->deleteAll();
menu_reset_static_cache();
}
......@@ -3258,13 +3258,13 @@ function _menu_clear_page_cache() {
// Clear the page and block caches, but at most twice, including at
// the end of the page load when there are multiple links saved or deleted.
if ($cache_cleared == 0) {
cache_invalidate(array('content' => TRUE));
cache_invalidate_tags(array('content' => TRUE));
// Keep track of which menus have expanded items.
_menu_set_expanded_menus();
$cache_cleared = 1;
}
elseif ($cache_cleared == 1) {
drupal_register_shutdown_function('cache_invalidate', array('content', TRUE));
drupal_register_shutdown_function('cache_invalidate_tags', array('content', TRUE));
// Keep track of which menus have expanded items.
drupal_register_shutdown_function('_menu_set_expanded_menus');
$cache_cleared = 2;
......
......@@ -103,7 +103,7 @@ function drupal_get_complete_schema($rebuild = FALSE) {
drupal_alter('schema', $schema);
if ($rebuild) {
cache()->invalidateTags(array('schema' => TRUE));
cache()->deleteTags(array('schema' => TRUE));
}
// If the schema is empty, avoid saving it: some database engines require
// the schema to perform queries, and this could lead to infinite loops.
......
......@@ -228,7 +228,7 @@ function update_prepare_d8_bootstrap() {
// Make sure that the bootstrap cache is cleared as that might contain
// incompatible data structures.
cache('bootstrap')->flush();
cache('bootstrap')->deleteAll();
// Retrieve all installed extensions from the {system} table.
// Uninstalled extensions are ignored and not converted.
......
......@@ -30,6 +30,15 @@ class BackendChain implements CacheBackendInterface {
*/
protected $backends = array();
/**
* Constructs a DatabaseBackend object.
*
* @param string $bin
* The cache bin for which the object is created.
*/
public function __construct($bin) {
}
/**
* Appends a cache backend to the cache chain.
*
......@@ -60,18 +69,12 @@ public function prependBackend(CacheBackendInterface $backend) {
return $this;
}
/**
* Implements Drupal\Core\Cache\CacheBackendInterface::__construct().
*/
public function __construct($bin) {
}
/**
* Implements Drupal\Core\Cache\CacheBackendInterface::get().
*/
public function get($cid) {
public function get($cid, $allow_invalid = FALSE) {
foreach ($this->backends as $index => $backend) {
if (($return = $backend->get($cid)) !== FALSE) {
if (($return = $backend->get($cid, $allow_invalid)) !== FALSE) {
// We found a result, propagate it to all missed backends.
if ($index > 0) {
for ($i = ($index - 1); 0 <= $i; --$i) {
......@@ -89,11 +92,11 @@ public function get($cid) {
/**
* Implements Drupal\Core\Cache\CacheBackendInterface::getMultiple().
*/
function getMultiple(&$cids) {
public function getMultiple(&$cids, $allow_invalid = FALSE) {
$return = array();
foreach ($this->backends as $index => $backend) {
$items = $backend->getMultiple($cids);
$items = $backend->getMultiple($cids, $allow_invalid);
// Propagate the values that could be retrieved from the current cache
// backend to all missed backends.
......@@ -120,7 +123,7 @@ function getMultiple(&$cids) {
/**
* Implements Drupal\Core\Cache\CacheBackendInterface::set().
*/
function set($cid, $data, $expire = CacheBackendInterface::CACHE_PERMANENT, array $tags = array()) {
public function set($cid, $data, $expire = CacheBackendInterface::CACHE_PERMANENT, array $tags = array()) {
foreach ($this->backends as $backend) {
$backend->set($cid, $data, $expire, $tags);
}
......@@ -129,7 +132,7 @@ function set($cid, $data, $expire = CacheBackendInterface::CACHE_PERMANENT, arra
/**
* Implements Drupal\Core\Cache\CacheBackendInterface::delete().
*/
function delete($cid) {
public function delete($cid) {
foreach ($this->backends as $backend) {
$backend->delete($cid);
}
......@@ -138,27 +141,54 @@ function delete($cid) {
/**
* Implements Drupal\Core\Cache\CacheBackendInterface::deleteMultiple().
*/
function deleteMultiple(array $cids) {
public function deleteMultiple(array $cids) {
foreach ($this->backends as $backend) {
$backend->deleteMultiple($cids);
}
}
/**
* Implements Drupal\Core\Cache\CacheBackendInterface::flush().
* Implements Drupal\Core\Cache\CacheBackendInterface::deleteTags().
*/
public function flush() {
public function deleteTags(array $tags) {
foreach ($this->backends as $backend) {
$backend->flush();
$backend->deleteTags($tags);
}
}
/**
* Implements Drupal\Core\Cache\CacheBackendInterface::deleteAll().
*/
public function deleteAll() {
foreach ($this->backends as $backend) {
$backend->deleteAll();
}
}
/**
* Implements Drupal\Core\Cache\CacheBackendInterface::expire().
*/
public function expire() {
public function deleteExpired() {
foreach ($this->backends as $backend) {
$backend->expire();
$backend->deleteExpired();
}
}
/**
* Implements Drupal\Core\Cache\CacheBackendInterface::invalidate().
*/
public function invalidate($cid) {
foreach ($this->backends as $backend) {
$backend->invalidate($cid);
}
}
/**
* Implements Drupal\Core\Cache\CacheBackendInterface::invalidateMultiple().
*/
public function invalidateMultiple(array $cids) {
foreach ($this->backends as $backend) {
$backend->invalidateMultiple($cids);
}
}
......@@ -171,6 +201,15 @@ public function invalidateTags(array $tags) {
}
}
/**
* Implements Drupal\Core\Cache\CacheBackendInterface::invalidateAll().
*/
public function invalidateAll() {
foreach ($this->backends as $backend) {
$backend->invalidateAll();
}
}
/**
* Implements Drupal\Core\Cache\CacheBackendInterface::garbageCollection().
*/
......
......@@ -42,108 +42,259 @@
* cache('custom_bin')->get($cid);
* @endcode
*
* There are two ways to "remove" a cache item:
* - Deletion (using delete(), deleteMultiple(), deleteTags(), deleteAll() or
* deleteExpired()): Permanently removes the item from the cache.
* - Invalidation (using invalidate(), invalidateMultiple(), invalidateTags()
* or invalidateAll()): a "soft" delete that only marks the items as
* "invalid", meaning "not fresh" or "not fresh enough". Invalid items are
* not usually returned from the cache, so in most ways they behave as if they
* have been deleted. However, it is possible to retrieve the invalid entries,
* if they have not yet been permanently removed by the garbage collector, by
* passing TRUE as the second argument for get($cid, $allow_invalid).
*
* Cache items should be deleted if they are no longer considered useful. This
* is relevant e.g. if the cache item contains references to data that has been
* deleted. On the other hand, it may be relevant to just invalidate the item
* if the cached data may be useful to some callers until the cache item has
* been updated with fresh data. The fact that it was fresh a short while ago
* may often be sufficient.
*
* Invalidation is particularly useful to protect against stampedes. Rather than
* having multiple concurrent requests updating the same cache item when it
* expires or is deleted, there can be one request updating the cache, while
* the other requests can proceed using the stale value. As soon as the cache
* item has been updated, all future requests will use the updated value.
*
* @see cache()
* @see Drupal\Core\Cache\DatabaseBackend
*/
interface CacheBackendInterface {
/**
* Indicates that the item should never be removed unless explicitly selected.
*
* The item may be removed using cache()->delete() with a cache ID.
* Indicates that the item should never be removed unless explicitly deleted.
*/
const CACHE_PERMANENT = 0;
/**
* Returns data from the persistent cache.
*
* Data may be stored as either plain text or as serialized data. cache_get()
* will automatically return unserialized objects and arrays.
*
* @param $cid
* @param string $cid
* The cache ID of the data to retrieve.
* @param bool $allow_invalid
* (optional) If TRUE, a cache item may be returned even if it is expired or
* has been invalidated. Such items may sometimes be preferred, if the
* alternative is recalculating the value stored in the cache, especially
* if another concurrent request is already recalculating the same value.
* The "valid" property of the returned object indicates whether the item is
* valid or not. Defaults to FALSE.
*
* @return
* The cache or FALSE on failure.
* @return object|false
* The cache item or FALSE on failure.
*
* @see Drupal\Core\Cache\CacheBackendInterface::getMultiple()
*/
public function get($cid);
public function get($cid, $allow_invalid = FALSE);
/**
* Returns data from the persistent cache when given an array of cache IDs.
*
* @param $cids
* @param array $cids
* An array of cache IDs for the data to retrieve. This is passed by
* reference, and will have the IDs successfully returned from cache
* removed.
* @param bool $allow_invalid
* (optional) If TRUE, cache items may be returned even if they have expired
* or been invalidated. Such items may sometimes be preferred, if the
* alternative is recalculating the value stored in the cache, especially
* if another concurrent thread is already recalculating the same value. The
* "valid" property of the returned objects indicates whether the items are
* valid or not. Defaults to FALSE.
*
* @return
* An array of the items successfully returned from cache indexed by cid.
* @return array
* An array of cache item objects indexed by cache ID.
*
* @see Drupal\Core\Cache\CacheBackendInterface::get()
*/
public function getMultiple(&$cids);
public function getMultiple(&$cids, $allow_invalid = FALSE);
/**
* Stores data in the persistent cache.
*
* @param $cid
* @param string $cid
* The cache ID of the data to store.
* @param $data
* The data to store in the cache. Complex data types will be automatically
* serialized before insertion.
* Strings will be stored as plain text and not serialized.
* @param $expire
* @param mixed $data
* The data to store in the cache.
* @param int $expire
* One of the following values:
* - CacheBackendInterface::CACHE_PERMANENT: Indicates that the item
* should never be removed unless cache->delete($cid) is used explicitly.
* - A Unix timestamp: Indicates that the item should be kept at least until
* the given time.
* - CacheBackendInterface::CACHE_PERMANENT: Indicates that the item should
* not be removed unless it is deleted explicitly.
* - A Unix timestamp: Indicates that the item will be considered invalid
* after this time, i.e. it will not be returned by get() unless
* $allow_invalid has been set to TRUE. When the item has expired, it may
* be permanently deleted by the garbage collector at any time.
* @param array $tags
* An array of tags to be stored with the cache item. These should normally
* identify objects used to build the cache item, which should trigger
* cache invalidation when updated. For example if a cached item represents
* a node, both the node ID and the author's user ID might be passed in as
* tags. For example array('node' => array(123), 'user' => array(92)).
*
* @see Drupal\Core\Cache\CacheBackendInterface::get()
* @see Drupal\Core\Cache\CacheBackendInterface::getMultiple()
*/
public function set($cid, $data, $expire = CacheBackendInterface::CACHE_PERMANENT, array $tags = array());
/**
* Deletes an item from the cache.
*
* @param $cid
* The cache ID to delete.
* If the cache item is being deleted because it is no longer "fresh", you may
* consider using invalidate() instead. This allows callers to retrieve the
* invalid item 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 string $cid
* The cache ID to delete.
*
* @see Drupal\Core\Cache\CacheBackendInterface::invalidate()
* @see Drupal\Core\Cache\CacheBackendInterface::deleteMultiple()
* @see Drupal\Core\Cache\CacheBackendInterface::deleteTags()
* @see Drupal\Core\Cache\CacheBackendInterface::deleteAll()
* @see Drupal\Core\Cache\CacheBackendInterface::deleteExpired()
*/
public function delete($cid);
/**
* Deletes multiple items from the cache.
*
* @param $cids
* An array of $cids to delete.
* If the cache items are being deleted because they are no longer "fresh",
* you may consider using invalidateMultiple() 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 $cids
* An array of cache IDs to delete.
*
* @see Drupal\Core\Cache\CacheBackendInterface::invalidateMultiple()
* @see Drupal\Core\Cache\CacheBackendInterface::delete()
* @see Drupal\Core\Cache\CacheBackendInterface::deleteTags()
* @see Drupal\Core\Cache\CacheBackendInterface::deleteAll()
* @see Drupal\Core\Cache\CacheBackendInterface::deleteExpired()
*/
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()
* @see Drupal\Core\Cache\CacheBackendInterface::deleteExpired()
*/
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()
* @see Drupal\Core\Cache\CacheBackendInterface::deleteExpired()
*/
public function deleteMultiple(Array $cids);
public function deleteAll();
/**
* Flushes all cache items in a bin.
* Deletes expired items from the cache.
*
* @see Drupal\Core\Cache\CacheBackendInterface::delete()
* @see Drupal\Core\Cache\CacheBackendInterface::deleteMultiple()
* @see Drupal\Core\Cache\CacheBackendInterface::deleteTags()
* @see Drupal\Core\Cache\CacheBackendInterface::deleteAll()
* @see Drupal\Core\Cache\CacheBackendInterface::deleteExpired()
*/
public function deleteExpired();
/**
* Marks a cache item as invalid.
*
* Invalid items may be returned in later calls to get(), if the $allow_invalid
* argument is TRUE.
*
* @param string $cid
* The cache ID to invalidate.
*
* @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 flush();
public function invalidate($cid);
/**
* Expires temporary items from the cache.
* Marks 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::deleteMultiple()
* @see Drupal\Core\Cache\CacheBackendInterface::invalidate()
* @see Drupal\Core\Cache\CacheBackendInterface::invalidateTags()
* @see Drupal\Core\Cache\CacheBackendInterface::invalidateAll()
*/
public function expire();
public function invalidateMultiple(array $cids);
/**
* Invalidates each tag in the $tags array.
* 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 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();
/**
* Performs garbage collection on a cache bin.
*
* The backend may choose to delete expired or invalidated items.
*/
public function garbageCollection();
......
......@@ -9,6 +9,7 @@
use Drupal\Core\Database\Database;
use Exception;
use PDO;
/**
* Defines a default cache implementation.
......@@ -23,11 +24,6 @@ class DatabaseBackend implements CacheBackendInterface {
*/
protected $bin;
/**
* A static cache of all tags checked during the request.
*/
protected static $tagCache = array();
/**
* Constructs a DatabaseBackend object.
*
......@@ -46,16 +42,16 @@ public function __construct($bin) {
/**
* Implements Drupal\Core\Cache\CacheBackendInterface::get().
*/
public function get($cid) {
public function get($cid, $allow_invalid = FALSE) {
$cids = array($cid);
$cache = $this->getMultiple($cids);
$cache = $this->getMultiple($cids, $allow_invalid);
return reset($cache);
}
/**
* Implements Drupal\Core\Cache\CacheBackendInterface::getMultiple().
*/
public function getMultiple(&$cids) {
public function getMultiple(&$cids, $allow_invalid = FALSE) {
try {
// When serving cached pages, the overhead of using ::select() was found
// to add around 30% overhead to the request. Since $this->bin is a
......@@ -64,10 +60,10 @@ public function getMultiple(&$cids) {
// is used here only due to the performance overhead we would incur
// otherwise. When serving an uncached page, the overhead of using
// ::select() is a much smaller proportion of the request.
$result = Database::getConnection()->query('SELECT cid, data, created, expire, serialized, tags, checksum FROM {' . Database::getConnection()->escapeTable($this->bin) . '} WHERE cid IN (:cids)', array(':cids' => $cids));
$result = Database::getConnection()->query('SELECT cid, data, created, expire, serialized, tags, checksum_invalidations, checksum_deletions FROM {' . Database::getConnection()->escapeTable($this->bin) . '} WHERE cid IN (:cids)', array(':cids' => $cids));
$cache = array();
foreach ($result as $item) {
$item = $this->prepareItem($item);
$item = $this->prepareItem($item, $allow_invalid);
if ($item) {
$cache[$item->cid] = $item;
}
......@@ -90,22 +86,37 @@ public function getMultiple(&$cids) {
*
* @param stdClass $cache
* An item loaded from cache_get() or cache_get_multiple().
* @param bool $allow_invalid
* If FALSE, the method returns FALSE if the cache item is not valid.
*
* @return mixed
* The item with data unserialized as appropriate or FALSE if there is no
* valid item to load.
* @return mixed|false
* The item with data unserialized as appropriate and a property indicating