22use Liuggio\StatsdClient\Factory\StatsdDataFactoryInterface;
23use Psr\Log\LoggerAwareInterface;
24use Psr\Log\LoggerInterface;
25use 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;
292 $this->logger = $logger;
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 );
408 $valueKeys = self::prefixCacheKeys(
$keys, self::$VALUE_KEY_PREFIX );
410 $checkKeysForAll = [];
411 $checkKeysByKey = [];
413 foreach ( $checkKeys as $i => $checkKeyGroup ) {
414 $prefixed = self::prefixCacheKeys( (array)$checkKeyGroup, self::$TIME_KEY_PREFIX );
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 );
461 $safeTimestamp = $purge[self::$PURGE_TIME] + $purge[self::$PURGE_HOLDOFF];
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;
589 final public function set( $key, $value, $ttl = self::TTL_INDEFINITE, array $opts = [] ) {
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 ) {
621 $logicalTTL = self::TTL_SECOND;
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 ) {
648 $logicalTTL = self::TTL_SECOND;
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 ) {
857 $time = $purge[self::$PURGE_TIME];
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 ) {
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;
1328 $curTTL = self::PASS_BY_REF;
1329 $curInfo = self::PASS_BY_REF;
1330 $curValue = $this->
get( $key, $curTTL, $checkKeys, $curInfo );
1332 list( $curTTL, $LPT ) = $this->
resolveCTL( $curValue, $curTTL, $curInfo, $touchedCb );
1335 $this->
isValid( $curValue, $curInfo[
'asOf'], $minAsOf ) &&
1338 $preemptiveRefresh = (
1342 if ( !$preemptiveRefresh ) {
1343 $this->stats->increment(
"wanobjectcache.$kClass.hit.good" );
1345 return [ $curValue, $curInfo[
'version'], $curInfo[
'asOf'] ];
1347 $this->stats->increment(
"wanobjectcache.$kClass.hit.refresh" );
1349 return [ $curValue, $curInfo[
'version'], $curInfo[
'asOf'] ];
1354 $isKeyTombstoned = ( $curInfo[
'tombAsOf'] !== null );
1355 if ( $isKeyTombstoned ) {
1357 list( $possValue, $possInfo ) = $this->
getInterimValue( $key, $minAsOf );
1361 $possValue = $curValue;
1362 $possInfo = $curInfo;
1368 $this->
isValid( $possValue, $possInfo[
'asOf'], $minAsOf, $LPT ) &&
1371 $this->stats->increment(
"wanobjectcache.$kClass.hit.volatile" );
1373 return [ $possValue, $possInfo[
'version'], $curInfo[
'asOf'] ];
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;
1382 $useRegenerationLock =
1392 ( $curTTL !==
null && $curTTL <= 0 && abs( $curTTL ) <= $lockTSE ) ||
1395 ( $busyValue !==
null && $possValue ===
false );
1401 if ( $useRegenerationLock && !$hasLock ) {
1402 if ( $this->
isValid( $possValue, $possInfo[
'asOf'], $minAsOf ) ) {
1403 $this->stats->increment(
"wanobjectcache.$kClass.hit.stale" );
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" );
1410 return [ $this->
resolveBusyValue( $busyValue ), $version, $curInfo[
'asOf'] ];
1417 ++$this->callbackDepth;
1420 ( $curInfo[
'version'] === $version ) ? $curValue :
false,
1423 ( $curInfo[
'version'] === $version ) ? $curInfo[
'asOf'] : null
1426 --$this->callbackDepth;
1431 $elapsed = max( $postCallbackTime - $initialTime, 0.0 );
1436 ( $value !==
false && $ttl >= 0 ) &&
1438 ( !$useRegenerationLock || $hasLock || $isKeyTombstoned ) &&
1443 $walltime = max( $postCallbackTime - $preCallbackTime, 0.0 );
1444 $this->stats->timing(
"wanobjectcache.$kClass.regen_walltime", 1e3 * $walltime );
1446 if ( $isKeyTombstoned ) {
1447 $this->
setInterimValue( $key, $value, $lockTSE, $version, $walltime );
1450 'since' => $setOpts[
'since'] ?? $preCallbackTime,
1451 'version' => $version,
1452 'staleTTL' => $staleTTL,
1453 'lockTSE' => $lockTSE,
1454 'creating' => ( $curValue === false ),
1455 'walltime' => $walltime
1457 $this->
set( $key, $value, $ttl, $finalSetOpts );
1463 $miss = is_infinite( $minAsOf ) ?
'renew' :
'miss';
1464 $this->stats->increment(
"wanobjectcache.$kClass.$miss.compute" );
1466 return [ $value, $version, $curInfo[
'asOf'] ];
1475 return $this->cache->add( self::$MUTEX_KEY_PREFIX . $key, 1, self::$LOCK_TTL );
1487 $this->cache->changeTTL( self::$MUTEX_KEY_PREFIX . $key, $this->
getCurrentTime() - 60 );
1496 return ( $age < mt_rand( self::$RECENT_SET_LOW_MS, self::$RECENT_SET_HIGH_MS ) / 1e3 );
1508 $this->stats->timing(
"wanobjectcache.$kClass.regen_set_delay", 1e3 * $elapsed );
1516 if ( $lockTSE < 0 || $hasLock ) {
1518 } elseif ( $elapsed <= self::$SET_DELAY_HIGH_MS * 1e3 ) {
1522 $this->cache->clearLastError();
1524 !$this->cache->add( self::$COOLOFF_KEY_PREFIX . $key, 1, self::$COOLOFF_TTL ) &&
1528 $this->stats->increment(
"wanobjectcache.$kClass.cooloff_bounce" );
1544 private function resolveCTL( $value, $curTTL, $curInfo, $touchedCallback ) {
1545 if ( $touchedCallback ===
null || $value ===
false ) {
1546 return [ $curTTL, max( $curInfo[
'tombAsOf'], $curInfo[
'lastCKPurge'] ) ];
1549 $touched = $touchedCallback( $value );
1550 if ( $touched !==
null && $touched >= $curInfo[
'asOf'] ) {
1551 $curTTL = min( $curTTL, self::$TINY_NEGATIVE, $curInfo[
'asOf'] - $touched );
1554 return [ $curTTL, max( $curInfo[
'tombAsOf'], $curInfo[
'lastCKPurge'], $touched ) ];
1565 return ( $touchedCallback ===
null || $value ===
false )
1567 : max( $touchedCallback( $value ), $lastPurge );
1579 $wrapped = $this->cache->get( self::$INTERIM_KEY_PREFIX . $key );
1581 list( $value, $keyInfo ) = $this->
unwrap( $wrapped, $now );
1582 if ( $this->
isValid( $value, $keyInfo[
'asOf'], $minAsOf ) ) {
1583 return [ $value, $keyInfo ];
1587 return $this->
unwrap(
false, $now );
1598 $ttl = max( self::$INTERIM_KEY_TTL, (
int)$ttl );
1600 $wrapped = $this->
wrap( $value, $ttl, $version, $this->
getCurrentTime(), $walltime );
1601 $this->cache->merge(
1602 self::$INTERIM_KEY_PREFIX . $key,
1603 function () use ( $wrapped ) {
1616 return ( $busyValue instanceof Closure ) ? $busyValue() : $busyValue;
1686 ArrayIterator $keyedIds, $ttl, callable $callback, array $opts = []
1691 $opts[
'checkKeys'] ?? []
1693 $this->warmupKeyMisses = 0;
1697 $func =
function ( $oldValue, &$ttl, &$setOpts, $oldAsOf ) use ( $callback, &$id ) {
1698 return $callback( $id, $oldValue, $ttl, $setOpts, $oldAsOf );
1702 foreach ( $keyedIds as $key => $id ) {
1706 $this->warmupCache = [];
1777 ArrayIterator $keyedIds, $ttl, callable $callback, array $opts = []
1779 $checkKeys = $opts[
'checkKeys'] ?? [];
1780 unset( $opts[
'lockTSE'] );
1781 unset( $opts[
'busyValue'] );
1786 $this->warmupKeyMisses = 0;
1794 $curByKey = $this->
getMulti( $keysByIdGet, $curTTLs, $checkKeys, $asOfs );
1795 foreach ( $keysByIdGet as $id => $key ) {
1796 if ( !array_key_exists( $key, $curByKey ) || $curTTLs[$key] < 0 ) {
1803 $newTTLsById = array_fill_keys( $idsRegen, $ttl );
1804 $newValsById = $idsRegen ? $callback( $idsRegen, $newTTLsById, $newSetOpts ) : [];
1808 $func =
function ( $oldValue, &$ttl, &$setOpts, $oldAsOf )
1809 use ( $callback, &$id, $newValsById, $newTTLsById, $newSetOpts )
1811 if ( array_key_exists( $id, $newValsById ) ) {
1813 $newValue = $newValsById[$id];
1814 $ttl = $newTTLsById[$id];
1815 $setOpts = $newSetOpts;
1819 $ttls = [ $id => $ttl ];
1820 $newValue = $callback( [ $id ], $ttls, $setOpts )[$id];
1829 foreach ( $keyedIds as $key => $id ) {
1833 $this->warmupCache = [];
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 ) {
1855 $this->logger->warning(
"Reaping stale value key '$key'." );
1856 $ttlReap = self::HOLDOFF_TTL;
1857 $ok = $this->cache->changeTTL( self::$VALUE_KEY_PREFIX . $key, $ttlReap );
1859 $this->logger->error(
"Could not complete reap of key '$key'." );
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 ) {
1883 $this->logger->warning(
"Reaping stale check key '$key'." );
1884 $ok = $this->cache->changeTTL( self::$TIME_KEY_PREFIX . $key, self::TTL_SECOND );
1886 $this->logger->error(
"Could not complete reap of check key '$key'." );
1904 public function makeKey( $class, ...$components ) {
1905 return $this->cache->makeKey( ...func_get_args() );
1916 return $this->cache->makeGlobalKey( ...func_get_args() );
1927 return hash_hmac(
'sha256', $component, $this->secret );
1982 foreach ( $ids as $id ) {
1984 if ( strlen( $id ) > 64 ) {
1985 $this->logger->warning( __METHOD__ .
": long ID '$id'; use hash256()" );
1987 $key = $keyCallback( $id, $this );
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'"
1998 return new ArrayIterator( $idByKey );
2037 if ( count( $ids ) !== count(
$res ) ) {
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" );
2046 return array_combine( $ids,
$res );
2054 $code = $this->cache->getLastError();
2057 return self::ERR_NONE;
2059 return self::ERR_NO_RESPONSE;
2061 return self::ERR_UNREACHABLE;
2063 return self::ERR_UNEXPECTED;
2071 $this->cache->clearLastError();
2080 $this->processCaches = [];
2113 return $this->cache->getQoS( $flag );
2179 public function adaptiveTTL( $mtime, $maxTTL, $minTTL = 30, $factor = 0.2 ) {
2180 if ( is_float( $mtime ) || ctype_digit( $mtime ) ) {
2181 $mtime = (int)$mtime;
2184 if ( !is_int( $mtime ) || $mtime <= 0 ) {
2190 return (
int)min( $maxTTL, max( $minTTL, $factor * $age ) );
2198 return $this->warmupKeyMisses;
2212 if ( $this->mcrouterAware ) {
2215 $ok = $this->cache->set(
2216 "/*/{$this->cluster}/{$key}",
2222 $ok = $this->cache->set(
2239 if ( $this->mcrouterAware ) {
2242 $ok = $this->cache->delete(
"/*/{$this->cluster}/{$key}" );
2245 $ok = $this->cache->delete( $key );
2260 if ( !$this->asyncHandler ) {
2264 $func = $this->asyncHandler;
2265 $func(
function () use ( $key, $ttl, $callback, $opts ) {
2266 $opts[
'minAsOf'] = INF;
2287 if ( $curTTL > 0 ) {
2289 } elseif ( $graceTTL <= 0 ) {
2293 $ageStale = abs( $curTTL );
2294 $curGTTL = ( $graceTTL - $ageStale );
2295 if ( $curGTTL <= 0 ) {
2317 if ( $lowTTL <= 0 ) {
2319 } elseif ( $curTTL >= $lowTTL ) {
2321 } elseif ( $curTTL <= 0 ) {
2325 $chance = ( 1 - $curTTL / $lowTTL );
2328 return mt_rand( 1, 1e9 ) <= 1e9 * $chance;
2347 if ( $ageNew < 0 || $timeTillRefresh <= 0 ) {
2351 $age = $now - $asOf;
2352 $timeOld = $age - $ageNew;
2353 if ( $timeOld <= 0 ) {
2357 $popularHitsPerSec = 1;
2361 $refreshWindowSec = max( $timeTillRefresh - $ageNew - self::$RAMPUP_TTL / 2, 1 );
2365 $chance = 1 / ( $popularHitsPerSec * $refreshWindowSec );
2368 $chance *= ( $timeOld <= self::$RAMPUP_TTL ) ? $timeOld / self::$RAMPUP_TTL : 1;
2371 return mt_rand( 1, 1e9 ) <= 1e9 * $chance;
2383 protected function isValid( $value, $asOf, $minAsOf, $purgeTime =
null ) {
2385 $safeMinAsOf = max( $minAsOf, $purgeTime + self::$TINY_POSTIVE );
2387 if ( $value ===
false ) {
2389 } elseif ( $safeMinAsOf > 0 && $asOf < $minAsOf ) {
2404 private function wrap( $value, $ttl, $version, $now, $walltime ) {
2408 self::$FLD_FORMAT_VERSION => self::$VERSION,
2409 self::$FLD_VALUE => $value,
2410 self::$FLD_TTL => $ttl,
2411 self::$FLD_TIME => $now
2413 if ( $version !==
null ) {
2414 $wrapped[self::$FLD_VALUE_VERSION] = $version;
2416 if ( $walltime >= self::$GENERATION_SLOW_SEC ) {
2417 $wrapped[self::$FLD_GENERATION_TIME] = $walltime;
2436 $info = [
'asOf' =>
null,
'curTTL' =>
null,
'version' =>
null,
'tombAsOf' => null ];
2438 if ( is_array( $wrapped ) ) {
2441 ( $wrapped[self::$FLD_FORMAT_VERSION] ??
null ) === self::$VERSION &&
2442 $wrapped[self::$FLD_TIME] >= $this->epoch
2444 if ( $wrapped[self::$FLD_TTL] > 0 ) {
2446 $age = $now - $wrapped[self::$FLD_TIME];
2447 $curTTL = max( $wrapped[self::$FLD_TTL] - $age, 0.0 );
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;
2460 if ( $purge !==
false ) {
2462 $info[
'curTTL'] = min( $purge[self::$PURGE_TIME] - $now, self::$TINY_NEGATIVE );
2463 $info[
'tombAsOf'] = $purge[self::$PURGE_TIME];
2467 return [ $value, $info ];
2477 foreach (
$keys as $key ) {
2478 $res[] = $prefix . $key;
2489 $parts = explode(
':', $key, 3 );
2492 return strtr( $parts[1] ?? $parts[0],
'.',
'_' );
2501 if ( !is_string( $value ) ) {
2505 $segments = explode(
':', $value, 3 );
2507 !isset( $segments[0] ) ||
2508 !isset( $segments[1] ) ||
2509 "{$segments[0]}:" !== self::$PURGE_VAL_PREFIX
2514 if ( !isset( $segments[2] ) ) {
2516 $segments[2] = self::HOLDOFF_TTL;
2519 if ( $segments[1] < $this->epoch ) {
2525 self::$PURGE_TIME => (float)$segments[1],
2526 self::$PURGE_HOLDOFF => (
int)$segments[2],
2536 return self::$PURGE_VAL_PREFIX . (float)$timestamp .
':' . (
int)$holdoff;
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 );
2552 return $this->processCaches[$group];
2561 return $key .
' ' . (int)$version;
2570 $pcTTL = $opts[
'pcTTL'] ?? self::TTL_UNCACHEABLE;
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;
2583 return $keysMissing;
2598 foreach (
$keys as $key ) {
2599 $keysWarmUp[] = self::$VALUE_KEY_PREFIX . $key;
2602 foreach ( $checkKeys as $i => $checkKeyOrKeys ) {
2603 if ( is_int( $i ) ) {
2605 $keysWarmUp[] = self::$TIME_KEY_PREFIX . $checkKeyOrKeys;
2608 $keysWarmUp = array_merge(
2610 self::prefixCacheKeys( $checkKeyOrKeys, self::$TIME_KEY_PREFIX )
2626 if ( $this->wallClockOverride ) {
2627 return $this->wallClockOverride;
2630 $clockTime = (float)time();
2636 return max( microtime(
true ), $clockTime );
2644 $this->wallClockOverride =& $time;
2645 $this->cache->setMockTime( $time );
2646 foreach ( $this->processCaches as $pCache ) {
2647 $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)
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
static $INTERIM_KEY_PREFIX
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.
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 $COOLOFF_KEY_PREFIX
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.