1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
|
<?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. The
* fast backend must however also use a consistent cache tag invalidation, for
* example by using the cache tag checksum API.
*
* 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) {
$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;
if (!$allow_invalid || $item->valid) {
$this->fastBackend->set($item->cid, $item->data, $item->expire, $item->tags);
}
}
}
return $cache;
}
/**
* {@inheritdoc}
*/
public function set($cid, $data, $expire = Cache::PERMANENT, array $tags = []) {
$this->consistentBackend->set($cid, $data, $expire, $tags);
$this->markAsOutdated();
$this->fastBackend->set($cid, $data, $expire, $tags);
}
/**
* {@inheritdoc}
*/
public function setMultiple(array $items) {
$this->consistentBackend->setMultiple($items);
$this->markAsOutdated();
$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);
}
if ($this->fastBackend instanceof CacheTagsInvalidatorInterface) {
$this->fastBackend->invalidateTags($tags);
}
}
/**
* {@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. Add 50ms
// to the current time each time we write it to the persistent cache, and
// make sure it is always at least 1ms ahead of the current time. This
// somewhat protects against clock drift, while also reducing the number of
// persistent cache writes to one every 50ms if this method is called
// multiple times during a request.
$compare = round(microtime(TRUE) + .001, 3);
if ($compare > $this->getLastWriteTimestamp()) {
$now = round(microtime(TRUE) + .050, 3);
$this->lastWriteTimestamp = $now;
$this->consistentBackend->set(self::LAST_WRITE_TIMESTAMP_PREFIX . $this->bin, $this->lastWriteTimestamp);
}
}
}
|