MediaWiki  master
WANObjectCache.php
Go to the documentation of this file.
1 <?php
26 
116 class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInterface {
118  protected $cache;
120  protected $processCaches = [];
122  protected $logger;
124  protected $stats;
126  protected $asyncHandler;
127 
129  protected $mcrouterAware;
131  protected $region;
133  protected $cluster;
135  protected $useInterimHoldOffCaching = true;
137  protected $epoch;
139  protected $secret;
140 
142  private $callbackDepth = 0;
144  private $warmupCache = [];
146  private $warmupKeyMisses = 0;
147 
150 
152  const MAX_COMMIT_DELAY = 3;
154  const MAX_READ_LAG = 7;
156  const HOLDOFF_TTL = self::MAX_COMMIT_DELAY + self::MAX_READ_LAG + 1;
157 
159  const TTL_UNCACHEABLE = -1;
160 
162  const LOW_TTL = 30;
164  const TTL_LAGGED = 30;
165 
167  const HOT_TTR = 900;
169  const AGE_NEW = 60;
170 
172  const TSE_NONE = -1;
173 
175  const STALE_TTL_NONE = 0;
177  const GRACE_TTL_NONE = 0;
179  const HOLDOFF_TTL_NONE = 0;
181  const HOLDOFF_NONE = self::HOLDOFF_TTL_NONE;
182 
184  const MIN_TIMESTAMP_NONE = 0.0;
185 
187  const PC_PRIMARY = 'primary:1000';
188 
190  const PASS_BY_REF = -1;
191 
193  private static $CHECK_KEY_TTL = self::TTL_YEAR;
195  private static $INTERIM_KEY_TTL = 1;
196 
198  private static $LOCK_TTL = 10;
200  private static $COOLOFF_TTL = 1;
202  private static $RAMPUP_TTL = 30;
203 
205  private static $TINY_NEGATIVE = -0.000001;
207  private static $TINY_POSTIVE = 0.000001;
208 
210  private static $SET_DELAY_HIGH_MS = 50;
212  private static $RECENT_SET_LOW_MS = 50;
214  private static $RECENT_SET_HIGH_MS = 100;
215 
217  private static $GENERATION_SLOW_SEC = 3;
218 
220  private static $PURGE_TIME = 0;
222  private static $PURGE_HOLDOFF = 1;
223 
225  private static $VERSION = 1;
226 
228  private static $FLD_FORMAT_VERSION = 0;
230  private static $FLD_VALUE = 1;
232  private static $FLD_TTL = 2;
234  private static $FLD_TIME = 3;
236  private static $FLD_FLAGS = 4;
238  private static $FLD_VALUE_VERSION = 5;
240  private static $FLD_GENERATION_TIME = 6;
241 
242  private static $VALUE_KEY_PREFIX = 'WANCache:v:';
243  private static $INTERIM_KEY_PREFIX = 'WANCache:i:';
244  private static $TIME_KEY_PREFIX = 'WANCache:t:';
245  private static $MUTEX_KEY_PREFIX = 'WANCache:m:';
246  private static $COOLOFF_KEY_PREFIX = 'WANCache:c:';
247 
248  private static $PURGE_VAL_PREFIX = 'PURGED:';
249 
275  public function __construct( array $params ) {
276  $this->cache = $params['cache'];
277  $this->region = $params['region'] ?? 'main';
278  $this->cluster = $params['cluster'] ?? 'wan-main';
279  $this->mcrouterAware = !empty( $params['mcrouterAware'] );
280  $this->epoch = $params['epoch'] ?? 0;
281  $this->secret = $params['secret'] ?? (string)$this->epoch;
282 
283  $this->setLogger( $params['logger'] ?? new NullLogger() );
284  $this->stats = $params['stats'] ?? new NullStatsdDataFactory();
285  $this->asyncHandler = $params['asyncHandler'] ?? null;
286  }
287 
291  public function setLogger( LoggerInterface $logger ) {
292  $this->logger = $logger;
293  }
294 
300  public static function newEmpty() {
301  return new static( [ 'cache' => new EmptyBagOStuff() ] );
302  }
303 
354  final public function get(
355  $key, &$curTTL = null, array $checkKeys = [], &$info = null
356  ) {
357  $curTTLs = self::PASS_BY_REF;
358  $infoByKey = self::PASS_BY_REF;
359  $values = $this->getMulti( [ $key ], $curTTLs, $checkKeys, $infoByKey );
360  $curTTL = $curTTLs[$key] ?? null;
361  if ( $info === self::PASS_BY_REF ) {
362  $info = [
363  'asOf' => $infoByKey[$key]['asOf'] ?? null,
364  'tombAsOf' => $infoByKey[$key]['tombAsOf'] ?? null,
365  'lastCKPurge' => $infoByKey[$key]['lastCKPurge'] ?? null,
366  'version' => $infoByKey[$key]['version'] ?? null
367  ];
368  } else {
369  $info = $infoByKey[$key]['asOf'] ?? null; // b/c
370  }
371 
372  return $values[$key] ?? false;
373  }
374 
397  final public function getMulti(
398  array $keys,
399  &$curTTLs = [],
400  array $checkKeys = [],
401  &$info = null
402  ) {
403  $result = [];
404  $curTTLs = [];
405  $infoByKey = [];
406 
407  $vPrefixLen = strlen( self::$VALUE_KEY_PREFIX );
408  $valueKeys = self::prefixCacheKeys( $keys, self::$VALUE_KEY_PREFIX );
409 
410  $checkKeysForAll = [];
411  $checkKeysByKey = [];
412  $checkKeysFlat = [];
413  foreach ( $checkKeys as $i => $checkKeyGroup ) {
414  $prefixed = self::prefixCacheKeys( (array)$checkKeyGroup, self::$TIME_KEY_PREFIX );
415  $checkKeysFlat = array_merge( $checkKeysFlat, $prefixed );
416  // Are these check keys for a specific cache key, or for all keys being fetched?
417  if ( is_int( $i ) ) {
418  $checkKeysForAll = array_merge( $checkKeysForAll, $prefixed );
419  } else {
420  $checkKeysByKey[$i] = $prefixed;
421  }
422  }
423 
424  // Fetch all of the raw values
425  $keysGet = array_merge( $valueKeys, $checkKeysFlat );
426  if ( $this->warmupCache ) {
427  $wrappedValues = array_intersect_key( $this->warmupCache, array_flip( $keysGet ) );
428  $keysGet = array_diff( $keysGet, array_keys( $wrappedValues ) ); // keys left to fetch
429  $this->warmupKeyMisses += count( $keysGet );
430  } else {
431  $wrappedValues = [];
432  }
433  if ( $keysGet ) {
434  $wrappedValues += $this->cache->getMulti( $keysGet );
435  }
436  // Time used to compare/init "check" keys (derived after getMulti() to be pessimistic)
437  $now = $this->getCurrentTime();
438 
439  // Collect timestamps from all "check" keys
440  $purgeValuesForAll = $this->processCheckKeys( $checkKeysForAll, $wrappedValues, $now );
441  $purgeValuesByKey = [];
442  foreach ( $checkKeysByKey as $cacheKey => $checks ) {
443  $purgeValuesByKey[$cacheKey] =
444  $this->processCheckKeys( $checks, $wrappedValues, $now );
445  }
446 
447  // Get the main cache value for each key and validate them
448  foreach ( $valueKeys as $vKey ) {
449  $key = substr( $vKey, $vPrefixLen ); // unprefix
450  list( $value, $keyInfo ) = $this->unwrap( $wrappedValues[$vKey] ?? false, $now );
451  // Force dependent keys to be seen as stale for a while after purging
452  // to reduce race conditions involving stale data getting cached
453  $purgeValues = $purgeValuesForAll;
454  if ( isset( $purgeValuesByKey[$key] ) ) {
455  $purgeValues = array_merge( $purgeValues, $purgeValuesByKey[$key] );
456  }
457 
458  $lastCKPurge = null; // timestamp of the highest check key
459  foreach ( $purgeValues as $purge ) {
460  $lastCKPurge = max( $purge[self::$PURGE_TIME], $lastCKPurge );
461  $safeTimestamp = $purge[self::$PURGE_TIME] + $purge[self::$PURGE_HOLDOFF];
462  if ( $value !== false && $safeTimestamp >= $keyInfo['asOf'] ) {
463  // How long ago this value was invalidated by *this* check key
464  $ago = min( $purge[self::$PURGE_TIME] - $now, self::$TINY_NEGATIVE );
465  // How long ago this value was invalidated by *any* known check key
466  $keyInfo['curTTL'] = min( $keyInfo['curTTL'], $ago );
467  }
468  }
469  $keyInfo[ 'lastCKPurge'] = $lastCKPurge;
470 
471  if ( $value !== false ) {
472  $result[$key] = $value;
473  }
474  if ( $keyInfo['curTTL'] !== null ) {
475  $curTTLs[$key] = $keyInfo['curTTL'];
476  }
477 
478  $infoByKey[$key] = ( $info === self::PASS_BY_REF )
479  ? $keyInfo
480  : $keyInfo['asOf']; // b/c
481  }
482 
483  $info = $infoByKey;
484 
485  return $result;
486  }
487 
495  private function processCheckKeys( array $timeKeys, array $wrappedValues, $now ) {
496  $purgeValues = [];
497  foreach ( $timeKeys as $timeKey ) {
498  $purge = isset( $wrappedValues[$timeKey] )
499  ? $this->parsePurgeValue( $wrappedValues[$timeKey] )
500  : false;
501  if ( $purge === false ) {
502  // Key is not set or malformed; regenerate
503  $newVal = $this->makePurgeValue( $now, self::HOLDOFF_TTL );
504  $this->cache->add( $timeKey, $newVal, self::$CHECK_KEY_TTL );
505  $purge = $this->parsePurgeValue( $newVal );
506  }
507  $purgeValues[] = $purge;
508  }
509 
510  return $purgeValues;
511  }
512 
589  final public function set( $key, $value, $ttl = self::TTL_INDEFINITE, array $opts = [] ) {
590  $now = $this->getCurrentTime();
591  $lag = $opts['lag'] ?? 0;
592  $age = isset( $opts['since'] ) ? max( 0, $now - $opts['since'] ) : 0;
593  $pending = $opts['pending'] ?? false;
594  $lockTSE = $opts['lockTSE'] ?? self::TSE_NONE;
595  $staleTTL = $opts['staleTTL'] ?? self::STALE_TTL_NONE;
596  $creating = $opts['creating'] ?? false;
597  $version = $opts['version'] ?? null;
598  $walltime = $opts['walltime'] ?? 0.0;
599 
600  if ( $ttl < 0 ) {
601  return true;
602  }
603 
604  // Do not cache potentially uncommitted data as it might get rolled back
605  if ( $pending ) {
606  $this->logger->info(
607  'Rejected set() for {cachekey} due to pending writes.',
608  [ 'cachekey' => $key ]
609  );
610 
611  return true; // no-op the write for being unsafe
612  }
613 
614  $logicalTTL = null; // logical TTL override
615  // Check if there's a risk of writing stale data after the purge tombstone expired
616  if ( $lag === false || ( $lag + $age ) > self::MAX_READ_LAG ) {
617  // Case A: any long-running transaction
618  if ( $age > self::MAX_READ_LAG ) {
619  if ( $lockTSE >= 0 ) {
620  // Store value as *almost* stale to avoid cache and mutex stampedes
621  $logicalTTL = self::TTL_SECOND;
622  $this->logger->info(
623  'Lowered set() TTL for {cachekey} due to snapshot lag.',
624  [ 'cachekey' => $key, 'lag' => $lag, 'age' => $age ]
625  );
626  } else {
627  $this->logger->info(
628  'Rejected set() for {cachekey} due to snapshot lag.',
629  [ 'cachekey' => $key, 'lag' => $lag, 'age' => $age ]
630  );
631 
632  return true; // no-op the write for being unsafe
633  }
634  // Case B: high replication lag; lower TTL instead of ignoring all set()s
635  } elseif ( $lag === false || $lag > self::MAX_READ_LAG ) {
636  if ( $lockTSE >= 0 ) {
637  $logicalTTL = min( $ttl ?: INF, self::TTL_LAGGED );
638  } else {
639  $ttl = min( $ttl ?: INF, self::TTL_LAGGED );
640  }
641  $this->logger->warning(
642  'Lowered set() TTL for {cachekey} due to replication lag.',
643  [ 'cachekey' => $key, 'lag' => $lag, 'age' => $age ]
644  );
645  // Case C: medium length request with medium replication lag
646  } elseif ( $lockTSE >= 0 ) {
647  // Store value as *almost* stale to avoid cache and mutex stampedes
648  $logicalTTL = self::TTL_SECOND;
649  $this->logger->info(
650  'Lowered set() TTL for {cachekey} due to high read lag.',
651  [ 'cachekey' => $key, 'lag' => $lag, 'age' => $age ]
652  );
653  } else {
654  $this->logger->info(
655  'Rejected set() for {cachekey} due to high read lag.',
656  [ 'cachekey' => $key, 'lag' => $lag, 'age' => $age ]
657  );
658 
659  return true; // no-op the write for being unsafe
660  }
661  }
662 
663  // Wrap that value with time/TTL/version metadata
664  $wrapped = $this->wrap( $value, $logicalTTL ?: $ttl, $version, $now, $walltime );
665  $storeTTL = $ttl + $staleTTL;
666 
667  if ( $creating ) {
668  $ok = $this->cache->add( self::$VALUE_KEY_PREFIX . $key, $wrapped, $storeTTL );
669  } else {
670  $ok = $this->cache->merge(
671  self::$VALUE_KEY_PREFIX . $key,
672  function ( $cache, $key, $cWrapped ) use ( $wrapped ) {
673  // A string value means that it is a tombstone; do nothing in that case
674  return ( is_string( $cWrapped ) ) ? false : $wrapped;
675  },
676  $storeTTL,
677  1 // 1 attempt
678  );
679  }
680 
681  return $ok;
682  }
683 
745  final public function delete( $key, $ttl = self::HOLDOFF_TTL ) {
746  if ( $ttl <= 0 ) {
747  // Publish the purge to all datacenters
748  $ok = $this->relayDelete( self::$VALUE_KEY_PREFIX . $key );
749  } else {
750  // Publish the purge to all datacenters
751  $ok = $this->relayPurge( self::$VALUE_KEY_PREFIX . $key, $ttl, self::HOLDOFF_TTL_NONE );
752  }
753 
754  $kClass = $this->determineKeyClassForStats( $key );
755  $this->stats->increment( "wanobjectcache.$kClass.delete." . ( $ok ? 'ok' : 'error' ) );
756 
757  return $ok;
758  }
759 
779  final public function getCheckKeyTime( $key ) {
780  return $this->getMultiCheckKeyTime( [ $key ] )[$key];
781  }
782 
844  final public function getMultiCheckKeyTime( array $keys ) {
845  $rawKeys = [];
846  foreach ( $keys as $key ) {
847  $rawKeys[$key] = self::$TIME_KEY_PREFIX . $key;
848  }
849 
850  $rawValues = $this->cache->getMulti( $rawKeys );
851  $rawValues += array_fill_keys( $rawKeys, false );
852 
853  $times = [];
854  foreach ( $rawKeys as $key => $rawKey ) {
855  $purge = $this->parsePurgeValue( $rawValues[$rawKey] );
856  if ( $purge !== false ) {
857  $time = $purge[self::$PURGE_TIME];
858  } else {
859  // Casting assures identical floats for the next getCheckKeyTime() calls
860  $now = (string)$this->getCurrentTime();
861  $this->cache->add(
862  $rawKey,
863  $this->makePurgeValue( $now, self::HOLDOFF_TTL ),
864  self::$CHECK_KEY_TTL
865  );
866  $time = (float)$now;
867  }
868 
869  $times[$key] = $time;
870  }
871 
872  return $times;
873  }
874 
909  final public function touchCheckKey( $key, $holdoff = self::HOLDOFF_TTL ) {
910  // Publish the purge to all datacenters
911  $ok = $this->relayPurge( self::$TIME_KEY_PREFIX . $key, self::$CHECK_KEY_TTL, $holdoff );
912 
913  $kClass = $this->determineKeyClassForStats( $key );
914  $this->stats->increment( "wanobjectcache.$kClass.ck_touch." . ( $ok ? 'ok' : 'error' ) );
915 
916  return $ok;
917  }
918 
946  final public function resetCheckKey( $key ) {
947  // Publish the purge to all datacenters
948  $ok = $this->relayDelete( self::$TIME_KEY_PREFIX . $key );
949 
950  $kClass = $this->determineKeyClassForStats( $key );
951  $this->stats->increment( "wanobjectcache.$kClass.ck_reset." . ( $ok ? 'ok' : 'error' ) );
952 
953  return $ok;
954  }
955 
1261  final public function getWithSetCallback( $key, $ttl, $callback, array $opts = [] ) {
1262  $version = $opts['version'] ?? null;
1263  $pcTTL = $opts['pcTTL'] ?? self::TTL_UNCACHEABLE;
1264  $pCache = ( $pcTTL >= 0 )
1265  ? $this->getProcessCache( $opts['pcGroup'] ?? self::PC_PRIMARY )
1266  : null;
1267 
1268  // Use the process cache if requested as long as no outer cache callback is running.
1269  // Nested callback process cache use is not lag-safe with regard to HOLDOFF_TTL since
1270  // process cached values are more lagged than persistent ones as they are not purged.
1271  if ( $pCache && $this->callbackDepth == 0 ) {
1272  $cached = $pCache->get( $this->getProcessCacheKey( $key, $version ), $pcTTL, false );
1273  if ( $cached !== false ) {
1274  return $cached;
1275  }
1276  }
1277 
1278  $res = $this->fetchOrRegenerate( $key, $ttl, $callback, $opts );
1279  list( $value, $valueVersion, $curAsOf ) = $res;
1280  if ( $valueVersion !== $version ) {
1281  // Current value has a different version; use the variant key for this version.
1282  // Regenerate the variant value if it is not newer than the main value at $key
1283  // so that purges to the main key propagate to the variant value.
1284  list( $value ) = $this->fetchOrRegenerate(
1285  $this->makeGlobalKey( 'WANCache-key-variant', md5( $key ), $version ),
1286  $ttl,
1287  $callback,
1288  [ 'version' => null, 'minAsOf' => $curAsOf ] + $opts
1289  );
1290  }
1291 
1292  // Update the process cache if enabled
1293  if ( $pCache && $value !== false ) {
1294  $pCache->set( $this->getProcessCacheKey( $key, $version ), $value );
1295  }
1296 
1297  return $value;
1298  }
1299 
1316  private function fetchOrRegenerate( $key, $ttl, $callback, array $opts ) {
1317  $checkKeys = $opts['checkKeys'] ?? [];
1318  $graceTTL = $opts['graceTTL'] ?? self::GRACE_TTL_NONE;
1319  $minAsOf = $opts['minAsOf'] ?? self::MIN_TIMESTAMP_NONE;
1320  $hotTTR = $opts['hotTTR'] ?? self::HOT_TTR;
1321  $lowTTL = $opts['lowTTL'] ?? min( self::LOW_TTL, $ttl );
1322  $ageNew = $opts['ageNew'] ?? self::AGE_NEW;
1323  $touchedCb = $opts['touchedCallback'] ?? null;
1324  $initialTime = $this->getCurrentTime();
1325 
1326  $kClass = $this->determineKeyClassForStats( $key );
1327 
1328  // Get the current key value and its metadata
1329  $curTTL = self::PASS_BY_REF;
1330  $curInfo = self::PASS_BY_REF;
1331  $curValue = $this->get( $key, $curTTL, $checkKeys, $curInfo );
1332  // Apply any $touchedCb invalidation timestamp to get the "last purge timestamp"
1333  list( $curTTL, $LPT ) = $this->resolveCTL( $curValue, $curTTL, $curInfo, $touchedCb );
1334  // Use the cached value if it exists and is not due for synchronous regeneration
1335  if (
1336  $this->isValid( $curValue, $curInfo['asOf'], $minAsOf ) &&
1337  $this->isAliveOrInGracePeriod( $curTTL, $graceTTL )
1338  ) {
1339  $preemptiveRefresh = (
1340  $this->worthRefreshExpiring( $curTTL, $lowTTL ) ||
1341  $this->worthRefreshPopular( $curInfo['asOf'], $ageNew, $hotTTR, $initialTime )
1342  );
1343  if ( !$preemptiveRefresh ) {
1344  $this->stats->increment( "wanobjectcache.$kClass.hit.good" );
1345 
1346  return [ $curValue, $curInfo['version'], $curInfo['asOf'] ];
1347  } elseif ( $this->scheduleAsyncRefresh( $key, $ttl, $callback, $opts ) ) {
1348  $this->stats->increment( "wanobjectcache.$kClass.hit.refresh" );
1349 
1350  return [ $curValue, $curInfo['version'], $curInfo['asOf'] ];
1351  }
1352  }
1353 
1354  // Determine if there is stale or volatile cached value that is still usable
1355  $isKeyTombstoned = ( $curInfo['tombAsOf'] !== null );
1356  if ( $isKeyTombstoned ) {
1357  // Key is write-holed; use the (volatile) interim key as an alternative
1358  list( $possValue, $possInfo ) = $this->getInterimValue( $key, $minAsOf );
1359  // Update the "last purge time" since the $touchedCb timestamp depends on $value
1360  $LPT = $this->resolveTouched( $possValue, $LPT, $touchedCb );
1361  } else {
1362  $possValue = $curValue;
1363  $possInfo = $curInfo;
1364  }
1365 
1366  // Avoid overhead from callback runs, regeneration locks, and cache sets during
1367  // hold-off periods for the key by reusing very recently generated cached values
1368  if (
1369  $this->isValid( $possValue, $possInfo['asOf'], $minAsOf, $LPT ) &&
1370  $this->isVolatileValueAgeNegligible( $initialTime - $possInfo['asOf'] )
1371  ) {
1372  $this->stats->increment( "wanobjectcache.$kClass.hit.volatile" );
1373 
1374  return [ $possValue, $possInfo['version'], $curInfo['asOf'] ];
1375  }
1376 
1377  $lockTSE = $opts['lockTSE'] ?? self::TSE_NONE;
1378  $busyValue = $opts['busyValue'] ?? null;
1379  $staleTTL = $opts['staleTTL'] ?? self::STALE_TTL_NONE;
1380  $version = $opts['version'] ?? null;
1381 
1382  // Determine whether one thread per datacenter should handle regeneration at a time
1383  $useRegenerationLock =
1384  // Note that since tombstones no-op set(), $lockTSE and $curTTL cannot be used to
1385  // deduce the key hotness because |$curTTL| will always keep increasing until the
1386  // tombstone expires or is overwritten by a new tombstone. Also, even if $lockTSE
1387  // is not set, constant regeneration of a key for the tombstone lifetime might be
1388  // very expensive. Assume tombstoned keys are possibly hot in order to reduce
1389  // the risk of high regeneration load after the delete() method is called.
1390  $isKeyTombstoned ||
1391  // Assume a key is hot if requested soon ($lockTSE seconds) after invalidation.
1392  // This avoids stampedes when timestamps from $checkKeys/$touchedCb bump.
1393  ( $curTTL !== null && $curTTL <= 0 && abs( $curTTL ) <= $lockTSE ) ||
1394  // Assume a key is hot if there is no value and a busy fallback is given.
1395  // This avoids stampedes on eviction or preemptive regeneration taking too long.
1396  ( $busyValue !== null && $possValue === false );
1397 
1398  // If a regeneration lock is required, threads that do not get the lock will try to use
1399  // the stale value, the interim value, or the $busyValue placeholder, in that order. If
1400  // none of those are set then all threads will bypass the lock and regenerate the value.
1401  $hasLock = $useRegenerationLock && $this->claimStampedeLock( $key );
1402  if ( $useRegenerationLock && !$hasLock ) {
1403  if ( $this->isValid( $possValue, $possInfo['asOf'], $minAsOf ) ) {
1404  $this->stats->increment( "wanobjectcache.$kClass.hit.stale" );
1405 
1406  return [ $possValue, $possInfo['version'], $curInfo['asOf'] ];
1407  } elseif ( $busyValue !== null ) {
1408  $miss = is_infinite( $minAsOf ) ? 'renew' : 'miss';
1409  $this->stats->increment( "wanobjectcache.$kClass.$miss.busy" );
1410 
1411  return [ $this->resolveBusyValue( $busyValue ), $version, $curInfo['asOf'] ];
1412  }
1413  }
1414 
1415  // Generate the new value given any prior value with a matching version
1416  $setOpts = [];
1417  $preCallbackTime = $this->getCurrentTime();
1419  try {
1420  $value = $callback(
1421  ( $curInfo['version'] === $version ) ? $curValue : false,
1422  $ttl,
1423  $setOpts,
1424  ( $curInfo['version'] === $version ) ? $curInfo['asOf'] : null
1425  );
1426  } finally {
1428  }
1429  $postCallbackTime = $this->getCurrentTime();
1430 
1431  // How long it took to fetch, validate, and generate the value
1432  $elapsed = max( $postCallbackTime - $initialTime, 0.0 );
1433 
1434  // Attempt to save the newly generated value if applicable
1435  if (
1436  // Callback yielded a cacheable value
1437  ( $value !== false && $ttl >= 0 ) &&
1438  // Current thread was not raced out of a regeneration lock or key is tombstoned
1439  ( !$useRegenerationLock || $hasLock || $isKeyTombstoned ) &&
1440  // Key does not appear to be undergoing a set() stampede
1441  $this->checkAndSetCooloff( $key, $kClass, $elapsed, $lockTSE, $hasLock )
1442  ) {
1443  // How long it took to generate the value
1444  $walltime = max( $postCallbackTime - $preCallbackTime, 0.0 );
1445  $this->stats->timing( "wanobjectcache.$kClass.regen_walltime", 1e3 * $walltime );
1446  // If the key is write-holed then use the (volatile) interim key as an alternative
1447  if ( $isKeyTombstoned ) {
1448  $this->setInterimValue( $key, $value, $lockTSE, $version, $walltime );
1449  } else {
1450  $finalSetOpts = [
1451  'since' => $setOpts['since'] ?? $preCallbackTime,
1452  'version' => $version,
1453  'staleTTL' => $staleTTL,
1454  'lockTSE' => $lockTSE, // informs lag vs performance trade-offs
1455  'creating' => ( $curValue === false ), // optimization
1456  'walltime' => $walltime
1457  ] + $setOpts;
1458  $this->set( $key, $value, $ttl, $finalSetOpts );
1459  }
1460  }
1461 
1462  $this->yieldStampedeLock( $key, $hasLock );
1463 
1464  $miss = is_infinite( $minAsOf ) ? 'renew' : 'miss';
1465  $this->stats->increment( "wanobjectcache.$kClass.$miss.compute" );
1466 
1467  return [ $value, $version, $curInfo['asOf'] ];
1468  }
1469 
1474  private function claimStampedeLock( $key ) {
1475  // Note that locking is not bypassed due to I/O errors; this avoids stampedes
1476  return $this->cache->add( self::$MUTEX_KEY_PREFIX . $key, 1, self::$LOCK_TTL );
1477  }
1478 
1483  private function yieldStampedeLock( $key, $hasLock ) {
1484  if ( $hasLock ) {
1485  // The backend might be a mcrouter proxy set to broadcast DELETE to *all* the local
1486  // datacenter cache servers via OperationSelectorRoute (for increased consistency).
1487  // Since that would be excessive for these locks, use TOUCH to expire the key.
1488  $this->cache->changeTTL( self::$MUTEX_KEY_PREFIX . $key, $this->getCurrentTime() - 60 );
1489  }
1490  }
1491 
1496  private function isVolatileValueAgeNegligible( $age ) {
1497  return ( $age < mt_rand( self::$RECENT_SET_LOW_MS, self::$RECENT_SET_HIGH_MS ) / 1e3 );
1498  }
1499 
1508  private function checkAndSetCooloff( $key, $kClass, $elapsed, $lockTSE, $hasLock ) {
1509  $this->stats->timing( "wanobjectcache.$kClass.regen_set_delay", 1e3 * $elapsed );
1510 
1511  // If $lockTSE is set, the lock was bypassed because there was no stale/interim value,
1512  // and $elapsed indicates that regeration is slow, then there is a risk of set()
1513  // stampedes with large blobs. With a typical scale-out infrastructure, CPU and query
1514  // load from $callback invocations is distributed among appservers and replica DBs,
1515  // but cache operations for a given key route to a single cache server (e.g. striped
1516  // consistent hashing).
1517  if ( $lockTSE < 0 || $hasLock ) {
1518  return true; // either not a priori hot or thread has the lock
1519  } elseif ( $elapsed <= self::$SET_DELAY_HIGH_MS * 1e3 ) {
1520  return true; // not enough time for threads to pile up
1521  }
1522 
1523  $this->cache->clearLastError();
1524  if (
1525  !$this->cache->add( self::$COOLOFF_KEY_PREFIX . $key, 1, self::$COOLOFF_TTL ) &&
1526  // Don't treat failures due to I/O errors as the key being in cooloff
1527  $this->cache->getLastError() === BagOStuff::ERR_NONE
1528  ) {
1529  $this->stats->increment( "wanobjectcache.$kClass.cooloff_bounce" );
1530 
1531  return false;
1532  }
1533 
1534  return true;
1535  }
1536 
1545  private function resolveCTL( $value, $curTTL, $curInfo, $touchedCallback ) {
1546  if ( $touchedCallback === null || $value === false ) {
1547  return [ $curTTL, max( $curInfo['tombAsOf'], $curInfo['lastCKPurge'] ) ];
1548  }
1549 
1550  $touched = $touchedCallback( $value );
1551  if ( $touched !== null && $touched >= $curInfo['asOf'] ) {
1552  $curTTL = min( $curTTL, self::$TINY_NEGATIVE, $curInfo['asOf'] - $touched );
1553  }
1554 
1555  return [ $curTTL, max( $curInfo['tombAsOf'], $curInfo['lastCKPurge'], $touched ) ];
1556  }
1557 
1565  private function resolveTouched( $value, $lastPurge, $touchedCallback ) {
1566  return ( $touchedCallback === null || $value === false )
1567  ? $lastPurge // nothing to derive the "touched timestamp" from
1568  : max( $touchedCallback( $value ), $lastPurge );
1569  }
1570 
1576  private function getInterimValue( $key, $minAsOf ) {
1577  $now = $this->getCurrentTime();
1578 
1579  if ( $this->useInterimHoldOffCaching ) {
1580  $wrapped = $this->cache->get( self::$INTERIM_KEY_PREFIX . $key );
1581 
1582  list( $value, $keyInfo ) = $this->unwrap( $wrapped, $now );
1583  if ( $this->isValid( $value, $keyInfo['asOf'], $minAsOf ) ) {
1584  return [ $value, $keyInfo ];
1585  }
1586  }
1587 
1588  return $this->unwrap( false, $now );
1589  }
1590 
1598  private function setInterimValue( $key, $value, $ttl, $version, $walltime ) {
1599  $ttl = max( self::$INTERIM_KEY_TTL, (int)$ttl );
1600 
1601  $wrapped = $this->wrap( $value, $ttl, $version, $this->getCurrentTime(), $walltime );
1602  $this->cache->merge(
1603  self::$INTERIM_KEY_PREFIX . $key,
1604  function () use ( $wrapped ) {
1605  return $wrapped;
1606  },
1607  $ttl,
1608  1
1609  );
1610  }
1611 
1616  private function resolveBusyValue( $busyValue ) {
1617  return ( $busyValue instanceof Closure ) ? $busyValue() : $busyValue;
1618  }
1619 
1686  final public function getMultiWithSetCallback(
1687  ArrayIterator $keyedIds, $ttl, callable $callback, array $opts = []
1688  ) {
1689  // Load required keys into process cache in one go
1690  $this->warmupCache = $this->getRawKeysForWarmup(
1691  $this->getNonProcessCachedMultiKeys( $keyedIds, $opts ),
1692  $opts['checkKeys'] ?? []
1693  );
1694  $this->warmupKeyMisses = 0;
1695 
1696  // Wrap $callback to match the getWithSetCallback() format while passing $id to $callback
1697  $id = null; // current entity ID
1698  $func = function ( $oldValue, &$ttl, &$setOpts, $oldAsOf ) use ( $callback, &$id ) {
1699  return $callback( $id, $oldValue, $ttl, $setOpts, $oldAsOf );
1700  };
1701 
1702  $values = [];
1703  foreach ( $keyedIds as $key => $id ) { // preserve order
1704  $values[$key] = $this->getWithSetCallback( $key, $ttl, $func, $opts );
1705  }
1706 
1707  $this->warmupCache = [];
1708 
1709  return $values;
1710  }
1711 
1777  final public function getMultiWithUnionSetCallback(
1778  ArrayIterator $keyedIds, $ttl, callable $callback, array $opts = []
1779  ) {
1780  $checkKeys = $opts['checkKeys'] ?? [];
1781  unset( $opts['lockTSE'] ); // incompatible
1782  unset( $opts['busyValue'] ); // incompatible
1783 
1784  // Load required keys into process cache in one go
1785  $keysByIdGet = $this->getNonProcessCachedMultiKeys( $keyedIds, $opts );
1786  $this->warmupCache = $this->getRawKeysForWarmup( $keysByIdGet, $checkKeys );
1787  $this->warmupKeyMisses = 0;
1788 
1789  // IDs of entities known to be in need of regeneration
1790  $idsRegen = [];
1791 
1792  // Find out which keys are missing/deleted/stale
1793  $curTTLs = [];
1794  $asOfs = [];
1795  $curByKey = $this->getMulti( $keysByIdGet, $curTTLs, $checkKeys, $asOfs );
1796  foreach ( $keysByIdGet as $id => $key ) {
1797  if ( !array_key_exists( $key, $curByKey ) || $curTTLs[$key] < 0 ) {
1798  $idsRegen[] = $id;
1799  }
1800  }
1801 
1802  // Run the callback to populate the regeneration value map for all required IDs
1803  $newSetOpts = [];
1804  $newTTLsById = array_fill_keys( $idsRegen, $ttl );
1805  $newValsById = $idsRegen ? $callback( $idsRegen, $newTTLsById, $newSetOpts ) : [];
1806 
1807  // Wrap $callback to match the getWithSetCallback() format while passing $id to $callback
1808  $id = null; // current entity ID
1809  $func = function ( $oldValue, &$ttl, &$setOpts, $oldAsOf )
1810  use ( $callback, &$id, $newValsById, $newTTLsById, $newSetOpts )
1811  {
1812  if ( array_key_exists( $id, $newValsById ) ) {
1813  // Value was already regerated as expected, so use the value in $newValsById
1814  $newValue = $newValsById[$id];
1815  $ttl = $newTTLsById[$id];
1816  $setOpts = $newSetOpts;
1817  } else {
1818  // Pre-emptive/popularity refresh and version mismatch cases are not detected
1819  // above and thus $newValsById has no entry. Run $callback on this single entity.
1820  $ttls = [ $id => $ttl ];
1821  $newValue = $callback( [ $id ], $ttls, $setOpts )[$id];
1822  $ttl = $ttls[$id];
1823  }
1824 
1825  return $newValue;
1826  };
1827 
1828  // Run the cache-aside logic using warmupCache instead of persistent cache queries
1829  $values = [];
1830  foreach ( $keyedIds as $key => $id ) { // preserve order
1831  $values[$key] = $this->getWithSetCallback( $key, $ttl, $func, $opts );
1832  }
1833 
1834  $this->warmupCache = [];
1835 
1836  return $values;
1837  }
1838 
1851  final public function reap( $key, $purgeTimestamp, &$isStale = false ) {
1852  $minAsOf = $purgeTimestamp + self::HOLDOFF_TTL;
1853  $wrapped = $this->cache->get( self::$VALUE_KEY_PREFIX . $key );
1854  if ( is_array( $wrapped ) && $wrapped[self::$FLD_TIME] < $minAsOf ) {
1855  $isStale = true;
1856  $this->logger->warning( "Reaping stale value key '$key'." );
1857  $ttlReap = self::HOLDOFF_TTL; // avoids races with tombstone creation
1858  $ok = $this->cache->changeTTL( self::$VALUE_KEY_PREFIX . $key, $ttlReap );
1859  if ( !$ok ) {
1860  $this->logger->error( "Could not complete reap of key '$key'." );
1861  }
1862 
1863  return $ok;
1864  }
1865 
1866  $isStale = false;
1867 
1868  return true;
1869  }
1870 
1880  final public function reapCheckKey( $key, $purgeTimestamp, &$isStale = false ) {
1881  $purge = $this->parsePurgeValue( $this->cache->get( self::$TIME_KEY_PREFIX . $key ) );
1882  if ( $purge && $purge[self::$PURGE_TIME] < $purgeTimestamp ) {
1883  $isStale = true;
1884  $this->logger->warning( "Reaping stale check key '$key'." );
1885  $ok = $this->cache->changeTTL( self::$TIME_KEY_PREFIX . $key, self::TTL_SECOND );
1886  if ( !$ok ) {
1887  $this->logger->error( "Could not complete reap of check key '$key'." );
1888  }
1889 
1890  return $ok;
1891  }
1892 
1893  $isStale = false;
1894 
1895  return false;
1896  }
1897 
1905  public function makeKey( $class, ...$components ) {
1906  return $this->cache->makeKey( ...func_get_args() );
1907  }
1908 
1916  public function makeGlobalKey( $class, ...$components ) {
1917  return $this->cache->makeGlobalKey( ...func_get_args() );
1918  }
1919 
1927  public function hash256( $component ) {
1928  return hash_hmac( 'sha256', $component, $this->secret );
1929  }
1930 
1981  final public function makeMultiKeys( array $ids, $keyCallback ) {
1982  $idByKey = [];
1983  foreach ( $ids as $id ) {
1984  // Discourage triggering of automatic makeKey() hashing in some backends
1985  if ( strlen( $id ) > 64 ) {
1986  $this->logger->warning( __METHOD__ . ": long ID '$id'; use hash256()" );
1987  }
1988  $key = $keyCallback( $id, $this );
1989  // Edge case: ignore key collisions due to duplicate $ids like "42" and 42
1990  if ( !isset( $idByKey[$key] ) ) {
1991  $idByKey[$key] = $id;
1992  } elseif ( (string)$id !== (string)$idByKey[$key] ) {
1993  throw new UnexpectedValueException(
1994  "Cache key collision; IDs ('$id','{$idByKey[$key]}') map to '$key'"
1995  );
1996  }
1997  }
1998 
1999  return new ArrayIterator( $idByKey );
2000  }
2001 
2037  final public function multiRemap( array $ids, array $res ) {
2038  if ( count( $ids ) !== count( $res ) ) {
2039  // If makeMultiKeys() is called on a list of non-unique IDs, then the resulting
2040  // ArrayIterator will have less entries due to "first appearance" de-duplication
2041  $ids = array_keys( array_flip( $ids ) );
2042  if ( count( $ids ) !== count( $res ) ) {
2043  throw new UnexpectedValueException( "Multi-key result does not match ID list" );
2044  }
2045  }
2046 
2047  return array_combine( $ids, $res );
2048  }
2049 
2054  final public function getLastError() {
2055  $code = $this->cache->getLastError();
2056  switch ( $code ) {
2057  case BagOStuff::ERR_NONE:
2058  return self::ERR_NONE;
2060  return self::ERR_NO_RESPONSE;
2062  return self::ERR_UNREACHABLE;
2063  default:
2064  return self::ERR_UNEXPECTED;
2065  }
2066  }
2067 
2071  final public function clearLastError() {
2072  $this->cache->clearLastError();
2073  }
2074 
2080  public function clearProcessCache() {
2081  $this->processCaches = [];
2082  }
2083 
2104  final public function useInterimHoldOffCaching( $enabled ) {
2105  $this->useInterimHoldOffCaching = $enabled;
2106  }
2107 
2113  public function getQoS( $flag ) {
2114  return $this->cache->getQoS( $flag );
2115  }
2116 
2180  public function adaptiveTTL( $mtime, $maxTTL, $minTTL = 30, $factor = 0.2 ) {
2181  if ( is_float( $mtime ) || ctype_digit( $mtime ) ) {
2182  $mtime = (int)$mtime; // handle fractional seconds and string integers
2183  }
2184 
2185  if ( !is_int( $mtime ) || $mtime <= 0 ) {
2186  return $minTTL; // no last-modified time provided
2187  }
2188 
2189  $age = $this->getCurrentTime() - $mtime;
2190 
2191  return (int)min( $maxTTL, max( $minTTL, $factor * $age ) );
2192  }
2193 
2198  final public function getWarmupKeyMisses() {
2199  return $this->warmupKeyMisses;
2200  }
2201 
2212  protected function relayPurge( $key, $ttl, $holdoff ) {
2213  if ( $this->mcrouterAware ) {
2214  // See https://github.com/facebook/mcrouter/wiki/Multi-cluster-broadcast-setup
2215  // Wildcards select all matching routes, e.g. the WAN cluster on all DCs
2216  $ok = $this->cache->set(
2217  "/*/{$this->cluster}/{$key}",
2218  $this->makePurgeValue( $this->getCurrentTime(), $holdoff ),
2219  $ttl
2220  );
2221  } else {
2222  // Some other proxy handles broadcasting or there is only one datacenter
2223  $ok = $this->cache->set(
2224  $key,
2225  $this->makePurgeValue( $this->getCurrentTime(), $holdoff ),
2226  $ttl
2227  );
2228  }
2229 
2230  return $ok;
2231  }
2232 
2239  protected function relayDelete( $key ) {
2240  if ( $this->mcrouterAware ) {
2241  // See https://github.com/facebook/mcrouter/wiki/Multi-cluster-broadcast-setup
2242  // Wildcards select all matching routes, e.g. the WAN cluster on all DCs
2243  $ok = $this->cache->delete( "/*/{$this->cluster}/{$key}" );
2244  } else {
2245  // Some other proxy handles broadcasting or there is only one datacenter
2246  $ok = $this->cache->delete( $key );
2247  }
2248 
2249  return $ok;
2250  }
2251 
2260  private function scheduleAsyncRefresh( $key, $ttl, $callback, $opts ) {
2261  if ( !$this->asyncHandler ) {
2262  return false;
2263  }
2264  // Update the cache value later, such during post-send of an HTTP request
2265  $func = $this->asyncHandler;
2266  $func( function () use ( $key, $ttl, $callback, $opts ) {
2267  $opts['minAsOf'] = INF; // force a refresh
2268  $this->fetchOrRegenerate( $key, $ttl, $callback, $opts );
2269  } );
2270 
2271  return true;
2272  }
2273 
2287  private function isAliveOrInGracePeriod( $curTTL, $graceTTL ) {
2288  if ( $curTTL > 0 ) {
2289  return true;
2290  } elseif ( $graceTTL <= 0 ) {
2291  return false;
2292  }
2293 
2294  $ageStale = abs( $curTTL ); // seconds of staleness
2295  $curGTTL = ( $graceTTL - $ageStale ); // current grace-time-to-live
2296  if ( $curGTTL <= 0 ) {
2297  return false; // already out of grace period
2298  }
2299 
2300  // Chance of using a stale value is the complement of the chance of refreshing it
2301  return !$this->worthRefreshExpiring( $curGTTL, $graceTTL );
2302  }
2303 
2317  protected function worthRefreshExpiring( $curTTL, $lowTTL ) {
2318  if ( $lowTTL <= 0 ) {
2319  return false;
2320  } elseif ( $curTTL >= $lowTTL ) {
2321  return false;
2322  } elseif ( $curTTL <= 0 ) {
2323  return false;
2324  }
2325 
2326  $chance = ( 1 - $curTTL / $lowTTL );
2327 
2328  // @phan-suppress-next-line PhanTypeMismatchArgumentInternal
2329  return mt_rand( 1, 1e9 ) <= 1e9 * $chance;
2330  }
2331 
2347  protected function worthRefreshPopular( $asOf, $ageNew, $timeTillRefresh, $now ) {
2348  if ( $ageNew < 0 || $timeTillRefresh <= 0 ) {
2349  return false;
2350  }
2351 
2352  $age = $now - $asOf;
2353  $timeOld = $age - $ageNew;
2354  if ( $timeOld <= 0 ) {
2355  return false;
2356  }
2357 
2358  $popularHitsPerSec = 1;
2359  // Lifecycle is: new, ramp-up refresh chance, full refresh chance.
2360  // Note that the "expected # of refreshes" for the ramp-up time range is half
2361  // of what it would be if P(refresh) was at its full value during that time range.
2362  $refreshWindowSec = max( $timeTillRefresh - $ageNew - self::$RAMPUP_TTL / 2, 1 );
2363  // P(refresh) * (# hits in $refreshWindowSec) = (expected # of refreshes)
2364  // P(refresh) * ($refreshWindowSec * $popularHitsPerSec) = 1 (by definition)
2365  // P(refresh) = 1/($refreshWindowSec * $popularHitsPerSec)
2366  $chance = 1 / ( $popularHitsPerSec * $refreshWindowSec );
2367 
2368  // Ramp up $chance from 0 to its nominal value over RAMPUP_TTL seconds to avoid stampedes
2369  $chance *= ( $timeOld <= self::$RAMPUP_TTL ) ? $timeOld / self::$RAMPUP_TTL : 1;
2370 
2371  // @phan-suppress-next-line PhanTypeMismatchArgumentInternal
2372  return mt_rand( 1, 1e9 ) <= 1e9 * $chance;
2373  }
2374 
2384  protected function isValid( $value, $asOf, $minAsOf, $purgeTime = null ) {
2385  // Avoid reading any key not generated after the latest delete() or touch
2386  $safeMinAsOf = max( $minAsOf, $purgeTime + self::$TINY_POSTIVE );
2387 
2388  if ( $value === false ) {
2389  return false;
2390  } elseif ( $safeMinAsOf > 0 && $asOf < $minAsOf ) {
2391  return false;
2392  }
2393 
2394  return true;
2395  }
2396 
2405  private function wrap( $value, $ttl, $version, $now, $walltime ) {
2406  // Returns keys in ascending integer order for PHP7 array packing:
2407  // https://nikic.github.io/2014/12/22/PHPs-new-hashtable-implementation.html
2408  $wrapped = [
2409  self::$FLD_FORMAT_VERSION => self::$VERSION,
2410  self::$FLD_VALUE => $value,
2411  self::$FLD_TTL => $ttl,
2412  self::$FLD_TIME => $now
2413  ];
2414  if ( $version !== null ) {
2415  $wrapped[self::$FLD_VALUE_VERSION] = $version;
2416  }
2417  if ( $walltime >= self::$GENERATION_SLOW_SEC ) {
2418  $wrapped[self::$FLD_GENERATION_TIME] = $walltime;
2419  }
2420 
2421  return $wrapped;
2422  }
2423 
2435  private function unwrap( $wrapped, $now ) {
2436  $value = false;
2437  $info = [ 'asOf' => null, 'curTTL' => null, 'version' => null, 'tombAsOf' => null ];
2438 
2439  if ( is_array( $wrapped ) ) {
2440  // Entry expected to be a cached value; validate it
2441  if (
2442  ( $wrapped[self::$FLD_FORMAT_VERSION] ?? null ) === self::$VERSION &&
2443  $wrapped[self::$FLD_TIME] >= $this->epoch
2444  ) {
2445  if ( $wrapped[self::$FLD_TTL] > 0 ) {
2446  // Get the approximate time left on the key
2447  $age = $now - $wrapped[self::$FLD_TIME];
2448  $curTTL = max( $wrapped[self::$FLD_TTL] - $age, 0.0 );
2449  } else {
2450  // Key had no TTL, so the time left is unbounded
2451  $curTTL = INF;
2452  }
2453  $value = $wrapped[self::$FLD_VALUE];
2454  $info['version'] = $wrapped[self::$FLD_VALUE_VERSION] ?? null;
2455  $info['asOf'] = $wrapped[self::$FLD_TIME];
2456  $info['curTTL'] = $curTTL;
2457  }
2458  } else {
2459  // Entry expected to be a tombstone; parse it
2460  $purge = $this->parsePurgeValue( $wrapped );
2461  if ( $purge !== false ) {
2462  // Tombstoned keys should always have a negative current $ttl
2463  $info['curTTL'] = min( $purge[self::$PURGE_TIME] - $now, self::$TINY_NEGATIVE );
2464  $info['tombAsOf'] = $purge[self::$PURGE_TIME];
2465  }
2466  }
2467 
2468  return [ $value, $info ];
2469  }
2470 
2476  protected static function prefixCacheKeys( array $keys, $prefix ) {
2477  $res = [];
2478  foreach ( $keys as $key ) {
2479  $res[] = $prefix . $key;
2480  }
2481 
2482  return $res;
2483  }
2484 
2489  private function determineKeyClassForStats( $key ) {
2490  $parts = explode( ':', $key, 3 );
2491  // Sanity fallback in case the key was not made by makeKey.
2492  // Replace dots because they are special in StatsD (T232907)
2493  return strtr( $parts[1] ?? $parts[0], '.', '_' );
2494  }
2495 
2501  private function parsePurgeValue( $value ) {
2502  if ( !is_string( $value ) ) {
2503  return false;
2504  }
2505 
2506  $segments = explode( ':', $value, 3 );
2507  if (
2508  !isset( $segments[0] ) ||
2509  !isset( $segments[1] ) ||
2510  "{$segments[0]}:" !== self::$PURGE_VAL_PREFIX
2511  ) {
2512  return false;
2513  }
2514 
2515  if ( !isset( $segments[2] ) ) {
2516  // Back-compat with old purge values without holdoff
2517  $segments[2] = self::HOLDOFF_TTL;
2518  }
2519 
2520  if ( $segments[1] < $this->epoch ) {
2521  // Values this old are ignored
2522  return false;
2523  }
2524 
2525  return [
2526  self::$PURGE_TIME => (float)$segments[1],
2527  self::$PURGE_HOLDOFF => (int)$segments[2],
2528  ];
2529  }
2530 
2536  private function makePurgeValue( $timestamp, $holdoff ) {
2537  return self::$PURGE_VAL_PREFIX . (float)$timestamp . ':' . (int)$holdoff;
2538  }
2539 
2544  private function getProcessCache( $group ) {
2545  if ( !isset( $this->processCaches[$group] ) ) {
2546  list( , $size ) = explode( ':', $group );
2547  $this->processCaches[$group] = new MapCacheLRU( (int)$size );
2548  if ( $this->wallClockOverride !== null ) {
2549  $this->processCaches[$group]->setMockTime( $this->wallClockOverride );
2550  }
2551  }
2552 
2553  return $this->processCaches[$group];
2554  }
2555 
2561  private function getProcessCacheKey( $key, $version ) {
2562  return $key . ' ' . (int)$version;
2563  }
2564 
2570  private function getNonProcessCachedMultiKeys( ArrayIterator $keys, array $opts ) {
2571  $pcTTL = $opts['pcTTL'] ?? self::TTL_UNCACHEABLE;
2572 
2573  $keysMissing = [];
2574  if ( $pcTTL > 0 && $this->callbackDepth == 0 ) {
2575  $version = $opts['version'] ?? null;
2576  $pCache = $this->getProcessCache( $opts['pcGroup'] ?? self::PC_PRIMARY );
2577  foreach ( $keys as $key => $id ) {
2578  if ( !$pCache->has( $this->getProcessCacheKey( $key, $version ), $pcTTL ) ) {
2579  $keysMissing[$id] = $key;
2580  }
2581  }
2582  }
2583 
2584  return $keysMissing;
2585  }
2586 
2592  private function getRawKeysForWarmup( array $keys, array $checkKeys ) {
2593  if ( !$keys ) {
2594  return [];
2595  }
2596 
2597  $keysWarmUp = [];
2598  // Get all the value keys to fetch...
2599  foreach ( $keys as $key ) {
2600  $keysWarmUp[] = self::$VALUE_KEY_PREFIX . $key;
2601  }
2602  // Get all the check keys to fetch...
2603  foreach ( $checkKeys as $i => $checkKeyOrKeys ) {
2604  if ( is_int( $i ) ) {
2605  // Single check key that applies to all value keys
2606  $keysWarmUp[] = self::$TIME_KEY_PREFIX . $checkKeyOrKeys;
2607  } else {
2608  // List of check keys that apply to value key $i
2609  $keysWarmUp = array_merge(
2610  $keysWarmUp,
2611  self::prefixCacheKeys( $checkKeyOrKeys, self::$TIME_KEY_PREFIX )
2612  );
2613  }
2614  }
2615 
2616  $warmupCache = $this->cache->getMulti( $keysWarmUp );
2617  $warmupCache += array_fill_keys( $keysWarmUp, false );
2618 
2619  return $warmupCache;
2620  }
2621 
2626  protected function getCurrentTime() {
2627  if ( $this->wallClockOverride ) {
2628  return $this->wallClockOverride;
2629  }
2630 
2631  $clockTime = (float)time(); // call this first
2632  // microtime() uses an initial gettimeofday() call added to usage clocks.
2633  // This can severely drift from time() and the microtime() value of other threads
2634  // due to undercounting of the amount of time elapsed. Instead of seeing the current
2635  // time as being in the past, use the value of time(). This avoids setting cache values
2636  // that will immediately be seen as expired and possibly cause stampedes.
2637  return max( microtime( true ), $clockTime );
2638  }
2639 
2644  public function setMockTime( &$time ) {
2645  $this->wallClockOverride =& $time;
2646  $this->cache->setMockTime( $time );
2647  foreach ( $this->processCaches as $pCache ) {
2648  $pCache->setMockTime( $time );
2649  }
2650  }
2651 }
processCheckKeys(array $timeKeys, array $wrappedValues, $now)
static int $VERSION
Cache format version number.
string $region
Physical region for mcrouter use.
makePurgeValue( $timestamp, $holdoff)
reapCheckKey( $key, $purgeTimestamp, &$isStale=false)
Set a "check" key to soon expire in the local cluster if it pre-dates $purgeTimestamp.
static $COOLOFF_KEY_PREFIX
static int $INTERIM_KEY_TTL
Seconds to keep interim value keys for tombstoned keys around.
clearProcessCache()
Clear the in-process caches; useful for testing.
static prefixCacheKeys(array $keys, $prefix)
static int $FLD_VALUE_VERSION
Key to collection cache version number.
determineKeyClassForStats( $key)
relayDelete( $key)
Do the actual async bus delete of a key.
makeMultiKeys(array $ids, $keyCallback)
Get an iterator of (cache key => entity ID) for a list of entity IDs.
LoggerInterface $logger
mixed [] $warmupCache
Temporary warm-up cache.
static int $FLD_GENERATION_TIME
Key to how long it took to generate the value.
getMultiWithSetCallback(ArrayIterator $keyedIds, $ttl, callable $callback, array $opts=[])
Method to fetch multiple cache keys at once with regeneration.
static float $TINY_NEGATIVE
Tiny negative float to use when CTL comes up >= 0 due to clock skew.
useInterimHoldOffCaching( $enabled)
Enable or disable the use of brief caching for tombstoned keys.
string $secret
Stable secret used for hasing long strings into key components.
bool $mcrouterAware
Whether to use mcrouter key prefixing for routing.
scheduleAsyncRefresh( $key, $ttl, $callback, $opts)
getWithSetCallback( $key, $ttl, $callback, array $opts=[])
Method to fetch/regenerate cache keys.
hash256( $component)
Hash a possibly long string into a suitable component for makeKey()/makeGlobalKey() ...
worthRefreshPopular( $asOf, $ageNew, $timeTillRefresh, $now)
Check if a key is due for randomized regeneration due to its popularity.
static int $RECENT_SET_LOW_MS
Min millisecond set() backoff during hold-off (far less than INTERIM_KEY_TTL)
checkAndSetCooloff( $key, $kClass, $elapsed, $lockTSE, $hasLock)
resolveBusyValue( $busyValue)
getMultiWithUnionSetCallback(ArrayIterator $keyedIds, $ttl, callable $callback, array $opts=[])
Method to fetch/regenerate multiple cache keys at once.
float $epoch
Unix timestamp of the oldest possible valid values.
int $warmupKeyMisses
Key fetched.
static newEmpty()
Get an instance that wraps EmptyBagOStuff.
static $VALUE_KEY_PREFIX
string $cluster
Cache cluster name for mcrouter use.
makeKey( $class,... $components)
worthRefreshExpiring( $curTTL, $lowTTL)
Check if a key is nearing expiration and thus due for randomized regeneration.
getInterimValue( $key, $minAsOf)
getLastError()
Get the "last error" registered; clearLastError() should be called manually.
static int $FLD_FORMAT_VERSION
Key to WAN cache version number.
getMultiCheckKeyTime(array $keys)
Fetch the values of each timestamp "check" key.
__construct(array $params)
reap( $key, $purgeTimestamp, &$isStale=false)
Set a key to soon expire in the local cluster if it pre-dates $purgeTimestamp.
wrap( $value, $ttl, $version, $now, $walltime)
static int $SET_DELAY_HIGH_MS
Milliseconds of key fetch/validate/regenerate delay prone to set() stampedes.
getMulti(array $keys, &$curTTLs=[], array $checkKeys=[], &$info=null)
Fetch the value of several keys from cache.
static int $CHECK_KEY_TTL
Seconds to keep dependency purge keys around.
unwrap( $wrapped, $now)
adaptiveTTL( $mtime, $maxTTL, $minTTL=30, $factor=0.2)
Get a TTL that is higher for objects that have not changed recently.
static $MUTEX_KEY_PREFIX
relayPurge( $key, $ttl, $holdoff)
Do the actual async bus purge of a key.
float null $wallClockOverride
getProcessCache( $group)
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...
static int $COOLOFF_TTL
Seconds to no-op key set() calls to avoid large blob I/O stampedes.
getCheckKeyTime( $key)
Fetch the value of a timestamp "check" key.
int $callbackDepth
Callback stack depth for getWithSetCallback()
static int $FLD_VALUE
Key to the cached value.
resetCheckKey( $key)
Delete a "check" key from all datacenters, invalidating keys that use it.
touchCheckKey( $key, $holdoff=self::HOLDOFF_TTL)
Purge a "check" key from all datacenters, invalidating keys that use it.
getNonProcessCachedMultiKeys(ArrayIterator $keys, array $opts)
static $PURGE_VAL_PREFIX
clearLastError()
Clear the "last error" registry.
callable null $asyncHandler
Function that takes a WAN cache callback and runs it later.
yieldStampedeLock( $key, $hasLock)
isVolatileValueAgeNegligible( $age)
getRawKeysForWarmup(array $keys, array $checkKeys)
resolveCTL( $value, $curTTL, $curInfo, $touchedCallback)
isAliveOrInGracePeriod( $curTTL, $graceTTL)
Check if a key is fresh or in the grace window and thus due for randomized reuse. ...
static int $FLD_TTL
Key to the original TTL.
bool $useInterimHoldOffCaching
Whether to use "interim" caching while keys are tombstoned.
static int $FLD_TIME
Key to the cache timestamp.
static int $RECENT_SET_HIGH_MS
Max millisecond set() backoff during hold-off (far less than INTERIM_KEY_TTL)
static float $TINY_POSTIVE
Tiny positive float to use when using "minTime" to assert an inequality.
static int $LOCK_TTL
Seconds to keep lock keys around.
static int $PURGE_HOLDOFF
Key to the tombstone entry hold-off TTL.
StatsdDataFactoryInterface $stats
setInterimValue( $key, $value, $ttl, $version, $walltime)
makeGlobalKey( $class,... $components)
Generic interface for object stores with key encoding methods.
MapCacheLRU [] $processCaches
Map of group PHP instance caches.
setLogger(LoggerInterface $logger)
parsePurgeValue( $value)
getProcessCacheKey( $key, $version)
static $INTERIM_KEY_PREFIX
static int $GENERATION_SLOW_SEC
Consider value generation slow if it takes more than this many seconds.
isValid( $value, $asOf, $minAsOf, $purgeTime=null)
Check if $value is not false, versioned (if needed), and not older than $minTime (if set) ...
static int $PURGE_TIME
Key to the tombstone entry timestamp.
fetchOrRegenerate( $key, $ttl, $callback, array $opts)
Do the actual I/O for getWithSetCallback() when needed.
static int $FLD_FLAGS
PhpUnusedPrivateFieldInspection
BagOStuff $cache
The local datacenter cache.
resolveTouched( $value, $lastPurge, $touchedCallback)
static int $RAMPUP_TTL
Seconds to ramp up the chance of regeneration due to expected time-till-refresh.