annotate core/lib/Drupal/Core/Cache/ChainedFastBackend.php @ 19:fa3358dc1485 tip

Add ndrum files
author Chris Cannam
date Wed, 28 Aug 2019 13:14:47 +0100
parents 4c8ae668cc8c
children
rev   line source
Chris@0 1 <?php
Chris@0 2
Chris@0 3 namespace Drupal\Core\Cache;
Chris@0 4
Chris@0 5 /**
Chris@0 6 * Defines a backend with a fast and a consistent backend chain.
Chris@0 7 *
Chris@0 8 * In order to mitigate a network roundtrip for each cache get operation, this
Chris@0 9 * cache allows a fast backend to be put in front of a slow(er) backend.
Chris@0 10 * Typically the fast backend will be something like APCu, and be bound to a
Chris@0 11 * single web node, and will not require a network round trip to fetch a cache
Chris@0 12 * item. The fast backend will also typically be inconsistent (will only see
Chris@0 13 * changes from one web node). The slower backend will be something like Mysql,
Chris@0 14 * Memcached or Redis, and will be used by all web nodes, thus making it
Chris@0 15 * consistent, but also require a network round trip for each cache get.
Chris@0 16 *
Chris@0 17 * In addition to being useful for sites running on multiple web nodes, this
Chris@0 18 * backend can also be useful for sites running on a single web node where the
Chris@0 19 * fast backend (e.g., APCu) isn't shareable between the web and CLI processes.
Chris@0 20 * Single-node configurations that don't have that limitation can just use the
Chris@0 21 * fast cache backend directly.
Chris@0 22 *
Chris@0 23 * We always use the fast backend when reading (get()) entries from cache, but
Chris@0 24 * check whether they were created before the last write (set()) to this
Chris@0 25 * (chained) cache backend. Those cache entries that were created before the
Chris@0 26 * last write are discarded, but we use their cache IDs to then read them from
Chris@0 27 * the consistent (slower) cache backend instead; at the same time we update
Chris@0 28 * the fast cache backend so that the next read will hit the faster backend
Chris@0 29 * again. Hence we can guarantee that the cache entries we return are all
Chris@0 30 * up-to-date, and maximally exploit the faster cache backend. This cache
Chris@0 31 * backend uses and maintains a "last write timestamp" to determine which cache
Chris@0 32 * entries should be discarded.
Chris@0 33 *
Chris@0 34 * Because this backend will mark all the cache entries in a bin as out-dated
Chris@0 35 * for each write to a bin, it is best suited to bins with fewer changes.
Chris@0 36 *
Chris@0 37 * Note that this is designed specifically for combining a fast inconsistent
Chris@0 38 * cache backend with a slower consistent cache back-end. To still function
Chris@0 39 * correctly, it needs to do a consistency check (see the "last write timestamp"
Chris@0 40 * logic). This contrasts with \Drupal\Core\Cache\BackendChain, which assumes
Chris@0 41 * both chained cache backends are consistent, thus a consistency check being
Chris@0 42 * pointless.
Chris@0 43 *
Chris@0 44 * @see \Drupal\Core\Cache\BackendChain
Chris@0 45 *
Chris@0 46 * @ingroup cache
Chris@0 47 */
Chris@0 48 class ChainedFastBackend implements CacheBackendInterface, CacheTagsInvalidatorInterface {
Chris@0 49
Chris@0 50 /**
Chris@0 51 * Cache key prefix for the bin-specific entry to track the last write.
Chris@0 52 */
Chris@0 53 const LAST_WRITE_TIMESTAMP_PREFIX = 'last_write_timestamp_';
Chris@0 54
Chris@0 55 /**
Chris@0 56 * @var string
Chris@0 57 */
Chris@0 58 protected $bin;
Chris@0 59
Chris@0 60 /**
Chris@0 61 * The consistent cache backend.
Chris@0 62 *
Chris@0 63 * @var \Drupal\Core\Cache\CacheBackendInterface
Chris@0 64 */
Chris@0 65 protected $consistentBackend;
Chris@0 66
Chris@0 67 /**
Chris@0 68 * The fast cache backend.
Chris@0 69 *
Chris@0 70 * @var \Drupal\Core\Cache\CacheBackendInterface
Chris@0 71 */
Chris@0 72 protected $fastBackend;
Chris@0 73
Chris@0 74 /**
Chris@0 75 * The time at which the last write to this cache bin happened.
Chris@0 76 *
Chris@0 77 * @var float
Chris@0 78 */
Chris@0 79 protected $lastWriteTimestamp;
Chris@0 80
Chris@0 81 /**
Chris@0 82 * Constructs a ChainedFastBackend object.
Chris@0 83 *
Chris@0 84 * @param \Drupal\Core\Cache\CacheBackendInterface $consistent_backend
Chris@0 85 * The consistent cache backend.
Chris@0 86 * @param \Drupal\Core\Cache\CacheBackendInterface $fast_backend
Chris@0 87 * The fast cache backend.
Chris@0 88 * @param string $bin
Chris@0 89 * The cache bin for which the object is created.
Chris@0 90 */
Chris@0 91 public function __construct(CacheBackendInterface $consistent_backend, CacheBackendInterface $fast_backend, $bin) {
Chris@0 92 $this->consistentBackend = $consistent_backend;
Chris@0 93 $this->fastBackend = $fast_backend;
Chris@0 94 $this->bin = 'cache_' . $bin;
Chris@0 95 $this->lastWriteTimestamp = NULL;
Chris@0 96 }
Chris@0 97
Chris@0 98 /**
Chris@0 99 * {@inheritdoc}
Chris@0 100 */
Chris@0 101 public function get($cid, $allow_invalid = FALSE) {
Chris@0 102 $cids = [$cid];
Chris@0 103 $cache = $this->getMultiple($cids, $allow_invalid);
Chris@0 104 return reset($cache);
Chris@0 105 }
Chris@0 106
Chris@0 107 /**
Chris@0 108 * {@inheritdoc}
Chris@0 109 */
Chris@0 110 public function getMultiple(&$cids, $allow_invalid = FALSE) {
Chris@0 111 $cids_copy = $cids;
Chris@0 112 $cache = [];
Chris@0 113
Chris@0 114 // If we can determine the time at which the last write to the consistent
Chris@0 115 // backend occurred (we might not be able to if it has been recently
Chris@0 116 // flushed/restarted), then we can use that to validate items from the fast
Chris@0 117 // backend, so try to get those first. Otherwise, we can't assume that
Chris@0 118 // anything in the fast backend is valid, so don't even bother fetching
Chris@0 119 // from there.
Chris@0 120 $last_write_timestamp = $this->getLastWriteTimestamp();
Chris@0 121 if ($last_write_timestamp) {
Chris@0 122 // Items in the fast backend might be invalid based on their timestamp,
Chris@0 123 // but we can't check the timestamp prior to getting the item, which
Chris@0 124 // includes unserializing it. However, unserializing an invalid item can
Chris@0 125 // throw an exception. For example, a __wakeup() implementation that
Chris@0 126 // receives object properties containing references to code or data that
Chris@0 127 // no longer exists in the application's current state.
Chris@0 128 //
Chris@0 129 // Unserializing invalid data, whether it throws an exception or not, is
Chris@0 130 // a waste of time, but we only incur it while a cache invalidation has
Chris@0 131 // not yet finished propagating to all the fast backend instances.
Chris@0 132 //
Chris@0 133 // Most cache backend implementations should not wrap their internal
Chris@0 134 // get() implementations with a try/catch, because they have no reason to
Chris@0 135 // assume that their data is invalid, and doing so would mask
Chris@0 136 // unserialization errors of valid data. We do so here, only because the
Chris@0 137 // fast backend is non-authoritative, and after discarding its
Chris@0 138 // exceptions, we proceed to check the consistent (authoritative) backend
Chris@0 139 // and allow exceptions from that to bubble up.
Chris@0 140 try {
Chris@0 141 $items = $this->fastBackend->getMultiple($cids, $allow_invalid);
Chris@0 142 }
Chris@0 143 catch (\Exception $e) {
Chris@0 144 $cids = $cids_copy;
Chris@0 145 $items = [];
Chris@0 146 }
Chris@0 147
Chris@0 148 // Even if items were successfully fetched from the fast backend, they
Chris@0 149 // are potentially invalid if older than the last time the bin was
Chris@0 150 // written to in the consistent backend, so only keep ones that aren't.
Chris@0 151 foreach ($items as $item) {
Chris@0 152 if ($item->created < $last_write_timestamp) {
Chris@0 153 $cids[array_search($item->cid, $cids_copy)] = $item->cid;
Chris@0 154 }
Chris@0 155 else {
Chris@0 156 $cache[$item->cid] = $item;
Chris@0 157 }
Chris@0 158 }
Chris@0 159 }
Chris@0 160
Chris@0 161 // If there were any cache entries that were not available in the fast
Chris@0 162 // backend, retrieve them from the consistent backend and store them in the
Chris@0 163 // fast one.
Chris@0 164 if ($cids) {
Chris@0 165 foreach ($this->consistentBackend->getMultiple($cids, $allow_invalid) as $item) {
Chris@0 166 $cache[$item->cid] = $item;
Chris@0 167 // Don't write the cache tags to the fast backend as any cache tag
Chris@0 168 // invalidation results in an invalidation of the whole fast backend.
Chris@0 169 $this->fastBackend->set($item->cid, $item->data, $item->expire);
Chris@0 170 }
Chris@0 171 }
Chris@0 172
Chris@0 173 return $cache;
Chris@0 174 }
Chris@0 175
Chris@0 176 /**
Chris@0 177 * {@inheritdoc}
Chris@0 178 */
Chris@0 179 public function set($cid, $data, $expire = Cache::PERMANENT, array $tags = []) {
Chris@0 180 $this->consistentBackend->set($cid, $data, $expire, $tags);
Chris@0 181 $this->markAsOutdated();
Chris@0 182 // Don't write the cache tags to the fast backend as any cache tag
Chris@0 183 // invalidation results in an invalidation of the whole fast backend.
Chris@0 184 $this->fastBackend->set($cid, $data, $expire);
Chris@0 185 }
Chris@0 186
Chris@0 187 /**
Chris@0 188 * {@inheritdoc}
Chris@0 189 */
Chris@0 190 public function setMultiple(array $items) {
Chris@0 191 $this->consistentBackend->setMultiple($items);
Chris@0 192 $this->markAsOutdated();
Chris@0 193 // Don't write the cache tags to the fast backend as any cache tag
Chris@0 194 // invalidation results in an invalidation of the whole fast backend.
Chris@0 195 foreach ($items as &$item) {
Chris@0 196 unset($item['tags']);
Chris@0 197 }
Chris@0 198 $this->fastBackend->setMultiple($items);
Chris@0 199 }
Chris@0 200
Chris@0 201 /**
Chris@0 202 * {@inheritdoc}
Chris@0 203 */
Chris@0 204 public function delete($cid) {
Chris@0 205 $this->consistentBackend->deleteMultiple([$cid]);
Chris@0 206 $this->markAsOutdated();
Chris@0 207 }
Chris@0 208
Chris@0 209 /**
Chris@0 210 * {@inheritdoc}
Chris@0 211 */
Chris@0 212 public function deleteMultiple(array $cids) {
Chris@0 213 $this->consistentBackend->deleteMultiple($cids);
Chris@0 214 $this->markAsOutdated();
Chris@0 215 }
Chris@0 216
Chris@0 217 /**
Chris@0 218 * {@inheritdoc}
Chris@0 219 */
Chris@0 220 public function deleteAll() {
Chris@0 221 $this->consistentBackend->deleteAll();
Chris@0 222 $this->markAsOutdated();
Chris@0 223 }
Chris@0 224
Chris@0 225 /**
Chris@0 226 * {@inheritdoc}
Chris@0 227 */
Chris@0 228 public function invalidate($cid) {
Chris@0 229 $this->invalidateMultiple([$cid]);
Chris@0 230 }
Chris@0 231
Chris@0 232 /**
Chris@0 233 * {@inheritdoc}
Chris@0 234 */
Chris@0 235 public function invalidateMultiple(array $cids) {
Chris@0 236 $this->consistentBackend->invalidateMultiple($cids);
Chris@0 237 $this->markAsOutdated();
Chris@0 238 }
Chris@0 239
Chris@0 240 /**
Chris@0 241 * {@inheritdoc}
Chris@0 242 */
Chris@0 243 public function invalidateTags(array $tags) {
Chris@0 244 if ($this->consistentBackend instanceof CacheTagsInvalidatorInterface) {
Chris@0 245 $this->consistentBackend->invalidateTags($tags);
Chris@0 246 }
Chris@0 247 $this->markAsOutdated();
Chris@0 248 }
Chris@0 249
Chris@0 250 /**
Chris@0 251 * {@inheritdoc}
Chris@0 252 */
Chris@0 253 public function invalidateAll() {
Chris@0 254 $this->consistentBackend->invalidateAll();
Chris@0 255 $this->markAsOutdated();
Chris@0 256 }
Chris@0 257
Chris@0 258 /**
Chris@0 259 * {@inheritdoc}
Chris@0 260 */
Chris@0 261 public function garbageCollection() {
Chris@0 262 $this->consistentBackend->garbageCollection();
Chris@0 263 $this->fastBackend->garbageCollection();
Chris@0 264 }
Chris@0 265
Chris@0 266 /**
Chris@0 267 * {@inheritdoc}
Chris@0 268 */
Chris@0 269 public function removeBin() {
Chris@0 270 $this->consistentBackend->removeBin();
Chris@0 271 $this->fastBackend->removeBin();
Chris@0 272 }
Chris@0 273
Chris@0 274 /**
Chris@0 275 * @todo Document in https://www.drupal.org/node/2311945.
Chris@0 276 */
Chris@0 277 public function reset() {
Chris@0 278 $this->lastWriteTimestamp = NULL;
Chris@0 279 }
Chris@0 280
Chris@0 281 /**
Chris@0 282 * Gets the last write timestamp.
Chris@0 283 */
Chris@0 284 protected function getLastWriteTimestamp() {
Chris@0 285 if ($this->lastWriteTimestamp === NULL) {
Chris@0 286 $cache = $this->consistentBackend->get(self::LAST_WRITE_TIMESTAMP_PREFIX . $this->bin);
Chris@0 287 $this->lastWriteTimestamp = $cache ? $cache->data : 0;
Chris@0 288 }
Chris@0 289 return $this->lastWriteTimestamp;
Chris@0 290 }
Chris@0 291
Chris@0 292 /**
Chris@0 293 * Marks the fast cache bin as outdated because of a write.
Chris@0 294 */
Chris@0 295 protected function markAsOutdated() {
Chris@0 296 // Clocks on a single server can drift. Multiple servers may have slightly
Chris@0 297 // differing opinions about the current time. Given that, do not assume
Chris@0 298 // 'now' on this server is always later than our stored timestamp.
Chris@0 299 // Also add 1 millisecond, to ensure that caches written earlier in the same
Chris@0 300 // millisecond are invalidated. It is possible that caches will be later in
Chris@0 301 // the same millisecond and are then incorrectly invalidated, but that only
Chris@0 302 // costs one additional roundtrip to the persistent cache.
Chris@0 303 $now = round(microtime(TRUE) + .001, 3);
Chris@0 304 if ($now > $this->getLastWriteTimestamp()) {
Chris@0 305 $this->lastWriteTimestamp = $now;
Chris@0 306 $this->consistentBackend->set(self::LAST_WRITE_TIMESTAMP_PREFIX . $this->bin, $this->lastWriteTimestamp);
Chris@0 307 }
Chris@0 308 }
Chris@0 309
Chris@0 310 }