MediaWiki REL1_34
WANObjectCache.php
Go to the documentation of this file.
1<?php
22use Liuggio\StatsdClient\Factory\StatsdDataFactoryInterface;
23use Psr\Log\LoggerAwareInterface;
24use Psr\Log\LoggerInterface;
25use Psr\Log\NullLogger;
26
116class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInterface {
118 protected $cache;
120 protected $processCaches = [];
122 protected $logger;
124 protected $stats;
126 protected $asyncHandler;
127
129 protected $mcrouterAware;
131 protected $region;
133 protected $cluster;
137 protected $epoch;
139 protected $secret;
140
142 private $callbackDepth = 0;
144 private $warmupCache = [];
146 private $warmupKeyMisses = 0;
147
150
152 const MAX_COMMIT_DELAY = 3;
154 const MAX_READ_LAG = 7;
156 const HOLDOFF_TTL = self::MAX_COMMIT_DELAY + self::MAX_READ_LAG + 1;
157
159 const TTL_UNCACHEABLE = -1;
160
162 const LOW_TTL = 30;
164 const TTL_LAGGED = 30;
165
167 const HOT_TTR = 900;
169 const AGE_NEW = 60;
170
172 const TSE_NONE = -1;
173
175 const STALE_TTL_NONE = 0;
177 const GRACE_TTL_NONE = 0;
179 const HOLDOFF_TTL_NONE = 0;
181 const HOLDOFF_NONE = self::HOLDOFF_TTL_NONE;
182
184 const MIN_TIMESTAMP_NONE = 0.0;
185
187 const PC_PRIMARY = 'primary:1000';
188
190 const PASS_BY_REF = -1;
191
193 private static $CHECK_KEY_TTL = self::TTL_YEAR;
195 private static $INTERIM_KEY_TTL = 1;
196
198 private static $LOCK_TTL = 10;
200 private static $COOLOFF_TTL = 1;
202 private static $RAMPUP_TTL = 30;
203
205 private static $TINY_NEGATIVE = -0.000001;
207 private static $TINY_POSTIVE = 0.000001;
208
210 private static $SET_DELAY_HIGH_MS = 50;
212 private static $RECENT_SET_LOW_MS = 50;
214 private static $RECENT_SET_HIGH_MS = 100;
215
217 private static $GENERATION_SLOW_SEC = 3;
218
220 private static $PURGE_TIME = 0;
222 private static $PURGE_HOLDOFF = 1;
223
225 private static $VERSION = 1;
226
228 private static $FLD_FORMAT_VERSION = 0;
230 private static $FLD_VALUE = 1;
232 private static $FLD_TTL = 2;
234 private static $FLD_TIME = 3;
236 private static $FLD_FLAGS = 4;
238 private static $FLD_VALUE_VERSION = 5;
240 private static $FLD_GENERATION_TIME = 6;
241
242 private static $VALUE_KEY_PREFIX = 'WANCache:v:';
243 private static $INTERIM_KEY_PREFIX = 'WANCache:i:';
244 private static $TIME_KEY_PREFIX = 'WANCache:t:';
245 private static $MUTEX_KEY_PREFIX = 'WANCache:m:';
246 private static $COOLOFF_KEY_PREFIX = 'WANCache:c:';
247
248 private static $PURGE_VAL_PREFIX = 'PURGED:';
249
275 public function __construct( array $params ) {
276 $this->cache = $params['cache'];
277 $this->region = $params['region'] ?? 'main';
278 $this->cluster = $params['cluster'] ?? 'wan-main';
279 $this->mcrouterAware = !empty( $params['mcrouterAware'] );
280 $this->epoch = $params['epoch'] ?? 0;
281 $this->secret = $params['secret'] ?? (string)$this->epoch;
282
283 $this->setLogger( $params['logger'] ?? new NullLogger() );
284 $this->stats = $params['stats'] ?? new NullStatsdDataFactory();
285 $this->asyncHandler = $params['asyncHandler'] ?? null;
286 }
287
291 public function setLogger( LoggerInterface $logger ) {
292 $this->logger = $logger;
293 }
294
300 public static function newEmpty() {
301 return new static( [ 'cache' => new EmptyBagOStuff() ] );
302 }
303
354 final public function get(
355 $key, &$curTTL = null, array $checkKeys = [], &$info = null
356 ) {
357 $curTTLs = self::PASS_BY_REF;
358 $infoByKey = self::PASS_BY_REF;
359 $values = $this->getMulti( [ $key ], $curTTLs, $checkKeys, $infoByKey );
360 $curTTL = $curTTLs[$key] ?? null;
361 if ( $info === self::PASS_BY_REF ) {
362 $info = [
363 'asOf' => $infoByKey[$key]['asOf'] ?? null,
364 'tombAsOf' => $infoByKey[$key]['tombAsOf'] ?? null,
365 'lastCKPurge' => $infoByKey[$key]['lastCKPurge'] ?? null,
366 'version' => $infoByKey[$key]['version'] ?? null
367 ];
368 } else {
369 $info = $infoByKey[$key]['asOf'] ?? null; // b/c
370 }
371
372 return $values[$key] ?? false;
373 }
374
397 final public function getMulti(
398 array $keys,
399 &$curTTLs = [],
400 array $checkKeys = [],
401 &$info = null
402 ) {
403 $result = [];
404 $curTTLs = [];
405 $infoByKey = [];
406
407 $vPrefixLen = strlen( self::$VALUE_KEY_PREFIX );
408 $valueKeys = self::prefixCacheKeys( $keys, self::$VALUE_KEY_PREFIX );
409
410 $checkKeysForAll = [];
411 $checkKeysByKey = [];
412 $checkKeysFlat = [];
413 foreach ( $checkKeys as $i => $checkKeyGroup ) {
414 $prefixed = self::prefixCacheKeys( (array)$checkKeyGroup, self::$TIME_KEY_PREFIX );
415 $checkKeysFlat = array_merge( $checkKeysFlat, $prefixed );
416 // Are these check keys for a specific cache key, or for all keys being fetched?
417 if ( is_int( $i ) ) {
418 $checkKeysForAll = array_merge( $checkKeysForAll, $prefixed );
419 } else {
420 $checkKeysByKey[$i] = $prefixed;
421 }
422 }
423
424 // Fetch all of the raw values
425 $keysGet = array_merge( $valueKeys, $checkKeysFlat );
426 if ( $this->warmupCache ) {
427 $wrappedValues = array_intersect_key( $this->warmupCache, array_flip( $keysGet ) );
428 $keysGet = array_diff( $keysGet, array_keys( $wrappedValues ) ); // keys left to fetch
429 $this->warmupKeyMisses += count( $keysGet );
430 } else {
431 $wrappedValues = [];
432 }
433 if ( $keysGet ) {
434 $wrappedValues += $this->cache->getMulti( $keysGet );
435 }
436 // Time used to compare/init "check" keys (derived after getMulti() to be pessimistic)
437 $now = $this->getCurrentTime();
438
439 // Collect timestamps from all "check" keys
440 $purgeValuesForAll = $this->processCheckKeys( $checkKeysForAll, $wrappedValues, $now );
441 $purgeValuesByKey = [];
442 foreach ( $checkKeysByKey as $cacheKey => $checks ) {
443 $purgeValuesByKey[$cacheKey] =
444 $this->processCheckKeys( $checks, $wrappedValues, $now );
445 }
446
447 // Get the main cache value for each key and validate them
448 foreach ( $valueKeys as $vKey ) {
449 $key = substr( $vKey, $vPrefixLen ); // unprefix
450 list( $value, $keyInfo ) = $this->unwrap( $wrappedValues[$vKey] ?? false, $now );
451 // Force dependent keys to be seen as stale for a while after purging
452 // to reduce race conditions involving stale data getting cached
453 $purgeValues = $purgeValuesForAll;
454 if ( isset( $purgeValuesByKey[$key] ) ) {
455 $purgeValues = array_merge( $purgeValues, $purgeValuesByKey[$key] );
456 }
457
458 $lastCKPurge = null; // timestamp of the highest check key
459 foreach ( $purgeValues as $purge ) {
460 $lastCKPurge = max( $purge[self::$PURGE_TIME], $lastCKPurge );
461 $safeTimestamp = $purge[self::$PURGE_TIME] + $purge[self::$PURGE_HOLDOFF];
462 if ( $value !== false && $safeTimestamp >= $keyInfo['asOf'] ) {
463 // How long ago this value was invalidated by *this* check key
464 $ago = min( $purge[self::$PURGE_TIME] - $now, self::$TINY_NEGATIVE );
465 // How long ago this value was invalidated by *any* known check key
466 $keyInfo['curTTL'] = min( $keyInfo['curTTL'], $ago );
467 }
468 }
469 $keyInfo[ 'lastCKPurge'] = $lastCKPurge;
470
471 if ( $value !== false ) {
472 $result[$key] = $value;
473 }
474 if ( $keyInfo['curTTL'] !== null ) {
475 $curTTLs[$key] = $keyInfo['curTTL'];
476 }
477
478 $infoByKey[$key] = ( $info === self::PASS_BY_REF )
479 ? $keyInfo
480 : $keyInfo['asOf']; // b/c
481 }
482
483 $info = $infoByKey;
484
485 return $result;
486 }
487
495 private function processCheckKeys( array $timeKeys, array $wrappedValues, $now ) {
496 $purgeValues = [];
497 foreach ( $timeKeys as $timeKey ) {
498 $purge = isset( $wrappedValues[$timeKey] )
499 ? $this->parsePurgeValue( $wrappedValues[$timeKey] )
500 : false;
501 if ( $purge === false ) {
502 // Key is not set or malformed; regenerate
503 $newVal = $this->makePurgeValue( $now, self::HOLDOFF_TTL );
504 $this->cache->add( $timeKey, $newVal, self::$CHECK_KEY_TTL );
505 $purge = $this->parsePurgeValue( $newVal );
506 }
507 $purgeValues[] = $purge;
508 }
509
510 return $purgeValues;
511 }
512
589 final public function set( $key, $value, $ttl = self::TTL_INDEFINITE, array $opts = [] ) {
590 $now = $this->getCurrentTime();
591 $lag = $opts['lag'] ?? 0;
592 $age = isset( $opts['since'] ) ? max( 0, $now - $opts['since'] ) : 0;
593 $pending = $opts['pending'] ?? false;
594 $lockTSE = $opts['lockTSE'] ?? self::TSE_NONE;
595 $staleTTL = $opts['staleTTL'] ?? self::STALE_TTL_NONE;
596 $creating = $opts['creating'] ?? false;
597 $version = $opts['version'] ?? null;
598 $walltime = $opts['walltime'] ?? 0.0;
599
600 if ( $ttl < 0 ) {
601 return true;
602 }
603
604 // Do not cache potentially uncommitted data as it might get rolled back
605 if ( $pending ) {
606 $this->logger->info(
607 'Rejected set() for {cachekey} due to pending writes.',
608 [ 'cachekey' => $key ]
609 );
610
611 return true; // no-op the write for being unsafe
612 }
613
614 $logicalTTL = null; // logical TTL override
615 // Check if there's a risk of writing stale data after the purge tombstone expired
616 if ( $lag === false || ( $lag + $age ) > self::MAX_READ_LAG ) {
617 // Case A: any long-running transaction
618 if ( $age > self::MAX_READ_LAG ) {
619 if ( $lockTSE >= 0 ) {
620 // Store value as *almost* stale to avoid cache and mutex stampedes
621 $logicalTTL = self::TTL_SECOND;
622 $this->logger->info(
623 'Lowered set() TTL for {cachekey} due to snapshot lag.',
624 [ 'cachekey' => $key, 'lag' => $lag, 'age' => $age ]
625 );
626 } else {
627 $this->logger->info(
628 'Rejected set() for {cachekey} due to snapshot lag.',
629 [ 'cachekey' => $key, 'lag' => $lag, 'age' => $age ]
630 );
631
632 return true; // no-op the write for being unsafe
633 }
634 // Case B: high replication lag; lower TTL instead of ignoring all set()s
635 } elseif ( $lag === false || $lag > self::MAX_READ_LAG ) {
636 if ( $lockTSE >= 0 ) {
637 $logicalTTL = min( $ttl ?: INF, self::TTL_LAGGED );
638 } else {
639 $ttl = min( $ttl ?: INF, self::TTL_LAGGED );
640 }
641 $this->logger->warning(
642 'Lowered set() TTL for {cachekey} due to replication lag.',
643 [ 'cachekey' => $key, 'lag' => $lag, 'age' => $age ]
644 );
645 // Case C: medium length request with medium replication lag
646 } elseif ( $lockTSE >= 0 ) {
647 // Store value as *almost* stale to avoid cache and mutex stampedes
648 $logicalTTL = self::TTL_SECOND;
649 $this->logger->info(
650 'Lowered set() TTL for {cachekey} due to high read lag.',
651 [ 'cachekey' => $key, 'lag' => $lag, 'age' => $age ]
652 );
653 } else {
654 $this->logger->info(
655 'Rejected set() for {cachekey} due to high read lag.',
656 [ 'cachekey' => $key, 'lag' => $lag, 'age' => $age ]
657 );
658
659 return true; // no-op the write for being unsafe
660 }
661 }
662
663 // Wrap that value with time/TTL/version metadata
664 $wrapped = $this->wrap( $value, $logicalTTL ?: $ttl, $version, $now, $walltime );
665 $storeTTL = $ttl + $staleTTL;
666
667 if ( $creating ) {
668 $ok = $this->cache->add( self::$VALUE_KEY_PREFIX . $key, $wrapped, $storeTTL );
669 } else {
670 $ok = $this->cache->merge(
671 self::$VALUE_KEY_PREFIX . $key,
672 function ( $cache, $key, $cWrapped ) use ( $wrapped ) {
673 // A string value means that it is a tombstone; do nothing in that case
674 return ( is_string( $cWrapped ) ) ? false : $wrapped;
675 },
676 $storeTTL,
677 1 // 1 attempt
678 );
679 }
680
681 return $ok;
682 }
683
745 final public function delete( $key, $ttl = self::HOLDOFF_TTL ) {
746 if ( $ttl <= 0 ) {
747 // Publish the purge to all datacenters
748 $ok = $this->relayDelete( self::$VALUE_KEY_PREFIX . $key );
749 } else {
750 // Publish the purge to all datacenters
751 $ok = $this->relayPurge( self::$VALUE_KEY_PREFIX . $key, $ttl, self::HOLDOFF_TTL_NONE );
752 }
753
754 $kClass = $this->determineKeyClassForStats( $key );
755 $this->stats->increment( "wanobjectcache.$kClass.delete." . ( $ok ? 'ok' : 'error' ) );
756
757 return $ok;
758 }
759
779 final public function getCheckKeyTime( $key ) {
780 return $this->getMultiCheckKeyTime( [ $key ] )[$key];
781 }
782
844 final public function getMultiCheckKeyTime( array $keys ) {
845 $rawKeys = [];
846 foreach ( $keys as $key ) {
847 $rawKeys[$key] = self::$TIME_KEY_PREFIX . $key;
848 }
849
850 $rawValues = $this->cache->getMulti( $rawKeys );
851 $rawValues += array_fill_keys( $rawKeys, false );
852
853 $times = [];
854 foreach ( $rawKeys as $key => $rawKey ) {
855 $purge = $this->parsePurgeValue( $rawValues[$rawKey] );
856 if ( $purge !== false ) {
857 $time = $purge[self::$PURGE_TIME];
858 } else {
859 // Casting assures identical floats for the next getCheckKeyTime() calls
860 $now = (string)$this->getCurrentTime();
861 $this->cache->add(
862 $rawKey,
863 $this->makePurgeValue( $now, self::HOLDOFF_TTL ),
864 self::$CHECK_KEY_TTL
865 );
866 $time = (float)$now;
867 }
868
869 $times[$key] = $time;
870 }
871
872 return $times;
873 }
874
909 final public function touchCheckKey( $key, $holdoff = self::HOLDOFF_TTL ) {
910 // Publish the purge to all datacenters
911 $ok = $this->relayPurge( self::$TIME_KEY_PREFIX . $key, self::$CHECK_KEY_TTL, $holdoff );
912
913 $kClass = $this->determineKeyClassForStats( $key );
914 $this->stats->increment( "wanobjectcache.$kClass.ck_touch." . ( $ok ? 'ok' : 'error' ) );
915
916 return $ok;
917 }
918
946 final public function resetCheckKey( $key ) {
947 // Publish the purge to all datacenters
948 $ok = $this->relayDelete( self::$TIME_KEY_PREFIX . $key );
949
950 $kClass = $this->determineKeyClassForStats( $key );
951 $this->stats->increment( "wanobjectcache.$kClass.ck_reset." . ( $ok ? 'ok' : 'error' ) );
952
953 return $ok;
954 }
955
1261 final public function getWithSetCallback( $key, $ttl, $callback, array $opts = [] ) {
1262 $version = $opts['version'] ?? null;
1263 $pcTTL = $opts['pcTTL'] ?? self::TTL_UNCACHEABLE;
1264 $pCache = ( $pcTTL >= 0 )
1265 ? $this->getProcessCache( $opts['pcGroup'] ?? self::PC_PRIMARY )
1266 : null;
1267
1268 // Use the process cache if requested as long as no outer cache callback is running.
1269 // Nested callback process cache use is not lag-safe with regard to HOLDOFF_TTL since
1270 // process cached values are more lagged than persistent ones as they are not purged.
1271 if ( $pCache && $this->callbackDepth == 0 ) {
1272 $cached = $pCache->get( $this->getProcessCacheKey( $key, $version ), $pcTTL, false );
1273 if ( $cached !== false ) {
1274 return $cached;
1275 }
1276 }
1277
1278 $res = $this->fetchOrRegenerate( $key, $ttl, $callback, $opts );
1279 list( $value, $valueVersion, $curAsOf ) = $res;
1280 if ( $valueVersion !== $version ) {
1281 // Current value has a different version; use the variant key for this version.
1282 // Regenerate the variant value if it is not newer than the main value at $key
1283 // so that purges to the main key propagate to the variant value.
1284 list( $value ) = $this->fetchOrRegenerate(
1285 $this->makeGlobalKey( 'WANCache-key-variant', md5( $key ), $version ),
1286 $ttl,
1287 $callback,
1288 [ 'version' => null, 'minAsOf' => $curAsOf ] + $opts
1289 );
1290 }
1291
1292 // Update the process cache if enabled
1293 if ( $pCache && $value !== false ) {
1294 $pCache->set( $this->getProcessCacheKey( $key, $version ), $value );
1295 }
1296
1297 return $value;
1298 }
1299
1315 private function fetchOrRegenerate( $key, $ttl, $callback, array $opts ) {
1316 $checkKeys = $opts['checkKeys'] ?? [];
1317 $graceTTL = $opts['graceTTL'] ?? self::GRACE_TTL_NONE;
1318 $minAsOf = $opts['minAsOf'] ?? self::MIN_TIMESTAMP_NONE;
1319 $hotTTR = $opts['hotTTR'] ?? self::HOT_TTR;
1320 $lowTTL = $opts['lowTTL'] ?? min( self::LOW_TTL, $ttl );
1321 $ageNew = $opts['ageNew'] ?? self::AGE_NEW;
1322 $touchedCb = $opts['touchedCallback'] ?? null;
1323 $initialTime = $this->getCurrentTime();
1324
1325 $kClass = $this->determineKeyClassForStats( $key );
1326
1327 // Get the current key value and its metadata
1328 $curTTL = self::PASS_BY_REF;
1329 $curInfo = self::PASS_BY_REF;
1330 $curValue = $this->get( $key, $curTTL, $checkKeys, $curInfo );
1331 // Apply any $touchedCb invalidation timestamp to get the "last purge timestamp"
1332 list( $curTTL, $LPT ) = $this->resolveCTL( $curValue, $curTTL, $curInfo, $touchedCb );
1333 // Use the cached value if it exists and is not due for synchronous regeneration
1334 if (
1335 $this->isValid( $curValue, $curInfo['asOf'], $minAsOf ) &&
1336 $this->isAliveOrInGracePeriod( $curTTL, $graceTTL )
1337 ) {
1338 $preemptiveRefresh = (
1339 $this->worthRefreshExpiring( $curTTL, $lowTTL ) ||
1340 $this->worthRefreshPopular( $curInfo['asOf'], $ageNew, $hotTTR, $initialTime )
1341 );
1342 if ( !$preemptiveRefresh ) {
1343 $this->stats->increment( "wanobjectcache.$kClass.hit.good" );
1344
1345 return [ $curValue, $curInfo['version'], $curInfo['asOf'] ];
1346 } elseif ( $this->scheduleAsyncRefresh( $key, $ttl, $callback, $opts ) ) {
1347 $this->stats->increment( "wanobjectcache.$kClass.hit.refresh" );
1348
1349 return [ $curValue, $curInfo['version'], $curInfo['asOf'] ];
1350 }
1351 }
1352
1353 // Determine if there is stale or volatile cached value that is still usable
1354 $isKeyTombstoned = ( $curInfo['tombAsOf'] !== null );
1355 if ( $isKeyTombstoned ) {
1356 // Key is write-holed; use the (volatile) interim key as an alternative
1357 list( $possValue, $possInfo ) = $this->getInterimValue( $key, $minAsOf );
1358 // Update the "last purge time" since the $touchedCb timestamp depends on $value
1359 $LPT = $this->resolveTouched( $possValue, $LPT, $touchedCb );
1360 } else {
1361 $possValue = $curValue;
1362 $possInfo = $curInfo;
1363 }
1364
1365 // Avoid overhead from callback runs, regeneration locks, and cache sets during
1366 // hold-off periods for the key by reusing very recently generated cached values
1367 if (
1368 $this->isValid( $possValue, $possInfo['asOf'], $minAsOf, $LPT ) &&
1369 $this->isVolatileValueAgeNegligible( $initialTime - $possInfo['asOf'] )
1370 ) {
1371 $this->stats->increment( "wanobjectcache.$kClass.hit.volatile" );
1372
1373 return [ $possValue, $possInfo['version'], $curInfo['asOf'] ];
1374 }
1375
1376 $lockTSE = $opts['lockTSE'] ?? self::TSE_NONE;
1377 $busyValue = $opts['busyValue'] ?? null;
1378 $staleTTL = $opts['staleTTL'] ?? self::STALE_TTL_NONE;
1379 $version = $opts['version'] ?? null;
1380
1381 // Determine whether one thread per datacenter should handle regeneration at a time
1382 $useRegenerationLock =
1383 // Note that since tombstones no-op set(), $lockTSE and $curTTL cannot be used to
1384 // deduce the key hotness because |$curTTL| will always keep increasing until the
1385 // tombstone expires or is overwritten by a new tombstone. Also, even if $lockTSE
1386 // is not set, constant regeneration of a key for the tombstone lifetime might be
1387 // very expensive. Assume tombstoned keys are possibly hot in order to reduce
1388 // the risk of high regeneration load after the delete() method is called.
1389 $isKeyTombstoned ||
1390 // Assume a key is hot if requested soon ($lockTSE seconds) after invalidation.
1391 // This avoids stampedes when timestamps from $checkKeys/$touchedCb bump.
1392 ( $curTTL !== null && $curTTL <= 0 && abs( $curTTL ) <= $lockTSE ) ||
1393 // Assume a key is hot if there is no value and a busy fallback is given.
1394 // This avoids stampedes on eviction or preemptive regeneration taking too long.
1395 ( $busyValue !== null && $possValue === false );
1396
1397 // If a regeneration lock is required, threads that do not get the lock will try to use
1398 // the stale value, the interim value, or the $busyValue placeholder, in that order. If
1399 // none of those are set then all threads will bypass the lock and regenerate the value.
1400 $hasLock = $useRegenerationLock && $this->claimStampedeLock( $key );
1401 if ( $useRegenerationLock && !$hasLock ) {
1402 if ( $this->isValid( $possValue, $possInfo['asOf'], $minAsOf ) ) {
1403 $this->stats->increment( "wanobjectcache.$kClass.hit.stale" );
1404
1405 return [ $possValue, $possInfo['version'], $curInfo['asOf'] ];
1406 } elseif ( $busyValue !== null ) {
1407 $miss = is_infinite( $minAsOf ) ? 'renew' : 'miss';
1408 $this->stats->increment( "wanobjectcache.$kClass.$miss.busy" );
1409
1410 return [ $this->resolveBusyValue( $busyValue ), $version, $curInfo['asOf'] ];
1411 }
1412 }
1413
1414 // Generate the new value given any prior value with a matching version
1415 $setOpts = [];
1416 $preCallbackTime = $this->getCurrentTime();
1417 ++$this->callbackDepth;
1418 try {
1419 $value = $callback(
1420 ( $curInfo['version'] === $version ) ? $curValue : false,
1421 $ttl,
1422 $setOpts,
1423 ( $curInfo['version'] === $version ) ? $curInfo['asOf'] : null
1424 );
1425 } finally {
1426 --$this->callbackDepth;
1427 }
1428 $postCallbackTime = $this->getCurrentTime();
1429
1430 // How long it took to fetch, validate, and generate the value
1431 $elapsed = max( $postCallbackTime - $initialTime, 0.0 );
1432
1433 // Attempt to save the newly generated value if applicable
1434 if (
1435 // Callback yielded a cacheable value
1436 ( $value !== false && $ttl >= 0 ) &&
1437 // Current thread was not raced out of a regeneration lock or key is tombstoned
1438 ( !$useRegenerationLock || $hasLock || $isKeyTombstoned ) &&
1439 // Key does not appear to be undergoing a set() stampede
1440 $this->checkAndSetCooloff( $key, $kClass, $elapsed, $lockTSE, $hasLock )
1441 ) {
1442 // How long it took to generate the value
1443 $walltime = max( $postCallbackTime - $preCallbackTime, 0.0 );
1444 $this->stats->timing( "wanobjectcache.$kClass.regen_walltime", 1e3 * $walltime );
1445 // If the key is write-holed then use the (volatile) interim key as an alternative
1446 if ( $isKeyTombstoned ) {
1447 $this->setInterimValue( $key, $value, $lockTSE, $version, $walltime );
1448 } else {
1449 $finalSetOpts = [
1450 'since' => $setOpts['since'] ?? $preCallbackTime,
1451 'version' => $version,
1452 'staleTTL' => $staleTTL,
1453 'lockTSE' => $lockTSE, // informs lag vs performance trade-offs
1454 'creating' => ( $curValue === false ), // optimization
1455 'walltime' => $walltime
1456 ] + $setOpts;
1457 $this->set( $key, $value, $ttl, $finalSetOpts );
1458 }
1459 }
1460
1461 $this->yieldStampedeLock( $key, $hasLock );
1462
1463 $miss = is_infinite( $minAsOf ) ? 'renew' : 'miss';
1464 $this->stats->increment( "wanobjectcache.$kClass.$miss.compute" );
1465
1466 return [ $value, $version, $curInfo['asOf'] ];
1467 }
1468
1473 private function claimStampedeLock( $key ) {
1474 // Note that locking is not bypassed due to I/O errors; this avoids stampedes
1475 return $this->cache->add( self::$MUTEX_KEY_PREFIX . $key, 1, self::$LOCK_TTL );
1476 }
1477
1482 private function yieldStampedeLock( $key, $hasLock ) {
1483 if ( $hasLock ) {
1484 // The backend might be a mcrouter proxy set to broadcast DELETE to *all* the local
1485 // datacenter cache servers via OperationSelectorRoute (for increased consistency).
1486 // Since that would be excessive for these locks, use TOUCH to expire the key.
1487 $this->cache->changeTTL( self::$MUTEX_KEY_PREFIX . $key, $this->getCurrentTime() - 60 );
1488 }
1489 }
1490
1495 private function isVolatileValueAgeNegligible( $age ) {
1496 return ( $age < mt_rand( self::$RECENT_SET_LOW_MS, self::$RECENT_SET_HIGH_MS ) / 1e3 );
1497 }
1498
1507 private function checkAndSetCooloff( $key, $kClass, $elapsed, $lockTSE, $hasLock ) {
1508 $this->stats->timing( "wanobjectcache.$kClass.regen_set_delay", 1e3 * $elapsed );
1509
1510 // If $lockTSE is set, the lock was bypassed because there was no stale/interim value,
1511 // and $elapsed indicates that regeration is slow, then there is a risk of set()
1512 // stampedes with large blobs. With a typical scale-out infrastructure, CPU and query
1513 // load from $callback invocations is distributed among appservers and replica DBs,
1514 // but cache operations for a given key route to a single cache server (e.g. striped
1515 // consistent hashing).
1516 if ( $lockTSE < 0 || $hasLock ) {
1517 return true; // either not a priori hot or thread has the lock
1518 } elseif ( $elapsed <= self::$SET_DELAY_HIGH_MS * 1e3 ) {
1519 return true; // not enough time for threads to pile up
1520 }
1521
1522 $this->cache->clearLastError();
1523 if (
1524 !$this->cache->add( self::$COOLOFF_KEY_PREFIX . $key, 1, self::$COOLOFF_TTL ) &&
1525 // Don't treat failures due to I/O errors as the key being in cooloff
1526 $this->cache->getLastError() === BagOStuff::ERR_NONE
1527 ) {
1528 $this->stats->increment( "wanobjectcache.$kClass.cooloff_bounce" );
1529
1530 return false;
1531 }
1532
1533 return true;
1534 }
1535
1544 private function resolveCTL( $value, $curTTL, $curInfo, $touchedCallback ) {
1545 if ( $touchedCallback === null || $value === false ) {
1546 return [ $curTTL, max( $curInfo['tombAsOf'], $curInfo['lastCKPurge'] ) ];
1547 }
1548
1549 $touched = $touchedCallback( $value );
1550 if ( $touched !== null && $touched >= $curInfo['asOf'] ) {
1551 $curTTL = min( $curTTL, self::$TINY_NEGATIVE, $curInfo['asOf'] - $touched );
1552 }
1553
1554 return [ $curTTL, max( $curInfo['tombAsOf'], $curInfo['lastCKPurge'], $touched ) ];
1555 }
1556
1564 private function resolveTouched( $value, $lastPurge, $touchedCallback ) {
1565 return ( $touchedCallback === null || $value === false )
1566 ? $lastPurge // nothing to derive the "touched timestamp" from
1567 : max( $touchedCallback( $value ), $lastPurge );
1568 }
1569
1575 private function getInterimValue( $key, $minAsOf ) {
1576 $now = $this->getCurrentTime();
1577
1578 if ( $this->useInterimHoldOffCaching ) {
1579 $wrapped = $this->cache->get( self::$INTERIM_KEY_PREFIX . $key );
1580
1581 list( $value, $keyInfo ) = $this->unwrap( $wrapped, $now );
1582 if ( $this->isValid( $value, $keyInfo['asOf'], $minAsOf ) ) {
1583 return [ $value, $keyInfo ];
1584 }
1585 }
1586
1587 return $this->unwrap( false, $now );
1588 }
1589
1597 private function setInterimValue( $key, $value, $ttl, $version, $walltime ) {
1598 $ttl = max( self::$INTERIM_KEY_TTL, (int)$ttl );
1599
1600 $wrapped = $this->wrap( $value, $ttl, $version, $this->getCurrentTime(), $walltime );
1601 $this->cache->merge(
1602 self::$INTERIM_KEY_PREFIX . $key,
1603 function () use ( $wrapped ) {
1604 return $wrapped;
1605 },
1606 $ttl,
1607 1
1608 );
1609 }
1610
1615 private function resolveBusyValue( $busyValue ) {
1616 return ( $busyValue instanceof Closure ) ? $busyValue() : $busyValue;
1617 }
1618
1685 final public function getMultiWithSetCallback(
1686 ArrayIterator $keyedIds, $ttl, callable $callback, array $opts = []
1687 ) {
1688 // Load required keys into process cache in one go
1689 $this->warmupCache = $this->getRawKeysForWarmup(
1690 $this->getNonProcessCachedMultiKeys( $keyedIds, $opts ),
1691 $opts['checkKeys'] ?? []
1692 );
1693 $this->warmupKeyMisses = 0;
1694
1695 // Wrap $callback to match the getWithSetCallback() format while passing $id to $callback
1696 $id = null; // current entity ID
1697 $func = function ( $oldValue, &$ttl, &$setOpts, $oldAsOf ) use ( $callback, &$id ) {
1698 return $callback( $id, $oldValue, $ttl, $setOpts, $oldAsOf );
1699 };
1700
1701 $values = [];
1702 foreach ( $keyedIds as $key => $id ) { // preserve order
1703 $values[$key] = $this->getWithSetCallback( $key, $ttl, $func, $opts );
1704 }
1705
1706 $this->warmupCache = [];
1707
1708 return $values;
1709 }
1710
1776 final public function getMultiWithUnionSetCallback(
1777 ArrayIterator $keyedIds, $ttl, callable $callback, array $opts = []
1778 ) {
1779 $checkKeys = $opts['checkKeys'] ?? [];
1780 unset( $opts['lockTSE'] ); // incompatible
1781 unset( $opts['busyValue'] ); // incompatible
1782
1783 // Load required keys into process cache in one go
1784 $keysByIdGet = $this->getNonProcessCachedMultiKeys( $keyedIds, $opts );
1785 $this->warmupCache = $this->getRawKeysForWarmup( $keysByIdGet, $checkKeys );
1786 $this->warmupKeyMisses = 0;
1787
1788 // IDs of entities known to be in need of regeneration
1789 $idsRegen = [];
1790
1791 // Find out which keys are missing/deleted/stale
1792 $curTTLs = [];
1793 $asOfs = [];
1794 $curByKey = $this->getMulti( $keysByIdGet, $curTTLs, $checkKeys, $asOfs );
1795 foreach ( $keysByIdGet as $id => $key ) {
1796 if ( !array_key_exists( $key, $curByKey ) || $curTTLs[$key] < 0 ) {
1797 $idsRegen[] = $id;
1798 }
1799 }
1800
1801 // Run the callback to populate the regeneration value map for all required IDs
1802 $newSetOpts = [];
1803 $newTTLsById = array_fill_keys( $idsRegen, $ttl );
1804 $newValsById = $idsRegen ? $callback( $idsRegen, $newTTLsById, $newSetOpts ) : [];
1805
1806 // Wrap $callback to match the getWithSetCallback() format while passing $id to $callback
1807 $id = null; // current entity ID
1808 $func = function ( $oldValue, &$ttl, &$setOpts, $oldAsOf )
1809 use ( $callback, &$id, $newValsById, $newTTLsById, $newSetOpts )
1810 {
1811 if ( array_key_exists( $id, $newValsById ) ) {
1812 // Value was already regerated as expected, so use the value in $newValsById
1813 $newValue = $newValsById[$id];
1814 $ttl = $newTTLsById[$id];
1815 $setOpts = $newSetOpts;
1816 } else {
1817 // Pre-emptive/popularity refresh and version mismatch cases are not detected
1818 // above and thus $newValsById has no entry. Run $callback on this single entity.
1819 $ttls = [ $id => $ttl ];
1820 $newValue = $callback( [ $id ], $ttls, $setOpts )[$id];
1821 $ttl = $ttls[$id];
1822 }
1823
1824 return $newValue;
1825 };
1826
1827 // Run the cache-aside logic using warmupCache instead of persistent cache queries
1828 $values = [];
1829 foreach ( $keyedIds as $key => $id ) { // preserve order
1830 $values[$key] = $this->getWithSetCallback( $key, $ttl, $func, $opts );
1831 }
1832
1833 $this->warmupCache = [];
1834
1835 return $values;
1836 }
1837
1850 final public function reap( $key, $purgeTimestamp, &$isStale = false ) {
1851 $minAsOf = $purgeTimestamp + self::HOLDOFF_TTL;
1852 $wrapped = $this->cache->get( self::$VALUE_KEY_PREFIX . $key );
1853 if ( is_array( $wrapped ) && $wrapped[self::$FLD_TIME] < $minAsOf ) {
1854 $isStale = true;
1855 $this->logger->warning( "Reaping stale value key '$key'." );
1856 $ttlReap = self::HOLDOFF_TTL; // avoids races with tombstone creation
1857 $ok = $this->cache->changeTTL( self::$VALUE_KEY_PREFIX . $key, $ttlReap );
1858 if ( !$ok ) {
1859 $this->logger->error( "Could not complete reap of key '$key'." );
1860 }
1861
1862 return $ok;
1863 }
1864
1865 $isStale = false;
1866
1867 return true;
1868 }
1869
1879 final public function reapCheckKey( $key, $purgeTimestamp, &$isStale = false ) {
1880 $purge = $this->parsePurgeValue( $this->cache->get( self::$TIME_KEY_PREFIX . $key ) );
1881 if ( $purge && $purge[self::$PURGE_TIME] < $purgeTimestamp ) {
1882 $isStale = true;
1883 $this->logger->warning( "Reaping stale check key '$key'." );
1884 $ok = $this->cache->changeTTL( self::$TIME_KEY_PREFIX . $key, self::TTL_SECOND );
1885 if ( !$ok ) {
1886 $this->logger->error( "Could not complete reap of check key '$key'." );
1887 }
1888
1889 return $ok;
1890 }
1891
1892 $isStale = false;
1893
1894 return false;
1895 }
1896
1904 public function makeKey( $class, ...$components ) {
1905 return $this->cache->makeKey( ...func_get_args() );
1906 }
1907
1915 public function makeGlobalKey( $class, ...$components ) {
1916 return $this->cache->makeGlobalKey( ...func_get_args() );
1917 }
1918
1926 public function hash256( $component ) {
1927 return hash_hmac( 'sha256', $component, $this->secret );
1928 }
1929
1980 final public function makeMultiKeys( array $ids, $keyCallback ) {
1981 $idByKey = [];
1982 foreach ( $ids as $id ) {
1983 // Discourage triggering of automatic makeKey() hashing in some backends
1984 if ( strlen( $id ) > 64 ) {
1985 $this->logger->warning( __METHOD__ . ": long ID '$id'; use hash256()" );
1986 }
1987 $key = $keyCallback( $id, $this );
1988 // Edge case: ignore key collisions due to duplicate $ids like "42" and 42
1989 if ( !isset( $idByKey[$key] ) ) {
1990 $idByKey[$key] = $id;
1991 } elseif ( (string)$id !== (string)$idByKey[$key] ) {
1992 throw new UnexpectedValueException(
1993 "Cache key collision; IDs ('$id','{$idByKey[$key]}') map to '$key'"
1994 );
1995 }
1996 }
1997
1998 return new ArrayIterator( $idByKey );
1999 }
2000
2036 final public function multiRemap( array $ids, array $res ) {
2037 if ( count( $ids ) !== count( $res ) ) {
2038 // If makeMultiKeys() is called on a list of non-unique IDs, then the resulting
2039 // ArrayIterator will have less entries due to "first appearance" de-duplication
2040 $ids = array_keys( array_flip( $ids ) );
2041 if ( count( $ids ) !== count( $res ) ) {
2042 throw new UnexpectedValueException( "Multi-key result does not match ID list" );
2043 }
2044 }
2045
2046 return array_combine( $ids, $res );
2047 }
2048
2053 final public function getLastError() {
2054 $code = $this->cache->getLastError();
2055 switch ( $code ) {
2057 return self::ERR_NONE;
2059 return self::ERR_NO_RESPONSE;
2061 return self::ERR_UNREACHABLE;
2062 default:
2063 return self::ERR_UNEXPECTED;
2064 }
2065 }
2066
2070 final public function clearLastError() {
2071 $this->cache->clearLastError();
2072 }
2073
2079 public function clearProcessCache() {
2080 $this->processCaches = [];
2081 }
2082
2103 final public function useInterimHoldOffCaching( $enabled ) {
2104 $this->useInterimHoldOffCaching = $enabled;
2105 }
2106
2112 public function getQoS( $flag ) {
2113 return $this->cache->getQoS( $flag );
2114 }
2115
2179 public function adaptiveTTL( $mtime, $maxTTL, $minTTL = 30, $factor = 0.2 ) {
2180 if ( is_float( $mtime ) || ctype_digit( $mtime ) ) {
2181 $mtime = (int)$mtime; // handle fractional seconds and string integers
2182 }
2183
2184 if ( !is_int( $mtime ) || $mtime <= 0 ) {
2185 return $minTTL; // no last-modified time provided
2186 }
2187
2188 $age = $this->getCurrentTime() - $mtime;
2189
2190 return (int)min( $maxTTL, max( $minTTL, $factor * $age ) );
2191 }
2192
2197 final public function getWarmupKeyMisses() {
2198 return $this->warmupKeyMisses;
2199 }
2200
2211 protected function relayPurge( $key, $ttl, $holdoff ) {
2212 if ( $this->mcrouterAware ) {
2213 // See https://github.com/facebook/mcrouter/wiki/Multi-cluster-broadcast-setup
2214 // Wildcards select all matching routes, e.g. the WAN cluster on all DCs
2215 $ok = $this->cache->set(
2216 "/*/{$this->cluster}/{$key}",
2217 $this->makePurgeValue( $this->getCurrentTime(), $holdoff ),
2218 $ttl
2219 );
2220 } else {
2221 // Some other proxy handles broadcasting or there is only one datacenter
2222 $ok = $this->cache->set(
2223 $key,
2224 $this->makePurgeValue( $this->getCurrentTime(), $holdoff ),
2225 $ttl
2226 );
2227 }
2228
2229 return $ok;
2230 }
2231
2238 protected function relayDelete( $key ) {
2239 if ( $this->mcrouterAware ) {
2240 // See https://github.com/facebook/mcrouter/wiki/Multi-cluster-broadcast-setup
2241 // Wildcards select all matching routes, e.g. the WAN cluster on all DCs
2242 $ok = $this->cache->delete( "/*/{$this->cluster}/{$key}" );
2243 } else {
2244 // Some other proxy handles broadcasting or there is only one datacenter
2245 $ok = $this->cache->delete( $key );
2246 }
2247
2248 return $ok;
2249 }
2250
2259 private function scheduleAsyncRefresh( $key, $ttl, $callback, $opts ) {
2260 if ( !$this->asyncHandler ) {
2261 return false;
2262 }
2263 // Update the cache value later, such during post-send of an HTTP request
2264 $func = $this->asyncHandler;
2265 $func( function () use ( $key, $ttl, $callback, $opts ) {
2266 $opts['minAsOf'] = INF; // force a refresh
2267 $this->fetchOrRegenerate( $key, $ttl, $callback, $opts );
2268 } );
2269
2270 return true;
2271 }
2272
2286 private function isAliveOrInGracePeriod( $curTTL, $graceTTL ) {
2287 if ( $curTTL > 0 ) {
2288 return true;
2289 } elseif ( $graceTTL <= 0 ) {
2290 return false;
2291 }
2292
2293 $ageStale = abs( $curTTL ); // seconds of staleness
2294 $curGTTL = ( $graceTTL - $ageStale ); // current grace-time-to-live
2295 if ( $curGTTL <= 0 ) {
2296 return false; // already out of grace period
2297 }
2298
2299 // Chance of using a stale value is the complement of the chance of refreshing it
2300 return !$this->worthRefreshExpiring( $curGTTL, $graceTTL );
2301 }
2302
2316 protected function worthRefreshExpiring( $curTTL, $lowTTL ) {
2317 if ( $lowTTL <= 0 ) {
2318 return false;
2319 } elseif ( $curTTL >= $lowTTL ) {
2320 return false;
2321 } elseif ( $curTTL <= 0 ) {
2322 return false;
2323 }
2324
2325 $chance = ( 1 - $curTTL / $lowTTL );
2326
2327 // @phan-suppress-next-line PhanTypeMismatchArgumentInternal
2328 return mt_rand( 1, 1e9 ) <= 1e9 * $chance;
2329 }
2330
2346 protected function worthRefreshPopular( $asOf, $ageNew, $timeTillRefresh, $now ) {
2347 if ( $ageNew < 0 || $timeTillRefresh <= 0 ) {
2348 return false;
2349 }
2350
2351 $age = $now - $asOf;
2352 $timeOld = $age - $ageNew;
2353 if ( $timeOld <= 0 ) {
2354 return false;
2355 }
2356
2357 $popularHitsPerSec = 1;
2358 // Lifecycle is: new, ramp-up refresh chance, full refresh chance.
2359 // Note that the "expected # of refreshes" for the ramp-up time range is half
2360 // of what it would be if P(refresh) was at its full value during that time range.
2361 $refreshWindowSec = max( $timeTillRefresh - $ageNew - self::$RAMPUP_TTL / 2, 1 );
2362 // P(refresh) * (# hits in $refreshWindowSec) = (expected # of refreshes)
2363 // P(refresh) * ($refreshWindowSec * $popularHitsPerSec) = 1 (by definition)
2364 // P(refresh) = 1/($refreshWindowSec * $popularHitsPerSec)
2365 $chance = 1 / ( $popularHitsPerSec * $refreshWindowSec );
2366
2367 // Ramp up $chance from 0 to its nominal value over RAMPUP_TTL seconds to avoid stampedes
2368 $chance *= ( $timeOld <= self::$RAMPUP_TTL ) ? $timeOld / self::$RAMPUP_TTL : 1;
2369
2370 // @phan-suppress-next-line PhanTypeMismatchArgumentInternal
2371 return mt_rand( 1, 1e9 ) <= 1e9 * $chance;
2372 }
2373
2383 protected function isValid( $value, $asOf, $minAsOf, $purgeTime = null ) {
2384 // Avoid reading any key not generated after the latest delete() or touch
2385 $safeMinAsOf = max( $minAsOf, $purgeTime + self::$TINY_POSTIVE );
2386
2387 if ( $value === false ) {
2388 return false;
2389 } elseif ( $safeMinAsOf > 0 && $asOf < $minAsOf ) {
2390 return false;
2391 }
2392
2393 return true;
2394 }
2395
2404 private function wrap( $value, $ttl, $version, $now, $walltime ) {
2405 // Returns keys in ascending integer order for PHP7 array packing:
2406 // https://nikic.github.io/2014/12/22/PHPs-new-hashtable-implementation.html
2407 $wrapped = [
2408 self::$FLD_FORMAT_VERSION => self::$VERSION,
2409 self::$FLD_VALUE => $value,
2410 self::$FLD_TTL => $ttl,
2411 self::$FLD_TIME => $now
2412 ];
2413 if ( $version !== null ) {
2414 $wrapped[self::$FLD_VALUE_VERSION] = $version;
2415 }
2416 if ( $walltime >= self::$GENERATION_SLOW_SEC ) {
2417 $wrapped[self::$FLD_GENERATION_TIME] = $walltime;
2418 }
2419
2420 return $wrapped;
2421 }
2422
2434 private function unwrap( $wrapped, $now ) {
2435 $value = false;
2436 $info = [ 'asOf' => null, 'curTTL' => null, 'version' => null, 'tombAsOf' => null ];
2437
2438 if ( is_array( $wrapped ) ) {
2439 // Entry expected to be a cached value; validate it
2440 if (
2441 ( $wrapped[self::$FLD_FORMAT_VERSION] ?? null ) === self::$VERSION &&
2442 $wrapped[self::$FLD_TIME] >= $this->epoch
2443 ) {
2444 if ( $wrapped[self::$FLD_TTL] > 0 ) {
2445 // Get the approximate time left on the key
2446 $age = $now - $wrapped[self::$FLD_TIME];
2447 $curTTL = max( $wrapped[self::$FLD_TTL] - $age, 0.0 );
2448 } else {
2449 // Key had no TTL, so the time left is unbounded
2450 $curTTL = INF;
2451 }
2452 $value = $wrapped[self::$FLD_VALUE];
2453 $info['version'] = $wrapped[self::$FLD_VALUE_VERSION] ?? null;
2454 $info['asOf'] = $wrapped[self::$FLD_TIME];
2455 $info['curTTL'] = $curTTL;
2456 }
2457 } else {
2458 // Entry expected to be a tombstone; parse it
2459 $purge = $this->parsePurgeValue( $wrapped );
2460 if ( $purge !== false ) {
2461 // Tombstoned keys should always have a negative current $ttl
2462 $info['curTTL'] = min( $purge[self::$PURGE_TIME] - $now, self::$TINY_NEGATIVE );
2463 $info['tombAsOf'] = $purge[self::$PURGE_TIME];
2464 }
2465 }
2466
2467 return [ $value, $info ];
2468 }
2469
2475 protected static function prefixCacheKeys( array $keys, $prefix ) {
2476 $res = [];
2477 foreach ( $keys as $key ) {
2478 $res[] = $prefix . $key;
2479 }
2480
2481 return $res;
2482 }
2483
2488 private function determineKeyClassForStats( $key ) {
2489 $parts = explode( ':', $key, 3 );
2490 // Sanity fallback in case the key was not made by makeKey.
2491 // Replace dots because they are special in StatsD (T232907)
2492 return strtr( $parts[1] ?? $parts[0], '.', '_' );
2493 }
2494
2500 private function parsePurgeValue( $value ) {
2501 if ( !is_string( $value ) ) {
2502 return false;
2503 }
2504
2505 $segments = explode( ':', $value, 3 );
2506 if (
2507 !isset( $segments[0] ) ||
2508 !isset( $segments[1] ) ||
2509 "{$segments[0]}:" !== self::$PURGE_VAL_PREFIX
2510 ) {
2511 return false;
2512 }
2513
2514 if ( !isset( $segments[2] ) ) {
2515 // Back-compat with old purge values without holdoff
2516 $segments[2] = self::HOLDOFF_TTL;
2517 }
2518
2519 if ( $segments[1] < $this->epoch ) {
2520 // Values this old are ignored
2521 return false;
2522 }
2523
2524 return [
2525 self::$PURGE_TIME => (float)$segments[1],
2526 self::$PURGE_HOLDOFF => (int)$segments[2],
2527 ];
2528 }
2529
2535 private function makePurgeValue( $timestamp, $holdoff ) {
2536 return self::$PURGE_VAL_PREFIX . (float)$timestamp . ':' . (int)$holdoff;
2537 }
2538
2543 private function getProcessCache( $group ) {
2544 if ( !isset( $this->processCaches[$group] ) ) {
2545 list( , $size ) = explode( ':', $group );
2546 $this->processCaches[$group] = new MapCacheLRU( (int)$size );
2547 if ( $this->wallClockOverride !== null ) {
2548 $this->processCaches[$group]->setMockTime( $this->wallClockOverride );
2549 }
2550 }
2551
2552 return $this->processCaches[$group];
2553 }
2554
2560 private function getProcessCacheKey( $key, $version ) {
2561 return $key . ' ' . (int)$version;
2562 }
2563
2569 private function getNonProcessCachedMultiKeys( ArrayIterator $keys, array $opts ) {
2570 $pcTTL = $opts['pcTTL'] ?? self::TTL_UNCACHEABLE;
2571
2572 $keysMissing = [];
2573 if ( $pcTTL > 0 && $this->callbackDepth == 0 ) {
2574 $version = $opts['version'] ?? null;
2575 $pCache = $this->getProcessCache( $opts['pcGroup'] ?? self::PC_PRIMARY );
2576 foreach ( $keys as $key => $id ) {
2577 if ( !$pCache->has( $this->getProcessCacheKey( $key, $version ), $pcTTL ) ) {
2578 $keysMissing[$id] = $key;
2579 }
2580 }
2581 }
2582
2583 return $keysMissing;
2584 }
2585
2591 private function getRawKeysForWarmup( array $keys, array $checkKeys ) {
2592 if ( !$keys ) {
2593 return [];
2594 }
2595
2596 $keysWarmUp = [];
2597 // Get all the value keys to fetch...
2598 foreach ( $keys as $key ) {
2599 $keysWarmUp[] = self::$VALUE_KEY_PREFIX . $key;
2600 }
2601 // Get all the check keys to fetch...
2602 foreach ( $checkKeys as $i => $checkKeyOrKeys ) {
2603 if ( is_int( $i ) ) {
2604 // Single check key that applies to all value keys
2605 $keysWarmUp[] = self::$TIME_KEY_PREFIX . $checkKeyOrKeys;
2606 } else {
2607 // List of check keys that apply to value key $i
2608 $keysWarmUp = array_merge(
2609 $keysWarmUp,
2610 self::prefixCacheKeys( $checkKeyOrKeys, self::$TIME_KEY_PREFIX )
2611 );
2612 }
2613 }
2614
2615 $warmupCache = $this->cache->getMulti( $keysWarmUp );
2616 $warmupCache += array_fill_keys( $keysWarmUp, false );
2617
2618 return $warmupCache;
2619 }
2620
2625 protected function getCurrentTime() {
2626 if ( $this->wallClockOverride ) {
2627 return $this->wallClockOverride;
2628 }
2629
2630 $clockTime = (float)time(); // call this first
2631 // microtime() uses an initial gettimeofday() call added to usage clocks.
2632 // This can severely drift from time() and the microtime() value of other threads
2633 // due to undercounting of the amount of time elapsed. Instead of seeing the current
2634 // time as being in the past, use the value of time(). This avoids setting cache values
2635 // that will immediately be seen as expired and possibly cause stampedes.
2636 return max( microtime( true ), $clockTime );
2637 }
2638
2643 public function setMockTime( &$time ) {
2644 $this->wallClockOverride =& $time;
2645 $this->cache->setMockTime( $time );
2646 foreach ( $this->processCaches as $pCache ) {
2647 $pCache->setMockTime( $time );
2648 }
2649 }
2650}
Class representing a cache/ephemeral data store.
Definition BagOStuff.php:63
A BagOStuff object with no objects in it.
Handles a simple LRU key/value map with a maximum number of entries.
Multi-datacenter aware caching interface.
int $callbackDepth
Callback stack depth for getWithSetCallback()
__construct(array $params)
static int $FLD_FLAGS
@noinspection PhpUnusedPrivateFieldInspection
resolveCTL( $value, $curTTL, $curInfo, $touchedCallback)
unwrap( $wrapped, $now)
checkAndSetCooloff( $key, $kClass, $elapsed, $lockTSE, $hasLock)
static int $PURGE_TIME
Key to the tombstone entry timestamp.
worthRefreshPopular( $asOf, $ageNew, $timeTillRefresh, $now)
Check if a key is due for randomized regeneration due to its popularity.
static int $FLD_GENERATION_TIME
Key to how long it took to generate the value.
multiRemap(array $ids, array $res)
Get an (ID => value) map from (i) a non-unique list of entity IDs, and (ii) the list of corresponding...
determineKeyClassForStats( $key)
static int $FLD_VALUE_VERSION
Key to collection cache version number.
touchCheckKey( $key, $holdoff=self::HOLDOFF_TTL)
Purge a "check" key from all datacenters, invalidating keys that use it.
isValid( $value, $asOf, $minAsOf, $purgeTime=null)
Check if $value is not false, versioned (if needed), and not older than $minTime (if set)
adaptiveTTL( $mtime, $maxTTL, $minTTL=30, $factor=0.2)
Get a TTL that is higher for objects that have not changed recently.
string $cluster
Cache cluster name for mcrouter use.
static int $LOCK_TTL
Seconds to keep lock keys around.
isVolatileValueAgeNegligible( $age)
int $warmupKeyMisses
Key fetched.
getProcessCacheKey( $key, $version)
float null $wallClockOverride
scheduleAsyncRefresh( $key, $ttl, $callback, $opts)
mixed[] $warmupCache
Temporary warm-up cache.
static int $RAMPUP_TTL
Seconds to ramp up the chance of regeneration due to expected time-till-refresh.
static int $PURGE_HOLDOFF
Key to the tombstone entry hold-off TTL.
getMulti(array $keys, &$curTTLs=[], array $checkKeys=[], &$info=null)
Fetch the value of several keys from cache.
relayPurge( $key, $ttl, $holdoff)
Do the actual async bus purge of a key.
getLastError()
Get the "last error" registered; clearLastError() should be called manually.
BagOStuff $cache
The local datacenter cache.
static int $FLD_TTL
Key to the original TTL.
processCheckKeys(array $timeKeys, array $wrappedValues, $now)
getCheckKeyTime( $key)
Fetch the value of a timestamp "check" key.
getNonProcessCachedMultiKeys(ArrayIterator $keys, array $opts)
relayDelete( $key)
Do the actual async bus delete of a key.
getMultiWithUnionSetCallback(ArrayIterator $keyedIds, $ttl, callable $callback, array $opts=[])
Method to fetch/regenerate multiple cache keys at once.
LoggerInterface $logger
static prefixCacheKeys(array $keys, $prefix)
getMultiWithSetCallback(ArrayIterator $keyedIds, $ttl, callable $callback, array $opts=[])
Method to fetch multiple cache keys at once with regeneration.
wrap( $value, $ttl, $version, $now, $walltime)
makeMultiKeys(array $ids, $keyCallback)
Get an iterator of (cache key => entity ID) for a list of entity IDs.
static newEmpty()
Get an instance that wraps EmptyBagOStuff.
worthRefreshExpiring( $curTTL, $lowTTL)
Check if a key is nearing expiration and thus due for randomized regeneration.
getWithSetCallback( $key, $ttl, $callback, array $opts=[])
Method to fetch/regenerate cache keys.
bool $useInterimHoldOffCaching
Whether to use "interim" caching while keys are tombstoned.
static float $TINY_NEGATIVE
Tiny negative float to use when CTL comes up >= 0 due to clock skew.
makeGlobalKey( $class,... $components)
resolveTouched( $value, $lastPurge, $touchedCallback)
makeKey( $class,... $components)
setInterimValue( $key, $value, $ttl, $version, $walltime)
useInterimHoldOffCaching( $enabled)
Enable or disable the use of brief caching for tombstoned keys.
static int $CHECK_KEY_TTL
Seconds to keep dependency purge keys around.
static int $COOLOFF_TTL
Seconds to no-op key set() calls to avoid large blob I/O stampedes.
StatsdDataFactoryInterface $stats
clearProcessCache()
Clear the in-process caches; useful for testing.
string $region
Physical region for mcrouter use.
static int $INTERIM_KEY_TTL
Seconds to keep interim value keys for tombstoned keys around.
fetchOrRegenerate( $key, $ttl, $callback, array $opts)
Do the actual I/O for getWithSetCallback() when needed.
float $epoch
Unix timestamp of the oldest possible valid values.
callable null $asyncHandler
Function that takes a WAN cache callback and runs it later.
resolveBusyValue( $busyValue)
reap( $key, $purgeTimestamp, &$isStale=false)
Set a key to soon expire in the local cluster if it pre-dates $purgeTimestamp.
makePurgeValue( $timestamp, $holdoff)
getRawKeysForWarmup(array $keys, array $checkKeys)
setLogger(LoggerInterface $logger)
static int $FLD_FORMAT_VERSION
Key to WAN cache version number.
reapCheckKey( $key, $purgeTimestamp, &$isStale=false)
Set a "check" key to soon expire in the local cluster if it pre-dates $purgeTimestamp.
static int $RECENT_SET_LOW_MS
Min millisecond set() backoff during hold-off (far less than INTERIM_KEY_TTL)
static int $GENERATION_SLOW_SEC
Consider value generation slow if it takes more than this many seconds.
static int $FLD_TIME
Key to the cache timestamp.
clearLastError()
Clear the "last error" registry.
static int $RECENT_SET_HIGH_MS
Max millisecond set() backoff during hold-off (far less than INTERIM_KEY_TTL)
static int $SET_DELAY_HIGH_MS
Milliseconds of key fetch/validate/regenerate delay prone to set() stampedes.
static int $FLD_VALUE
Key to the cached value.
MapCacheLRU[] $processCaches
Map of group PHP instance caches.
static float $TINY_POSTIVE
Tiny positive float to use when using "minTime" to assert an inequality.
string $secret
Stable secret used for hasing long strings into key components.
resetCheckKey( $key)
Delete a "check" key from all datacenters, invalidating keys that use it.
static int $VERSION
Cache format version number.
getInterimValue( $key, $minAsOf)
yieldStampedeLock( $key, $hasLock)
isAliveOrInGracePeriod( $curTTL, $graceTTL)
Check if a key is fresh or in the grace window and thus due for randomized reuse.
hash256( $component)
Hash a possibly long string into a suitable component for makeKey()/makeGlobalKey()
getMultiCheckKeyTime(array $keys)
Fetch the values of each timestamp "check" key.
bool $mcrouterAware
Whether to use mcrouter key prefixing for routing.
Generic interface for lightweight expiring object stores.
Generic interface for object stores with key encoding methods.
$cache
Definition mcc.php:33