22 use Liuggio\StatsdClient\Factory\StatsdDataFactoryInterface;
23 use Psr\Log\LoggerAwareInterface;
24 use Psr\Log\LoggerInterface;
25 use Psr\Log\NullLogger;
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;
159 const TTL_UNCACHEABLE = -1;
164 const TTL_LAGGED = 30;
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;
184 const MIN_TIMESTAMP_NONE = 0.0;
187 const PC_PRIMARY =
'primary:1000';
190 const PASS_BY_REF = -1;
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;
283 $this->
setLogger( $params[
'logger'] ??
new NullLogger() );
285 $this->asyncHandler = $params[
'asyncHandler'] ??
null;
354 final public function get(
355 $key, &$curTTL =
null, array $checkKeys = [], &$info = null
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 ) {
363 'asOf' => $infoByKey[$key][
'asOf'] ??
null,
364 'tombAsOf' => $infoByKey[$key][
'tombAsOf'] ??
null,
365 'lastCKPurge' => $infoByKey[$key][
'lastCKPurge'] ??
null,
366 'version' => $infoByKey[$key][
'version'] ?? null
369 $info = $infoByKey[$key][
'asOf'] ??
null;
372 return $values[$key] ??
false;
400 array $checkKeys = [],
407 $vPrefixLen = strlen( self::$VALUE_KEY_PREFIX );
410 $checkKeysForAll = [];
411 $checkKeysByKey = [];
413 foreach ( $checkKeys as $i => $checkKeyGroup ) {
415 $checkKeysFlat = array_merge( $checkKeysFlat, $prefixed );
417 if ( is_int( $i ) ) {
418 $checkKeysForAll = array_merge( $checkKeysForAll, $prefixed );
420 $checkKeysByKey[$i] = $prefixed;
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 ) );
429 $this->warmupKeyMisses += count( $keysGet );
434 $wrappedValues += $this->cache->getMulti( $keysGet );
440 $purgeValuesForAll = $this->
processCheckKeys( $checkKeysForAll, $wrappedValues, $now );
441 $purgeValuesByKey = [];
442 foreach ( $checkKeysByKey as $cacheKey => $checks ) {
443 $purgeValuesByKey[$cacheKey] =
448 foreach ( $valueKeys as $vKey ) {
449 $key = substr( $vKey, $vPrefixLen );
450 list( $value, $keyInfo ) = $this->
unwrap( $wrappedValues[$vKey] ??
false, $now );
453 $purgeValues = $purgeValuesForAll;
454 if ( isset( $purgeValuesByKey[$key] ) ) {
455 $purgeValues = array_merge( $purgeValues, $purgeValuesByKey[$key] );
459 foreach ( $purgeValues as $purge ) {
460 $lastCKPurge = max( $purge[self::$PURGE_TIME], $lastCKPurge );
462 if ( $value !==
false && $safeTimestamp >= $keyInfo[
'asOf'] ) {
464 $ago = min( $purge[self::$PURGE_TIME] - $now, self::$TINY_NEGATIVE );
466 $keyInfo[
'curTTL'] = min( $keyInfo[
'curTTL'], $ago );
469 $keyInfo[
'lastCKPurge'] = $lastCKPurge;
471 if ( $value !==
false ) {
472 $result[$key] = $value;
474 if ( $keyInfo[
'curTTL'] !==
null ) {
475 $curTTLs[$key] = $keyInfo[
'curTTL'];
478 $infoByKey[$key] = ( $info === self::PASS_BY_REF )
497 foreach ( $timeKeys as $timeKey ) {
498 $purge = isset( $wrappedValues[$timeKey] )
501 if ( $purge ===
false ) {
504 $this->cache->add( $timeKey, $newVal, self::$CHECK_KEY_TTL );
507 $purgeValues[] = $purge;
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;
607 'Rejected set() for {cachekey} due to pending writes.',
608 [
'cachekey' => $key ]
616 if ( $lag ===
false || ( $lag + $age ) > self::MAX_READ_LAG ) {
618 if ( $age > self::MAX_READ_LAG ) {
619 if ( $lockTSE >= 0 ) {
623 'Lowered set() TTL for {cachekey} due to snapshot lag.',
624 [
'cachekey' => $key,
'lag' => $lag,
'age' => $age ]
628 'Rejected set() for {cachekey} due to snapshot lag.',
629 [
'cachekey' => $key,
'lag' => $lag,
'age' => $age ]
635 } elseif ( $lag ===
false || $lag > self::MAX_READ_LAG ) {
636 if ( $lockTSE >= 0 ) {
637 $logicalTTL = min( $ttl ?: INF, self::TTL_LAGGED );
639 $ttl = min( $ttl ?: INF, self::TTL_LAGGED );
641 $this->logger->warning(
642 'Lowered set() TTL for {cachekey} due to replication lag.',
643 [
'cachekey' => $key,
'lag' => $lag,
'age' => $age ]
646 } elseif ( $lockTSE >= 0 ) {
650 'Lowered set() TTL for {cachekey} due to high read lag.',
651 [
'cachekey' => $key,
'lag' => $lag,
'age' => $age ]
655 'Rejected set() for {cachekey} due to high read lag.',
656 [
'cachekey' => $key,
'lag' => $lag,
'age' => $age ]
664 $wrapped = $this->
wrap( $value, $logicalTTL ?: $ttl, $version, $now, $walltime );
665 $storeTTL = $ttl + $staleTTL;
668 $ok = $this->cache->add( self::$VALUE_KEY_PREFIX . $key, $wrapped, $storeTTL );
670 $ok = $this->cache->merge(
671 self::$VALUE_KEY_PREFIX . $key,
672 function (
$cache, $key, $cWrapped ) use ( $wrapped ) {
674 return ( is_string( $cWrapped ) ) ?
false : $wrapped;
745 final public function delete( $key, $ttl = self::HOLDOFF_TTL ) {
748 $ok = $this->
relayDelete( self::$VALUE_KEY_PREFIX . $key );
751 $ok = $this->
relayPurge( self::$VALUE_KEY_PREFIX . $key, $ttl, self::HOLDOFF_TTL_NONE );
755 $this->stats->increment(
"wanobjectcache.$kClass.delete." . ( $ok ?
'ok' :
'error' ) );
846 foreach (
$keys as $key ) {
847 $rawKeys[$key] = self::$TIME_KEY_PREFIX . $key;
850 $rawValues = $this->cache->getMulti( $rawKeys );
851 $rawValues += array_fill_keys( $rawKeys,
false );
854 foreach ( $rawKeys as $key => $rawKey ) {
856 if ( $purge !==
false ) {
869 $times[$key] = $time;
911 $ok = $this->
relayPurge( self::$TIME_KEY_PREFIX . $key, self::$CHECK_KEY_TTL, $holdoff );
914 $this->stats->increment(
"wanobjectcache.$kClass.ck_touch." . ( $ok ?
'ok' :
'error' ) );
948 $ok = $this->
relayDelete( self::$TIME_KEY_PREFIX . $key );
951 $this->stats->increment(
"wanobjectcache.$kClass.ck_reset." . ( $ok ?
'ok' :
'error' ) );
1262 $version = $opts[
'version'] ??
null;
1263 $pcTTL = $opts[
'pcTTL'] ?? self::TTL_UNCACHEABLE;
1264 $pCache = ( $pcTTL >= 0 )
1271 if ( $pCache && $this->callbackDepth == 0 ) {
1272 $cached = $pCache->get( $this->
getProcessCacheKey( $key, $version ), $pcTTL,
false );
1273 if ( $cached !==
false ) {
1279 list( $value, $valueVersion, $curAsOf ) =
$res;
1280 if ( $valueVersion !== $version ) {
1285 $this->
makeGlobalKey(
'WANCache-key-variant', md5( $key ), $version ),
1288 [
'version' =>
null,
'minAsOf' => $curAsOf ] + $opts
1293 if ( $pCache && $value !==
false ) {
1317 $checkKeys = $opts[
'checkKeys'] ?? [];
1318 $graceTTL = $opts[
'graceTTL'] ?? self::GRACE_TTL_NONE;
1319 $minAsOf = $opts[
'minAsOf'] ?? self::MIN_TIMESTAMP_NONE;
1320 $hotTTR = $opts[
'hotTTR'] ?? self::HOT_TTR;
1321 $lowTTL = $opts[
'lowTTL'] ?? min( self::LOW_TTL, $ttl );
1322 $ageNew = $opts[
'ageNew'] ?? self::AGE_NEW;
1323 $touchedCb = $opts[
'touchedCallback'] ??
null;
1329 $curTTL = self::PASS_BY_REF;
1330 $curInfo = self::PASS_BY_REF;
1331 $curValue = $this->
get( $key, $curTTL, $checkKeys, $curInfo );
1333 list( $curTTL, $LPT ) = $this->
resolveCTL( $curValue, $curTTL, $curInfo, $touchedCb );
1336 $this->
isValid( $curValue, $curInfo[
'asOf'], $minAsOf ) &&
1339 $preemptiveRefresh = (
1343 if ( !$preemptiveRefresh ) {
1344 $this->stats->increment(
"wanobjectcache.$kClass.hit.good" );
1346 return [ $curValue, $curInfo[
'version'], $curInfo[
'asOf'] ];
1348 $this->stats->increment(
"wanobjectcache.$kClass.hit.refresh" );
1350 return [ $curValue, $curInfo[
'version'], $curInfo[
'asOf'] ];
1355 $isKeyTombstoned = ( $curInfo[
'tombAsOf'] !== null );
1356 if ( $isKeyTombstoned ) {
1358 list( $possValue, $possInfo ) = $this->
getInterimValue( $key, $minAsOf );
1362 $possValue = $curValue;
1363 $possInfo = $curInfo;
1369 $this->
isValid( $possValue, $possInfo[
'asOf'], $minAsOf, $LPT ) &&
1372 $this->stats->increment(
"wanobjectcache.$kClass.hit.volatile" );
1374 return [ $possValue, $possInfo[
'version'], $curInfo[
'asOf'] ];
1377 $lockTSE = $opts[
'lockTSE'] ?? self::TSE_NONE;
1378 $busyValue = $opts[
'busyValue'] ??
null;
1379 $staleTTL = $opts[
'staleTTL'] ?? self::STALE_TTL_NONE;
1380 $version = $opts[
'version'] ??
null;
1383 $useRegenerationLock =
1393 ( $curTTL !==
null && $curTTL <= 0 && abs( $curTTL ) <= $lockTSE ) ||
1396 ( $busyValue !==
null && $possValue ===
false );
1402 if ( $useRegenerationLock && !$hasLock ) {
1403 if ( $this->
isValid( $possValue, $possInfo[
'asOf'], $minAsOf ) ) {
1404 $this->stats->increment(
"wanobjectcache.$kClass.hit.stale" );
1406 return [ $possValue, $possInfo[
'version'], $curInfo[
'asOf'] ];
1407 } elseif ( $busyValue !==
null ) {
1408 $miss = is_infinite( $minAsOf ) ?
'renew' :
'miss';
1409 $this->stats->increment(
"wanobjectcache.$kClass.$miss.busy" );
1411 return [ $this->
resolveBusyValue( $busyValue ), $version, $curInfo[
'asOf'] ];
1421 ( $curInfo[
'version'] === $version ) ? $curValue :
false,
1424 ( $curInfo[
'version'] === $version ) ? $curInfo[
'asOf'] :
null
1432 $elapsed = max( $postCallbackTime - $initialTime, 0.0 );
1437 ( $value !==
false && $ttl >= 0 ) &&
1439 ( !$useRegenerationLock || $hasLock || $isKeyTombstoned ) &&
1444 $walltime = max( $postCallbackTime - $preCallbackTime, 0.0 );
1445 $this->stats->timing(
"wanobjectcache.$kClass.regen_walltime", 1e3 * $walltime );
1447 if ( $isKeyTombstoned ) {
1448 $this->
setInterimValue( $key, $value, $lockTSE, $version, $walltime );
1451 'since' => $setOpts[
'since'] ?? $preCallbackTime,
1452 'version' => $version,
1453 'staleTTL' => $staleTTL,
1454 'lockTSE' => $lockTSE,
1455 'creating' => ( $curValue === false ),
1456 'walltime' => $walltime
1458 $this->
set( $key, $value, $ttl, $finalSetOpts );
1464 $miss = is_infinite( $minAsOf ) ?
'renew' :
'miss';
1465 $this->stats->increment(
"wanobjectcache.$kClass.$miss.compute" );
1467 return [ $value, $version, $curInfo[
'asOf'] ];
1476 return $this->cache->add( self::$MUTEX_KEY_PREFIX . $key, 1, self::$LOCK_TTL );
1488 $this->cache->changeTTL( self::$MUTEX_KEY_PREFIX . $key, $this->
getCurrentTime() - 60 );
1497 return ( $age < mt_rand( self::$RECENT_SET_LOW_MS, self::$RECENT_SET_HIGH_MS ) / 1e3 );
1509 $this->stats->timing(
"wanobjectcache.$kClass.regen_set_delay", 1e3 * $elapsed );
1517 if ( $lockTSE < 0 || $hasLock ) {
1519 } elseif ( $elapsed <= self::$SET_DELAY_HIGH_MS * 1e3 ) {
1523 $this->cache->clearLastError();
1525 !$this->cache->add( self::$COOLOFF_KEY_PREFIX . $key, 1, self::$COOLOFF_TTL ) &&
1529 $this->stats->increment(
"wanobjectcache.$kClass.cooloff_bounce" );
1545 private function resolveCTL( $value, $curTTL, $curInfo, $touchedCallback ) {
1546 if ( $touchedCallback ===
null || $value ===
false ) {
1547 return [ $curTTL, max( $curInfo[
'tombAsOf'], $curInfo[
'lastCKPurge'] ) ];
1550 $touched = $touchedCallback( $value );
1551 if ( $touched !==
null && $touched >= $curInfo[
'asOf'] ) {
1552 $curTTL = min( $curTTL, self::$TINY_NEGATIVE, $curInfo[
'asOf'] - $touched );
1555 return [ $curTTL, max( $curInfo[
'tombAsOf'], $curInfo[
'lastCKPurge'], $touched ) ];
1566 return ( $touchedCallback ===
null || $value ===
false )
1568 : max( $touchedCallback( $value ), $lastPurge );
1580 $wrapped = $this->cache->get( self::$INTERIM_KEY_PREFIX . $key );
1582 list( $value, $keyInfo ) = $this->
unwrap( $wrapped, $now );
1583 if ( $this->
isValid( $value, $keyInfo[
'asOf'], $minAsOf ) ) {
1584 return [ $value, $keyInfo ];
1588 return $this->
unwrap(
false, $now );
1599 $ttl = max( self::$INTERIM_KEY_TTL, (
int)$ttl );
1601 $wrapped = $this->
wrap( $value, $ttl, $version, $this->
getCurrentTime(), $walltime );
1602 $this->cache->merge(
1603 self::$INTERIM_KEY_PREFIX . $key,
1604 function () use ( $wrapped ) {
1617 return ( $busyValue instanceof Closure ) ? $busyValue() : $busyValue;
1687 ArrayIterator $keyedIds, $ttl, callable $callback, array $opts = []
1692 $opts[
'checkKeys'] ?? []
1694 $this->warmupKeyMisses = 0;
1698 $func =
function ( $oldValue, &$ttl, &$setOpts, $oldAsOf ) use ( $callback, &$id ) {
1699 return $callback( $id, $oldValue, $ttl, $setOpts, $oldAsOf );
1703 foreach ( $keyedIds as $key => $id ) {
1707 $this->warmupCache = [];
1778 ArrayIterator $keyedIds, $ttl, callable $callback, array $opts = []
1780 $checkKeys = $opts[
'checkKeys'] ?? [];
1781 unset( $opts[
'lockTSE'] );
1782 unset( $opts[
'busyValue'] );
1787 $this->warmupKeyMisses = 0;
1795 $curByKey = $this->
getMulti( $keysByIdGet, $curTTLs, $checkKeys, $asOfs );
1796 foreach ( $keysByIdGet as $id => $key ) {
1797 if ( !array_key_exists( $key, $curByKey ) || $curTTLs[$key] < 0 ) {
1804 $newTTLsById = array_fill_keys( $idsRegen, $ttl );
1805 $newValsById = $idsRegen ? $callback( $idsRegen, $newTTLsById, $newSetOpts ) : [];
1809 $func =
function ( $oldValue, &$ttl, &$setOpts, $oldAsOf )
1810 use ( $callback, &$id, $newValsById, $newTTLsById, $newSetOpts )
1812 if ( array_key_exists( $id, $newValsById ) ) {
1814 $newValue = $newValsById[$id];
1815 $ttl = $newTTLsById[$id];
1816 $setOpts = $newSetOpts;
1820 $ttls = [ $id => $ttl ];
1821 $newValue = $callback( [ $id ], $ttls, $setOpts )[$id];
1830 foreach ( $keyedIds as $key => $id ) {
1834 $this->warmupCache = [];
1851 final public function reap( $key, $purgeTimestamp, &$isStale =
false ) {
1852 $minAsOf = $purgeTimestamp + self::HOLDOFF_TTL;
1853 $wrapped = $this->cache->get( self::$VALUE_KEY_PREFIX . $key );
1854 if ( is_array( $wrapped ) && $wrapped[self::$FLD_TIME] < $minAsOf ) {
1856 $this->logger->warning(
"Reaping stale value key '$key'." );
1857 $ttlReap = self::HOLDOFF_TTL;
1858 $ok = $this->cache->changeTTL( self::$VALUE_KEY_PREFIX . $key, $ttlReap );
1860 $this->logger->error(
"Could not complete reap of key '$key'." );
1880 final public function reapCheckKey( $key, $purgeTimestamp, &$isStale =
false ) {
1881 $purge = $this->
parsePurgeValue( $this->cache->get( self::$TIME_KEY_PREFIX . $key ) );
1882 if ( $purge && $purge[self::$PURGE_TIME] < $purgeTimestamp ) {
1884 $this->logger->warning(
"Reaping stale check key '$key'." );
1885 $ok = $this->cache->changeTTL( self::$TIME_KEY_PREFIX . $key, self::TTL_SECOND );
1887 $this->logger->error(
"Could not complete reap of check key '$key'." );
1905 public function makeKey( $class, ...$components ) {
1906 return $this->cache->makeKey( ...func_get_args() );
1917 return $this->cache->makeGlobalKey( ...func_get_args() );
1928 return hash_hmac(
'sha256', $component, $this->secret );
1983 foreach ( $ids as $id ) {
1985 if ( strlen( $id ) > 64 ) {
1986 $this->logger->warning( __METHOD__ .
": long ID '$id'; use hash256()" );
1988 $key = $keyCallback( $id, $this );
1990 if ( !isset( $idByKey[$key] ) ) {
1991 $idByKey[$key] = $id;
1992 } elseif ( (
string)$id !== (
string)$idByKey[$key] ) {
1993 throw new UnexpectedValueException(
1994 "Cache key collision; IDs ('$id','{$idByKey[$key]}') map to '$key'"
1999 return new ArrayIterator( $idByKey );
2038 if ( count( $ids ) !== count(
$res ) ) {
2041 $ids = array_keys( array_flip( $ids ) );
2042 if ( count( $ids ) !== count(
$res ) ) {
2043 throw new UnexpectedValueException(
"Multi-key result does not match ID list" );
2047 return array_combine( $ids,
$res );
2055 $code = $this->cache->getLastError();
2072 $this->cache->clearLastError();
2081 $this->processCaches = [];
2114 return $this->cache->getQoS( $flag );
2180 public function adaptiveTTL( $mtime, $maxTTL, $minTTL = 30, $factor = 0.2 ) {
2181 if ( is_float( $mtime ) || ctype_digit( $mtime ) ) {
2182 $mtime = (int)$mtime;
2185 if ( !is_int( $mtime ) || $mtime <= 0 ) {
2191 return (
int)min( $maxTTL, max( $minTTL, $factor * $age ) );
2213 if ( $this->mcrouterAware ) {
2216 $ok = $this->cache->set(
2217 "/*/{$this->cluster}/{$key}",
2223 $ok = $this->cache->set(
2240 if ( $this->mcrouterAware ) {
2243 $ok = $this->cache->delete(
"/*/{$this->cluster}/{$key}" );
2246 $ok = $this->cache->delete( $key );
2261 if ( !$this->asyncHandler ) {
2266 $func(
function () use ( $key, $ttl, $callback, $opts ) {
2267 $opts[
'minAsOf'] = INF;
2288 if ( $curTTL > 0 ) {
2290 } elseif ( $graceTTL <= 0 ) {
2294 $ageStale = abs( $curTTL );
2295 $curGTTL = ( $graceTTL - $ageStale );
2296 if ( $curGTTL <= 0 ) {
2318 if ( $lowTTL <= 0 ) {
2320 } elseif ( $curTTL >= $lowTTL ) {
2322 } elseif ( $curTTL <= 0 ) {
2326 $chance = ( 1 - $curTTL / $lowTTL );
2329 return mt_rand( 1, 1e9 ) <= 1e9 * $chance;
2348 if ( $ageNew < 0 || $timeTillRefresh <= 0 ) {
2352 $age = $now - $asOf;
2353 $timeOld = $age - $ageNew;
2354 if ( $timeOld <= 0 ) {
2358 $popularHitsPerSec = 1;
2362 $refreshWindowSec = max( $timeTillRefresh - $ageNew - self::$RAMPUP_TTL / 2, 1 );
2366 $chance = 1 / ( $popularHitsPerSec * $refreshWindowSec );
2369 $chance *= ( $timeOld <=
self::$RAMPUP_TTL ) ? $timeOld / self::$RAMPUP_TTL : 1;
2372 return mt_rand( 1, 1e9 ) <= 1e9 * $chance;
2384 protected function isValid( $value, $asOf, $minAsOf, $purgeTime =
null ) {
2386 $safeMinAsOf = max( $minAsOf, $purgeTime + self::$TINY_POSTIVE );
2388 if ( $value ===
false ) {
2390 } elseif ( $safeMinAsOf > 0 && $asOf < $minAsOf ) {
2405 private function wrap( $value, $ttl, $version, $now, $walltime ) {
2410 self::$FLD_VALUE => $value,
2411 self::$FLD_TTL => $ttl,
2412 self::$FLD_TIME => $now
2414 if ( $version !==
null ) {
2417 if ( $walltime >= self::$GENERATION_SLOW_SEC ) {
2437 $info = [
'asOf' =>
null,
'curTTL' =>
null,
'version' =>
null,
'tombAsOf' => null ];
2439 if ( is_array( $wrapped ) ) {
2442 ( $wrapped[self::$FLD_FORMAT_VERSION] ??
null ) === self::$VERSION &&
2443 $wrapped[self::$FLD_TIME] >= $this->epoch
2445 if ( $wrapped[self::$FLD_TTL] > 0 ) {
2448 $curTTL = max( $wrapped[self::$FLD_TTL] - $age, 0.0 );
2456 $info[
'curTTL'] = $curTTL;
2461 if ( $purge !==
false ) {
2463 $info[
'curTTL'] = min( $purge[self::$PURGE_TIME] - $now, self::$TINY_NEGATIVE );
2468 return [ $value, $info ];
2478 foreach (
$keys as $key ) {
2479 $res[] = $prefix . $key;
2490 $parts = explode(
':', $key, 3 );
2493 return strtr( $parts[1] ?? $parts[0],
'.',
'_' );
2502 if ( !is_string( $value ) ) {
2506 $segments = explode(
':', $value, 3 );
2508 !isset( $segments[0] ) ||
2509 !isset( $segments[1] ) ||
2510 "{$segments[0]}:" !== self::$PURGE_VAL_PREFIX
2515 if ( !isset( $segments[2] ) ) {
2517 $segments[2] = self::HOLDOFF_TTL;
2520 if ( $segments[1] < $this->epoch ) {
2526 self::$PURGE_TIME => (float)$segments[1],
2527 self::$PURGE_HOLDOFF => (
int)$segments[2],
2537 return self::$PURGE_VAL_PREFIX . (float)$timestamp .
':' . (
int)$holdoff;
2545 if ( !isset( $this->processCaches[$group] ) ) {
2546 list( , $size ) = explode(
':', $group );
2547 $this->processCaches[$group] =
new MapCacheLRU( (
int)$size );
2548 if ( $this->wallClockOverride !==
null ) {
2549 $this->processCaches[$group]->setMockTime( $this->wallClockOverride );
2553 return $this->processCaches[$group];
2562 return $key .
' ' . (int)$version;
2571 $pcTTL = $opts[
'pcTTL'] ?? self::TTL_UNCACHEABLE;
2574 if ( $pcTTL > 0 && $this->callbackDepth == 0 ) {
2575 $version = $opts[
'version'] ??
null;
2576 $pCache = $this->
getProcessCache( $opts[
'pcGroup'] ?? self::PC_PRIMARY );
2577 foreach (
$keys as $key => $id ) {
2578 if ( !$pCache->has( $this->getProcessCacheKey( $key, $version ), $pcTTL ) ) {
2579 $keysMissing[$id] = $key;
2584 return $keysMissing;
2599 foreach (
$keys as $key ) {
2600 $keysWarmUp[] = self::$VALUE_KEY_PREFIX . $key;
2603 foreach ( $checkKeys as $i => $checkKeyOrKeys ) {
2604 if ( is_int( $i ) ) {
2606 $keysWarmUp[] = self::$TIME_KEY_PREFIX . $checkKeyOrKeys;
2609 $keysWarmUp = array_merge(
2611 self::prefixCacheKeys( $checkKeyOrKeys, self::$TIME_KEY_PREFIX )
2627 if ( $this->wallClockOverride ) {
2631 $clockTime = (float)time();
2637 return max( microtime(
true ), $clockTime );
2645 $this->wallClockOverride =& $time;
2646 $this->cache->setMockTime( $time );
2647 foreach ( $this->processCaches as $pCache ) {
2648 $pCache->setMockTime( $time );