From 0f849c259091f34650051db071a5bc01bc16b95c Mon Sep 17 00:00:00 2001
From: Steven Merrill <steven.merrill@gmail.com>
Date: Sun, 13 Apr 2014 15:43:07 -0400
Subject:  Steven Merrill: Generated with Drush iq

---
 core/core.services.yml                             |   7 +
 core/lib/Drupal/Core/Cache/ApcBackend.php          | 316 +++++++++++++++++++++
 core/lib/Drupal/Core/Cache/ApcBackendFactory.php   |  30 ++
 core/lib/Drupal/Core/Cache/CacheFactory.php        |   2 +-
 .../ChainedConsistentAndInconsistentBackend.php    | 257 +++++++++++++++++
 ...inedConsistentAndInconsistentBackendFactory.php |  46 +++
 core/lib/Drupal/Core/Cache/DatabaseBackend.php     |   9 +-
 .../Tests/Core/Cache/InconsistentBackendTest.php   |  69 +++++
 8 files changed, 730 insertions(+), 6 deletions(-)
 create mode 100644 core/lib/Drupal/Core/Cache/ApcBackend.php
 create mode 100644 core/lib/Drupal/Core/Cache/ApcBackendFactory.php
 create mode 100644 core/lib/Drupal/Core/Cache/ChainedConsistentAndInconsistentBackend.php
 create mode 100644 core/lib/Drupal/Core/Cache/ChainedConsistentAndInconsistentBackendFactory.php
 create mode 100644 core/tests/Drupal/Tests/Core/Cache/InconsistentBackendTest.php

diff --git a/core/core.services.yml b/core/core.services.yml
index f3c6cd9..a55844a 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -22,6 +22,13 @@ services:
     arguments: ['@request', '@theme.negotiator']
     tags:
       - { name: cache.context}
+  cache_inconsistent_factory:
+    class: Drupal\Core\Cache\ChainedConsistentAndInconsistentBackendFactory
+    arguments: ['@settings']
+    calls:
+      - [setContainer, ['@service_container']]
+  cache.backend.apc:
+    class: Drupal\Core\Cache\ApcBackendFactory
   cache.backend.database:
     class: Drupal\Core\Cache\DatabaseBackendFactory
     arguments: ['@database']
diff --git a/core/lib/Drupal/Core/Cache/ApcBackend.php b/core/lib/Drupal/Core/Cache/ApcBackend.php
new file mode 100644
index 0000000..40032ba
--- /dev/null
+++ b/core/lib/Drupal/Core/Cache/ApcBackend.php
@@ -0,0 +1,316 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Cache\ApcBackend.
+ */
+
+namespace Drupal\Core\Cache;
+
+/**
+ * Defines an APC user cache implementation.
+ *
+ * @ingroup cache
+ */
+class ApcBackend implements CacheBackendInterface {
+
+  /**
+   * @var string
+   */
+  protected $tagPrefix;
+
+  /**
+   * @var string
+   */
+  protected $bin;
+
+  /**
+   * Constructs an ApcBackend object.
+   *
+   * @param string $bin
+   *   The cache bin for which the object is created.
+   */
+  public function __construct($bin) {
+    $bin = 'cache_' . $bin;
+    $this->bin = $bin;
+    $this->tagPrefix = 'tags-' . $this->bin . '_';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function get($cid, $allow_invalid = FALSE) {
+    $cids = array($cid);
+    $cache = $this->getMultiple($cids, $allow_invalid);
+    return reset($cache);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getMultiple(&$cids, $allow_invalid = FALSE) {
+    $prefixed_cids = array();
+    foreach ($cids as $cid) {
+      $prefixed_cids[$this->bin . '-' . $cid] = $cid;
+    }
+    $result = apc_fetch(array_keys($prefixed_cids));
+    $cache = array();
+    foreach ($result as $item) {
+      if ($item = $this->prepareItem($item, $allow_invalid)) {
+        $cache[$item->cid] = $item;
+      }
+    }
+    $cids = array_diff($cids, array_keys($cache));
+    return $cache;
+  }
+
+  /**
+   * Prepares a cached item.
+   *
+   * Checks that items are either permanent or did not expire, and unserializes
+   * data as appropriate.
+   *
+   * @param object $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|false
+   *   The item with data unserialized as appropriate and a property indicating
+   *   whether the item is valid, or FALSE if there is no valid item to load.
+   */
+  protected function prepareItem($cache, $allow_invalid) {
+    if (!isset($cache->data)) {
+      return FALSE;
+    }
+
+    $cache->tags = $cache->tags ? explode(' ', $cache->tags) : array();
+
+    $checksum = $this->checksumTags($cache->tags);
+
+    // Check if deleteTags() has been called with any of the entry's tags.
+    if ($cache->checksum_deletions != $checksum['deletions']) {
+      return FALSE;
+    }
+
+    // Check expire time.
+    $cache->valid = $cache->expire == Cache::PERMANENT || $cache->expire >= REQUEST_TIME;
+
+    // Check if invalidateTags() has been called with any of the entry's tags.
+    if ($cache->checksum_invalidations != $checksum['invalidations']) {
+      $cache->valid = FALSE;
+    }
+
+    if (!$allow_invalid && !$cache->valid) {
+      return FALSE;
+    }
+
+    // Unserialize and return the cached data.
+    if ($cache->serialized) {
+      $cache->data = unserialize($cache->data);
+    }
+
+    return $cache;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function set($cid, $data, $expire = Cache::PERMANENT, array $tags = array()) {
+    $flat_tags = $this->flattenTags($tags);
+    $deleted_tags = &drupal_static('Drupal\Core\Cache\ApcBackend::deletedTags', array());
+    $invalidated_tags = &drupal_static('Drupal\Core\Cache\ApcBackend::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 = (object) array(
+      'serialized' => 0,
+      'created' => REQUEST_TIME,
+      'expire' => $expire,
+      'tags' => implode(' ', $flat_tags),
+      'checksum_invalidations' => $checksum['invalidations'],
+      'checksum_deletions' => $checksum['deletions'],
+      'cid' => $cid,
+    );
+    if (!is_string($data)) {
+      $fields->data = serialize($data);
+      $fields->serialized = 1;
+    }
+    else {
+      $fields->data = $data;
+      $fields->serialized = 0;
+    }
+
+    apc_store(array($this->bin . '-' . $cid => $fields));
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function delete($cid) {
+    $this->deleteMultiple(array($cid));
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function deleteMultiple(array $cids) {
+    $prefixed_cids = array();
+    foreach ($cids as $cid) {
+      $prefixed_cids[$this->bin . '-' . $cid] = $cid;
+    }
+    apc_delete($prefixed_cids);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function deleteTags(array $tags) {
+    $tag_cache = &drupal_static('Drupal\Core\Cache\CacheBackendInterface::tagCache', array());
+    $deleted_tags = &drupal_static('Drupal\Core\Cache\ApcBackend::deletedTags', array());
+    foreach ($this->flattenTags($tags) as $tag) {
+      // Only delete tags once per request unless they are written again.
+      if (isset($deleted_tags[$tag])) {
+        continue;
+      }
+      $deleted_tags[$tag] = TRUE;
+      unset($tag_cache[$tag]);
+      $prefixed_tags = $this->tagPrefix . $tag;
+    }
+    apc_delete($prefixed_tags);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function deleteAll() {
+    apc_delete(new APCIterator('user', '/^' . $this->bin . '-/'));
+    apc_delete(new APCIterator('user', '/^' . $this->tagPrefix . '/'));
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function invalidate($cid) {
+    $this->invalidateMultiple(array($cid));
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function invalidateMultiple(array $cids) {
+    $this->deleteMultiple($cids);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function invalidateTags(array $tags) {
+    foreach ($tags as $tag) {
+      apc_inc($this->tagPrefix . $tag);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function invalidateAll() {
+    $this->deleteAll();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function garbageCollection() {
+    // No work to be done.
+  }
+
+  /**
+   * 'Flattens' a tags array into an array of strings.
+   *
+   * @param array $tags
+   *   Associative array of tags to flatten.
+   *
+   * @return array
+   *   An indexed array of flattened tag identifiers.
+   */
+  protected function flattenTags(array $tags) {
+    if (isset($tags[0])) {
+      return $tags;
+    }
+
+    $flat_tags = array();
+    foreach ($tags as $namespace => $values) {
+      if (is_array($values)) {
+        foreach ($values as $value) {
+          $flat_tags[] = "$namespace:$value";
+        }
+      }
+      else {
+        $flat_tags[] = "$namespace:$values";
+      }
+    }
+    return $flat_tags;
+  }
+
+  /**
+   * Returns the sum total of validations for a given set of tags.
+   *
+   * @param array $flat_tags
+   *   Array of flat tags.
+   *
+   * @return array
+   *   Array containing the sums of all invalidations and deletions.
+   *
+   * @see \Drupal\Core\Cache\ApcBackend::flattenTags()
+   */
+  protected function checksumTags($flat_tags) {
+    $tag_cache = &drupal_static('Drupal\Core\Cache\CacheBackendInterface::tagCache', array());
+
+    $checksum = array(
+      'invalidations' => 0,
+      'deletions' => 0,
+    );
+
+    $query_tags = array_diff($flat_tags, array_keys($tag_cache));
+    if ($query_tags) {
+      $db_tags = apc_fetch($flat_tags);
+      $tag_cache += $db_tags;
+
+      // Fill static cache with empty objects for tags not found in the APC user
+      // cache.
+      $tag_cache += array_fill_keys(array_diff($query_tags, array_keys($db_tags)), $checksum);
+    }
+
+    foreach ($flat_tags as $tag) {
+      $checksum['invalidations'] += $tag_cache[$tag]['invalidations'];
+      $checksum['deletions'] += $tag_cache[$tag]['deletions'];
+    }
+
+    return $checksum;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isEmpty() {
+    $apc = new ApcIterator('user');
+    return $apc->getTotalCount > 0;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function removeBin() {
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Cache/ApcBackendFactory.php b/core/lib/Drupal/Core/Cache/ApcBackendFactory.php
new file mode 100644
index 0000000..86e9c5f
--- /dev/null
+++ b/core/lib/Drupal/Core/Cache/ApcBackendFactory.php
@@ -0,0 +1,30 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Cache\ApcBackendFactory.
+ */
+
+namespace Drupal\Core\Cache;
+
+/**
+ * Defines the APC cache backend factory.
+ */
+use Drupal\Component\Utility\Settings;
+
+class ApcBackendFactory implements CacheFactoryInterface {
+
+  /**
+   * Instantiates a cache backend class for a given cache bin.
+   *
+   * @param string $bin
+   *   The cache bin for which a cache backend object should be returned.
+   *
+   * @return \Drupal\Core\Cache\ApcBackend
+   *   An APC cache backend for the given bin.
+   */
+  public function get($bin) {
+    return new ApcBackend($bin);
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Cache/CacheFactory.php b/core/lib/Drupal/Core/Cache/CacheFactory.php
index 7f061e5..94b85f4 100644
--- a/core/lib/Drupal/Core/Cache/CacheFactory.php
+++ b/core/lib/Drupal/Core/Cache/CacheFactory.php
@@ -51,7 +51,7 @@ function __construct(Settings $settings) {
   public function get($bin) {
     $cache_settings = $this->settings->get('cache');
     if (isset($cache_settings['bins'][$bin])) {
-      $service_name = $cache_settings[$bin];
+      $service_name = $cache_settings['bins'][$bin];
     }
     elseif (isset($cache_settings['default'])) {
       $service_name = $cache_settings['default'];
diff --git a/core/lib/Drupal/Core/Cache/ChainedConsistentAndInconsistentBackend.php b/core/lib/Drupal/Core/Cache/ChainedConsistentAndInconsistentBackend.php
new file mode 100644
index 0000000..50397ba
--- /dev/null
+++ b/core/lib/Drupal/Core/Cache/ChainedConsistentAndInconsistentBackend.php
@@ -0,0 +1,257 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Cache\ChainedConsistentAndInconsistentBackend.
+ */
+
+namespace Drupal\Core\Cache;
+
+use Drupal\Core\KeyValueStore\StateInterface;
+
+/**
+ * Defines a backend with an inconsistent and consistent backend chain.
+ *
+ * We always use the inconsistent (but faster) backend when reading (get())
+ * entries from cache, but check whether they were created before the last write
+ * (set()) to this (chained) cache backend. Those cache entries that were
+ * created before the last write are discarded, but we use their cache IDs to
+ * then read them from the consistent (slower) cache backend instead; at the
+ * same time we update the inconsistent cache backend so that the next read will
+ * hit the (faster) inconsistent backend again. Hence we can guarantee that the
+ * cache entries we return are all up-to-date, and maximally exploit the faster
+ * cache backend.
+ * This cache backend uses and maintains a "last write timestamp" to determine
+ * which cache entries should be discarded.
+ *
+ * @ingroup cache
+ */
+class ChainedConsistentAndInconsistentBackend implements CacheBackendInterface {
+
+  /**
+   * State entry key prefix, for the bin-specific entry to track the last write.
+   */
+  const LAST_WRITE_TIMESTAMP_PREFIX = 'cache_last_write_timestamp_';
+
+  /**
+   * @var string
+   */
+  protected $bin;
+
+  /**
+   * The consistent cache backend.
+   *
+   * @var \Drupal\Core\Cache\CacheBackendInterface
+   */
+  protected $consistentBackend;
+
+  /**
+   * The inconsistent cache backend.
+   *
+   * @var \Drupal\Core\Cache\CacheBackendInterface
+   */
+  protected $inconsistentBackend;
+
+  /**
+   * The state instance to get and set the last write timestamp.
+   *
+   * @var \Drupal\Core\KeyValueStore\StateInterface
+   */
+  protected $state;
+
+  /**
+   * The time at which the last write to this cache bin happened.
+   *
+   * @var int
+   */
+  protected $lastWriteTimestamp;
+
+  /**
+   * Constructs a ChainedConsistentAndInconsistentBackend object.
+   *
+   * @param \Drupal\Core\Cache\CacheBackendInterface
+   *   The consistent cache backend.
+   * @param \Drupal\Core\Cache\CacheBackendInterface
+   *   The inconsistent cache backend.
+   * @param string $bin
+   *   The cache bin for which the object is created.
+   * @param \Drupal\Core\KeyValueStore\StateInterface $state
+   *   The state instance to get and set the last write timestamp.
+   */
+  public function __construct(CacheBackendInterface $consistent_backend, CacheBackendInterface $inconsistent_backend, $bin, StateInterface $state) {
+    $this->consistentBackend = $consistent_backend;
+    $this->inconsistentBackend = $inconsistent_backend;
+    $this->bin = 'cache_' . $bin;
+    $this->state = $state;
+    $this->lastWriteTimestamp = NULL;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function get($cid, $allow_invalid = FALSE) {
+    $cids = array($cid);
+    $cache = $this->getMultiple($cids, $allow_invalid);
+    return reset($cache);
+  }
+
+  /**
+   * Initialize the last write timestamp using the state instance.
+   */
+  public function lastWriteTimestamp() {
+    if ($this->lastWriteTimestamp === NULL) {
+      $this->lastWriteTimestamp = $this->state->get(self::LAST_WRITE_TIMESTAMP_PREFIX . $this->bin);
+    }
+    return $this->lastWriteTimestamp;
+  }
+
+  protected function setLastWriteTimestamp($timestamp) {
+    $this->lastWriteTimestamp = $timestamp;
+  }
+
+  /**
+   * Mark the inconsistent cache bin as outdated because of a write.
+   */
+  protected function markAsOutdated() {
+    $now = time();
+    if ($now > $this->lastWriteTimestamp()) {
+      $this->setLastWriteTimestamp($now);
+      $this->state->set(self::LAST_WRITE_TIMESTAMP_PREFIX . $this->bin, $this->lastWriteTimestamp);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getMultiple(&$cids, $allow_invalid = FALSE) {
+    // Retrieve as many cache entries as possible from the (faster) inconsistent
+    // backend. (Some cache entries may have been created before the last write
+    // to this cache bin and therefore be stale/wrong/inconsistent.)
+    $cache = array();
+    if ($this->lastWriteTimestamp()) {
+      $inconsistent_cache_items = $this->inconsistentBackend->getMultiple($cids, $allow_invalid);
+      $inconsistent_last_write = array_reduce($inconsistent_cache_items, function ($memo, $item) {
+        return $memo > $item->created ? $memo : $item->created;
+      }, 0);
+
+      if ($inconsistent_last_write > $this->lastWriteTimestamp()) {
+        foreach ($inconsistent_cache_items as $item) {
+          $cache[$item->cid] = $item;
+        }
+      }
+      else {
+        foreach ($inconsistent_cache_items as $item) {
+          $cids[] = $item->cid;
+        }
+      }
+    }
+
+    // If there were any cache entries that weren't available in the
+    // inconsistent backend, retrieve them from the consistent backend and store
+    // them in the inconsistent one.
+    if ($cids) {
+      foreach ($this->consistentBackend->getMultiple($cids, $allow_invalid) as $item) {
+        $cache[$item->cid] = $item;
+        $this->inconsistentBackend->set($item->cid, $item->data);
+      }
+    }
+
+    return $cache;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function set($cid, $data, $expire = Cache::PERMANENT, array $tags = array()) {
+    $this->markAsOutdated();
+    $this->consistentBackend->set($cid, $data, $expire, $tags);
+    $this->inconsistentBackend->set($cid, $data, $expire, $tags);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function delete($cid) {
+    $this->markAsOutdated();
+    $this->consistentBackend->deleteMultiple(array($cid));
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function deleteMultiple(array $cids) {
+    $this->markAsOutdated();
+    $this->consistentBackend->deleteMultiple($cids);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function deleteTags(array $tags) {
+    $this->markAsOutdated();
+    $this->consistentBackend->deleteTags($tags);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function deleteAll() {
+    $this->markAsOutdated();
+    $this->consistentBackend->deleteAll();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function invalidate($cid) {
+    $this->invalidateMultiple(array($cid));
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function invalidateMultiple(array $cids) {
+    $this->markAsOutdated();
+    $this->consistentBackend->invalidateMultiple($cids);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function invalidateTags(array $tags) {
+    $this->markAsOutdated();
+    $this->consistentBackend->invalidateTags($tags);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function invalidateAll() {
+    $this->markAsOutdated();
+    $this->consistentBackend->invalidateAll();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function garbageCollection() {
+    $this->consistentBackend->garbageCollection();
+    $this->inconsistentBackend->garbageCollection();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isEmpty() {
+    return $this->consistentBackend->isEmpty();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function removeBin() {
+    $this->consistentBackend->removeBin();
+    $this->inconsistentBackend->removeBin();
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Cache/ChainedConsistentAndInconsistentBackendFactory.php b/core/lib/Drupal/Core/Cache/ChainedConsistentAndInconsistentBackendFactory.php
new file mode 100644
index 0000000..6bf440e
--- /dev/null
+++ b/core/lib/Drupal/Core/Cache/ChainedConsistentAndInconsistentBackendFactory.php
@@ -0,0 +1,46 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Cache\CacheFactory.
+ */
+
+namespace Drupal\Core\Cache;
+
+/**
+ * Defines the chained inconsistent & consistent cache backend factory.
+ */
+class ChainedConsistentAndInconsistentBackendFactory extends CacheFactory {
+
+  /**
+   * Instantiates a chained inconsistent cache backend class for a given cache bin.
+   *
+   * @param string $bin
+   *   The cache bin for which a cache backend object should be returned.
+   *
+   * @return \Drupal\Core\Cache\CacheBackendInterface
+   *   The cache backend object associated with the specified bin.
+   */
+  public function get($bin) {
+    $consistent_service = 'cache.backend.database';
+    $inconsistent_service = 'cache.backend.apc';
+
+    $cache_settings = $this->settings->get('cache');
+    if (isset($cache_settings['inconsistent_cache']) && is_array($cache_settings['inconsistent_cache'])) {
+      if (!empty($cache_settings['inconsistent_cache']['consistent'])) {
+        $consistent_service = $cache_settings['inconsistent_cache']['consistent'];
+      }
+      if (!empty($cache_settings['inconsistent_cache']['inconsistent'])) {
+        $inconsistent_service = $cache_settings['inconsistent_cache']['inconsistent'];
+      }
+    }
+
+    return new ChainedConsistentAndInconsistentBackend(
+      $this->container->get($consistent_service)->get($bin),
+      $this->container->get($inconsistent_service)->get($bin),
+      $bin,
+      $this->container->get('state')
+    );
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Cache/DatabaseBackend.php b/core/lib/Drupal/Core/Cache/DatabaseBackend.php
index 4c431dc..73b708f 100644
--- a/core/lib/Drupal/Core/Cache/DatabaseBackend.php
+++ b/core/lib/Drupal/Core/Cache/DatabaseBackend.php
@@ -25,7 +25,6 @@ class DatabaseBackend implements CacheBackendInterface {
    */
   protected $bin;
 
-
   /**
    * The database connection.
    *
@@ -388,15 +387,15 @@ protected function flattenTags(array $tags) {
   /**
    * Returns the sum total of validations for a given set of tags.
    *
-   * @param array $tags
+   * @param array $flat_tags
    *   Array of flat tags.
    *
-   * @return int
-   *   Sum of all invalidations.
+   * @return array
+   *   Array containing the sums of all invalidations and deletions.
    *
    * @see \Drupal\Core\Cache\DatabaseBackend::flattenTags()
    */
-  protected function checksumTags($flat_tags) {
+  protected function checksumTags(array $flat_tags) {
     $tag_cache = &drupal_static('Drupal\Core\Cache\CacheBackendInterface::tagCache', array());
 
     $checksum = array(
diff --git a/core/tests/Drupal/Tests/Core/Cache/InconsistentBackendTest.php b/core/tests/Drupal/Tests/Core/Cache/InconsistentBackendTest.php
new file mode 100644
index 0000000..b6f55a4
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Cache/InconsistentBackendTest.php
@@ -0,0 +1,69 @@
+<?php
+
+namespace Drupal\Tests\Core\Cache;
+
+use Drupal\Core\Cache\MemoryBackend;
+use Drupal\Core\Cache\ChainedConsistentAndInconsistentBackend;
+use Drupal\Core\KeyValueStore\StateInterface;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ * @group Cache
+ */
+class InconsistentBackendTest extends UnitTestCase {
+
+  public static function getInfo() {
+    return array(
+      'name' => '',
+      'description' => '',
+      'group' => 'Cache'
+    );
+  }
+
+  public function setUp() {
+    $this->consistent_cache = new MemoryBackend("foo");
+    $this->inconsistent_cache = new MemoryBackend("foo");
+    $this->bin = "somebin";
+  }
+
+  public function testGetFromInconsistentCache() {
+    $state = $this->getMock("Drupal\Core\KeyValueStore\StateInterface");
+    $state->expects($this->once())
+          ->method("get")
+          ->will($this->returnValue(1234));
+
+    $inconsistent_backend = new ChainedConsistentAndInconsistentBackend(
+      $this->consistent_cache,
+      $this->inconsistent_cache,
+      $this->bin,
+      $state
+    );
+
+    $this->consistent_cache->set("foo", "baz");
+    $inconsistent_backend->set("foo", "bar");
+    $this->assertEquals("bar", $inconsistent_backend->get("foo")->data);
+  }
+
+  public function testFallThroughToConsistentCache() {
+    // TODO: Refactor to use mocks for at least inconsistent cache backend so
+    // that we can assert that ::set gets called n times.
+    $state = $this->getMock("Drupal\Core\KeyValueStore\StateInterface");
+    $state->expects($this->once())
+          ->method("get")
+          ->will($this->returnValue(time() + 99999));
+
+    $inconsistent_backend = new ChainedConsistentAndInconsistentBackend(
+      $this->consistent_cache,
+      $this->inconsistent_cache,
+      $this->bin,
+      $state
+    );
+
+    $this->inconsistent_cache->set("foo", "bar");
+    $this->consistent_cache->set("foo", "baz");
+    $this->assertEquals("baz", $inconsistent_backend->get("foo")->data);
+  }
+
+}
+
+
-- 
1.8.5.3

