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 }
|