Completed
Branch master (771964)
by
unknown
26:13
created

WANObjectCache::clearProcessCache()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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