diff --git a/core/lib/Drupal/Core/Cache/DatabaseBackend.php b/core/lib/Drupal/Core/Cache/DatabaseBackend.php index cec770868d3..cd81acc65a5 100644 --- a/core/lib/Drupal/Core/Cache/DatabaseBackend.php +++ b/core/lib/Drupal/Core/Cache/DatabaseBackend.php @@ -165,12 +165,17 @@ class DatabaseBackend implements CacheBackendInterface { protected function doSet($cid, $data, $expire, $tags) { $flat_tags = $this->flattenTags($tags); $deleted_tags = &drupal_static('Drupal\Core\Cache\DatabaseBackend::deletedTags', array()); - // Remove tags that were already deleted during this request from the static - // cache so that another deletion for them will be correctly updated. + $invalidated_tags = &drupal_static('Drupal\Core\Cache\DatabaseBackend::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 ($flat_tags as $tag) { if (isset($deleted_tags[$tag])) { unset($deleted_tags[$tag]); } + if (isset($invalidated_tags[$tag])) { + unset($invalidated_tags[$tag]); + } } $checksum = $this->checksumTags($flat_tags); $fields = array( @@ -301,7 +306,13 @@ class DatabaseBackend implements CacheBackendInterface { public function invalidateTags(array $tags) { try { $tag_cache = &drupal_static('Drupal\Core\Cache\CacheBackendInterface::tagCache', array()); + $invalidated_tags = &drupal_static('Drupal\Core\Cache\DatabaseBackend::invalidatedTags', array()); foreach ($this->flattenTags($tags) as $tag) { + // Only invalidate tags once per request unless they are written again. + if (isset($invalidated_tags[$tag])) { + continue; + } + $invalidated_tags[$tag] = TRUE; unset($tag_cache[$tag]); $this->connection->merge('cache_tags') ->insertFields(array('invalidations' => 1)) diff --git a/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php b/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php index ccefb316a0d..97954558159 100644 --- a/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php +++ b/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php @@ -1055,6 +1055,7 @@ abstract class WebTestBase extends TestBase { // Clear the tag cache. drupal_static_reset('Drupal\Core\Cache\CacheBackendInterface::tagCache'); drupal_static_reset('Drupal\Core\Cache\DatabaseBackend::deletedTags'); + drupal_static_reset('Drupal\Core\Cache\DatabaseBackend::invalidatedTags'); $this->container->get('config.factory')->reset(); $this->container->get('state')->resetCache(); diff --git a/core/modules/system/lib/Drupal/system/Tests/Cache/DatabaseBackendTagTest.php b/core/modules/system/lib/Drupal/system/Tests/Cache/DatabaseBackendTagTest.php new file mode 100644 index 00000000000..1645779b3d4 --- /dev/null +++ b/core/modules/system/lib/Drupal/system/Tests/Cache/DatabaseBackendTagTest.php @@ -0,0 +1,95 @@ + 'Database backend tag test', + 'description' => 'Tests database backend cache tag implementation.', + 'group' => 'Cache', + ); + } + + /** + * {@inheritdoc} + */ + public function containerBuild(ContainerBuilder $container) { + parent::containerBuild($container); + // Change container to database cache backends. + $container + ->register('cache_factory', 'Drupal\Core\Cache\CacheFactory') + ->addArgument(new Reference('settings')) + ->addMethodCall('setContainer', array(new Reference('service_container'))); + } + + public function testTagInvalidations() { + // Create cache entry in multiple bins. + $tags = array('test_tag' => array(1, 2, 3)); + $bins = array('path', 'bootstrap', 'page'); + foreach ($bins as $bin) { + $bin = \Drupal::cache($bin); + $bin->set('test', 'value', Cache::PERMANENT, $tags); + $this->assertTrue($bin->get('test'), 'Cache item was set in bin.'); + } + + $invalidations_before = intval(db_select('cache_tags')->fields('cache_tags', array('invalidations'))->condition('tag', 'test_tag:2')->execute()->fetchField()); + Cache::invalidateTags(array('test_tag' => array(2))); + + // Test that cache entry has been invalidated in multiple bins. + foreach ($bins as $bin) { + $bin = \Drupal::cache($bin); + $this->assertFalse($bin->get('test'), 'Tag invalidation affected item in bin.'); + } + + // Test that only one tag invalidation has occurred. + $invalidations_after = intval(db_select('cache_tags')->fields('cache_tags', array('invalidations'))->condition('tag', 'test_tag:2')->execute()->fetchField()); + $this->assertEqual($invalidations_after, $invalidations_before + 1, 'Only one addition cache tag invalidation has occurred after invalidating a tag used in multiple bins.'); + } + + public function testTagDeletetions() { + // Create cache entry in multiple bins. + $tags = array('test_tag' => array(1, 2, 3)); + $bins = array('path', 'bootstrap', 'page'); + foreach ($bins as $bin) { + $bin = \Drupal::cache($bin); + $bin->set('test', 'value', Cache::PERMANENT, $tags); + $this->assertTrue($bin->get('test'), 'Cache item was set in bin.'); + } + + $deletions_before = intval(db_select('cache_tags')->fields('cache_tags', array('deletions'))->condition('tag', 'test_tag:2')->execute()->fetchField()); + Cache::deleteTags(array('test_tag' => array(2))); + + // Test that cache entry has been deleted in multiple bins. + foreach ($bins as $bin) { + $bin = \Drupal::cache($bin); + $this->assertFalse($bin->get('test'), 'Tag invalidation affected item in bin.'); + } + + // Test that only one tag deletion has occurred. + $deletions_after = intval(db_select('cache_tags')->fields('cache_tags', array('deletions'))->condition('tag', 'test_tag:2')->execute()->fetchField()); + $this->assertEqual($deletions_after, $deletions_before + 1, 'Only one addition cache tag deletion has occurred after deleting a tag used in multiple bins.'); + } + +}