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