201private $callbackDepth = 0;
203private $warmupCache = [];
205private $warmupKeyMisses = 0;
208private $wallClockOverride;
211privateconst MAX_COMMIT_DELAY = 3;
213privateconst MAX_READ_LAG = 7;
215publicconstHOLDOFF_TTL = self::MAX_COMMIT_DELAY + self::MAX_READ_LAG + 1;
218privateconst LOW_TTL = 60;
223privateconst HOT_TTR = 900;
225privateconst AGE_NEW = 60;
228privateconst TSE_NONE = -1;
238publicconst MIN_TIMESTAMP_NONE = 0.0;
241privateconst PC_PRIMARY =
'primary:1000';
247privateconst SCHEME_HASH_TAG = 1;
249privateconst SCHEME_HASH_STOP = 2;
252privateconst CHECK_KEY_TTL = self::TTL_YEAR;
254privateconst INTERIM_KEY_TTL = 2;
257privateconst LOCK_TTL = 10;
259privateconst RAMPUP_TTL = 30;
262privateconst TINY_NEGATIVE = -0.000001;
264privateconst TINY_POSITIVE = 0.000001;
267privateconst RECENT_SET_LOW_MS = 50;
269privateconst RECENT_SET_HIGH_MS = 100;
272privateconst GENERATION_HIGH_SEC = 0.2;
275privateconst PURGE_TIME = 0;
277privateconst PURGE_HOLDOFF = 1;
280privateconst VERSION = 1;
296privateconst RES_VALUE = 0;
298privateconst RES_VERSION = 1;
300privateconst RES_AS_OF = 2;
302privateconst RES_TTL = 3;
304privateconst RES_TOMB_AS_OF = 4;
306privateconst RES_CHECK_AS_OF = 5;
308privateconst RES_TOUCH_AS_OF = 6;
310privateconst RES_CUR_TTL = 7;
313privateconst FLD_FORMAT_VERSION = 0;
315privateconst FLD_VALUE = 1;
317privateconst FLD_TTL = 2;
319privateconst FLD_TIME = 3;
321privateconst FLD_FLAGS = 4;
323privateconst FLD_VALUE_VERSION = 5;
324privateconst FLD_GENERATION_TIME = 6;
327privateconst TYPE_VALUE =
'v';
329privateconst TYPE_TIMESTAMP =
't';
331privateconst TYPE_MUTEX =
'm';
333privateconst TYPE_INTERIM =
'i';
336privateconst PURGE_VAL_PREFIX =
'PURGED';
366 $this->cache = $params[
'cache'];
367 $this->broadcastRoute = $params[
'broadcastRoutingPrefix'] ??
null;
368 $this->epoch = $params[
'epoch'] ?? 0;
369if ( ( $params[
'coalesceScheme'] ??
'' ) ===
'hash_tag' ) {
370// https://redis.io/topics/cluster-spec 371// https://github.com/twitter/twemproxy/blob/v0.4.1/notes/recommendation.md#hash-tags 372// https://github.com/Netflix/dynomite/blob/v0.7.0/notes/recommendation.md#hash-tags 373 $this->coalesceScheme = self::SCHEME_HASH_TAG;
375// https://github.com/facebook/mcrouter/wiki/Key-syntax 376 $this->coalesceScheme = self::SCHEME_HASH_STOP;
379 $this->
setLogger( $params[
'logger'] ??
new NullLogger() );
380 $this->tracer = $params[
'tracer'] ??
newNoopTracer();
381 $this->stats = $params[
'stats'] ?? StatsFactory::newNull();
383 $this->asyncHandler = $params[
'asyncHandler'] ??
null;
384 $this->missLog = array_fill( 0, 10, [
'', 0.0 ] );
455finalpublicfunctionget( $key, &$curTTL =
null, array $checkKeys = [], &$info = [] ) {
456// Note that an undeclared variable passed as $info starts as null (not the default). 457// Also, if no $info parameter is provided, then it doesn't matter how it changes here. 458 $legacyInfo = ( $info !== self::PASS_BY_REF );
461 $span = $this->startOperationSpan( __FUNCTION__, $key, $checkKeys );
463 $now = $this->getCurrentTime();
464 $res = $this->fetchKeys( [ $key ], $checkKeys, $now )[$key];
466 $curTTL = $res[self::RES_CUR_TTL];
468 ? $res[self::RES_AS_OF]
470 self::KEY_VERSION => $res[self::RES_VERSION],
471 self::KEY_AS_OF => $res[self::RES_AS_OF],
472 self::KEY_TTL => $res[self::RES_TTL],
473 self::KEY_CUR_TTL => $res[self::RES_CUR_TTL],
474 self::KEY_TOMB_AS_OF => $res[self::RES_TOMB_AS_OF],
475 self::KEY_CHECK_AS_OF => $res[self::RES_CHECK_AS_OF]
478if ( $curTTL ===
null || $curTTL <= 0 ) {
479// Log the timestamp in case a corresponding set() call does not provide "walltime" 480 unset( $this->missLog[array_key_first( $this->missLog )] );
481 $this->missLog[] = [ $key, $this->getCurrentTime() ];
484return $res[self::RES_VALUE];
514 array $checkKeys = [],
517// Note that an undeclared variable passed as $info starts as null (not the default). 518// Also, if no $info parameter is provided, then it doesn't matter how it changes here. 519 $legacyInfo = ( $info !== self::PASS_BY_REF );
522 $span = $this->startOperationSpan( __FUNCTION__, $keys, $checkKeys );
528 $now = $this->getCurrentTime();
529 $resByKey = $this->fetchKeys( $keys, $checkKeys, $now );
530foreach ( $resByKey as $key => $res ) {
531if ( $res[self::RES_VALUE] !==
false ) {
532 $valuesByKey[$key] = $res[self::RES_VALUE];
535if ( $res[self::RES_CUR_TTL] !==
null ) {
536 $curTTLs[$key] = $res[self::RES_CUR_TTL];
538 $info[$key] = $legacyInfo
539 ? $res[self::RES_AS_OF]
541 self::KEY_VERSION => $res[self::RES_VERSION],
542 self::KEY_AS_OF => $res[self::RES_AS_OF],
543 self::KEY_TTL => $res[self::RES_TTL],
544 self::KEY_CUR_TTL => $res[self::RES_CUR_TTL],
545 self::KEY_TOMB_AS_OF => $res[self::RES_TOMB_AS_OF],
546 self::KEY_CHECK_AS_OF => $res[self::RES_CHECK_AS_OF]
568protectedfunctionfetchKeys( array $keys, array $checkKeys,
float $now, $touchedCb =
null ) {
571// List of all sister keys that need to be fetched from cache 573// Order-corresponding value sister key list for the base key list ($keys) 574 $valueSisterKeys = [];
575// List of "check" sister keys to compare all value sister keys against 576 $checkSisterKeysForAll = [];
577// Map of (base key => additional "check" sister key(s) to compare against) 578 $checkSisterKeysByKey = [];
580foreach ( $keys as $key ) {
581 $sisterKey = $this->makeSisterKey( $key, self::TYPE_VALUE );
582 $allSisterKeys[] = $sisterKey;
583 $valueSisterKeys[] = $sisterKey;
586foreach ( $checkKeys as $i => $checkKeyOrKeyGroup ) {
587// Note: avoid array_merge() inside loop in case there are many keys 589// Single "check" key that applies to all base keys 590 $sisterKey = $this->makeSisterKey( $checkKeyOrKeyGroup, self::TYPE_TIMESTAMP );
591 $allSisterKeys[] = $sisterKey;
592 $checkSisterKeysForAll[] = $sisterKey;
594// List of "check" keys that apply to a specific base key 595foreach ( (array)$checkKeyOrKeyGroup as $checkKey ) {
596 $sisterKey = $this->makeSisterKey( $checkKey, self::TYPE_TIMESTAMP );
597 $allSisterKeys[] = $sisterKey;
598 $checkSisterKeysByKey[$i][] = $sisterKey;
603if ( $this->warmupCache ) {
604// Get the wrapped values of the sister keys from the warmup cache 605 $wrappedBySisterKey = $this->warmupCache;
606 $sisterKeysMissing = array_diff( $allSisterKeys, array_keys( $wrappedBySisterKey ) );
607if ( $sisterKeysMissing ) {
608 $this->warmupKeyMisses += count( $sisterKeysMissing );
609 $wrappedBySisterKey += $this->cache->getMulti( $sisterKeysMissing );
612// Fetch the wrapped values of the sister keys from the backend 613 $wrappedBySisterKey = $this->cache->getMulti( $allSisterKeys );
616// List of "check" sister key purge timestamps to compare all value sister keys against 617 $ckPurgesForAll = $this->processCheckKeys(
618 $checkSisterKeysForAll,
622// Map of (base key => extra "check" sister key purge timestamp(s) to compare against) 624foreach ( $checkSisterKeysByKey as $keyWithCheckKeys => $checkKeysForKey ) {
625 $ckPurgesByKey[$keyWithCheckKeys] = $this->processCheckKeys(
632// Unwrap and validate any value found for each base key (under the value sister key) 634 array_map(
null, $valueSisterKeys, $keys )
635 as [ $valueSisterKey, $key ]
637if ( array_key_exists( $valueSisterKey, $wrappedBySisterKey ) ) {
638// Key exists as either a live value or tombstone value 639 $wrapped = $wrappedBySisterKey[$valueSisterKey];
645 $res = $this->unwrap( $wrapped, $now );
646 $value = $res[self::RES_VALUE];
648foreach ( array_merge( $ckPurgesForAll, $ckPurgesByKey[$key] ?? [] ) as $ckPurge ) {
649 $res[self::RES_CHECK_AS_OF] = max(
650 $ckPurge[self::PURGE_TIME],
651 $res[self::RES_CHECK_AS_OF]
653// Timestamp marking the end of the hold-off period for this purge 654 $holdoffDeadline = $ckPurge[self::PURGE_TIME] + $ckPurge[self::PURGE_HOLDOFF];
655// Check if the value was generated during the hold-off period 656if ( $value !==
false && $holdoffDeadline >= $res[self::RES_AS_OF] ) {
657// How long ago this value was purged by *this* "check" key 658 $ago = min( $ckPurge[self::PURGE_TIME] - $now, self::TINY_NEGATIVE );
659// How long ago this value was purged by *any* known "check" key 660 $res[self::RES_CUR_TTL] = min( $res[self::RES_CUR_TTL], $ago );
664if ( $touchedCb !==
null && $value !==
false ) {
665 $touched = $touchedCb( $value );
666if ( $touched !==
null && $touched >= $res[self::RES_AS_OF] ) {
667 $res[self::RES_CUR_TTL] = min(
668 $res[self::RES_CUR_TTL],
669 $res[self::RES_AS_OF] - $touched,
677 $res[self::RES_TOUCH_AS_OF] = max( $res[self::RES_TOUCH_AS_OF], $touched );
679 $resByKey[$key] = $res;
691privatefunction processCheckKeys(
692 array $checkSisterKeys,
693 array $wrappedBySisterKey,
698foreach ( $checkSisterKeys as $timeKey ) {
699 $purge = isset( $wrappedBySisterKey[$timeKey] )
700 ? $this->parsePurgeValue( $wrappedBySisterKey[$timeKey] )
703if ( $purge ===
null ) {
704// No holdoff when lazy creating a check key, use cache right away (T344191) 705 $wrapped = $this->makeCheckPurgeValue( $now, self::HOLDOFF_TTL_NONE, $purge );
710 $this->cache::WRITE_BACKGROUND
803finalpublicfunctionset( $key, $value, $ttl = self::TTL_INDEFINITE, array $opts = [] ) {
805 $span = $this->startOperationSpan( __FUNCTION__, $key );
807 $keygroup = $this->determineKeyGroupForStats( $key );
809 $ok = $this->setMainValue(
813 $opts[
'version'] ??
null,
814 $opts[
'walltime'] ??
null,
816 $opts[
'since'] ??
null,
817 $opts[
'pending'] ??
false,
818 $opts[
'lockTSE'] ?? self::TSE_NONE,
819 $opts[
'staleTTL'] ?? self::STALE_TTL_NONE,
820 $opts[
'segmentable'] ??
false,
821 $opts[
'creating'] ??
false 824 $this->stats->getCounter(
'wanobjectcache_set_total' )
825 ->setLabel(
'keygroup', $keygroup )
826 ->setLabel(
'result', ( $ok ?
'ok' :
'error' ) )
827 ->copyToStatsdAt(
"wanobjectcache.$keygroup.set." . ( $ok ?
'ok' :
'error' ) )
848privatefunction setMainValue(
856bool $dataPendingCommit,
867 $now = $this->getCurrentTime();
869 $walltime ??= $this->timeSinceLoggedMiss( $key, $now );
870 $dataSnapshotLag = ( $dataReadSince !== null ) ? max( 0, $now - $dataReadSince ) : 0;
871 $dataCombinedLag = $dataReplicaLag + $dataSnapshotLag;
873// Forbid caching data that only exists within an uncommitted transaction. Also, lower 874// the TTL when the data has a "since" time so far in the past that a delete() tombstone, 875// made after that time, could have already expired (the key is no longer write-holed). 876// The mitigation TTL depends on whether this data lag is assumed to systemically effect 877// regeneration attempts in the near future. The TTL also reflects regeneration wall time. 878if ( $dataPendingCommit ) {
879// Case A: data comes from an uncommitted write transaction 880 $mitigated =
'pending writes';
881// Data might never be committed; rely on a less problematic regeneration attempt 882 $mitigationTTL = self::TTL_UNCACHEABLE;
883 } elseif ( $dataSnapshotLag > self::MAX_READ_LAG ) {
884// Case B: high snapshot lag 885 $pregenSnapshotLag = ( $walltime !== null ) ? ( $dataSnapshotLag - $walltime ) : 0;
886if ( ( $pregenSnapshotLag + self::GENERATION_HIGH_SEC ) > self::MAX_READ_LAG ) {
887// Case B1: generation started when transaction duration was already long 888 $mitigated =
'snapshot lag (late generation)';
889// Probably non-systemic; rely on a less problematic regeneration attempt 890 $mitigationTTL = self::TTL_UNCACHEABLE;
892// Case B2: slow generation made transaction duration long 893 $mitigated =
'snapshot lag (high generation time)';
894// Probably systemic; use a low TTL to avoid stampedes/uncacheability 895 $mitigationTTL = self::TTL_LAGGED;
897 } elseif ( $dataReplicaLag ===
false || $dataReplicaLag > self::MAX_READ_LAG ) {
898// Case C: low/medium snapshot lag with high replication lag 899 $mitigated =
'replication lag';
900// Probably systemic; use a low TTL to avoid stampedes/uncacheability 901 $mitigationTTL = self::TTL_LAGGED;
902 } elseif ( $dataCombinedLag > self::MAX_READ_LAG ) {
903 $pregenCombinedLag = ( $walltime !== null ) ? ( $dataCombinedLag - $walltime ) : 0;
904// Case D: medium snapshot lag with medium replication lag 905if ( ( $pregenCombinedLag + self::GENERATION_HIGH_SEC ) > self::MAX_READ_LAG ) {
906// Case D1: generation started when read lag was too high 907 $mitigated =
'read lag (late generation)';
908// Probably non-systemic; rely on a less problematic regeneration attempt 909 $mitigationTTL = self::TTL_UNCACHEABLE;
911// Case D2: slow generation made read lag too high 912 $mitigated =
'read lag (high generation time)';
913// Probably systemic; use a low TTL to avoid stampedes/uncacheability 914 $mitigationTTL = self::TTL_LAGGED;
917// Case E: new value generated with recent data 919// Nothing to mitigate 920 $mitigationTTL =
null;
923if ( $mitigationTTL === self::TTL_UNCACHEABLE ) {
924 $this->logger->warning(
925"Rejected set() for {cachekey} due to $mitigated.",
928'lag' => $dataReplicaLag,
929'age' => $dataSnapshotLag,
930'walltime' => $walltime
934// no-op the write for being unsafe 938// TTL to use in staleness checks (does not effect persistence layer TTL) 941if ( $mitigationTTL !==
null ) {
942// New value was generated from data that is old enough to be risky 943if ( $lockTSE >= 0 ) {
944// Persist the value as long as normal, but make it count as stale sooner 945 $logicalTTL = min( $ttl ?: INF, $mitigationTTL );
947// Persist the value for a shorter duration 948 $ttl = min( $ttl ?: INF, $mitigationTTL );
951 $this->logger->warning(
952"Lowered set() TTL for {cachekey} due to $mitigated.",
955'lag' => $dataReplicaLag,
956'age' => $dataSnapshotLag,
957'walltime' => $walltime
962// Wrap that value with time/TTL/version metadata 963 $wrapped = $this->wrap( $value, $logicalTTL ?: $ttl, $version, $now );
964 $storeTTL = $ttl + $staleTTL;
966 $flags = $this->cache::WRITE_BACKGROUND;
968 $flags |= $this->cache::WRITE_ALLOW_SEGMENTS;
972 $ok = $this->cache->add(
973 $this->makeSisterKey( $key, self::TYPE_VALUE ),
979 $ok = $this->cache->merge(
980 $this->makeSisterKey( $key, self::TYPE_VALUE ),
981staticfunction ( $cache, $key, $cWrapped ) use ( $wrapped ) {
982// A string value means that it is a tombstone; do nothing in that case 983return ( is_string( $cWrapped ) ) ?
false : $wrapped;
986 $this->cache::MAX_CONFLICTS_ONE,
1056finalpublicfunctiondelete( $key, $ttl = self::HOLDOFF_TTL ) {
1058 $span = $this->startOperationSpan( __FUNCTION__, $key );
1060// Purge values must be stored under the value key so that WANObjectCache::set() 1061// can atomically merge values without accidentally undoing a recent purge and thus 1062// violating the holdoff TTL restriction. 1063 $valueSisterKey = $this->makeSisterKey( $key, self::TYPE_VALUE );
1066// A client or cache cleanup script is requesting a cache purge, so there is no 1067// volatility period due to replica DB lag. Any recent change to an entity cached 1068// in this key should have triggered an appropriate purge event. 1069 $ok = $this->cache->delete( $this->getRouteKey( $valueSisterKey ), $this->cache::WRITE_BACKGROUND );
1071// A cacheable entity recently changed, so there might be a volatility period due 1072// to replica DB lag. Clients usually expect their actions to be reflected in any 1073// of their subsequent web request. This is attainable if (a) purge relay lag is 1074// lower than the time it takes for subsequent request by the client to arrive, 1075// and, (b) DB replica queries have "read-your-writes" consistency due to DB lag 1076// mitigation systems. 1077 $now = $this->getCurrentTime();
1078// Set the key to the purge value in all datacenters 1079 $purge = self::PURGE_VAL_PREFIX .
':' . (int)$now;
1080 $ok = $this->cache->set(
1081 $this->getRouteKey( $valueSisterKey ),
1084 $this->cache::WRITE_BACKGROUND
1088 $keygroup = $this->determineKeyGroupForStats( $key );
1090 $this->stats->getCounter(
'wanobjectcache_delete_total' )
1091 ->setLabel(
'keygroup', $keygroup )
1092 ->setLabel(
'result', ( $ok ?
'ok' :
'error' ) )
1093 ->copyToStatsdAt(
"wanobjectcache.$keygroup.delete." . ( $ok ?
'ok' :
'error' ) )
1120 $span = $this->startOperationSpan( __FUNCTION__, $key );
1122return $this->getMultiCheckKeyTime( [ $key ] )[$key];
1188 $span = $this->startOperationSpan( __FUNCTION__, $keys );
1190 $checkSisterKeysByKey = [];
1191foreach ( $keys as $key ) {
1192 $checkSisterKeysByKey[$key] = $this->makeSisterKey( $key, self::TYPE_TIMESTAMP );
1195 $wrappedBySisterKey = $this->cache->getMulti( $checkSisterKeysByKey );
1196 $wrappedBySisterKey += array_fill_keys( $checkSisterKeysByKey,
false );
1198 $now = $this->getCurrentTime();
1200foreach ( $checkSisterKeysByKey as $key => $checkSisterKey ) {
1201 $purge = $this->parsePurgeValue( $wrappedBySisterKey[$checkSisterKey] );
1202if ( $purge ===
null ) {
1203 $wrapped = $this->makeCheckPurgeValue( $now, self::HOLDOFF_TTL_NONE, $purge );
1207 self::CHECK_KEY_TTL,
1208 $this->cache::WRITE_BACKGROUND
1212 $times[$key] = $purge[self::PURGE_TIME];
1253 $span = $this->startOperationSpan( __FUNCTION__, $key );
1255 $checkSisterKey = $this->makeSisterKey( $key, self::TYPE_TIMESTAMP );
1257 $now = $this->getCurrentTime();
1258 $purge = $this->makeCheckPurgeValue( $now, $holdoff );
1259 $ok = $this->cache->set(
1260 $this->getRouteKey( $checkSisterKey ),
1262 self::CHECK_KEY_TTL,
1263 $this->cache::WRITE_BACKGROUND
1266 $keygroup = $this->determineKeyGroupForStats( $key );
1268 $this->stats->getCounter(
'wanobjectcache_check_total' )
1269 ->setLabel(
'keygroup', $keygroup )
1270 ->setLabel(
'result', ( $ok ?
'ok' :
'error' ) )
1271 ->copyToStatsdAt(
"wanobjectcache.$keygroup.ck_touch." . ( $ok ?
'ok' :
'error' ) )
1306 $span = $this->startOperationSpan( __FUNCTION__, $key );
1308 $checkSisterKey = $this->makeSisterKey( $key, self::TYPE_TIMESTAMP );
1309 $ok = $this->cache->delete( $this->getRouteKey( $checkSisterKey ), $this->cache::WRITE_BACKGROUND );
1311 $keygroup = $this->determineKeyGroupForStats( $key );
1313 $this->stats->getCounter(
'wanobjectcache_reset_total' )
1314 ->setLabel(
'keygroup', $keygroup )
1315 ->setLabel(
'result', ( $ok ?
'ok' :
'error' ) )
1316 ->copyToStatsdAt(
"wanobjectcache.$keygroup.ck_reset." . ( $ok ?
'ok' :
'error' ) )
1624 $key, $ttl, $callback, array $opts = [], array $cbParams = []
1627 $span = $this->startOperationSpan( __FUNCTION__, $key );
1629 $version = $opts[
'version'] ??
null;
1630 $pcTTL = $opts[
'pcTTL'] ?? self::TTL_UNCACHEABLE;
1631 $pCache = ( $pcTTL >= 0 )
1632 ? $this->getProcessCache( $opts[
'pcGroup'] ?? self::PC_PRIMARY )
1635// Use the process cache if requested as long as no outer cache callback is running. 1636// Nested callback process cache use is not lag-safe with regard to HOLDOFF_TTL since 1637// process cached values are more lagged than persistent ones as they are not purged. 1638if ( $pCache && $this->callbackDepth == 0 ) {
1639 $cached = $pCache->get( $key, $pcTTL,
false );
1640if ( $cached !==
false ) {
1641 $this->logger->debug(
"getWithSetCallback($key): process cache hit" );
1646 [ $value, $valueVersion, $curAsOf ] = $this->fetchOrRegenerate( $key, $ttl, $callback, $opts, $cbParams );
1647if ( $valueVersion !== $version ) {
1648// Current value has a different version; use the variant key for this version. 1649// Regenerate the variant value if it is not newer than the main value at $key 1650// so that purges to the main key propagate to the variant value. 1651 $this->logger->debug(
"getWithSetCallback($key): using variant key" );
1652 [ $value ] = $this->fetchOrRegenerate(
1653 $this->makeGlobalKey(
'WANCache-key-variant', md5( $key ), (
string)$version ),
1656 [
'version' =>
null,
'minAsOf' => $curAsOf ] + $opts,
1661// Update the process cache if enabled 1662if ( $pCache && $value !==
false ) {
1663 $pCache->set( $key, $value );
1685privatefunction fetchOrRegenerate( $key, $ttl, $callback, array $opts, array $cbParams ) {
1686 $checkKeys = $opts[
'checkKeys'] ?? [];
1687 $graceTTL = $opts[
'graceTTL'] ?? self::GRACE_TTL_NONE;
1688 $minAsOf = $opts[
'minAsOf'] ?? self::MIN_TIMESTAMP_NONE;
1689 $hotTTR = $opts[
'hotTTR'] ?? self::HOT_TTR;
1690 $lowTTL = $opts[
'lowTTL'] ?? min( self::LOW_TTL, $ttl );
1691 $ageNew = $opts[
'ageNew'] ?? self::AGE_NEW;
1692 $touchedCb = $opts[
'touchedCallback'] ??
null;
1693 $startTime = $this->getCurrentTime();
1695 $keygroup = $this->determineKeyGroupForStats( $key );
1697// Get the current key value and its metadata 1698 $curState = $this->fetchKeys( [ $key ], $checkKeys, $startTime, $touchedCb )[$key];
1699 $curValue = $curState[self::RES_VALUE];
1701// Use the cached value if it exists and is not due for synchronous regeneration 1702if ( $this->isAcceptablyFreshValue( $curState, $graceTTL, $minAsOf ) ) {
1703if ( !$this->isLotteryRefreshDue( $curState, $lowTTL, $ageNew, $hotTTR, $startTime ) ) {
1704 $this->stats->getTiming(
'wanobjectcache_getwithset_seconds' )
1705 ->setLabel(
'keygroup', $keygroup )
1706 ->setLabel(
'result',
'hit' )
1707 ->setLabel(
'reason',
'good' )
1708 ->copyToStatsdAt(
"wanobjectcache.$keygroup.hit.good" )
1709 ->observe( 1e3 * ( $this->getCurrentTime() - $startTime ) );
1711return [ $curValue, $curState[self::RES_VERSION], $curState[self::RES_AS_OF] ];
1712 } elseif ( $this->scheduleAsyncRefresh( $key, $ttl, $callback, $opts, $cbParams ) ) {
1713 $this->logger->debug(
"fetchOrRegenerate($key): hit with async refresh" );
1715 $this->stats->getTiming(
'wanobjectcache_getwithset_seconds' )
1716 ->setLabel(
'keygroup', $keygroup )
1717 ->setLabel(
'result',
'hit' )
1718 ->setLabel(
'reason',
'refresh' )
1719 ->copyToStatsdAt(
"wanobjectcache.$keygroup.hit.refresh" )
1720 ->observe( 1e3 * ( $this->getCurrentTime() - $startTime ) );
1722return [ $curValue, $curState[self::RES_VERSION], $curState[self::RES_AS_OF] ];
1724 $this->logger->debug(
"fetchOrRegenerate($key): hit with sync refresh" );
1728 $isKeyTombstoned = ( $curState[self::RES_TOMB_AS_OF] !== null );
1729// Use the interim key as a temporary alternative if the key is tombstoned 1730if ( $isKeyTombstoned ) {
1731 $volState = $this->getInterimValue( $key, $minAsOf, $startTime, $touchedCb );
1732 $volValue = $volState[self::RES_VALUE];
1734 $volState = $curState;
1735 $volValue = $curValue;
1738// During the volatile "hold-off" period that follows a purge of the key, the value 1739// will be regenerated many times if frequently accessed. This is done to mitigate 1740// the effects of backend replication lag as soon as possible. However, throttle the 1741// overhead of locking and regeneration by reusing values recently written to cache 1742// tens of milliseconds ago. Verify the "as of" time against the last purge event. 1743 $lastPurgeTime = max(
1744// RES_TOUCH_AS_OF depends on the value (possibly from the interim key) 1745 $volState[self::RES_TOUCH_AS_OF],
1746 $curState[self::RES_TOMB_AS_OF],
1747 $curState[self::RES_CHECK_AS_OF]
1749 $safeMinAsOf = max( $minAsOf, $lastPurgeTime + self::TINY_POSITIVE );
1751if ( $volState[self::RES_VALUE] ===
false || $volState[self::RES_AS_OF] < $safeMinAsOf ) {
1752 $isExtremelyNewValue =
false;
1754 $age = $startTime - $volState[self::RES_AS_OF];
1755 $isExtremelyNewValue = ( $age < mt_rand( self::RECENT_SET_LOW_MS, self::RECENT_SET_HIGH_MS ) / 1e3 );
1757if ( $isExtremelyNewValue ) {
1758 $this->logger->debug(
"fetchOrRegenerate($key): volatile hit" );
1760 $this->stats->getTiming(
'wanobjectcache_getwithset_seconds' )
1761 ->setLabel(
'keygroup', $keygroup )
1762 ->setLabel(
'result',
'hit' )
1763 ->setLabel(
'reason',
'volatile' )
1764 ->copyToStatsdAt(
"wanobjectcache.$keygroup.hit.volatile" )
1765 ->observe( 1e3 * ( $this->getCurrentTime() - $startTime ) );
1767return [ $volValue, $volState[self::RES_VERSION], $curState[self::RES_AS_OF] ];
1770 $lockTSE = $opts[
'lockTSE'] ?? self::TSE_NONE;
1771 $busyValue = $opts[
'busyValue'] ??
null;
1772 $staleTTL = $opts[
'staleTTL'] ?? self::STALE_TTL_NONE;
1773 $segmentable = $opts[
'segmentable'] ??
false;
1774 $version = $opts[
'version'] ??
null;
1776// Determine whether one thread per datacenter should handle regeneration at a time 1777 $useRegenerationLock =
1778// Note that since tombstones no-op set(), $lockTSE and $curTTL cannot be used to 1779// deduce the key hotness because |$curTTL| will always keep increasing until the 1780// tombstone expires or is overwritten by a new tombstone. Also, even if $lockTSE 1781// is not set, constant regeneration of a key for the tombstone lifetime might be 1782// very expensive. Assume tombstoned keys are possibly hot in order to reduce 1783// the risk of high regeneration load after the delete() method is called. 1785// Assume a key is hot if requested soon ($lockTSE seconds) after purge. 1786// This avoids stampedes when timestamps from $checkKeys/$touchedCb bump. 1788 $curState[self::RES_CUR_TTL] !==
null &&
1789 $curState[self::RES_CUR_TTL] <= 0 &&
1790 abs( $curState[self::RES_CUR_TTL] ) <= $lockTSE
1792// Assume a key is hot if there is no value and a busy fallback is given. 1793// This avoids stampedes on eviction or preemptive regeneration taking too long. 1794 ( $busyValue !==
null && $volValue ===
false );
1796// If a regeneration lock is required, threads that do not get the lock will try to use 1797// the stale value, the interim value, or the $busyValue placeholder, in that order. If 1798// none of those are set then all threads will bypass the lock and regenerate the value. 1799 $mutexKey = $this->makeSisterKey( $key, self::TYPE_MUTEX );
1800// Note that locking is not bypassed due to I/O errors; this avoids stampedes 1801 $hasLock = $useRegenerationLock && $this->cache->add( $mutexKey, 1, self::LOCK_TTL );
1802if ( $useRegenerationLock && !$hasLock ) {
1803// Determine if there is stale or volatile cached value that is still usable 1804// @phan-suppress-next-line PhanTypeMismatchArgumentNullable False positive 1805if ( $this->isValid( $volValue, $volState[self::RES_AS_OF], $minAsOf ) ) {
1806 $this->logger->debug(
"fetchOrRegenerate($key): returning stale value" );
1808 $this->stats->getTiming(
'wanobjectcache_getwithset_seconds' )
1809 ->setLabel(
'keygroup', $keygroup )
1810 ->setLabel(
'result',
'hit' )
1811 ->setLabel(
'reason',
'stale' )
1812 ->copyToStatsdAt(
"wanobjectcache.$keygroup.hit.stale" )
1813 ->observe( 1e3 * ( $this->getCurrentTime() - $startTime ) );
1815return [ $volValue, $volState[self::RES_VERSION], $curState[self::RES_AS_OF] ];
1816 } elseif ( $busyValue !==
null ) {
1817 $miss = is_infinite( $minAsOf ) ?
'renew' :
'miss';
1818 $this->logger->debug(
"fetchOrRegenerate($key): busy $miss" );
1820 $this->stats->getTiming(
'wanobjectcache_getwithset_seconds' )
1821 ->setLabel(
'keygroup', $keygroup )
1822 ->setLabel(
'result', $miss )
1823 ->setLabel(
'reason',
'busy' )
1824 ->copyToStatsdAt(
"wanobjectcache.$keygroup.$miss.busy" )
1825 ->observe( 1e3 * ( $this->getCurrentTime() - $startTime ) );
1827 $placeholderValue = ( $busyValue instanceof Closure ) ? $busyValue() : $busyValue;
1829return [ $placeholderValue, $version, $curState[self::RES_AS_OF] ];
1833// Generate the new value given any prior value with a matching version 1835 $preCallbackTime = $this->getCurrentTime();
1836 ++$this->callbackDepth;
1837// https://github.com/phan/phan/issues/4419 1842 ( $curState[self::RES_VERSION] === $version ) ? $curValue : false,
1845 ( $curState[self::RES_VERSION] === $version ) ? $curState[self::RES_AS_OF] : null,
1849 --$this->callbackDepth;
1851 $postCallbackTime = $this->getCurrentTime();
1853// How long it took to generate the value 1854 $walltime = max( $postCallbackTime - $preCallbackTime, 0.0 );
1856 $this->stats->getTiming(
'wanobjectcache_regen_seconds' )
1857 ->setLabel(
'keygroup', $keygroup )
1858 ->copyToStatsdAt(
"wanobjectcache.$keygroup.regen_walltime" )
1859 ->observe( 1e3 * $walltime );
1861// Attempt to save the newly generated value if applicable 1863// Callback yielded a cacheable value 1864 ( $value !==
false && $ttl >= 0 ) &&
1865// Current thread was not raced out of a regeneration lock or key is tombstoned 1866 ( !$useRegenerationLock || $hasLock || $isKeyTombstoned )
1868// If the key is write-holed then use the (volatile) interim key as an alternative 1869if ( $isKeyTombstoned ) {
1870 $this->setInterimValue(
1878 $this->setMainValue(
1884// @phan-suppress-next-line PhanCoalescingAlwaysNull 1885 $setOpts[
'lag'] ?? 0,
1886// @phan-suppress-next-line PhanCoalescingAlwaysNull 1887 $setOpts[
'since'] ?? $preCallbackTime,
1888// @phan-suppress-next-line PhanCoalescingAlwaysNull 1889 $setOpts[
'pending'] ??
false,
1893 ( $curValue ===
false )
1899 $this->cache->delete( $mutexKey, $this->cache::WRITE_BACKGROUND );
1902 $miss = is_infinite( $minAsOf ) ?
'renew' :
'miss';
1903 $this->logger->debug(
"fetchOrRegenerate($key): $miss, new value computed" );
1905 $this->stats->getTiming(
'wanobjectcache_getwithset_seconds' )
1906 ->setLabel(
'keygroup', $keygroup )
1907 ->setLabel(
'result', $miss )
1908 ->setLabel(
'reason',
'compute' )
1909 ->copyToStatsdAt(
"wanobjectcache.$keygroup.$miss.compute" )
1910 ->observe( 1e3 * ( $this->getCurrentTime() - $startTime ) );
1912return [ $value, $version, $curState[self::RES_AS_OF] ];
1924privatefunction makeSisterKey(
string $baseKey,
string $typeChar ) {
1925if ( $this->coalesceScheme === self::SCHEME_HASH_STOP ) {
1926// Key style: "WANCache:<base key>|#|<character>" 1927 $sisterKey =
'WANCache:' . $baseKey .
'|#|' . $typeChar;
1929// Key style: "WANCache:{<base key>}:<character>" 1930 $sisterKey =
'WANCache:{' . $baseKey .
'}:' . $typeChar;
1944privatefunction getInterimValue( $key, $minAsOf, $now, $touchedCb ) {
1945if ( $this->useInterimHoldOffCaching ) {
1946 $interimSisterKey = $this->makeSisterKey( $key, self::TYPE_INTERIM );
1947 $wrapped = $this->cache->get( $interimSisterKey );
1948 $res = $this->unwrap( $wrapped, $now );
1949if ( $res[self::RES_VALUE] !==
false && $res[self::RES_AS_OF] >= $minAsOf ) {
1950if ( $touchedCb !==
null ) {
1951// Update "last purge time" since the $touchedCb timestamp depends on $value 1952// Get the new "touched timestamp", accounting for callback-checked dependencies 1953 $res[self::RES_TOUCH_AS_OF] = max(
1954 $touchedCb( $res[self::RES_VALUE] ),
1955 $res[self::RES_TOUCH_AS_OF]
1963return $this->unwrap(
false, $now );
1974privatefunction setInterimValue(
1981 $now = $this->getCurrentTime();
1982 $ttl = max( self::INTERIM_KEY_TTL, (
int)$ttl );
1984// Wrap that value with time/TTL/version metadata 1985 $wrapped = $this->wrap( $value, $ttl, $version, $now );
1987 $flags = $this->cache::WRITE_BACKGROUND;
1988if ( $segmentable ) {
1989 $flags |= $this->cache::WRITE_ALLOW_SEGMENTS;
1992return $this->cache->set(
1993 $this->makeSisterKey( $key, self::TYPE_INTERIM ),
2066 ArrayIterator $keyedIds, $ttl, callable $callback, array $opts = []
2068 $span = $this->startOperationSpan( __FUNCTION__,
'' );
2069if ( $span->getContext()->isSampled() ) {
2070 $span->setAttributes( [
2071'org.wikimedia.wancache.multi_count' => $keyedIds->count(),
2072'org.wikimedia.wancache.ttl' => $ttl,
2075// Batch load required keys into the in-process warmup cache 2076 $this->warmupCache = $this->fetchWrappedValuesForWarmupCache(
2077 $this->getNonProcessCachedMultiKeys( $keyedIds, $opts ),
2078 $opts[
'checkKeys'] ?? []
2080 $this->warmupKeyMisses = 0;
2082// The required callback signature includes $id as the first argument for convenience 2083// to distinguish different items. To reuse the code in getWithSetCallback(), wrap the 2084// callback with a proxy callback that has the standard getWithSetCallback() signature. 2085// This is defined only once per batch to avoid closure creation overhead. 2086 $proxyCb =
staticfunction ( $oldValue, &$ttl, &$setOpts, $oldAsOf, $params )
2089return $callback( $params[
'id'], $oldValue, $ttl, $setOpts, $oldAsOf );
2092// Get the order-preserved result map using the warm-up cache 2094foreach ( $keyedIds as $key => $id ) {
2095 $values[$key] = $this->getWithSetCallback(
2104 $this->warmupCache = [];
2176 ArrayIterator $keyedIds, $ttl, callable $callback, array $opts = []
2178 $span = $this->startOperationSpan( __FUNCTION__,
'' );
2179if ( $span->getContext()->isSampled() ) {
2180 $span->setAttributes( [
2181'org.wikimedia.wancache.multi_count' => $keyedIds->count(),
2182'org.wikimedia.wancache.ttl' => $ttl,
2185 $checkKeys = $opts[
'checkKeys'] ?? [];
// TODO: ??? 2186 $minAsOf = $opts[
'minAsOf'] ?? self::MIN_TIMESTAMP_NONE;
2188// unset incompatible keys 2189 unset( $opts[
'lockTSE'] );
2190 unset( $opts[
'busyValue'] );
2192// Batch load required keys into the in-process warmup cache 2193 $keysByIdGet = $this->getNonProcessCachedMultiKeys( $keyedIds, $opts );
2194 $this->warmupCache = $this->fetchWrappedValuesForWarmupCache( $keysByIdGet, $checkKeys );
2195 $this->warmupKeyMisses = 0;
2197// IDs of entities known to be in need of generation 2200// Find out which keys are missing/deleted/stale 2201 $now = $this->getCurrentTime();
2202 $resByKey = $this->fetchKeys( $keysByIdGet, $checkKeys, $now );
2203foreach ( $keysByIdGet as $id => $key ) {
2204 $res = $resByKey[$key];
2206 $res[self::RES_VALUE] ===
false ||
2207 $res[self::RES_CUR_TTL] < 0 ||
2208 $res[self::RES_AS_OF] < $minAsOf
2214// Run the callback to populate the generation value map for all required IDs 2216 $newTTLsById = array_fill_keys( $idsRegen, $ttl );
2217 $newValsById = $idsRegen ? $callback( $idsRegen, $newTTLsById, $newSetOpts ) : [];
2219 $method = __METHOD__;
2220// The required callback signature includes $id as the first argument for convenience 2221// to distinguish different items. To reuse the code in getWithSetCallback(), wrap the 2222// callback with a proxy callback that has the standard getWithSetCallback() signature. 2223// This is defined only once per batch to avoid closure creation overhead. 2224 $proxyCb =
function ( $oldValue, &$ttl, &$setOpts, $oldAsOf, $params )
2225 use ( $callback, $newValsById, $newTTLsById, $newSetOpts, $method )
2227 $id = $params[
'id'];
2229if ( array_key_exists( $id, $newValsById ) ) {
2230// Value was already regenerated as expected, so use the value in $newValsById 2231 $newValue = $newValsById[$id];
2232 $ttl = $newTTLsById[$id];
2233 $setOpts = $newSetOpts;
2235// Pre-emptive/popularity refresh and version mismatch cases are not detected 2236// above and thus $newValsById has no entry. Run $callback on this single entity. 2237 $ttls = [ $id => $ttl ];
2238 $result = $callback( [ $id ], $ttls, $setOpts );
2239if ( !isset( $result[$id] ) ) {
2241 $this->logger->warning(
2242 $method .
' failed due to {id} not set in result {result}', [
2244'result' => json_encode( $result )
2247 $newValue = $result[$id];
2254// Get the order-preserved result map using the warm-up cache 2256foreach ( $keyedIds as $key => $id ) {
2257 $values[$key] = $this->getWithSetCallback(
2266 $this->warmupCache = [];
2279return $this->cache->makeGlobalKey( $keygroup, ...$components );
2289publicfunctionmakeKey( $keygroup, ...$components ) {
2290return $this->cache->makeKey( $keygroup, ...$components );
2336foreach ( $ids as $id ) {
2337 $key = $keyCallback( $id, $this );
2338// Edge case: ignore key collisions due to duplicate $ids like "42" and 42 2339if ( !isset( $idByKey[$key] ) ) {
2340 $idByKey[$key] = $id;
2341 } elseif ( (
string)$id !== (
string)$idByKey[$key] ) {
2342thrownew UnexpectedValueException(
2343"Cache key collision; IDs ('$id','{$idByKey[$key]}') map to '$key'" 2348returnnew ArrayIterator( $idByKey );
2387if ( count( $ids ) !== count( $res ) ) {
2388// If makeMultiKeys() is called on a list of non-unique IDs, then the resulting 2389// ArrayIterator will have less entries due to "first appearance" de-duplication 2390 $ids = array_keys( array_fill_keys( $ids,
true ) );
2391if ( count( $ids ) !== count( $res ) ) {
2392thrownew UnexpectedValueException(
"Multi-key result does not match ID list" );
2396return array_combine( $ids, $res );
2406return $this->cache->watchErrors();
2427 $code = $this->cache->getLastError( $watchPoint );
2446 $this->processCaches = [];
2470 $this->useInterimHoldOffCaching = $enabled;
2479return $this->cache->getQoS( $flag );
2545publicfunctionadaptiveTTL( $mtime, $maxTTL, $minTTL = 30, $factor = 0.2 ) {
2546// handle fractional seconds and string integers 2547 $mtime = (int)$mtime;
2549// no last-modified time provided 2553 $age = (int)$this->getCurrentTime() - $mtime;
2555return (
int)min( $maxTTL, max( $minTTL, $factor * $age ) );
2564// Number of misses in $this->warmupCache during the last call to certain methods 2565return $this->warmupKeyMisses;
2573if ( $this->broadcastRoute !==
null ) {
2574if ( $sisterKey[0] ===
'/' ) {
2575thrownew RuntimeException(
"Sister key '$sisterKey' already contains a route." );
2577return $this->broadcastRoute . $sisterKey;
2593privatefunction scheduleAsyncRefresh( $key, $ttl, $callback, array $opts, array $cbParams ) {
2594if ( !$this->asyncHandler ) {
2597// Update the cache value later, such during post-send of an HTTP request. This forces 2598// cache regeneration by setting "minAsOf" to infinity, meaning that no existing value 2599// is considered valid. Furthermore, note that preemptive regeneration is not applicable 2600// to invalid values, so there is no risk of infinite preemptive regeneration loops. 2601 $func = $this->asyncHandler;
2602 $func(
function () use ( $key, $ttl, $callback, $opts, $cbParams ) {
2603 $opts[
'minAsOf'] = INF;
2605 $this->fetchOrRegenerate( $key, $ttl, $callback, $opts, $cbParams );
2606 }
catch ( Exception $e ) {
2607// Log some context for easier debugging 2608 $this->logger->error(
'Async refresh failed for {key}', [
2628privatefunction isAcceptablyFreshValue( $res, $graceTTL, $minAsOf ) {
2629if ( !$this->isValid( $res[self::RES_VALUE], $res[self::RES_AS_OF], $minAsOf ) ) {
2630// Value does not exists or is too old 2634 $curTTL = $res[self::RES_CUR_TTL];
2636// Value is definitely still fresh 2640// Remaining seconds during which this stale value can be used 2641 $curGraceTTL = $graceTTL + $curTTL;
2643return ( $curGraceTTL > 0 )
2644// Chance of using the value decreases as $curTTL goes from 0 to -$graceTTL 2645 ? !$this->worthRefreshExpiring( $curGraceTTL, $graceTTL, $graceTTL )
2646// Value is too stale to fall in the grace period 2661 $curTTL = $res[self::RES_CUR_TTL];
2662 $logicalTTL = $res[self::RES_TTL];
2663 $asOf = $res[self::RES_AS_OF];
2666 $this->worthRefreshExpiring( $curTTL, $logicalTTL, $lowTTL ) ||
2667 $this->worthRefreshPopular( $asOf, $ageNew, $hotTTR, $now )
2687if ( $ageNew < 0 || $timeTillRefresh <= 0 ) {
2691 $age = $now - $asOf;
2692 $timeOld = $age - $ageNew;
2693if ( $timeOld <= 0 ) {
2697 $popularHitsPerSec = 1;
2698// Lifecycle is: new, ramp-up refresh chance, full refresh chance. 2699// Note that the "expected # of refreshes" for the ramp-up time range is half 2700// of what it would be if P(refresh) was at its full value during that time range. 2701 $refreshWindowSec = max( $timeTillRefresh - $ageNew - self::RAMPUP_TTL / 2, 1 );
2702// P(refresh) * (# hits in $refreshWindowSec) = (expected # of refreshes) 2703// P(refresh) * ($refreshWindowSec * $popularHitsPerSec) = 1 (by definition) 2704// P(refresh) = 1/($refreshWindowSec * $popularHitsPerSec) 2705 $chance = 1 / ( $popularHitsPerSec * $refreshWindowSec );
2706// Ramp up $chance from 0 to its nominal value over RAMPUP_TTL seconds to avoid stampedes 2707 $chance *= ( $timeOld <= self::RAMPUP_TTL ) ? $timeOld / self::RAMPUP_TTL : 1;
2709return ( mt_rand( 1, 1_000_000_000 ) <= 1_000_000_000 * $chance );
2731if ( $lowTTL <= 0 ) {
2734// T264787: avoid having keys start off with a high chance of being refreshed; 2735// the point where refreshing becomes possible cannot precede the key lifetime. 2736 $effectiveLowTTL = min( $lowTTL, $logicalTTL ?: INF );
2738// How long the value was in the "low TTL" phase 2739 $timeOld = $effectiveLowTTL - $curTTL;
2740if ( $timeOld <= 0 || $timeOld >= $effectiveLowTTL ) {
2744// Ratio of the low TTL phase that has elapsed (r) 2745 $ttrRatio = $timeOld / $effectiveLowTTL;
2746// Use p(r) as the monotonically increasing "chance of refresh" function, 2747// having p(0)=0 and p(1)=1. The value expires at the nominal expiry. 2748 $chance = $ttrRatio ** 4;
2750return ( mt_rand( 1, 1_000_000_000 ) <= 1_000_000_000 * $chance );
2761protectedfunctionisValid( $value, $asOf, $minAsOf ) {
2762return ( $value !==
false && $asOf >= $minAsOf );
2772privatefunction wrap( $value, $ttl, $version, $now ) {
2773// Returns keys in ascending integer order for PHP7 array packing: 2774// https://nikic.github.io/2014/12/22/PHPs-new-hashtable-implementation.html 2776 self::FLD_FORMAT_VERSION => self::VERSION,
2777 self::FLD_VALUE => $value,
2778 self::FLD_TTL => $ttl,
2779 self::FLD_TIME => $now
2781if ( $version !==
null ) {
2782 $wrapped[self::FLD_VALUE_VERSION] = $version;
2802privatefunction unwrap( $wrapped, $now ) {
2803// https://nikic.github.io/2014/12/22/PHPs-new-hashtable-implementation.html 2805// Attributes that only depend on the fetched key value 2806 self::RES_VALUE =>
false,
2807 self::RES_VERSION =>
null,
2808 self::RES_AS_OF =>
null,
2809 self::RES_TTL =>
null,
2810 self::RES_TOMB_AS_OF =>
null,
2811// Attributes that depend on caller-specific "check" keys or "touched callbacks" 2812 self::RES_CHECK_AS_OF =>
null,
2813 self::RES_TOUCH_AS_OF =>
null,
2814 self::RES_CUR_TTL => null
2817if ( is_array( $wrapped ) ) {
2818// Entry expected to be a cached value; validate it 2820 ( $wrapped[self::FLD_FORMAT_VERSION] ??
null ) === self::VERSION &&
2821 $wrapped[self::FLD_TIME] >= $this->epoch
2823if ( $wrapped[self::FLD_TTL] > 0 ) {
2824// Get the approximate time left on the key 2825 $age = $now - $wrapped[self::FLD_TIME];
2826 $curTTL = max( $wrapped[self::FLD_TTL] - $age, 0.0 );
2828// Key had no TTL, so the time left is unbounded 2831 $res[self::RES_VALUE] = $wrapped[self::FLD_VALUE];
2832 $res[self::RES_VERSION] = $wrapped[self::FLD_VALUE_VERSION] ??
null;
2833 $res[self::RES_AS_OF] = $wrapped[self::FLD_TIME];
2834 $res[self::RES_CUR_TTL] = $curTTL;
2835 $res[self::RES_TTL] = $wrapped[self::FLD_TTL];
2838// Entry expected to be a tombstone; parse it 2839 $purge = $this->parsePurgeValue( $wrapped );
2840if ( $purge !==
null ) {
2841// Tombstoned keys should always have a negative "current TTL" 2842 $curTTL = min( $purge[self::PURGE_TIME] - $now, self::TINY_NEGATIVE );
2843 $res[self::RES_CUR_TTL] = $curTTL;
2844 $res[self::RES_TOMB_AS_OF] = $purge[self::PURGE_TIME];
2856privatefunction determineKeyGroupForStats( $key ) {
2857 $parts = explode(
':', $key, 3 );
2858// Fallback in case the key was not made by makeKey. 2859// Replace dots because they are special in StatsD (T232907) 2860return strtr( $parts[1] ?? $parts[0],
'.',
'_' );
2871privatefunction parsePurgeValue( $value ) {
2872if ( !is_string( $value ) ) {
2876 $segments = explode(
':', $value, 3 );
2877 $prefix = $segments[0];
2878if ( $prefix !== self::PURGE_VAL_PREFIX ) {
2883 $timestamp = (float)$segments[1];
2884// makeTombstonePurgeValue() doesn't store hold-off TTLs 2885 $holdoff = isset( $segments[2] ) ? (int)$segments[2] : self::HOLDOFF_TTL;
2887if ( $timestamp < $this->epoch ) {
2888// Purge value is too old 2892return [ self::PURGE_TIME => $timestamp, self::PURGE_HOLDOFF => $holdoff ];
2901privatefunction makeCheckPurgeValue(
float $timestamp,
int $holdoff, ?array &$purge =
null ) {
2902 $normalizedTime = (int)$timestamp;
2903// Purge array that matches what parsePurgeValue() would have returned 2904 $purge = [ self::PURGE_TIME => (float)$normalizedTime, self::PURGE_HOLDOFF => $holdoff ];
2906return self::PURGE_VAL_PREFIX .
":$normalizedTime:$holdoff";
2913privatefunction getProcessCache( $group ) {
2914if ( !isset( $this->processCaches[$group] ) ) {
2915 [ , $size ] = explode(
':', $group );
2916 $this->processCaches[$group] =
new MapCacheLRU( (
int)$size );
2917if ( $this->wallClockOverride !==
null ) {
2918 $this->processCaches[$group]->setMockTime( $this->wallClockOverride );
2922return $this->processCaches[$group];
2930privatefunction getNonProcessCachedMultiKeys( ArrayIterator $keys, array $opts ) {
2931 $pcTTL = $opts[
'pcTTL'] ?? self::TTL_UNCACHEABLE;
2934if ( $pcTTL > 0 && $this->callbackDepth == 0 ) {
2935 $pCache = $this->getProcessCache( $opts[
'pcGroup'] ?? self::PC_PRIMARY );
2936foreach ( $keys as $key => $id ) {
2937if ( !$pCache->has( $key, $pcTTL ) ) {
2938 $keysMissing[$id] = $key;
2952privatefunction fetchWrappedValuesForWarmupCache( array $keys, array $checkKeys ) {
2957// Get all the value keys to fetch... 2959foreach ( $keys as $baseKey ) {
2960 $sisterKeys[] = $this->makeSisterKey( $baseKey, self::TYPE_VALUE );
2962// Get all the "check" keys to fetch... 2963foreach ( $checkKeys as $i => $checkKeyOrKeyGroup ) {
2964// Note: avoid array_merge() inside loop in case there are many keys 2965if ( is_int( $i ) ) {
2966// Single "check" key that applies to all value keys 2967 $sisterKeys[] = $this->makeSisterKey( $checkKeyOrKeyGroup, self::TYPE_TIMESTAMP );
2969// List of "check" keys that apply to a specific value key 2970foreach ( (array)$checkKeyOrKeyGroup as $checkKey ) {
2971 $sisterKeys[] = $this->makeSisterKey( $checkKey, self::TYPE_TIMESTAMP );
2976 $wrappedBySisterKey = $this->cache->getMulti( $sisterKeys );
2977 $wrappedBySisterKey += array_fill_keys( $sisterKeys,
false );
2979return $wrappedBySisterKey;
2987privatefunction timeSinceLoggedMiss( $key, $now ) {
2988// phpcs:ignore Generic.CodeAnalysis.AssignmentInCondition.Found 2989for ( end( $this->missLog ); $miss = current( $this->missLog ); prev( $this->missLog ) ) {
2990if ( $miss[0] === $key ) {
2991return ( $now - $miss[1] );
3003return $this->wallClockOverride ?: microtime(
true );
3011 $this->wallClockOverride =& $time;
3012 $this->cache->setMockTime( $time );
3013foreach ( $this->processCaches as $pCache ) {
3014 $pCache->setMockTime( $time );
3029privatefunction startOperationSpan( $opName, $keys, $checkKeys = [] ) {
3030 $span = $this->tracer->createSpan(
"WANObjectCache::$opName" )
3034if ( !$span->getContext()->isSampled() ) {
3038 $keys = is_array( $keys ) ? implode(
' ', $keys ) : $keys;
3040if ( count( $checkKeys ) > 0 ) {
3041 $checkKeys = array_map(
3042static fn ( $checkKeyOrKeyGroup ) =>
3043 is_array( $checkKeyOrKeyGroup )
3044 ? implode(
' ', $checkKeyOrKeyGroup )
3045 : $checkKeyOrKeyGroup,
3048 $checkKeys = implode(
' ', $checkKeys );
3049 $span->setAttributes( [
'org.wikimedia.wancache.check_keys' => $checkKeys ] );
3052 $span->setAttributes( [
'org.wikimedia.wancache.keys' => $keys ] );