MediaWiki  master
WANObjectCache.php
Go to the documentation of this file.
1 <?php
22 use Liuggio\StatsdClient\Factory\StatsdDataFactoryInterface;
23 use Psr\Log\LoggerAwareInterface;
24 use Psr\Log\LoggerInterface;
25 use Psr\Log\NullLogger;
28 
120 class WANObjectCache implements
124  LoggerAwareInterface
125 {
127  protected $cache;
129  protected $processCaches = [];
131  protected $logger;
133  protected $stats;
135  protected $asyncHandler;
136 
138  protected $mcrouterAware;
140  protected $region;
142  protected $cluster;
144  protected $useInterimHoldOffCaching = true;
146  protected $epoch;
148  protected $secret;
150  protected $coalesceKeys;
152  protected $coalesceScheme;
153 
155  private $keyHighQps;
158 
160  private $callbackDepth = 0;
162  private $warmupCache = [];
164  private $warmupKeyMisses = 0;
165 
168 
170  private const MAX_COMMIT_DELAY = 3;
172  private const MAX_READ_LAG = 7;
174  public const HOLDOFF_TTL = self::MAX_COMMIT_DELAY + self::MAX_READ_LAG + 1;
175 
177  private const LOW_TTL = 30;
179  public const TTL_LAGGED = 30;
180 
182  private const HOT_TTR = 900;
184  private const AGE_NEW = 60;
185 
187  private const TSE_NONE = -1;
188 
190  public const STALE_TTL_NONE = 0;
192  public const GRACE_TTL_NONE = 0;
194  public const HOLDOFF_TTL_NONE = 0;
196  public const HOLDOFF_NONE = self::HOLDOFF_TTL_NONE;
197 
199  public const MIN_TIMESTAMP_NONE = 0.0;
200 
202  private const PC_PRIMARY = 'primary:1000';
203 
205  public const PASS_BY_REF = [];
206 
208  private const SCHEME_HASH_TAG = 1;
210  private const SCHEME_HASH_STOP = 2;
211 
213  private static $CHECK_KEY_TTL = self::TTL_YEAR;
215  private static $INTERIM_KEY_TTL = 1;
216 
218  private static $LOCK_TTL = 10;
220  private static $COOLOFF_TTL = 1;
222  private static $RAMPUP_TTL = 30;
223 
225  private static $TINY_NEGATIVE = -0.000001;
227  private static $TINY_POSTIVE = 0.000001;
228 
230  private static $RECENT_SET_LOW_MS = 50;
232  private static $RECENT_SET_HIGH_MS = 100;
233 
235  private static $GENERATION_SLOW_SEC = 3;
236 
238  private static $PURGE_TIME = 0;
240  private static $PURGE_HOLDOFF = 1;
241 
243  private static $VERSION = 1;
244 
246  private const RES_VALUE = 0;
248  private const RES_METADATA = 1;
249 
251  public const KEY_VERSION = 'version';
253  public const KEY_AS_OF = 'asOf';
255  public const KEY_TTL = 'ttl';
257  public const KEY_CUR_TTL = 'curTTL';
259  public const KEY_TOMB_AS_OF = 'tombAsOf';
261  public const KEY_CHECK_AS_OF = 'lastCKPurge';
262 
264  private static $FLD_FORMAT_VERSION = 0;
266  private static $FLD_VALUE = 1;
268  private static $FLD_TTL = 2;
270  private static $FLD_TIME = 3;
272  private static $FLD_FLAGS = 4;
274  private static $FLD_VALUE_VERSION = 5;
276  private static $FLD_GENERATION_TIME = 6;
277 
279  private const TYPE_VALUE = 'v';
281  private const TYPE_TIMESTAMP = 't';
283  private const TYPE_MUTEX = 'm';
285  private const TYPE_INTERIM = 'i';
287  private const TYPE_COOLOFF = 'c';
288 
290  private const PURGE_VAL_PREFIX = 'PURGED:';
291 
328  public function __construct( array $params ) {
329  $this->cache = $params['cache'];
330  $this->region = $params['region'] ?? 'main';
331  $this->cluster = $params['cluster'] ?? 'wan-main';
332  $this->mcrouterAware = !empty( $params['mcrouterAware'] );
333  $this->epoch = $params['epoch'] ?? 0;
334  $this->secret = $params['secret'] ?? (string)$this->epoch;
335  $this->coalesceKeys = $params['coalesceKeys'] ?? false;
336  if ( !empty( $params['mcrouterAware'] ) ) {
337  // https://github.com/facebook/mcrouter/wiki/Key-syntax
338  $this->coalesceScheme = self::SCHEME_HASH_STOP;
339  } else {
340  // https://redis.io/topics/cluster-spec
341  // https://github.com/twitter/twemproxy/blob/v0.4.1/notes/recommendation.md#hash-tags
342  // https://github.com/Netflix/dynomite/blob/v0.7.0/notes/recommendation.md#hash-tags
343  $this->coalesceScheme = self::SCHEME_HASH_TAG;
344  }
345 
346  $this->keyHighQps = $params['keyHighQps'] ?? 100;
347  $this->keyHighUplinkBps = $params['keyHighUplinkBps'] ?? ( 1e9 / 8 / 100 );
348 
349  $this->setLogger( $params['logger'] ?? new NullLogger() );
350  $this->stats = $params['stats'] ?? new NullStatsdDataFactory();
351  $this->asyncHandler = $params['asyncHandler'] ?? null;
352 
353  $this->cache->registerWrapperInfoForStats(
354  'WANCache',
355  'wanobjectcache',
356  [ __CLASS__, 'getCollectionFromKey' ]
357  );
358  }
359 
363  public function setLogger( LoggerInterface $logger ) {
364  $this->logger = $logger;
365  }
366 
372  public static function newEmpty() {
373  return new static( [ 'cache' => new EmptyBagOStuff() ] );
374  }
375 
430  final public function get( $key, &$curTTL = null, array $checkKeys = [], &$info = [] ) {
431  // Note that an undeclared variable passed as $info starts as null (not the default).
432  // Also, if no $info parameter is provided, then it doesn't matter how it changes here.
433  $legacyInfo = ( $info !== self::PASS_BY_REF );
434 
435  $res = $this->fetchKeys( [ $key ], $checkKeys )[$key];
436  $value = $res[self::RES_VALUE];
437  $metadata = $res[self::RES_METADATA];
438 
439  $curTTL = $metadata[self::KEY_CUR_TTL];
440  $info = $legacyInfo ? $metadata[self::KEY_AS_OF] : $metadata;
441 
442  return $value;
443  }
444 
468  final public function getMulti(
469  array $keys,
470  &$curTTLs = [],
471  array $checkKeys = [],
472  &$info = []
473  ) {
474  // Note that an undeclared variable passed as $info starts as null (not the default).
475  // Also, if no $info parameter is provided, then it doesn't matter how it changes here.
476  $legacyInfo = ( $info !== self::PASS_BY_REF );
477 
478  $curTTLs = [];
479  $info = [];
480  $valuesByKey = [];
481 
482  $resByKey = $this->fetchKeys( $keys, $checkKeys );
483  foreach ( $resByKey as $key => $res ) {
484  $value = $res[self::RES_VALUE];
485  $metadata = $res[self::RES_METADATA];
486 
487  if ( $value !== false ) {
488  $valuesByKey[$key] = $value;
489  }
490 
491  if ( $metadata[self::KEY_CUR_TTL] !== null ) {
492  $curTTLs[$key] = $metadata[self::KEY_CUR_TTL];
493  }
494 
495  $info[$key] = $legacyInfo ? $metadata[self::KEY_AS_OF] : $metadata;
496  }
497 
498  return $valuesByKey;
499  }
500 
517  protected function fetchKeys( array $keys, array $checkKeys ) {
518  $resByKey = [];
519 
520  // Order-corresponding list of value keys for the provided base keys
521  $valueKeys = $this->makeSisterKeys( $keys, self::TYPE_VALUE );
522 
523  $fullKeysNeeded = $valueKeys;
524  $checkKeysForAll = [];
525  $checkKeysByKey = [];
526  foreach ( $checkKeys as $i => $checkKeyOrKeyGroup ) {
527  // Note: avoid array_merge() inside loop in case there are many keys
528  if ( is_int( $i ) ) {
529  // Single check key that applies to all value keys
530  $fullKey = $this->makeSisterKey( $checkKeyOrKeyGroup, self::TYPE_TIMESTAMP );
531  $fullKeysNeeded[] = $fullKey;
532  $checkKeysForAll[] = $fullKey;
533  } else {
534  // List of check keys that apply to a specific value key
535  foreach ( (array)$checkKeyOrKeyGroup as $checkKey ) {
536  $fullKey = $this->makeSisterKey( $checkKey, self::TYPE_TIMESTAMP );
537  $fullKeysNeeded[] = $fullKey;
538  $checkKeysByKey[$i][] = $fullKey;
539  }
540  }
541  }
542 
543  if ( $this->warmupCache ) {
544  // Get the raw values of the keys from the warmup cache
545  $wrappedValues = $this->warmupCache;
546  $fullKeysMissing = array_diff( $fullKeysNeeded, array_keys( $wrappedValues ) );
547  if ( $fullKeysMissing ) { // sanity
548  $this->warmupKeyMisses += count( $fullKeysMissing );
549  $wrappedValues += $this->cache->getMulti( $fullKeysMissing );
550  }
551  } else {
552  // Fetch the raw values of the keys from the backend
553  $wrappedValues = $this->cache->getMulti( $fullKeysNeeded );
554  }
555 
556  // Time used to compare/init "check" keys (derived after getMulti() to be pessimistic)
557  $now = $this->getCurrentTime();
558 
559  // Collect timestamps from all "check" keys
560  $purgeValuesForAll = $this->processCheckKeys( $checkKeysForAll, $wrappedValues, $now );
561  $purgeValuesByKey = [];
562  foreach ( $checkKeysByKey as $cacheKey => $checks ) {
563  $purgeValuesByKey[$cacheKey] = $this->processCheckKeys( $checks, $wrappedValues, $now );
564  }
565 
566  // Get the main cache value for each key and validate them
567  reset( $keys );
568  foreach ( $valueKeys as $i => $vKey ) {
569  // Get the corresponding base key for this value key
570  $key = current( $keys );
571  next( $keys );
572 
573  list( $value, $metadata ) = $this->unwrap(
574  array_key_exists( $vKey, $wrappedValues ) ? $wrappedValues[$vKey] : false,
575  $now
576  );
577  // Force dependent keys to be seen as stale for a while after purging
578  // to reduce race conditions involving stale data getting cached
579  $purgeValues = $purgeValuesForAll;
580  if ( isset( $purgeValuesByKey[$key] ) ) {
581  $purgeValues = array_merge( $purgeValues, $purgeValuesByKey[$key] );
582  }
583 
584  $lastCKPurge = null; // timestamp of the highest check key
585  foreach ( $purgeValues as $purge ) {
586  $lastCKPurge = max( $purge[self::$PURGE_TIME], $lastCKPurge );
587  $safeTimestamp = $purge[self::$PURGE_TIME] + $purge[self::$PURGE_HOLDOFF];
588  if ( $value !== false && $safeTimestamp >= $metadata[self::KEY_AS_OF] ) {
589  // How long ago this value was invalidated by *this* check key
590  $ago = min( $purge[self::$PURGE_TIME] - $now, self::$TINY_NEGATIVE );
591  // How long ago this value was invalidated by *any* known check key
592  $metadata[self::KEY_CUR_TTL] = min( $metadata[self::KEY_CUR_TTL], $ago );
593  }
594  }
595  $metadata[self::KEY_CHECK_AS_OF] = $lastCKPurge;
596 
597  $resByKey[$key] = [
598  self::RES_VALUE => $value,
599  self::RES_METADATA => $metadata
600  ];
601  }
602 
603  return $resByKey;
604  }
605 
613  private function processCheckKeys( array $timeKeys, array $wrappedValues, $now ) {
614  $purgeValues = [];
615  foreach ( $timeKeys as $timeKey ) {
616  $purge = isset( $wrappedValues[$timeKey] )
617  ? $this->parsePurgeValue( $wrappedValues[$timeKey] )
618  : false;
619  if ( $purge === false ) {
620  // Key is not set or malformed; regenerate
621  $newVal = $this->makePurgeValue( $now, self::HOLDOFF_TTL );
622  $this->cache->add( $timeKey, $newVal, self::$CHECK_KEY_TTL );
623  $purge = $this->parsePurgeValue( $newVal );
624  }
625  $purgeValues[] = $purge;
626  }
627 
628  return $purgeValues;
629  }
630 
710  final public function set( $key, $value, $ttl = self::TTL_INDEFINITE, array $opts = [] ) {
711  $now = $this->getCurrentTime();
712  $lag = $opts['lag'] ?? 0;
713  $age = isset( $opts['since'] ) ? max( 0, $now - $opts['since'] ) : 0;
714  $pending = $opts['pending'] ?? false;
715  $lockTSE = $opts['lockTSE'] ?? self::TSE_NONE;
716  $staleTTL = $opts['staleTTL'] ?? self::STALE_TTL_NONE;
717  $creating = $opts['creating'] ?? false;
718  $version = $opts['version'] ?? null;
719  $walltime = $opts['walltime'] ?? null;
720 
721  if ( $ttl < 0 ) {
722  return true; // not cacheable
723  }
724 
725  // Do not cache potentially uncommitted data as it might get rolled back
726  if ( $pending ) {
727  $this->logger->info(
728  'Rejected set() for {cachekey} due to pending writes.',
729  [ 'cachekey' => $key ]
730  );
731 
732  return true; // no-op the write for being unsafe
733  }
734 
735  // Check if there is a risk of caching (stale) data that predates the last delete()
736  // tombstone due to the tombstone having expired. If so, then the behavior should depend
737  // on whether the problem is specific to this regeneration attempt or systemically affects
738  // attempts to regenerate this key. For systemic cases, the cache writes should set a low
739  // TTL so that the value at least remains cacheable. For non-systemic cases, the cache
740  // write can simply be rejected.
741  if ( $age > self::MAX_READ_LAG ) {
742  // Case A: high snapshot lag
743  if ( $walltime === null ) {
744  // Case A0: high snapshot lag without regeneration wall time info.
745  // Probably systemic; use a low TTL to avoid stampedes/uncacheability.
746  $mitigated = 'snapshot lag';
747  $mitigationTTL = self::TTL_SECOND;
748  } elseif ( ( $age - $walltime ) > self::MAX_READ_LAG ) {
749  // Case A1: value regeneration during an already long-running transaction.
750  // Probably non-systemic; rely on a less problematic regeneration attempt.
751  $mitigated = 'snapshot lag (late regeneration)';
752  $mitigationTTL = self::TTL_UNCACHEABLE;
753  } else {
754  // Case A2: value regeneration takes a long time.
755  // Probably systemic; use a low TTL to avoid stampedes/uncacheability.
756  $mitigated = 'snapshot lag (high regeneration time)';
757  $mitigationTTL = self::TTL_SECOND;
758  }
759  } elseif ( $lag === false || $lag > self::MAX_READ_LAG ) {
760  // Case B: high replication lag without high snapshot lag
761  // Probably systemic; use a low TTL to avoid stampedes/uncacheability
762  $mitigated = 'replication lag';
763  $mitigationTTL = self::TTL_LAGGED;
764  } elseif ( ( $lag + $age ) > self::MAX_READ_LAG ) {
765  // Case C: medium length request with medium replication lag
766  // Probably non-systemic; rely on a less problematic regeneration attempt
767  $mitigated = 'read lag';
768  $mitigationTTL = self::TTL_UNCACHEABLE;
769  } else {
770  // New value generated with recent enough data
771  $mitigated = null;
772  $mitigationTTL = null;
773  }
774 
775  if ( $mitigationTTL === self::TTL_UNCACHEABLE ) {
776  $this->logger->warning(
777  "Rejected set() for {cachekey} due to $mitigated.",
778  [ 'cachekey' => $key, 'lag' => $lag, 'age' => $age, 'walltime' => $walltime ]
779  );
780 
781  return true; // no-op the write for being unsafe
782  }
783 
784  // TTL to use in staleness checks (does not effect persistence layer TTL)
785  $logicalTTL = null;
786 
787  if ( $mitigationTTL !== null ) {
788  // New value generated from data that is old enough to be risky
789  if ( $lockTSE >= 0 ) {
790  // Value will have the normal expiry but will be seen as stale sooner
791  $logicalTTL = min( $ttl ?: INF, $mitigationTTL );
792  } else {
793  // Value expires sooner (leaving enough TTL for preemptive refresh)
794  $ttl = min( $ttl ?: INF, max( $mitigationTTL, self::LOW_TTL ) );
795  }
796 
797  $this->logger->warning(
798  "Lowered set() TTL for {cachekey} due to $mitigated.",
799  [ 'cachekey' => $key, 'lag' => $lag, 'age' => $age, 'walltime' => $walltime ]
800  );
801  }
802 
803  // Wrap that value with time/TTL/version metadata
804  $wrapped = $this->wrap( $value, $logicalTTL ?: $ttl, $version, $now, $walltime );
805  $storeTTL = $ttl + $staleTTL;
806 
807  if ( $creating ) {
808  $ok = $this->cache->add(
809  $this->makeSisterKey( $key, self::TYPE_VALUE ),
810  $wrapped,
811  $storeTTL
812  );
813  } else {
814  $ok = $this->cache->merge(
815  $this->makeSisterKey( $key, self::TYPE_VALUE ),
816  static function ( $cache, $key, $cWrapped ) use ( $wrapped ) {
817  // A string value means that it is a tombstone; do nothing in that case
818  return ( is_string( $cWrapped ) ) ? false : $wrapped;
819  },
820  $storeTTL,
821  1 // 1 attempt
822  );
823  }
824 
825  return $ok;
826  }
827 
889  final public function delete( $key, $ttl = self::HOLDOFF_TTL ) {
890  if ( $ttl <= 0 ) {
891  // Publish the purge to all datacenters
892  $ok = $this->relayDelete( $this->makeSisterKey( $key, self::TYPE_VALUE ) );
893  } else {
894  // Publish the purge to all datacenters
895  $ok = $this->relayPurge(
896  $this->makeSisterKey( $key, self::TYPE_VALUE ),
897  $ttl,
898  self::HOLDOFF_TTL_NONE
899  );
900  }
901 
902  $kClass = $this->determineKeyClassForStats( $key );
903  $this->stats->increment( "wanobjectcache.$kClass.delete." . ( $ok ? 'ok' : 'error' ) );
904 
905  return $ok;
906  }
907 
927  final public function getCheckKeyTime( $key ) {
928  return $this->getMultiCheckKeyTime( [ $key ] )[$key];
929  }
930 
992  final public function getMultiCheckKeyTime( array $keys ) {
993  $rawKeys = [];
994  foreach ( $keys as $key ) {
995  $rawKeys[$key] = $this->makeSisterKey( $key, self::TYPE_TIMESTAMP );
996  }
997 
998  $rawValues = $this->cache->getMulti( $rawKeys );
999  $rawValues += array_fill_keys( $rawKeys, false );
1000 
1001  $times = [];
1002  foreach ( $rawKeys as $key => $rawKey ) {
1003  $purge = $this->parsePurgeValue( $rawValues[$rawKey] );
1004  if ( $purge !== false ) {
1005  $time = $purge[self::$PURGE_TIME];
1006  } else {
1007  // Casting assures identical floats for the next getCheckKeyTime() calls
1008  $now = (string)$this->getCurrentTime();
1009  $this->cache->add(
1010  $rawKey,
1011  $this->makePurgeValue( $now, self::HOLDOFF_TTL ),
1013  );
1014  $time = (float)$now;
1015  }
1016 
1017  $times[$key] = $time;
1018  }
1019 
1020  return $times;
1021  }
1022 
1057  final public function touchCheckKey( $key, $holdoff = self::HOLDOFF_TTL ) {
1058  // Publish the purge to all datacenters
1059  $ok = $this->relayPurge(
1060  $this->makeSisterKey( $key, self::TYPE_TIMESTAMP ),
1061  self::$CHECK_KEY_TTL,
1062  $holdoff
1063  );
1064 
1065  $kClass = $this->determineKeyClassForStats( $key );
1066  $this->stats->increment( "wanobjectcache.$kClass.ck_touch." . ( $ok ? 'ok' : 'error' ) );
1067 
1068  return $ok;
1069  }
1070 
1098  final public function resetCheckKey( $key ) {
1099  // Publish the purge to all datacenters
1100  $ok = $this->relayDelete( $this->makeSisterKey( $key, self::TYPE_TIMESTAMP ) );
1101 
1102  $kClass = $this->determineKeyClassForStats( $key );
1103  $this->stats->increment( "wanobjectcache.$kClass.ck_reset." . ( $ok ? 'ok' : 'error' ) );
1104 
1105  return $ok;
1106  }
1107 
1412  final public function getWithSetCallback(
1413  $key, $ttl, $callback, array $opts = [], array $cbParams = []
1414  ) {
1415  $version = $opts['version'] ?? null;
1416  $pcTTL = $opts['pcTTL'] ?? self::TTL_UNCACHEABLE;
1417  $pCache = ( $pcTTL >= 0 )
1418  ? $this->getProcessCache( $opts['pcGroup'] ?? self::PC_PRIMARY )
1419  : null;
1420 
1421  // Use the process cache if requested as long as no outer cache callback is running.
1422  // Nested callback process cache use is not lag-safe with regard to HOLDOFF_TTL since
1423  // process cached values are more lagged than persistent ones as they are not purged.
1424  if ( $pCache && $this->callbackDepth == 0 ) {
1425  $cached = $pCache->get( $this->getProcessCacheKey( $key, $version ), $pcTTL, false );
1426  if ( $cached !== false ) {
1427  $this->logger->debug( "getWithSetCallback($key): process cache hit" );
1428  return $cached;
1429  }
1430  }
1431 
1432  $res = $this->fetchOrRegenerate( $key, $ttl, $callback, $opts, $cbParams );
1433  list( $value, $valueVersion, $curAsOf ) = $res;
1434  if ( $valueVersion !== $version ) {
1435  // Current value has a different version; use the variant key for this version.
1436  // Regenerate the variant value if it is not newer than the main value at $key
1437  // so that purges to the main key propagate to the variant value.
1438  $this->logger->debug( "getWithSetCallback($key): using variant key" );
1439  list( $value ) = $this->fetchOrRegenerate(
1440  $this->makeGlobalKey( 'WANCache-key-variant', md5( $key ), $version ),
1441  $ttl,
1442  $callback,
1443  [ 'version' => null, 'minAsOf' => $curAsOf ] + $opts,
1444  $cbParams
1445  );
1446  }
1447 
1448  // Update the process cache if enabled
1449  if ( $pCache && $value !== false ) {
1450  $pCache->set( $this->getProcessCacheKey( $key, $version ), $value );
1451  }
1452 
1453  return $value;
1454  }
1455 
1472  private function fetchOrRegenerate( $key, $ttl, $callback, array $opts, array $cbParams ) {
1473  $checkKeys = $opts['checkKeys'] ?? [];
1474  $graceTTL = $opts['graceTTL'] ?? self::GRACE_TTL_NONE;
1475  $minAsOf = $opts['minAsOf'] ?? self::MIN_TIMESTAMP_NONE;
1476  $hotTTR = $opts['hotTTR'] ?? self::HOT_TTR;
1477  $lowTTL = $opts['lowTTL'] ?? min( self::LOW_TTL, $ttl );
1478  $ageNew = $opts['ageNew'] ?? self::AGE_NEW;
1479  $touchedCb = $opts['touchedCallback'] ?? null;
1480  $initialTime = $this->getCurrentTime();
1481 
1482  $kClass = $this->determineKeyClassForStats( $key );
1483 
1484  // Get the current key value and its metadata
1485  $res = $this->fetchKeys( [ $key ], $checkKeys )[$key];
1486  $curValue = $res[self::RES_VALUE];
1487  $curInfo = $res[self::RES_METADATA];
1488  $curTTL = $curInfo[self::KEY_CUR_TTL];
1489  // Apply any $touchedCb invalidation timestamp to get the "last purge timestamp"
1490  list( $curTTL, $LPT ) = $this->resolveCTL( $curValue, $curTTL, $curInfo, $touchedCb );
1491  // Use the cached value if it exists and is not due for synchronous regeneration
1492  if (
1493  $this->isValid( $curValue, $curInfo[self::KEY_AS_OF], $minAsOf ) &&
1494  $this->isAliveOrInGracePeriod( $curTTL, $graceTTL )
1495  ) {
1496  $preemptiveRefresh = (
1497  $this->worthRefreshExpiring( $curTTL, $curInfo[self::KEY_TTL], $lowTTL ) ||
1498  $this->worthRefreshPopular( $curInfo['asOf'], $ageNew, $hotTTR, $initialTime )
1499  );
1500  if ( !$preemptiveRefresh ) {
1501  $this->stats->timing(
1502  "wanobjectcache.$kClass.hit.good",
1503  1e3 * ( $this->getCurrentTime() - $initialTime )
1504  );
1505 
1506  return [ $curValue, $curInfo[self::KEY_VERSION], $curInfo[self::KEY_AS_OF] ];
1507  } elseif ( $this->scheduleAsyncRefresh( $key, $ttl, $callback, $opts, $cbParams ) ) {
1508  $this->logger->debug( "fetchOrRegenerate($key): hit with async refresh" );
1509  $this->stats->timing(
1510  "wanobjectcache.$kClass.hit.refresh",
1511  1e3 * ( $this->getCurrentTime() - $initialTime )
1512  );
1513 
1514  return [ $curValue, $curInfo[self::KEY_VERSION], $curInfo[self::KEY_AS_OF] ];
1515  } else {
1516  $this->logger->debug( "fetchOrRegenerate($key): hit with sync refresh" );
1517  }
1518  }
1519 
1520  // Determine if there is stale or volatile cached value that is still usable
1521  $isKeyTombstoned = ( $curInfo[self::KEY_TOMB_AS_OF] !== null );
1522  if ( $isKeyTombstoned ) {
1523  // Key is write-holed; use the (volatile) interim key as an alternative
1524  list( $possValue, $possInfo ) = $this->getInterimValue( $key, $minAsOf );
1525  // Update the "last purge time" since the $touchedCb timestamp depends on $value
1526  $LPT = $this->resolveTouched( $possValue, $LPT, $touchedCb );
1527  } else {
1528  $possValue = $curValue;
1529  $possInfo = $curInfo;
1530  }
1531 
1532  // Avoid overhead from callback runs, regeneration locks, and cache sets during
1533  // hold-off periods for the key by reusing very recently generated cached values
1534  if (
1535  $this->isValid( $possValue, $possInfo[self::KEY_AS_OF], $minAsOf, $LPT ) &&
1536  $this->isVolatileValueAgeNegligible( $initialTime - $possInfo[self::KEY_AS_OF] )
1537  ) {
1538  $this->logger->debug( "fetchOrRegenerate($key): volatile hit" );
1539  $this->stats->timing(
1540  "wanobjectcache.$kClass.hit.volatile",
1541  1e3 * ( $this->getCurrentTime() - $initialTime )
1542  );
1543 
1544  return [ $possValue, $possInfo[self::KEY_VERSION], $curInfo[self::KEY_AS_OF] ];
1545  }
1546 
1547  $lockTSE = $opts['lockTSE'] ?? self::TSE_NONE;
1548  $busyValue = $opts['busyValue'] ?? null;
1549  $staleTTL = $opts['staleTTL'] ?? self::STALE_TTL_NONE;
1550  $version = $opts['version'] ?? null;
1551 
1552  // Determine whether one thread per datacenter should handle regeneration at a time
1553  $useRegenerationLock =
1554  // Note that since tombstones no-op set(), $lockTSE and $curTTL cannot be used to
1555  // deduce the key hotness because |$curTTL| will always keep increasing until the
1556  // tombstone expires or is overwritten by a new tombstone. Also, even if $lockTSE
1557  // is not set, constant regeneration of a key for the tombstone lifetime might be
1558  // very expensive. Assume tombstoned keys are possibly hot in order to reduce
1559  // the risk of high regeneration load after the delete() method is called.
1560  $isKeyTombstoned ||
1561  // Assume a key is hot if requested soon ($lockTSE seconds) after invalidation.
1562  // This avoids stampedes when timestamps from $checkKeys/$touchedCb bump.
1563  ( $curTTL !== null && $curTTL <= 0 && abs( $curTTL ) <= $lockTSE ) ||
1564  // Assume a key is hot if there is no value and a busy fallback is given.
1565  // This avoids stampedes on eviction or preemptive regeneration taking too long.
1566  ( $busyValue !== null && $possValue === false );
1567 
1568  // If a regeneration lock is required, threads that do not get the lock will try to use
1569  // the stale value, the interim value, or the $busyValue placeholder, in that order. If
1570  // none of those are set then all threads will bypass the lock and regenerate the value.
1571  $hasLock = $useRegenerationLock && $this->claimStampedeLock( $key );
1572  if ( $useRegenerationLock && !$hasLock ) {
1573  if ( $this->isValid( $possValue, $possInfo[self::KEY_AS_OF], $minAsOf ) ) {
1574  $this->logger->debug( "fetchOrRegenerate($key): returning stale value" );
1575  $this->stats->timing(
1576  "wanobjectcache.$kClass.hit.stale",
1577  1e3 * ( $this->getCurrentTime() - $initialTime )
1578  );
1579 
1580  return [ $possValue, $possInfo[self::KEY_VERSION], $curInfo[self::KEY_AS_OF] ];
1581  } elseif ( $busyValue !== null ) {
1582  $miss = is_infinite( $minAsOf ) ? 'renew' : 'miss';
1583  $this->logger->debug( "fetchOrRegenerate($key): busy $miss" );
1584  $this->stats->timing(
1585  "wanobjectcache.$kClass.$miss.busy",
1586  1e3 * ( $this->getCurrentTime() - $initialTime )
1587  );
1588  $placeholderValue = $this->resolveBusyValue( $busyValue );
1589 
1590  return [ $placeholderValue, $version, $curInfo[self::KEY_AS_OF] ];
1591  }
1592  }
1593 
1594  // Generate the new value given any prior value with a matching version
1595  $setOpts = [];
1596  $preCallbackTime = $this->getCurrentTime();
1598  try {
1599  $value = $callback(
1600  ( $curInfo[self::KEY_VERSION] === $version ) ? $curValue : false,
1601  $ttl,
1602  $setOpts,
1603  ( $curInfo[self::KEY_VERSION] === $version ) ? $curInfo[self::KEY_AS_OF] : null,
1604  $cbParams
1605  );
1606  } finally {
1608  }
1609  $postCallbackTime = $this->getCurrentTime();
1610 
1611  // How long it took to fetch, validate, and generate the value
1612  $elapsed = max( $postCallbackTime - $initialTime, 0.0 );
1613 
1614  // Attempt to save the newly generated value if applicable
1615  if (
1616  // Callback yielded a cacheable value
1617  ( $value !== false && $ttl >= 0 ) &&
1618  // Current thread was not raced out of a regeneration lock or key is tombstoned
1619  ( !$useRegenerationLock || $hasLock || $isKeyTombstoned ) &&
1620  // Key does not appear to be undergoing a set() stampede
1621  $this->checkAndSetCooloff( $key, $kClass, $value, $elapsed, $hasLock )
1622  ) {
1623  // How long it took to generate the value
1624  $walltime = max( $postCallbackTime - $preCallbackTime, 0.0 );
1625  $this->stats->timing( "wanobjectcache.$kClass.regen_walltime", 1e3 * $walltime );
1626  // If the key is write-holed then use the (volatile) interim key as an alternative
1627  if ( $isKeyTombstoned ) {
1628  $this->setInterimValue( $key, $value, $lockTSE, $version, $walltime );
1629  } else {
1630  $finalSetOpts = [
1631  // @phan-suppress-next-line PhanUselessBinaryAddRight
1632  'since' => $setOpts['since'] ?? $preCallbackTime,
1633  'version' => $version,
1634  'staleTTL' => $staleTTL,
1635  'lockTSE' => $lockTSE, // informs lag vs performance trade-offs
1636  'creating' => ( $curValue === false ), // optimization
1637  'walltime' => $walltime
1638  ] + $setOpts;
1639  $this->set( $key, $value, $ttl, $finalSetOpts );
1640  }
1641  }
1642 
1643  $this->yieldStampedeLock( $key, $hasLock );
1644 
1645  $miss = is_infinite( $minAsOf ) ? 'renew' : 'miss';
1646  $this->logger->debug( "fetchOrRegenerate($key): $miss, new value computed" );
1647  $this->stats->timing(
1648  "wanobjectcache.$kClass.$miss.compute",
1649  1e3 * ( $this->getCurrentTime() - $initialTime )
1650  );
1651 
1652  return [ $value, $version, $curInfo[self::KEY_AS_OF] ];
1653  }
1654 
1659  private function claimStampedeLock( $key ) {
1660  // Note that locking is not bypassed due to I/O errors; this avoids stampedes
1661  return $this->cache->add(
1662  $this->makeSisterKey( $key, self::TYPE_MUTEX ),
1663  1,
1664  self::$LOCK_TTL
1665  );
1666  }
1667 
1672  private function yieldStampedeLock( $key, $hasLock ) {
1673  if ( $hasLock ) {
1674  // The backend might be a mcrouter proxy set to broadcast DELETE to *all* the local
1675  // datacenter cache servers via OperationSelectorRoute (for increased consistency).
1676  // Since that would be excessive for these locks, use TOUCH to expire the key.
1677  $this->cache->changeTTL(
1678  $this->makeSisterKey( $key, self::TYPE_MUTEX ),
1679  $this->getCurrentTime() - 60
1680  );
1681  }
1682  }
1683 
1691  private function makeSisterKeys( array $baseKeys, $type ) {
1692  $keys = [];
1693  foreach ( $baseKeys as $baseKey ) {
1694  $keys[] = $this->makeSisterKey( $baseKey, $type );
1695  }
1696 
1697  return $keys;
1698  }
1699 
1707  private function makeSisterKey( $baseKey, $typeChar ) {
1708  if ( $this->coalesceKeys === 'non-global' ) {
1709  $useColocationScheme = ( strncmp( $baseKey, "global:", 7 ) !== 0 );
1710  } else {
1711  $useColocationScheme = ( $this->coalesceKeys === true );
1712  }
1713 
1714  if ( !$useColocationScheme ) {
1715  // Old key style: "WANCache:<character>:<base key>"
1716  $fullKey = 'WANCache:' . $typeChar . ':' . $baseKey;
1717  } elseif ( $this->coalesceScheme === self::SCHEME_HASH_STOP ) {
1718  // Key style: "WANCache:<base key>|#|<character>"
1719  $fullKey = 'WANCache:' . $baseKey . '|#|' . $typeChar;
1720  } else {
1721  // Key style: "WANCache:{<base key>}:<character>"
1722  $fullKey = 'WANCache:{' . $baseKey . '}:' . $typeChar;
1723  }
1724 
1725  return $fullKey;
1726  }
1727 
1734  public static function getCollectionFromKey( string $sisterKey ) {
1735  if ( substr( $sisterKey, -4 ) === '|#|v' ) {
1736  // Key style: "WANCache:<base key>|#|<character>"
1737  $collection = substr( $sisterKey, 9, strcspn( $sisterKey, ':|', 9 ) );
1738  } elseif ( substr( $sisterKey, -3 ) === '}:v' ) {
1739  // Key style: "WANCache:{<base key>}:<character>"
1740  $collection = substr( $sisterKey, 10, strcspn( $sisterKey, ':}', 10 ) );
1741  } elseif ( substr( $sisterKey, 9, 2 ) === 'v:' ) {
1742  // Old key style: "WANCache:<character>:<base key>"
1743  $collection = substr( $sisterKey, 11, strcspn( $sisterKey, ':', 11 ) );
1744  } else {
1745  $collection = 'internal';
1746  }
1747 
1748  return $collection;
1749  }
1750 
1755  private function isVolatileValueAgeNegligible( $age ) {
1756  return ( $age < mt_rand( self::$RECENT_SET_LOW_MS, self::$RECENT_SET_HIGH_MS ) / 1e3 );
1757  }
1758 
1780  private function checkAndSetCooloff( $key, $kClass, $value, $elapsed, $hasLock ) {
1781  $valueKey = $this->makeSisterKey( $key, self::TYPE_VALUE );
1782  list( $estimatedSize ) = $this->cache->setNewPreparedValues( [ $valueKey => $value ] );
1783 
1784  if ( !$hasLock ) {
1785  // Suppose that this cache key is very popular (KEY_HIGH_QPS reads/second).
1786  // After eviction, there will be cache misses until it gets regenerated and saved.
1787  // If the time window when the key is missing lasts less than one second, then the
1788  // number of misses will not reach KEY_HIGH_QPS. This window largely corresponds to
1789  // the key regeneration time. Estimate the count/rate of cache misses, e.g.:
1790  // - 100 QPS, 20ms regeneration => ~2 misses (< 1s)
1791  // - 100 QPS, 100ms regeneration => ~10 misses (< 1s)
1792  // - 100 QPS, 3000ms regeneration => ~300 misses (100/s for 3s)
1793  $missesPerSecForHighQPS = ( min( $elapsed, 1 ) * $this->keyHighQps );
1794 
1795  // Determine whether there is enough I/O stampede risk to justify throttling set().
1796  // Estimate unthrottled set() overhead, as bps, from miss count/rate and value size,
1797  // comparing it to the per-key uplink bps limit (KEY_HIGH_UPLINK_BPS), e.g.:
1798  // - 2 misses (< 1s), 10KB value, 1250000 bps limit => 160000 bits (low risk)
1799  // - 2 misses (< 1s), 100KB value, 1250000 bps limit => 1600000 bits (high risk)
1800  // - 10 misses (< 1s), 10KB value, 1250000 bps limit => 800000 bits (low risk)
1801  // - 10 misses (< 1s), 100KB value, 1250000 bps limit => 8000000 bits (high risk)
1802  // - 300 misses (100/s), 1KB value, 1250000 bps limit => 800000 bps (low risk)
1803  // - 300 misses (100/s), 10KB value, 1250000 bps limit => 8000000 bps (high risk)
1804  // - 300 misses (100/s), 100KB value, 1250000 bps limit => 80000000 bps (high risk)
1805  if ( ( $missesPerSecForHighQPS * $estimatedSize ) >= $this->keyHighUplinkBps ) {
1806  $this->cache->clearLastError();
1807  if (
1808  !$this->cache->add(
1809  $this->makeSisterKey( $key, self::TYPE_COOLOFF ),
1810  1,
1811  self::$COOLOFF_TTL
1812  ) &&
1813  // Don't treat failures due to I/O errors as the key being in cooloff
1814  $this->cache->getLastError() === BagOStuff::ERR_NONE
1815  ) {
1816  $this->stats->increment( "wanobjectcache.$kClass.cooloff_bounce" );
1817 
1818  return false;
1819  }
1820  }
1821  }
1822 
1823  // Corresponding metrics for cache writes that actually get sent over the write
1824  $this->stats->timing( "wanobjectcache.$kClass.regen_set_delay", 1e3 * $elapsed );
1825  $this->stats->updateCount( "wanobjectcache.$kClass.regen_set_bytes", $estimatedSize );
1826 
1827  return true;
1828  }
1829 
1838  private function resolveCTL( $value, $curTTL, $curInfo, $touchedCallback ) {
1839  if ( $touchedCallback === null || $value === false ) {
1840  return [
1841  $curTTL,
1842  max( $curInfo[self::KEY_TOMB_AS_OF], $curInfo[self::KEY_CHECK_AS_OF] )
1843  ];
1844  }
1845 
1846  $touched = $touchedCallback( $value );
1847  if ( $touched !== null && $touched >= $curInfo[self::KEY_AS_OF] ) {
1848  $curTTL = min( $curTTL, self::$TINY_NEGATIVE, $curInfo[self::KEY_AS_OF] - $touched );
1849  }
1850 
1851  return [
1852  $curTTL,
1853  max(
1854  $curInfo[self::KEY_TOMB_AS_OF],
1855  $curInfo[self::KEY_CHECK_AS_OF],
1856  $touched
1857  )
1858  ];
1859  }
1860 
1868  private function resolveTouched( $value, $lastPurge, $touchedCallback ) {
1869  return ( $touchedCallback === null || $value === false )
1870  ? $lastPurge // nothing to derive the "touched timestamp" from
1871  : max( $touchedCallback( $value ), $lastPurge );
1872  }
1873 
1879  private function getInterimValue( $key, $minAsOf ) {
1880  $now = $this->getCurrentTime();
1881 
1882  if ( $this->useInterimHoldOffCaching ) {
1883  $wrapped = $this->cache->get(
1884  $this->makeSisterKey( $key, self::TYPE_INTERIM )
1885  );
1886 
1887  list( $value, $keyInfo ) = $this->unwrap( $wrapped, $now );
1888  if ( $this->isValid( $value, $keyInfo[self::KEY_AS_OF], $minAsOf ) ) {
1889  return [ $value, $keyInfo ];
1890  }
1891  }
1892 
1893  return $this->unwrap( false, $now );
1894  }
1895 
1903  private function setInterimValue( $key, $value, $ttl, $version, $walltime ) {
1904  $ttl = max( self::$INTERIM_KEY_TTL, (int)$ttl );
1905 
1906  $wrapped = $this->wrap( $value, $ttl, $version, $this->getCurrentTime(), $walltime );
1907  $this->cache->merge(
1908  $this->makeSisterKey( $key, self::TYPE_INTERIM ),
1909  static function () use ( $wrapped ) {
1910  return $wrapped;
1911  },
1912  $ttl,
1913  1
1914  );
1915  }
1916 
1921  private function resolveBusyValue( $busyValue ) {
1922  return ( $busyValue instanceof Closure ) ? $busyValue() : $busyValue;
1923  }
1924 
1989  final public function getMultiWithSetCallback(
1990  ArrayIterator $keyedIds, $ttl, callable $callback, array $opts = []
1991  ) {
1992  // Load required keys into process cache in one go
1993  $this->warmupCache = $this->getRawKeysForWarmup(
1994  $this->getNonProcessCachedMultiKeys( $keyedIds, $opts ),
1995  $opts['checkKeys'] ?? []
1996  );
1997  $this->warmupKeyMisses = 0;
1998 
1999  // The required callback signature includes $id as the first argument for convenience
2000  // to distinguish different items. To reuse the code in getWithSetCallback(), wrap the
2001  // callback with a proxy callback that has the standard getWithSetCallback() signature.
2002  // This is defined only once per batch to avoid closure creation overhead.
2003  $proxyCb = static function ( $oldValue, &$ttl, &$setOpts, $oldAsOf, $params ) use ( $callback ) {
2004  return $callback( $params['id'], $oldValue, $ttl, $setOpts, $oldAsOf );
2005  };
2006 
2007  $values = [];
2008  foreach ( $keyedIds as $key => $id ) { // preserve order
2009  $values[$key] = $this->getWithSetCallback(
2010  $key,
2011  $ttl,
2012  $proxyCb,
2013  $opts,
2014  [ 'id' => $id ]
2015  );
2016  }
2017 
2018  $this->warmupCache = [];
2019 
2020  return $values;
2021  }
2022 
2088  final public function getMultiWithUnionSetCallback(
2089  ArrayIterator $keyedIds, $ttl, callable $callback, array $opts = []
2090  ) {
2091  $checkKeys = $opts['checkKeys'] ?? [];
2092  unset( $opts['lockTSE'] ); // incompatible
2093  unset( $opts['busyValue'] ); // incompatible
2094 
2095  // Load required keys into process cache in one go
2096  $keysByIdGet = $this->getNonProcessCachedMultiKeys( $keyedIds, $opts );
2097  $this->warmupCache = $this->getRawKeysForWarmup( $keysByIdGet, $checkKeys );
2098  $this->warmupKeyMisses = 0;
2099 
2100  // IDs of entities known to be in need of regeneration
2101  $idsRegen = [];
2102 
2103  // Find out which keys are missing/deleted/stale
2104  $resByKey = $this->fetchKeys( $keysByIdGet, $checkKeys );
2105  foreach ( $keysByIdGet as $id => $key ) {
2106  $res = $resByKey[$key];
2107  $value = $res[self::RES_VALUE];
2108  $metadata = $res[self::RES_METADATA];
2109  if ( $value === false || $metadata[self::KEY_CUR_TTL] < 0 ) {
2110  $idsRegen[] = $id;
2111  }
2112  }
2113 
2114  // Run the callback to populate the regeneration value map for all required IDs
2115  $newSetOpts = [];
2116  $newTTLsById = array_fill_keys( $idsRegen, $ttl );
2117  $newValsById = $idsRegen ? $callback( $idsRegen, $newTTLsById, $newSetOpts ) : [];
2118 
2119  // The required callback signature includes $id as the first argument for convenience
2120  // to distinguish different items. To reuse the code in getWithSetCallback(), wrap the
2121  // callback with a proxy callback that has the standard getWithSetCallback() signature.
2122  // This is defined only once per batch to avoid closure creation overhead.
2123  $proxyCb = static function ( $oldValue, &$ttl, &$setOpts, $oldAsOf, $params )
2124  use ( $callback, $newValsById, $newTTLsById, $newSetOpts )
2125  {
2126  $id = $params['id'];
2127 
2128  if ( array_key_exists( $id, $newValsById ) ) {
2129  // Value was already regerated as expected, so use the value in $newValsById
2130  $newValue = $newValsById[$id];
2131  $ttl = $newTTLsById[$id];
2132  $setOpts = $newSetOpts;
2133  } else {
2134  // Pre-emptive/popularity refresh and version mismatch cases are not detected
2135  // above and thus $newValsById has no entry. Run $callback on this single entity.
2136  $ttls = [ $id => $ttl ];
2137  $newValue = $callback( [ $id ], $ttls, $setOpts )[$id];
2138  $ttl = $ttls[$id];
2139  }
2140 
2141  return $newValue;
2142  };
2143 
2144  // Run the cache-aside logic using warmupCache instead of persistent cache queries
2145  $values = [];
2146  foreach ( $keyedIds as $key => $id ) { // preserve order
2147  $values[$key] = $this->getWithSetCallback(
2148  $key,
2149  $ttl,
2150  $proxyCb,
2151  $opts,
2152  [ 'id' => $id ]
2153  );
2154  }
2155 
2156  $this->warmupCache = [];
2157 
2158  return $values;
2159  }
2160 
2173  final public function reap( $key, $purgeTimestamp, &$isStale = false ) {
2174  $minAsOf = $purgeTimestamp + self::HOLDOFF_TTL;
2175  $wrapped = $this->cache->get( $this->makeSisterKey( $key, self::TYPE_VALUE ) );
2176  if ( is_array( $wrapped ) && $wrapped[self::$FLD_TIME] < $minAsOf ) {
2177  $isStale = true;
2178  $this->logger->warning( "Reaping stale value key '$key'." );
2179  $ttlReap = self::HOLDOFF_TTL; // avoids races with tombstone creation
2180  $ok = $this->cache->changeTTL(
2181  $this->makeSisterKey( $key, self::TYPE_VALUE ),
2182  $ttlReap
2183  );
2184  if ( !$ok ) {
2185  $this->logger->error( "Could not complete reap of key '$key'." );
2186  }
2187 
2188  return $ok;
2189  }
2190 
2191  $isStale = false;
2192 
2193  return true;
2194  }
2195 
2205  final public function reapCheckKey( $key, $purgeTimestamp, &$isStale = false ) {
2206  $purge = $this->parsePurgeValue(
2207  $this->cache->get( $this->makeSisterKey( $key, self::TYPE_TIMESTAMP ) )
2208  );
2209  if ( $purge && $purge[self::$PURGE_TIME] < $purgeTimestamp ) {
2210  $isStale = true;
2211  $this->logger->warning( "Reaping stale check key '$key'." );
2212  $ok = $this->cache->changeTTL(
2213  $this->makeSisterKey( $key, self::TYPE_TIMESTAMP ),
2214  self::TTL_SECOND
2215  );
2216  if ( !$ok ) {
2217  $this->logger->error( "Could not complete reap of check key '$key'." );
2218  }
2219 
2220  return $ok;
2221  }
2222 
2223  $isStale = false;
2224 
2225  return false;
2226  }
2227 
2238  public function makeGlobalKey( $collection, ...$components ) {
2239  return $this->cache->makeGlobalKey( ...func_get_args() );
2240  }
2241 
2252  public function makeKey( $collection, ...$components ) {
2253  return $this->cache->makeKey( ...func_get_args() );
2254  }
2255 
2263  public function hash256( $component ) {
2264  return hash_hmac( 'sha256', $component, $this->secret );
2265  }
2266 
2317  final public function makeMultiKeys( array $ids, $keyCallback ) {
2318  $idByKey = [];
2319  foreach ( $ids as $id ) {
2320  // Discourage triggering of automatic makeKey() hashing in some backends
2321  if ( strlen( $id ) > 64 ) {
2322  $this->logger->warning( __METHOD__ . ": long ID '$id'; use hash256()" );
2323  }
2324  $key = $keyCallback( $id, $this );
2325  // Edge case: ignore key collisions due to duplicate $ids like "42" and 42
2326  if ( !isset( $idByKey[$key] ) ) {
2327  $idByKey[$key] = $id;
2328  } elseif ( (string)$id !== (string)$idByKey[$key] ) {
2329  throw new UnexpectedValueException(
2330  "Cache key collision; IDs ('$id','{$idByKey[$key]}') map to '$key'"
2331  );
2332  }
2333  }
2334 
2335  return new ArrayIterator( $idByKey );
2336  }
2337 
2373  final public function multiRemap( array $ids, array $res ) {
2374  if ( count( $ids ) !== count( $res ) ) {
2375  // If makeMultiKeys() is called on a list of non-unique IDs, then the resulting
2376  // ArrayIterator will have less entries due to "first appearance" de-duplication
2377  $ids = array_keys( array_flip( $ids ) );
2378  if ( count( $ids ) !== count( $res ) ) {
2379  throw new UnexpectedValueException( "Multi-key result does not match ID list" );
2380  }
2381  }
2382 
2383  return array_combine( $ids, $res );
2384  }
2385 
2390  final public function getLastError() {
2391  $code = $this->cache->getLastError();
2392  switch ( $code ) {
2393  case BagOStuff::ERR_NONE:
2394  return self::ERR_NONE;
2395  case BagOStuff::ERR_NO_RESPONSE:
2396  return self::ERR_NO_RESPONSE;
2397  case BagOStuff::ERR_UNREACHABLE:
2398  return self::ERR_UNREACHABLE;
2399  default:
2400  return self::ERR_UNEXPECTED;
2401  }
2402  }
2403 
2407  final public function clearLastError() {
2408  $this->cache->clearLastError();
2409  }
2410 
2416  public function clearProcessCache() {
2417  $this->processCaches = [];
2418  }
2419 
2440  final public function useInterimHoldOffCaching( $enabled ) {
2441  $this->useInterimHoldOffCaching = $enabled;
2442  }
2443 
2449  public function getQoS( $flag ) {
2450  return $this->cache->getQoS( $flag );
2451  }
2452 
2516  public function adaptiveTTL( $mtime, $maxTTL, $minTTL = 30, $factor = 0.2 ) {
2517  if ( is_float( $mtime ) || ctype_digit( $mtime ) ) {
2518  $mtime = (int)$mtime; // handle fractional seconds and string integers
2519  }
2520 
2521  if ( !is_int( $mtime ) || $mtime <= 0 ) {
2522  return $minTTL; // no last-modified time provided
2523  }
2524 
2525  $age = $this->getCurrentTime() - $mtime;
2526 
2527  return (int)min( $maxTTL, max( $minTTL, $factor * $age ) );
2528  }
2529 
2534  final public function getWarmupKeyMisses() {
2535  return $this->warmupKeyMisses;
2536  }
2537 
2548  protected function relayPurge( $key, $ttl, $holdoff ) {
2549  if ( $this->mcrouterAware ) {
2550  // See https://github.com/facebook/mcrouter/wiki/Multi-cluster-broadcast-setup
2551  // Wildcards select all matching routes, e.g. the WAN cluster on all DCs
2552  $ok = $this->cache->set(
2553  "/*/{$this->cluster}/{$key}",
2554  $this->makePurgeValue( $this->getCurrentTime(), $holdoff ),
2555  $ttl
2556  );
2557  } else {
2558  // Some other proxy handles broadcasting or there is only one datacenter
2559  $ok = $this->cache->set(
2560  $key,
2561  $this->makePurgeValue( $this->getCurrentTime(), $holdoff ),
2562  $ttl
2563  );
2564  }
2565 
2566  return $ok;
2567  }
2568 
2575  protected function relayDelete( $key ) {
2576  if ( $this->mcrouterAware ) {
2577  // See https://github.com/facebook/mcrouter/wiki/Multi-cluster-broadcast-setup
2578  // Wildcards select all matching routes, e.g. the WAN cluster on all DCs
2579  $ok = $this->cache->delete( "/*/{$this->cluster}/{$key}" );
2580  } else {
2581  // Some other proxy handles broadcasting or there is only one datacenter
2582  $ok = $this->cache->delete( $key );
2583  }
2584 
2585  return $ok;
2586  }
2587 
2599  private function scheduleAsyncRefresh( $key, $ttl, $callback, array $opts, array $cbParams ) {
2600  if ( !$this->asyncHandler ) {
2601  return false;
2602  }
2603  // Update the cache value later, such during post-send of an HTTP request. This forces
2604  // cache regeneration by setting "minAsOf" to infinity, meaning that no existing value
2605  // is considered valid. Furthermore, note that preemptive regeneration is not applicable
2606  // to invalid values, so there is no risk of infinite preemptive regeneration loops.
2607  $func = $this->asyncHandler;
2608  $func( function () use ( $key, $ttl, $callback, $opts, $cbParams ) {
2609  $opts['minAsOf'] = INF;
2610  try {
2611  $this->fetchOrRegenerate( $key, $ttl, $callback, $opts, $cbParams );
2612  } catch ( Exception $e ) {
2613  // Log some context for easier debugging
2614  $this->logger->error( 'Async refresh failed for {key}', [
2615  'key' => $key,
2616  'ttl' => $ttl,
2617  'exception' => $e
2618  ] );
2619  throw $e;
2620  }
2621  } );
2622 
2623  return true;
2624  }
2625 
2640  private function isAliveOrInGracePeriod( $curTTL, $graceTTL ) {
2641  if ( $curTTL > 0 ) {
2642  return true;
2643  } elseif ( $graceTTL <= 0 ) {
2644  return false;
2645  }
2646 
2647  $ageStale = abs( $curTTL ); // seconds of staleness
2648  $curGraceTTL = ( $graceTTL - $ageStale ); // current grace-time-to-live
2649  if ( $curGraceTTL <= 0 ) {
2650  return false; // already out of grace period
2651  }
2652 
2653  // Chance of using a stale value is the complement of the chance of refreshing it
2654  return !$this->worthRefreshExpiring( $curGraceTTL, $graceTTL, $graceTTL );
2655  }
2656 
2674  protected function worthRefreshExpiring( $curTTL, $logicalTTL, $lowTTL ) {
2675  if ( $lowTTL <= 0 ) {
2676  return false;
2677  }
2678 
2679  // T264787: avoid having keys start off with a high chance of being refreshed;
2680  // the point where refreshing becomes possible cannot precede the key lifetime.
2681  $effectiveLowTTL = min( $lowTTL, $logicalTTL ?: INF );
2682 
2683  if ( $curTTL >= $effectiveLowTTL || $curTTL <= 0 ) {
2684  return false;
2685  }
2686 
2687  $chance = ( 1 - $curTTL / $effectiveLowTTL );
2688 
2689  // @phan-suppress-next-line PhanTypeMismatchArgumentInternal
2690  $decision = ( mt_rand( 1, 1e9 ) <= 1e9 * $chance );
2691 
2692  $this->logger->debug(
2693  "worthRefreshExpiring($curTTL, $logicalTTL, $lowTTL): " .
2694  "p = $chance; refresh = " . ( $decision ? 'Y' : 'N' )
2695  );
2696 
2697  return $decision;
2698  }
2699 
2715  protected function worthRefreshPopular( $asOf, $ageNew, $timeTillRefresh, $now ) {
2716  if ( $ageNew < 0 || $timeTillRefresh <= 0 ) {
2717  return false;
2718  }
2719 
2720  $age = $now - $asOf;
2721  $timeOld = $age - $ageNew;
2722  if ( $timeOld <= 0 ) {
2723  return false;
2724  }
2725 
2726  $popularHitsPerSec = 1;
2727  // Lifecycle is: new, ramp-up refresh chance, full refresh chance.
2728  // Note that the "expected # of refreshes" for the ramp-up time range is half
2729  // of what it would be if P(refresh) was at its full value during that time range.
2730  $refreshWindowSec = max( $timeTillRefresh - $ageNew - self::$RAMPUP_TTL / 2, 1 );
2731  // P(refresh) * (# hits in $refreshWindowSec) = (expected # of refreshes)
2732  // P(refresh) * ($refreshWindowSec * $popularHitsPerSec) = 1 (by definition)
2733  // P(refresh) = 1/($refreshWindowSec * $popularHitsPerSec)
2734  $chance = 1 / ( $popularHitsPerSec * $refreshWindowSec );
2735  // Ramp up $chance from 0 to its nominal value over RAMPUP_TTL seconds to avoid stampedes
2736  $chance *= ( $timeOld <= self::$RAMPUP_TTL ) ? $timeOld / self::$RAMPUP_TTL : 1;
2737 
2738  // @phan-suppress-next-line PhanTypeMismatchArgumentInternal
2739  $decision = ( mt_rand( 1, 1e9 ) <= 1e9 * $chance );
2740 
2741  $this->logger->debug(
2742  "worthRefreshPopular($asOf, $ageNew, $timeTillRefresh, $now): " .
2743  "p = $chance; refresh = " . ( $decision ? 'Y' : 'N' )
2744  );
2745 
2746  return $decision;
2747  }
2748 
2758  protected function isValid( $value, $asOf, $minAsOf, $purgeTime = null ) {
2759  // Avoid reading any key not generated after the latest delete() or touch
2760  $safeMinAsOf = max( $minAsOf, $purgeTime + self::$TINY_POSTIVE );
2761 
2762  if ( $value === false ) {
2763  return false;
2764  } elseif ( $safeMinAsOf > 0 && $asOf < $minAsOf ) {
2765  return false;
2766  }
2767 
2768  return true;
2769  }
2770 
2779  private function wrap( $value, $ttl, $version, $now, $walltime ) {
2780  // Returns keys in ascending integer order for PHP7 array packing:
2781  // https://nikic.github.io/2014/12/22/PHPs-new-hashtable-implementation.html
2782  $wrapped = [
2783  self::$FLD_FORMAT_VERSION => self::$VERSION,
2784  self::$FLD_VALUE => $value,
2785  self::$FLD_TTL => $ttl,
2786  self::$FLD_TIME => $now
2787  ];
2788  if ( $version !== null ) {
2789  $wrapped[self::$FLD_VALUE_VERSION] = $version;
2790  }
2791  if ( $walltime >= self::$GENERATION_SLOW_SEC ) {
2792  $wrapped[self::$FLD_GENERATION_TIME] = $walltime;
2793  }
2794 
2795  return $wrapped;
2796  }
2797 
2810  private function unwrap( $wrapped, $now ) {
2811  $value = false;
2812  $info = $this->newKeyInfoPlaceholder();
2813 
2814  if ( is_array( $wrapped ) ) {
2815  // Entry expected to be a cached value; validate it
2816  if (
2817  ( $wrapped[self::$FLD_FORMAT_VERSION] ?? null ) === self::$VERSION &&
2818  $wrapped[self::$FLD_TIME] >= $this->epoch
2819  ) {
2820  if ( $wrapped[self::$FLD_TTL] > 0 ) {
2821  // Get the approximate time left on the key
2822  $age = $now - $wrapped[self::$FLD_TIME];
2823  $curTTL = max( $wrapped[self::$FLD_TTL] - $age, 0.0 );
2824  } else {
2825  // Key had no TTL, so the time left is unbounded
2826  $curTTL = INF;
2827  }
2828  $value = $wrapped[self::$FLD_VALUE];
2829  $info[self::KEY_VERSION] = $wrapped[self::$FLD_VALUE_VERSION] ?? null;
2830  $info[self::KEY_AS_OF] = $wrapped[self::$FLD_TIME];
2831  $info[self::KEY_CUR_TTL] = $curTTL;
2832  $info[self::KEY_TTL] = $wrapped[self::$FLD_TTL];
2833  }
2834  } else {
2835  // Entry expected to be a tombstone; parse it
2836  $purge = $this->parsePurgeValue( $wrapped );
2837  if ( $purge !== false ) {
2838  // Tombstoned keys should always have a negative current $ttl
2839  $info[self::KEY_CUR_TTL] =
2840  min( $purge[self::$PURGE_TIME] - $now, self::$TINY_NEGATIVE );
2841  $info[self::KEY_TOMB_AS_OF] = $purge[self::$PURGE_TIME];
2842  }
2843  }
2844 
2845  return [ $value, $info ];
2846  }
2847 
2851  private function newKeyInfoPlaceholder() {
2852  return [
2853  self::KEY_VERSION => null,
2854  self::KEY_AS_OF => null,
2855  self::KEY_TTL => null,
2856  self::KEY_CUR_TTL => null,
2857  self::KEY_TOMB_AS_OF => null
2858  ];
2859  }
2860 
2865  private function determineKeyClassForStats( $key ) {
2866  $parts = explode( ':', $key, 3 );
2867  // Sanity fallback in case the key was not made by makeKey.
2868  // Replace dots because they are special in StatsD (T232907)
2869  return strtr( $parts[1] ?? $parts[0], '.', '_' );
2870  }
2871 
2877  private function parsePurgeValue( $value ) {
2878  if ( !is_string( $value ) ) {
2879  return false;
2880  }
2881 
2882  $segments = explode( ':', $value, 3 );
2883  if (
2884  !isset( $segments[0] ) ||
2885  !isset( $segments[1] ) ||
2886  "{$segments[0]}:" !== self::PURGE_VAL_PREFIX
2887  ) {
2888  return false;
2889  }
2890 
2891  if ( !isset( $segments[2] ) ) {
2892  // Back-compat with old purge values without holdoff
2893  $segments[2] = self::HOLDOFF_TTL;
2894  }
2895 
2896  if ( $segments[1] < $this->epoch ) {
2897  // Values this old are ignored
2898  return false;
2899  }
2900 
2901  return [
2902  self::$PURGE_TIME => (float)$segments[1],
2903  self::$PURGE_HOLDOFF => (int)$segments[2],
2904  ];
2905  }
2906 
2912  private function makePurgeValue( $timestamp, $holdoff ) {
2913  return self::PURGE_VAL_PREFIX . (float)$timestamp . ':' . (int)$holdoff;
2914  }
2915 
2920  private function getProcessCache( $group ) {
2921  if ( !isset( $this->processCaches[$group] ) ) {
2922  list( , $size ) = explode( ':', $group );
2923  $this->processCaches[$group] = new MapCacheLRU( (int)$size );
2924  if ( $this->wallClockOverride !== null ) {
2925  $this->processCaches[$group]->setMockTime( $this->wallClockOverride );
2926  }
2927  }
2928 
2929  return $this->processCaches[$group];
2930  }
2931 
2937  private function getProcessCacheKey( $key, $version ) {
2938  return $key . ' ' . (int)$version;
2939  }
2940 
2946  private function getNonProcessCachedMultiKeys( ArrayIterator $keys, array $opts ) {
2947  $pcTTL = $opts['pcTTL'] ?? self::TTL_UNCACHEABLE;
2948 
2949  $keysMissing = [];
2950  if ( $pcTTL > 0 && $this->callbackDepth == 0 ) {
2951  $version = $opts['version'] ?? null;
2952  $pCache = $this->getProcessCache( $opts['pcGroup'] ?? self::PC_PRIMARY );
2953  foreach ( $keys as $key => $id ) {
2954  if ( !$pCache->has( $this->getProcessCacheKey( $key, $version ), $pcTTL ) ) {
2955  $keysMissing[$id] = $key;
2956  }
2957  }
2958  }
2959 
2960  return $keysMissing;
2961  }
2962 
2968  private function getRawKeysForWarmup( array $keys, array $checkKeys ) {
2969  if ( !$keys ) {
2970  return [];
2971  }
2972 
2973  // Get all the value keys to fetch...
2974  $keysWarmup = $this->makeSisterKeys( $keys, self::TYPE_VALUE );
2975  // Get all the check keys to fetch...
2976  foreach ( $checkKeys as $i => $checkKeyOrKeyGroup ) {
2977  // Note: avoid array_merge() inside loop in case there are many keys
2978  if ( is_int( $i ) ) {
2979  // Single check key that applies to all value keys
2980  $keysWarmup[] = $this->makeSisterKey( $checkKeyOrKeyGroup, self::TYPE_TIMESTAMP );
2981  } else {
2982  // List of check keys that apply to a specific value key
2983  foreach ( (array)$checkKeyOrKeyGroup as $checkKey ) {
2984  $keysWarmup[] = $this->makeSisterKey( $checkKey, self::TYPE_TIMESTAMP );
2985  }
2986  }
2987  }
2988 
2989  $warmupCache = $this->cache->getMulti( $keysWarmup );
2990  $warmupCache += array_fill_keys( $keysWarmup, false );
2991 
2992  return $warmupCache;
2993  }
2994 
2999  protected function getCurrentTime() {
3000  if ( $this->wallClockOverride ) {
3001  return $this->wallClockOverride;
3002  }
3003 
3004  $clockTime = (float)time(); // call this first
3005  // microtime() uses an initial gettimeofday() call added to usage clocks.
3006  // This can severely drift from time() and the microtime() value of other threads
3007  // due to undercounting of the amount of time elapsed. Instead of seeing the current
3008  // time as being in the past, use the value of time(). This avoids setting cache values
3009  // that will immediately be seen as expired and possibly cause stampedes.
3010  return max( microtime( true ), $clockTime );
3011  }
3012 
3017  public function setMockTime( &$time ) {
3018  $this->wallClockOverride =& $time;
3019  $this->cache->setMockTime( $time );
3020  foreach ( $this->processCaches as $pCache ) {
3021  $pCache->setMockTime( $time );
3022  }
3023  }
3024 }
WANObjectCache\$RAMPUP_TTL
static int $RAMPUP_TTL
Seconds to ramp up the chance of regeneration due to expected time-till-refresh.
Definition: WANObjectCache.php:222
WANObjectCache\getWithSetCallback
getWithSetCallback( $key, $ttl, $callback, array $opts=[], array $cbParams=[])
Method to fetch/regenerate a cache key.
Definition: WANObjectCache.php:1412
WANObjectCache\getCollectionFromKey
static getCollectionFromKey(string $sisterKey)
Definition: WANObjectCache.php:1734
WANObjectCache\getQoS
getQoS( $flag)
Definition: WANObjectCache.php:2449
WANObjectCache\$VERSION
static int $VERSION
Cache format version number.
Definition: WANObjectCache.php:243
WANObjectCache\hash256
hash256( $component)
Hash a possibly long string into a suitable component for makeKey()/makeGlobalKey()
Definition: WANObjectCache.php:2263
WANObjectCache\determineKeyClassForStats
determineKeyClassForStats( $key)
Definition: WANObjectCache.php:2865
WANObjectCache\relayDelete
relayDelete( $key)
Do the actual async bus delete of a key.
Definition: WANObjectCache.php:2575
WANObjectCache\setMockTime
setMockTime(&$time)
Definition: WANObjectCache.php:3017
WANObjectCache\makePurgeValue
makePurgeValue( $timestamp, $holdoff)
Definition: WANObjectCache.php:2912
WANObjectCache\$INTERIM_KEY_TTL
static int $INTERIM_KEY_TTL
Seconds to keep interim value keys for tombstoned keys around.
Definition: WANObjectCache.php:215
WANObjectCache\$warmupCache
mixed[] $warmupCache
Temporary warm-up cache.
Definition: WANObjectCache.php:162
WANObjectCache\RES_VALUE
const RES_VALUE
The key value component of a fetchMulti() result.
Definition: WANObjectCache.php:246
EmptyBagOStuff
A BagOStuff object with no objects in it.
Definition: EmptyBagOStuff.php:29
WANObjectCache\$cache
BagOStuff $cache
The local datacenter cache.
Definition: WANObjectCache.php:127
WANObjectCache\fetchOrRegenerate
fetchOrRegenerate( $key, $ttl, $callback, array $opts, array $cbParams)
Do the actual I/O for getWithSetCallback() when needed.
Definition: WANObjectCache.php:1472
WANObjectCache\isValid
isValid( $value, $asOf, $minAsOf, $purgeTime=null)
Check if $value is not false, versioned (if needed), and not older than $minTime (if set)
Definition: WANObjectCache.php:2758
true
return true
Definition: router.php:90
Wikimedia\LightweightObjectStore\ExpirationAwareness
Generic interface providing Time-To-Live constants for expirable object storage.
Definition: ExpirationAwareness.php:32
WANObjectCache\$FLD_GENERATION_TIME
static int $FLD_GENERATION_TIME
Key to how long it took to generate the value.
Definition: WANObjectCache.php:276
NullStatsdDataFactory
Definition: NullStatsdDataFactory.php:10
WANObjectCache\getMultiWithSetCallback
getMultiWithSetCallback(ArrayIterator $keyedIds, $ttl, callable $callback, array $opts=[])
Method to fetch multiple cache keys at once with regeneration.
Definition: WANObjectCache.php:1989
WANObjectCache\$TINY_NEGATIVE
static float $TINY_NEGATIVE
Tiny negative float to use when CTL comes up >= 0 due to clock skew.
Definition: WANObjectCache.php:225
BagOStuff
Class representing a cache/ephemeral data store.
Definition: BagOStuff.php:86
WANObjectCache\getMultiWithUnionSetCallback
getMultiWithUnionSetCallback(ArrayIterator $keyedIds, $ttl, callable $callback, array $opts=[])
Method to fetch/regenerate multiple cache keys at once.
Definition: WANObjectCache.php:2088
WANObjectCache\resolveTouched
resolveTouched( $value, $lastPurge, $touchedCallback)
Definition: WANObjectCache.php:1868
WANObjectCache\PASS_BY_REF
const PASS_BY_REF
Idiom for get()/getMulti() to return extra information by reference.
Definition: WANObjectCache.php:205
WANObjectCache\worthRefreshExpiring
worthRefreshExpiring( $curTTL, $logicalTTL, $lowTTL)
Check if a key is nearing expiration and thus due for randomized regeneration.
Definition: WANObjectCache.php:2674
$res
$res
Definition: testCompression.php:57
WANObjectCache\fetchKeys
fetchKeys(array $keys, array $checkKeys)
Fetch the value and key metadata of several keys from cache.
Definition: WANObjectCache.php:517
WANObjectCache\reapCheckKey
reapCheckKey( $key, $purgeTimestamp, &$isStale=false)
Set a "check" key to soon expire in the local cluster if it pre-dates $purgeTimestamp.
Definition: WANObjectCache.php:2205
WANObjectCache\getCheckKeyTime
getCheckKeyTime( $key)
Fetch the value of a timestamp "check" key.
Definition: WANObjectCache.php:927
WANObjectCache\resolveBusyValue
resolveBusyValue( $busyValue)
Definition: WANObjectCache.php:1921
WANObjectCache\$logger
LoggerInterface $logger
Definition: WANObjectCache.php:131
WANObjectCache\$FLD_VALUE
static int $FLD_VALUE
Key to the cached value.
Definition: WANObjectCache.php:266
WANObjectCache\$warmupKeyMisses
int $warmupKeyMisses
Key fetched.
Definition: WANObjectCache.php:164
WANObjectCache\newEmpty
static newEmpty()
Get an instance that wraps EmptyBagOStuff.
Definition: WANObjectCache.php:372
WANObjectCache\$RECENT_SET_LOW_MS
static int $RECENT_SET_LOW_MS
Min millisecond set() backoff during hold-off (far less than INTERIM_KEY_TTL)
Definition: WANObjectCache.php:230
WANObjectCache\clearProcessCache
clearProcessCache()
Clear the in-process caches; useful for testing.
Definition: WANObjectCache.php:2416
WANObjectCache\getInterimValue
getInterimValue( $key, $minAsOf)
Definition: WANObjectCache.php:1879
WANObjectCache\$secret
string $secret
Stable secret used for hasing long strings into key components.
Definition: WANObjectCache.php:148
WANObjectCache\clearLastError
clearLastError()
Clear the "last error" registry.
Definition: WANObjectCache.php:2407
WANObjectCache\getCurrentTime
getCurrentTime()
Definition: WANObjectCache.php:2999
WANObjectCache\$FLD_FORMAT_VERSION
static int $FLD_FORMAT_VERSION
Key to WAN cache version number.
Definition: WANObjectCache.php:264
WANObjectCache\unwrap
unwrap( $wrapped, $now)
Definition: WANObjectCache.php:2810
WANObjectCache\getLastError
getLastError()
Get the "last error" registered; clearLastError() should be called manually.
Definition: WANObjectCache.php:2390
WANObjectCache\makeSisterKeys
makeSisterKeys(array $baseKeys, $type)
Get cache keys that should be collocated with their corresponding base keys.
Definition: WANObjectCache.php:1691
WANObjectCache\makeMultiKeys
makeMultiKeys(array $ids, $keyCallback)
Get an iterator of (cache key => entity ID) for a list of entity IDs.
Definition: WANObjectCache.php:2317
WANObjectCache\getMulti
getMulti(array $keys, &$curTTLs=[], array $checkKeys=[], &$info=[])
Fetch the value of several keys from cache.
Definition: WANObjectCache.php:468
WANObjectCache\__construct
__construct(array $params)
Definition: WANObjectCache.php:328
WANObjectCache\useInterimHoldOffCaching
useInterimHoldOffCaching( $enabled)
Enable or disable the use of brief caching for tombstoned keys.
Definition: WANObjectCache.php:2440
WANObjectCache\reap
reap( $key, $purgeTimestamp, &$isStale=false)
Set a key to soon expire in the local cluster if it pre-dates $purgeTimestamp.
Definition: WANObjectCache.php:2173
WANObjectCache\$keyHighQps
int $keyHighQps
Reads/second assumed during a hypothetical cache write stampede for a key.
Definition: WANObjectCache.php:155
WANObjectCache\touchCheckKey
touchCheckKey( $key, $holdoff=self::HOLDOFF_TTL)
Purge a "check" key from all datacenters, invalidating keys that use it.
Definition: WANObjectCache.php:1057
MapCacheLRU
Handles a simple LRU key/value map with a maximum number of entries.
Definition: MapCacheLRU.php:37
WANObjectCache\RES_METADATA
const RES_METADATA
The key metadata component of a fetchMulti() result.
Definition: WANObjectCache.php:248
WANObjectCache\$mcrouterAware
bool $mcrouterAware
Whether to use mcrouter key prefixing for routing.
Definition: WANObjectCache.php:138
WANObjectCache\wrap
wrap( $value, $ttl, $version, $now, $walltime)
Definition: WANObjectCache.php:2779
WANObjectCache\adaptiveTTL
adaptiveTTL( $mtime, $maxTTL, $minTTL=30, $factor=0.2)
Get a TTL that is higher for objects that have not changed recently.
Definition: WANObjectCache.php:2516
WANObjectCache\getNonProcessCachedMultiKeys
getNonProcessCachedMultiKeys(ArrayIterator $keys, array $opts)
Definition: WANObjectCache.php:2946
WANObjectCache\$epoch
float $epoch
Unix timestamp of the oldest possible valid values.
Definition: WANObjectCache.php:146
WANObjectCache\relayPurge
relayPurge( $key, $ttl, $holdoff)
Do the actual async bus purge of a key.
Definition: WANObjectCache.php:2548
WANObjectCache\$wallClockOverride
float null $wallClockOverride
Definition: WANObjectCache.php:167
WANObjectCache\$CHECK_KEY_TTL
static int $CHECK_KEY_TTL
Seconds to keep dependency purge keys around.
Definition: WANObjectCache.php:213
Wikimedia\LightweightObjectStore\StorageAwareness
Generic interface providing error code and quality-of-service constants for object stores.
Definition: StorageAwareness.php:32
WANObjectCache\makeGlobalKey
makeGlobalKey( $collection,... $components)
Make a cache key for the global keyspace and given components.
Definition: WANObjectCache.php:2238
WANObjectCache\isVolatileValueAgeNegligible
isVolatileValueAgeNegligible( $age)
Definition: WANObjectCache.php:1755
WANObjectCache\getMultiCheckKeyTime
getMultiCheckKeyTime(array $keys)
Fetch the values of each timestamp "check" key.
Definition: WANObjectCache.php:992
WANObjectCache\$COOLOFF_TTL
static int $COOLOFF_TTL
Seconds to no-op key set() calls to avoid large blob I/O stampedes.
Definition: WANObjectCache.php:220
WANObjectCache\worthRefreshPopular
worthRefreshPopular( $asOf, $ageNew, $timeTillRefresh, $now)
Check if a key is due for randomized regeneration due to its popularity.
Definition: WANObjectCache.php:2715
WANObjectCache\$LOCK_TTL
static int $LOCK_TTL
Seconds to keep lock keys around.
Definition: WANObjectCache.php:218
WANObjectCache\isAliveOrInGracePeriod
isAliveOrInGracePeriod( $curTTL, $graceTTL)
Check if a key is fresh or in the grace window and thus due for randomized reuse.
Definition: WANObjectCache.php:2640
WANObjectCache\$useInterimHoldOffCaching
bool $useInterimHoldOffCaching
Whether to use "interim" caching while keys are tombstoned.
Definition: WANObjectCache.php:144
WANObjectCache\$cluster
string $cluster
Cache cluster name for mcrouter use.
Definition: WANObjectCache.php:142
WANObjectCache
Multi-datacenter aware caching interface.
Definition: WANObjectCache.php:125
WANObjectCache\getProcessCache
getProcessCache( $group)
Definition: WANObjectCache.php:2920
WANObjectCache\multiRemap
multiRemap(array $ids, array $res)
Get an (ID => value) map from (i) a non-unique list of entity IDs, and (ii) the list of corresponding...
Definition: WANObjectCache.php:2373
WANObjectCache\getRawKeysForWarmup
getRawKeysForWarmup(array $keys, array $checkKeys)
Definition: WANObjectCache.php:2968
WANObjectCache\$keyHighUplinkBps
float $keyHighUplinkBps
Max tolerable bytes/second to spend on a cache write stampede for a key.
Definition: WANObjectCache.php:157
WANObjectCache\checkAndSetCooloff
checkAndSetCooloff( $key, $kClass, $value, $elapsed, $hasLock)
Check whether set() is rate-limited to avoid concurrent I/O spikes.
Definition: WANObjectCache.php:1780
WANObjectCache\$FLD_FLAGS
static int $FLD_FLAGS
@noinspection PhpUnusedPrivateFieldInspection
Definition: WANObjectCache.php:272
WANObjectCache\resetCheckKey
resetCheckKey( $key)
Delete a "check" key from all datacenters, invalidating keys that use it.
Definition: WANObjectCache.php:1098
WANObjectCache\newKeyInfoPlaceholder
newKeyInfoPlaceholder()
Definition: WANObjectCache.php:2851
WANObjectCache\claimStampedeLock
claimStampedeLock( $key)
Definition: WANObjectCache.php:1659
WANObjectCache\parsePurgeValue
parsePurgeValue( $value)
Definition: WANObjectCache.php:2877
WANObjectCache\$RECENT_SET_HIGH_MS
static int $RECENT_SET_HIGH_MS
Max millisecond set() backoff during hold-off (far less than INTERIM_KEY_TTL)
Definition: WANObjectCache.php:232
WANObjectCache\getProcessCacheKey
getProcessCacheKey( $key, $version)
Definition: WANObjectCache.php:2937
WANObjectCache\yieldStampedeLock
yieldStampedeLock( $key, $hasLock)
Definition: WANObjectCache.php:1672
WANObjectCache\$TINY_POSTIVE
static float $TINY_POSTIVE
Tiny positive float to use when using "minTime" to assert an inequality.
Definition: WANObjectCache.php:227
WANObjectCache\$asyncHandler
callable null $asyncHandler
Function that takes a WAN cache callback and runs it later.
Definition: WANObjectCache.php:135
WANObjectCache\getWarmupKeyMisses
getWarmupKeyMisses()
Definition: WANObjectCache.php:2534
$keys
$keys
Definition: testCompression.php:72
WANObjectCache\$GENERATION_SLOW_SEC
static int $GENERATION_SLOW_SEC
Consider value generation slow if it takes more than this many seconds.
Definition: WANObjectCache.php:235
WANObjectCache\makeSisterKey
makeSisterKey( $baseKey, $typeChar)
Get a cache key that should be collocated with a base key.
Definition: WANObjectCache.php:1707
WANObjectCache\resolveCTL
resolveCTL( $value, $curTTL, $curInfo, $touchedCallback)
Definition: WANObjectCache.php:1838
WANObjectCache\$PURGE_HOLDOFF
static int $PURGE_HOLDOFF
Key to the tombstone entry hold-off TTL.
Definition: WANObjectCache.php:240
WANObjectCache\$callbackDepth
int $callbackDepth
Callback stack depth for getWithSetCallback()
Definition: WANObjectCache.php:160
WANObjectCache\$coalesceScheme
int $coalesceScheme
Scheme to use for key coalescing (Hash Tags or Hash Stops)
Definition: WANObjectCache.php:152
IStoreKeyEncoder
Generic interface for object stores with key encoding methods.
Definition: IStoreKeyEncoder.php:9
WANObjectCache\setLogger
setLogger(LoggerInterface $logger)
Definition: WANObjectCache.php:363
WANObjectCache\$FLD_TTL
static int $FLD_TTL
Key to the original TTL.
Definition: WANObjectCache.php:268
WANObjectCache\makeKey
makeKey( $collection,... $components)
Make a cache key using the "global" keyspace for the given components.
Definition: WANObjectCache.php:2252
WANObjectCache\$coalesceKeys
string bool $coalesceKeys
Whether "sister" keys should be coalesced to the same cache server.
Definition: WANObjectCache.php:150
WANObjectCache\$FLD_VALUE_VERSION
static int $FLD_VALUE_VERSION
Key to collection cache version number.
Definition: WANObjectCache.php:274
WANObjectCache\$FLD_TIME
static int $FLD_TIME
Key to the cache timestamp.
Definition: WANObjectCache.php:270
WANObjectCache\$PURGE_TIME
static int $PURGE_TIME
Key to the tombstone entry timestamp.
Definition: WANObjectCache.php:238
WANObjectCache\$stats
StatsdDataFactoryInterface $stats
Definition: WANObjectCache.php:133
WANObjectCache\setInterimValue
setInterimValue( $key, $value, $ttl, $version, $walltime)
Definition: WANObjectCache.php:1903
WANObjectCache\$processCaches
MapCacheLRU[] $processCaches
Map of group PHP instance caches.
Definition: WANObjectCache.php:129
WANObjectCache\scheduleAsyncRefresh
scheduleAsyncRefresh( $key, $ttl, $callback, array $opts, array $cbParams)
Schedule a deferred cache regeneration if possible.
Definition: WANObjectCache.php:2599
WANObjectCache\processCheckKeys
processCheckKeys(array $timeKeys, array $wrappedValues, $now)
Definition: WANObjectCache.php:613
$type
$type
Definition: testCompression.php:52
WANObjectCache\$region
string $region
Physical region for mcrouter use.
Definition: WANObjectCache.php:140