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  private const STALE_TTL_NONE = 0;
192  private 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 = -1;
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 static $FLD_FORMAT_VERSION = 0;
248  private static $FLD_VALUE = 1;
250  private static $FLD_TTL = 2;
252  private static $FLD_TIME = 3;
254  private static $FLD_FLAGS = 4;
256  private static $FLD_VALUE_VERSION = 5;
258  private static $FLD_GENERATION_TIME = 6;
259 
261  private const TYPE_VALUE = 'v';
263  private const TYPE_TIMESTAMP = 't';
265  private const TYPE_MUTEX = 'm';
267  private const TYPE_INTERIM = 'i';
269  private const TYPE_COOLOFF = 'c';
270 
272  private const PURGE_VAL_PREFIX = 'PURGED:';
273 
310  public function __construct( array $params ) {
311  $this->cache = $params['cache'];
312  $this->region = $params['region'] ?? 'main';
313  $this->cluster = $params['cluster'] ?? 'wan-main';
314  $this->mcrouterAware = !empty( $params['mcrouterAware'] );
315  $this->epoch = $params['epoch'] ?? 0;
316  $this->secret = $params['secret'] ?? (string)$this->epoch;
317  $this->coalesceKeys = $params['coalesceKeys'] ?? false;
318  if ( !empty( $params['mcrouterAware'] ) ) {
319  // https://github.com/facebook/mcrouter/wiki/Key-syntax
320  $this->coalesceScheme = self::SCHEME_HASH_STOP;
321  } else {
322  // https://redis.io/topics/cluster-spec
323  // https://github.com/twitter/twemproxy/blob/v0.4.1/notes/recommendation.md#hash-tags
324  // https://github.com/Netflix/dynomite/blob/v0.7.0/notes/recommendation.md#hash-tags
325  $this->coalesceScheme = self::SCHEME_HASH_TAG;
326  }
327 
328  $this->keyHighQps = $params['keyHighQps'] ?? 100;
329  $this->keyHighUplinkBps = $params['keyHighUplinkBps'] ?? ( 1e9 / 8 / 100 );
330 
331  $this->setLogger( $params['logger'] ?? new NullLogger() );
332  $this->stats = $params['stats'] ?? new NullStatsdDataFactory();
333  $this->asyncHandler = $params['asyncHandler'] ?? null;
334  }
335 
339  public function setLogger( LoggerInterface $logger ) {
340  $this->logger = $logger;
341  }
342 
348  public static function newEmpty() {
349  return new static( [ 'cache' => new EmptyBagOStuff() ] );
350  }
351 
402  final public function get(
403  $key, &$curTTL = null, array $checkKeys = [], &$info = null
404  ) {
405  $curTTLs = self::PASS_BY_REF;
406  $infoByKey = self::PASS_BY_REF;
407  $values = $this->getMulti( [ $key ], $curTTLs, $checkKeys, $infoByKey );
408 
409  $curTTL = $curTTLs[$key] ?? null;
410  if ( $info === self::PASS_BY_REF ) {
411  $info = [
412  'asOf' => $infoByKey[$key]['asOf'] ?? null,
413  'tombAsOf' => $infoByKey[$key]['tombAsOf'] ?? null,
414  'lastCKPurge' => $infoByKey[$key]['lastCKPurge'] ?? null,
415  'version' => $infoByKey[$key]['version'] ?? null
416  ];
417  } else {
418  $info = $infoByKey[$key]['asOf'] ?? null; // b/c
419  }
420 
421  return array_key_exists( $key, $values ) ? $values[$key] : false;
422  }
423 
446  final public function getMulti(
447  array $keys,
448  &$curTTLs = [],
449  array $checkKeys = [],
450  &$info = null
451  ) {
452  $result = [];
453  $curTTLs = [];
454  $infoByKey = [];
455 
456  // Order-corresponding list of value keys for the provided base keys
457  $valueKeys = $this->makeSisterKeys( $keys, self::TYPE_VALUE );
458 
459  $fullKeysNeeded = $valueKeys;
460  $checkKeysForAll = [];
461  $checkKeysByKey = [];
462  foreach ( $checkKeys as $i => $checkKeyOrKeyGroup ) {
463  // Note: avoid array_merge() inside loop in case there are many keys
464  if ( is_int( $i ) ) {
465  // Single check key that applies to all value keys
466  $fullKey = $this->makeSisterKey( $checkKeyOrKeyGroup, self::TYPE_TIMESTAMP );
467  $fullKeysNeeded[] = $fullKey;
468  $checkKeysForAll[] = $fullKey;
469  } else {
470  // List of check keys that apply to a specific value key
471  foreach ( (array)$checkKeyOrKeyGroup as $checkKey ) {
472  $fullKey = $this->makeSisterKey( $checkKey, self::TYPE_TIMESTAMP );
473  $fullKeysNeeded[] = $fullKey;
474  $checkKeysByKey[$i][] = $fullKey;
475  }
476  }
477  }
478 
479  if ( $this->warmupCache ) {
480  // Get the raw values of the keys from the warmup cache
481  $wrappedValues = $this->warmupCache;
482  $fullKeysMissing = array_diff( $fullKeysNeeded, array_keys( $wrappedValues ) );
483  if ( $fullKeysMissing ) { // sanity
484  $this->warmupKeyMisses += count( $fullKeysMissing );
485  $wrappedValues += $this->cache->getMulti( $fullKeysMissing );
486  }
487  } else {
488  // Fetch the raw values of the keys from the backend
489  $wrappedValues = $this->cache->getMulti( $fullKeysNeeded );
490  }
491 
492  // Time used to compare/init "check" keys (derived after getMulti() to be pessimistic)
493  $now = $this->getCurrentTime();
494 
495  // Collect timestamps from all "check" keys
496  $purgeValuesForAll = $this->processCheckKeys( $checkKeysForAll, $wrappedValues, $now );
497  $purgeValuesByKey = [];
498  foreach ( $checkKeysByKey as $cacheKey => $checks ) {
499  $purgeValuesByKey[$cacheKey] = $this->processCheckKeys( $checks, $wrappedValues, $now );
500  }
501 
502  // Get the main cache value for each key and validate them
503  reset( $keys );
504  foreach ( $valueKeys as $i => $vKey ) {
505  // Get the corresponding base key for this value key
506  $key = current( $keys );
507  next( $keys );
508 
509  list( $value, $keyInfo ) = $this->unwrap(
510  array_key_exists( $vKey, $wrappedValues ) ? $wrappedValues[$vKey] : false,
511  $now
512  );
513  // Force dependent keys to be seen as stale for a while after purging
514  // to reduce race conditions involving stale data getting cached
515  $purgeValues = $purgeValuesForAll;
516  if ( isset( $purgeValuesByKey[$key] ) ) {
517  $purgeValues = array_merge( $purgeValues, $purgeValuesByKey[$key] );
518  }
519 
520  $lastCKPurge = null; // timestamp of the highest check key
521  foreach ( $purgeValues as $purge ) {
522  $lastCKPurge = max( $purge[self::$PURGE_TIME], $lastCKPurge );
523  $safeTimestamp = $purge[self::$PURGE_TIME] + $purge[self::$PURGE_HOLDOFF];
524  if ( $value !== false && $safeTimestamp >= $keyInfo['asOf'] ) {
525  // How long ago this value was invalidated by *this* check key
526  $ago = min( $purge[self::$PURGE_TIME] - $now, self::$TINY_NEGATIVE );
527  // How long ago this value was invalidated by *any* known check key
528  $keyInfo['curTTL'] = min( $keyInfo['curTTL'], $ago );
529  }
530  }
531  $keyInfo[ 'lastCKPurge'] = $lastCKPurge;
532 
533  if ( $value !== false ) {
534  $result[$key] = $value;
535  }
536  if ( $keyInfo['curTTL'] !== null ) {
537  $curTTLs[$key] = $keyInfo['curTTL'];
538  }
539 
540  $infoByKey[$key] = ( $info === self::PASS_BY_REF )
541  ? $keyInfo
542  : $keyInfo['asOf']; // b/c
543  }
544 
545  $info = $infoByKey;
546 
547  return $result;
548  }
549 
557  private function processCheckKeys( array $timeKeys, array $wrappedValues, $now ) {
558  $purgeValues = [];
559  foreach ( $timeKeys as $timeKey ) {
560  $purge = isset( $wrappedValues[$timeKey] )
561  ? $this->parsePurgeValue( $wrappedValues[$timeKey] )
562  : false;
563  if ( $purge === false ) {
564  // Key is not set or malformed; regenerate
565  $newVal = $this->makePurgeValue( $now, self::HOLDOFF_TTL );
566  $this->cache->add( $timeKey, $newVal, self::$CHECK_KEY_TTL );
567  $purge = $this->parsePurgeValue( $newVal );
568  }
569  $purgeValues[] = $purge;
570  }
571 
572  return $purgeValues;
573  }
574 
651  final public function set( $key, $value, $ttl = self::TTL_INDEFINITE, array $opts = [] ) {
652  $now = $this->getCurrentTime();
653  $lag = $opts['lag'] ?? 0;
654  $age = isset( $opts['since'] ) ? max( 0, $now - $opts['since'] ) : 0;
655  $pending = $opts['pending'] ?? false;
656  $lockTSE = $opts['lockTSE'] ?? self::TSE_NONE;
657  $staleTTL = $opts['staleTTL'] ?? self::STALE_TTL_NONE;
658  $creating = $opts['creating'] ?? false;
659  $version = $opts['version'] ?? null;
660  $walltime = $opts['walltime'] ?? null;
661 
662  if ( $ttl < 0 ) {
663  return true; // not cacheable
664  }
665 
666  // Do not cache potentially uncommitted data as it might get rolled back
667  if ( $pending ) {
668  $this->logger->info(
669  'Rejected set() for {cachekey} due to pending writes.',
670  [ 'cachekey' => $key ]
671  );
672 
673  return true; // no-op the write for being unsafe
674  }
675 
676  // Check if there is a risk of caching (stale) data that predates the last delete()
677  // tombstone due to the tombstone having expired. If so, then the behavior should depend
678  // on whether the problem is specific to this regeneration attempt or systemically affects
679  // attempts to regenerate this key. For systemic cases, the cache writes should set a low
680  // TTL so that the value at least remains cacheable. For non-systemic cases, the cache
681  // write can simply be rejected.
682  if ( $age > self::MAX_READ_LAG ) {
683  // Case A: high snapshot lag
684  if ( $walltime === null ) {
685  // Case A0: high snapshot lag without regeneration wall time info.
686  // Probably systemic; use a low TTL to avoid stampedes/uncacheability.
687  $mitigated = 'snapshot lag';
688  $mitigationTTL = self::TTL_SECOND;
689  } elseif ( ( $age - $walltime ) > self::MAX_READ_LAG ) {
690  // Case A1: value regeneration during an already long-running transaction.
691  // Probably non-systemic; rely on a less problematic regeneration attempt.
692  $mitigated = 'snapshot lag (late regeneration)';
693  $mitigationTTL = self::TTL_UNCACHEABLE;
694  } else {
695  // Case A2: value regeneration takes a long time.
696  // Probably systemic; use a low TTL to avoid stampedes/uncacheability.
697  $mitigated = 'snapshot lag (high regeneration time)';
698  $mitigationTTL = self::TTL_SECOND;
699  }
700  } elseif ( $lag === false || $lag > self::MAX_READ_LAG ) {
701  // Case B: high replication lag without high snapshot lag
702  // Probably systemic; use a low TTL to avoid stampedes/uncacheability
703  $mitigated = 'replication lag';
704  $mitigationTTL = self::TTL_LAGGED;
705  } elseif ( ( $lag + $age ) > self::MAX_READ_LAG ) {
706  // Case C: medium length request with medium replication lag
707  // Probably non-systemic; rely on a less problematic regeneration attempt
708  $mitigated = 'read lag';
709  $mitigationTTL = self::TTL_UNCACHEABLE;
710  } else {
711  // New value generated with recent enough data
712  $mitigated = null;
713  $mitigationTTL = null;
714  }
715 
716  if ( $mitigationTTL === self::TTL_UNCACHEABLE ) {
717  $this->logger->warning(
718  "Rejected set() for {cachekey} due to $mitigated.",
719  [ 'cachekey' => $key, 'lag' => $lag, 'age' => $age, 'walltime' => $walltime ]
720  );
721 
722  return true; // no-op the write for being unsafe
723  }
724 
725  // TTL to use in staleness checks (does not effect persistence layer TTL)
726  $logicalTTL = null;
727 
728  if ( $mitigationTTL !== null ) {
729  // New value generated from data that is old enough to be risky
730  if ( $lockTSE >= 0 ) {
731  // Value will have the normal expiry but will be seen as stale sooner
732  $logicalTTL = min( $ttl ?: INF, $mitigationTTL );
733  } else {
734  // Value expires sooner (leaving enough TTL for preemptive refresh)
735  $ttl = min( $ttl ?: INF, max( $mitigationTTL, self::LOW_TTL ) );
736  }
737 
738  $this->logger->warning(
739  "Lowered set() TTL for {cachekey} due to $mitigated.",
740  [ 'cachekey' => $key, 'lag' => $lag, 'age' => $age, 'walltime' => $walltime ]
741  );
742  }
743 
744  // Wrap that value with time/TTL/version metadata
745  $wrapped = $this->wrap( $value, $logicalTTL ?: $ttl, $version, $now, $walltime );
746  $storeTTL = $ttl + $staleTTL;
747 
748  if ( $creating ) {
749  $ok = $this->cache->add(
750  $this->makeSisterKey( $key, self::TYPE_VALUE ),
751  $wrapped,
752  $storeTTL
753  );
754  } else {
755  $ok = $this->cache->merge(
756  $this->makeSisterKey( $key, self::TYPE_VALUE ),
757  function ( $cache, $key, $cWrapped ) use ( $wrapped ) {
758  // A string value means that it is a tombstone; do nothing in that case
759  return ( is_string( $cWrapped ) ) ? false : $wrapped;
760  },
761  $storeTTL,
762  1 // 1 attempt
763  );
764  }
765 
766  return $ok;
767  }
768 
830  final public function delete( $key, $ttl = self::HOLDOFF_TTL ) {
831  if ( $ttl <= 0 ) {
832  // Publish the purge to all datacenters
833  $ok = $this->relayDelete( $this->makeSisterKey( $key, self::TYPE_VALUE ) );
834  } else {
835  // Publish the purge to all datacenters
836  $ok = $this->relayPurge(
837  $this->makeSisterKey( $key, self::TYPE_VALUE ),
838  $ttl,
839  self::HOLDOFF_TTL_NONE
840  );
841  }
842 
843  $kClass = $this->determineKeyClassForStats( $key );
844  $this->stats->increment( "wanobjectcache.$kClass.delete." . ( $ok ? 'ok' : 'error' ) );
845 
846  return $ok;
847  }
848 
868  final public function getCheckKeyTime( $key ) {
869  return $this->getMultiCheckKeyTime( [ $key ] )[$key];
870  }
871 
933  final public function getMultiCheckKeyTime( array $keys ) {
934  $rawKeys = [];
935  foreach ( $keys as $key ) {
936  $rawKeys[$key] = $this->makeSisterKey( $key, self::TYPE_TIMESTAMP );
937  }
938 
939  $rawValues = $this->cache->getMulti( $rawKeys );
940  $rawValues += array_fill_keys( $rawKeys, false );
941 
942  $times = [];
943  foreach ( $rawKeys as $key => $rawKey ) {
944  $purge = $this->parsePurgeValue( $rawValues[$rawKey] );
945  if ( $purge !== false ) {
946  $time = $purge[self::$PURGE_TIME];
947  } else {
948  // Casting assures identical floats for the next getCheckKeyTime() calls
949  $now = (string)$this->getCurrentTime();
950  $this->cache->add(
951  $rawKey,
952  $this->makePurgeValue( $now, self::HOLDOFF_TTL ),
954  );
955  $time = (float)$now;
956  }
957 
958  $times[$key] = $time;
959  }
960 
961  return $times;
962  }
963 
998  final public function touchCheckKey( $key, $holdoff = self::HOLDOFF_TTL ) {
999  // Publish the purge to all datacenters
1000  $ok = $this->relayPurge(
1001  $this->makeSisterKey( $key, self::TYPE_TIMESTAMP ),
1002  self::$CHECK_KEY_TTL,
1003  $holdoff
1004  );
1005 
1006  $kClass = $this->determineKeyClassForStats( $key );
1007  $this->stats->increment( "wanobjectcache.$kClass.ck_touch." . ( $ok ? 'ok' : 'error' ) );
1008 
1009  return $ok;
1010  }
1011 
1039  final public function resetCheckKey( $key ) {
1040  // Publish the purge to all datacenters
1041  $ok = $this->relayDelete( $this->makeSisterKey( $key, self::TYPE_TIMESTAMP ) );
1042 
1043  $kClass = $this->determineKeyClassForStats( $key );
1044  $this->stats->increment( "wanobjectcache.$kClass.ck_reset." . ( $ok ? 'ok' : 'error' ) );
1045 
1046  return $ok;
1047  }
1048 
1356  final public function getWithSetCallback(
1357  $key, $ttl, $callback, array $opts = [], array $cbParams = []
1358  ) {
1359  $version = $opts['version'] ?? null;
1360  $pcTTL = $opts['pcTTL'] ?? self::TTL_UNCACHEABLE;
1361  $pCache = ( $pcTTL >= 0 )
1362  ? $this->getProcessCache( $opts['pcGroup'] ?? self::PC_PRIMARY )
1363  : null;
1364 
1365  // Use the process cache if requested as long as no outer cache callback is running.
1366  // Nested callback process cache use is not lag-safe with regard to HOLDOFF_TTL since
1367  // process cached values are more lagged than persistent ones as they are not purged.
1368  if ( $pCache && $this->callbackDepth == 0 ) {
1369  $cached = $pCache->get( $this->getProcessCacheKey( $key, $version ), $pcTTL, false );
1370  if ( $cached !== false ) {
1371  $this->logger->debug( "getWithSetCallback($key): process cache hit" );
1372  return $cached;
1373  }
1374  }
1375 
1376  $res = $this->fetchOrRegenerate( $key, $ttl, $callback, $opts, $cbParams );
1377  list( $value, $valueVersion, $curAsOf ) = $res;
1378  if ( $valueVersion !== $version ) {
1379  // Current value has a different version; use the variant key for this version.
1380  // Regenerate the variant value if it is not newer than the main value at $key
1381  // so that purges to the main key propagate to the variant value.
1382  $this->logger->debug( "getWithSetCallback($key): using variant key" );
1383  list( $value ) = $this->fetchOrRegenerate(
1384  $this->makeGlobalKey( 'WANCache-key-variant', md5( $key ), $version ),
1385  $ttl,
1386  $callback,
1387  [ 'version' => null, 'minAsOf' => $curAsOf ] + $opts,
1388  $cbParams
1389  );
1390  }
1391 
1392  // Update the process cache if enabled
1393  if ( $pCache && $value !== false ) {
1394  $pCache->set( $this->getProcessCacheKey( $key, $version ), $value );
1395  }
1396 
1397  return $value;
1398  }
1399 
1416  private function fetchOrRegenerate( $key, $ttl, $callback, array $opts, array $cbParams ) {
1417  $checkKeys = $opts['checkKeys'] ?? [];
1418  $graceTTL = $opts['graceTTL'] ?? self::GRACE_TTL_NONE;
1419  $minAsOf = $opts['minAsOf'] ?? self::MIN_TIMESTAMP_NONE;
1420  $hotTTR = $opts['hotTTR'] ?? self::HOT_TTR;
1421  $lowTTL = $opts['lowTTL'] ?? min( self::LOW_TTL, $ttl );
1422  $ageNew = $opts['ageNew'] ?? self::AGE_NEW;
1423  $touchedCb = $opts['touchedCallback'] ?? null;
1424  $initialTime = $this->getCurrentTime();
1425 
1426  $kClass = $this->determineKeyClassForStats( $key );
1427 
1428  // Get the current key value and its metadata
1429  $curTTL = self::PASS_BY_REF;
1430  $curInfo = self::PASS_BY_REF;
1431  $curValue = $this->get( $key, $curTTL, $checkKeys, $curInfo );
1433  '@phan-var array $curInfo';
1434  // Apply any $touchedCb invalidation timestamp to get the "last purge timestamp"
1435  list( $curTTL, $LPT ) = $this->resolveCTL( $curValue, $curTTL, $curInfo, $touchedCb );
1436  // Use the cached value if it exists and is not due for synchronous regeneration
1437  if (
1438  $this->isValid( $curValue, $curInfo['asOf'], $minAsOf ) &&
1439  $this->isAliveOrInGracePeriod( $curTTL, $graceTTL )
1440  ) {
1441  $preemptiveRefresh = (
1442  $this->worthRefreshExpiring( $curTTL, $lowTTL ) ||
1443  $this->worthRefreshPopular( $curInfo['asOf'], $ageNew, $hotTTR, $initialTime )
1444  );
1445  if ( !$preemptiveRefresh ) {
1446  $this->stats->timing(
1447  "wanobjectcache.$kClass.hit.good",
1448  1e3 * ( $this->getCurrentTime() - $initialTime )
1449  );
1450 
1451  return [ $curValue, $curInfo['version'], $curInfo['asOf'] ];
1452  } elseif ( $this->scheduleAsyncRefresh( $key, $ttl, $callback, $opts, $cbParams ) ) {
1453  $this->logger->debug( "fetchOrRegenerate($key): hit with async refresh" );
1454  $this->stats->timing(
1455  "wanobjectcache.$kClass.hit.refresh",
1456  1e3 * ( $this->getCurrentTime() - $initialTime )
1457  );
1458 
1459  return [ $curValue, $curInfo['version'], $curInfo['asOf'] ];
1460  } else {
1461  $this->logger->debug( "fetchOrRegenerate($key): hit with sync refresh" );
1462  }
1463  }
1464 
1465  // Determine if there is stale or volatile cached value that is still usable
1466  $isKeyTombstoned = ( $curInfo['tombAsOf'] !== null );
1467  if ( $isKeyTombstoned ) {
1468  // Key is write-holed; use the (volatile) interim key as an alternative
1469  list( $possValue, $possInfo ) = $this->getInterimValue( $key, $minAsOf );
1470  // Update the "last purge time" since the $touchedCb timestamp depends on $value
1471  $LPT = $this->resolveTouched( $possValue, $LPT, $touchedCb );
1472  } else {
1473  $possValue = $curValue;
1474  $possInfo = $curInfo;
1475  }
1476 
1477  // Avoid overhead from callback runs, regeneration locks, and cache sets during
1478  // hold-off periods for the key by reusing very recently generated cached values
1479  if (
1480  $this->isValid( $possValue, $possInfo['asOf'], $minAsOf, $LPT ) &&
1481  $this->isVolatileValueAgeNegligible( $initialTime - $possInfo['asOf'] )
1482  ) {
1483  $this->logger->debug( "fetchOrRegenerate($key): volatile hit" );
1484  $this->stats->timing(
1485  "wanobjectcache.$kClass.hit.volatile",
1486  1e3 * ( $this->getCurrentTime() - $initialTime )
1487  );
1488 
1489  return [ $possValue, $possInfo['version'], $curInfo['asOf'] ];
1490  }
1491 
1492  $lockTSE = $opts['lockTSE'] ?? self::TSE_NONE;
1493  $busyValue = $opts['busyValue'] ?? null;
1494  $staleTTL = $opts['staleTTL'] ?? self::STALE_TTL_NONE;
1495  $version = $opts['version'] ?? null;
1496 
1497  // Determine whether one thread per datacenter should handle regeneration at a time
1498  $useRegenerationLock =
1499  // Note that since tombstones no-op set(), $lockTSE and $curTTL cannot be used to
1500  // deduce the key hotness because |$curTTL| will always keep increasing until the
1501  // tombstone expires or is overwritten by a new tombstone. Also, even if $lockTSE
1502  // is not set, constant regeneration of a key for the tombstone lifetime might be
1503  // very expensive. Assume tombstoned keys are possibly hot in order to reduce
1504  // the risk of high regeneration load after the delete() method is called.
1505  $isKeyTombstoned ||
1506  // Assume a key is hot if requested soon ($lockTSE seconds) after invalidation.
1507  // This avoids stampedes when timestamps from $checkKeys/$touchedCb bump.
1508  ( $curTTL !== null && $curTTL <= 0 && abs( $curTTL ) <= $lockTSE ) ||
1509  // Assume a key is hot if there is no value and a busy fallback is given.
1510  // This avoids stampedes on eviction or preemptive regeneration taking too long.
1511  ( $busyValue !== null && $possValue === false );
1512 
1513  // If a regeneration lock is required, threads that do not get the lock will try to use
1514  // the stale value, the interim value, or the $busyValue placeholder, in that order. If
1515  // none of those are set then all threads will bypass the lock and regenerate the value.
1516  $hasLock = $useRegenerationLock && $this->claimStampedeLock( $key );
1517  if ( $useRegenerationLock && !$hasLock ) {
1518  if ( $this->isValid( $possValue, $possInfo['asOf'], $minAsOf ) ) {
1519  $this->logger->debug( "fetchOrRegenerate($key): returning stale value" );
1520  $this->stats->timing(
1521  "wanobjectcache.$kClass.hit.stale",
1522  1e3 * ( $this->getCurrentTime() - $initialTime )
1523  );
1524 
1525  return [ $possValue, $possInfo['version'], $curInfo['asOf'] ];
1526  } elseif ( $busyValue !== null ) {
1527  $miss = is_infinite( $minAsOf ) ? 'renew' : 'miss';
1528  $this->logger->debug( "fetchOrRegenerate($key): busy $miss" );
1529  $this->stats->timing(
1530  "wanobjectcache.$kClass.$miss.busy",
1531  1e3 * ( $this->getCurrentTime() - $initialTime )
1532  );
1533 
1534  return [ $this->resolveBusyValue( $busyValue ), $version, $curInfo['asOf'] ];
1535  }
1536  }
1537 
1538  // Generate the new value given any prior value with a matching version
1539  $setOpts = [];
1540  $preCallbackTime = $this->getCurrentTime();
1542  try {
1543  $value = $callback(
1544  ( $curInfo['version'] === $version ) ? $curValue : false,
1545  $ttl,
1546  $setOpts,
1547  ( $curInfo['version'] === $version ) ? $curInfo['asOf'] : null,
1548  $cbParams
1549  );
1550  } finally {
1552  }
1553  $postCallbackTime = $this->getCurrentTime();
1554 
1555  // How long it took to fetch, validate, and generate the value
1556  $elapsed = max( $postCallbackTime - $initialTime, 0.0 );
1557 
1558  // Attempt to save the newly generated value if applicable
1559  if (
1560  // Callback yielded a cacheable value
1561  ( $value !== false && $ttl >= 0 ) &&
1562  // Current thread was not raced out of a regeneration lock or key is tombstoned
1563  ( !$useRegenerationLock || $hasLock || $isKeyTombstoned ) &&
1564  // Key does not appear to be undergoing a set() stampede
1565  $this->checkAndSetCooloff( $key, $kClass, $value, $elapsed, $hasLock )
1566  ) {
1567  // How long it took to generate the value
1568  $walltime = max( $postCallbackTime - $preCallbackTime, 0.0 );
1569  $this->stats->timing( "wanobjectcache.$kClass.regen_walltime", 1e3 * $walltime );
1570  // If the key is write-holed then use the (volatile) interim key as an alternative
1571  if ( $isKeyTombstoned ) {
1572  $this->setInterimValue( $key, $value, $lockTSE, $version, $walltime );
1573  } else {
1574  $finalSetOpts = [
1575  // @phan-suppress-next-line PhanUselessBinaryAddRight
1576  'since' => $setOpts['since'] ?? $preCallbackTime,
1577  'version' => $version,
1578  'staleTTL' => $staleTTL,
1579  'lockTSE' => $lockTSE, // informs lag vs performance trade-offs
1580  'creating' => ( $curValue === false ), // optimization
1581  'walltime' => $walltime
1582  ] + $setOpts;
1583  $this->set( $key, $value, $ttl, $finalSetOpts );
1584  }
1585  }
1586 
1587  $this->yieldStampedeLock( $key, $hasLock );
1588 
1589  $miss = is_infinite( $minAsOf ) ? 'renew' : 'miss';
1590  $this->logger->debug( "fetchOrRegenerate($key): $miss, new value computed" );
1591  $this->stats->timing(
1592  "wanobjectcache.$kClass.$miss.compute",
1593  1e3 * ( $this->getCurrentTime() - $initialTime )
1594  );
1595 
1596  return [ $value, $version, $curInfo['asOf'] ];
1597  }
1598 
1603  private function claimStampedeLock( $key ) {
1604  // Note that locking is not bypassed due to I/O errors; this avoids stampedes
1605  return $this->cache->add(
1606  $this->makeSisterKey( $key, self::TYPE_MUTEX ),
1607  1,
1608  self::$LOCK_TTL
1609  );
1610  }
1611 
1616  private function yieldStampedeLock( $key, $hasLock ) {
1617  if ( $hasLock ) {
1618  // The backend might be a mcrouter proxy set to broadcast DELETE to *all* the local
1619  // datacenter cache servers via OperationSelectorRoute (for increased consistency).
1620  // Since that would be excessive for these locks, use TOUCH to expire the key.
1621  $this->cache->changeTTL(
1622  $this->makeSisterKey( $key, self::TYPE_MUTEX ),
1623  $this->getCurrentTime() - 60
1624  );
1625  }
1626  }
1627 
1635  private function makeSisterKeys( array $baseKeys, $type ) {
1636  $keys = [];
1637  foreach ( $baseKeys as $baseKey ) {
1638  $keys[] = $this->makeSisterKey( $baseKey, $type );
1639  }
1640 
1641  return $keys;
1642  }
1643 
1651  private function makeSisterKey( $baseKey, $typeChar ) {
1652  if ( $this->coalesceKeys === 'non-global' ) {
1653  $useColocationScheme = ( strncmp( $baseKey, "global:", 7 ) !== 0 );
1654  } else {
1655  $useColocationScheme = ( $this->coalesceKeys === true );
1656  }
1657 
1658  if ( !$useColocationScheme ) {
1659  // Old key style: "WANCache:<character>:<base key>"
1660  $fullKey = 'WANCache:' . $typeChar . ':' . $baseKey;
1661  } elseif ( $this->coalesceScheme === self::SCHEME_HASH_STOP ) {
1662  // Key style: "WANCache:<base key>|#|<character>"
1663  $fullKey = 'WANCache:' . $baseKey . '|#|' . $typeChar;
1664  } else {
1665  // Key style: "WANCache:{<base key>}:<character>"
1666  $fullKey = 'WANCache:{' . $baseKey . '}:' . $typeChar;
1667  }
1668 
1669  return $fullKey;
1670  }
1671 
1676  private function isVolatileValueAgeNegligible( $age ) {
1677  return ( $age < mt_rand( self::$RECENT_SET_LOW_MS, self::$RECENT_SET_HIGH_MS ) / 1e3 );
1678  }
1679 
1701  private function checkAndSetCooloff( $key, $kClass, $value, $elapsed, $hasLock ) {
1702  $valueKey = $this->makeSisterKey( $key, self::TYPE_VALUE );
1703  list( $estimatedSize ) = $this->cache->setNewPreparedValues( [ $valueKey => $value ] );
1704 
1705  if ( !$hasLock ) {
1706  // Suppose that this cache key is very popular (KEY_HIGH_QPS reads/second).
1707  // After eviction, there will be cache misses until it gets regenerated and saved.
1708  // If the time window when the key is missing lasts less than one second, then the
1709  // number of misses will not reach KEY_HIGH_QPS. This window largely corresponds to
1710  // the key regeneration time. Estimate the count/rate of cache misses, e.g.:
1711  // - 100 QPS, 20ms regeneration => ~2 misses (< 1s)
1712  // - 100 QPS, 100ms regeneration => ~10 misses (< 1s)
1713  // - 100 QPS, 3000ms regeneration => ~300 misses (100/s for 3s)
1714  $missesPerSecForHighQPS = ( min( $elapsed, 1 ) * $this->keyHighQps );
1715 
1716  // Determine whether there is enough I/O stampede risk to justify throttling set().
1717  // Estimate unthrottled set() overhead, as bps, from miss count/rate and value size,
1718  // comparing it to the per-key uplink bps limit (KEY_HIGH_UPLINK_BPS), e.g.:
1719  // - 2 misses (< 1s), 10KB value, 1250000 bps limit => 160000 bits (low risk)
1720  // - 2 misses (< 1s), 100KB value, 1250000 bps limit => 1600000 bits (high risk)
1721  // - 10 misses (< 1s), 10KB value, 1250000 bps limit => 800000 bits (low risk)
1722  // - 10 misses (< 1s), 100KB value, 1250000 bps limit => 8000000 bits (high risk)
1723  // - 300 misses (100/s), 1KB value, 1250000 bps limit => 800000 bps (low risk)
1724  // - 300 misses (100/s), 10KB value, 1250000 bps limit => 8000000 bps (high risk)
1725  // - 300 misses (100/s), 100KB value, 1250000 bps limit => 80000000 bps (high risk)
1726  if ( ( $missesPerSecForHighQPS * $estimatedSize ) >= $this->keyHighUplinkBps ) {
1727  $this->cache->clearLastError();
1728  if (
1729  !$this->cache->add(
1730  $this->makeSisterKey( $key, self::TYPE_COOLOFF ),
1731  1,
1732  self::$COOLOFF_TTL
1733  ) &&
1734  // Don't treat failures due to I/O errors as the key being in cooloff
1735  $this->cache->getLastError() === BagOStuff::ERR_NONE
1736  ) {
1737  $this->stats->increment( "wanobjectcache.$kClass.cooloff_bounce" );
1738 
1739  return false;
1740  }
1741  }
1742  }
1743 
1744  // Corresponding metrics for cache writes that actually get sent over the write
1745  $this->stats->timing( "wanobjectcache.$kClass.regen_set_delay", 1e3 * $elapsed );
1746  $this->stats->updateCount( "wanobjectcache.$kClass.regen_set_bytes", $estimatedSize );
1747 
1748  return true;
1749  }
1750 
1759  private function resolveCTL( $value, $curTTL, $curInfo, $touchedCallback ) {
1760  if ( $touchedCallback === null || $value === false ) {
1761  return [ $curTTL, max( $curInfo['tombAsOf'], $curInfo['lastCKPurge'] ) ];
1762  }
1763 
1764  $touched = $touchedCallback( $value );
1765  if ( $touched !== null && $touched >= $curInfo['asOf'] ) {
1766  $curTTL = min( $curTTL, self::$TINY_NEGATIVE, $curInfo['asOf'] - $touched );
1767  }
1768 
1769  return [ $curTTL, max( $curInfo['tombAsOf'], $curInfo['lastCKPurge'], $touched ) ];
1770  }
1771 
1779  private function resolveTouched( $value, $lastPurge, $touchedCallback ) {
1780  return ( $touchedCallback === null || $value === false )
1781  ? $lastPurge // nothing to derive the "touched timestamp" from
1782  : max( $touchedCallback( $value ), $lastPurge );
1783  }
1784 
1790  private function getInterimValue( $key, $minAsOf ) {
1791  $now = $this->getCurrentTime();
1792 
1793  if ( $this->useInterimHoldOffCaching ) {
1794  $wrapped = $this->cache->get(
1795  $this->makeSisterKey( $key, self::TYPE_INTERIM )
1796  );
1797 
1798  list( $value, $keyInfo ) = $this->unwrap( $wrapped, $now );
1799  if ( $this->isValid( $value, $keyInfo['asOf'], $minAsOf ) ) {
1800  return [ $value, $keyInfo ];
1801  }
1802  }
1803 
1804  return $this->unwrap( false, $now );
1805  }
1806 
1814  private function setInterimValue( $key, $value, $ttl, $version, $walltime ) {
1815  $ttl = max( self::$INTERIM_KEY_TTL, (int)$ttl );
1816 
1817  $wrapped = $this->wrap( $value, $ttl, $version, $this->getCurrentTime(), $walltime );
1818  $this->cache->merge(
1819  $this->makeSisterKey( $key, self::TYPE_INTERIM ),
1820  function () use ( $wrapped ) {
1821  return $wrapped;
1822  },
1823  $ttl,
1824  1
1825  );
1826  }
1827 
1832  private function resolveBusyValue( $busyValue ) {
1833  return ( $busyValue instanceof Closure ) ? $busyValue() : $busyValue;
1834  }
1835 
1900  final public function getMultiWithSetCallback(
1901  ArrayIterator $keyedIds, $ttl, callable $callback, array $opts = []
1902  ) {
1903  // Load required keys into process cache in one go
1904  $this->warmupCache = $this->getRawKeysForWarmup(
1905  $this->getNonProcessCachedMultiKeys( $keyedIds, $opts ),
1906  $opts['checkKeys'] ?? []
1907  );
1908  $this->warmupKeyMisses = 0;
1909 
1910  // The required callback signature includes $id as the first argument for convenience
1911  // to distinguish different items. To reuse the code in getWithSetCallback(), wrap the
1912  // callback with a proxy callback that has the standard getWithSetCallback() signature.
1913  // This is defined only once per batch to avoid closure creation overhead.
1914  $proxyCb = function ( $oldValue, &$ttl, &$setOpts, $oldAsOf, $params ) use ( $callback ) {
1915  return $callback( $params['id'], $oldValue, $ttl, $setOpts, $oldAsOf );
1916  };
1917 
1918  $values = [];
1919  foreach ( $keyedIds as $key => $id ) { // preserve order
1920  $values[$key] = $this->getWithSetCallback(
1921  $key,
1922  $ttl,
1923  $proxyCb,
1924  $opts,
1925  [ 'id' => $id ]
1926  );
1927  }
1928 
1929  $this->warmupCache = [];
1930 
1931  return $values;
1932  }
1933 
1999  final public function getMultiWithUnionSetCallback(
2000  ArrayIterator $keyedIds, $ttl, callable $callback, array $opts = []
2001  ) {
2002  $checkKeys = $opts['checkKeys'] ?? [];
2003  unset( $opts['lockTSE'] ); // incompatible
2004  unset( $opts['busyValue'] ); // incompatible
2005 
2006  // Load required keys into process cache in one go
2007  $keysByIdGet = $this->getNonProcessCachedMultiKeys( $keyedIds, $opts );
2008  $this->warmupCache = $this->getRawKeysForWarmup( $keysByIdGet, $checkKeys );
2009  $this->warmupKeyMisses = 0;
2010 
2011  // IDs of entities known to be in need of regeneration
2012  $idsRegen = [];
2013 
2014  // Find out which keys are missing/deleted/stale
2015  $curTTLs = [];
2016  $asOfs = [];
2017  $curByKey = $this->getMulti( $keysByIdGet, $curTTLs, $checkKeys, $asOfs );
2018  foreach ( $keysByIdGet as $id => $key ) {
2019  if ( !array_key_exists( $key, $curByKey ) || $curTTLs[$key] < 0 ) {
2020  $idsRegen[] = $id;
2021  }
2022  }
2023 
2024  // Run the callback to populate the regeneration value map for all required IDs
2025  $newSetOpts = [];
2026  $newTTLsById = array_fill_keys( $idsRegen, $ttl );
2027  $newValsById = $idsRegen ? $callback( $idsRegen, $newTTLsById, $newSetOpts ) : [];
2028 
2029  // The required callback signature includes $id as the first argument for convenience
2030  // to distinguish different items. To reuse the code in getWithSetCallback(), wrap the
2031  // callback with a proxy callback that has the standard getWithSetCallback() signature.
2032  // This is defined only once per batch to avoid closure creation overhead.
2033  $proxyCb = function ( $oldValue, &$ttl, &$setOpts, $oldAsOf, $params )
2034  use ( $callback, $newValsById, $newTTLsById, $newSetOpts )
2035  {
2036  $id = $params['id'];
2037 
2038  if ( array_key_exists( $id, $newValsById ) ) {
2039  // Value was already regerated as expected, so use the value in $newValsById
2040  $newValue = $newValsById[$id];
2041  $ttl = $newTTLsById[$id];
2042  $setOpts = $newSetOpts;
2043  } else {
2044  // Pre-emptive/popularity refresh and version mismatch cases are not detected
2045  // above and thus $newValsById has no entry. Run $callback on this single entity.
2046  $ttls = [ $id => $ttl ];
2047  $newValue = $callback( [ $id ], $ttls, $setOpts )[$id];
2048  $ttl = $ttls[$id];
2049  }
2050 
2051  return $newValue;
2052  };
2053 
2054  // Run the cache-aside logic using warmupCache instead of persistent cache queries
2055  $values = [];
2056  foreach ( $keyedIds as $key => $id ) { // preserve order
2057  $values[$key] = $this->getWithSetCallback(
2058  $key,
2059  $ttl,
2060  $proxyCb,
2061  $opts,
2062  [ 'id' => $id ]
2063  );
2064  }
2065 
2066  $this->warmupCache = [];
2067 
2068  return $values;
2069  }
2070 
2083  final public function reap( $key, $purgeTimestamp, &$isStale = false ) {
2084  $minAsOf = $purgeTimestamp + self::HOLDOFF_TTL;
2085  $wrapped = $this->cache->get( $this->makeSisterKey( $key, self::TYPE_VALUE ) );
2086  if ( is_array( $wrapped ) && $wrapped[self::$FLD_TIME] < $minAsOf ) {
2087  $isStale = true;
2088  $this->logger->warning( "Reaping stale value key '$key'." );
2089  $ttlReap = self::HOLDOFF_TTL; // avoids races with tombstone creation
2090  $ok = $this->cache->changeTTL(
2091  $this->makeSisterKey( $key, self::TYPE_VALUE ),
2092  $ttlReap
2093  );
2094  if ( !$ok ) {
2095  $this->logger->error( "Could not complete reap of key '$key'." );
2096  }
2097 
2098  return $ok;
2099  }
2100 
2101  $isStale = false;
2102 
2103  return true;
2104  }
2105 
2115  final public function reapCheckKey( $key, $purgeTimestamp, &$isStale = false ) {
2116  $purge = $this->parsePurgeValue(
2117  $this->cache->get( $this->makeSisterKey( $key, self::TYPE_TIMESTAMP ) )
2118  );
2119  if ( $purge && $purge[self::$PURGE_TIME] < $purgeTimestamp ) {
2120  $isStale = true;
2121  $this->logger->warning( "Reaping stale check key '$key'." );
2122  $ok = $this->cache->changeTTL(
2123  $this->makeSisterKey( $key, self::TYPE_TIMESTAMP ),
2124  self::TTL_SECOND
2125  );
2126  if ( !$ok ) {
2127  $this->logger->error( "Could not complete reap of check key '$key'." );
2128  }
2129 
2130  return $ok;
2131  }
2132 
2133  $isStale = false;
2134 
2135  return false;
2136  }
2137 
2145  public function makeKey( $class, ...$components ) {
2146  return $this->cache->makeKey( ...func_get_args() );
2147  }
2148 
2156  public function makeGlobalKey( $class, ...$components ) {
2157  return $this->cache->makeGlobalKey( ...func_get_args() );
2158  }
2159 
2167  public function hash256( $component ) {
2168  return hash_hmac( 'sha256', $component, $this->secret );
2169  }
2170 
2221  final public function makeMultiKeys( array $ids, $keyCallback ) {
2222  $idByKey = [];
2223  foreach ( $ids as $id ) {
2224  // Discourage triggering of automatic makeKey() hashing in some backends
2225  if ( strlen( $id ) > 64 ) {
2226  $this->logger->warning( __METHOD__ . ": long ID '$id'; use hash256()" );
2227  }
2228  $key = $keyCallback( $id, $this );
2229  // Edge case: ignore key collisions due to duplicate $ids like "42" and 42
2230  if ( !isset( $idByKey[$key] ) ) {
2231  $idByKey[$key] = $id;
2232  } elseif ( (string)$id !== (string)$idByKey[$key] ) {
2233  throw new UnexpectedValueException(
2234  "Cache key collision; IDs ('$id','{$idByKey[$key]}') map to '$key'"
2235  );
2236  }
2237  }
2238 
2239  return new ArrayIterator( $idByKey );
2240  }
2241 
2277  final public function multiRemap( array $ids, array $res ) {
2278  if ( count( $ids ) !== count( $res ) ) {
2279  // If makeMultiKeys() is called on a list of non-unique IDs, then the resulting
2280  // ArrayIterator will have less entries due to "first appearance" de-duplication
2281  $ids = array_keys( array_flip( $ids ) );
2282  if ( count( $ids ) !== count( $res ) ) {
2283  throw new UnexpectedValueException( "Multi-key result does not match ID list" );
2284  }
2285  }
2286 
2287  return array_combine( $ids, $res );
2288  }
2289 
2294  final public function getLastError() {
2295  $code = $this->cache->getLastError();
2296  switch ( $code ) {
2297  case BagOStuff::ERR_NONE:
2298  return self::ERR_NONE;
2299  case BagOStuff::ERR_NO_RESPONSE:
2300  return self::ERR_NO_RESPONSE;
2301  case BagOStuff::ERR_UNREACHABLE:
2302  return self::ERR_UNREACHABLE;
2303  default:
2304  return self::ERR_UNEXPECTED;
2305  }
2306  }
2307 
2311  final public function clearLastError() {
2312  $this->cache->clearLastError();
2313  }
2314 
2320  public function clearProcessCache() {
2321  $this->processCaches = [];
2322  }
2323 
2344  final public function useInterimHoldOffCaching( $enabled ) {
2345  $this->useInterimHoldOffCaching = $enabled;
2346  }
2347 
2353  public function getQoS( $flag ) {
2354  return $this->cache->getQoS( $flag );
2355  }
2356 
2420  public function adaptiveTTL( $mtime, $maxTTL, $minTTL = 30, $factor = 0.2 ) {
2421  if ( is_float( $mtime ) || ctype_digit( $mtime ) ) {
2422  $mtime = (int)$mtime; // handle fractional seconds and string integers
2423  }
2424 
2425  if ( !is_int( $mtime ) || $mtime <= 0 ) {
2426  return $minTTL; // no last-modified time provided
2427  }
2428 
2429  $age = $this->getCurrentTime() - $mtime;
2430 
2431  return (int)min( $maxTTL, max( $minTTL, $factor * $age ) );
2432  }
2433 
2438  final public function getWarmupKeyMisses() {
2439  return $this->warmupKeyMisses;
2440  }
2441 
2452  protected function relayPurge( $key, $ttl, $holdoff ) {
2453  if ( $this->mcrouterAware ) {
2454  // See https://github.com/facebook/mcrouter/wiki/Multi-cluster-broadcast-setup
2455  // Wildcards select all matching routes, e.g. the WAN cluster on all DCs
2456  $ok = $this->cache->set(
2457  "/*/{$this->cluster}/{$key}",
2458  $this->makePurgeValue( $this->getCurrentTime(), $holdoff ),
2459  $ttl
2460  );
2461  } else {
2462  // Some other proxy handles broadcasting or there is only one datacenter
2463  $ok = $this->cache->set(
2464  $key,
2465  $this->makePurgeValue( $this->getCurrentTime(), $holdoff ),
2466  $ttl
2467  );
2468  }
2469 
2470  return $ok;
2471  }
2472 
2479  protected function relayDelete( $key ) {
2480  if ( $this->mcrouterAware ) {
2481  // See https://github.com/facebook/mcrouter/wiki/Multi-cluster-broadcast-setup
2482  // Wildcards select all matching routes, e.g. the WAN cluster on all DCs
2483  $ok = $this->cache->delete( "/*/{$this->cluster}/{$key}" );
2484  } else {
2485  // Some other proxy handles broadcasting or there is only one datacenter
2486  $ok = $this->cache->delete( $key );
2487  }
2488 
2489  return $ok;
2490  }
2491 
2503  private function scheduleAsyncRefresh( $key, $ttl, $callback, array $opts, array $cbParams ) {
2504  if ( !$this->asyncHandler ) {
2505  return false;
2506  }
2507  // Update the cache value later, such during post-send of an HTTP request. This forces
2508  // cache regeneration by setting "minAsOf" to infinity, meaning that no existing value
2509  // is considered valid. Furthermore, note that preemptive regeneration is not applicable
2510  // to invalid values, so there is no risk of infinite preemptive regeneration loops.
2511  $func = $this->asyncHandler;
2512  $func( function () use ( $key, $ttl, $callback, $opts, $cbParams ) {
2513  $opts['minAsOf'] = INF;
2514  $this->fetchOrRegenerate( $key, $ttl, $callback, $opts, $cbParams );
2515  } );
2516 
2517  return true;
2518  }
2519 
2533  private function isAliveOrInGracePeriod( $curTTL, $graceTTL ) {
2534  if ( $curTTL > 0 ) {
2535  return true;
2536  } elseif ( $graceTTL <= 0 ) {
2537  return false;
2538  }
2539 
2540  $ageStale = abs( $curTTL ); // seconds of staleness
2541  $curGTTL = ( $graceTTL - $ageStale ); // current grace-time-to-live
2542  if ( $curGTTL <= 0 ) {
2543  return false; // already out of grace period
2544  }
2545 
2546  // Chance of using a stale value is the complement of the chance of refreshing it
2547  return !$this->worthRefreshExpiring( $curGTTL, $graceTTL );
2548  }
2549 
2563  protected function worthRefreshExpiring( $curTTL, $lowTTL ) {
2564  if ( $lowTTL <= 0 ) {
2565  return false;
2566  } elseif ( $curTTL >= $lowTTL ) {
2567  return false;
2568  } elseif ( $curTTL <= 0 ) {
2569  return false;
2570  }
2571 
2572  $chance = ( 1 - $curTTL / $lowTTL );
2573 
2574  // @phan-suppress-next-line PhanTypeMismatchArgumentInternal
2575  $decision = ( mt_rand( 1, 1e9 ) <= 1e9 * $chance );
2576 
2577  $this->logger->debug(
2578  "worthRefreshExpiring($curTTL, $lowTTL): " .
2579  "p = $chance; refresh = " . ( $decision ? 'Y' : 'N' )
2580  );
2581 
2582  return $decision;
2583  }
2584 
2600  protected function worthRefreshPopular( $asOf, $ageNew, $timeTillRefresh, $now ) {
2601  if ( $ageNew < 0 || $timeTillRefresh <= 0 ) {
2602  return false;
2603  }
2604 
2605  $age = $now - $asOf;
2606  $timeOld = $age - $ageNew;
2607  if ( $timeOld <= 0 ) {
2608  return false;
2609  }
2610 
2611  $popularHitsPerSec = 1;
2612  // Lifecycle is: new, ramp-up refresh chance, full refresh chance.
2613  // Note that the "expected # of refreshes" for the ramp-up time range is half
2614  // of what it would be if P(refresh) was at its full value during that time range.
2615  $refreshWindowSec = max( $timeTillRefresh - $ageNew - self::$RAMPUP_TTL / 2, 1 );
2616  // P(refresh) * (# hits in $refreshWindowSec) = (expected # of refreshes)
2617  // P(refresh) * ($refreshWindowSec * $popularHitsPerSec) = 1 (by definition)
2618  // P(refresh) = 1/($refreshWindowSec * $popularHitsPerSec)
2619  $chance = 1 / ( $popularHitsPerSec * $refreshWindowSec );
2620  // Ramp up $chance from 0 to its nominal value over RAMPUP_TTL seconds to avoid stampedes
2621  $chance *= ( $timeOld <= self::$RAMPUP_TTL ) ? $timeOld / self::$RAMPUP_TTL : 1;
2622 
2623  // @phan-suppress-next-line PhanTypeMismatchArgumentInternal
2624  $decision = ( mt_rand( 1, 1e9 ) <= 1e9 * $chance );
2625 
2626  $this->logger->debug(
2627  "worthRefreshPopular($asOf, $ageNew, $timeTillRefresh, $now): " .
2628  "p = $chance; refresh = " . ( $decision ? 'Y' : 'N' )
2629  );
2630 
2631  return $decision;
2632  }
2633 
2643  protected function isValid( $value, $asOf, $minAsOf, $purgeTime = null ) {
2644  // Avoid reading any key not generated after the latest delete() or touch
2645  $safeMinAsOf = max( $minAsOf, $purgeTime + self::$TINY_POSTIVE );
2646 
2647  if ( $value === false ) {
2648  return false;
2649  } elseif ( $safeMinAsOf > 0 && $asOf < $minAsOf ) {
2650  return false;
2651  }
2652 
2653  return true;
2654  }
2655 
2664  private function wrap( $value, $ttl, $version, $now, $walltime ) {
2665  // Returns keys in ascending integer order for PHP7 array packing:
2666  // https://nikic.github.io/2014/12/22/PHPs-new-hashtable-implementation.html
2667  $wrapped = [
2668  self::$FLD_FORMAT_VERSION => self::$VERSION,
2669  self::$FLD_VALUE => $value,
2670  self::$FLD_TTL => $ttl,
2671  self::$FLD_TIME => $now
2672  ];
2673  if ( $version !== null ) {
2674  $wrapped[self::$FLD_VALUE_VERSION] = $version;
2675  }
2676  if ( $walltime >= self::$GENERATION_SLOW_SEC ) {
2677  $wrapped[self::$FLD_GENERATION_TIME] = $walltime;
2678  }
2679 
2680  return $wrapped;
2681  }
2682 
2694  private function unwrap( $wrapped, $now ) {
2695  $value = false;
2696  $info = [ 'asOf' => null, 'curTTL' => null, 'version' => null, 'tombAsOf' => null ];
2697 
2698  if ( is_array( $wrapped ) ) {
2699  // Entry expected to be a cached value; validate it
2700  if (
2701  ( $wrapped[self::$FLD_FORMAT_VERSION] ?? null ) === self::$VERSION &&
2702  $wrapped[self::$FLD_TIME] >= $this->epoch
2703  ) {
2704  if ( $wrapped[self::$FLD_TTL] > 0 ) {
2705  // Get the approximate time left on the key
2706  $age = $now - $wrapped[self::$FLD_TIME];
2707  $curTTL = max( $wrapped[self::$FLD_TTL] - $age, 0.0 );
2708  } else {
2709  // Key had no TTL, so the time left is unbounded
2710  $curTTL = INF;
2711  }
2712  $value = $wrapped[self::$FLD_VALUE];
2713  $info['version'] = $wrapped[self::$FLD_VALUE_VERSION] ?? null;
2714  $info['asOf'] = $wrapped[self::$FLD_TIME];
2715  $info['curTTL'] = $curTTL;
2716  }
2717  } else {
2718  // Entry expected to be a tombstone; parse it
2719  $purge = $this->parsePurgeValue( $wrapped );
2720  if ( $purge !== false ) {
2721  // Tombstoned keys should always have a negative current $ttl
2722  $info['curTTL'] = min( $purge[self::$PURGE_TIME] - $now, self::$TINY_NEGATIVE );
2723  $info['tombAsOf'] = $purge[self::$PURGE_TIME];
2724  }
2725  }
2726 
2727  return [ $value, $info ];
2728  }
2729 
2734  private function determineKeyClassForStats( $key ) {
2735  $parts = explode( ':', $key, 3 );
2736  // Sanity fallback in case the key was not made by makeKey.
2737  // Replace dots because they are special in StatsD (T232907)
2738  return strtr( $parts[1] ?? $parts[0], '.', '_' );
2739  }
2740 
2746  private function parsePurgeValue( $value ) {
2747  if ( !is_string( $value ) ) {
2748  return false;
2749  }
2750 
2751  $segments = explode( ':', $value, 3 );
2752  if (
2753  !isset( $segments[0] ) ||
2754  !isset( $segments[1] ) ||
2755  "{$segments[0]}:" !== self::PURGE_VAL_PREFIX
2756  ) {
2757  return false;
2758  }
2759 
2760  if ( !isset( $segments[2] ) ) {
2761  // Back-compat with old purge values without holdoff
2762  $segments[2] = self::HOLDOFF_TTL;
2763  }
2764 
2765  if ( $segments[1] < $this->epoch ) {
2766  // Values this old are ignored
2767  return false;
2768  }
2769 
2770  return [
2771  self::$PURGE_TIME => (float)$segments[1],
2772  self::$PURGE_HOLDOFF => (int)$segments[2],
2773  ];
2774  }
2775 
2781  private function makePurgeValue( $timestamp, $holdoff ) {
2782  return self::PURGE_VAL_PREFIX . (float)$timestamp . ':' . (int)$holdoff;
2783  }
2784 
2789  private function getProcessCache( $group ) {
2790  if ( !isset( $this->processCaches[$group] ) ) {
2791  list( , $size ) = explode( ':', $group );
2792  $this->processCaches[$group] = new MapCacheLRU( (int)$size );
2793  if ( $this->wallClockOverride !== null ) {
2794  $this->processCaches[$group]->setMockTime( $this->wallClockOverride );
2795  }
2796  }
2797 
2798  return $this->processCaches[$group];
2799  }
2800 
2806  private function getProcessCacheKey( $key, $version ) {
2807  return $key . ' ' . (int)$version;
2808  }
2809 
2815  private function getNonProcessCachedMultiKeys( ArrayIterator $keys, array $opts ) {
2816  $pcTTL = $opts['pcTTL'] ?? self::TTL_UNCACHEABLE;
2817 
2818  $keysMissing = [];
2819  if ( $pcTTL > 0 && $this->callbackDepth == 0 ) {
2820  $version = $opts['version'] ?? null;
2821  $pCache = $this->getProcessCache( $opts['pcGroup'] ?? self::PC_PRIMARY );
2822  foreach ( $keys as $key => $id ) {
2823  if ( !$pCache->has( $this->getProcessCacheKey( $key, $version ), $pcTTL ) ) {
2824  $keysMissing[$id] = $key;
2825  }
2826  }
2827  }
2828 
2829  return $keysMissing;
2830  }
2831 
2837  private function getRawKeysForWarmup( array $keys, array $checkKeys ) {
2838  if ( !$keys ) {
2839  return [];
2840  }
2841 
2842  // Get all the value keys to fetch...
2843  $keysWarmup = $this->makeSisterKeys( $keys, self::TYPE_VALUE );
2844  // Get all the check keys to fetch...
2845  foreach ( $checkKeys as $i => $checkKeyOrKeyGroup ) {
2846  // Note: avoid array_merge() inside loop in case there are many keys
2847  if ( is_int( $i ) ) {
2848  // Single check key that applies to all value keys
2849  $keysWarmup[] = $this->makeSisterKey( $checkKeyOrKeyGroup, self::TYPE_TIMESTAMP );
2850  } else {
2851  // List of check keys that apply to a specific value key
2852  foreach ( (array)$checkKeyOrKeyGroup as $checkKey ) {
2853  $keysWarmup[] = $this->makeSisterKey( $checkKey, self::TYPE_TIMESTAMP );
2854  }
2855  }
2856  }
2857 
2858  $warmupCache = $this->cache->getMulti( $keysWarmup );
2859  $warmupCache += array_fill_keys( $keysWarmup, false );
2860 
2861  return $warmupCache;
2862  }
2863 
2868  protected function getCurrentTime() {
2869  if ( $this->wallClockOverride ) {
2870  return $this->wallClockOverride;
2871  }
2872 
2873  $clockTime = (float)time(); // call this first
2874  // microtime() uses an initial gettimeofday() call added to usage clocks.
2875  // This can severely drift from time() and the microtime() value of other threads
2876  // due to undercounting of the amount of time elapsed. Instead of seeing the current
2877  // time as being in the past, use the value of time(). This avoids setting cache values
2878  // that will immediately be seen as expired and possibly cause stampedes.
2879  return max( microtime( true ), $clockTime );
2880  }
2881 
2886  public function setMockTime( &$time ) {
2887  $this->wallClockOverride =& $time;
2888  $this->cache->setMockTime( $time );
2889  foreach ( $this->processCaches as $pCache ) {
2890  $pCache->setMockTime( $time );
2891  }
2892  }
2893 }
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:1356
WANObjectCache\getQoS
getQoS( $flag)
Definition: WANObjectCache.php:2353
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:2167
WANObjectCache\determineKeyClassForStats
determineKeyClassForStats( $key)
Definition: WANObjectCache.php:2734
WANObjectCache\relayDelete
relayDelete( $key)
Do the actual async bus delete of a key.
Definition: WANObjectCache.php:2479
WANObjectCache\setMockTime
setMockTime(&$time)
Definition: WANObjectCache.php:2886
WANObjectCache\makePurgeValue
makePurgeValue( $timestamp, $holdoff)
Definition: WANObjectCache.php:2781
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\makeGlobalKey
makeGlobalKey( $class,... $components)
Definition: WANObjectCache.php:2156
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:1416
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:2643
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:258
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:1900
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:71
WANObjectCache\getMultiWithUnionSetCallback
getMultiWithUnionSetCallback(ArrayIterator $keyedIds, $ttl, callable $callback, array $opts=[])
Method to fetch/regenerate multiple cache keys at once.
Definition: WANObjectCache.php:1999
WANObjectCache\resolveTouched
resolveTouched( $value, $lastPurge, $touchedCallback)
Definition: WANObjectCache.php:1779
WANObjectCache\getMulti
getMulti(array $keys, &$curTTLs=[], array $checkKeys=[], &$info=null)
Fetch the value of several keys from cache.
Definition: WANObjectCache.php:446
WANObjectCache\makeKey
makeKey( $class,... $components)
Definition: WANObjectCache.php:2145
$res
$res
Definition: testCompression.php:57
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:2115
WANObjectCache\getCheckKeyTime
getCheckKeyTime( $key)
Fetch the value of a timestamp "check" key.
Definition: WANObjectCache.php:868
WANObjectCache\resolveBusyValue
resolveBusyValue( $busyValue)
Definition: WANObjectCache.php:1832
WANObjectCache\$logger
LoggerInterface $logger
Definition: WANObjectCache.php:131
WANObjectCache\$FLD_VALUE
static int $FLD_VALUE
Key to the cached value.
Definition: WANObjectCache.php:248
WANObjectCache\$warmupKeyMisses
int $warmupKeyMisses
Key fetched.
Definition: WANObjectCache.php:164
WANObjectCache\newEmpty
static newEmpty()
Get an instance that wraps EmptyBagOStuff.
Definition: WANObjectCache.php:348
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:2320
WANObjectCache\getInterimValue
getInterimValue( $key, $minAsOf)
Definition: WANObjectCache.php:1790
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:2311
WANObjectCache\getCurrentTime
getCurrentTime()
Definition: WANObjectCache.php:2868
WANObjectCache\$FLD_FORMAT_VERSION
static int $FLD_FORMAT_VERSION
Key to WAN cache version number.
Definition: WANObjectCache.php:246
WANObjectCache\unwrap
unwrap( $wrapped, $now)
Definition: WANObjectCache.php:2694
WANObjectCache\getLastError
getLastError()
Get the "last error" registered; clearLastError() should be called manually.
Definition: WANObjectCache.php:2294
WANObjectCache\makeSisterKeys
makeSisterKeys(array $baseKeys, $type)
Get cache keys that should be collocated with their corresponding base keys.
Definition: WANObjectCache.php:1635
WANObjectCache\makeMultiKeys
makeMultiKeys(array $ids, $keyCallback)
Get an iterator of (cache key => entity ID) for a list of entity IDs.
Definition: WANObjectCache.php:2221
WANObjectCache\__construct
__construct(array $params)
Definition: WANObjectCache.php:310
WANObjectCache\useInterimHoldOffCaching
useInterimHoldOffCaching( $enabled)
Enable or disable the use of brief caching for tombstoned keys.
Definition: WANObjectCache.php:2344
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:2083
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:998
MapCacheLRU
Handles a simple LRU key/value map with a maximum number of entries.
Definition: MapCacheLRU.php:37
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:2664
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:2420
WANObjectCache\getNonProcessCachedMultiKeys
getNonProcessCachedMultiKeys(ArrayIterator $keys, array $opts)
Definition: WANObjectCache.php:2815
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:2452
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\isVolatileValueAgeNegligible
isVolatileValueAgeNegligible( $age)
Definition: WANObjectCache.php:1676
WANObjectCache\getMultiCheckKeyTime
getMultiCheckKeyTime(array $keys)
Fetch the values of each timestamp "check" key.
Definition: WANObjectCache.php:933
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:2600
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:2533
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\worthRefreshExpiring
worthRefreshExpiring( $curTTL, $lowTTL)
Check if a key is nearing expiration and thus due for randomized regeneration.
Definition: WANObjectCache.php:2563
WANObjectCache
Multi-datacenter aware caching interface.
Definition: WANObjectCache.php:125
WANObjectCache\getProcessCache
getProcessCache( $group)
Definition: WANObjectCache.php:2789
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:2277
WANObjectCache\getRawKeysForWarmup
getRawKeysForWarmup(array $keys, array $checkKeys)
Definition: WANObjectCache.php:2837
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:1701
WANObjectCache\$FLD_FLAGS
static int $FLD_FLAGS
@noinspection PhpUnusedPrivateFieldInspection
Definition: WANObjectCache.php:254
WANObjectCache\resetCheckKey
resetCheckKey( $key)
Delete a "check" key from all datacenters, invalidating keys that use it.
Definition: WANObjectCache.php:1039
WANObjectCache\claimStampedeLock
claimStampedeLock( $key)
Definition: WANObjectCache.php:1603
WANObjectCache\parsePurgeValue
parsePurgeValue( $value)
Definition: WANObjectCache.php:2746
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:2806
WANObjectCache\yieldStampedeLock
yieldStampedeLock( $key, $hasLock)
Definition: WANObjectCache.php:1616
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:2438
$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:1651
WANObjectCache\resolveCTL
resolveCTL( $value, $curTTL, $curInfo, $touchedCallback)
Definition: WANObjectCache.php:1759
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:339
WANObjectCache\$FLD_TTL
static int $FLD_TTL
Key to the original TTL.
Definition: WANObjectCache.php:250
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:256
WANObjectCache\$FLD_TIME
static int $FLD_TIME
Key to the cache timestamp.
Definition: WANObjectCache.php:252
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:1814
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:2503
WANObjectCache\processCheckKeys
processCheckKeys(array $timeKeys, array $wrappedValues, $now)
Definition: WANObjectCache.php:557
$type
$type
Definition: testCompression.php:52
WANObjectCache\$region
string $region
Physical region for mcrouter use.
Definition: WANObjectCache.php:140