diff --git a/core/lib/Drupal/Core/Cache/CacheTagInvalidationStorageInterface.php b/core/lib/Drupal/Core/Cache/CacheTagInvalidationStorageInterface.php index d039f00..a200d44 100644 --- a/core/lib/Drupal/Core/Cache/CacheTagInvalidationStorageInterface.php +++ b/core/lib/Drupal/Core/Cache/CacheTagInvalidationStorageInterface.php @@ -13,16 +13,26 @@ interface CacheTagInvalidationStorageInterface { /** - * Returns cache tag invalidations. + * Returns the sum total of validations for a given set of tags. * - * @return array + * @param array $tags + * Array of cache tags. + * + * @return int[] + * Array with cache tag invalidations and deletions for the set of passed in + * cache tags. + */ + public function checksumTags(array $tags); + + /** + * Notifies the cache tag invalidation storage about cache tags being written. */ - public function getTagInvalidations(); + public function onCacheTagsWrite(array $tags); /** - * Returns cache tag deletions. + * Reset statically cached tags informatoin. * - * @return array + * This is only used by tests. */ - public function getTagDeletions(); + public function reset(); } diff --git a/core/lib/Drupal/Core/Cache/DatabaseBackend.php b/core/lib/Drupal/Core/Cache/DatabaseBackend.php index 0ef37d7..56ebbb6 100644 --- a/core/lib/Drupal/Core/Cache/DatabaseBackend.php +++ b/core/lib/Drupal/Core/Cache/DatabaseBackend.php @@ -126,7 +126,7 @@ protected function prepareItem($cache, $allow_invalid) { $cache->tags = $cache->tags ? explode(' ', $cache->tags) : array(); - $checksum = $this->checksumTags($cache->tags); + $checksum = $this->cacheTagStorage->checksumTags($cache->tags); // Check if deleteTags() has been called with any of the entry's tags. if ($cache->checksum_deletions != $checksum['deletions']) { @@ -157,10 +157,13 @@ protected function prepareItem($cache, $allow_invalid) { * Implements Drupal\Core\Cache\CacheBackendInterface::set(). */ public function set($cid, $data, $expire = Cache::PERMANENT, array $tags = array()) { - Cache::validateTags($tags); - $tags = array_unique($tags); - // Sort the cache tags so that they are stored consistently in the database. - sort($tags); + if ($tags) { + Cache::validateTags($tags); + $tags = array_unique($tags); + // Sort the cache tags so that they are stored consistently in the database. + sort($tags); + $this->cacheTagStorage->onCacheTagsWrite($tags); + } $try_again = FALSE; try { // The bin might not yet exist. @@ -184,20 +187,7 @@ public function set($cid, $data, $expire = Cache::PERMANENT, array $tags = array * Actually set the cache. */ protected function doSet($cid, $data, $expire, $tags) { - $deleted_tags = &drupal_static('Drupal\Core\Cache\DatabaseCacheTagStorage::deletedTags', array()); - $invalidated_tags = &drupal_static('Drupal\Core\Cache\DatabaseCacheTagStorage::invalidatedTags', array()); - // Remove tags that were already deleted or invalidated during this request - // from the static caches so that another deletion or invalidation can - // occur. - foreach ($tags as $tag) { - if (isset($deleted_tags[$tag])) { - unset($deleted_tags[$tag]); - } - if (isset($invalidated_tags[$tag])) { - unset($invalidated_tags[$tag]); - } - } - $checksum = $this->checksumTags($tags); + $checksum = $this->cacheTagStorage->checksumTags($tags); $fields = array( 'serialized' => 0, 'created' => round(microtime(TRUE), 3), @@ -225,9 +215,6 @@ protected function doSet($cid, $data, $expire, $tags) { * {@inheritdoc} */ public function setMultiple(array $items) { - $deleted_tags = &drupal_static('Drupal\Core\Cache\DatabaseCacheTagStorage::deletedTags', array()); - $invalidated_tags = &drupal_static('Drupal\Core\Cache\DatabaseCacheTagStorage::invalidatedTags', array()); - // Use a transaction so that the database can write the changes in a single // commit. $transaction = $this->connection->startTransaction(); @@ -247,24 +234,14 @@ public function setMultiple(array $items) { 'tags' => array(), ); - Cache::validateTags($item['tags']); - $item['tags'] = array_unique($item['tags']); - // Sort the cache tags so that they are stored consistently in the DB. - sort($item['tags']); - - // Remove tags that were already deleted or invalidated during this - // request from the static caches so that another deletion or - // invalidation can occur. - foreach ($item['tags'] as $tag) { - if (isset($deleted_tags[$tag])) { - unset($deleted_tags[$tag]); - } - if (isset($invalidated_tags[$tag])) { - unset($invalidated_tags[$tag]); - } + if ($item['tags']) { + Cache::validateTags($item['tags']); + $item['tags'] = array_unique($item['tags']); + // Sort the cache tags so that they are stored consistently in the DB. + sort($item['tags']); + $this->cacheTagStorage->onCacheTagsWrite($item['tags']); } - - $checksum = $this->checksumTags($item['tags']); + $checksum = $this->cacheTagStorage->checksumTags($item['tags']); $fields = array( 'cid' => $cid, @@ -411,40 +388,6 @@ public function garbageCollection() { } /** - * Returns the sum total of validations for a given set of tags. - * - * @param array $tags - * Array of cache tags. - * - * @return int - * Sum of all invalidations. - */ - protected function checksumTags(array $tags) { - $tag_cache = &drupal_static('Drupal\Core\Cache\DatabaseCacheTagStorage::tagCache', array()); - - $checksum = array( - 'invalidations' => 0, - 'deletions' => 0, - ); - - $query_tags = array_diff($tags, array_keys($tag_cache)); - if ($query_tags) { - $db_tags = $this->connection->query('SELECT tag, invalidations, deletions FROM {cachetags} WHERE tag IN (:tags)', array(':tags' => $query_tags))->fetchAllAssoc('tag', \PDO::FETCH_ASSOC); - $tag_cache += $db_tags; - - // Fill static cache with empty objects for tags not found in the database. - $tag_cache += array_fill_keys(array_diff($query_tags, array_keys($db_tags)), $checksum); - } - - foreach ($tags as $tag) { - $checksum['invalidations'] += $tag_cache[$tag]['invalidations']; - $checksum['deletions'] += $tag_cache[$tag]['deletions']; - } - - return $checksum; - } - - /** * {@inheritdoc} */ public function removeBin() { @@ -464,11 +407,7 @@ protected function ensureBinExists() { $database_schema = $this->connection->schema(); if (!$database_schema->tableExists($this->bin)) { $schema_definition = $this->schemaDefinition(); - $database_schema->createTable($this->bin, $schema_definition['bin']); - // If the bin doesn't exist, the cache tags table may also not exist. - if (!$database_schema->tableExists('cachetags')) { - $database_schema->createTable('cachetags', $schema_definition['cachetags']); - } + $database_schema->createTable($this->bin, $schema_definition); return TRUE; } } @@ -522,10 +461,10 @@ protected function normalizeCid($cid) { } /** - * Defines the schema for the {cache_*} bin and {cachetags} tables. + * Defines the schema for the {cache_*} bin table. */ public function schemaDefinition() { - $schema['bin'] = array( + $schema = array( 'description' => 'Storage for the cache API.', 'fields' => array( 'cid' => array( @@ -587,31 +526,6 @@ public function schemaDefinition() { ), 'primary key' => array('cid'), ); - $schema['cachetags'] = array( - 'description' => 'Cache table for tracking cache tags related to the cache bin.', - 'fields' => array( - 'tag' => array( - 'description' => 'Namespace-prefixed tag string.', - 'type' => 'varchar', - 'length' => 255, - 'not null' => TRUE, - 'default' => '', - ), - 'invalidations' => array( - 'description' => 'Number incremented when the tag is invalidated.', - 'type' => 'int', - 'not null' => TRUE, - 'default' => 0, - ), - 'deletions' => array( - 'description' => 'Number incremented when the tag is deleted.', - 'type' => 'int', - 'not null' => TRUE, - 'default' => 0, - ), - ), - 'primary key' => array('tag'), - ); return $schema; } } diff --git a/core/lib/Drupal/Core/Cache/DatabaseCacheTagStorage.php b/core/lib/Drupal/Core/Cache/DatabaseCacheTagStorage.php index 0a9259c..150d073 100644 --- a/core/lib/Drupal/Core/Cache/DatabaseCacheTagStorage.php +++ b/core/lib/Drupal/Core/Cache/DatabaseCacheTagStorage.php @@ -8,6 +8,7 @@ namespace Drupal\Core\Cache; use \Drupal\Core\Database\Connection; +use Drupal\Core\Database\SchemaObjectExistsException; /** * Storage for cache tag invalidations and deletions. @@ -22,6 +23,31 @@ class DatabaseCacheTagStorage implements CacheTagInvalidationStorageInterface, C protected $connection; /** + * Contains already loaded tags from the database. + * + * @var array + */ + protected $tagCache = array(); + + /** + * A list of tags that have already been deleted in this requested. + * + * Used to prevent the deletion of the same cache tag multiple times. + * + * @var array + */ + protected $deletedTags = array(); + + /** + * A list of tags that have already been invalidated in this requested. + * + * Used to prevent the invalidation of the same cache tag multiple times. + * + * @var array + */ + protected $invalidatedTags = array(); + + /** * Constructs a DatabaseBackend object. * * @param \Drupal\Core\Database\Connection $connection @@ -32,22 +58,17 @@ public function __construct(Connection $connection) { } /** - * Stores cache tag invalidations. - * - * @param string[] $tags - * The list of tags for which to store invalidations. + * {@inheritdoc} */ public function invalidateTags(array $tags) { try { - $tag_cache = &drupal_static('Drupal\Core\Cache\DatabaseCacheTagStorage::tagCache', array()); - $invalidated_tags = &drupal_static('Drupal\Core\Cache\DatabaseCacheTagStorage::invalidatedTags', array()); foreach ($tags as $tag) { // Only invalidate tags once per request unless they are written again. - if (isset($invalidated_tags[$tag])) { + if (isset($this->invalidatedTags[$tag])) { continue; } - $invalidated_tags[$tag] = TRUE; - unset($tag_cache[$tag]); + $this->invalidatedTags[$tag] = TRUE; + unset($this->tagCache[$tag]); $this->connection->merge('cachetags') ->insertFields(array('invalidations' => 1)) ->expression('invalidations', 'invalidations + 1') @@ -56,26 +77,26 @@ public function invalidateTags(array $tags) { } } catch (\Exception $e) { - $this->catchException($e, 'cachetags'); + // Create the cache table, which will be empty. This fixes cases during + // core install where a cache table is cleared before it is set + // with {cache_render} and {cache_data}. + if (!$this->ensureTableExists()) { + $this->catchException($e); + } } } /** - * Stores cache tag deletions. - * - * @param string[] $tags - * The list of tags for which to store deletions. + * {@inheritdoc} */ public function deleteTags(array $tags) { - $tag_cache = &drupal_static('Drupal\Core\Cache\DatabaseCacheTagStorage::tagCache', array()); - $deleted_tags = &drupal_static('Drupal\Core\Cache\DatabaseCacheTagStorage::deletedTags', array()); foreach ($tags as $tag) { // Only delete tags once per request unless they are written again. - if (isset($deleted_tags[$tag])) { + if (isset($this->deletedTags[$tag])) { continue; } - $deleted_tags[$tag] = TRUE; - unset($tag_cache[$tag]); + $this->deletedTags[$tag] = TRUE; + unset($this->tagCache[$tag]); try { $this->connection->merge('cachetags') ->insertFields(array('deletions' => 1)) @@ -84,7 +105,12 @@ public function deleteTags(array $tags) { ->execute(); } catch (\Exception $e) { - $this->catchException($e); + // Create the cache table, which will be empty. This fixes cases during + // core install where a cache table is cleared before it is set + // with {cache_render} and {cache_data}. + if (!$this->ensureTableExists()) { + $this->catchException($e); + } } } } @@ -92,15 +118,113 @@ public function deleteTags(array $tags) { /** * {@inheritdoc} */ - public function getTagInvalidations() { + public function checksumTags(array $tags) { + $checksum = array( + 'invalidations' => 0, + 'deletions' => 0, + ); + $query_tags = array_diff($tags, array_keys($this->tagCache)); + if ($query_tags) { + $db_tags = array(); + try { + $db_tags = $this->connection->query('SELECT tag, invalidations, deletions FROM {cachetags} WHERE tag IN (:tags)', array(':tags' => $query_tags)) + ->fetchAllAssoc('tag', \PDO::FETCH_ASSOC); + $this->tagCache += $db_tags; + } + catch (\Exception $e) { + // If the table does not exist yet, create. + if (!$this->ensureTableExists()) { + $this->catchException($e); + } + } + // Fill static cache with empty objects for tags not found in the database. + $this->tagCache += array_fill_keys(array_diff($query_tags, array_keys($db_tags)), $checksum); + } + + foreach ($tags as $tag) { + $checksum['invalidations'] += $this->tagCache[$tag]['invalidations']; + $checksum['deletions'] += $this->tagCache[$tag]['deletions']; + } + + return $checksum; } /** * {@inheritdoc} */ - public function getTagDeletions() { + public function onCacheTagsWrite(array $tags) { + // Remove tags that were already deleted or invalidated during this request + // from the static caches so that another deletion or invalidation can + // occur. + foreach ($tags as $tag) { + unset($this->deletedTags[$tag]); + unset($this->invalidatedTags[$tag]); + } + } + /** + * {@inheritdoc} + */ + public function reset() { + $this->tagCache = array(); + $this->deletedTags = array(); + $this->invalidatedTags = array(); + } + + /** + * Check if the cache tags table exists and create it if not. + */ + protected function ensureTableExists() { + try { + $database_schema = $this->connection->schema(); + // Create the cache tags table if it does not exist. + if (!$database_schema->tableExists('cachetags')) { + $schema_definition = $this->schemaDefinition(); + $database_schema->createTable('cachetags', $schema_definition); + + return TRUE; + } + // If another process has already created the cache tags table, attempting to + // recreate it will throw an exception. In this case just catch the + // exception and do nothing. + } + catch (SchemaObjectExistsException $e) { + return TRUE; + } + return FALSE; + } + + /** + * Defines the schema for the {cachetags} table. + */ + public function schemaDefinition() { + $schema = array( + 'description' => 'Cache table for tracking cache tags related to the cache bin.', + 'fields' => array( + 'tag' => array( + 'description' => 'Namespace-prefixed tag string.', + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + ), + 'invalidations' => array( + 'description' => 'Number incremented when the tag is invalidated.', + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, + ), + 'deletions' => array( + 'description' => 'Number incremented when the tag is deleted.', + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, + ), + ), + 'primary key' => array('tag'), + ); + return $schema; } /** diff --git a/core/modules/locale/src/Tests/LocaleTranslationUiTest.php b/core/modules/locale/src/Tests/LocaleTranslationUiTest.php index 1cb8de7..740bb0d 100644 --- a/core/modules/locale/src/Tests/LocaleTranslationUiTest.php +++ b/core/modules/locale/src/Tests/LocaleTranslationUiTest.php @@ -137,7 +137,7 @@ public function testStringTranslation() { // Reset the tag cache on the tester side in order to pick up the call to // Cache::deleteTags() on the tested side. - drupal_static_reset('Drupal\Core\Cache\DatabaseCacheTagStorage::tagCache'); + \Drupal::service('cache_tag_storage')->reset(); $this->assertTrue($name != $translation && t($name, array(), array('langcode' => $langcode)) == $translation, 't() works for non-English.'); // Refresh the locale() cache to get fresh data from t() below. We are in diff --git a/core/modules/simpletest/src/WebTestBase.php b/core/modules/simpletest/src/WebTestBase.php index 92d7de9..deda382 100644 --- a/core/modules/simpletest/src/WebTestBase.php +++ b/core/modules/simpletest/src/WebTestBase.php @@ -1148,12 +1148,7 @@ protected function resetAll() { */ protected function refreshVariables() { // Clear the tag cache. - // @todo Replace drupal_static() usage within classes and provide a - // proper interface for invoking reset() on a cache backend: - // https://www.drupal.org/node/2311945. - drupal_static_reset('Drupal\Core\Cache\DatabaseCacheTagStorage::tagCache'); - drupal_static_reset('Drupal\Core\Cache\DatabaseCacheTagStorage::deletedTags'); - drupal_static_reset('Drupal\Core\Cache\DatabaseCacheTagStorage::invalidatedTags'); + \Drupal::service('cache_tag_storage')->reset(); foreach (Cache::getBins() as $backend) { if (is_callable(array($backend, 'reset'))) { $backend->reset();