Commit b0964231 authored by catch's avatar catch

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().
*/
......
......@@ -36,14 +36,14 @@ class InstallBackend extends DatabaseBackend {
/**
* Overrides Drupal\Core\Cache\DatabaseBackend::get().
*/
public function get($cid) {
public function get($cid, $allow_invalid = FALSE) {
return FALSE;
}
/**
* Overrides Drupal\Core\Cache\DatabaseBackend::getMultiple().
*/
public function getMultiple(&$cids) {
public function getMultiple(&$cids, $allow_invalid = FALSE) {
return array();
}
......@@ -77,36 +77,84 @@ public function deleteMultiple(array $cids) {
}
/**
* Overrides Drupal\Core\Cache\DatabaseBackend::invalidateTags().
* Overrides Drupal\Core\Cache\DatabaseBackend::deleteAll().
*/
public function invalidateTags(array $tags) {
public function deleteAll() {
try {
if (class_exists('Drupal\Core\Database\Database')) {
parent::invalidateTags($tags);
parent::deleteAll();
}
}
catch (Exception $e) {}
}
/**
* Overrides Drupal\Core\Cache\DatabaseBackend::deleteExpired().
*/
public function deleteExpired() {
try {
if (class_exists('Drupal\Core\Database\Database')) {
parent::deleteExpired();
}
}
catch (Exception $e) {}
}
/**
* Overrides Drupal\Core\Cache\DatabaseBackend::deleteTags().
*/
public function deleteTags(array $tags) {
try {
if (class_exists('Drupal\Core\Database\Database')) {
parent::deleteTags($tags);
}
}
catch (Exception $e) {}
}
/**
* Overrides Drupal\Core\Cache\DatabaseBackend::flush().
* Overrides Drupal\Core\Cache\DatabaseBackend::invalidate().
*/
public function flush() {
public function invalidate($cid) {
try {
if (class_exists('Drupal\Core\Database\Database')) {
parent::flush();
parent::invalidate($cid);
}
}
catch (Exception $e) {}
}
/**
* Overrides Drupal\Core\Cache\DatabaseBackend::invalidateMultiple().
*/
public function invalidateMultiple(array $cids) {
try {
if (class_exists('Drupal\Core\Database\Database')) {
parent::invalidateMultiple($cids);
}
}
catch (Exception $e) {}
}
/**
* Overrides Drupal\Core\Cache\DatabaseBackend::invalidateTags().
*/
public function invalidateTags(array $tags) {
try {
if (class_exists('Drupal\Core\Database\Database')) {
parent::invalidateTags($tags);
}
}
catch (Exception $e) {}
}
/**
* Overrides Drupal\Core\Cache\DatabaseBackend::expire().
* Implements Drupal\Core\Cache\CacheBackendInterface::invalidateAll().
*/
public function expire() {
public function invalidateAll() {
try {
if (class_exists('Drupal\Core\Database\Database')) {
parent::expire();
parent::invalidateAll($tags);
}
}
catch (Exception $e) {}
......
......@@ -23,11 +23,6 @@ class MemoryBackend implements CacheBackendInterface {
*/
protected $cache = array();
/**
* All tags invalidated during the request.
*/
protected $invalidatedTags = array();
/**
* Constructs a MemoryBackend object.
*
......@@ -40,9 +35,9 @@ public function __construct($bin) {
/**
* Implements Drupal\Core\Cache\CacheBackendInterface::get().
*/
public function get($cid) {
public function get($cid, $allow_invalid = FALSE) {
if (isset($this->cache[$cid])) {
return $this->prepareItem($this->cache[$cid]);
return $this->prepareItem($this->cache[$cid], $allow_invalid);
}
else {
return FALSE;
......@@ -52,13 +47,13 @@ public function get($cid) {
/**
* Implements Drupal\Core\Cache\CacheBackendInterface::getMultiple().
*/
public function getMultiple(&$cids) {
public function getMultiple(&$cids, $allow_invalid = FALSE) {
$ret = array();
$items = array_intersect_key($this->cache, array_flip($cids));
foreach ($items as $item) {
$item = $this->prepareItem($item);
$item = $this->prepareItem($item, $allow_invalid);
if ($item) {
$ret[$item->cid] = $item;
}
......@@ -82,13 +77,15 @@ public function getMultiple(&$cids) {
* The item with data as appropriate or FALSE if there is no
* valid item to load.
*/
protected function prepareItem($cache) {
protected function prepareItem($cache, $allow_invalid) {
if (!isset($cache->data)) {
return FALSE;
}
// The cache data is invalid if any of its tags have been cleared since.
if (count($cache->tags) && $this->hasInvalidatedTags($cache)) {
// Check expire time.
$cache->valid = $cache->expire == CacheBackendInterface::CACHE_PERMANENT || $cache->expire >= REQUEST_TIME;
if (!$allow_invalid && !$cache->valid) {
return FALSE;
}
......@@ -102,28 +99,12 @@ public function set($cid, $data, $expire = CacheBackendInterface::CACHE_PERMANEN
$this->cache[$cid] = (object) array(
'cid' => $cid,
'data' => $data,
'created' => REQUEST_TIME,
'expire' => $expire,
'tags' => $tags,
'checksum' => $this->checksum($this->flattenTags($tags)),
'tags' => $this->flattenTags($tags),
);
}
/**
* Calculates a checksum so data can be invalidated using tags.
*/
public function checksum($tags) {
$checksum = '';
foreach ($tags as $tag) {
// Has the tag already been invalidated.
if (isset($this->invalidatedTags[$tag])) {
$checksum = $checksum . $tag . ':' . $this->invalidatedTags[$tag];
}
}
return $checksum;
}
/**
* Implements Drupal\Core\Cache\CacheBackendInterface::delete().
*/
......@@ -139,47 +120,79 @@ public function deleteMultiple(array $cids) {
}
/**
* Implements Drupal\Core\Cache\CacheBackendInterface::flush().
* Implements Drupal\Core\Cache\CacheBackendInterface::deleteTags().
*/
public function flush() {
public function deleteTags(array $tags) {
$flat_tags = $this->flattenTags($tags);
foreach ($this->cache as $cid => $item) {
if (array_intersect($flat_tags, $item->tags)) {
unset($this->cache[$cid]);
}
}
}
/**
* Implements Drupal\Core\Cache\CacheBackendInterface::deleteAll().
*/
public function deleteAll() {
$this->cache = array();
}
/**
* Implements Drupal\Core\Cache\CacheBackendInterface::expire().
* Implements Drupal\Core\Cache\CacheBackendInterface::deleteExpired().
*
* Cache expiration is not implemented for PHP ArrayBackend as this backend
* only persists during a single request and expiration are done using
* Cache expiration is not implemented for MemoryBackend as this backend only
* persists during a single request and expiration are done using
* REQUEST_TIME.
*/
public function expire() {
public function deleteExpired() {
}
/**
* Checks to see if any of the tags associated with a cache object have been
* invalidated.
*
* @param object @cache
* An cache object to calculate and compare it's original checksum for.
*
* @return boolean
* TRUE if the a tag has been invalidated, FALSE otherwise.
* Implements Drupal\Core\Cache\CacheBackendInterface::invalidate().
*/
public function invalidate($cid) {
$this->cache[$cid]->expire = REQUEST_TIME - 1;
}
/**
* Implements Drupal\Core\Cache\CacheBackendInterface::invalidateMultiple().
*/
protected function hasInvalidatedTags($cache) {
if ($cache->checksum != $this->checksum($this->flattenTags($cache->tags))) {
return TRUE;
public function invalidateMultiple(array $cids) {
foreach ($cids as $cid) {
$this->cache[$cid]->expire = REQUEST_TIME - 1;
}
return FALSE;
}
/**
* Flattens a tags array into a numeric array suitable for string storage.
* Implements Drupal\Core\Cache\CacheBackendInterface::invalidateTags().
*/
public function invalidateTags(array $tags) {
$flat_tags = $this->flattenTags($tags);
foreach ($this->cache as $cid => $item) {
if (array_intersect($flat_tags, $item->tags)) {
$this->cache[$cid]->expire = REQUEST_TIME - 1;
}
}
}
/**
* Implements Drupal\Core\Cache\CacheBackendInterface::invalidateAll().
*/
public function invalidateAll() {
foreach ($this->cache as $cid => $item) {
$this->cache[$cid]->expire = REQUEST_TIME - 1;
}
}
/**
* 'Flattens' a tags array into an array of strings.
*
* @param array $tags
* Associative array of tags to flatten.
*
* @return
* An array of flattened tag identifiers.
* @return array
* An indexed array of strings.
*/
protected function flattenTags(array $tags) {
if (isset($tags[0])) {
......@@ -200,20 +213,6 @@ protected function flattenTags(array $tags) {
return $flat_tags;
}
/**
* Implements Drupal\Core\Cache\CacheBackendInterface::invalidateTags().
*/
public function invalidateTags(array $tags) {
foreach ($this->flattenTags($tags) as $tag) {
if (isset($this->invalidatedTags[$tag])) {
$this->invalidatedTags[$tag] = $this->invalidatedTags[$tag] + 1;
}
else {
$this->invalidatedTags[$tag] = 1;
}
}
}
/**
* Implements Drupal\Core\Cache\CacheBackendInterface::isEmpty().
*/
......
......@@ -31,14 +31,14 @@ public function __construct($bin) {}
/**
* Implements Drupal\Core\Cache\CacheBackendInterface::get().
*/
public function get($cid) {
public function get($cid, $allow_invalid = FALSE) {
return FALSE;
}
/**
* Implements Drupal\Core\Cache\CacheBackendInterface::getMultiple().
*/
public function getMultiple(&$cids) {
public function getMultiple(&$cids, $allow_invalid = FALSE) {
return array();
}
......@@ -58,25 +58,45 @@ public function delete($cid) {}
public function deleteMultiple(array $cids) {}
/**
* Implements Drupal\Core\Cache\CacheBackendInterface::flush().
* Implements Drupal\Core\Cache\CacheBackendInterface::deleteAll().
*/
public function flush() {}
public function deleteAll() {}
/**
* Implements Drupal\Core\Cache\CacheBackendInterface::expire().
* Implements Drupal\Core\Cache\CacheBackendInterface::deleteExpired().
*/
public function expire() {}
public function deleteExpired() {}
/**
* Implements Drupal\Core\Cache\CacheBackendInterface::garbageCollection().
* Implements Drupal\Core\Cache\CacheBackendInterface::deleteTags().
*/
public function garbageCollection() {}
public function deleteTags(array $tags) {}
/**
* Implements Drupal\Core\Cache\CacheBackendInterface::invalidate().
*/
public function invalidate($cid) {}
/**
* Implements Drupal\Core\Cache\CacheBackendInterface::invalidateMultiple().
*/
public function invalidateMultiple(array $cids) {}
/**
* Implements Drupal\Core\Cache\CacheBackendInterface::invalidateTags().
*/
public function invalidateTags(array $tags) {}
/**
* Implements Drupal\Core\Cache\CacheBackendInterface::invalidateAll().
*/
public function invalidateAll() {}
/**
* Implements Drupal\Core\Cache\CacheBackendInterface::garbageCollection().
*/
public function garbageCollection() {}
/**
* Implements Drupal\Core\Cache\CacheBackendInterface::isEmpty().
*/
......
......@@ -45,7 +45,7 @@ function aggregator_aggregator_parse($feed) {
$feed->modified = $modified;
// Clear the page and block caches.
cache_invalidate(array('content' => TRUE));
cache_invalidate_tags(array('content' => TRUE));
return TRUE;
}
......
......@@ -207,7 +207,7 @@ function block_admin_display_form_submit($form, &$form_state) {
throw $e;
}
drupal_set_message(t('The block settings have been updated.'));
cache_invalidate(array('content' => TRUE));
cache_invalidate_tags(array('content' => TRUE));
}
/**
......@@ -606,7 +606,7 @@ function block_admin_configure_submit($form, &$form_state) {
throw $e;
}
drupal_set_message(t('The block configuration has been saved.'));
cache_invalidate(array('content' => TRUE));
cache_invalidate_tags(array('content' => TRUE));
$form_state['redirect'] = 'admin/structure/block';
}
}
......@@ -715,7 +715,7 @@ function block_add_block_form_submit($form, &$form_state) {
}
drupal_set_message(t('The block has been created.'));
cache_invalidate(array('content' => TRUE));
cache_invalidate_tags(array('content' => TRUE));
$form_state['redirect'] = 'admin/structure/block';
}
......@@ -763,7 +763,7 @@ function block_custom_block_delete_submit($form, &$form_state) {
->execute();
drupal_set_message(t('The block %name has been removed.', array('%name' => $form_state['values']['info'])));
cache_invalidate(array('content' => TRUE));
cache_invalidate_tags(array('content' => TRUE));
$form_state['redirect'] = 'admin/structure/block';
return;
}
......
......@@ -74,7 +74,7 @@ function testCachePerRole() {
$this->assertText($old_content, 'Block is served from the cache.');
// Clear the cache and verify that the stale data is no longer there.
cache_invalidate(array('content' => TRUE));
cache_invalidate_tags(array('content' => TRUE));
$this->drupalGet('');
$this->assertNoText($old_content, 'Block cache clear removes stale cache data.');
$this->assertText($current_content, 'Fresh block content is displayed after clearing the cache.');
......
......@@ -204,7 +204,7 @@ function comment_admin_overview_submit($form, &$form_state) {
}
drupal_set_message(t('The update has been performed.'));
$form_state['redirect'] = 'admin/content/comment';
cache_invalidate(array('content' => TRUE));
cache_invalidate_tags(array('content' => TRUE));