22use Liuggio\StatsdClient\Factory\StatsdDataFactoryInterface;
23use Psr\Log\LoggerAwareInterface;
24use Psr\Log\LoggerInterface;
25use 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 private const STALE_TTL_NONE = 0;
192 private 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';
205 public const PASS_BY_REF = -1;
208 private const SCHEME_HASH_TAG = 1;
210 private const SCHEME_HASH_STOP = 2;
311 $this->cache = $params[
'cache'];
312 $this->region = $params[
'region'] ??
'main';
313 $this->cluster = $params[
'cluster'] ??
'wan-main';
314 $this->mcrouterAware = !empty( $params[
'mcrouterAware'] );
315 $this->epoch = $params[
'epoch'] ?? 0;
316 $this->secret = $params[
'secret'] ?? (string)$this->epoch;
317 $this->coalesceKeys = $params[
'coalesceKeys'] ??
false;
318 if ( !empty( $params[
'mcrouterAware'] ) ) {
320 $this->coalesceScheme = self::SCHEME_HASH_STOP;
325 $this->coalesceScheme = self::SCHEME_HASH_TAG;
328 $this->keyHighQps = $params[
'keyHighQps'] ?? 100;
329 $this->keyHighUplinkBps = $params[
'keyHighUplinkBps'] ?? ( 1e9 / 8 / 100 );
331 $this->
setLogger( $params[
'logger'] ??
new NullLogger() );
333 $this->asyncHandler = $params[
'asyncHandler'] ??
null;
340 $this->logger = $logger;
402 final public function get(
403 $key, &$curTTL =
null, array $checkKeys = [], &$info = null
405 $curTTLs = self::PASS_BY_REF;
406 $infoByKey = self::PASS_BY_REF;
407 $values = $this->
getMulti( [ $key ], $curTTLs, $checkKeys, $infoByKey );
409 $curTTL = $curTTLs[$key] ??
null;
410 if ( $info === self::PASS_BY_REF ) {
412 'asOf' => $infoByKey[$key][
'asOf'] ??
null,
413 'tombAsOf' => $infoByKey[$key][
'tombAsOf'] ??
null,
414 'lastCKPurge' => $infoByKey[$key][
'lastCKPurge'] ??
null,
415 'version' => $infoByKey[$key][
'version'] ?? null
418 $info = $infoByKey[$key][
'asOf'] ??
null;
421 return array_key_exists( $key, $values ) ? $values[$key] :
false;
449 array $checkKeys = [],
459 $fullKeysNeeded = $valueKeys;
460 $checkKeysForAll = [];
461 $checkKeysByKey = [];
462 foreach ( $checkKeys as $i => $checkKeyOrKeyGroup ) {
464 if ( is_int( $i ) ) {
466 $fullKey = $this->
makeSisterKey( $checkKeyOrKeyGroup, self::$TYPE_TIMESTAMP );
467 $fullKeysNeeded[] = $fullKey;
468 $checkKeysForAll[] = $fullKey;
471 foreach ( (array)$checkKeyOrKeyGroup as $checkKey ) {
472 $fullKey = $this->
makeSisterKey( $checkKey, self::$TYPE_TIMESTAMP );
473 $fullKeysNeeded[] = $fullKey;
474 $checkKeysByKey[$i][] = $fullKey;
479 if ( $this->warmupCache ) {
481 $wrappedValues = $this->warmupCache;
482 $fullKeysMissing = array_diff( $fullKeysNeeded, array_keys( $wrappedValues ) );
483 if ( $fullKeysMissing ) {
484 $this->warmupKeyMisses += count( $fullKeysMissing );
485 $wrappedValues += $this->cache->getMulti( $fullKeysMissing );
489 $wrappedValues = $this->cache->getMulti( $fullKeysNeeded );
496 $purgeValuesForAll = $this->
processCheckKeys( $checkKeysForAll, $wrappedValues, $now );
497 $purgeValuesByKey = [];
498 foreach ( $checkKeysByKey as $cacheKey => $checks ) {
499 $purgeValuesByKey[$cacheKey] = $this->
processCheckKeys( $checks, $wrappedValues, $now );
504 foreach ( $valueKeys as $i => $vKey ) {
506 $key = current(
$keys );
509 list( $value, $keyInfo ) = $this->
unwrap(
510 array_key_exists( $vKey, $wrappedValues ) ? $wrappedValues[$vKey] :
false,
515 $purgeValues = $purgeValuesForAll;
516 if ( isset( $purgeValuesByKey[$key] ) ) {
517 $purgeValues = array_merge( $purgeValues, $purgeValuesByKey[$key] );
521 foreach ( $purgeValues as $purge ) {
522 $lastCKPurge = max( $purge[self::$PURGE_TIME], $lastCKPurge );
523 $safeTimestamp = $purge[self::$PURGE_TIME] + $purge[self::$PURGE_HOLDOFF];
524 if ( $value !==
false && $safeTimestamp >= $keyInfo[
'asOf'] ) {
526 $ago = min( $purge[self::$PURGE_TIME] - $now, self::$TINY_NEGATIVE );
528 $keyInfo[
'curTTL'] = min( $keyInfo[
'curTTL'], $ago );
531 $keyInfo[
'lastCKPurge'] = $lastCKPurge;
533 if ( $value !==
false ) {
534 $result[$key] = $value;
536 if ( $keyInfo[
'curTTL'] !==
null ) {
537 $curTTLs[$key] = $keyInfo[
'curTTL'];
540 $infoByKey[$key] = ( $info === self::PASS_BY_REF )
559 foreach ( $timeKeys as $timeKey ) {
560 $purge = isset( $wrappedValues[$timeKey] )
563 if ( $purge ===
false ) {
566 $this->cache->add( $timeKey, $newVal, self::$CHECK_KEY_TTL );
569 $purgeValues[] = $purge;
651 final public function set( $key, $value, $ttl = self::TTL_INDEFINITE, array $opts = [] ) {
653 $lag = $opts[
'lag'] ?? 0;
654 $age = isset( $opts[
'since'] ) ? max( 0, $now - $opts[
'since'] ) : 0;
655 $pending = $opts[
'pending'] ??
false;
656 $lockTSE = $opts[
'lockTSE'] ?? self::TSE_NONE;
657 $staleTTL = $opts[
'staleTTL'] ?? self::STALE_TTL_NONE;
658 $creating = $opts[
'creating'] ??
false;
659 $version = $opts[
'version'] ??
null;
660 $walltime = $opts[
'walltime'] ??
null;
669 'Rejected set() for {cachekey} due to pending writes.',
670 [
'cachekey' => $key ]
682 if ( $age > self::MAX_READ_LAG ) {
684 if ( $walltime ===
null ) {
687 $mitigated =
'snapshot lag';
688 $mitigationTTL = self::TTL_SECOND;
689 } elseif ( ( $age - $walltime ) > self::MAX_READ_LAG ) {
692 $mitigated =
'snapshot lag (late regeneration)';
693 $mitigationTTL = self::TTL_UNCACHEABLE;
697 $mitigated =
'snapshot lag (high regeneration time)';
698 $mitigationTTL = self::TTL_SECOND;
700 } elseif ( $lag ===
false || $lag > self::MAX_READ_LAG ) {
703 $mitigated =
'replication lag';
704 $mitigationTTL = self::TTL_LAGGED;
705 } elseif ( ( $lag + $age ) > self::MAX_READ_LAG ) {
708 $mitigated =
'read lag';
709 $mitigationTTL = self::TTL_UNCACHEABLE;
713 $mitigationTTL =
null;
716 if ( $mitigationTTL === self::TTL_UNCACHEABLE ) {
717 $this->logger->warning(
718 "Rejected set() for {cachekey} due to $mitigated.",
719 [
'cachekey' => $key,
'lag' => $lag,
'age' => $age,
'walltime' => $walltime ]
728 if ( $mitigationTTL !==
null ) {
730 if ( $lockTSE >= 0 ) {
732 $logicalTTL = min( $ttl ?: INF, $mitigationTTL );
735 $ttl = min( $ttl ?: INF, max( $mitigationTTL, self::LOW_TTL ) );
738 $this->logger->warning(
739 "Lowered set() TTL for {cachekey} due to $mitigated.",
740 [
'cachekey' => $key,
'lag' => $lag,
'age' => $age,
'walltime' => $walltime ]
745 $wrapped = $this->
wrap( $value, $logicalTTL ?: $ttl, $version, $now, $walltime );
746 $storeTTL = $ttl + $staleTTL;
749 $ok = $this->cache->add(
755 $ok = $this->cache->merge(
757 function (
$cache, $key, $cWrapped ) use ( $wrapped ) {
759 return ( is_string( $cWrapped ) ) ?
false : $wrapped;
830 final public function delete( $key, $ttl = self::HOLDOFF_TTL ) {
839 self::HOLDOFF_TTL_NONE
844 $this->stats->increment(
"wanobjectcache.$kClass.delete." . ( $ok ?
'ok' :
'error' ) );
935 foreach (
$keys as $key ) {
936 $rawKeys[$key] = $this->
makeSisterKey( $key, self::$TYPE_TIMESTAMP );
939 $rawValues = $this->cache->getMulti( $rawKeys );
940 $rawValues += array_fill_keys( $rawKeys,
false );
943 foreach ( $rawKeys as $key => $rawKey ) {
945 if ( $purge !==
false ) {
946 $time = $purge[self::$PURGE_TIME];
958 $times[$key] = $time;
1002 self::$CHECK_KEY_TTL,
1007 $this->stats->increment(
"wanobjectcache.$kClass.ck_touch." . ( $ok ?
'ok' :
'error' ) );
1044 $this->stats->increment(
"wanobjectcache.$kClass.ck_reset." . ( $ok ?
'ok' :
'error' ) );
1357 $key, $ttl, $callback, array $opts = [], array $cbParams = []
1359 $version = $opts[
'version'] ??
null;
1360 $pcTTL = $opts[
'pcTTL'] ?? self::TTL_UNCACHEABLE;
1361 $pCache = ( $pcTTL >= 0 )
1368 if ( $pCache && $this->callbackDepth == 0 ) {
1369 $cached = $pCache->get( $this->
getProcessCacheKey( $key, $version ), $pcTTL, false );
1370 if ( $cached !==
false ) {
1371 $this->logger->debug(
"getWithSetCallback($key): process cache hit" );
1377 list( $value, $valueVersion, $curAsOf ) =
$res;
1378 if ( $valueVersion !== $version ) {
1382 $this->logger->debug(
"getWithSetCallback($key): using variant key" );
1384 $this->
makeGlobalKey(
'WANCache-key-variant', md5( $key ), $version ),
1387 [
'version' =>
null,
'minAsOf' => $curAsOf ] + $opts,
1393 if ( $pCache && $value !==
false ) {
1417 $checkKeys = $opts[
'checkKeys'] ?? [];
1418 $graceTTL = $opts[
'graceTTL'] ?? self::GRACE_TTL_NONE;
1419 $minAsOf = $opts[
'minAsOf'] ?? self::MIN_TIMESTAMP_NONE;
1420 $hotTTR = $opts[
'hotTTR'] ?? self::HOT_TTR;
1421 $lowTTL = $opts[
'lowTTL'] ?? min( self::LOW_TTL, $ttl );
1422 $ageNew = $opts[
'ageNew'] ?? self::AGE_NEW;
1423 $touchedCb = $opts[
'touchedCallback'] ??
null;
1429 $curTTL = self::PASS_BY_REF;
1430 $curInfo = self::PASS_BY_REF;
1431 $curValue = $this->
get( $key, $curTTL, $checkKeys, $curInfo );
1433 '@phan-var array $curInfo';
1435 list( $curTTL, $LPT ) = $this->
resolveCTL( $curValue, $curTTL, $curInfo, $touchedCb );
1438 $this->
isValid( $curValue, $curInfo[
'asOf'], $minAsOf ) &&
1441 $preemptiveRefresh = (
1445 if ( !$preemptiveRefresh ) {
1446 $this->stats->increment(
"wanobjectcache.$kClass.hit.good" );
1448 return [ $curValue, $curInfo[
'version'], $curInfo[
'asOf'] ];
1450 $this->logger->debug(
"fetchOrRegenerate($key): hit with async refresh" );
1451 $this->stats->increment(
"wanobjectcache.$kClass.hit.refresh" );
1453 return [ $curValue, $curInfo[
'version'], $curInfo[
'asOf'] ];
1455 $this->logger->debug(
"fetchOrRegenerate($key): hit with sync refresh" );
1460 $isKeyTombstoned = ( $curInfo[
'tombAsOf'] !== null );
1461 if ( $isKeyTombstoned ) {
1463 list( $possValue, $possInfo ) = $this->
getInterimValue( $key, $minAsOf );
1467 $possValue = $curValue;
1468 $possInfo = $curInfo;
1474 $this->
isValid( $possValue, $possInfo[
'asOf'], $minAsOf, $LPT ) &&
1477 $this->logger->debug(
"fetchOrRegenerate($key): volatile hit" );
1478 $this->stats->increment(
"wanobjectcache.$kClass.hit.volatile" );
1480 return [ $possValue, $possInfo[
'version'], $curInfo[
'asOf'] ];
1483 $lockTSE = $opts[
'lockTSE'] ?? self::TSE_NONE;
1484 $busyValue = $opts[
'busyValue'] ??
null;
1485 $staleTTL = $opts[
'staleTTL'] ?? self::STALE_TTL_NONE;
1486 $version = $opts[
'version'] ??
null;
1489 $useRegenerationLock =
1499 ( $curTTL !==
null && $curTTL <= 0 && abs( $curTTL ) <= $lockTSE ) ||
1502 ( $busyValue !==
null && $possValue ===
false );
1508 if ( $useRegenerationLock && !$hasLock ) {
1509 if ( $this->
isValid( $possValue, $possInfo[
'asOf'], $minAsOf ) ) {
1510 $this->logger->debug(
"fetchOrRegenerate($key): returning stale value" );
1511 $this->stats->increment(
"wanobjectcache.$kClass.hit.stale" );
1513 return [ $possValue, $possInfo[
'version'], $curInfo[
'asOf'] ];
1514 } elseif ( $busyValue !==
null ) {
1515 $miss = is_infinite( $minAsOf ) ?
'renew' :
'miss';
1516 $this->logger->debug(
"fetchOrRegenerate($key): busy $miss" );
1517 $this->stats->increment(
"wanobjectcache.$kClass.$miss.busy" );
1519 return [ $this->
resolveBusyValue( $busyValue ), $version, $curInfo[
'asOf'] ];
1526 ++$this->callbackDepth;
1529 ( $curInfo[
'version'] === $version ) ? $curValue :
false,
1532 ( $curInfo[
'version'] === $version ) ? $curInfo[
'asOf'] :
null,
1536 --$this->callbackDepth;
1541 $elapsed = max( $postCallbackTime - $initialTime, 0.0 );
1546 ( $value !==
false && $ttl >= 0 ) &&
1548 ( !$useRegenerationLock || $hasLock || $isKeyTombstoned ) &&
1553 $walltime = max( $postCallbackTime - $preCallbackTime, 0.0 );
1554 $this->stats->timing(
"wanobjectcache.$kClass.regen_walltime", 1e3 * $walltime );
1556 if ( $isKeyTombstoned ) {
1557 $this->
setInterimValue( $key, $value, $lockTSE, $version, $walltime );
1561 'since' => $setOpts[
'since'] ?? $preCallbackTime,
1562 'version' => $version,
1563 'staleTTL' => $staleTTL,
1564 'lockTSE' => $lockTSE,
1565 'creating' => ( $curValue === false ),
1566 'walltime' => $walltime
1568 $this->
set( $key, $value, $ttl, $finalSetOpts );
1574 $miss = is_infinite( $minAsOf ) ?
'renew' :
'miss';
1575 $this->logger->debug(
"fetchOrRegenerate($key): $miss, new value computed" );
1576 $this->stats->increment(
"wanobjectcache.$kClass.$miss.compute" );
1578 return [ $value, $version, $curInfo[
'asOf'] ];
1587 return $this->cache->add(
1603 $this->cache->changeTTL(
1619 foreach ( $baseKeys as $baseKey ) {
1634 if ( $this->coalesceKeys ===
'non-global' ) {
1635 $useColocationScheme = ( strncmp( $baseKey,
"global:", 7 ) !== 0 );
1637 $useColocationScheme = ( $this->coalesceKeys ===
true );
1640 if ( !$useColocationScheme ) {
1642 $fullKey =
'WANCache:' . $typeChar .
':' . $baseKey;
1643 } elseif ( $this->coalesceScheme === self::SCHEME_HASH_STOP ) {
1645 $fullKey =
'WANCache:' . $baseKey .
'|#|' . $typeChar;
1648 $fullKey =
'WANCache:{' . $baseKey .
'}:' . $typeChar;
1659 return ( $age < mt_rand( self::$RECENT_SET_LOW_MS, self::$RECENT_SET_HIGH_MS ) / 1e3 );
1684 $valueKey = $this->
makeSisterKey( $key, self::$TYPE_VALUE );
1685 list( $estimatedSize ) = $this->cache->setNewPreparedValues( [ $valueKey => $value ] );
1696 $missesPerSecForHighQPS = ( min( $elapsed, 1 ) * $this->keyHighQps );
1708 if ( ( $missesPerSecForHighQPS * $estimatedSize ) >= $this->keyHighUplinkBps ) {
1709 $this->cache->clearLastError();
1712 $this->makeSisterKey( $key, self::$TYPE_COOLOFF ),
1717 $this->cache->getLastError() === BagOStuff::ERR_NONE
1719 $this->stats->increment(
"wanobjectcache.$kClass.cooloff_bounce" );
1727 $this->stats->timing(
"wanobjectcache.$kClass.regen_set_delay", 1e3 * $elapsed );
1728 $this->stats->updateCount(
"wanobjectcache.$kClass.regen_set_bytes", $estimatedSize );
1741 private function resolveCTL( $value, $curTTL, $curInfo, $touchedCallback ) {
1742 if ( $touchedCallback ===
null || $value ===
false ) {
1743 return [ $curTTL, max( $curInfo[
'tombAsOf'], $curInfo[
'lastCKPurge'] ) ];
1746 $touched = $touchedCallback( $value );
1747 if ( $touched !==
null && $touched >= $curInfo[
'asOf'] ) {
1748 $curTTL = min( $curTTL, self::$TINY_NEGATIVE, $curInfo[
'asOf'] - $touched );
1751 return [ $curTTL, max( $curInfo[
'tombAsOf'], $curInfo[
'lastCKPurge'], $touched ) ];
1762 return ( $touchedCallback ===
null || $value ===
false )
1764 : max( $touchedCallback( $value ), $lastPurge );
1776 $wrapped = $this->cache->get(
1780 list( $value, $keyInfo ) = $this->
unwrap( $wrapped, $now );
1781 if ( $this->
isValid( $value, $keyInfo[
'asOf'], $minAsOf ) ) {
1782 return [ $value, $keyInfo ];
1786 return $this->
unwrap(
false, $now );
1797 $ttl = max( self::$INTERIM_KEY_TTL, (
int)$ttl );
1799 $wrapped = $this->
wrap( $value, $ttl, $version, $this->
getCurrentTime(), $walltime );
1800 $this->cache->merge(
1802 function () use ( $wrapped ) {
1815 return ( $busyValue instanceof Closure ) ? $busyValue() : $busyValue;
1883 ArrayIterator $keyedIds, $ttl, callable $callback, array $opts = []
1888 $opts[
'checkKeys'] ?? []
1890 $this->warmupKeyMisses = 0;
1896 $proxyCb =
function ( $oldValue, &$ttl, &$setOpts, $oldAsOf, $params ) use ( $callback ) {
1897 return $callback( $params[
'id'], $oldValue, $ttl, $setOpts, $oldAsOf );
1901 foreach ( $keyedIds as $key => $id ) {
1911 $this->warmupCache = [];
1982 ArrayIterator $keyedIds, $ttl, callable $callback, array $opts = []
1984 $checkKeys = $opts[
'checkKeys'] ?? [];
1985 unset( $opts[
'lockTSE'] );
1986 unset( $opts[
'busyValue'] );
1991 $this->warmupKeyMisses = 0;
1999 $curByKey = $this->
getMulti( $keysByIdGet, $curTTLs, $checkKeys, $asOfs );
2000 foreach ( $keysByIdGet as $id => $key ) {
2001 if ( !array_key_exists( $key, $curByKey ) || $curTTLs[$key] < 0 ) {
2008 $newTTLsById = array_fill_keys( $idsRegen, $ttl );
2009 $newValsById = $idsRegen ? $callback( $idsRegen, $newTTLsById, $newSetOpts ) : [];
2015 $proxyCb =
function ( $oldValue, &$ttl, &$setOpts, $oldAsOf, $params )
2016 use ( $callback, $newValsById, $newTTLsById, $newSetOpts )
2018 $id = $params[
'id'];
2020 if ( array_key_exists( $id, $newValsById ) ) {
2022 $newValue = $newValsById[$id];
2023 $ttl = $newTTLsById[$id];
2024 $setOpts = $newSetOpts;
2028 $ttls = [ $id => $ttl ];
2029 $newValue = $callback( [ $id ], $ttls, $setOpts )[$id];
2038 foreach ( $keyedIds as $key => $id ) {
2048 $this->warmupCache = [];
2065 final public function reap( $key, $purgeTimestamp, &$isStale =
false ) {
2066 $minAsOf = $purgeTimestamp + self::HOLDOFF_TTL;
2067 $wrapped = $this->cache->get( $this->
makeSisterKey( $key, self::$TYPE_VALUE ) );
2068 if ( is_array( $wrapped ) && $wrapped[self::$FLD_TIME] < $minAsOf ) {
2070 $this->logger->warning(
"Reaping stale value key '$key'." );
2071 $ttlReap = self::HOLDOFF_TTL;
2072 $ok = $this->cache->changeTTL(
2077 $this->logger->error(
"Could not complete reap of key '$key'." );
2097 final public function reapCheckKey( $key, $purgeTimestamp, &$isStale =
false ) {
2099 $this->cache->get( $this->makeSisterKey( $key, self::$TYPE_TIMESTAMP ) )
2101 if ( $purge && $purge[self::$PURGE_TIME] < $purgeTimestamp ) {
2103 $this->logger->warning(
"Reaping stale check key '$key'." );
2104 $ok = $this->cache->changeTTL(
2109 $this->logger->error(
"Could not complete reap of check key '$key'." );
2127 public function makeKey( $class, ...$components ) {
2128 return $this->cache->makeKey( ...func_get_args() );
2139 return $this->cache->makeGlobalKey( ...func_get_args() );
2150 return hash_hmac(
'sha256', $component, $this->secret );
2205 foreach ( $ids as $id ) {
2207 if ( strlen( $id ) > 64 ) {
2208 $this->logger->warning( __METHOD__ .
": long ID '$id'; use hash256()" );
2210 $key = $keyCallback( $id, $this );
2212 if ( !isset( $idByKey[$key] ) ) {
2213 $idByKey[$key] = $id;
2214 } elseif ( (
string)$id !== (
string)$idByKey[$key] ) {
2215 throw new UnexpectedValueException(
2216 "Cache key collision; IDs ('$id','{$idByKey[$key]}') map to '$key'"
2221 return new ArrayIterator( $idByKey );
2260 if ( count( $ids ) !== count(
$res ) ) {
2263 $ids = array_keys( array_flip( $ids ) );
2264 if ( count( $ids ) !== count(
$res ) ) {
2265 throw new UnexpectedValueException(
"Multi-key result does not match ID list" );
2269 return array_combine( $ids,
$res );
2277 $code = $this->cache->getLastError();
2279 case BagOStuff::ERR_NONE:
2280 return self::ERR_NONE;
2281 case BagOStuff::ERR_NO_RESPONSE:
2282 return self::ERR_NO_RESPONSE;
2283 case BagOStuff::ERR_UNREACHABLE:
2284 return self::ERR_UNREACHABLE;
2286 return self::ERR_UNEXPECTED;
2294 $this->cache->clearLastError();
2303 $this->processCaches = [];
2336 return $this->cache->getQoS( $flag );
2402 public function adaptiveTTL( $mtime, $maxTTL, $minTTL = 30, $factor = 0.2 ) {
2403 $mtime = (int)$mtime;
2404 if ( $mtime <= 0 ) {
2410 return (
int)min( $maxTTL, max( $minTTL, $factor * $age ) );
2418 return $this->warmupKeyMisses;
2432 if ( $this->mcrouterAware ) {
2435 $ok = $this->cache->set(
2436 "/*/{$this->cluster}/{$key}",
2442 $ok = $this->cache->set(
2459 if ( $this->mcrouterAware ) {
2462 $ok = $this->cache->delete(
"/*/{$this->cluster}/{$key}" );
2465 $ok = $this->cache->delete( $key );
2483 if ( !$this->asyncHandler ) {
2490 $func = $this->asyncHandler;
2491 $func(
function () use ( $key, $ttl, $callback, $opts, $cbParams ) {
2492 $opts[
'minAsOf'] = INF;
2513 if ( $curTTL > 0 ) {
2515 } elseif ( $graceTTL <= 0 ) {
2519 $ageStale = abs( $curTTL );
2520 $curGTTL = ( $graceTTL - $ageStale );
2521 if ( $curGTTL <= 0 ) {
2543 if ( $lowTTL <= 0 ) {
2545 } elseif ( $curTTL >= $lowTTL ) {
2547 } elseif ( $curTTL <= 0 ) {
2551 $chance = ( 1 - $curTTL / $lowTTL );
2554 $decision = ( mt_rand( 1, 1e9 ) <= 1e9 * $chance );
2556 $this->logger->debug(
2557 "worthRefreshExpiring($curTTL, $lowTTL): " .
2558 "p = $chance; refresh = " . ( $decision ?
'Y' :
'N' )
2580 if ( $ageNew < 0 || $timeTillRefresh <= 0 ) {
2584 $age = $now - $asOf;
2585 $timeOld = $age - $ageNew;
2586 if ( $timeOld <= 0 ) {
2590 $popularHitsPerSec = 1;
2594 $refreshWindowSec = max( $timeTillRefresh - $ageNew - self::$RAMPUP_TTL / 2, 1 );
2598 $chance = 1 / ( $popularHitsPerSec * $refreshWindowSec );
2600 $chance *= ( $timeOld <= self::$RAMPUP_TTL ) ? $timeOld / self::$RAMPUP_TTL : 1;
2603 $decision = ( mt_rand( 1, 1e9 ) <= 1e9 * $chance );
2605 $this->logger->debug(
2606 "worthRefreshPopular($asOf, $ageNew, $timeTillRefresh, $now): " .
2607 "p = $chance; refresh = " . ( $decision ?
'Y' :
'N' )
2622 protected function isValid( $value, $asOf, $minAsOf, $purgeTime =
null ) {
2624 $safeMinAsOf = max( $minAsOf, $purgeTime + self::$TINY_POSTIVE );
2626 if ( $value ===
false ) {
2628 } elseif ( $safeMinAsOf > 0 && $asOf < $minAsOf ) {
2643 private function wrap( $value, $ttl, $version, $now, $walltime ) {
2647 self::$FLD_FORMAT_VERSION => self::$VERSION,
2648 self::$FLD_VALUE => $value,
2649 self::$FLD_TTL => $ttl,
2650 self::$FLD_TIME => $now
2652 if ( $version !==
null ) {
2653 $wrapped[self::$FLD_VALUE_VERSION] = $version;
2655 if ( $walltime >= self::$GENERATION_SLOW_SEC ) {
2656 $wrapped[self::$FLD_GENERATION_TIME] = $walltime;
2675 $info = [
'asOf' =>
null,
'curTTL' =>
null,
'version' =>
null,
'tombAsOf' => null ];
2677 if ( is_array( $wrapped ) ) {
2680 ( $wrapped[self::$FLD_FORMAT_VERSION] ??
null ) === self::$VERSION &&
2681 $wrapped[self::$FLD_TIME] >= $this->epoch
2683 if ( $wrapped[self::$FLD_TTL] > 0 ) {
2685 $age = $now - $wrapped[self::$FLD_TIME];
2686 $curTTL = max( $wrapped[self::$FLD_TTL] - $age, 0.0 );
2691 $value = $wrapped[self::$FLD_VALUE];
2692 $info[
'version'] = $wrapped[self::$FLD_VALUE_VERSION] ??
null;
2693 $info[
'asOf'] = $wrapped[self::$FLD_TIME];
2694 $info[
'curTTL'] = $curTTL;
2699 if ( $purge !==
false ) {
2701 $info[
'curTTL'] = min( $purge[self::$PURGE_TIME] - $now, self::$TINY_NEGATIVE );
2702 $info[
'tombAsOf'] = $purge[self::$PURGE_TIME];
2706 return [ $value, $info ];
2714 $parts = explode(
':', $key, 3 );
2717 return strtr( $parts[1] ?? $parts[0],
'.',
'_' );
2726 if ( !is_string( $value ) ) {
2730 $segments = explode(
':', $value, 3 );
2732 !isset( $segments[0] ) ||
2733 !isset( $segments[1] ) ||
2734 "{$segments[0]}:" !== self::$PURGE_VAL_PREFIX
2739 if ( !isset( $segments[2] ) ) {
2741 $segments[2] = self::HOLDOFF_TTL;
2744 if ( $segments[1] < $this->epoch ) {
2750 self::$PURGE_TIME => (float)$segments[1],
2751 self::$PURGE_HOLDOFF => (
int)$segments[2],
2761 return self::$PURGE_VAL_PREFIX . (float)$timestamp .
':' . (
int)$holdoff;
2769 if ( !isset( $this->processCaches[$group] ) ) {
2770 list( , $size ) = explode(
':', $group );
2771 $this->processCaches[$group] =
new MapCacheLRU( (
int)$size );
2772 if ( $this->wallClockOverride !==
null ) {
2773 $this->processCaches[$group]->setMockTime( $this->wallClockOverride );
2777 return $this->processCaches[$group];
2786 return $key .
' ' . (int)$version;
2795 $pcTTL = $opts[
'pcTTL'] ?? self::TTL_UNCACHEABLE;
2798 if ( $pcTTL > 0 && $this->callbackDepth == 0 ) {
2799 $version = $opts[
'version'] ??
null;
2800 $pCache = $this->
getProcessCache( $opts[
'pcGroup'] ?? self::PC_PRIMARY );
2801 foreach (
$keys as $key => $id ) {
2802 if ( !$pCache->has( $this->getProcessCacheKey( $key, $version ), $pcTTL ) ) {
2803 $keysMissing[$id] = $key;
2808 return $keysMissing;
2824 foreach ( $checkKeys as $i => $checkKeyOrKeyGroup ) {
2826 if ( is_int( $i ) ) {
2828 $keysWarmup[] = $this->
makeSisterKey( $checkKeyOrKeyGroup, self::$TYPE_TIMESTAMP );
2831 foreach ( (array)$checkKeyOrKeyGroup as $checkKey ) {
2832 $keysWarmup[] = $this->
makeSisterKey( $checkKey, self::$TYPE_TIMESTAMP );
2848 if ( $this->wallClockOverride ) {
2849 return $this->wallClockOverride;
2852 $clockTime = (float)time();
2858 return max( microtime(
true ), $clockTime );
2866 $this->wallClockOverride =& $time;
2867 $this->cache->setMockTime( $time );
2868 foreach ( $this->processCaches as $pCache ) {
2869 $pCache->setMockTime( $time );
Class representing a cache/ephemeral data store.
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)
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.
fetchOrRegenerate( $key, $ttl, $callback, array $opts, array $cbParams)
Do the actual I/O for getWithSetCallback() when needed.
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.
makeSisterKeys(array $baseKeys, $type)
Get cache keys that should be collocated with their corresponding base keys.
isValid( $value, $asOf, $minAsOf, $purgeTime=null)
Check if $value is not false, versioned (if needed), and not older than $minTime (if set)
static string $TYPE_TIMESTAMP
Single character timestamp key component.
string bool $coalesceKeys
Whether "sister" keys should be coalesced to the same cache server.
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
static string $TYPE_INTERIM
Single character interium key component.
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.
scheduleAsyncRefresh( $key, $ttl, $callback, array $opts, array $cbParams)
Schedule a deferred cache regeneration if possible.
processCheckKeys(array $timeKeys, array $wrappedValues, $now)
getWithSetCallback( $key, $ttl, $callback, array $opts=[], array $cbParams=[])
Method to fetch/regenerate a cache key.
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.
static string $TYPE_COOLOFF
Single character cool-off key component.
getMultiWithSetCallback(ArrayIterator $keyedIds, $ttl, callable $callback, array $opts=[])
Method to fetch multiple cache keys at once with regeneration.
static string $TYPE_VALUE
Single character value mutex key component.
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.
makeSisterKey( $baseKey, $typeChar)
Get a cache key that should be collocated with a base key.
int $coalesceScheme
Scheme to use for key coalescing (Hash Tags or Hash Stops)
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.
static string $TYPE_MUTEX
Single character mutex key component.
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 string $PURGE_VAL_PREFIX
Prefix for tombstone key values.
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)
checkAndSetCooloff( $key, $kClass, $value, $elapsed, $hasLock)
Check whether set() is rate-limited to avoid concurrent I/O spikes.
float $keyHighUplinkBps
Max tolerable bytes/second to spend on a cache write stampede for a key.
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 $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.
int $keyHighQps
Reads/second assumed during a hypothetical cache write stampede for a key.
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 object stores with key encoding methods.