22 use Liuggio\StatsdClient\Factory\StatsdDataFactoryInterface;
23 use Psr\Log\LoggerAwareInterface;
24 use Psr\Log\LoggerInterface;
25 use Psr\Log\NullLogger;
170 private const MAX_COMMIT_DELAY = 3;
172 private const MAX_READ_LAG = 7;
174 public const HOLDOFF_TTL = self::MAX_COMMIT_DELAY + self::MAX_READ_LAG + 1;
177 private const LOW_TTL = 30;
179 public const TTL_LAGGED = 30;
182 private const HOT_TTR = 900;
184 private const AGE_NEW = 60;
187 private const TSE_NONE = -1;
190 public const STALE_TTL_NONE = 0;
192 public const GRACE_TTL_NONE = 0;
194 public const HOLDOFF_TTL_NONE = 0;
196 public const HOLDOFF_NONE = self::HOLDOFF_TTL_NONE;
199 public const MIN_TIMESTAMP_NONE = 0.0;
202 private const PC_PRIMARY =
'primary:1000';
208 private const SCHEME_HASH_TAG = 1;
210 private const SCHEME_HASH_STOP = 2;
251 public const KEY_VERSION =
'version';
253 public const KEY_AS_OF =
'asOf';
255 public const KEY_TTL =
'ttl';
257 public const KEY_CUR_TTL =
'curTTL';
259 public const KEY_TOMB_AS_OF =
'tombAsOf';
261 public const KEY_CHECK_AS_OF =
'lastCKPurge';
279 private const TYPE_VALUE =
'v';
281 private const TYPE_TIMESTAMP =
't';
283 private const TYPE_MUTEX =
'm';
285 private const TYPE_INTERIM =
'i';
287 private const TYPE_COOLOFF =
'c';
290 private const PURGE_VAL_PREFIX =
'PURGED:';
329 $this->cache = $params[
'cache'];
330 $this->region = $params[
'region'] ??
'main';
331 $this->cluster = $params[
'cluster'] ??
'wan-main';
332 $this->mcrouterAware = !empty( $params[
'mcrouterAware'] );
333 $this->epoch = $params[
'epoch'] ?? 0;
334 $this->secret = $params[
'secret'] ?? (string)$this->epoch;
335 $this->coalesceKeys = $params[
'coalesceKeys'] ??
false;
336 if ( !empty( $params[
'mcrouterAware'] ) ) {
338 $this->coalesceScheme = self::SCHEME_HASH_STOP;
343 $this->coalesceScheme = self::SCHEME_HASH_TAG;
346 $this->keyHighQps = $params[
'keyHighQps'] ?? 100;
347 $this->keyHighUplinkBps = $params[
'keyHighUplinkBps'] ?? ( 1e9 / 8 / 100 );
349 $this->
setLogger( $params[
'logger'] ??
new NullLogger() );
351 $this->asyncHandler = $params[
'asyncHandler'] ??
null;
353 $this->cache->registerWrapperInfoForStats(
356 [ __CLASS__,
'getCollectionFromKey' ]
430 final public function get( $key, &$curTTL =
null, array $checkKeys = [], &$info = [] ) {
439 $curTTL = $metadata[self::KEY_CUR_TTL];
440 $info = $legacyInfo ? $metadata[self::KEY_AS_OF] : $metadata;
471 array $checkKeys = [],
482 $resByKey = $this->
fetchKeys( $keys, $checkKeys );
483 foreach ( $resByKey as $key =>
$res ) {
487 if ( $value !==
false ) {
488 $valuesByKey[$key] = $value;
491 if ( $metadata[self::KEY_CUR_TTL] !==
null ) {
492 $curTTLs[$key] = $metadata[self::KEY_CUR_TTL];
495 $info[$key] = $legacyInfo ? $metadata[self::KEY_AS_OF] : $metadata;
523 $fullKeysNeeded = $valueKeys;
524 $checkKeysForAll = [];
525 $checkKeysByKey = [];
526 foreach ( $checkKeys as $i => $checkKeyOrKeyGroup ) {
528 if ( is_int( $i ) ) {
530 $fullKey = $this->
makeSisterKey( $checkKeyOrKeyGroup, self::TYPE_TIMESTAMP );
531 $fullKeysNeeded[] = $fullKey;
532 $checkKeysForAll[] = $fullKey;
535 foreach ( (array)$checkKeyOrKeyGroup as $checkKey ) {
536 $fullKey = $this->
makeSisterKey( $checkKey, self::TYPE_TIMESTAMP );
537 $fullKeysNeeded[] = $fullKey;
538 $checkKeysByKey[$i][] = $fullKey;
543 if ( $this->warmupCache ) {
546 $fullKeysMissing = array_diff( $fullKeysNeeded, array_keys( $wrappedValues ) );
547 if ( $fullKeysMissing ) {
548 $this->warmupKeyMisses += count( $fullKeysMissing );
549 $wrappedValues += $this->cache->getMulti( $fullKeysMissing );
553 $wrappedValues = $this->cache->getMulti( $fullKeysNeeded );
560 $purgeValuesForAll = $this->
processCheckKeys( $checkKeysForAll, $wrappedValues, $now );
561 $purgeValuesByKey = [];
562 foreach ( $checkKeysByKey as $cacheKey => $checks ) {
563 $purgeValuesByKey[$cacheKey] = $this->
processCheckKeys( $checks, $wrappedValues, $now );
568 foreach ( $valueKeys as $i => $vKey ) {
570 $key = current(
$keys );
573 list( $value, $metadata ) = $this->
unwrap(
574 array_key_exists( $vKey, $wrappedValues ) ? $wrappedValues[$vKey] :
false,
579 $purgeValues = $purgeValuesForAll;
580 if ( isset( $purgeValuesByKey[$key] ) ) {
581 $purgeValues = array_merge( $purgeValues, $purgeValuesByKey[$key] );
585 foreach ( $purgeValues as $purge ) {
586 $lastCKPurge = max( $purge[self::$PURGE_TIME], $lastCKPurge );
588 if ( $value !==
false && $safeTimestamp >= $metadata[self::KEY_AS_OF] ) {
590 $ago = min( $purge[self::$PURGE_TIME] - $now, self::$TINY_NEGATIVE );
592 $metadata[self::KEY_CUR_TTL] = min( $metadata[self::KEY_CUR_TTL], $ago );
595 $metadata[self::KEY_CHECK_AS_OF] = $lastCKPurge;
598 self::RES_VALUE => $value,
599 self::RES_METADATA => $metadata
615 foreach ( $timeKeys as $timeKey ) {
616 $purge = isset( $wrappedValues[$timeKey] )
619 if ( $purge ===
false ) {
622 $this->cache->add( $timeKey, $newVal, self::$CHECK_KEY_TTL );
625 $purgeValues[] = $purge;
710 final public function set( $key, $value, $ttl = self::TTL_INDEFINITE, array $opts = [] ) {
712 $lag = $opts[
'lag'] ?? 0;
713 $age = isset( $opts[
'since'] ) ? max( 0, $now - $opts[
'since'] ) : 0;
714 $pending = $opts[
'pending'] ??
false;
715 $lockTSE = $opts[
'lockTSE'] ?? self::TSE_NONE;
716 $staleTTL = $opts[
'staleTTL'] ?? self::STALE_TTL_NONE;
717 $creating = $opts[
'creating'] ??
false;
718 $version = $opts[
'version'] ??
null;
719 $walltime = $opts[
'walltime'] ??
null;
728 'Rejected set() for {cachekey} due to pending writes.',
729 [
'cachekey' => $key ]
741 if ( $age > self::MAX_READ_LAG ) {
743 if ( $walltime ===
null ) {
746 $mitigated =
'snapshot lag';
747 $mitigationTTL = self::TTL_SECOND;
748 } elseif ( ( $age - $walltime ) > self::MAX_READ_LAG ) {
751 $mitigated =
'snapshot lag (late regeneration)';
752 $mitigationTTL = self::TTL_UNCACHEABLE;
756 $mitigated =
'snapshot lag (high regeneration time)';
757 $mitigationTTL = self::TTL_SECOND;
759 } elseif ( $lag ===
false || $lag > self::MAX_READ_LAG ) {
762 $mitigated =
'replication lag';
763 $mitigationTTL = self::TTL_LAGGED;
764 } elseif ( ( $lag + $age ) > self::MAX_READ_LAG ) {
767 $mitigated =
'read lag';
768 $mitigationTTL = self::TTL_UNCACHEABLE;
772 $mitigationTTL =
null;
775 if ( $mitigationTTL === self::TTL_UNCACHEABLE ) {
776 $this->logger->warning(
777 "Rejected set() for {cachekey} due to $mitigated.",
778 [
'cachekey' => $key,
'lag' => $lag,
'age' => $age,
'walltime' => $walltime ]
787 if ( $mitigationTTL !==
null ) {
789 if ( $lockTSE >= 0 ) {
791 $logicalTTL = min( $ttl ?: INF, $mitigationTTL );
794 $ttl = min( $ttl ?: INF, max( $mitigationTTL, self::LOW_TTL ) );
797 $this->logger->warning(
798 "Lowered set() TTL for {cachekey} due to $mitigated.",
799 [
'cachekey' => $key,
'lag' => $lag,
'age' => $age,
'walltime' => $walltime ]
804 $wrapped = $this->
wrap( $value, $logicalTTL ?: $ttl, $version, $now, $walltime );
805 $storeTTL = $ttl + $staleTTL;
808 $ok = $this->cache->add(
814 $ok = $this->cache->merge(
816 static function (
$cache, $key, $cWrapped ) use ( $wrapped ) {
818 return ( is_string( $cWrapped ) ) ?
false : $wrapped;
889 final public function delete( $key, $ttl = self::HOLDOFF_TTL ) {
898 self::HOLDOFF_TTL_NONE
903 $this->stats->increment(
"wanobjectcache.$kClass.delete." . ( $ok ?
'ok' :
'error' ) );
994 foreach (
$keys as $key ) {
995 $rawKeys[$key] = $this->
makeSisterKey( $key, self::TYPE_TIMESTAMP );
998 $rawValues = $this->cache->getMulti( $rawKeys );
999 $rawValues += array_fill_keys( $rawKeys,
false );
1002 foreach ( $rawKeys as $key => $rawKey ) {
1004 if ( $purge !==
false ) {
1014 $time = (float)$now;
1017 $times[$key] = $time;
1061 self::$CHECK_KEY_TTL,
1066 $this->stats->increment(
"wanobjectcache.$kClass.ck_touch." . ( $ok ?
'ok' :
'error' ) );
1103 $this->stats->increment(
"wanobjectcache.$kClass.ck_reset." . ( $ok ?
'ok' :
'error' ) );
1413 $key, $ttl, $callback, array $opts = [], array $cbParams = []
1415 $version = $opts[
'version'] ??
null;
1416 $pcTTL = $opts[
'pcTTL'] ?? self::TTL_UNCACHEABLE;
1417 $pCache = ( $pcTTL >= 0 )
1424 if ( $pCache && $this->callbackDepth == 0 ) {
1425 $cached = $pCache->get( $this->
getProcessCacheKey( $key, $version ), $pcTTL, false );
1426 if ( $cached !==
false ) {
1427 $this->logger->debug(
"getWithSetCallback($key): process cache hit" );
1433 list( $value, $valueVersion, $curAsOf ) =
$res;
1434 if ( $valueVersion !== $version ) {
1438 $this->logger->debug(
"getWithSetCallback($key): using variant key" );
1440 $this->
makeGlobalKey(
'WANCache-key-variant', md5( $key ), $version ),
1443 [
'version' =>
null,
'minAsOf' => $curAsOf ] + $opts,
1449 if ( $pCache && $value !==
false ) {
1473 $checkKeys = $opts[
'checkKeys'] ?? [];
1474 $graceTTL = $opts[
'graceTTL'] ?? self::GRACE_TTL_NONE;
1475 $minAsOf = $opts[
'minAsOf'] ?? self::MIN_TIMESTAMP_NONE;
1476 $hotTTR = $opts[
'hotTTR'] ?? self::HOT_TTR;
1477 $lowTTL = $opts[
'lowTTL'] ?? min( self::LOW_TTL, $ttl );
1478 $ageNew = $opts[
'ageNew'] ?? self::AGE_NEW;
1479 $touchedCb = $opts[
'touchedCallback'] ??
null;
1488 $curTTL = $curInfo[self::KEY_CUR_TTL];
1490 list( $curTTL, $LPT ) = $this->
resolveCTL( $curValue, $curTTL, $curInfo, $touchedCb );
1493 $this->
isValid( $curValue, $curInfo[self::KEY_AS_OF], $minAsOf ) &&
1496 $preemptiveRefresh = (
1500 if ( !$preemptiveRefresh ) {
1501 $this->stats->timing(
1502 "wanobjectcache.$kClass.hit.good",
1506 return [ $curValue, $curInfo[self::KEY_VERSION], $curInfo[self::KEY_AS_OF] ];
1508 $this->logger->debug(
"fetchOrRegenerate($key): hit with async refresh" );
1509 $this->stats->timing(
1510 "wanobjectcache.$kClass.hit.refresh",
1514 return [ $curValue, $curInfo[self::KEY_VERSION], $curInfo[self::KEY_AS_OF] ];
1516 $this->logger->debug(
"fetchOrRegenerate($key): hit with sync refresh" );
1521 $isKeyTombstoned = ( $curInfo[self::KEY_TOMB_AS_OF] !== null );
1522 if ( $isKeyTombstoned ) {
1524 list( $possValue, $possInfo ) = $this->
getInterimValue( $key, $minAsOf );
1528 $possValue = $curValue;
1529 $possInfo = $curInfo;
1535 $this->
isValid( $possValue, $possInfo[self::KEY_AS_OF], $minAsOf, $LPT ) &&
1538 $this->logger->debug(
"fetchOrRegenerate($key): volatile hit" );
1539 $this->stats->timing(
1540 "wanobjectcache.$kClass.hit.volatile",
1544 return [ $possValue, $possInfo[self::KEY_VERSION], $curInfo[self::KEY_AS_OF] ];
1547 $lockTSE = $opts[
'lockTSE'] ?? self::TSE_NONE;
1548 $busyValue = $opts[
'busyValue'] ??
null;
1549 $staleTTL = $opts[
'staleTTL'] ?? self::STALE_TTL_NONE;
1550 $version = $opts[
'version'] ??
null;
1553 $useRegenerationLock =
1563 ( $curTTL !==
null && $curTTL <= 0 && abs( $curTTL ) <= $lockTSE ) ||
1566 ( $busyValue !==
null && $possValue ===
false );
1572 if ( $useRegenerationLock && !$hasLock ) {
1573 if ( $this->
isValid( $possValue, $possInfo[self::KEY_AS_OF], $minAsOf ) ) {
1574 $this->logger->debug(
"fetchOrRegenerate($key): returning stale value" );
1575 $this->stats->timing(
1576 "wanobjectcache.$kClass.hit.stale",
1580 return [ $possValue, $possInfo[self::KEY_VERSION], $curInfo[self::KEY_AS_OF] ];
1581 } elseif ( $busyValue !==
null ) {
1582 $miss = is_infinite( $minAsOf ) ?
'renew' :
'miss';
1583 $this->logger->debug(
"fetchOrRegenerate($key): busy $miss" );
1584 $this->stats->timing(
1585 "wanobjectcache.$kClass.$miss.busy",
1590 return [ $placeholderValue, $version, $curInfo[self::KEY_AS_OF] ];
1600 ( $curInfo[self::KEY_VERSION] === $version ) ? $curValue :
false,
1603 ( $curInfo[self::KEY_VERSION] === $version ) ? $curInfo[self::KEY_AS_OF] :
null,
1612 $elapsed = max( $postCallbackTime - $initialTime, 0.0 );
1617 ( $value !==
false && $ttl >= 0 ) &&
1619 ( !$useRegenerationLock || $hasLock || $isKeyTombstoned ) &&
1624 $walltime = max( $postCallbackTime - $preCallbackTime, 0.0 );
1625 $this->stats->timing(
"wanobjectcache.$kClass.regen_walltime", 1e3 * $walltime );
1627 if ( $isKeyTombstoned ) {
1628 $this->
setInterimValue( $key, $value, $lockTSE, $version, $walltime );
1632 'since' => $setOpts[
'since'] ?? $preCallbackTime,
1633 'version' => $version,
1634 'staleTTL' => $staleTTL,
1635 'lockTSE' => $lockTSE,
1636 'creating' => ( $curValue === false ),
1637 'walltime' => $walltime
1639 $this->
set( $key, $value, $ttl, $finalSetOpts );
1645 $miss = is_infinite( $minAsOf ) ?
'renew' :
'miss';
1646 $this->logger->debug(
"fetchOrRegenerate($key): $miss, new value computed" );
1647 $this->stats->timing(
1648 "wanobjectcache.$kClass.$miss.compute",
1652 return [ $value, $version, $curInfo[self::KEY_AS_OF] ];
1661 return $this->cache->add(
1677 $this->cache->changeTTL(
1693 foreach ( $baseKeys as $baseKey ) {
1708 if ( $this->coalesceKeys ===
'non-global' ) {
1709 $useColocationScheme = ( strncmp( $baseKey,
"global:", 7 ) !== 0 );
1711 $useColocationScheme = ( $this->coalesceKeys ===
true );
1714 if ( !$useColocationScheme ) {
1716 $fullKey =
'WANCache:' . $typeChar .
':' . $baseKey;
1717 } elseif ( $this->coalesceScheme === self::SCHEME_HASH_STOP ) {
1719 $fullKey =
'WANCache:' . $baseKey .
'|#|' . $typeChar;
1722 $fullKey =
'WANCache:{' . $baseKey .
'}:' . $typeChar;
1735 if ( substr( $sisterKey, -4 ) ===
'|#|v' ) {
1737 $collection = substr( $sisterKey, 9, strcspn( $sisterKey,
':|', 9 ) );
1738 } elseif ( substr( $sisterKey, -3 ) ===
'}:v' ) {
1740 $collection = substr( $sisterKey, 10, strcspn( $sisterKey,
':}', 10 ) );
1741 } elseif ( substr( $sisterKey, 9, 2 ) ===
'v:' ) {
1743 $collection = substr( $sisterKey, 11, strcspn( $sisterKey,
':', 11 ) );
1745 $collection =
'internal';
1756 return ( $age < mt_rand( self::$RECENT_SET_LOW_MS, self::$RECENT_SET_HIGH_MS ) / 1e3 );
1782 list( $estimatedSize ) = $this->cache->setNewPreparedValues( [ $valueKey => $value ] );
1806 $this->cache->clearLastError();
1809 $this->makeSisterKey( $key, self::TYPE_COOLOFF ),
1814 $this->cache->getLastError() === BagOStuff::ERR_NONE
1816 $this->stats->increment(
"wanobjectcache.$kClass.cooloff_bounce" );
1824 $this->stats->timing(
"wanobjectcache.$kClass.regen_set_delay", 1e3 * $elapsed );
1825 $this->stats->updateCount(
"wanobjectcache.$kClass.regen_set_bytes", $estimatedSize );
1838 private function resolveCTL( $value, $curTTL, $curInfo, $touchedCallback ) {
1839 if ( $touchedCallback ===
null || $value ===
false ) {
1842 max( $curInfo[self::KEY_TOMB_AS_OF], $curInfo[self::KEY_CHECK_AS_OF] )
1846 $touched = $touchedCallback( $value );
1847 if ( $touched !==
null && $touched >= $curInfo[self::KEY_AS_OF] ) {
1848 $curTTL = min( $curTTL, self::$TINY_NEGATIVE, $curInfo[self::KEY_AS_OF] - $touched );
1854 $curInfo[self::KEY_TOMB_AS_OF],
1855 $curInfo[self::KEY_CHECK_AS_OF],
1869 return ( $touchedCallback ===
null || $value ===
false )
1871 : max( $touchedCallback( $value ), $lastPurge );
1883 $wrapped = $this->cache->get(
1887 list( $value, $keyInfo ) = $this->
unwrap( $wrapped, $now );
1888 if ( $this->
isValid( $value, $keyInfo[self::KEY_AS_OF], $minAsOf ) ) {
1889 return [ $value, $keyInfo ];
1893 return $this->
unwrap(
false, $now );
1904 $ttl = max( self::$INTERIM_KEY_TTL, (
int)$ttl );
1906 $wrapped = $this->
wrap( $value, $ttl, $version, $this->
getCurrentTime(), $walltime );
1907 $this->cache->merge(
1909 static function () use ( $wrapped ) {
1922 return ( $busyValue instanceof Closure ) ? $busyValue() : $busyValue;
1990 ArrayIterator $keyedIds, $ttl, callable $callback, array $opts = []
1995 $opts[
'checkKeys'] ?? []
1997 $this->warmupKeyMisses = 0;
2003 $proxyCb =
static function ( $oldValue, &$ttl, &$setOpts, $oldAsOf, $params ) use ( $callback ) {
2004 return $callback( $params[
'id'], $oldValue, $ttl, $setOpts, $oldAsOf );
2008 foreach ( $keyedIds as $key => $id ) {
2018 $this->warmupCache = [];
2089 ArrayIterator $keyedIds, $ttl, callable $callback, array $opts = []
2091 $checkKeys = $opts[
'checkKeys'] ?? [];
2092 unset( $opts[
'lockTSE'] );
2093 unset( $opts[
'busyValue'] );
2098 $this->warmupKeyMisses = 0;
2104 $resByKey = $this->
fetchKeys( $keysByIdGet, $checkKeys );
2105 foreach ( $keysByIdGet as $id => $key ) {
2106 $res = $resByKey[$key];
2109 if ( $value ===
false || $metadata[self::KEY_CUR_TTL] < 0 ) {
2116 $newTTLsById = array_fill_keys( $idsRegen, $ttl );
2117 $newValsById = $idsRegen ? $callback( $idsRegen, $newTTLsById, $newSetOpts ) : [];
2123 $proxyCb =
static function ( $oldValue, &$ttl, &$setOpts, $oldAsOf, $params )
2124 use ( $callback, $newValsById, $newTTLsById, $newSetOpts )
2126 $id = $params[
'id'];
2128 if ( array_key_exists( $id, $newValsById ) ) {
2130 $newValue = $newValsById[$id];
2131 $ttl = $newTTLsById[$id];
2132 $setOpts = $newSetOpts;
2136 $ttls = [ $id => $ttl ];
2137 $newValue = $callback( [ $id ], $ttls, $setOpts )[$id];
2146 foreach ( $keyedIds as $key => $id ) {
2156 $this->warmupCache = [];
2173 final public function reap( $key, $purgeTimestamp, &$isStale =
false ) {
2174 $minAsOf = $purgeTimestamp + self::HOLDOFF_TTL;
2175 $wrapped = $this->cache->get( $this->
makeSisterKey( $key, self::TYPE_VALUE ) );
2176 if ( is_array( $wrapped ) && $wrapped[self::$FLD_TIME] < $minAsOf ) {
2178 $this->logger->warning(
"Reaping stale value key '$key'." );
2179 $ttlReap = self::HOLDOFF_TTL;
2180 $ok = $this->cache->changeTTL(
2185 $this->logger->error(
"Could not complete reap of key '$key'." );
2205 final public function reapCheckKey( $key, $purgeTimestamp, &$isStale =
false ) {
2207 $this->cache->get( $this->makeSisterKey( $key, self::TYPE_TIMESTAMP ) )
2209 if ( $purge && $purge[self::$PURGE_TIME] < $purgeTimestamp ) {
2211 $this->logger->warning(
"Reaping stale check key '$key'." );
2212 $ok = $this->cache->changeTTL(
2217 $this->logger->error(
"Could not complete reap of check key '$key'." );
2239 return $this->cache->makeGlobalKey( ...func_get_args() );
2252 public function makeKey( $collection, ...$components ) {
2253 return $this->cache->makeKey( ...func_get_args() );
2264 return hash_hmac(
'sha256', $component, $this->secret );
2319 foreach ( $ids as $id ) {
2321 if ( strlen( $id ) > 64 ) {
2322 $this->logger->warning( __METHOD__ .
": long ID '$id'; use hash256()" );
2324 $key = $keyCallback( $id, $this );
2326 if ( !isset( $idByKey[$key] ) ) {
2327 $idByKey[$key] = $id;
2328 } elseif ( (
string)$id !== (
string)$idByKey[$key] ) {
2329 throw new UnexpectedValueException(
2330 "Cache key collision; IDs ('$id','{$idByKey[$key]}') map to '$key'"
2335 return new ArrayIterator( $idByKey );
2374 if ( count( $ids ) !== count(
$res ) ) {
2377 $ids = array_keys( array_flip( $ids ) );
2378 if ( count( $ids ) !== count(
$res ) ) {
2379 throw new UnexpectedValueException(
"Multi-key result does not match ID list" );
2383 return array_combine( $ids,
$res );
2391 $code = $this->cache->getLastError();
2393 case BagOStuff::ERR_NONE:
2394 return self::ERR_NONE;
2395 case BagOStuff::ERR_NO_RESPONSE:
2396 return self::ERR_NO_RESPONSE;
2397 case BagOStuff::ERR_UNREACHABLE:
2398 return self::ERR_UNREACHABLE;
2400 return self::ERR_UNEXPECTED;
2408 $this->cache->clearLastError();
2417 $this->processCaches = [];
2450 return $this->cache->getQoS( $flag );
2516 public function adaptiveTTL( $mtime, $maxTTL, $minTTL = 30, $factor = 0.2 ) {
2517 if ( is_float( $mtime ) || ctype_digit( $mtime ) ) {
2518 $mtime = (int)$mtime;
2521 if ( !is_int( $mtime ) || $mtime <= 0 ) {
2527 return (
int)min( $maxTTL, max( $minTTL, $factor * $age ) );
2549 if ( $this->mcrouterAware ) {
2552 $ok = $this->cache->set(
2553 "/*/{$this->cluster}/{$key}",
2559 $ok = $this->cache->set(
2576 if ( $this->mcrouterAware ) {
2579 $ok = $this->cache->delete(
"/*/{$this->cluster}/{$key}" );
2582 $ok = $this->cache->delete( $key );
2600 if ( !$this->asyncHandler ) {
2608 $func(
function () use ( $key, $ttl, $callback, $opts, $cbParams ) {
2609 $opts[
'minAsOf'] = INF;
2612 }
catch ( Exception $e ) {
2614 $this->logger->error(
'Async refresh failed for {key}', [
2641 if ( $curTTL > 0 ) {
2643 } elseif ( $graceTTL <= 0 ) {
2647 $ageStale = abs( $curTTL );
2648 $curGraceTTL = ( $graceTTL - $ageStale );
2649 if ( $curGraceTTL <= 0 ) {
2675 if ( $lowTTL <= 0 ) {
2681 $effectiveLowTTL = min( $lowTTL, $logicalTTL ?: INF );
2683 if ( $curTTL >= $effectiveLowTTL || $curTTL <= 0 ) {
2687 $chance = ( 1 - $curTTL / $effectiveLowTTL );
2690 $decision = ( mt_rand( 1, 1e9 ) <= 1e9 * $chance );
2692 $this->logger->debug(
2693 "worthRefreshExpiring($curTTL, $logicalTTL, $lowTTL): " .
2694 "p = $chance; refresh = " . ( $decision ?
'Y' :
'N' )
2716 if ( $ageNew < 0 || $timeTillRefresh <= 0 ) {
2720 $age = $now - $asOf;
2721 $timeOld = $age - $ageNew;
2722 if ( $timeOld <= 0 ) {
2726 $popularHitsPerSec = 1;
2730 $refreshWindowSec = max( $timeTillRefresh - $ageNew - self::$RAMPUP_TTL / 2, 1 );
2734 $chance = 1 / ( $popularHitsPerSec * $refreshWindowSec );
2736 $chance *= ( $timeOld <=
self::$RAMPUP_TTL ) ? $timeOld / self::$RAMPUP_TTL : 1;
2739 $decision = ( mt_rand( 1, 1e9 ) <= 1e9 * $chance );
2741 $this->logger->debug(
2742 "worthRefreshPopular($asOf, $ageNew, $timeTillRefresh, $now): " .
2743 "p = $chance; refresh = " . ( $decision ?
'Y' :
'N' )
2758 protected function isValid( $value, $asOf, $minAsOf, $purgeTime =
null ) {
2760 $safeMinAsOf = max( $minAsOf, $purgeTime + self::$TINY_POSTIVE );
2762 if ( $value ===
false ) {
2764 } elseif ( $safeMinAsOf > 0 && $asOf < $minAsOf ) {
2779 private function wrap( $value, $ttl, $version, $now, $walltime ) {
2784 self::$FLD_VALUE => $value,
2785 self::$FLD_TTL => $ttl,
2786 self::$FLD_TIME => $now
2788 if ( $version !==
null ) {
2791 if ( $walltime >= self::$GENERATION_SLOW_SEC ) {
2814 if ( is_array( $wrapped ) ) {
2817 ( $wrapped[self::$FLD_FORMAT_VERSION] ??
null ) === self::$VERSION &&
2818 $wrapped[self::$FLD_TIME] >= $this->epoch
2820 if ( $wrapped[self::$FLD_TTL] > 0 ) {
2823 $curTTL = max( $wrapped[self::$FLD_TTL] - $age, 0.0 );
2831 $info[self::KEY_CUR_TTL] = $curTTL;
2837 if ( $purge !==
false ) {
2839 $info[self::KEY_CUR_TTL] =
2840 min( $purge[self::$PURGE_TIME] - $now, self::$TINY_NEGATIVE );
2845 return [ $value, $info ];
2853 self::KEY_VERSION =>
null,
2854 self::KEY_AS_OF =>
null,
2855 self::KEY_TTL =>
null,
2856 self::KEY_CUR_TTL =>
null,
2857 self::KEY_TOMB_AS_OF => null
2866 $parts = explode(
':', $key, 3 );
2869 return strtr( $parts[1] ?? $parts[0],
'.',
'_' );
2878 if ( !is_string( $value ) ) {
2882 $segments = explode(
':', $value, 3 );
2884 !isset( $segments[0] ) ||
2885 !isset( $segments[1] ) ||
2886 "{$segments[0]}:" !== self::PURGE_VAL_PREFIX
2891 if ( !isset( $segments[2] ) ) {
2893 $segments[2] = self::HOLDOFF_TTL;
2896 if ( $segments[1] < $this->epoch ) {
2902 self::$PURGE_TIME => (float)$segments[1],
2903 self::$PURGE_HOLDOFF => (
int)$segments[2],
2913 return self::PURGE_VAL_PREFIX . (float)$timestamp .
':' . (
int)$holdoff;
2921 if ( !isset( $this->processCaches[$group] ) ) {
2922 list( , $size ) = explode(
':', $group );
2923 $this->processCaches[$group] =
new MapCacheLRU( (
int)$size );
2924 if ( $this->wallClockOverride !==
null ) {
2925 $this->processCaches[$group]->setMockTime( $this->wallClockOverride );
2929 return $this->processCaches[$group];
2938 return $key .
' ' . (int)$version;
2947 $pcTTL = $opts[
'pcTTL'] ?? self::TTL_UNCACHEABLE;
2950 if ( $pcTTL > 0 && $this->callbackDepth == 0 ) {
2951 $version = $opts[
'version'] ??
null;
2952 $pCache = $this->
getProcessCache( $opts[
'pcGroup'] ?? self::PC_PRIMARY );
2953 foreach (
$keys as $key => $id ) {
2954 if ( !$pCache->has( $this->getProcessCacheKey( $key, $version ), $pcTTL ) ) {
2955 $keysMissing[$id] = $key;
2960 return $keysMissing;
2976 foreach ( $checkKeys as $i => $checkKeyOrKeyGroup ) {
2978 if ( is_int( $i ) ) {
2980 $keysWarmup[] = $this->
makeSisterKey( $checkKeyOrKeyGroup, self::TYPE_TIMESTAMP );
2983 foreach ( (array)$checkKeyOrKeyGroup as $checkKey ) {
2984 $keysWarmup[] = $this->
makeSisterKey( $checkKey, self::TYPE_TIMESTAMP );
3000 if ( $this->wallClockOverride ) {
3004 $clockTime = (float)time();
3010 return max( microtime(
true ), $clockTime );
3018 $this->wallClockOverride =& $time;
3019 $this->cache->setMockTime( $time );
3020 foreach ( $this->processCaches as $pCache ) {
3021 $pCache->setMockTime( $time );