1 | <?php |
||
2 | |||
3 | namespace MatthiasMullie\Scrapbook\Buffered\Utils; |
||
4 | |||
5 | use MatthiasMullie\Scrapbook\Adapters\Collections\MemoryStore as BufferCollection; |
||
6 | use MatthiasMullie\Scrapbook\KeyValueStore; |
||
7 | |||
8 | /** |
||
9 | * This is a helper class for BufferedStore & TransactionalStore, which buffer |
||
10 | * real cache requests in memory. |
||
11 | * |
||
12 | * This class accepts 2 caches: a KeyValueStore object (the real cache) and a |
||
13 | * Buffer instance (to read data from as long as it hasn't been committed) |
||
14 | * |
||
15 | * Every write action will first store the data in the Buffer instance, and |
||
16 | * then pas update along to $defer. |
||
17 | * Once commit() is called, $defer will execute all these updates against the |
||
18 | * real cache. All deferred writes that fail to apply will cause that cache key |
||
19 | * to be deleted, to ensure cache consistency. |
||
20 | * Until commit() is called, all data is read from the temporary Buffer instance. |
||
21 | * |
||
22 | * @author Matthias Mullie <[email protected]> |
||
23 | * @copyright Copyright (c) 2014, Matthias Mullie. All rights reserved |
||
24 | * @license LICENSE MIT |
||
25 | */ |
||
26 | class Transaction implements KeyValueStore |
||
27 | { |
||
28 | /** |
||
29 | * @var KeyValueStore |
||
30 | */ |
||
31 | protected $cache; |
||
32 | |||
33 | /** |
||
34 | * @var Buffer |
||
35 | */ |
||
36 | protected $local; |
||
37 | |||
38 | /** |
||
39 | * We'll return stub CAS tokens in order to reliably replay the CAS actions |
||
40 | * to the real cache. This will hold a map of stub token => value, used to |
||
41 | * verify when we do the actual CAS. |
||
42 | * |
||
43 | * @see cas() |
||
44 | * |
||
45 | * @var mixed[] |
||
46 | */ |
||
47 | protected $tokens = array(); |
||
48 | |||
49 | /** |
||
50 | * Deferred updates to be committed to real cache. |
||
51 | * |
||
52 | * @var Defer |
||
53 | */ |
||
54 | protected $defer; |
||
55 | |||
56 | /** |
||
57 | * Suspend reads from real cache. This is used when a flush is issued but it |
||
58 | * has not yet been committed. In that case, we don't want to fall back to |
||
59 | * real cache values, because they're about to be flushed. |
||
60 | * |
||
61 | * @var bool |
||
62 | */ |
||
63 | protected $suspend = false; |
||
64 | |||
65 | /** |
||
66 | * @var Transaction[] |
||
67 | */ |
||
68 | protected $collections = array(); |
||
69 | |||
70 | /** |
||
71 | * @param Buffer|BufferCollection $local |
||
72 | */ |
||
73 | public function __construct(/* Buffer|BufferCollection */ $local, KeyValueStore $cache) |
||
74 | { |
||
75 | // can't do double typehint, so let's manually check the type |
||
76 | if (!$local instanceof Buffer && !$local instanceof BufferCollection) { |
||
77 | $error = 'Invalid class for $local: '.get_class($local); |
||
78 | if (class_exists('\TypeError')) { |
||
79 | throw new \TypeError($error); |
||
80 | } |
||
81 | trigger_error($error, E_USER_ERROR); |
||
82 | } |
||
83 | |||
84 | $this->cache = $cache; |
||
85 | |||
86 | // (uncommitted) writes must never be evicted (even if that means |
||
87 | // crashing because we run out of memory) |
||
88 | $this->local = $local; |
||
89 | |||
90 | $this->defer = new Defer($this->cache); |
||
91 | } |
||
92 | |||
93 | /** |
||
94 | * {@inheritdoc} |
||
95 | */ |
||
96 | public function get($key, &$token = null) |
||
97 | { |
||
98 | $value = $this->local->get($key, $token); |
||
99 | |||
100 | // short-circuit reading from real cache if we have an uncommitted flush |
||
101 | if ($this->suspend && null === $token) { |
||
102 | // flush hasn't been committed yet, don't read from real cache! |
||
103 | return false; |
||
104 | } |
||
105 | |||
106 | if (false === $value) { |
||
107 | if ($this->local->expired($key)) { |
||
108 | /* |
||
109 | * Item used to exist in local cache, but is now expired. This |
||
110 | * is used when values are to be deleted: we don't want to reach |
||
111 | * out to real storage because that would respond with the not- |
||
112 | * yet-deleted value. |
||
113 | */ |
||
114 | |||
115 | return false; |
||
116 | } |
||
117 | |||
118 | // unknown in local cache = fetch from source cache |
||
119 | $value = $this->cache->get($key, $token); |
||
120 | } |
||
121 | |||
122 | // no value = quit early, don't generate a useless token |
||
123 | if (false === $value) { |
||
124 | return false; |
||
125 | } |
||
126 | |||
127 | /* |
||
128 | * $token will be unreliable to the deferred updates so generate |
||
129 | * a custom one and keep the associated value around. |
||
130 | * Read more details in PHPDoc for function cas(). |
||
131 | * uniqid is ok here. Doesn't really have to be unique across |
||
132 | * servers, just has to be unique every time it's called in this |
||
133 | * one particular request - which it is. |
||
134 | */ |
||
135 | $token = uniqid(); |
||
136 | $this->tokens[$token] = serialize($value); |
||
137 | |||
138 | return $value; |
||
139 | } |
||
140 | |||
141 | /** |
||
142 | * {@inheritdoc} |
||
143 | */ |
||
144 | public function getMulti(array $keys, array &$tokens = null) |
||
145 | { |
||
146 | // retrieve all that we can from local cache |
||
147 | $values = $this->local->getMulti($keys); |
||
148 | $tokens = array(); |
||
149 | |||
150 | // short-circuit reading from real cache if we have an uncommitted flush |
||
151 | if (!$this->suspend) { |
||
152 | // figure out which missing key we need to get from real cache |
||
153 | $keys = array_diff($keys, array_keys($values)); |
||
154 | foreach ($keys as $i => $key) { |
||
155 | // don't reach out to real cache for keys that are about to be gone |
||
156 | if ($this->local->expired($key)) { |
||
157 | unset($keys[$i]); |
||
158 | } |
||
159 | } |
||
160 | |||
161 | // fetch missing values from real cache |
||
162 | if ($keys) { |
||
0 ignored issues
–
show
|
|||
163 | $missing = $this->cache->getMulti($keys); |
||
164 | $values += $missing; |
||
165 | } |
||
166 | } |
||
167 | |||
168 | // any tokens we get will be unreliable, so generate some replacements |
||
169 | // (more elaborate explanation in get()) |
||
170 | foreach ($values as $key => $value) { |
||
171 | $token = uniqid(); |
||
172 | $tokens[$key] = $token; |
||
173 | $this->tokens[$token] = serialize($value); |
||
174 | } |
||
175 | |||
176 | return $values; |
||
177 | } |
||
178 | |||
179 | /** |
||
180 | * {@inheritdoc} |
||
181 | */ |
||
182 | public function set($key, $value, $expire = 0) |
||
183 | { |
||
184 | // store the value in memory, so that when we ask for it again later in |
||
185 | // this same request, we get the value we just set |
||
186 | $success = $this->local->set($key, $value, $expire); |
||
187 | if (false === $success) { |
||
188 | return false; |
||
189 | } |
||
190 | |||
191 | $this->defer->set($key, $value, $expire); |
||
192 | |||
193 | return true; |
||
194 | } |
||
195 | |||
196 | /** |
||
197 | * {@inheritdoc} |
||
198 | */ |
||
199 | public function setMulti(array $items, $expire = 0) |
||
200 | { |
||
201 | // store the values in memory, so that when we ask for it again later in |
||
202 | // this same request, we get the value we just set |
||
203 | $success = $this->local->setMulti($items, $expire); |
||
204 | |||
205 | // only attempt to store those that we've set successfully to local |
||
206 | $successful = array_intersect_key($items, $success); |
||
207 | if (!empty($successful)) { |
||
208 | $this->defer->setMulti($successful, $expire); |
||
209 | } |
||
210 | |||
211 | return $success; |
||
212 | } |
||
213 | |||
214 | /** |
||
215 | * {@inheritdoc} |
||
216 | */ |
||
217 | public function delete($key) |
||
218 | { |
||
219 | // check the current value to see if it currently exists, so we can |
||
220 | // properly return true/false as would be expected from KeyValueStore |
||
221 | $value = $this->get($key); |
||
222 | if (false === $value) { |
||
223 | return false; |
||
224 | } |
||
225 | |||
226 | /* |
||
227 | * To make sure that subsequent get() calls for this key don't return |
||
228 | * a value (it's supposed to be deleted), we'll make it expired in our |
||
229 | * temporary bag (as opposed to deleting it from out bag, in which case |
||
230 | * we'd fall back to fetching it from real store, where the transaction |
||
231 | * might not yet be committed) |
||
232 | */ |
||
233 | $this->local->set($key, $value, -1); |
||
234 | |||
235 | $this->defer->delete($key); |
||
236 | |||
237 | return true; |
||
238 | } |
||
239 | |||
240 | /** |
||
241 | * {@inheritdoc} |
||
242 | */ |
||
243 | public function deleteMulti(array $keys) |
||
244 | { |
||
245 | // check the current values to see if they currently exists, so we can |
||
246 | // properly return true/false as would be expected from KeyValueStore |
||
247 | $items = $this->getMulti($keys); |
||
248 | $success = array(); |
||
249 | foreach ($keys as $key) { |
||
250 | $success[$key] = array_key_exists($key, $items); |
||
251 | } |
||
252 | |||
253 | // only attempt to store those that we've deleted successfully to local |
||
254 | $values = array_intersect_key($success, array_flip($keys)); |
||
255 | if (empty($values)) { |
||
256 | return array(); |
||
257 | } |
||
258 | |||
259 | // mark all as expired in local cache (see comment in delete()) |
||
260 | $this->local->setMulti($values, -1); |
||
261 | |||
262 | $this->defer->deleteMulti(array_keys($values)); |
||
263 | |||
264 | return $success; |
||
265 | } |
||
266 | |||
267 | /** |
||
268 | * {@inheritdoc} |
||
269 | */ |
||
270 | public function add($key, $value, $expire = 0) |
||
271 | { |
||
272 | // before adding, make sure the value doesn't yet exist (in real cache, |
||
273 | // nor in memory) |
||
274 | if (false !== $this->get($key)) { |
||
275 | return false; |
||
276 | } |
||
277 | |||
278 | // store the value in memory, so that when we ask for it again later |
||
279 | // in this same request, we get the value we just set |
||
280 | $success = $this->local->set($key, $value, $expire); |
||
281 | if (false === $success) { |
||
282 | return false; |
||
283 | } |
||
284 | |||
285 | $this->defer->add($key, $value, $expire); |
||
286 | |||
287 | return true; |
||
288 | } |
||
289 | |||
290 | /** |
||
291 | * {@inheritdoc} |
||
292 | */ |
||
293 | public function replace($key, $value, $expire = 0) |
||
294 | { |
||
295 | // before replacing, make sure the value actually exists (in real cache, |
||
296 | // or already created in memory) |
||
297 | if (false === $this->get($key)) { |
||
298 | return false; |
||
299 | } |
||
300 | |||
301 | // store the value in memory, so that when we ask for it again later |
||
302 | // in this same request, we get the value we just set |
||
303 | $success = $this->local->set($key, $value, $expire); |
||
304 | if (false === $success) { |
||
305 | return false; |
||
306 | } |
||
307 | |||
308 | $this->defer->replace($key, $value, $expire); |
||
309 | |||
310 | return true; |
||
311 | } |
||
312 | |||
313 | /** |
||
314 | * Since our CAS is deferred, the CAS token we got from our original |
||
315 | * get() will likely not be valid by the time we want to store it to |
||
316 | * the real cache. Imagine this scenario: |
||
317 | * * a value is fetched from (real) cache |
||
318 | * * an new value key is CAS'ed (into temp cache - real CAS is deferred) |
||
319 | * * this key's value is fetched again (this time from temp cache) |
||
320 | * * and a new value is CAS'ed again (into temp cache...). |
||
321 | * |
||
322 | * In this scenario, when we finally want to replay the write actions |
||
323 | * onto the real cache, the first 3 actions would likely work fine. |
||
324 | * The last (second CAS) however would not, since it never got a real |
||
325 | * updated $token from the real cache. |
||
326 | * |
||
327 | * To work around this problem, all get() calls will return a unique |
||
328 | * CAS token and store the value-at-that-time associated with that |
||
329 | * token. All we have to do when we want to write the data to real cache |
||
330 | * is, right before was CAS for real, get the value & (real) cas token |
||
331 | * from storage & compare that value to the one we had stored. If that |
||
332 | * checks out, we can safely resume the CAS with the real token we just |
||
333 | * received. |
||
334 | * |
||
335 | * {@inheritdoc} |
||
336 | */ |
||
337 | public function cas($token, $key, $value, $expire = 0) |
||
338 | { |
||
339 | $originalValue = isset($this->tokens[$token]) ? $this->tokens[$token] : null; |
||
340 | |||
341 | // value is no longer the same as what we used for token |
||
342 | if (serialize($this->get($key)) !== $originalValue) { |
||
343 | return false; |
||
344 | } |
||
345 | |||
346 | // "CAS" value to local cache/memory |
||
347 | $success = $this->local->set($key, $value, $expire); |
||
348 | if (false === $success) { |
||
349 | return false; |
||
350 | } |
||
351 | |||
352 | // only schedule the CAS to be performed on real cache if it was OK on |
||
353 | // local cache |
||
354 | $this->defer->cas($originalValue, $key, $value, $expire); |
||
355 | |||
356 | return true; |
||
357 | } |
||
358 | |||
359 | /** |
||
360 | * {@inheritdoc} |
||
361 | */ |
||
362 | public function increment($key, $offset = 1, $initial = 0, $expire = 0) |
||
363 | { |
||
364 | if ($offset <= 0 || $initial < 0) { |
||
365 | return false; |
||
366 | } |
||
367 | |||
368 | // get existing value (from real cache or memory) so we know what to |
||
369 | // increment in memory (where we may not have anything yet, so we should |
||
370 | // adjust our initial value to what's already in real cache) |
||
371 | $value = $this->get($key); |
||
372 | if (false === $value) { |
||
373 | $value = $initial - $offset; |
||
374 | } |
||
375 | |||
376 | if (!is_numeric($value) || !is_numeric($offset)) { |
||
377 | return false; |
||
378 | } |
||
379 | |||
380 | // store the value in memory, so that when we ask for it again later |
||
381 | // in this same request, we get the value we just set |
||
382 | $value = max(0, $value + $offset); |
||
383 | $success = $this->local->set($key, $value, $expire); |
||
384 | if (false === $success) { |
||
385 | return false; |
||
386 | } |
||
387 | |||
388 | $this->defer->increment($key, $offset, $initial, $expire); |
||
389 | |||
390 | return $value; |
||
391 | } |
||
392 | |||
393 | /** |
||
394 | * {@inheritdoc} |
||
395 | */ |
||
396 | public function decrement($key, $offset = 1, $initial = 0, $expire = 0) |
||
397 | { |
||
398 | if ($offset <= 0 || $initial < 0) { |
||
399 | return false; |
||
400 | } |
||
401 | |||
402 | // get existing value (from real cache or memory) so we know what to |
||
403 | // increment in memory (where we may not have anything yet, so we should |
||
404 | // adjust our initial value to what's already in real cache) |
||
405 | $value = $this->get($key); |
||
406 | if (false === $value) { |
||
407 | $value = $initial + $offset; |
||
408 | } |
||
409 | |||
410 | if (!is_numeric($value) || !is_numeric($offset)) { |
||
411 | return false; |
||
412 | } |
||
413 | |||
414 | // store the value in memory, so that when we ask for it again later |
||
415 | // in this same request, we get the value we just set |
||
416 | $value = max(0, $value - $offset); |
||
417 | $success = $this->local->set($key, $value, $expire); |
||
418 | if (false === $success) { |
||
419 | return false; |
||
420 | } |
||
421 | |||
422 | $this->defer->decrement($key, $offset, $initial, $expire); |
||
423 | |||
424 | return $value; |
||
425 | } |
||
426 | |||
427 | /** |
||
428 | * {@inheritdoc} |
||
429 | */ |
||
430 | public function touch($key, $expire) |
||
431 | { |
||
432 | // grab existing value (from real cache or memory) and re-save (to |
||
433 | // memory) with updated expiration time |
||
434 | $value = $this->get($key); |
||
435 | if (false === $value) { |
||
436 | return false; |
||
437 | } |
||
438 | |||
439 | $success = $this->local->set($key, $value, $expire); |
||
440 | if (false === $success) { |
||
441 | return false; |
||
442 | } |
||
443 | |||
444 | $this->defer->touch($key, $expire); |
||
445 | |||
446 | return true; |
||
447 | } |
||
448 | |||
449 | /** |
||
450 | * {@inheritdoc} |
||
451 | */ |
||
452 | public function flush() |
||
453 | { |
||
454 | foreach ($this->collections as $collection) { |
||
455 | $collection->flush(); |
||
456 | } |
||
457 | |||
458 | $success = $this->local->flush(); |
||
459 | if (false === $success) { |
||
460 | return false; |
||
461 | } |
||
462 | |||
463 | // clear all buffered writes, flush wipes them out anyway |
||
464 | $this->clear(); |
||
465 | |||
466 | // make sure that reads, from now on until commit, don't read from cache |
||
467 | $this->suspend = true; |
||
468 | |||
469 | $this->defer->flush(); |
||
470 | |||
471 | return true; |
||
472 | } |
||
473 | |||
474 | /** |
||
475 | * {@inheritdoc} |
||
476 | */ |
||
477 | public function getCollection($name) |
||
478 | { |
||
479 | if (!isset($this->collections[$name])) { |
||
480 | $this->collections[$name] = new static( |
||
481 | $this->local->getCollection($name), |
||
482 | $this->cache->getCollection($name) |
||
483 | ); |
||
484 | } |
||
485 | |||
486 | return $this->collections[$name]; |
||
487 | } |
||
488 | |||
489 | /** |
||
490 | * Commits all deferred updates to real cache. |
||
491 | * that had already been written to will be deleted. |
||
492 | * |
||
493 | * @return bool |
||
494 | */ |
||
495 | public function commit() |
||
496 | { |
||
497 | $this->clear(); |
||
498 | |||
499 | return $this->defer->commit(); |
||
500 | } |
||
501 | |||
502 | /** |
||
503 | * Roll back all scheduled changes. |
||
504 | * |
||
505 | * @return bool |
||
506 | */ |
||
507 | public function rollback() |
||
508 | { |
||
509 | $this->clear(); |
||
510 | $this->defer->clear(); |
||
511 | |||
512 | return true; |
||
513 | } |
||
514 | |||
515 | /** |
||
516 | * Clears all transaction-related data stored in memory. |
||
517 | */ |
||
518 | protected function clear() |
||
519 | { |
||
520 | $this->tokens = array(); |
||
521 | $this->suspend = false; |
||
522 | } |
||
523 | } |
||
524 |
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)
or! empty(...)
instead.