123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310 |
- <?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);
- }
- }
- }
|