objectcache: add mcrouter support to WANObjectCache
[lhc/web/wiklou.git] / includes / libs / objectcache / WANObjectCache.php
1 <?php
2 /**
3 * This program is free software; you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation; either version 2 of the License, or
6 * (at your option) any later version.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License along
14 * with this program; if not, write to the Free Software Foundation, Inc.,
15 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 * http://www.gnu.org/copyleft/gpl.html
17 *
18 * @file
19 * @ingroup Cache
20 * @author Aaron Schulz
21 */
22
23 use Psr\Log\LoggerAwareInterface;
24 use Psr\Log\LoggerInterface;
25 use Psr\Log\NullLogger;
26
27 /**
28 * Multi-datacenter aware caching interface
29 *
30 * All operations go to the local datacenter cache, except for delete(),
31 * touchCheckKey(), and resetCheckKey(), which broadcast to all datacenters.
32 *
33 * This class is intended for caching data from primary stores.
34 * If the get() method does not return a value, then the caller
35 * should query the new value and backfill the cache using set().
36 * When querying the store on cache miss, the closest DB replica
37 * should be used. Try to avoid heavyweight DB master or quorum reads.
38 * When the source data changes, a purge method should be called.
39 * Since purges are expensive, they should be avoided. One can do so if:
40 * - a) The object cached is immutable; or
41 * - b) Validity is checked against the source after get(); or
42 * - c) Using a modest TTL is reasonably correct and performant
43 *
44 * The simplest purge method is delete().
45 *
46 * There are two supported ways to handle broadcasted operations:
47 * - a) Configure the 'purge' EventRelayer to point to a valid PubSub endpoint
48 * that has subscribed listeners on the cache servers applying the cache updates.
49 * - b) Ignore the 'purge' EventRelayer configuration (default is NullEventRelayer)
50 * and set up mcrouter as the underlying cache backend. Using OperationSelectorRoute,
51 * configure 'set' and 'delete' operations to go to all DCs via AllAsyncRoute and
52 * configure other operations to go to the local DC via PoolRoute (for reference,
53 * see https://github.com/facebook/mcrouter/wiki/List-of-Route-Handles).
54 *
55 * Broadcasted operations like delete() and touchCheckKey() are done asynchronously
56 * in all datacenters this way, though the local one should likely be near immediate.
57 *
58 * This means that callers in all datacenters may see older values for however many
59 * milliseconds the the purge took to reach that datacenter. As with any cache, this
60 * should not be relied on for cases where reads are used to determine writes to source
61 * (e.g. non-cache) data stores.
62 *
63 * All values are wrapped in metadata arrays. Keys use a "WANCache:" prefix
64 * to avoid collisions with keys that are not wrapped as metadata arrays. The
65 * prefixes are as follows:
66 * - a) "WANCache:v" : used for regular value keys
67 * - b) "WANCache:i" : used for temporarily storing values of tombstoned keys
68 * - c) "WANCache:t" : used for storing timestamp "check" keys
69 * - d) "WANCache:m" : used for temporary mutex keys to avoid cache stampedes
70 *
71 * @ingroup Cache
72 * @since 1.26
73 */
74 class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
75 /** @var BagOStuff The local datacenter cache */
76 protected $cache;
77 /** @var HashBagOStuff Script instance PHP cache */
78 protected $procCache;
79 /** @var string Purge channel name */
80 protected $purgeChannel;
81 /** @var EventRelayer Bus that handles purge broadcasts */
82 protected $purgeRelayer;
83 /** @var LoggerInterface */
84 protected $logger;
85
86 /** @var int ERR_* constant for the "last error" registry */
87 protected $lastRelayError = self::ERR_NONE;
88
89 /** Max time expected to pass between delete() and DB commit finishing */
90 const MAX_COMMIT_DELAY = 3;
91 /** Max replication+snapshot lag before applying TTL_LAGGED or disallowing set() */
92 const MAX_READ_LAG = 7;
93 /** Seconds to tombstone keys on delete() */
94 const HOLDOFF_TTL = 11; // MAX_COMMIT_DELAY + MAX_READ_LAG + 1
95
96 /** Seconds to keep dependency purge keys around */
97 const CHECK_KEY_TTL = self::TTL_YEAR;
98 /** Seconds to keep lock keys around */
99 const LOCK_TTL = 10;
100 /** Default remaining TTL at which to consider pre-emptive regeneration */
101 const LOW_TTL = 30;
102 /** Default time-since-expiry on a miss that makes a key "hot" */
103 const LOCK_TSE = 1;
104
105 /** Idiom for getWithSetCallback() callbacks to avoid calling set() */
106 const TTL_UNCACHEABLE = -1;
107 /** Idiom for getWithSetCallback() callbacks to 'lockTSE' logic */
108 const TSE_NONE = -1;
109 /** Max TTL to store keys when a data sourced is lagged */
110 const TTL_LAGGED = 30;
111 /** Idiom for delete() for "no hold-off" */
112 const HOLDOFF_NONE = 0;
113
114 /** Tiny negative float to use when CTL comes up >= 0 due to clock skew */
115 const TINY_NEGATIVE = -0.000001;
116
117 /** Cache format version number */
118 const VERSION = 1;
119
120 const FLD_VERSION = 0; // key to cache version number
121 const FLD_VALUE = 1; // key to the cached value
122 const FLD_TTL = 2; // key to the original TTL
123 const FLD_TIME = 3; // key to the cache time
124 const FLD_FLAGS = 4; // key to the flags bitfield
125 const FLD_HOLDOFF = 5; // key to any hold-off TTL
126
127 /** @var integer Treat this value as expired-on-arrival */
128 const FLG_STALE = 1;
129
130 const ERR_NONE = 0; // no error
131 const ERR_NO_RESPONSE = 1; // no response
132 const ERR_UNREACHABLE = 2; // can't connect
133 const ERR_UNEXPECTED = 3; // response gave some error
134 const ERR_RELAY = 4; // relay broadcast failed
135
136 const VALUE_KEY_PREFIX = 'WANCache:v:';
137 const INTERIM_KEY_PREFIX = 'WANCache:i:';
138 const TIME_KEY_PREFIX = 'WANCache:t:';
139 const MUTEX_KEY_PREFIX = 'WANCache:m:';
140
141 const PURGE_VAL_PREFIX = 'PURGED:';
142
143 const VFLD_DATA = 'WOC:d'; // key to the value of versioned data
144 const VFLD_VERSION = 'WOC:v'; // key to the version of the value present
145
146 const MAX_PC_KEYS = 1000; // max keys to keep in process cache
147
148 const DEFAULT_PURGE_CHANNEL = 'wancache-purge';
149
150 /**
151 * @param array $params
152 * - cache : BagOStuff object for a persistent cache
153 * - channels : Map of (action => channel string). Actions include "purge".
154 * - relayers : Map of (action => EventRelayer object). Actions include "purge".
155 * - logger : LoggerInterface object
156 */
157 public function __construct( array $params ) {
158 $this->cache = $params['cache'];
159 $this->procCache = new HashBagOStuff( [ 'maxKeys' => self::MAX_PC_KEYS ] );
160 $this->purgeChannel = isset( $params['channels']['purge'] )
161 ? $params['channels']['purge']
162 : self::DEFAULT_PURGE_CHANNEL;
163 $this->purgeRelayer = isset( $params['relayers']['purge'] )
164 ? $params['relayers']['purge']
165 : new EventRelayerNull( [] );
166 $this->setLogger( isset( $params['logger'] ) ? $params['logger'] : new NullLogger() );
167 }
168
169 public function setLogger( LoggerInterface $logger ) {
170 $this->logger = $logger;
171 }
172
173 /**
174 * Get an instance that wraps EmptyBagOStuff
175 *
176 * @return WANObjectCache
177 */
178 public static function newEmpty() {
179 return new self( [
180 'cache' => new EmptyBagOStuff(),
181 'pool' => 'empty',
182 'relayer' => new EventRelayerNull( [] )
183 ] );
184 }
185
186 /**
187 * Fetch the value of a key from cache
188 *
189 * If supplied, $curTTL is set to the remaining TTL (current time left):
190 * - a) INF; if $key exists, has no TTL, and is not expired by $checkKeys
191 * - b) float (>=0); if $key exists, has a TTL, and is not expired by $checkKeys
192 * - c) float (<0); if $key is tombstoned, stale, or existing but expired by $checkKeys
193 * - d) null; if $key does not exist and is not tombstoned
194 *
195 * If a key is tombstoned, $curTTL will reflect the time since delete().
196 *
197 * The timestamp of $key will be checked against the last-purge timestamp
198 * of each of $checkKeys. Those $checkKeys not in cache will have the last-purge
199 * initialized to the current timestamp. If any of $checkKeys have a timestamp
200 * greater than that of $key, then $curTTL will reflect how long ago $key
201 * became invalid. Callers can use $curTTL to know when the value is stale.
202 * The $checkKeys parameter allow mass invalidations by updating a single key:
203 * - a) Each "check" key represents "last purged" of some source data
204 * - b) Callers pass in relevant "check" keys as $checkKeys in get()
205 * - c) When the source data that "check" keys represent changes,
206 * the touchCheckKey() method is called on them
207 *
208 * Source data entities might exists in a DB that uses snapshot isolation
209 * (e.g. the default REPEATABLE-READ in innoDB). Even for mutable data, that
210 * isolation can largely be maintained by doing the following:
211 * - a) Calling delete() on entity change *and* creation, before DB commit
212 * - b) Keeping transaction duration shorter than delete() hold-off TTL
213 *
214 * However, pre-snapshot values might still be seen if an update was made
215 * in a remote datacenter but the purge from delete() didn't relay yet.
216 *
217 * Consider using getWithSetCallback() instead of get() and set() cycles.
218 * That method has cache slam avoiding features for hot/expensive keys.
219 *
220 * @param string $key Cache key
221 * @param mixed $curTTL Approximate TTL left on the key if present/tombstoned [returned]
222 * @param array $checkKeys List of "check" keys
223 * @param float &$asOf UNIX timestamp of cached value; null on failure [returned]
224 * @return mixed Value of cache key or false on failure
225 */
226 final public function get( $key, &$curTTL = null, array $checkKeys = [], &$asOf = null ) {
227 $curTTLs = [];
228 $asOfs = [];
229 $values = $this->getMulti( [ $key ], $curTTLs, $checkKeys, $asOfs );
230 $curTTL = isset( $curTTLs[$key] ) ? $curTTLs[$key] : null;
231 $asOf = isset( $asOfs[$key] ) ? $asOfs[$key] : null;
232
233 return isset( $values[$key] ) ? $values[$key] : false;
234 }
235
236 /**
237 * Fetch the value of several keys from cache
238 *
239 * @see WANObjectCache::get()
240 *
241 * @param array $keys List of cache keys
242 * @param array $curTTLs Map of (key => approximate TTL left) for existing keys [returned]
243 * @param array $checkKeys List of check keys to apply to all $keys. May also apply "check"
244 * keys to specific cache keys only by using cache keys as keys in the $checkKeys array.
245 * @param float[] &$asOfs Map of (key => UNIX timestamp of cached value; null on failure)
246 * @return array Map of (key => value) for keys that exist
247 */
248 final public function getMulti(
249 array $keys, &$curTTLs = [], array $checkKeys = [], array &$asOfs = []
250 ) {
251 $result = [];
252 $curTTLs = [];
253 $asOfs = [];
254
255 $vPrefixLen = strlen( self::VALUE_KEY_PREFIX );
256 $valueKeys = self::prefixCacheKeys( $keys, self::VALUE_KEY_PREFIX );
257
258 $checkKeysForAll = [];
259 $checkKeysByKey = [];
260 $checkKeysFlat = [];
261 foreach ( $checkKeys as $i => $keys ) {
262 $prefixed = self::prefixCacheKeys( (array)$keys, self::TIME_KEY_PREFIX );
263 $checkKeysFlat = array_merge( $checkKeysFlat, $prefixed );
264 // Is this check keys for a specific cache key, or for all keys being fetched?
265 if ( is_int( $i ) ) {
266 $checkKeysForAll = array_merge( $checkKeysForAll, $prefixed );
267 } else {
268 $checkKeysByKey[$i] = isset( $checkKeysByKey[$i] )
269 ? array_merge( $checkKeysByKey[$i], $prefixed )
270 : $prefixed;
271 }
272 }
273
274 // Fetch all of the raw values
275 $wrappedValues = $this->cache->getMulti( array_merge( $valueKeys, $checkKeysFlat ) );
276 // Time used to compare/init "check" keys (derived after getMulti() to be pessimistic)
277 $now = microtime( true );
278
279 // Collect timestamps from all "check" keys
280 $purgeValuesForAll = $this->processCheckKeys( $checkKeysForAll, $wrappedValues, $now );
281 $purgeValuesByKey = [];
282 foreach ( $checkKeysByKey as $cacheKey => $checks ) {
283 $purgeValuesByKey[$cacheKey] =
284 $this->processCheckKeys( $checks, $wrappedValues, $now );
285 }
286
287 // Get the main cache value for each key and validate them
288 foreach ( $valueKeys as $vKey ) {
289 if ( !isset( $wrappedValues[$vKey] ) ) {
290 continue; // not found
291 }
292
293 $key = substr( $vKey, $vPrefixLen ); // unprefix
294
295 list( $value, $curTTL ) = $this->unwrap( $wrappedValues[$vKey], $now );
296 if ( $value !== false ) {
297 $result[$key] = $value;
298
299 // Force dependant keys to be invalid for a while after purging
300 // to reduce race conditions involving stale data getting cached
301 $purgeValues = $purgeValuesForAll;
302 if ( isset( $purgeValuesByKey[$key] ) ) {
303 $purgeValues = array_merge( $purgeValues, $purgeValuesByKey[$key] );
304 }
305 foreach ( $purgeValues as $purge ) {
306 $safeTimestamp = $purge[self::FLD_TIME] + $purge[self::FLD_HOLDOFF];
307 if ( $safeTimestamp >= $wrappedValues[$vKey][self::FLD_TIME] ) {
308 // How long ago this value was expired by *this* check key
309 $ago = min( $purge[self::FLD_TIME] - $now, self::TINY_NEGATIVE );
310 // How long ago this value was expired by *any* known check key
311 $curTTL = min( $curTTL, $ago );
312 }
313 }
314 }
315 $curTTLs[$key] = $curTTL;
316 $asOfs[$key] = ( $value !== false ) ? $wrappedValues[$vKey][self::FLD_TIME] : null;
317 }
318
319 return $result;
320 }
321
322 /**
323 * @since 1.27
324 * @param array $timeKeys List of prefixed time check keys
325 * @param array $wrappedValues
326 * @param float $now
327 * @return array List of purge value arrays
328 */
329 private function processCheckKeys( array $timeKeys, array $wrappedValues, $now ) {
330 $purgeValues = [];
331 foreach ( $timeKeys as $timeKey ) {
332 $purge = isset( $wrappedValues[$timeKey] )
333 ? self::parsePurgeValue( $wrappedValues[$timeKey] )
334 : false;
335 if ( $purge === false ) {
336 // Key is not set or invalid; regenerate
337 $newVal = $this->makePurgeValue( $now, self::HOLDOFF_TTL );
338 $this->cache->add( $timeKey, $newVal, self::CHECK_KEY_TTL );
339 $purge = self::parsePurgeValue( $newVal );
340 }
341 $purgeValues[] = $purge;
342 }
343 return $purgeValues;
344 }
345
346 /**
347 * Set the value of a key in cache
348 *
349 * Simply calling this method when source data changes is not valid because
350 * the changes do not replicate to the other WAN sites. In that case, delete()
351 * should be used instead. This method is intended for use on cache misses.
352 *
353 * If the data was read from a snapshot-isolated transactions (e.g. the default
354 * REPEATABLE-READ in innoDB), use 'since' to avoid the following race condition:
355 * - a) T1 starts
356 * - b) T2 updates a row, calls delete(), and commits
357 * - c) The HOLDOFF_TTL passes, expiring the delete() tombstone
358 * - d) T1 reads the row and calls set() due to a cache miss
359 * - e) Stale value is stuck in cache
360 *
361 * Setting 'lag' and 'since' help avoids keys getting stuck in stale states.
362 *
363 * Example usage:
364 * @code
365 * $dbr = wfGetDB( DB_SLAVE );
366 * $setOpts = Database::getCacheSetOptions( $dbr );
367 * // Fetch the row from the DB
368 * $row = $dbr->selectRow( ... );
369 * $key = $cache->makeKey( 'building', $buildingId );
370 * $cache->set( $key, $row, $cache::TTL_DAY, $setOpts );
371 * @endcode
372 *
373 * @param string $key Cache key
374 * @param mixed $value
375 * @param integer $ttl Seconds to live. Special values are:
376 * - WANObjectCache::TTL_INDEFINITE: Cache forever
377 * @param array $opts Options map:
378 * - lag : Seconds of slave lag. Typically, this is either the slave lag
379 * before the data was read or, if applicable, the slave lag before
380 * the snapshot-isolated transaction the data was read from started.
381 * Default: 0 seconds
382 * - since : UNIX timestamp of the data in $value. Typically, this is either
383 * the current time the data was read or (if applicable) the time when
384 * the snapshot-isolated transaction the data was read from started.
385 * Default: 0 seconds
386 * - pending : Whether this data is possibly from an uncommitted write transaction.
387 * Generally, other threads should not see values from the future and
388 * they certainly should not see ones that ended up getting rolled back.
389 * Default: false
390 * - lockTSE : if excessive replication/snapshot lag is detected, then store the value
391 * with this TTL and flag it as stale. This is only useful if the reads for
392 * this key use getWithSetCallback() with "lockTSE" set.
393 * Default: WANObjectCache::TSE_NONE
394 * @return bool Success
395 */
396 final public function set( $key, $value, $ttl = 0, array $opts = [] ) {
397 $now = microtime( true );
398 $lockTSE = isset( $opts['lockTSE'] ) ? $opts['lockTSE'] : self::TSE_NONE;
399 $age = isset( $opts['since'] ) ? max( 0, $now - $opts['since'] ) : 0;
400 $lag = isset( $opts['lag'] ) ? $opts['lag'] : 0;
401
402 // Do not cache potentially uncommitted data as it might get rolled back
403 if ( !empty( $opts['pending'] ) ) {
404 $this->logger->info( "Rejected set() for $key due to pending writes." );
405
406 return true; // no-op the write for being unsafe
407 }
408
409 $wrapExtra = []; // additional wrapped value fields
410 // Check if there's a risk of writing stale data after the purge tombstone expired
411 if ( $lag === false || ( $lag + $age ) > self::MAX_READ_LAG ) {
412 // Case A: read lag with "lockTSE"; save but record value as stale
413 if ( $lockTSE >= 0 ) {
414 $ttl = max( 1, (int)$lockTSE ); // set() expects seconds
415 $wrapExtra[self::FLD_FLAGS] = self::FLG_STALE; // mark as stale
416 // Case B: any long-running transaction; ignore this set()
417 } elseif ( $age > self::MAX_READ_LAG ) {
418 $this->logger->warning( "Rejected set() for $key due to snapshot lag." );
419
420 return true; // no-op the write for being unsafe
421 // Case C: high replication lag; lower TTL instead of ignoring all set()s
422 } elseif ( $lag === false || $lag > self::MAX_READ_LAG ) {
423 $ttl = $ttl ? min( $ttl, self::TTL_LAGGED ) : self::TTL_LAGGED;
424 $this->logger->warning( "Lowered set() TTL for $key due to replication lag." );
425 // Case D: medium length request with medium replication lag; ignore this set()
426 } else {
427 $this->logger->warning( "Rejected set() for $key due to high read lag." );
428
429 return true; // no-op the write for being unsafe
430 }
431 }
432
433 // Wrap that value with time/TTL/version metadata
434 $wrapped = $this->wrap( $value, $ttl, $now ) + $wrapExtra;
435
436 $func = function ( $cache, $key, $cWrapped ) use ( $wrapped ) {
437 return ( is_string( $cWrapped ) )
438 ? false // key is tombstoned; do nothing
439 : $wrapped;
440 };
441
442 return $this->cache->merge( self::VALUE_KEY_PREFIX . $key, $func, $ttl, 1 );
443 }
444
445 /**
446 * Purge a key from all datacenters
447 *
448 * This should only be called when the underlying data (being cached)
449 * changes in a significant way. This deletes the key and starts a hold-off
450 * period where the key cannot be written to for a few seconds (HOLDOFF_TTL).
451 * This is done to avoid the following race condition:
452 * - a) Some DB data changes and delete() is called on a corresponding key
453 * - b) A request refills the key with a stale value from a lagged DB
454 * - c) The stale value is stuck there until the key is expired/evicted
455 *
456 * This is implemented by storing a special "tombstone" value at the cache
457 * key that this class recognizes; get() calls will return false for the key
458 * and any set() calls will refuse to replace tombstone values at the key.
459 * For this to always avoid stale value writes, the following must hold:
460 * - a) Replication lag is bounded to being less than HOLDOFF_TTL; or
461 * - b) If lag is higher, the DB will have gone into read-only mode already
462 *
463 * Note that set() can also be lag-aware and lower the TTL if it's high.
464 *
465 * When using potentially long-running ACID transactions, a good pattern is
466 * to use a pre-commit hook to issue the delete. This means that immediately
467 * after commit, callers will see the tombstone in cache upon purge relay.
468 * It also avoids the following race condition:
469 * - a) T1 begins, changes a row, and calls delete()
470 * - b) The HOLDOFF_TTL passes, expiring the delete() tombstone
471 * - c) T2 starts, reads the row and calls set() due to a cache miss
472 * - d) T1 finally commits
473 * - e) Stale value is stuck in cache
474 *
475 * Example usage:
476 * @code
477 * $dbw->begin( __METHOD__ ); // start of request
478 * ... <execute some stuff> ...
479 * // Update the row in the DB
480 * $dbw->update( ... );
481 * $key = $cache->makeKey( 'homes', $homeId );
482 * // Purge the corresponding cache entry just before committing
483 * $dbw->onTransactionPreCommitOrIdle( function() use ( $cache, $key ) {
484 * $cache->delete( $key );
485 * } );
486 * ... <execute some stuff> ...
487 * $dbw->commit( __METHOD__ ); // end of request
488 * @endcode
489 *
490 * The $ttl parameter can be used when purging values that have not actually changed
491 * recently. For example, a cleanup script to purge cache entries does not really need
492 * a hold-off period, so it can use HOLDOFF_NONE. Likewise for user-requested purge.
493 * Note that $ttl limits the effective range of 'lockTSE' for getWithSetCallback().
494 *
495 * If called twice on the same key, then the last hold-off TTL takes precedence. For
496 * idempotence, the $ttl should not vary for different delete() calls on the same key.
497 *
498 * @param string $key Cache key
499 * @param integer $ttl Tombstone TTL; Default: WANObjectCache::HOLDOFF_TTL
500 * @return bool True if the item was purged or not found, false on failure
501 */
502 final public function delete( $key, $ttl = self::HOLDOFF_TTL ) {
503 $key = self::VALUE_KEY_PREFIX . $key;
504
505 if ( $ttl <= 0 ) {
506 // Publish the purge to all datacenters
507 $ok = $this->relayDelete( $key );
508 } else {
509 // Publish the purge to all datacenters
510 $ok = $this->relayPurge( $key, $ttl, self::HOLDOFF_NONE );
511 }
512
513 return $ok;
514 }
515
516 /**
517 * Fetch the value of a timestamp "check" key
518 *
519 * The key will be *initialized* to the current time if not set,
520 * so only call this method if this behavior is actually desired
521 *
522 * The timestamp can be used to check whether a cached value is valid.
523 * Callers should not assume that this returns the same timestamp in
524 * all datacenters due to relay delays.
525 *
526 * The level of staleness can roughly be estimated from this key, but
527 * if the key was evicted from cache, such calculations may show the
528 * time since expiry as ~0 seconds.
529 *
530 * Note that "check" keys won't collide with other regular keys.
531 *
532 * @param string $key
533 * @return float UNIX timestamp of the check key
534 */
535 final public function getCheckKeyTime( $key ) {
536 $key = self::TIME_KEY_PREFIX . $key;
537
538 $purge = self::parsePurgeValue( $this->cache->get( $key ) );
539 if ( $purge !== false ) {
540 $time = $purge[self::FLD_TIME];
541 } else {
542 // Casting assures identical floats for the next getCheckKeyTime() calls
543 $now = (string)microtime( true );
544 $this->cache->add( $key,
545 $this->makePurgeValue( $now, self::HOLDOFF_TTL ),
546 self::CHECK_KEY_TTL
547 );
548 $time = (float)$now;
549 }
550
551 return $time;
552 }
553
554 /**
555 * Purge a "check" key from all datacenters, invalidating keys that use it
556 *
557 * This should only be called when the underlying data (being cached)
558 * changes in a significant way, and it is impractical to call delete()
559 * on all keys that should be changed. When get() is called on those
560 * keys, the relevant "check" keys must be supplied for this to work.
561 *
562 * The "check" key essentially represents a last-modified field.
563 * When touched, the field will be updated on all cache servers.
564 * Keys using it via get(), getMulti(), or getWithSetCallback() will
565 * be invalidated. It is treated as being HOLDOFF_TTL seconds in the future
566 * by those methods to avoid race conditions where dependent keys get updated
567 * with stale values (e.g. from a DB slave).
568 *
569 * This is typically useful for keys with hardcoded names or in some cases
570 * dynamically generated names where a low number of combinations exist.
571 * When a few important keys get a large number of hits, a high cache
572 * time is usually desired as well as "lockTSE" logic. The resetCheckKey()
573 * method is less appropriate in such cases since the "time since expiry"
574 * cannot be inferred, causing any get() after the reset to treat the key
575 * as being "hot", resulting in more stale value usage.
576 *
577 * Note that "check" keys won't collide with other regular keys.
578 *
579 * @see WANObjectCache::get()
580 * @see WANObjectCache::getWithSetCallback()
581 * @see WANObjectCache::resetCheckKey()
582 *
583 * @param string $key Cache key
584 * @param int $holdoff HOLDOFF_TTL or HOLDOFF_NONE constant
585 * @return bool True if the item was purged or not found, false on failure
586 */
587 final public function touchCheckKey( $key, $holdoff = self::HOLDOFF_TTL ) {
588 // Publish the purge to all datacenters
589 return $this->relayPurge( self::TIME_KEY_PREFIX . $key, self::CHECK_KEY_TTL, $holdoff );
590 }
591
592 /**
593 * Delete a "check" key from all datacenters, invalidating keys that use it
594 *
595 * This is similar to touchCheckKey() in that keys using it via get(), getMulti(),
596 * or getWithSetCallback() will be invalidated. The differences are:
597 * - a) The "check" key will be deleted from all caches and lazily
598 * re-initialized when accessed (rather than set everywhere)
599 * - b) Thus, dependent keys will be known to be invalid, but not
600 * for how long (they are treated as "just" purged), which
601 * effects any lockTSE logic in getWithSetCallback()
602 * - c) Since "check" keys are initialized only on the server the key hashes
603 * to, any temporary ejection of that server will cause the value to be
604 * seen as purged as a new server will initialize the "check" key.
605 *
606 * The advantage is that this does not place high TTL keys on every cache
607 * server, making it better for code that will cache many different keys
608 * and either does not use lockTSE or uses a low enough TTL anyway.
609 *
610 * This is typically useful for keys with dynamically generated names
611 * where a high number of combinations exist.
612 *
613 * Note that "check" keys won't collide with other regular keys.
614 *
615 * @see WANObjectCache::get()
616 * @see WANObjectCache::getWithSetCallback()
617 * @see WANObjectCache::touchCheckKey()
618 *
619 * @param string $key Cache key
620 * @return bool True if the item was purged or not found, false on failure
621 */
622 final public function resetCheckKey( $key ) {
623 // Publish the purge to all datacenters
624 return $this->relayDelete( self::TIME_KEY_PREFIX . $key );
625 }
626
627 /**
628 * Method to fetch/regenerate cache keys
629 *
630 * On cache miss, the key will be set to the callback result via set()
631 * (unless the callback returns false) and that result will be returned.
632 * The arguments supplied to the callback are:
633 * - $oldValue : current cache value or false if not present
634 * - &$ttl : a reference to the TTL which can be altered
635 * - &$setOpts : a reference to options for set() which can be altered
636 *
637 * It is strongly recommended to set the 'lag' and 'since' fields to avoid race conditions
638 * that can cause stale values to get stuck at keys. Usually, callbacks ignore the current
639 * value, but it can be used to maintain "most recent X" values that come from time or
640 * sequence based source data, provided that the "as of" id/time is tracked. Note that
641 * preemptive regeneration and $checkKeys can result in a non-false current value.
642 *
643 * Usage of $checkKeys is similar to get() and getMulti(). However, rather than the caller
644 * having to inspect a "current time left" variable (e.g. $curTTL, $curTTLs), a cache
645 * regeneration will automatically be triggered using the callback.
646 *
647 * The simplest way to avoid stampedes for hot keys is to use
648 * the 'lockTSE' option in $opts. If cache purges are needed, also:
649 * - a) Pass $key into $checkKeys
650 * - b) Use touchCheckKey( $key ) instead of delete( $key )
651 *
652 * Example usage (typical key):
653 * @code
654 * $catInfo = $cache->getWithSetCallback(
655 * // Key to store the cached value under
656 * $cache->makeKey( 'cat-attributes', $catId ),
657 * // Time-to-live (in seconds)
658 * $cache::TTL_MINUTE,
659 * // Function that derives the new key value
660 * function ( $oldValue, &$ttl, array &$setOpts ) {
661 * $dbr = wfGetDB( DB_SLAVE );
662 * // Account for any snapshot/slave lag
663 * $setOpts += Database::getCacheSetOptions( $dbr );
664 *
665 * return $dbr->selectRow( ... );
666 * }
667 * );
668 * @endcode
669 *
670 * Example usage (key that is expensive and hot):
671 * @code
672 * $catConfig = $cache->getWithSetCallback(
673 * // Key to store the cached value under
674 * $cache->makeKey( 'site-cat-config' ),
675 * // Time-to-live (in seconds)
676 * $cache::TTL_DAY,
677 * // Function that derives the new key value
678 * function ( $oldValue, &$ttl, array &$setOpts ) {
679 * $dbr = wfGetDB( DB_SLAVE );
680 * // Account for any snapshot/slave lag
681 * $setOpts += Database::getCacheSetOptions( $dbr );
682 *
683 * return CatConfig::newFromRow( $dbr->selectRow( ... ) );
684 * },
685 * [
686 * // Calling touchCheckKey() on this key invalidates the cache
687 * 'checkKeys' => [ $cache->makeKey( 'site-cat-config' ) ],
688 * // Try to only let one datacenter thread manage cache updates at a time
689 * 'lockTSE' => 30
690 * ]
691 * );
692 * @endcode
693 *
694 * Example usage (key with dynamic dependencies):
695 * @code
696 * $catState = $cache->getWithSetCallback(
697 * // Key to store the cached value under
698 * $cache->makeKey( 'cat-state', $cat->getId() ),
699 * // Time-to-live (seconds)
700 * $cache::TTL_HOUR,
701 * // Function that derives the new key value
702 * function ( $oldValue, &$ttl, array &$setOpts ) {
703 * // Determine new value from the DB
704 * $dbr = wfGetDB( DB_SLAVE );
705 * // Account for any snapshot/slave lag
706 * $setOpts += Database::getCacheSetOptions( $dbr );
707 *
708 * return CatState::newFromResults( $dbr->select( ... ) );
709 * },
710 * [
711 * // The "check" keys that represent things the value depends on;
712 * // Calling touchCheckKey() on any of them invalidates the cache
713 * 'checkKeys' => [
714 * $cache->makeKey( 'sustenance-bowls', $cat->getRoomId() ),
715 * $cache->makeKey( 'people-present', $cat->getHouseId() ),
716 * $cache->makeKey( 'cat-laws', $cat->getCityId() ),
717 * ]
718 * ]
719 * );
720 * @endcode
721 *
722 * Example usage (hot key holding most recent 100 events):
723 * @code
724 * $lastCatActions = $cache->getWithSetCallback(
725 * // Key to store the cached value under
726 * $cache->makeKey( 'cat-last-actions', 100 ),
727 * // Time-to-live (in seconds)
728 * 10,
729 * // Function that derives the new key value
730 * function ( $oldValue, &$ttl, array &$setOpts ) {
731 * $dbr = wfGetDB( DB_SLAVE );
732 * // Account for any snapshot/slave lag
733 * $setOpts += Database::getCacheSetOptions( $dbr );
734 *
735 * // Start off with the last cached list
736 * $list = $oldValue ?: [];
737 * // Fetch the last 100 relevant rows in descending order;
738 * // only fetch rows newer than $list[0] to reduce scanning
739 * $rows = iterator_to_array( $dbr->select( ... ) );
740 * // Merge them and get the new "last 100" rows
741 * return array_slice( array_merge( $new, $list ), 0, 100 );
742 * },
743 * // Try to only let one datacenter thread manage cache updates at a time
744 * [ 'lockTSE' => 30 ]
745 * );
746 * @endcode
747 *
748 * @see WANObjectCache::get()
749 * @see WANObjectCache::set()
750 *
751 * @param string $key Cache key
752 * @param integer $ttl Seconds to live for key updates. Special values are:
753 * - WANObjectCache::TTL_INDEFINITE: Cache forever
754 * - WANObjectCache::TTL_UNCACHEABLE: Do not cache at all
755 * @param callable $callback Value generation function
756 * @param array $opts Options map:
757 * - checkKeys: List of "check" keys. The key at $key will be seen as invalid when either
758 * touchCheckKey() or resetCheckKey() is called on any of these keys.
759 * Default: [].
760 * - lowTTL: Consider pre-emptive updates when the current TTL (seconds) of the key is less
761 * than this. It becomes more likely over time, becoming certain once the key is expired.
762 * Default: WANObjectCache::LOW_TTL.
763 * - lockTSE: If the key is tombstoned or expired (by checkKeys) less than this many seconds
764 * ago, then try to have a single thread handle cache regeneration at any given time.
765 * Other threads will try to use stale values if possible. If, on miss, the time since
766 * expiration is low, the assumption is that the key is hot and that a stampede is worth
767 * avoiding. Setting this above WANObjectCache::HOLDOFF_TTL makes no difference. The
768 * higher this is set, the higher the worst-case staleness can be.
769 * Use WANObjectCache::TSE_NONE to disable this logic.
770 * Default: WANObjectCache::TSE_NONE.
771 * - busyValue: If no value exists and another thread is currently regenerating it, use this
772 * as a fallback value (or a callback to generate such a value). This assures that cache
773 * stampedes cannot happen if the value falls out of cache. This can be used as insurance
774 * against cache regeneration becoming very slow for some reason (greater than the TTL).
775 * Default: null.
776 * - pcTTL: Process cache the value in this PHP instance for this many seconds. This avoids
777 * network I/O when a key is read several times. This will not cache when the callback
778 * returns false, however. Note that any purges will not be seen while process cached;
779 * since the callback should use slave DBs and they may be lagged or have snapshot
780 * isolation anyway, this should not typically matter.
781 * Default: WANObjectCache::TTL_UNCACHEABLE.
782 * - version: Integer version number. This allows for callers to make breaking changes to
783 * how values are stored while maintaining compatability and correct cache purges. New
784 * versions are stored alongside older versions concurrently. Avoid storing class objects
785 * however, as this reduces compatibility (due to serialization).
786 * Default: null.
787 * @return mixed Value found or written to the key
788 * @note Callable type hints are not used to avoid class-autoloading
789 */
790 final public function getWithSetCallback( $key, $ttl, $callback, array $opts = [] ) {
791 $pcTTL = isset( $opts['pcTTL'] ) ? $opts['pcTTL'] : self::TTL_UNCACHEABLE;
792
793 // Try the process cache if enabled
794 $value = ( $pcTTL >= 0 ) ? $this->procCache->get( $key ) : false;
795
796 if ( $value === false ) {
797 unset( $opts['minTime'] ); // not a public feature
798
799 // Fetch the value over the network
800 if ( isset( $opts['version'] ) ) {
801 $version = $opts['version'];
802 $asOf = null;
803 $cur = $this->doGetWithSetCallback(
804 $key,
805 $ttl,
806 function ( $oldValue, &$ttl, &$setOpts ) use ( $callback, $version ) {
807 if ( is_array( $oldValue )
808 && array_key_exists( self::VFLD_DATA, $oldValue )
809 ) {
810 $oldData = $oldValue[self::VFLD_DATA];
811 } else {
812 // VFLD_DATA is not set if an old, unversioned, key is present
813 $oldData = false;
814 }
815
816 return [
817 self::VFLD_DATA => $callback( $oldData, $ttl, $setOpts ),
818 self::VFLD_VERSION => $version
819 ];
820 },
821 $opts,
822 $asOf
823 );
824 if ( $cur[self::VFLD_VERSION] === $version ) {
825 // Value created or existed before with version; use it
826 $value = $cur[self::VFLD_DATA];
827 } else {
828 // Value existed before with a different version; use variant key.
829 // Reflect purges to $key by requiring that this key value be newer.
830 $value = $this->doGetWithSetCallback(
831 'cache-variant:' . md5( $key ) . ":$version",
832 $ttl,
833 $callback,
834 // Regenerate value if not newer than $key
835 [ 'version' => null, 'minTime' => $asOf ] + $opts
836 );
837 }
838 } else {
839 $value = $this->doGetWithSetCallback( $key, $ttl, $callback, $opts );
840 }
841
842 // Update the process cache if enabled
843 if ( $pcTTL >= 0 && $value !== false ) {
844 $this->procCache->set( $key, $value, $pcTTL );
845 }
846 }
847
848 return $value;
849 }
850
851 /**
852 * Do the actual I/O for getWithSetCallback() when needed
853 *
854 * @see WANObjectCache::getWithSetCallback()
855 *
856 * @param string $key
857 * @param integer $ttl
858 * @param callback $callback
859 * @param array $opts Options map for getWithSetCallback() which also includes:
860 * - minTime: Treat values older than this UNIX timestamp as not existing. Default: null.
861 * @param float &$asOf Cache generation timestamp of returned value [returned]
862 * @return mixed
863 * @note Callable type hints are not used to avoid class-autoloading
864 */
865 protected function doGetWithSetCallback( $key, $ttl, $callback, array $opts, &$asOf = null ) {
866 $lowTTL = isset( $opts['lowTTL'] ) ? $opts['lowTTL'] : min( self::LOW_TTL, $ttl );
867 $lockTSE = isset( $opts['lockTSE'] ) ? $opts['lockTSE'] : self::TSE_NONE;
868 $checkKeys = isset( $opts['checkKeys'] ) ? $opts['checkKeys'] : [];
869 $busyValue = isset( $opts['busyValue'] ) ? $opts['busyValue'] : null;
870 $minTime = isset( $opts['minTime'] ) ? $opts['minTime'] : 0.0;
871 $versioned = isset( $opts['version'] );
872
873 // Get the current key value
874 $curTTL = null;
875 $cValue = $this->get( $key, $curTTL, $checkKeys, $asOf ); // current value
876 $value = $cValue; // return value
877
878 // Determine if a regeneration is desired
879 if ( $value !== false
880 && $curTTL > 0
881 && $this->isValid( $value, $versioned, $asOf, $minTime )
882 && !$this->worthRefresh( $curTTL, $lowTTL )
883 ) {
884 return $value;
885 }
886
887 // A deleted key with a negative TTL left must be tombstoned
888 $isTombstone = ( $curTTL !== null && $value === false );
889 // Assume a key is hot if requested soon after invalidation
890 $isHot = ( $curTTL !== null && $curTTL <= 0 && abs( $curTTL ) <= $lockTSE );
891 // Use the mutex if there is no value and a busy fallback is given
892 $checkBusy = ( $busyValue !== null && $value === false );
893 // Decide whether a single thread should handle regenerations.
894 // This avoids stampedes when $checkKeys are bumped and when preemptive
895 // renegerations take too long. It also reduces regenerations while $key
896 // is tombstoned. This balances cache freshness with avoiding DB load.
897 $useMutex = ( $isHot || ( $isTombstone && $lockTSE > 0 ) || $checkBusy );
898
899 $lockAcquired = false;
900 if ( $useMutex ) {
901 // Acquire a datacenter-local non-blocking lock
902 if ( $this->cache->add( self::MUTEX_KEY_PREFIX . $key, 1, self::LOCK_TTL ) ) {
903 // Lock acquired; this thread should update the key
904 $lockAcquired = true;
905 } elseif ( $value !== false && $this->isValid( $value, $versioned, $asOf, $minTime ) ) {
906 // If it cannot be acquired; then the stale value can be used
907 return $value;
908 } else {
909 // Use the INTERIM value for tombstoned keys to reduce regeneration load.
910 // For hot keys, either another thread has the lock or the lock failed;
911 // use the INTERIM value from the last thread that regenerated it.
912 $wrapped = $this->cache->get( self::INTERIM_KEY_PREFIX . $key );
913 list( $value ) = $this->unwrap( $wrapped, microtime( true ) );
914 if ( $value !== false && $this->isValid( $value, $versioned, $asOf, $minTime ) ) {
915 $asOf = $wrapped[self::FLD_TIME];
916
917 return $value;
918 }
919 // Use the busy fallback value if nothing else
920 if ( $busyValue !== null ) {
921 return is_callable( $busyValue ) ? $busyValue() : $busyValue;
922 }
923 }
924 }
925
926 if ( !is_callable( $callback ) ) {
927 throw new InvalidArgumentException( "Invalid cache miss callback provided." );
928 }
929
930 // Generate the new value from the callback...
931 $setOpts = [];
932 $value = call_user_func_array( $callback, [ $cValue, &$ttl, &$setOpts ] );
933 $asOf = microtime( true );
934 // When delete() is called, writes are write-holed by the tombstone,
935 // so use a special INTERIM key to pass the new value around threads.
936 if ( ( $isTombstone && $lockTSE > 0 ) && $value !== false && $ttl >= 0 ) {
937 $tempTTL = max( 1, (int)$lockTSE ); // set() expects seconds
938 $wrapped = $this->wrap( $value, $tempTTL, $asOf );
939 // Avoid using set() to avoid pointless mcrouter broadcasting
940 $this->cache->merge(
941 self::INTERIM_KEY_PREFIX . $key,
942 function () use ( $wrapped ) {
943 return $wrapped;
944 },
945 $tempTTL,
946 1
947 );
948 }
949
950 if ( $value !== false && $ttl >= 0 ) {
951 // Update the cache; this will fail if the key is tombstoned
952 $setOpts['lockTSE'] = $lockTSE;
953 $this->set( $key, $value, $ttl, $setOpts );
954 }
955
956 if ( $lockAcquired ) {
957 // Avoid using delete() to avoid pointless mcrouter broadcasting
958 $this->cache->changeTTL( self::MUTEX_KEY_PREFIX . $key, 1 );
959 }
960
961 return $value;
962 }
963
964 /**
965 * @see BagOStuff::makeKey()
966 * @param string ... Key component
967 * @return string
968 * @since 1.27
969 */
970 public function makeKey() {
971 return call_user_func_array( [ $this->cache, __FUNCTION__ ], func_get_args() );
972 }
973
974 /**
975 * @see BagOStuff::makeGlobalKey()
976 * @param string ... Key component
977 * @return string
978 * @since 1.27
979 */
980 public function makeGlobalKey() {
981 return call_user_func_array( [ $this->cache, __FUNCTION__ ], func_get_args() );
982 }
983
984 /**
985 * Get the "last error" registered; clearLastError() should be called manually
986 * @return int ERR_* class constant for the "last error" registry
987 */
988 final public function getLastError() {
989 if ( $this->lastRelayError ) {
990 // If the cache and the relayer failed, focus on the later.
991 // An update not making it to the relayer means it won't show up
992 // in other DCs (nor will consistent re-hashing see up-to-date values).
993 // On the other hand, if just the cache update failed, then it should
994 // eventually be applied by the relayer.
995 return $this->lastRelayError;
996 }
997
998 $code = $this->cache->getLastError();
999 switch ( $code ) {
1000 case BagOStuff::ERR_NONE:
1001 return self::ERR_NONE;
1002 case BagOStuff::ERR_NO_RESPONSE:
1003 return self::ERR_NO_RESPONSE;
1004 case BagOStuff::ERR_UNREACHABLE:
1005 return self::ERR_UNREACHABLE;
1006 default:
1007 return self::ERR_UNEXPECTED;
1008 }
1009 }
1010
1011 /**
1012 * Clear the "last error" registry
1013 */
1014 final public function clearLastError() {
1015 $this->cache->clearLastError();
1016 $this->lastRelayError = self::ERR_NONE;
1017 }
1018
1019 /**
1020 * Clear the in-process caches; useful for testing
1021 *
1022 * @since 1.27
1023 */
1024 public function clearProcessCache() {
1025 $this->procCache->clear();
1026 }
1027
1028 /**
1029 * @param integer $flag ATTR_* class constant
1030 * @return integer QOS_* class constant
1031 * @since 1.28
1032 */
1033 public function getQoS( $flag ) {
1034 return $this->cache->getQoS( $flag );
1035 }
1036
1037 /**
1038 * Do the actual async bus purge of a key
1039 *
1040 * This must set the key to "PURGED:<UNIX timestamp>:<holdoff>"
1041 *
1042 * @param string $key Cache key
1043 * @param integer $ttl How long to keep the tombstone [seconds]
1044 * @param integer $holdoff HOLDOFF_* constant controlling how long to ignore sets for this key
1045 * @return bool Success
1046 */
1047 protected function relayPurge( $key, $ttl, $holdoff ) {
1048 if ( $this->purgeRelayer instanceof EventRelayerNull ) {
1049 // This handles the mcrouter and the single-DC case
1050 $ok = $this->cache->set( $key,
1051 $this->makePurgeValue( microtime( true ), self::HOLDOFF_NONE ),
1052 $ttl
1053 );
1054 } else {
1055 $event = $this->cache->modifySimpleRelayEvent( [
1056 'cmd' => 'set',
1057 'key' => $key,
1058 'val' => 'PURGED:$UNIXTIME$:' . (int)$holdoff,
1059 'ttl' => max( $ttl, 1 ),
1060 'sbt' => true, // substitute $UNIXTIME$ with actual microtime
1061 ] );
1062
1063 $ok = $this->purgeRelayer->notify( $this->purgeChannel, $event );
1064 if ( !$ok ) {
1065 $this->lastRelayError = self::ERR_RELAY;
1066 }
1067 }
1068
1069 return $ok;
1070 }
1071
1072 /**
1073 * Do the actual async bus delete of a key
1074 *
1075 * @param string $key Cache key
1076 * @return bool Success
1077 */
1078 protected function relayDelete( $key ) {
1079 if ( $this->purgeRelayer instanceof EventRelayerNull ) {
1080 // This handles the mcrouter and the single-DC case
1081 $ok = $this->cache->delete( $key );
1082 } else {
1083 $event = $this->cache->modifySimpleRelayEvent( [
1084 'cmd' => 'delete',
1085 'key' => $key,
1086 ] );
1087
1088 $ok = $this->purgeRelayer->notify( $this->purgeChannel, $event );
1089 if ( !$ok ) {
1090 $this->lastRelayError = self::ERR_RELAY;
1091 }
1092 }
1093
1094 return $ok;
1095 }
1096
1097 /**
1098 * Check if a key should be regenerated (using random probability)
1099 *
1100 * This returns false if $curTTL >= $lowTTL. Otherwise, the chance
1101 * of returning true increases steadily from 0% to 100% as the $curTTL
1102 * moves from $lowTTL to 0 seconds. This handles widely varying
1103 * levels of cache access traffic.
1104 *
1105 * @param float $curTTL Approximate TTL left on the key if present
1106 * @param float $lowTTL Consider a refresh when $curTTL is less than this
1107 * @return bool
1108 */
1109 protected function worthRefresh( $curTTL, $lowTTL ) {
1110 if ( $curTTL >= $lowTTL ) {
1111 return false;
1112 } elseif ( $curTTL <= 0 ) {
1113 return true;
1114 }
1115
1116 $chance = ( 1 - $curTTL / $lowTTL );
1117
1118 return mt_rand( 1, 1e9 ) <= 1e9 * $chance;
1119 }
1120
1121 /**
1122 * Check whether $value is appropriately versioned and not older than $minTime (if set)
1123 *
1124 * @param array $value
1125 * @param bool $versioned
1126 * @param float $asOf The time $value was generated
1127 * @param float $minTime The last time the main value was generated (0.0 if unknown)
1128 * @return bool
1129 */
1130 protected function isValid( $value, $versioned, $asOf, $minTime ) {
1131 if ( $versioned && !isset( $value[self::VFLD_VERSION] ) ) {
1132 return false;
1133 } elseif ( $minTime > 0 && $asOf < $minTime ) {
1134 return false;
1135 }
1136
1137 return true;
1138 }
1139
1140 /**
1141 * Do not use this method outside WANObjectCache
1142 *
1143 * @param mixed $value
1144 * @param integer $ttl [0=forever]
1145 * @param float $now Unix Current timestamp just before calling set()
1146 * @return array
1147 */
1148 protected function wrap( $value, $ttl, $now ) {
1149 return [
1150 self::FLD_VERSION => self::VERSION,
1151 self::FLD_VALUE => $value,
1152 self::FLD_TTL => $ttl,
1153 self::FLD_TIME => $now
1154 ];
1155 }
1156
1157 /**
1158 * Do not use this method outside WANObjectCache
1159 *
1160 * @param array|string|bool $wrapped
1161 * @param float $now Unix Current timestamp (preferrably pre-query)
1162 * @return array (mixed; false if absent/invalid, current time left)
1163 */
1164 protected function unwrap( $wrapped, $now ) {
1165 // Check if the value is a tombstone
1166 $purge = self::parsePurgeValue( $wrapped );
1167 if ( $purge !== false ) {
1168 // Purged values should always have a negative current $ttl
1169 $curTTL = min( $purge[self::FLD_TIME] - $now, self::TINY_NEGATIVE );
1170 return [ false, $curTTL ];
1171 }
1172
1173 if ( !is_array( $wrapped ) // not found
1174 || !isset( $wrapped[self::FLD_VERSION] ) // wrong format
1175 || $wrapped[self::FLD_VERSION] !== self::VERSION // wrong version
1176 ) {
1177 return [ false, null ];
1178 }
1179
1180 $flags = isset( $wrapped[self::FLD_FLAGS] ) ? $wrapped[self::FLD_FLAGS] : 0;
1181 if ( ( $flags & self::FLG_STALE ) == self::FLG_STALE ) {
1182 // Treat as expired, with the cache time as the expiration
1183 $age = $now - $wrapped[self::FLD_TIME];
1184 $curTTL = min( -$age, self::TINY_NEGATIVE );
1185 } elseif ( $wrapped[self::FLD_TTL] > 0 ) {
1186 // Get the approximate time left on the key
1187 $age = $now - $wrapped[self::FLD_TIME];
1188 $curTTL = max( $wrapped[self::FLD_TTL] - $age, 0.0 );
1189 } else {
1190 // Key had no TTL, so the time left is unbounded
1191 $curTTL = INF;
1192 }
1193
1194 return [ $wrapped[self::FLD_VALUE], $curTTL ];
1195 }
1196
1197 /**
1198 * @param array $keys
1199 * @param string $prefix
1200 * @return string[]
1201 */
1202 protected static function prefixCacheKeys( array $keys, $prefix ) {
1203 $res = [];
1204 foreach ( $keys as $key ) {
1205 $res[] = $prefix . $key;
1206 }
1207
1208 return $res;
1209 }
1210
1211 /**
1212 * @param string $value Wrapped value like "PURGED:<timestamp>:<holdoff>"
1213 * @return array|bool Array containing a UNIX timestamp (float) and holdoff period (integer),
1214 * or false if value isn't a valid purge value
1215 */
1216 protected static function parsePurgeValue( $value ) {
1217 if ( !is_string( $value ) ) {
1218 return false;
1219 }
1220 $segments = explode( ':', $value, 3 );
1221 if ( !isset( $segments[0] ) || !isset( $segments[1] )
1222 || "{$segments[0]}:" !== self::PURGE_VAL_PREFIX
1223 ) {
1224 return false;
1225 }
1226 if ( !isset( $segments[2] ) ) {
1227 // Back-compat with old purge values without holdoff
1228 $segments[2] = self::HOLDOFF_TTL;
1229 }
1230 return [
1231 self::FLD_TIME => (float)$segments[1],
1232 self::FLD_HOLDOFF => (int)$segments[2],
1233 ];
1234 }
1235
1236 /**
1237 * @param float $timestamp
1238 * @param int $holdoff In seconds
1239 * @return string Wrapped purge value
1240 */
1241 protected function makePurgeValue( $timestamp, $holdoff ) {
1242 return self::PURGE_VAL_PREFIX . (float)$timestamp . ':' . (int)$holdoff;
1243 }
1244 }