Mercurial > hg > isophonics-drupal-site
comparison 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 |
comparison
equal
deleted
inserted
replaced
-1:000000000000 | 0:4c8ae668cc8c |
---|---|
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 } |