Mercurial > hg > isophonics-drupal-site
diff core/lib/Drupal/Core/Cache/ChainedFastBackend.php @ 0:4c8ae668cc8c
Initial import (non-working)
author | Chris Cannam |
---|---|
date | Wed, 29 Nov 2017 16:09:58 +0000 |
parents | |
children |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/core/lib/Drupal/Core/Cache/ChainedFastBackend.php Wed Nov 29 16:09:58 2017 +0000 @@ -0,0 +1,310 @@ +<?php + +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, + * Memcached 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. + * + * In addition to being useful for sites running on multiple web nodes, this + * backend can also be useful for sites running on a single web node where the + * fast backend (e.g., APCu) isn't shareable between the web and CLI processes. + * Single-node configurations that don't have that limitation 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. + * + * Note that this is designed specifically for combining a fast inconsistent + * cache backend with a slower consistent cache back-end. To still function + * correctly, it needs to do a consistency check (see the "last write timestamp" + * logic). This contrasts with \Drupal\Core\Cache\BackendChain, which assumes + * both chained cache backends are consistent, thus a consistency check being + * pointless. + * + * @see \Drupal\Core\Cache\BackendChain + * + * @ingroup cache + */ +class ChainedFastBackend implements CacheBackendInterface, CacheTagsInvalidatorInterface { + + /** + * 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 float + */ + 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 = [$cid]; + $cache = $this->getMultiple($cids, $allow_invalid); + return reset($cache); + } + + /** + * {@inheritdoc} + */ + public function getMultiple(&$cids, $allow_invalid = FALSE) { + $cids_copy = $cids; + $cache = []; + + // If we can determine the time at which the last write to the consistent + // backend occurred (we might not be able to if it has been recently + // flushed/restarted), then we can use that to validate items from the fast + // backend, so try to get those first. Otherwise, we can't assume that + // anything in the fast backend is valid, so don't even bother fetching + // from there. + $last_write_timestamp = $this->getLastWriteTimestamp(); + if ($last_write_timestamp) { + // Items in the fast backend might be invalid based on their timestamp, + // but we can't check the timestamp prior to getting the item, which + // includes unserializing it. However, unserializing an invalid item can + // throw an exception. For example, a __wakeup() implementation that + // receives object properties containing references to code or data that + // no longer exists in the application's current state. + // + // Unserializing invalid data, whether it throws an exception or not, is + // a waste of time, but we only incur it while a cache invalidation has + // not yet finished propagating to all the fast backend instances. + // + // Most cache backend implementations should not wrap their internal + // get() implementations with a try/catch, because they have no reason to + // assume that their data is invalid, and doing so would mask + // unserialization errors of valid data. We do so here, only because the + // fast backend is non-authoritative, and after discarding its + // exceptions, we proceed to check the consistent (authoritative) backend + // and allow exceptions from that to bubble up. + try { + $items = $this->fastBackend->getMultiple($cids, $allow_invalid); + } + catch (\Exception $e) { + $cids = $cids_copy; + $items = []; + } + + // Even if items were successfully fetched from the fast backend, they + // are potentially invalid if older than the last time the bin was + // written to in the consistent backend, so only keep ones that aren't. + foreach ($items 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; + // Don't write the cache tags to the fast backend as any cache tag + // invalidation results in an invalidation of the whole fast backend. + $this->fastBackend->set($item->cid, $item->data, $item->expire); + } + } + + return $cache; + } + + /** + * {@inheritdoc} + */ + public function set($cid, $data, $expire = Cache::PERMANENT, array $tags = []) { + $this->consistentBackend->set($cid, $data, $expire, $tags); + $this->markAsOutdated(); + // Don't write the cache tags to the fast backend as any cache tag + // invalidation results in an invalidation of the whole fast backend. + $this->fastBackend->set($cid, $data, $expire); + } + + /** + * {@inheritdoc} + */ + public function setMultiple(array $items) { + $this->consistentBackend->setMultiple($items); + $this->markAsOutdated(); + // Don't write the cache tags to the fast backend as any cache tag + // invalidation results in an invalidation of the whole fast backend. + foreach ($items as &$item) { + unset($item['tags']); + } + $this->fastBackend->setMultiple($items); + } + + /** + * {@inheritdoc} + */ + public function delete($cid) { + $this->consistentBackend->deleteMultiple([$cid]); + $this->markAsOutdated(); + } + + /** + * {@inheritdoc} + */ + public function deleteMultiple(array $cids) { + $this->consistentBackend->deleteMultiple($cids); + $this->markAsOutdated(); + } + + /** + * {@inheritdoc} + */ + public function deleteAll() { + $this->consistentBackend->deleteAll(); + $this->markAsOutdated(); + } + + /** + * {@inheritdoc} + */ + public function invalidate($cid) { + $this->invalidateMultiple([$cid]); + } + + /** + * {@inheritdoc} + */ + public function invalidateMultiple(array $cids) { + $this->consistentBackend->invalidateMultiple($cids); + $this->markAsOutdated(); + } + + /** + * {@inheritdoc} + */ + public function invalidateTags(array $tags) { + if ($this->consistentBackend instanceof CacheTagsInvalidatorInterface) { + $this->consistentBackend->invalidateTags($tags); + } + $this->markAsOutdated(); + } + + /** + * {@inheritdoc} + */ + public function invalidateAll() { + $this->consistentBackend->invalidateAll(); + $this->markAsOutdated(); + } + + /** + * {@inheritdoc} + */ + public function garbageCollection() { + $this->consistentBackend->garbageCollection(); + $this->fastBackend->garbageCollection(); + } + + /** + * {@inheritdoc} + */ + public function removeBin() { + $this->consistentBackend->removeBin(); + $this->fastBackend->removeBin(); + } + + /** + * @todo Document in https://www.drupal.org/node/2311945. + */ + public function reset() { + $this->lastWriteTimestamp = NULL; + } + + /** + * 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. + // Also add 1 millisecond, to ensure that caches written earlier in the same + // millisecond are invalidated. It is possible that caches will be later in + // the same millisecond and are then incorrectly invalidated, but that only + // costs one additional roundtrip to the persistent cache. + $now = round(microtime(TRUE) + .001, 3); + if ($now > $this->getLastWriteTimestamp()) { + $this->lastWriteTimestamp = $now; + $this->consistentBackend->set(self::LAST_WRITE_TIMESTAMP_PREFIX . $this->bin, $this->lastWriteTimestamp); + } + } + +}