Commit 8c90c366 authored by catch's avatar catch

Issue #2231595 by beejeebus, Steven Merrill, kim.pepper, Wim Leers,...

Issue #2231595 by beejeebus, Steven Merrill, kim.pepper, Wim Leers, msonnabaum: Add a cache backend that checks an inconsistent cache, then falls back to a consistent cache backend.
parent 1cf3a533
......@@ -26,6 +26,11 @@ services:
class: Drupal\Core\Cache\TimeZoneCacheContext
tags:
- { name: cache.context}
cache.backend.chainedfast:
class: Drupal\Core\Cache\ChainedFastBackendFactory
arguments: ['@settings']
calls:
- [setContainer, ['@service_container']]
cache.backend.database:
class: Drupal\Core\Cache\DatabaseBackendFactory
arguments: ['@database']
......
<?php
/**
* @file
* Contains \Drupal\Core\Cache\ChainedFastBackend.
*/
namespace Drupal\Core\Cache;
/**
* Defines a backend with a fast and a consistent backend chain.
*
* In order to mitigate a network roundtrip for each cache get operation, this
* cache allows a fast backend to be put in front of a slow(er) backend.
* Typically the fast backend will be something like APCu, and be bound to a
* single web node, and will not require a network round trip to fetch a cache
* item. The fast backend will also typically be inconsistent (will only see
* changes from one web node). The slower backend will be something like Mysql,
* Mecached or Redis, and will be used by all web nodes, thus making it
* consistent, but also require a network round trip for each cache get.
*
* It is expected this backend will be used primarily on sites running on
* multiple web nodes, as single-node configurations can just use the fast
* cache backend directly.
*
* We always use the fast 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 fast cache backend so that the next read will hit the faster 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.
*
* Because this backend will mark all the cache entries in a bin as out-dated
* for each write to a bin, it is best suited to bins with fewer changes.
*
* @ingroup cache
*/
class ChainedFastBackend implements CacheBackendInterface {
/**
* Cache key prefix for the bin-specific entry to track the last write.
*/
const LAST_WRITE_TIMESTAMP_PREFIX = 'last_write_timestamp_';
/**
* @var string
*/
protected $bin;
/**
* The consistent cache backend.
*
* @var \Drupal\Core\Cache\CacheBackendInterface
*/
protected $consistentBackend;
/**
* The fast cache backend.
*
* @var \Drupal\Core\Cache\CacheBackendInterface
*/
protected $fastBackend;
/**
* The time at which the last write to this cache bin happened.
*
* @var int
*/
protected $lastWriteTimestamp;
/**
* Constructs a ChainedFastBackend object.
*
* @param \Drupal\Core\Cache\CacheBackendInterface $consistent_backend
* The consistent cache backend.
* @param \Drupal\Core\Cache\CacheBackendInterface $fast_backend
* The fast cache backend.
* @param string $bin
* The cache bin for which the object is created.
*/
public function __construct(CacheBackendInterface $consistent_backend, CacheBackendInterface $fast_backend, $bin) {
$this->consistentBackend = $consistent_backend;
$this->fastBackend = $fast_backend;
$this->bin = 'cache_' . $bin;
$this->lastWriteTimestamp = NULL;
}
/**
* {@inheritdoc}
*/
public function get($cid, $allow_invalid = FALSE) {
$cids = array($cid);
$cache = $this->getMultiple($cids, $allow_invalid);
return reset($cache);
}
/**
* {@inheritdoc}
*/
public function setMultiple(array $items) {
$this->markAsOutdated();
$this->consistentBackend->setMultiple($items);
$this->fastBackend->setMultiple($items);
}
/**
* {@inheritdoc}
*/
public function getMultiple(&$cids, $allow_invalid = FALSE) {
// Retrieve as many cache items as possible from the fast backend. (Some
// cache entries may have been created before the last write to this cache
// bin and therefore be stale/wrong/inconsistent.)
$cids_copy = $cids;
$cache = array();
$last_write_timestamp = $this->getLastWriteTimestamp();
if ($last_write_timestamp) {
foreach ($this->fastBackend->getMultiple($cids, $allow_invalid) as $item) {
if ($item->created < $last_write_timestamp) {
$cids[array_search($item->cid, $cids_copy)] = $item->cid;
}
else {
$cache[$item->cid] = $item;
}
}
}
// If there were any cache entries that were not available in the fast
// backend, retrieve them from the consistent backend and store them in the
// fast one.
if ($cids) {
foreach ($this->consistentBackend->getMultiple($cids, $allow_invalid) as $item) {
$cache[$item->cid] = $item;
$this->fastBackend->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->fastBackend->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->fastBackend->garbageCollection();
}
/**
* {@inheritdoc}
*/
public function removeBin() {
$this->consistentBackend->removeBin();
$this->fastBackend->removeBin();
}
/**
* Gets the last write timestamp.
*/
protected function getLastWriteTimestamp() {
if ($this->lastWriteTimestamp === NULL) {
$cache = $this->consistentBackend->get(self::LAST_WRITE_TIMESTAMP_PREFIX . $this->bin);
$this->lastWriteTimestamp = $cache ? $cache->data : 0;
}
return $this->lastWriteTimestamp;
}
/**
* Marks the fast cache bin as outdated because of a write.
*/
protected function markAsOutdated() {
// Clocks on a single server can drift. Multiple servers may have slightly
// differing opinions about the current time. Given that, do not assume
// 'now' on this server is always later than our stored timestamp.
$now = microtime(TRUE);
if ($now > $this->getLastWriteTimestamp()) {
$this->lastWriteTimestamp = $now;
$this->consistentBackend->set(self::LAST_WRITE_TIMESTAMP_PREFIX . $this->bin, $this->lastWriteTimestamp);
}
}
}
<?php
/**
* @file
* Contains \Drupal\Core\Cache\ChainedFastBackendFactory.
*/
namespace Drupal\Core\Cache;
/**
* Defines the chained fast cache backend factory.
*/
class ChainedFastBackendFactory extends CacheFactory {
/**
* Instantiates a chained, fast 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';
$fast_service = 'cache.backend.apcu';
$cache_settings = $this->settings->get('cache');
if (isset($cache_settings['chained_fast_cache']) && is_array($cache_settings['chained_fast_cache'])) {
if (!empty($cache_settings['chained_fast_cache']['consistent'])) {
$consistent_service = $cache_settings['chained_fast_cache']['consistent'];
}
if (!empty($cache_settings['chained_fast_cache']['fast'])) {
$fast_service = $cache_settings['chained_fast_cache']['fast'];
}
}
return new ChainedFastBackend(
$this->container->get($consistent_service)->get($bin),
$this->container->get($fast_service)->get($bin),
$bin
);
}
}
<?php
/**
* @file
* Contains \Drupal\Tests\Core\Cache\ChainedFastBackendTest.
*/
namespace Drupal\Tests\Core\Cache;
use Drupal\Core\Cache\ChainedFastBackend;
use Drupal\Core\Cache\MemoryBackend;
use Drupal\Tests\UnitTestCase;
/**
* Tests the chained fast cache backend.
*
* @coversDefaultClass \Drupal\Core\Cache\ChainedFastBackend
*
* @group Cache
*/
class ChainedFastBackendTest extends UnitTestCase {
/**
* The consistent cache backend.
*
* @var \Drupal\Core\Cache\CacheBackendInterface
*/
protected $consistentCache;
/**
* The fast cache backend.
*
* @var \Drupal\Core\Cache\CacheBackendInterface
*/
protected $fastCache;
/**
* The cache bin.
*
* @var string
*/
protected $bin;
/**
* {@inheritdoc}
*/
public static function getInfo() {
return array(
'name' => 'Chained Fast Cache Test',
'description' => 'Tests the chained fast cache',
'group' => 'Cache',
);
}
/**
* Tests a get() on the fast backend, with no hit on the consistent backend.
*/
public function testGetDoesntHitConsistentBackend() {
$consistent_cache = $this->getMock('Drupal\Core\Cache\CacheBackendInterface');
$timestamp_cid = ChainedFastBackend::LAST_WRITE_TIMESTAMP_PREFIX . 'cache_foo';
$timestamp_item = (object) array('cid' => $timestamp_cid, 'data' => time() - 60);
$consistent_cache->expects($this->once())
->method('get')->with($timestamp_cid)
->will($this->returnValue($timestamp_item));
$consistent_cache->expects($this->never())
->method('getMultiple');
$fast_cache = new MemoryBackend('foo');
$fast_cache->set('foo', 'baz');
$chained_fast_backend = new ChainedFastBackend(
$consistent_cache,
$fast_cache,
'foo'
);
$this->assertEquals('baz', $chained_fast_backend->get('foo')->data);
}
/**
* Tests a fast cache miss gets data from the consistent cache backend.
*/
public function testFallThroughToConsistentCache() {
$timestamp_item = (object) array(
'cid' => ChainedFastBackend::LAST_WRITE_TIMESTAMP_PREFIX . 'cache_foo',
'data' => time() + 60, // Time travel is easy.
);
$cache_item = (object) array(
'cid' => 'foo',
'data' => 'baz',
'created' => time(),
);
$consistent_cache = $this->getMock('Drupal\Core\Cache\CacheBackendInterface');
$fast_cache = $this->getMock('Drupal\Core\Cache\CacheBackendInterface');
// We should get a call for the timestamp on the consistent backend.
$consistent_cache->expects($this->once())
->method('get')
->with($timestamp_item->cid)
->will($this->returnValue($timestamp_item));
// We should get a call for the cache item on the consistent backend.
$consistent_cache->expects($this->once())
->method('getMultiple')
->with(array($cache_item->cid))
->will($this->returnValue(array($cache_item->cid => $cache_item)));
// We should get a call for the cache item on the fast backend.
$fast_cache->expects($this->once())
->method('getMultiple')
->with(array($cache_item->cid))
->will($this->returnValue(array($cache_item->cid => $cache_item)));
// We should get a call to set the cache item on the fast backend.
$fast_cache->expects($this->once())
->method('set')
->with($cache_item->cid, $cache_item->data);
$chained_fast_backend = new ChainedFastBackend(
$consistent_cache,
$fast_cache,
'foo'
);
$this->assertEquals('baz', $chained_fast_backend->get('foo')->data);
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment