From 1747f674f9695b5f6aedfe973b983d1616385c96 Mon Sep 17 00:00:00 2001
From: Nathaniel Catchpole <catch@35733.no-reply.drupal.org>
Date: Mon, 24 Feb 2014 10:36:22 +0000
Subject: [PATCH] Issue #2202537 by alexpott: Avoid repeated cache tag
 invalidations.

---
 .../lib/Drupal/Core/Cache/DatabaseBackend.php | 15 ++-
 .../lib/Drupal/simpletest/WebTestBase.php     |  1 +
 .../Tests/Cache/DatabaseBackendTagTest.php    | 95 +++++++++++++++++++
 3 files changed, 109 insertions(+), 2 deletions(-)
 create mode 100644 core/modules/system/lib/Drupal/system/Tests/Cache/DatabaseBackendTagTest.php

diff --git a/core/lib/Drupal/Core/Cache/DatabaseBackend.php b/core/lib/Drupal/Core/Cache/DatabaseBackend.php
index cec770868d37..cd81acc65a53 100644
--- a/core/lib/Drupal/Core/Cache/DatabaseBackend.php
+++ b/core/lib/Drupal/Core/Cache/DatabaseBackend.php
@@ -165,12 +165,17 @@ public function set($cid, $data, $expire = Cache::PERMANENT, array $tags = array
   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 @@ public function invalidateMultiple(array $cids) {
   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 ccefb316a0dc..97954558159d 100644
--- a/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php
+++ b/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php
@@ -1055,6 +1055,7 @@ protected function refreshVariables() {
     // 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 000000000000..1645779b3d47
--- /dev/null
+++ b/core/modules/system/lib/Drupal/system/Tests/Cache/DatabaseBackendTagTest.php
@@ -0,0 +1,95 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\system\Tests\Cache\DatabaseBackendTagTest.
+ */
+
+namespace Drupal\system\Tests\Cache;
+
+use Drupal\Core\Cache\Cache;
+use Drupal\Core\DependencyInjection\ContainerBuilder;
+use Drupal\simpletest\DrupalUnitTestBase;
+use Symfony\Component\DependencyInjection\Reference;
+
+/**
+ * Tests DatabaseBackend cache tag implementation.
+ */
+class DatabaseBackendTagTest extends DrupalUnitTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = array('system');
+
+  public static function getInfo() {
+    return array(
+      'name' => '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.');
+  }
+
+}
-- 
GitLab