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