202 private $callbackDepth = 0;
204 private $warmupCache = [];
206 private $warmupKeyMisses = 0;
209 private $wallClockOverride;
212 private const MAX_COMMIT_DELAY = 3;
214 private const MAX_READ_LAG = 7;
216 public const HOLDOFF_TTL = self::MAX_COMMIT_DELAY + self::MAX_READ_LAG + 1;
219 private const LOW_TTL = 60;
224 private const HOT_TTR = 900;
226 private const AGE_NEW = 60;
229 private const TSE_NONE = -1;
239 public const MIN_TIMESTAMP_NONE = 0.0;
242 private const PC_PRIMARY =
'primary:1000';
248 private const SCHEME_HASH_TAG = 1;
250 private const SCHEME_HASH_STOP = 2;
253 private const CHECK_KEY_TTL = self::TTL_YEAR;
255 private const INTERIM_KEY_TTL = 2;
258 private const LOCK_TTL = 10;
260 private const RAMPUP_TTL = 30;
263 private const TINY_NEGATIVE = -0.000001;
265 private const TINY_POSITIVE = 0.000001;
268 private const RECENT_SET_LOW_MS = 50;
270 private const RECENT_SET_HIGH_MS = 100;
273 private const GENERATION_HIGH_SEC = 0.2;
276 private const PURGE_TIME = 0;
278 private const PURGE_HOLDOFF = 1;
281 private const VERSION = 1;
297 private const RES_VALUE = 0;
299 private const RES_VERSION = 1;
301 private const RES_AS_OF = 2;
303 private const RES_TTL = 3;
305 private const RES_TOMB_AS_OF = 4;
307 private const RES_CHECK_AS_OF = 5;
309 private const RES_TOUCH_AS_OF = 6;
311 private const RES_CUR_TTL = 7;
314 private const FLD_FORMAT_VERSION = 0;
316 private const FLD_VALUE = 1;
318 private const FLD_TTL = 2;
320 private const FLD_TIME = 3;
322 private const FLD_FLAGS = 4;
324 private const FLD_VALUE_VERSION = 5;
325 private const FLD_GENERATION_TIME = 6;
328 private const TYPE_VALUE =
'v';
330 private const TYPE_TIMESTAMP =
't';
332 private const TYPE_MUTEX =
'm';
334 private const TYPE_INTERIM =
'i';
337 private const PURGE_VAL_PREFIX =
'PURGED';
367 $this->cache =
$params[
'cache'];
368 $this->broadcastRoute =
$params[
'broadcastRoutingPrefix'] ??
null;
369 $this->epoch =
$params[
'epoch'] ?? 0;
370 $this->secret =
$params[
'secret'] ?? (string)$this->epoch;
371 if ( (
$params[
'coalesceScheme'] ??
'' ) ===
'hash_tag' ) {
375 $this->coalesceScheme = self::SCHEME_HASH_TAG;
378 $this->coalesceScheme = self::SCHEME_HASH_STOP;
381 $this->
setLogger( $params[
'logger'] ??
new NullLogger() );
386 'Use of StatsdDataFactory is deprecated in 1.43. Use StatsFactory instead.'
390 $this->stats =
$params[
'stats'] ?? StatsFactory::newNull();
392 $this->asyncHandler =
$params[
'asyncHandler'] ??
null;
393 $this->missLog = array_fill( 0, 10, [
'', 0.0 ] );
467 final public function get( $key, &$curTTL =
null, array $checkKeys = [], &$info = [] ) {
473 $res = $this->
fetchKeys( [ $key ], $checkKeys, $now )[$key];
475 $curTTL = $res[self::RES_CUR_TTL];
477 ? $res[self::RES_AS_OF]
479 self::KEY_VERSION => $res[self::RES_VERSION],
480 self::KEY_AS_OF => $res[self::RES_AS_OF],
481 self::KEY_TTL => $res[self::RES_TTL],
482 self::KEY_CUR_TTL => $res[self::RES_CUR_TTL],
483 self::KEY_TOMB_AS_OF => $res[self::RES_TOMB_AS_OF],
484 self::KEY_CHECK_AS_OF => $res[self::RES_CHECK_AS_OF]
487 if ( $curTTL ===
null || $curTTL <= 0 ) {
489 unset( $this->missLog[array_key_first( $this->missLog )] );
493 return $res[self::RES_VALUE];
523 array $checkKeys = [],
535 $resByKey = $this->
fetchKeys( $keys, $checkKeys, $now );
536 foreach ( $resByKey as $key => $res ) {
537 if ( $res[self::RES_VALUE] !==
false ) {
538 $valuesByKey[$key] = $res[self::RES_VALUE];
541 if ( $res[self::RES_CUR_TTL] !==
null ) {
542 $curTTLs[$key] = $res[self::RES_CUR_TTL];
544 $info[$key] = $legacyInfo
545 ? $res[self::RES_AS_OF]
547 self::KEY_VERSION => $res[self::RES_VERSION],
548 self::KEY_AS_OF => $res[self::RES_AS_OF],
549 self::KEY_TTL => $res[self::RES_TTL],
550 self::KEY_CUR_TTL => $res[self::RES_CUR_TTL],
551 self::KEY_TOMB_AS_OF => $res[self::RES_TOMB_AS_OF],
552 self::KEY_CHECK_AS_OF => $res[self::RES_CHECK_AS_OF]
574 protected function fetchKeys( array $keys, array $checkKeys,
float $now, $touchedCb =
null ) {
580 $valueSisterKeys = [];
582 $checkSisterKeysForAll = [];
584 $checkSisterKeysByKey = [];
586 foreach ( $keys as $key ) {
587 $sisterKey = $this->makeSisterKey( $key, self::TYPE_VALUE );
588 $allSisterKeys[] = $sisterKey;
589 $valueSisterKeys[] = $sisterKey;
592 foreach ( $checkKeys as $i => $checkKeyOrKeyGroup ) {
594 if ( is_int( $i ) ) {
596 $sisterKey = $this->makeSisterKey( $checkKeyOrKeyGroup, self::TYPE_TIMESTAMP );
597 $allSisterKeys[] = $sisterKey;
598 $checkSisterKeysForAll[] = $sisterKey;
601 foreach ( (array)$checkKeyOrKeyGroup as $checkKey ) {
602 $sisterKey = $this->makeSisterKey( $checkKey, self::TYPE_TIMESTAMP );
603 $allSisterKeys[] = $sisterKey;
604 $checkSisterKeysByKey[$i][] = $sisterKey;
609 if ( $this->warmupCache ) {
611 $wrappedBySisterKey = $this->warmupCache;
612 $sisterKeysMissing = array_diff( $allSisterKeys, array_keys( $wrappedBySisterKey ) );
613 if ( $sisterKeysMissing ) {
614 $this->warmupKeyMisses += count( $sisterKeysMissing );
615 $wrappedBySisterKey += $this->cache->getMulti( $sisterKeysMissing );
619 $wrappedBySisterKey = $this->cache->getMulti( $allSisterKeys );
623 $ckPurgesForAll = $this->processCheckKeys(
624 $checkSisterKeysForAll,
630 foreach ( $checkSisterKeysByKey as $keyWithCheckKeys => $checkKeysForKey ) {
631 $ckPurgesByKey[$keyWithCheckKeys] = $this->processCheckKeys(
640 foreach ( $valueSisterKeys as $valueSisterKey ) {
642 $key = current( $keys );
645 if ( array_key_exists( $valueSisterKey, $wrappedBySisterKey ) ) {
647 $wrapped = $wrappedBySisterKey[$valueSisterKey];
653 $res = $this->unwrap( $wrapped, $now );
654 $value = $res[self::RES_VALUE];
656 foreach ( array_merge( $ckPurgesForAll, $ckPurgesByKey[$key] ?? [] ) as $ckPurge ) {
657 $res[self::RES_CHECK_AS_OF] = max(
658 $ckPurge[self::PURGE_TIME],
659 $res[self::RES_CHECK_AS_OF]
662 $holdoffDeadline = $ckPurge[self::PURGE_TIME] + $ckPurge[self::PURGE_HOLDOFF];
664 if ( $value !==
false && $holdoffDeadline >= $res[self::RES_AS_OF] ) {
666 $ago = min( $ckPurge[self::PURGE_TIME] - $now, self::TINY_NEGATIVE );
668 $res[self::RES_CUR_TTL] = min( $res[self::RES_CUR_TTL], $ago );
672 if ( $touchedCb !==
null && $value !==
false ) {
673 $touched = $touchedCb( $value );
674 if ( $touched !==
null && $touched >= $res[self::RES_AS_OF] ) {
675 $res[self::RES_CUR_TTL] = min(
676 $res[self::RES_CUR_TTL],
677 $res[self::RES_AS_OF] - $touched,
685 $res[self::RES_TOUCH_AS_OF] = max( $res[self::RES_TOUCH_AS_OF], $touched );
687 $resByKey[$key] = $res;
699 private function processCheckKeys(
700 array $checkSisterKeys,
701 array $wrappedBySisterKey,
706 foreach ( $checkSisterKeys as $timeKey ) {
707 $purge = isset( $wrappedBySisterKey[$timeKey] )
708 ? $this->parsePurgeValue( $wrappedBySisterKey[$timeKey] )
711 if ( $purge ===
null ) {
713 $wrapped = $this->makeCheckPurgeValue( $now, self::HOLDOFF_TTL_NONE, $purge );
718 $this->cache::WRITE_BACKGROUND
811 final public function set( $key, $value, $ttl = self::TTL_INDEFINITE, array $opts = [] ) {
812 $keygroup = $this->determineKeyGroupForStats( $key );
814 $ok = $this->setMainValue(
818 $opts[
'version'] ??
null,
819 $opts[
'walltime'] ??
null,
821 $opts[
'since'] ??
null,
822 $opts[
'pending'] ??
false,
823 $opts[
'lockTSE'] ?? self::TSE_NONE,
824 $opts[
'staleTTL'] ?? self::STALE_TTL_NONE,
825 $opts[
'segmentable'] ??
false,
826 $opts[
'creating'] ??
false
829 $this->stats->getCounter(
'wanobjectcache_set_total' )
830 ->setLabel(
'keygroup', $keygroup )
831 ->setLabel(
'result', ( $ok ?
'ok' :
'error' ) )
832 ->copyToStatsdAt(
"wanobjectcache.$keygroup.set." . ( $ok ?
'ok' :
'error' ) )
853 private function setMainValue(
861 bool $dataPendingCommit,
874 $walltime ??= $this->timeSinceLoggedMiss( $key, $now );
875 $dataSnapshotLag = ( $dataReadSince !== null ) ? max( 0, $now - $dataReadSince ) : 0;
876 $dataCombinedLag = $dataReplicaLag + $dataSnapshotLag;
883 if ( $dataPendingCommit ) {
885 $mitigated =
'pending writes';
887 $mitigationTTL = self::TTL_UNCACHEABLE;
888 } elseif ( $dataSnapshotLag > self::MAX_READ_LAG ) {
890 $pregenSnapshotLag = ( $walltime !== null ) ? ( $dataSnapshotLag - $walltime ) : 0;
891 if ( ( $pregenSnapshotLag + self::GENERATION_HIGH_SEC ) > self::MAX_READ_LAG ) {
893 $mitigated =
'snapshot lag (late generation)';
895 $mitigationTTL = self::TTL_UNCACHEABLE;
898 $mitigated =
'snapshot lag (high generation time)';
902 } elseif ( $dataReplicaLag ===
false || $dataReplicaLag > self::MAX_READ_LAG ) {
904 $mitigated =
'replication lag';
907 } elseif ( $dataCombinedLag > self::MAX_READ_LAG ) {
908 $pregenCombinedLag = ( $walltime !== null ) ? ( $dataCombinedLag - $walltime ) : 0;
910 if ( ( $pregenCombinedLag + self::GENERATION_HIGH_SEC ) > self::MAX_READ_LAG ) {
912 $mitigated =
'read lag (late generation)';
914 $mitigationTTL = self::TTL_UNCACHEABLE;
917 $mitigated =
'read lag (high generation time)';
925 $mitigationTTL =
null;
928 if ( $mitigationTTL === self::TTL_UNCACHEABLE ) {
929 $this->logger->warning(
930 "Rejected set() for {cachekey} due to $mitigated.",
933 'lag' => $dataReplicaLag,
934 'age' => $dataSnapshotLag,
935 'walltime' => $walltime
946 if ( $mitigationTTL !==
null ) {
948 if ( $lockTSE >= 0 ) {
950 $logicalTTL = min( $ttl ?: INF, $mitigationTTL );
953 $ttl = min( $ttl ?: INF, $mitigationTTL );
956 $this->logger->warning(
957 "Lowered set() TTL for {cachekey} due to $mitigated.",
960 'lag' => $dataReplicaLag,
961 'age' => $dataSnapshotLag,
962 'walltime' => $walltime
968 $wrapped = $this->wrap( $value, $logicalTTL ?: $ttl, $version, $now );
969 $storeTTL = $ttl + $staleTTL;
971 $flags = $this->cache::WRITE_BACKGROUND;
972 if ( $segmentable ) {
973 $flags |= $this->cache::WRITE_ALLOW_SEGMENTS;
977 $ok = $this->cache->add(
978 $this->makeSisterKey( $key, self::TYPE_VALUE ),
984 $ok = $this->cache->merge(
985 $this->makeSisterKey( $key, self::TYPE_VALUE ),
986 static function (
$cache, $key, $cWrapped ) use ( $wrapped ) {
988 return ( is_string( $cWrapped ) ) ?
false : $wrapped;
991 $this->cache::MAX_CONFLICTS_ONE,
1065 $valueSisterKey = $this->makeSisterKey( $key, self::TYPE_VALUE );
1081 $purge = $this->makeTombstonePurgeValue( $now );
1085 $keygroup = $this->determineKeyGroupForStats( $key );
1087 $this->stats->getCounter(
'wanobjectcache_delete_total' )
1088 ->setLabel(
'keygroup', $keygroup )
1089 ->setLabel(
'result', ( $ok ?
'ok' :
'error' ) )
1090 ->copyToStatsdAt(
"wanobjectcache.$keygroup.delete." . ( $ok ?
'ok' :
'error' ) )
1181 $checkSisterKeysByKey = [];
1182 foreach ( $keys as $key ) {
1183 $checkSisterKeysByKey[$key] = $this->makeSisterKey( $key, self::TYPE_TIMESTAMP );
1186 $wrappedBySisterKey = $this->cache->getMulti( $checkSisterKeysByKey );
1187 $wrappedBySisterKey += array_fill_keys( $checkSisterKeysByKey,
false );
1191 foreach ( $checkSisterKeysByKey as $key => $checkSisterKey ) {
1192 $purge = $this->parsePurgeValue( $wrappedBySisterKey[$checkSisterKey] );
1193 if ( $purge ===
null ) {
1194 $wrapped = $this->makeCheckPurgeValue( $now, self::HOLDOFF_TTL_NONE, $purge );
1198 self::CHECK_KEY_TTL,
1199 $this->cache::WRITE_BACKGROUND
1203 $times[$key] = $purge[self::PURGE_TIME];
1243 $checkSisterKey = $this->makeSisterKey( $key, self::TYPE_TIMESTAMP );
1246 $purge = $this->makeCheckPurgeValue( $now, $holdoff );
1249 $keygroup = $this->determineKeyGroupForStats( $key );
1251 $this->stats->getCounter(
'wanobjectcache_check_total' )
1252 ->setLabel(
'keygroup', $keygroup )
1253 ->setLabel(
'result', ( $ok ?
'ok' :
'error' ) )
1254 ->copyToStatsdAt(
"wanobjectcache.$keygroup.ck_touch." . ( $ok ?
'ok' :
'error' ) )
1288 $checkSisterKey = $this->makeSisterKey( $key, self::TYPE_TIMESTAMP );
1291 $keygroup = $this->determineKeyGroupForStats( $key );
1293 $this->stats->getCounter(
'wanobjectcache_reset_total' )
1294 ->setLabel(
'keygroup', $keygroup )
1295 ->setLabel(
'result', ( $ok ?
'ok' :
'error' ) )
1296 ->copyToStatsdAt(
"wanobjectcache.$keygroup.ck_reset." . ( $ok ?
'ok' :
'error' ) )
1604 $key, $ttl, $callback, array $opts = [], array $cbParams = []
1606 $version = $opts[
'version'] ??
null;
1607 $pcTTL = $opts[
'pcTTL'] ?? self::TTL_UNCACHEABLE;
1608 $pCache = ( $pcTTL >= 0 )
1609 ? $this->getProcessCache( $opts[
'pcGroup'] ?? self::PC_PRIMARY )
1615 if ( $pCache && $this->callbackDepth == 0 ) {
1616 $cached = $pCache->get( $key, $pcTTL,
false );
1617 if ( $cached !==
false ) {
1618 $this->logger->debug(
"getWithSetCallback($key): process cache hit" );
1623 [ $value, $valueVersion, $curAsOf ] = $this->fetchOrRegenerate( $key, $ttl, $callback, $opts, $cbParams );
1624 if ( $valueVersion !== $version ) {
1628 $this->logger->debug(
"getWithSetCallback($key): using variant key" );
1629 [ $value ] = $this->fetchOrRegenerate(
1630 $this->
makeGlobalKey(
'WANCache-key-variant', md5( $key ), (
string)$version ),
1633 [
'version' =>
null,
'minAsOf' => $curAsOf ] + $opts,
1639 if ( $pCache && $value !==
false ) {
1640 $pCache->set( $key, $value );
1662 private function fetchOrRegenerate( $key, $ttl, $callback, array $opts, array $cbParams ) {
1663 $checkKeys = $opts[
'checkKeys'] ?? [];
1665 $minAsOf = $opts[
'minAsOf'] ?? self::MIN_TIMESTAMP_NONE;
1666 $hotTTR = $opts[
'hotTTR'] ?? self::HOT_TTR;
1667 $lowTTL = $opts[
'lowTTL'] ?? min( self::LOW_TTL, $ttl );
1668 $ageNew = $opts[
'ageNew'] ?? self::AGE_NEW;
1669 $touchedCb = $opts[
'touchedCallback'] ??
null;
1672 $keygroup = $this->determineKeyGroupForStats( $key );
1675 $curState = $this->
fetchKeys( [ $key ], $checkKeys, $startTime, $touchedCb )[$key];
1676 $curValue = $curState[self::RES_VALUE];
1679 if ( $this->isAcceptablyFreshValue( $curState, $graceTTL, $minAsOf ) ) {
1681 $this->stats->getTiming(
'wanobjectcache_getwithset_seconds' )
1682 ->setLabel(
'keygroup', $keygroup )
1683 ->setLabel(
'result',
'hit' )
1684 ->setLabel(
'reason',
'good' )
1685 ->copyToStatsdAt(
"wanobjectcache.$keygroup.hit.good" )
1688 return [ $curValue, $curState[self::RES_VERSION], $curState[self::RES_AS_OF] ];
1689 } elseif ( $this->scheduleAsyncRefresh( $key, $ttl, $callback, $opts, $cbParams ) ) {
1690 $this->logger->debug(
"fetchOrRegenerate($key): hit with async refresh" );
1692 $this->stats->getTiming(
'wanobjectcache_getwithset_seconds' )
1693 ->setLabel(
'keygroup', $keygroup )
1694 ->setLabel(
'result',
'hit' )
1695 ->setLabel(
'reason',
'refresh' )
1696 ->copyToStatsdAt(
"wanobjectcache.$keygroup.hit.refresh" )
1699 return [ $curValue, $curState[self::RES_VERSION], $curState[self::RES_AS_OF] ];
1701 $this->logger->debug(
"fetchOrRegenerate($key): hit with sync refresh" );
1705 $isKeyTombstoned = ( $curState[self::RES_TOMB_AS_OF] !== null );
1707 if ( $isKeyTombstoned ) {
1708 $volState = $this->getInterimValue( $key, $minAsOf, $startTime, $touchedCb );
1709 $volValue = $volState[self::RES_VALUE];
1711 $volState = $curState;
1712 $volValue = $curValue;
1720 $lastPurgeTime = max(
1722 $volState[self::RES_TOUCH_AS_OF],
1723 $curState[self::RES_TOMB_AS_OF],
1724 $curState[self::RES_CHECK_AS_OF]
1726 $safeMinAsOf = max( $minAsOf, $lastPurgeTime + self::TINY_POSITIVE );
1727 if ( $this->isExtremelyNewValue( $volState, $safeMinAsOf, $startTime ) ) {
1728 $this->logger->debug(
"fetchOrRegenerate($key): volatile hit" );
1730 $this->stats->getTiming(
'wanobjectcache_getwithset_seconds' )
1731 ->setLabel(
'keygroup', $keygroup )
1732 ->setLabel(
'result',
'hit' )
1733 ->setLabel(
'reason',
'volatile' )
1734 ->copyToStatsdAt(
"wanobjectcache.$keygroup.hit.volatile" )
1737 return [ $volValue, $volState[self::RES_VERSION], $curState[self::RES_AS_OF] ];
1740 $lockTSE = $opts[
'lockTSE'] ?? self::TSE_NONE;
1741 $busyValue = $opts[
'busyValue'] ??
null;
1743 $segmentable = $opts[
'segmentable'] ??
false;
1744 $version = $opts[
'version'] ??
null;
1747 $useRegenerationLock =
1758 $curState[self::RES_CUR_TTL] !==
null &&
1759 $curState[self::RES_CUR_TTL] <= 0 &&
1760 abs( $curState[self::RES_CUR_TTL] ) <= $lockTSE
1764 ( $busyValue !==
null && $volValue ===
false );
1769 $hasLock = $useRegenerationLock && $this->claimStampedeLock( $key );
1770 if ( $useRegenerationLock && !$hasLock ) {
1773 if ( $this->
isValid( $volValue, $volState[self::RES_AS_OF], $minAsOf ) ) {
1774 $this->logger->debug(
"fetchOrRegenerate($key): returning stale value" );
1776 $this->stats->getTiming(
'wanobjectcache_getwithset_seconds' )
1777 ->setLabel(
'keygroup', $keygroup )
1778 ->setLabel(
'result',
'hit' )
1779 ->setLabel(
'reason',
'stale' )
1780 ->copyToStatsdAt(
"wanobjectcache.$keygroup.hit.stale" )
1783 return [ $volValue, $volState[self::RES_VERSION], $curState[self::RES_AS_OF] ];
1784 } elseif ( $busyValue !==
null ) {
1785 $miss = is_infinite( $minAsOf ) ?
'renew' :
'miss';
1786 $this->logger->debug(
"fetchOrRegenerate($key): busy $miss" );
1788 $this->stats->getTiming(
'wanobjectcache_getwithset_seconds' )
1789 ->setLabel(
'keygroup', $keygroup )
1790 ->setLabel(
'result', $miss )
1791 ->setLabel(
'reason',
'busy' )
1792 ->copyToStatsdAt(
"wanobjectcache.$keygroup.$miss.busy" )
1795 $placeholderValue = $this->resolveBusyValue( $busyValue );
1797 return [ $placeholderValue, $version, $curState[self::RES_AS_OF] ];
1804 ++$this->callbackDepth;
1809 ( $curState[self::RES_VERSION] === $version ) ? $curValue : false,
1812 ( $curState[self::RES_VERSION] === $version ) ? $curState[self::RES_AS_OF] : null,
1816 --$this->callbackDepth;
1821 $walltime = max( $postCallbackTime - $preCallbackTime, 0.0 );
1823 $this->stats->getTiming(
'wanobjectcache_regen_seconds' )
1824 ->setLabel(
'keygroup', $keygroup )
1825 ->copyToStatsdAt(
"wanobjectcache.$keygroup.regen_walltime" )
1826 ->observe( 1e3 * $walltime );
1831 ( $value !==
false && $ttl >= 0 ) &&
1833 ( !$useRegenerationLock || $hasLock || $isKeyTombstoned )
1836 if ( $isKeyTombstoned ) {
1837 $this->setInterimValue(
1845 $this->setMainValue(
1852 $setOpts[
'lag'] ?? 0,
1854 $setOpts[
'since'] ?? $preCallbackTime,
1856 $setOpts[
'pending'] ??
false,
1860 ( $curValue ===
false )
1865 $this->yieldStampedeLock( $key, $hasLock );
1867 $miss = is_infinite( $minAsOf ) ?
'renew' :
'miss';
1868 $this->logger->debug(
"fetchOrRegenerate($key): $miss, new value computed" );
1870 $this->stats->getTiming(
'wanobjectcache_getwithset_seconds' )
1871 ->setLabel(
'keygroup', $keygroup )
1872 ->setLabel(
'result', $miss )
1873 ->setLabel(
'reason',
'compute' )
1874 ->copyToStatsdAt(
"wanobjectcache.$keygroup.$miss.compute" )
1877 return [ $value, $version, $curState[self::RES_AS_OF] ];
1884 private function claimStampedeLock( $key ) {
1885 $checkSisterKey = $this->makeSisterKey( $key, self::TYPE_MUTEX );
1887 return $this->cache->add( $checkSisterKey, 1, self::LOCK_TTL );
1894 private function yieldStampedeLock( $key, $hasLock ) {
1896 $checkSisterKey = $this->makeSisterKey( $key, self::TYPE_MUTEX );
1897 $this->cache->delete( $checkSisterKey, $this->cache::WRITE_BACKGROUND );
1911 private function makeSisterKeys( array $baseKeys,
string $type,
string $route =
null ) {
1913 foreach ( $baseKeys as $baseKey ) {
1914 $sisterKeys[] = $this->makeSisterKey( $baseKey, $type, $route );
1930 private function makeSisterKey(
string $baseKey,
string $typeChar,
string $route =
null ) {
1931 if ( $this->coalesceScheme === self::SCHEME_HASH_STOP ) {
1933 $sisterKey =
'WANCache:' . $baseKey .
'|#|' . $typeChar;
1936 $sisterKey =
'WANCache:{' . $baseKey .
'}:' . $typeChar;
1939 if ( $route !==
null ) {
1940 $sisterKey = $this->
prependRoute( $sisterKey, $route );
1958 private function isExtremelyNewValue( $res, $minAsOf, $now ) {
1959 if ( $res[self::RES_VALUE] ===
false || $res[self::RES_AS_OF] < $minAsOf ) {
1963 $age = $now - $res[self::RES_AS_OF];
1965 return ( $age < mt_rand( self::RECENT_SET_LOW_MS, self::RECENT_SET_HIGH_MS ) / 1e3 );
1977 private function getInterimValue( $key, $minAsOf, $now, $touchedCb ) {
1979 $interimSisterKey = $this->makeSisterKey( $key, self::TYPE_INTERIM );
1980 $wrapped = $this->cache->get( $interimSisterKey );
1981 $res = $this->unwrap( $wrapped, $now );
1982 if ( $res[self::RES_VALUE] !==
false && $res[self::RES_AS_OF] >= $minAsOf ) {
1983 if ( $touchedCb !==
null ) {
1986 $res[self::RES_TOUCH_AS_OF] = max(
1987 $touchedCb( $res[self::RES_VALUE] ),
1988 $res[self::RES_TOUCH_AS_OF]
1996 return $this->unwrap(
false, $now );
2007 private function setInterimValue(
2015 $ttl = max( self::INTERIM_KEY_TTL, (
int)$ttl );
2018 $wrapped = $this->wrap( $value, $ttl, $version, $now );
2020 $flags = $this->cache::WRITE_BACKGROUND;
2021 if ( $segmentable ) {
2022 $flags |= $this->cache::WRITE_ALLOW_SEGMENTS;
2025 return $this->cache->set(
2026 $this->makeSisterKey( $key, self::TYPE_INTERIM ),
2037 private function resolveBusyValue( $busyValue ) {
2038 return ( $busyValue instanceof Closure ) ? $busyValue() : $busyValue;
2107 ArrayIterator $keyedIds, $ttl, callable $callback, array $opts = []
2110 $this->warmupCache = $this->fetchWrappedValuesForWarmupCache(
2111 $this->getNonProcessCachedMultiKeys( $keyedIds, $opts ),
2112 $opts[
'checkKeys'] ?? []
2114 $this->warmupKeyMisses = 0;
2120 $proxyCb =
static function ( $oldValue, &$ttl, &$setOpts, $oldAsOf,
$params )
2123 return $callback(
$params[
'id'], $oldValue, $ttl, $setOpts, $oldAsOf );
2128 foreach ( $keyedIds as $key => $id ) {
2138 $this->warmupCache = [];
2210 ArrayIterator $keyedIds, $ttl, callable $callback, array $opts = []
2212 $checkKeys = $opts[
'checkKeys'] ?? [];
2213 $minAsOf = $opts[
'minAsOf'] ?? self::MIN_TIMESTAMP_NONE;
2216 unset( $opts[
'lockTSE'] );
2217 unset( $opts[
'busyValue'] );
2220 $keysByIdGet = $this->getNonProcessCachedMultiKeys( $keyedIds, $opts );
2221 $this->warmupCache = $this->fetchWrappedValuesForWarmupCache( $keysByIdGet, $checkKeys );
2222 $this->warmupKeyMisses = 0;
2229 $resByKey = $this->
fetchKeys( $keysByIdGet, $checkKeys, $now );
2230 foreach ( $keysByIdGet as $id => $key ) {
2231 $res = $resByKey[$key];
2233 $res[self::RES_VALUE] ===
false ||
2234 $res[self::RES_CUR_TTL] < 0 ||
2235 $res[self::RES_AS_OF] < $minAsOf
2243 $newTTLsById = array_fill_keys( $idsRegen, $ttl );
2244 $newValsById = $idsRegen ? $callback( $idsRegen, $newTTLsById, $newSetOpts ) : [];
2246 $method = __METHOD__;
2251 $proxyCb =
function ( $oldValue, &$ttl, &$setOpts, $oldAsOf,
$params )
2252 use ( $callback, $newValsById, $newTTLsById, $newSetOpts, $method )
2256 if ( array_key_exists( $id, $newValsById ) ) {
2258 $newValue = $newValsById[$id];
2259 $ttl = $newTTLsById[$id];
2260 $setOpts = $newSetOpts;
2264 $ttls = [ $id => $ttl ];
2265 $result = $callback( [ $id ], $ttls, $setOpts );
2266 if ( !isset( $result[$id] ) ) {
2268 $this->logger->warning(
2269 $method .
' failed due to {id} not set in result {result}', [
2271 'result' => json_encode( $result )
2274 $newValue = $result[$id];
2283 foreach ( $keyedIds as $key => $id ) {
2293 $this->warmupCache = [];
2306 return $this->cache->makeGlobalKey( $keygroup, ...$components );
2316 public function makeKey( $keygroup, ...$components ) {
2317 return $this->cache->makeKey( $keygroup, ...$components );
2328 return hash_hmac(
'sha256', $component, $this->secret );
2384 foreach ( $ids as $id ) {
2386 if ( strlen( $id ) > 64 ) {
2387 $this->logger->warning( __METHOD__ .
": long ID '$id'; use hash256()" );
2389 $key = $keyCallback( $id, $this );
2391 if ( !isset( $idByKey[$key] ) ) {
2392 $idByKey[$key] = $id;
2393 } elseif ( (
string)$id !== (
string)$idByKey[$key] ) {
2394 throw new UnexpectedValueException(
2395 "Cache key collision; IDs ('$id','{$idByKey[$key]}') map to '$key'"
2400 return new ArrayIterator( $idByKey );
2439 if ( count( $ids ) !== count( $res ) ) {
2442 $ids = array_keys( array_fill_keys( $ids,
true ) );
2443 if ( count( $ids ) !== count( $res ) ) {
2444 throw new UnexpectedValueException(
"Multi-key result does not match ID list" );
2448 return array_combine( $ids, $res );
2458 return $this->cache->watchErrors();
2479 $code = $this->cache->getLastError( $watchPoint );
2498 $this->cache->clearLastError();
2507 $this->processCaches = [];
2540 return $this->cache->getQoS( $flag );
2606 public function adaptiveTTL( $mtime, $maxTTL, $minTTL = 30, $factor = 0.2 ) {
2608 $mtime = (int)$mtime;
2609 if ( $mtime <= 0 ) {
2616 return (
int)min( $maxTTL, max( $minTTL, $factor * $age ) );
2626 return $this->warmupKeyMisses;
2644 if ( $this->broadcastRoute !==
null ) {
2645 $routeKey = $this->
prependRoute( $sisterKey, $this->broadcastRoute );
2647 $routeKey = $sisterKey;
2650 return $this->cache->set(
2654 $this->cache::WRITE_BACKGROUND
2667 if ( $this->broadcastRoute !==
null ) {
2668 $routeKey = $this->
prependRoute( $sisterKey, $this->broadcastRoute );
2670 $routeKey = $sisterKey;
2673 return $this->cache->delete( $routeKey, $this->cache::WRITE_BACKGROUND );
2682 if ( $sisterKey[0] ===
'/' ) {
2683 throw new RuntimeException(
"Sister key '$sisterKey' already contains a route." );
2686 return $route . $sisterKey;
2700 private function scheduleAsyncRefresh( $key, $ttl, $callback, array $opts, array $cbParams ) {
2701 if ( !$this->asyncHandler ) {
2709 $func(
function () use ( $key, $ttl, $callback, $opts, $cbParams ) {
2710 $opts[
'minAsOf'] = INF;
2712 $this->fetchOrRegenerate( $key, $ttl, $callback, $opts, $cbParams );
2713 }
catch ( Exception $e ) {
2715 $this->logger->error(
'Async refresh failed for {key}', [
2735 private function isAcceptablyFreshValue( $res, $graceTTL, $minAsOf ) {
2736 if ( !$this->
isValid( $res[self::RES_VALUE], $res[self::RES_AS_OF], $minAsOf ) ) {
2741 $curTTL = $res[self::RES_CUR_TTL];
2742 if ( $curTTL > 0 ) {
2748 $curGraceTTL = $graceTTL + $curTTL;
2750 return ( $curGraceTTL > 0 )
2768 $curTTL = $res[self::RES_CUR_TTL];
2769 $logicalTTL = $res[self::RES_TTL];
2770 $asOf = $res[self::RES_AS_OF];
2794 if ( $ageNew < 0 || $timeTillRefresh <= 0 ) {
2798 $age = $now - $asOf;
2799 $timeOld = $age - $ageNew;
2800 if ( $timeOld <= 0 ) {
2804 $popularHitsPerSec = 1;
2808 $refreshWindowSec = max( $timeTillRefresh - $ageNew - self::RAMPUP_TTL / 2, 1 );
2812 $chance = 1 / ( $popularHitsPerSec * $refreshWindowSec );
2814 $chance *= ( $timeOld <= self::RAMPUP_TTL ) ? $timeOld / self::RAMPUP_TTL : 1;
2816 return ( mt_rand( 1, 1_000_000_000 ) <= 1_000_000_000 * $chance );
2838 if ( $lowTTL <= 0 ) {
2843 $effectiveLowTTL = min( $lowTTL, $logicalTTL ?: INF );
2846 $timeOld = $effectiveLowTTL - $curTTL;
2847 if ( $timeOld <= 0 || $timeOld >= $effectiveLowTTL ) {
2852 $ttrRatio = $timeOld / $effectiveLowTTL;
2855 $chance = $ttrRatio ** 4;
2857 return ( mt_rand( 1, 1_000_000_000 ) <= 1_000_000_000 * $chance );
2868 protected function isValid( $value, $asOf, $minAsOf ) {
2869 return ( $value !==
false && $asOf >= $minAsOf );
2879 private function wrap( $value, $ttl, $version, $now ) {
2883 self::FLD_FORMAT_VERSION => self::VERSION,
2884 self::FLD_VALUE => $value,
2885 self::FLD_TTL => $ttl,
2886 self::FLD_TIME => $now
2888 if ( $version !==
null ) {
2889 $wrapped[self::FLD_VALUE_VERSION] = $version;
2909 private function unwrap( $wrapped, $now ) {
2913 self::RES_VALUE =>
false,
2914 self::RES_VERSION =>
null,
2915 self::RES_AS_OF =>
null,
2916 self::RES_TTL =>
null,
2917 self::RES_TOMB_AS_OF =>
null,
2919 self::RES_CHECK_AS_OF =>
null,
2920 self::RES_TOUCH_AS_OF =>
null,
2921 self::RES_CUR_TTL => null
2924 if ( is_array( $wrapped ) ) {
2927 ( $wrapped[self::FLD_FORMAT_VERSION] ??
null ) === self::VERSION &&
2928 $wrapped[self::FLD_TIME] >= $this->epoch
2930 if ( $wrapped[self::FLD_TTL] > 0 ) {
2932 $age = $now - $wrapped[self::FLD_TIME];
2933 $curTTL = max( $wrapped[self::FLD_TTL] - $age, 0.0 );
2938 $res[self::RES_VALUE] = $wrapped[self::FLD_VALUE];
2939 $res[self::RES_VERSION] = $wrapped[self::FLD_VALUE_VERSION] ??
null;
2940 $res[self::RES_AS_OF] = $wrapped[self::FLD_TIME];
2941 $res[self::RES_CUR_TTL] = $curTTL;
2942 $res[self::RES_TTL] = $wrapped[self::FLD_TTL];
2946 $purge = $this->parsePurgeValue( $wrapped );
2947 if ( $purge !==
null ) {
2949 $curTTL = min( $purge[self::PURGE_TIME] - $now, self::TINY_NEGATIVE );
2950 $res[self::RES_CUR_TTL] = $curTTL;
2951 $res[self::RES_TOMB_AS_OF] = $purge[self::PURGE_TIME];
2963 private function determineKeyGroupForStats( $key ) {
2964 $parts = explode(
':', $key, 3 );
2967 return strtr( $parts[1] ?? $parts[0],
'.',
'_' );
2978 private function parsePurgeValue( $value ) {
2979 if ( !is_string( $value ) ) {
2983 $segments = explode(
':', $value, 3 );
2984 $prefix = $segments[0];
2985 if ( $prefix !== self::PURGE_VAL_PREFIX ) {
2990 $timestamp = (float)$segments[1];
2992 $holdoff = isset( $segments[2] ) ? (int)$segments[2] : self::
HOLDOFF_TTL;
2994 if ( $timestamp < $this->epoch ) {
2999 return [ self::PURGE_TIME => $timestamp, self::PURGE_HOLDOFF => $holdoff ];
3006 private function makeTombstonePurgeValue(
float $timestamp ) {
3007 return self::PURGE_VAL_PREFIX .
':' . (int)$timestamp;
3016 private function makeCheckPurgeValue(
float $timestamp,
int $holdoff, array &$purge =
null ) {
3017 $normalizedTime = (int)$timestamp;
3019 $purge = [ self::PURGE_TIME => (float)$normalizedTime, self::PURGE_HOLDOFF => $holdoff ];
3021 return self::PURGE_VAL_PREFIX .
":$normalizedTime:$holdoff";
3028 private function getProcessCache( $group ) {
3029 if ( !isset( $this->processCaches[$group] ) ) {
3030 [ , $size ] = explode(
':', $group );
3031 $this->processCaches[$group] =
new MapCacheLRU( (
int)$size );
3032 if ( $this->wallClockOverride !==
null ) {
3033 $this->processCaches[$group]->setMockTime( $this->wallClockOverride );
3037 return $this->processCaches[$group];
3045 private function getNonProcessCachedMultiKeys( ArrayIterator $keys, array $opts ) {
3046 $pcTTL = $opts[
'pcTTL'] ?? self::TTL_UNCACHEABLE;
3049 if ( $pcTTL > 0 && $this->callbackDepth == 0 ) {
3050 $pCache = $this->getProcessCache( $opts[
'pcGroup'] ?? self::PC_PRIMARY );
3051 foreach ( $keys as $key => $id ) {
3052 if ( !$pCache->has( $key, $pcTTL ) ) {
3053 $keysMissing[$id] = $key;
3058 return $keysMissing;
3067 private function fetchWrappedValuesForWarmupCache( array $keys, array $checkKeys ) {
3073 $sisterKeys = $this->makeSisterKeys( $keys, self::TYPE_VALUE );
3075 foreach ( $checkKeys as $i => $checkKeyOrKeyGroup ) {
3077 if ( is_int( $i ) ) {
3079 $sisterKeys[] = $this->makeSisterKey( $checkKeyOrKeyGroup, self::TYPE_TIMESTAMP );
3082 foreach ( (array)$checkKeyOrKeyGroup as $checkKey ) {
3083 $sisterKeys[] = $this->makeSisterKey( $checkKey, self::TYPE_TIMESTAMP );
3088 $wrappedBySisterKey = $this->cache->getMulti( $sisterKeys );
3089 $wrappedBySisterKey += array_fill_keys( $sisterKeys,
false );
3091 return $wrappedBySisterKey;
3099 private function timeSinceLoggedMiss( $key, $now ) {
3101 for ( end( $this->missLog ); $miss = current( $this->missLog ); prev( $this->missLog ) ) {
3102 if ( $miss[0] === $key ) {
3103 return ( $now - $miss[1] );
3115 return $this->wallClockOverride ?: microtime(
true );
3123 $this->wallClockOverride =& $time;
3124 $this->cache->setMockTime( $time );
3125 foreach ( $this->processCaches as $pCache ) {
3126 $pCache->setMockTime( $time );