X-Git-Url: http://git.heureux-cyclage.org/?a=blobdiff_plain;f=includes%2Flibs%2Fobjectcache%2FWANObjectCache.php;h=69edb11d811a90be8d18a25df66d7f75c58f7e9b;hb=7af9c25c885aaac732b990fb4b0172f36d77129d;hp=07e4fadece7d638a8d4c2558f80770c6b0edfe9f;hpb=ea3396b81ddc2a2e3b245ad8a71e7551d27fc603;p=lhc%2Fweb%2Fwiklou.git diff --git a/includes/libs/objectcache/WANObjectCache.php b/includes/libs/objectcache/WANObjectCache.php index 07e4fadece..69edb11d81 100644 --- a/includes/libs/objectcache/WANObjectCache.php +++ b/includes/libs/objectcache/WANObjectCache.php @@ -118,22 +118,25 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt protected $cache; /** @var MapCacheLRU[] Map of group PHP instance caches */ protected $processCaches = []; + /** @var LoggerInterface */ + protected $logger; + /** @var StatsdDataFactoryInterface */ + protected $stats; + /** @var callable|null Function that takes a WAN cache callback and runs it later */ + protected $asyncHandler; + /** @bar bool Whether to use mcrouter key prefixing for routing */ protected $mcrouterAware; /** @var string Physical region for mcrouter use */ protected $region; /** @var string Cache cluster name for mcrouter use */ protected $cluster; - /** @var LoggerInterface */ - protected $logger; - /** @var StatsdDataFactoryInterface */ - protected $stats; /** @var bool Whether to use "interim" caching while keys are tombstoned */ protected $useInterimHoldOffCaching = true; - /** @var callable|null Function that takes a WAN cache callback and runs it later */ - protected $asyncHandler; /** @var float Unix timestamp of the oldest possible valid values */ protected $epoch; + /** @var string Stable secret used for hasing long strings into key components */ + protected $secret; /** @var int Callback stack depth for getWithSetCallback() */ private $callbackDepth = 0; @@ -145,93 +148,104 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt /** @var float|null */ private $wallClockOverride; - /** Max time expected to pass between delete() and DB commit finishing */ + /** @var int Max expected seconds to pass between delete() and DB commit finishing */ const MAX_COMMIT_DELAY = 3; - /** Max replication+snapshot lag before applying TTL_LAGGED or disallowing set() */ + /** @var int Max expected seconds of combined lag from replication and view snapshots */ const MAX_READ_LAG = 7; - /** Seconds to tombstone keys on delete() */ - const HOLDOFF_TTL = 11; // MAX_COMMIT_DELAY + MAX_READ_LAG + 1 - - /** Seconds to keep dependency purge keys around */ - const CHECK_KEY_TTL = self::TTL_YEAR; - /** Seconds to keep interim value keys for tombstoned keys around */ - const INTERIM_KEY_TTL = 1; - - /** Seconds to keep lock keys around */ - const LOCK_TTL = 10; - /** Seconds to no-op key set() calls to avoid large blob I/O stampedes */ - const COOLOFF_TTL = 1; - /** Default remaining TTL at which to consider pre-emptive regeneration */ + /** @var int Seconds to tombstone keys on delete() and treat as volatile after invalidation */ + const HOLDOFF_TTL = self::MAX_COMMIT_DELAY + self::MAX_READ_LAG + 1; + + /** @var int Idiom for getWithSetCallback() meaning "do not store the callback result" */ + const TTL_UNCACHEABLE = -1; + + /** @var int Consider regeneration if the key will expire within this many seconds */ const LOW_TTL = 30; - /** Max TTL to store keys when a data sourced is lagged */ + /** @var int Max TTL, in seconds, to store keys when a data sourced is lagged */ const TTL_LAGGED = 30; - /** Never consider performing "popularity" refreshes until a key reaches this age */ - const AGE_NEW = 60; - /** The time length of the "popularity" refresh window for hot keys */ + /** @var int Expected time-till-refresh, in seconds, if the key is accessed once per second */ const HOT_TTR = 900; - /** Hits/second for a refresh to be expected within the "popularity" window */ - const HIT_RATE_HIGH = 1; - /** Seconds to ramp up to the "popularity" refresh chance after a key is no longer new */ - const RAMPUP_TTL = 30; + /** @var int Minimum key age, in seconds, for expected time-till-refresh to be considered */ + const AGE_NEW = 60; - /** Idiom for getWithSetCallback() meaning "do not store the callback result" */ - const TTL_UNCACHEABLE = -1; - /** Idiom for getWithSetCallback() meaning "no regeneration mutex based on key hotness" */ + /** @var int Idiom for getWithSetCallback() meaning "no cache stampede mutex required" */ const TSE_NONE = -1; - /** Idiom for set()/getWithSetCallback() meaning "no post-expiration persistence" */ + + /** @var int Idiom for set()/getWithSetCallback() meaning "no post-expiration persistence" */ const STALE_TTL_NONE = 0; - /** Idiom for set()/getWithSetCallback() meaning "no post-expiration grace period" */ + /** @var int Idiom for set()/getWithSetCallback() meaning "no post-expiration grace period" */ const GRACE_TTL_NONE = 0; - /** Idiom for delete()/touchCheckKey() meaning "no hold-off period for cache writes" */ - const HOLDOFF_NONE = 0; + /** @var int Idiom for delete()/touchCheckKey() meaning "no hold-off period" */ + const HOLDOFF_TTL_NONE = 0; + /** @var int Alias for HOLDOFF_TTL_NONE (b/c) (deprecated since 1.34) */ + const HOLDOFF_NONE = self::HOLDOFF_TTL_NONE; - /** Idiom for getWithSetCallback() meaning "no minimum required as-of timestamp" */ + /** @var float Idiom for getWithSetCallback() meaning "no minimum required as-of timestamp" */ const MIN_TIMESTAMP_NONE = 0.0; - /** @var int One second into the UNIX timestamp epoch */ - const EPOCH_UNIX_ONE_SECOND = 1.0; - - /** Tiny negative float to use when CTL comes up >= 0 due to clock skew */ - const TINY_NEGATIVE = -0.000001; - /** Tiny positive float to use when using "minTime" to assert an inequality */ - const TINY_POSTIVE = 0.000001; - /** Milliseconds of delay after get() where set() storms are a consideration with "lockTSE" */ - const SET_DELAY_HIGH_MS = 50; - /** Min millisecond set() backoff for keys in hold-off (far less than INTERIM_KEY_TTL) */ - const RECENT_SET_LOW_MS = 50; - /** Max millisecond set() backoff for keys in hold-off (far less than INTERIM_KEY_TTL) */ - const RECENT_SET_HIGH_MS = 100; + /** @var string Default process cache name and max key count */ + const PC_PRIMARY = 'primary:1000'; - /** @var int Seconds needed for value generation considered slow */ - const GENERATION_SLOW_SEC = 3; - - /** Parameter to get()/getMulti() to return extra information by reference */ + /** @var int Idion for get()/getMulti() to return extra information by reference */ const PASS_BY_REF = -1; - /** Cache format version number */ - const VERSION = 1; - - const FLD_FORMAT_VERSION = 0; // key to WAN cache version number - const FLD_VALUE = 1; // key to the cached value - const FLD_TTL = 2; // key to the original TTL - const FLD_TIME = 3; // key to the cache timestamp - const FLD_FLAGS = 4; // key to the flags bitfield (reserved number) - const FLD_VALUE_VERSION = 5; // key to collection cache version number - const FLD_GENERATION_TIME = 6; // key to how long it took to generate the value - - const PURGE_TIME = 0; // key to the tombstone entry timestamp - const PURGE_HOLDOFF = 1; // key to the tombstone entry hold-off TTL - - const VALUE_KEY_PREFIX = 'WANCache:v:'; - const INTERIM_KEY_PREFIX = 'WANCache:i:'; - const TIME_KEY_PREFIX = 'WANCache:t:'; - const MUTEX_KEY_PREFIX = 'WANCache:m:'; - const COOLOFF_KEY_PREFIX = 'WANCache:c:'; - - const PURGE_VAL_PREFIX = 'PURGED:'; - - const PC_PRIMARY = 'primary:1000'; // process cache name and max key count + /** @var int Seconds to keep dependency purge keys around */ + private static $CHECK_KEY_TTL = self::TTL_YEAR; + /** @var int Seconds to keep interim value keys for tombstoned keys around */ + private static $INTERIM_KEY_TTL = 1; + + /** @var int Seconds to keep lock keys around */ + private static $LOCK_TTL = 10; + /** @var int Seconds to no-op key set() calls to avoid large blob I/O stampedes */ + private static $COOLOFF_TTL = 1; + /** @var int Seconds to ramp up the chance of regeneration due to expected time-till-refresh */ + private static $RAMPUP_TTL = 30; + + /** @var float Tiny negative float to use when CTL comes up >= 0 due to clock skew */ + private static $TINY_NEGATIVE = -0.000001; + /** @var float Tiny positive float to use when using "minTime" to assert an inequality */ + private static $TINY_POSTIVE = 0.000001; + + /** @var int Milliseconds of key fetch/validate/regenerate delay prone to set() stampedes */ + private static $SET_DELAY_HIGH_MS = 50; + /** @var int Min millisecond set() backoff during hold-off (far less than INTERIM_KEY_TTL) */ + private static $RECENT_SET_LOW_MS = 50; + /** @var int Max millisecond set() backoff during hold-off (far less than INTERIM_KEY_TTL) */ + private static $RECENT_SET_HIGH_MS = 100; + + /** @var int Consider value generation slow if it takes more than this many seconds */ + private static $GENERATION_SLOW_SEC = 3; + + /** @var int Key to the tombstone entry timestamp */ + private static $PURGE_TIME = 0; + /** @var int Key to the tombstone entry hold-off TTL */ + private static $PURGE_HOLDOFF = 1; + + /** @var int Cache format version number */ + private static $VERSION = 1; + + /** @var int Key to WAN cache version number */ + private static $FLD_FORMAT_VERSION = 0; + /** @var int Key to the cached value */ + private static $FLD_VALUE = 1; + /** @var int Key to the original TTL */ + private static $FLD_TTL = 2; + /** @var int Key to the cache timestamp */ + private static $FLD_TIME = 3; + /** @var int Key to the flags bit field (reserved number) */ + private static /** @noinspection PhpUnusedPrivateFieldInspection */ $FLD_FLAGS = 4; + /** @var int Key to collection cache version number */ + private static $FLD_VALUE_VERSION = 5; + /** @var int Key to how long it took to generate the value */ + private static $FLD_GENERATION_TIME = 6; + + private static $VALUE_KEY_PREFIX = 'WANCache:v:'; + private static $INTERIM_KEY_PREFIX = 'WANCache:i:'; + private static $TIME_KEY_PREFIX = 'WANCache:t:'; + private static $MUTEX_KEY_PREFIX = 'WANCache:m:'; + private static $COOLOFF_KEY_PREFIX = 'WANCache:c:'; + + private static $PURGE_VAL_PREFIX = 'PURGED:'; /** * @param array $params @@ -256,13 +270,15 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt * is configured to interpret /// key prefixes as routes. This * requires that "region" and "cluster" are both set above. [optional] * - epoch: lowest UNIX timestamp a value/tombstone must have to be valid. [optional] + * - secret: stable secret used for hashing long strings into key components. [optional] */ public function __construct( array $params ) { $this->cache = $params['cache']; $this->region = $params['region'] ?? 'main'; $this->cluster = $params['cluster'] ?? 'wan-main'; $this->mcrouterAware = !empty( $params['mcrouterAware'] ); - $this->epoch = $params['epoch'] ?? self::EPOCH_UNIX_ONE_SECOND; + $this->epoch = $params['epoch'] ?? 0; + $this->secret = $params['secret'] ?? (string)$this->epoch; $this->setLogger( $params['logger'] ?? new NullLogger() ); $this->stats = $params['stats'] ?? new NullStatsdDataFactory(); @@ -331,7 +347,7 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt * * @param string $key Cache key made from makeKey() or makeGlobalKey() * @param mixed|null &$curTTL Approximate TTL left on the key if present/tombstoned [returned] - * @param array $checkKeys List of "check" keys + * @param string[] $checkKeys The "check" keys used to validate the value * @param mixed|null &$info Key info if WANObjectCache::PASS_BY_REF [returned] * @return mixed Value of cache key or false on failure */ @@ -366,14 +382,17 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt * Othwerwise, $info will transform into a map of (cache key => cached value timestamp). * Only the cache keys listed in $keys that exists or are tombstoned will have an entry. * + * $checkKeys holds the "check" keys used to validate values of applicable keys. The integer + * indexes hold "check" keys that apply to all of $keys while the string indexes hold "check" + * keys that only apply to the cache key with that name. + * * @see WANObjectCache::get() * - * @param array $keys List of cache keys made from makeKey() or makeGlobalKey() + * @param string[] $keys List of cache keys made from makeKey() or makeGlobalKey() * @param mixed|null &$curTTLs Map of (key => TTL left) for existing/tombstoned keys [returned] - * @param array $checkKeys List of check keys to apply to all $keys. May also apply "check" - * keys to specific cache keys only by using cache keys as keys in the $checkKeys array. + * @param string[]|string[][] $checkKeys Map of (integer or cache key => "check" key(s)) * @param mixed|null &$info Map of (key => info) if WANObjectCache::PASS_BY_REF [returned] - * @return array Map of (key => value) for keys that exist and are not tombstoned + * @return mixed[] Map of (key => value) for existing values; order of $keys is preserved */ final public function getMulti( array $keys, @@ -385,14 +404,14 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt $curTTLs = []; $infoByKey = []; - $vPrefixLen = strlen( self::VALUE_KEY_PREFIX ); - $valueKeys = self::prefixCacheKeys( $keys, self::VALUE_KEY_PREFIX ); + $vPrefixLen = strlen( self::$VALUE_KEY_PREFIX ); + $valueKeys = self::prefixCacheKeys( $keys, self::$VALUE_KEY_PREFIX ); $checkKeysForAll = []; $checkKeysByKey = []; $checkKeysFlat = []; foreach ( $checkKeys as $i => $checkKeyGroup ) { - $prefixed = self::prefixCacheKeys( (array)$checkKeyGroup, self::TIME_KEY_PREFIX ); + $prefixed = self::prefixCacheKeys( (array)$checkKeyGroup, self::$TIME_KEY_PREFIX ); $checkKeysFlat = array_merge( $checkKeysFlat, $prefixed ); // Are these check keys for a specific cache key, or for all keys being fetched? if ( is_int( $i ) ) { @@ -438,11 +457,11 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt $lastCKPurge = null; // timestamp of the highest check key foreach ( $purgeValues as $purge ) { - $lastCKPurge = max( $purge[self::PURGE_TIME], $lastCKPurge ); - $safeTimestamp = $purge[self::PURGE_TIME] + $purge[self::PURGE_HOLDOFF]; + $lastCKPurge = max( $purge[self::$PURGE_TIME], $lastCKPurge ); + $safeTimestamp = $purge[self::$PURGE_TIME] + $purge[self::$PURGE_HOLDOFF]; if ( $value !== false && $safeTimestamp >= $keyInfo['asOf'] ) { // How long ago this value was invalidated by *this* check key - $ago = min( $purge[self::PURGE_TIME] - $now, self::TINY_NEGATIVE ); + $ago = min( $purge[self::$PURGE_TIME] - $now, self::$TINY_NEGATIVE ); // How long ago this value was invalidated by *any* known check key $keyInfo['curTTL'] = min( $keyInfo['curTTL'], $ago ); } @@ -468,10 +487,10 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt /** * @since 1.27 - * @param array $timeKeys List of prefixed time check keys - * @param array $wrappedValues + * @param string[] $timeKeys List of prefixed time check keys + * @param mixed[] $wrappedValues * @param float $now - * @return array List of purge value arrays + * @return array[] List of purge value arrays */ private function processCheckKeys( array $timeKeys, array $wrappedValues, $now ) { $purgeValues = []; @@ -482,7 +501,7 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt if ( $purge === false ) { // Key is not set or malformed; regenerate $newVal = $this->makePurgeValue( $now, self::HOLDOFF_TTL ); - $this->cache->add( $timeKey, $newVal, self::CHECK_KEY_TTL ); + $this->cache->add( $timeKey, $newVal, self::$CHECK_KEY_TTL ); $purge = $this->parsePurgeValue( $newVal ); } $purgeValues[] = $purge; @@ -642,10 +661,10 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt $storeTTL = $ttl + $staleTTL; if ( $creating ) { - $ok = $this->cache->add( self::VALUE_KEY_PREFIX . $key, $wrapped, $storeTTL ); + $ok = $this->cache->add( self::$VALUE_KEY_PREFIX . $key, $wrapped, $storeTTL ); } else { $ok = $this->cache->merge( - self::VALUE_KEY_PREFIX . $key, + self::$VALUE_KEY_PREFIX . $key, function ( $cache, $key, $cWrapped ) use ( $wrapped ) { // A string value means that it is a tombstone; do nothing in that case return ( is_string( $cWrapped ) ) ? false : $wrapped; @@ -709,7 +728,7 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt * * The $ttl parameter can be used when purging values that have not actually changed * recently. For example, a cleanup script to purge cache entries does not really need - * a hold-off period, so it can use HOLDOFF_NONE. Likewise for user-requested purge. + * a hold-off period, so it can use HOLDOFF_TTL_NONE. Likewise for user-requested purge. * Note that $ttl limits the effective range of 'lockTSE' for getWithSetCallback(). * * If called twice on the same key, then the last hold-off TTL takes precedence. For @@ -722,10 +741,10 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt final public function delete( $key, $ttl = self::HOLDOFF_TTL ) { if ( $ttl <= 0 ) { // Publish the purge to all datacenters - $ok = $this->relayDelete( self::VALUE_KEY_PREFIX . $key ); + $ok = $this->relayDelete( self::$VALUE_KEY_PREFIX . $key ); } else { // Publish the purge to all datacenters - $ok = $this->relayPurge( self::VALUE_KEY_PREFIX . $key, $ttl, self::HOLDOFF_NONE ); + $ok = $this->relayPurge( self::$VALUE_KEY_PREFIX . $key, $ttl, self::HOLDOFF_TTL_NONE ); } $kClass = $this->determineKeyClassForStats( $key ); @@ -814,14 +833,14 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt * @see WANObjectCache::getCheckKeyTime() * @see WANObjectCache::getWithSetCallback() * - * @param array $keys + * @param string[] $keys * @return float[] Map of (key => UNIX timestamp) * @since 1.31 */ final public function getMultiCheckKeyTime( array $keys ) { $rawKeys = []; foreach ( $keys as $key ) { - $rawKeys[$key] = self::TIME_KEY_PREFIX . $key; + $rawKeys[$key] = self::$TIME_KEY_PREFIX . $key; } $rawValues = $this->cache->getMulti( $rawKeys ); @@ -831,14 +850,14 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt foreach ( $rawKeys as $key => $rawKey ) { $purge = $this->parsePurgeValue( $rawValues[$rawKey] ); if ( $purge !== false ) { - $time = $purge[self::PURGE_TIME]; + $time = $purge[self::$PURGE_TIME]; } else { // Casting assures identical floats for the next getCheckKeyTime() calls $now = (string)$this->getCurrentTime(); $this->cache->add( $rawKey, $this->makePurgeValue( $now, self::HOLDOFF_TTL ), - self::CHECK_KEY_TTL + self::$CHECK_KEY_TTL ); $time = (float)$now; } @@ -880,12 +899,12 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt * @see WANObjectCache::resetCheckKey() * * @param string $key Cache key - * @param int $holdoff HOLDOFF_TTL or HOLDOFF_NONE constant + * @param int $holdoff HOLDOFF_TTL or HOLDOFF_TTL_NONE constant * @return bool True if the item was purged or not found, false on failure */ final public function touchCheckKey( $key, $holdoff = self::HOLDOFF_TTL ) { // Publish the purge to all datacenters - $ok = $this->relayPurge( self::TIME_KEY_PREFIX . $key, self::CHECK_KEY_TTL, $holdoff ); + $ok = $this->relayPurge( self::$TIME_KEY_PREFIX . $key, self::$CHECK_KEY_TTL, $holdoff ); $kClass = $this->determineKeyClassForStats( $key ); $this->stats->increment( "wanobjectcache.$kClass.ck_touch." . ( $ok ? 'ok' : 'error' ) ); @@ -922,7 +941,7 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt */ final public function resetCheckKey( $key ) { // Publish the purge to all datacenters - $ok = $this->relayDelete( self::TIME_KEY_PREFIX . $key ); + $ok = $this->relayDelete( self::$TIME_KEY_PREFIX . $key ); $kClass = $this->determineKeyClassForStats( $key ); $this->stats->increment( "wanobjectcache.$kClass.ck_reset." . ( $ok ? 'ok' : 'error' ) ); @@ -1234,18 +1253,18 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt final public function getWithSetCallback( $key, $ttl, $callback, array $opts = [] ) { $version = $opts['version'] ?? null; $pcTTL = $opts['pcTTL'] ?? self::TTL_UNCACHEABLE; + $pCache = ( $pcTTL >= 0 ) + ? $this->getProcessCache( $opts['pcGroup'] ?? self::PC_PRIMARY ) + : null; // Use the process cache if requested as long as no outer cache callback is running. // Nested callback process cache use is not lag-safe with regard to HOLDOFF_TTL since // process cached values are more lagged than persistent ones as they are not purged. - if ( $pcTTL >= 0 && $this->callbackDepth == 0 ) { - $pCache = $this->getProcessCache( $opts['pcGroup'] ?? self::PC_PRIMARY ); + if ( $pCache && $this->callbackDepth == 0 ) { $cached = $pCache->get( $this->getProcessCacheKey( $key, $version ), INF, false ); if ( $cached !== false ) { return $cached; } - } else { - $pCache = null; } $res = $this->fetchOrRegenerate( $key, $ttl, $callback, $opts ); @@ -1367,30 +1386,25 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt // This avoids stampedes on eviction or preemptive regeneration taking too long. ( $busyValue !== null && $possValue === false ); - // If a regeneration lock is required, threads that do not get the lock will use any - // available stale or volatile value. If there is none, then the cheap/placeholder - // value from $busyValue will be used if provided; failing that, all threads will try - // to regenerate the value and ignore the lock. - if ( $useRegenerationLock ) { - $hasLock = $this->cache->add( self::MUTEX_KEY_PREFIX . $key, 1, self::LOCK_TTL ); - if ( !$hasLock ) { - if ( $this->isValid( $possValue, $possInfo['asOf'], $minAsOf ) ) { - $this->stats->increment( "wanobjectcache.$kClass.hit.stale" ); - - return [ $possValue, $possInfo['version'], $curInfo['asOf'] ]; - } elseif ( $busyValue !== null ) { - $miss = is_infinite( $minAsOf ) ? 'renew' : 'miss'; - $this->stats->increment( "wanobjectcache.$kClass.$miss.busy" ); - - return [ - is_callable( $busyValue ) ? $busyValue() : $busyValue, - $version, - $curInfo['asOf'] - ]; - } + // If a regeneration lock is required, threads that do not get the lock will try to use + // the stale value, the interim value, or the $busyValue placeholder, in that order. If + // none of those are set then all threads will bypass the lock and regenerate the value. + $hasLock = $useRegenerationLock && $this->claimStampedeLock( $key ); + if ( $useRegenerationLock && !$hasLock ) { + if ( $this->isValid( $possValue, $possInfo['asOf'], $minAsOf ) ) { + $this->stats->increment( "wanobjectcache.$kClass.hit.stale" ); + + return [ $possValue, $possInfo['version'], $curInfo['asOf'] ]; + } elseif ( $busyValue !== null ) { + $miss = is_infinite( $minAsOf ) ? 'renew' : 'miss'; + $this->stats->increment( "wanobjectcache.$kClass.$miss.busy" ); + + return [ + is_callable( $busyValue ) ? $busyValue() : $busyValue, + $version, + $curInfo['asOf'] + ]; } - } else { - $hasLock = false; } // Generate the new value given any prior value with a matching version @@ -1440,9 +1454,7 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt } } - if ( $hasLock ) { - $this->cache->changeTTL( self::MUTEX_KEY_PREFIX . $key, (int)$initialTime - 60 ); - } + $this->yieldStampedeLock( $key, $hasLock ); $miss = is_infinite( $minAsOf ) ? 'renew' : 'miss'; $this->stats->increment( "wanobjectcache.$kClass.$miss.compute" ); @@ -1450,12 +1462,34 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt return [ $value, $version, $curInfo['asOf'] ]; } + /** + * @param string $key + * @return bool Success + */ + private function claimStampedeLock( $key ) { + // Note that locking is not bypassed due to I/O errors; this avoids stampedes + return $this->cache->add( self::$MUTEX_KEY_PREFIX . $key, 1, self::$LOCK_TTL ); + } + + /** + * @param string $key + * @param bool $hasLock + */ + private function yieldStampedeLock( $key, $hasLock ) { + if ( $hasLock ) { + // The backend might be a mcrouter proxy set to broadcast DELETE to *all* the local + // datacenter cache servers via OperationSelectorRoute (for increased consistency). + // Since that would be excessive for these locks, use TOUCH to expire the key. + $this->cache->changeTTL( self::$MUTEX_KEY_PREFIX . $key, $this->getCurrentTime() - 60 ); + } + } + /** * @param float $age Age of volatile/interim key in seconds * @return bool Whether the age of a volatile value is negligible */ private function isVolatileValueAgeNegligible( $age ) { - return ( $age < mt_rand( self::RECENT_SET_LOW_MS, self::RECENT_SET_HIGH_MS ) / 1e3 ); + return ( $age < mt_rand( self::$RECENT_SET_LOW_MS, self::$RECENT_SET_HIGH_MS ) / 1e3 ); } /** @@ -1477,13 +1511,13 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt // consistent hashing). if ( $lockTSE < 0 || $hasLock ) { return true; // either not a priori hot or thread has the lock - } elseif ( $elapsed <= self::SET_DELAY_HIGH_MS * 1e3 ) { + } elseif ( $elapsed <= self::$SET_DELAY_HIGH_MS * 1e3 ) { return true; // not enough time for threads to pile up } $this->cache->clearLastError(); if ( - !$this->cache->add( self::COOLOFF_KEY_PREFIX . $key, 1, self::COOLOFF_TTL ) && + !$this->cache->add( self::$COOLOFF_KEY_PREFIX . $key, 1, self::$COOLOFF_TTL ) && // Don't treat failures due to I/O errors as the key being in cooloff $this->cache->getLastError() === BagOStuff::ERR_NONE ) { @@ -1510,7 +1544,7 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt $touched = $touchedCallback( $value ); if ( $touched !== null && $touched >= $curInfo['asOf'] ) { - $curTTL = min( $curTTL, self::TINY_NEGATIVE, $curInfo['asOf'] - $touched ); + $curTTL = min( $curTTL, self::$TINY_NEGATIVE, $curInfo['asOf'] - $touched ); } return [ $curTTL, max( $curInfo['tombAsOf'], $curInfo['lastCKPurge'], $touched ) ]; @@ -1538,7 +1572,7 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt $now = $this->getCurrentTime(); if ( $this->useInterimHoldOffCaching ) { - $wrapped = $this->cache->get( self::INTERIM_KEY_PREFIX . $key ); + $wrapped = $this->cache->get( self::$INTERIM_KEY_PREFIX . $key ); list( $value, $keyInfo ) = $this->unwrap( $wrapped, $now ); if ( $this->isValid( $value, $keyInfo['asOf'], $minAsOf ) ) { @@ -1557,11 +1591,11 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt * @param float $walltime How long it took to generate the value in seconds */ private function setInterimValue( $key, $value, $ttl, $version, $walltime ) { - $ttl = max( self::INTERIM_KEY_TTL, (int)$ttl ); + $ttl = max( self::$INTERIM_KEY_TTL, (int)$ttl ); $wrapped = $this->wrap( $value, $ttl, $version, $this->getCurrentTime(), $walltime ); $this->cache->merge( - self::INTERIM_KEY_PREFIX . $key, + self::$INTERIM_KEY_PREFIX . $key, function () use ( $wrapped ) { return $wrapped; }, @@ -1594,7 +1628,7 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt * // Map of cache keys to entity IDs * $cache->makeMultiKeys( * $this->fileVersionIds(), - * function ( $id, WANObjectCache $cache ) { + * function ( $id ) use ( $cache ) { * return $cache->makeKey( 'file-version', $id ); * } * ), @@ -1633,17 +1667,15 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt * @param int $ttl Seconds to live for key updates * @param callable $callback Callback the yields entity regeneration callbacks * @param array $opts Options map - * @return array Map of (cache key => value) in the same order as $keyedIds + * @return mixed[] Map of (cache key => value) in the same order as $keyedIds * @since 1.28 */ final public function getMultiWithSetCallback( ArrayIterator $keyedIds, $ttl, callable $callback, array $opts = [] ) { - $valueKeys = array_keys( $keyedIds->getArrayCopy() ); - // Load required keys into process cache in one go $this->warmupCache = $this->getRawKeysForWarmup( - $this->getNonProcessCachedKeys( $valueKeys, $opts ), + $this->getNonProcessCachedMultiKeys( $keyedIds, $opts ), $opts['checkKeys'] ?? [] ); $this->warmupKeyMisses = 0; @@ -1686,7 +1718,7 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt * // Map of cache keys to entity IDs * $cache->makeMultiKeys( * $this->fileVersionIds(), - * function ( $id, WANObjectCache $cache ) { + * function ( $id ) use ( $cache ) { * return $cache->makeKey( 'file-version', $id ); * } * ), @@ -1726,21 +1758,19 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt * @param int $ttl Seconds to live for key updates * @param callable $callback Callback the yields entity regeneration callbacks * @param array $opts Options map - * @return array Map of (cache key => value) in the same order as $keyedIds + * @return mixed[] Map of (cache key => value) in the same order as $keyedIds * @since 1.30 */ final public function getMultiWithUnionSetCallback( ArrayIterator $keyedIds, $ttl, callable $callback, array $opts = [] ) { - $idsByValueKey = $keyedIds->getArrayCopy(); - $valueKeys = array_keys( $idsByValueKey ); $checkKeys = $opts['checkKeys'] ?? []; unset( $opts['lockTSE'] ); // incompatible unset( $opts['busyValue'] ); // incompatible // Load required keys into process cache in one go - $keysGet = $this->getNonProcessCachedKeys( $valueKeys, $opts ); - $this->warmupCache = $this->getRawKeysForWarmup( $keysGet, $checkKeys ); + $keysByIdGet = $this->getNonProcessCachedMultiKeys( $keyedIds, $opts ); + $this->warmupCache = $this->getRawKeysForWarmup( $keysByIdGet, $checkKeys ); $this->warmupKeyMisses = 0; // IDs of entities known to be in need of regeneration @@ -1749,10 +1779,10 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt // Find out which keys are missing/deleted/stale $curTTLs = []; $asOfs = []; - $curByKey = $this->getMulti( $keysGet, $curTTLs, $checkKeys, $asOfs ); - foreach ( $keysGet as $key ) { + $curByKey = $this->getMulti( $keysByIdGet, $curTTLs, $checkKeys, $asOfs ); + foreach ( $keysByIdGet as $id => $key ) { if ( !array_key_exists( $key, $curByKey ) || $curTTLs[$key] < 0 ) { - $idsRegen[] = $idsByValueKey[$key]; + $idsRegen[] = $id; } } @@ -1784,7 +1814,7 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt // Run the cache-aside logic using warmupCache instead of persistent cache queries $values = []; - foreach ( $idsByValueKey as $key => $id ) { // preserve order + foreach ( $keyedIds as $key => $id ) { // preserve order $values[$key] = $this->getWithSetCallback( $key, $ttl, $func, $opts ); } @@ -1807,12 +1837,12 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt */ final public function reap( $key, $purgeTimestamp, &$isStale = false ) { $minAsOf = $purgeTimestamp + self::HOLDOFF_TTL; - $wrapped = $this->cache->get( self::VALUE_KEY_PREFIX . $key ); - if ( is_array( $wrapped ) && $wrapped[self::FLD_TIME] < $minAsOf ) { + $wrapped = $this->cache->get( self::$VALUE_KEY_PREFIX . $key ); + if ( is_array( $wrapped ) && $wrapped[self::$FLD_TIME] < $minAsOf ) { $isStale = true; $this->logger->warning( "Reaping stale value key '$key'." ); $ttlReap = self::HOLDOFF_TTL; // avoids races with tombstone creation - $ok = $this->cache->changeTTL( self::VALUE_KEY_PREFIX . $key, $ttlReap ); + $ok = $this->cache->changeTTL( self::$VALUE_KEY_PREFIX . $key, $ttlReap ); if ( !$ok ) { $this->logger->error( "Could not complete reap of key '$key'." ); } @@ -1835,11 +1865,11 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt * @since 1.28 */ final public function reapCheckKey( $key, $purgeTimestamp, &$isStale = false ) { - $purge = $this->parsePurgeValue( $this->cache->get( self::TIME_KEY_PREFIX . $key ) ); - if ( $purge && $purge[self::PURGE_TIME] < $purgeTimestamp ) { + $purge = $this->parsePurgeValue( $this->cache->get( self::$TIME_KEY_PREFIX . $key ) ); + if ( $purge && $purge[self::$PURGE_TIME] < $purgeTimestamp ) { $isStale = true; $this->logger->warning( "Reaping stale check key '$key'." ); - $ok = $this->cache->changeTTL( self::TIME_KEY_PREFIX . $key, self::TTL_SECOND ); + $ok = $this->cache->changeTTL( self::$TIME_KEY_PREFIX . $key, self::TTL_SECOND ); if ( !$ok ) { $this->logger->error( "Could not complete reap of check key '$key'." ); } @@ -1855,38 +1885,153 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt /** * @see BagOStuff::makeKey() * @param string $class Key class - * @param string|null $component [optional] Key component (starting with a key collection name) - * @return string Colon-delimited list of $keyspace followed by escaped components of $args + * @param string ...$components Key components (starting with a key collection name) + * @return string Colon-delimited list of $keyspace followed by escaped components * @since 1.27 */ - public function makeKey( $class, $component = null ) { + public function makeKey( $class, ...$components ) { return $this->cache->makeKey( ...func_get_args() ); } /** * @see BagOStuff::makeGlobalKey() * @param string $class Key class - * @param string|null $component [optional] Key component (starting with a key collection name) - * @return string Colon-delimited list of $keyspace followed by escaped components of $args + * @param string ...$components Key components (starting with a key collection name) + * @return string Colon-delimited list of $keyspace followed by escaped components * @since 1.27 */ - public function makeGlobalKey( $class, $component = null ) { + public function makeGlobalKey( $class, ...$components ) { return $this->cache->makeGlobalKey( ...func_get_args() ); } /** - * @param array $entities List of entity IDs - * @param callable $keyFunc Callback yielding a key from (entity ID, this WANObjectCache) - * @return ArrayIterator Iterator yielding (cache key => entity ID) in $entities order + * Hash a possibly long string into a suitable component for makeKey()/makeGlobalKey() + * + * @param string $component A raw component used in building a cache key + * @return string 64 character HMAC using a stable secret for public collision resistance + * @since 1.34 + */ + public function hash256( $component ) { + return hash_hmac( 'sha256', $component, $this->secret ); + } + + /** + * Get an iterator of (cache key => entity ID) for a list of entity IDs + * + * The callback takes an ID string and returns a key via makeKey()/makeGlobalKey(). + * There should be no network nor filesystem I/O used in the callback. The entity + * ID/key mapping must be 1:1 or an exception will be thrown. If hashing is needed, + * then use the hash256() method. + * + * Example usage for the default keyspace: + * @code + * $keyedIds = $cache->makeMultiKeys( + * $modules, + * function ( $module ) use ( $cache ) { + * return $cache->makeKey( 'module-info', $module ); + * } + * ); + * @endcode + * + * Example usage for mixed default and global keyspace: + * @code + * $keyedIds = $cache->makeMultiKeys( + * $filters, + * function ( $filter ) use ( $cache ) { + * return ( strpos( $filter, 'central:' ) === 0 ) + * ? $cache->makeGlobalKey( 'regex-filter', $filter ) + * : $cache->makeKey( 'regex-filter', $filter ) + * } + * ); + * @endcode + * + * Example usage with hashing: + * @code + * $keyedIds = $cache->makeMultiKeys( + * $urls, + * function ( $url ) use ( $cache ) { + * return $cache->makeKey( 'url-info', $cache->hash256( $url ) ); + * } + * ); + * @endcode + * + * @see WANObjectCache::makeKey() + * @see WANObjectCache::makeGlobalKey() + * @see WANObjectCache::hash256() + * + * @param string[]|int[] $ids List of entity IDs + * @param callable $keyCallback Function returning makeKey()/makeGlobalKey() on the input ID + * @return ArrayIterator Iterator of (cache key => ID); order of $ids is preserved + * @throws UnexpectedValueException * @since 1.28 */ - final public function makeMultiKeys( array $entities, callable $keyFunc ) { - $map = []; - foreach ( $entities as $entity ) { - $map[$keyFunc( $entity, $this )] = $entity; + final public function makeMultiKeys( array $ids, $keyCallback ) { + $idByKey = []; + foreach ( $ids as $id ) { + // Discourage triggering of automatic makeKey() hashing in some backends + if ( strlen( $id ) > 64 ) { + $this->logger->warning( __METHOD__ . ": long ID '$id'; use hash256()" ); + } + $key = $keyCallback( $id, $this ); + // Edge case: ignore key collisions due to duplicate $ids like "42" and 42 + if ( !isset( $idByKey[$key] ) ) { + $idByKey[$key] = $id; + } elseif ( (string)$id !== (string)$idByKey[$key] ) { + throw new UnexpectedValueException( + "Cache key collision; IDs ('$id','{$idByKey[$key]}') map to '$key'" + ); + } + } + + return new ArrayIterator( $idByKey ); + } + + /** + * Get an (ID => value) map from (i) a non-unique list of entity IDs, and (ii) the list + * of corresponding entity values by first appearance of each ID in the entity ID list + * + * For use with getMultiWithSetCallback() and getMultiWithUnionSetCallback(). + * + * *Only* use this method if the entity ID/key mapping is trivially 1:1 without exception. + * Key generation method must utitilize the *full* entity ID in the key (not a hash of it). + * + * Example usage: + * @code + * $poems = $cache->getMultiWithSetCallback( + * $cache->makeMultiKeys( + * $uuids, + * function ( $uuid ) use ( $cache ) { + * return $cache->makeKey( 'poem', $uuid ); + * } + * ), + * $cache::TTL_DAY, + * function ( $uuid ) use ( $url ) { + * return $this->http->run( [ 'method' => 'GET', 'url' => "$url/$uuid" ] ); + * } + * ); + * $poemsByUUID = $cache->multiRemap( $uuids, $poems ); + * @endcode + * + * @see WANObjectCache::makeMultiKeys() + * @see WANObjectCache::getMultiWithSetCallback() + * @see WANObjectCache::getMultiWithUnionSetCallback() + * + * @param string[]|int[] $ids Entity ID list makeMultiKeys() + * @param mixed[] $res Result of getMultiWithSetCallback()/getMultiWithUnionSetCallback() + * @return mixed[] Map of (ID => value); order of $ids is preserved + * @since 1.34 + */ + final public function multiRemap( array $ids, array $res ) { + if ( count( $ids ) !== count( $res ) ) { + // If makeMultiKeys() is called on a list of non-unique IDs, then the resulting + // ArrayIterator will have less entries due to "first appearance" de-duplication + $ids = array_keys( array_flip( $ids ) ); + if ( count( $ids ) !== count( $res ) ) { + throw new UnexpectedValueException( "Multi-key result does not match ID list" ); + } } - return new ArrayIterator( $map ); + return array_combine( $ids, $res ); } /** @@ -2057,14 +2202,14 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt // Wildcards select all matching routes, e.g. the WAN cluster on all DCs $ok = $this->cache->set( "/*/{$this->cluster}/{$key}", - $this->makePurgeValue( $this->getCurrentTime(), self::HOLDOFF_NONE ), + $this->makePurgeValue( $this->getCurrentTime(), self::HOLDOFF_TTL_NONE ), $ttl ); } else { // This handles the mcrouter and the single-DC case $ok = $this->cache->set( $key, - $this->makePurgeValue( $this->getCurrentTime(), self::HOLDOFF_NONE ), + $this->makePurgeValue( $this->getCurrentTime(), self::HOLDOFF_TTL_NONE ), $ttl ); } @@ -2196,17 +2341,18 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt return false; } + $popularHitsPerSec = 1; // Lifecycle is: new, ramp-up refresh chance, full refresh chance. - // Note that the "expected # of refreshes" for the ramp-up time range is half of what it - // would be if P(refresh) was at its full value during that time range. - $refreshWindowSec = max( $timeTillRefresh - $ageNew - self::RAMPUP_TTL / 2, 1 ); + // Note that the "expected # of refreshes" for the ramp-up time range is half + // of what it would be if P(refresh) was at its full value during that time range. + $refreshWindowSec = max( $timeTillRefresh - $ageNew - self::$RAMPUP_TTL / 2, 1 ); // P(refresh) * (# hits in $refreshWindowSec) = (expected # of refreshes) - // P(refresh) * ($refreshWindowSec * $popularHitsPerSec) = 1 + // P(refresh) * ($refreshWindowSec * $popularHitsPerSec) = 1 (by definition) // P(refresh) = 1/($refreshWindowSec * $popularHitsPerSec) - $chance = 1 / ( self::HIT_RATE_HIGH * $refreshWindowSec ); + $chance = 1 / ( $popularHitsPerSec * $refreshWindowSec ); // Ramp up $chance from 0 to its nominal value over RAMPUP_TTL seconds to avoid stampedes - $chance *= ( $timeOld <= self::RAMPUP_TTL ) ? $timeOld / self::RAMPUP_TTL : 1; + $chance *= ( $timeOld <= self::$RAMPUP_TTL ) ? $timeOld / self::$RAMPUP_TTL : 1; return mt_rand( 1, 1e9 ) <= 1e9 * $chance; } @@ -2222,7 +2368,7 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt */ protected function isValid( $value, $asOf, $minAsOf, $purgeTime = null ) { // Avoid reading any key not generated after the latest delete() or touch - $safeMinAsOf = max( $minAsOf, $purgeTime + self::TINY_POSTIVE ); + $safeMinAsOf = max( $minAsOf, $purgeTime + self::$TINY_POSTIVE ); if ( $value === false ) { return false; @@ -2245,16 +2391,16 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt // Returns keys in ascending integer order for PHP7 array packing: // https://nikic.github.io/2014/12/22/PHPs-new-hashtable-implementation.html $wrapped = [ - self::FLD_FORMAT_VERSION => self::VERSION, - self::FLD_VALUE => $value, - self::FLD_TTL => $ttl, - self::FLD_TIME => $now + self::$FLD_FORMAT_VERSION => self::$VERSION, + self::$FLD_VALUE => $value, + self::$FLD_TTL => $ttl, + self::$FLD_TIME => $now ]; if ( $version !== null ) { - $wrapped[self::FLD_VALUE_VERSION] = $version; + $wrapped[self::$FLD_VALUE_VERSION] = $version; } - if ( $walltime >= self::GENERATION_SLOW_SEC ) { - $wrapped[self::FLD_GENERATION_TIME] = $walltime; + if ( $walltime >= self::$GENERATION_SLOW_SEC ) { + $wrapped[self::$FLD_GENERATION_TIME] = $walltime; } return $wrapped; @@ -2277,20 +2423,20 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt if ( is_array( $wrapped ) ) { // Entry expected to be a cached value; validate it if ( - ( $wrapped[self::FLD_FORMAT_VERSION] ?? null ) === self::VERSION && - $wrapped[self::FLD_TIME] >= $this->epoch + ( $wrapped[self::$FLD_FORMAT_VERSION] ?? null ) === self::$VERSION && + $wrapped[self::$FLD_TIME] >= $this->epoch ) { - if ( $wrapped[self::FLD_TTL] > 0 ) { + if ( $wrapped[self::$FLD_TTL] > 0 ) { // Get the approximate time left on the key - $age = $now - $wrapped[self::FLD_TIME]; - $curTTL = max( $wrapped[self::FLD_TTL] - $age, 0.0 ); + $age = $now - $wrapped[self::$FLD_TIME]; + $curTTL = max( $wrapped[self::$FLD_TTL] - $age, 0.0 ); } else { // Key had no TTL, so the time left is unbounded $curTTL = INF; } - $value = $wrapped[self::FLD_VALUE]; - $info['version'] = $wrapped[self::FLD_VALUE_VERSION] ?? null; - $info['asOf'] = $wrapped[self::FLD_TIME]; + $value = $wrapped[self::$FLD_VALUE]; + $info['version'] = $wrapped[self::$FLD_VALUE_VERSION] ?? null; + $info['asOf'] = $wrapped[self::$FLD_TIME]; $info['curTTL'] = $curTTL; } } else { @@ -2298,8 +2444,8 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt $purge = $this->parsePurgeValue( $wrapped ); if ( $purge !== false ) { // Tombstoned keys should always have a negative current $ttl - $info['curTTL'] = min( $purge[self::PURGE_TIME] - $now, self::TINY_NEGATIVE ); - $info['tombAsOf'] = $purge[self::PURGE_TIME]; + $info['curTTL'] = min( $purge[self::$PURGE_TIME] - $now, self::$TINY_NEGATIVE ); + $info['tombAsOf'] = $purge[self::$PURGE_TIME]; } } @@ -2307,9 +2453,9 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt } /** - * @param array $keys + * @param string[] $keys * @param string $prefix - * @return string[] + * @return string[] Prefix keys; the order of $keys is preserved */ protected static function prefixCacheKeys( array $keys, $prefix ) { $res = []; @@ -2341,8 +2487,10 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt } $segments = explode( ':', $value, 3 ); - if ( !isset( $segments[0] ) || !isset( $segments[1] ) - || "{$segments[0]}:" !== self::PURGE_VAL_PREFIX + if ( + !isset( $segments[0] ) || + !isset( $segments[1] ) || + "{$segments[0]}:" !== self::$PURGE_VAL_PREFIX ) { return false; } @@ -2358,8 +2506,8 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt } return [ - self::PURGE_TIME => (float)$segments[1], - self::PURGE_HOLDOFF => (int)$segments[2], + self::$PURGE_TIME => (float)$segments[1], + self::$PURGE_HOLDOFF => (int)$segments[2], ]; } @@ -2369,7 +2517,7 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt * @return string Wrapped purge value */ private function makePurgeValue( $timestamp, $holdoff ) { - return self::PURGE_VAL_PREFIX . (float)$timestamp . ':' . (int)$holdoff; + return self::$PURGE_VAL_PREFIX . (float)$timestamp . ':' . (int)$holdoff; } /** @@ -2395,31 +2543,31 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt } /** - * @param array $keys + * @param ArrayIterator $keys * @param array $opts - * @return string[] List of keys + * @return string[] Map of (ID => cache key) */ - private function getNonProcessCachedKeys( array $keys, array $opts ) { + private function getNonProcessCachedMultiKeys( ArrayIterator $keys, array $opts ) { $pcTTL = $opts['pcTTL'] ?? self::TTL_UNCACHEABLE; - $keysFound = []; + $keysMissing = []; if ( $pcTTL > 0 && $this->callbackDepth == 0 ) { $version = $opts['version'] ?? null; $pCache = $this->getProcessCache( $opts['pcGroup'] ?? self::PC_PRIMARY ); - foreach ( $keys as $key ) { - if ( $pCache->has( $this->getProcessCacheKey( $key, $version ), $pcTTL ) ) { - $keysFound[] = $key; + foreach ( $keys as $key => $id ) { + if ( !$pCache->has( $this->getProcessCacheKey( $key, $version ), $pcTTL ) ) { + $keysMissing[$id] = $key; } } } - return array_diff( $keys, $keysFound ); + return $keysMissing; } /** - * @param array $keys - * @param array $checkKeys - * @return array Map of (cache key => mixed) + * @param string[] $keys + * @param string[]|string[][] $checkKeys + * @return string[] List of cache keys */ private function getRawKeysForWarmup( array $keys, array $checkKeys ) { if ( !$keys ) { @@ -2429,18 +2577,18 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt $keysWarmUp = []; // Get all the value keys to fetch... foreach ( $keys as $key ) { - $keysWarmUp[] = self::VALUE_KEY_PREFIX . $key; + $keysWarmUp[] = self::$VALUE_KEY_PREFIX . $key; } // Get all the check keys to fetch... foreach ( $checkKeys as $i => $checkKeyOrKeys ) { if ( is_int( $i ) ) { // Single check key that applies to all value keys - $keysWarmUp[] = self::TIME_KEY_PREFIX . $checkKeyOrKeys; + $keysWarmUp[] = self::$TIME_KEY_PREFIX . $checkKeyOrKeys; } else { // List of check keys that apply to value key $i $keysWarmUp = array_merge( $keysWarmUp, - self::prefixCacheKeys( $checkKeyOrKeys, self::TIME_KEY_PREFIX ) + self::prefixCacheKeys( $checkKeyOrKeys, self::$TIME_KEY_PREFIX ) ); } }