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;
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 
1315  private function fetchOrRegenerate( $key, $ttl, $callback, array $opts ) {
1316  $checkKeys = $opts['checkKeys'] ?? [];
1317  $graceTTL = $opts['graceTTL'] ?? self::GRACE_TTL_NONE;
1318  $minAsOf = $opts['minAsOf'] ?? self::MIN_TIMESTAMP_NONE;
1319  $hotTTR = $opts['hotTTR'] ?? self::HOT_TTR;
1320  $lowTTL = $opts['lowTTL'] ?? min( self::LOW_TTL, $ttl );
1321  $ageNew = $opts['ageNew'] ?? self::AGE_NEW;
1322  $touchedCb = $opts['touchedCallback'] ?? null;
1323  $initialTime = $this->getCurrentTime();
1324 
1325  $kClass = $this->determineKeyClassForStats( $key );
1326 
1327  // Get the current key value and its metadata
1328  $curTTL = self::PASS_BY_REF;
1329  $curInfo = self::PASS_BY_REF;
1330  $curValue = $this->get( $key, $curTTL, $checkKeys, $curInfo );
1332  '@phan-var array $curInfo';
1333  // Apply any $touchedCb invalidation timestamp to get the "last purge timestamp"
1334  list( $curTTL, $LPT ) = $this->resolveCTL( $curValue, $curTTL, $curInfo, $touchedCb );
1335  // Use the cached value if it exists and is not due for synchronous regeneration
1336  if (
1337  $this->isValid( $curValue, $curInfo['asOf'], $minAsOf ) &&
1338  $this->isAliveOrInGracePeriod( $curTTL, $graceTTL )
1339  ) {
1340  $preemptiveRefresh = (
1341  $this->worthRefreshExpiring( $curTTL, $lowTTL ) ||
1342  $this->worthRefreshPopular( $curInfo['asOf'], $ageNew, $hotTTR, $initialTime )
1343  );
1344  if ( !$preemptiveRefresh ) {
1345  $this->stats->increment( "wanobjectcache.$kClass.hit.good" );
1346 
1347  return [ $curValue, $curInfo['version'], $curInfo['asOf'] ];
1348  } elseif ( $this->scheduleAsyncRefresh( $key, $ttl, $callback, $opts ) ) {
1349  $this->stats->increment( "wanobjectcache.$kClass.hit.refresh" );
1350 
1351  return [ $curValue, $curInfo['version'], $curInfo['asOf'] ];
1352  }
1353  }
1354 
1355  // Determine if there is stale or volatile cached value that is still usable
1356  $isKeyTombstoned = ( $curInfo['tombAsOf'] !== null );
1357  if ( $isKeyTombstoned ) {
1358  // Key is write-holed; use the (volatile) interim key as an alternative
1359  list( $possValue, $possInfo ) = $this->getInterimValue( $key, $minAsOf );
1360  // Update the "last purge time" since the $touchedCb timestamp depends on $value
1361  $LPT = $this->resolveTouched( $possValue, $LPT, $touchedCb );
1362  } else {
1363  $possValue = $curValue;
1364  $possInfo = $curInfo;
1365  }
1366 
1367  // Avoid overhead from callback runs, regeneration locks, and cache sets during
1368  // hold-off periods for the key by reusing very recently generated cached values
1369  if (
1370  $this->isValid( $possValue, $possInfo['asOf'], $minAsOf, $LPT ) &&
1371  $this->isVolatileValueAgeNegligible( $initialTime - $possInfo['asOf'] )
1372  ) {
1373  $this->stats->increment( "wanobjectcache.$kClass.hit.volatile" );
1374 
1375  return [ $possValue, $possInfo['version'], $curInfo['asOf'] ];
1376  }
1377 
1378  $lockTSE = $opts['lockTSE'] ?? self::TSE_NONE;
1379  $busyValue = $opts['busyValue'] ?? null;
1380  $staleTTL = $opts['staleTTL'] ?? self::STALE_TTL_NONE;
1381  $version = $opts['version'] ?? null;
1382 
1383  // Determine whether one thread per datacenter should handle regeneration at a time
1384  $useRegenerationLock =
1385  // Note that since tombstones no-op set(), $lockTSE and $curTTL cannot be used to
1386  // deduce the key hotness because |$curTTL| will always keep increasing until the
1387  // tombstone expires or is overwritten by a new tombstone. Also, even if $lockTSE
1388  // is not set, constant regeneration of a key for the tombstone lifetime might be
1389  // very expensive. Assume tombstoned keys are possibly hot in order to reduce
1390  // the risk of high regeneration load after the delete() method is called.
1391  $isKeyTombstoned ||
1392  // Assume a key is hot if requested soon ($lockTSE seconds) after invalidation.
1393  // This avoids stampedes when timestamps from $checkKeys/$touchedCb bump.
1394  ( $curTTL !== null && $curTTL <= 0 && abs( $curTTL ) <= $lockTSE ) ||
1395  // Assume a key is hot if there is no value and a busy fallback is given.
1396  // This avoids stampedes on eviction or preemptive regeneration taking too long.
1397  ( $busyValue !== null && $possValue === false );
1398 
1399  // If a regeneration lock is required, threads that do not get the lock will try to use
1400  // the stale value, the interim value, or the $busyValue placeholder, in that order. If
1401  // none of those are set then all threads will bypass the lock and regenerate the value.
1402  $hasLock = $useRegenerationLock && $this->claimStampedeLock( $key );
1403  if ( $useRegenerationLock && !$hasLock ) {
1404  if ( $this->isValid( $possValue, $possInfo['asOf'], $minAsOf ) ) {
1405  $this->stats->increment( "wanobjectcache.$kClass.hit.stale" );
1406 
1407  return [ $possValue, $possInfo['version'], $curInfo['asOf'] ];
1408  } elseif ( $busyValue !== null ) {
1409  $miss = is_infinite( $minAsOf ) ? 'renew' : 'miss';
1410  $this->stats->increment( "wanobjectcache.$kClass.$miss.busy" );
1411 
1412  return [ $this->resolveBusyValue( $busyValue ), $version, $curInfo['asOf'] ];
1413  }
1414  }
1415 
1416  // Generate the new value given any prior value with a matching version
1417  $setOpts = [];
1418  $preCallbackTime = $this->getCurrentTime();
1420  try {
1421  $value = $callback(
1422  ( $curInfo['version'] === $version ) ? $curValue : false,
1423  $ttl,
1424  $setOpts,
1425  ( $curInfo['version'] === $version ) ? $curInfo['asOf'] : null
1426  );
1427  } finally {
1429  }
1430  $postCallbackTime = $this->getCurrentTime();
1431 
1432  // How long it took to fetch, validate, and generate the value
1433  $elapsed = max( $postCallbackTime - $initialTime, 0.0 );
1434 
1435  // Attempt to save the newly generated value if applicable
1436  if (
1437  // Callback yielded a cacheable value
1438  ( $value !== false && $ttl >= 0 ) &&
1439  // Current thread was not raced out of a regeneration lock or key is tombstoned
1440  ( !$useRegenerationLock || $hasLock || $isKeyTombstoned ) &&
1441  // Key does not appear to be undergoing a set() stampede
1442  $this->checkAndSetCooloff( $key, $kClass, $elapsed, $lockTSE, $hasLock )
1443  ) {
1444  // How long it took to generate the value
1445  $walltime = max( $postCallbackTime - $preCallbackTime, 0.0 );
1446  $this->stats->timing( "wanobjectcache.$kClass.regen_walltime", 1e3 * $walltime );
1447  // If the key is write-holed then use the (volatile) interim key as an alternative
1448  if ( $isKeyTombstoned ) {
1449  $this->setInterimValue( $key, $value, $lockTSE, $version, $walltime );
1450  } else {
1451  $finalSetOpts = [
1452  // @phan-suppress-next-line PhanUselessBinaryAddRight
1453  'since' => $setOpts['since'] ?? $preCallbackTime,
1454  'version' => $version,
1455  'staleTTL' => $staleTTL,
1456  'lockTSE' => $lockTSE, // informs lag vs performance trade-offs
1457  'creating' => ( $curValue === false ), // optimization
1458  'walltime' => $walltime
1459  ] + $setOpts;
1460  $this->set( $key, $value, $ttl, $finalSetOpts );
1461  }
1462  }
1463 
1464  $this->yieldStampedeLock( $key, $hasLock );
1465 
1466  $miss = is_infinite( $minAsOf ) ? 'renew' : 'miss';
1467  $this->stats->increment( "wanobjectcache.$kClass.$miss.compute" );
1468 
1469  return [ $value, $version, $curInfo['asOf'] ];
1470  }
1471 
1476  private function claimStampedeLock( $key ) {
1477  // Note that locking is not bypassed due to I/O errors; this avoids stampedes
1478  return $this->cache->add( self::$MUTEX_KEY_PREFIX . $key, 1, self::$LOCK_TTL );
1479  }
1480 
1485  private function yieldStampedeLock( $key, $hasLock ) {
1486  if ( $hasLock ) {
1487  // The backend might be a mcrouter proxy set to broadcast DELETE to *all* the local
1488  // datacenter cache servers via OperationSelectorRoute (for increased consistency).
1489  // Since that would be excessive for these locks, use TOUCH to expire the key.
1490  $this->cache->changeTTL( self::$MUTEX_KEY_PREFIX . $key, $this->getCurrentTime() - 60 );
1491  }
1492  }
1493 
1498  private function isVolatileValueAgeNegligible( $age ) {
1499  return ( $age < mt_rand( self::$RECENT_SET_LOW_MS, self::$RECENT_SET_HIGH_MS ) / 1e3 );
1500  }
1501 
1510  private function checkAndSetCooloff( $key, $kClass, $elapsed, $lockTSE, $hasLock ) {
1511  $this->stats->timing( "wanobjectcache.$kClass.regen_set_delay", 1e3 * $elapsed );
1512 
1513  // If $lockTSE is set, the lock was bypassed because there was no stale/interim value,
1514  // and $elapsed indicates that regeration is slow, then there is a risk of set()
1515  // stampedes with large blobs. With a typical scale-out infrastructure, CPU and query
1516  // load from $callback invocations is distributed among appservers and replica DBs,
1517  // but cache operations for a given key route to a single cache server (e.g. striped
1518  // consistent hashing).
1519  if ( $lockTSE < 0 || $hasLock ) {
1520  return true; // either not a priori hot or thread has the lock
1521  } elseif ( $elapsed <= self::$SET_DELAY_HIGH_MS * 1e3 ) {
1522  return true; // not enough time for threads to pile up
1523  }
1524 
1525  $this->cache->clearLastError();
1526  if (
1527  !$this->cache->add( self::$COOLOFF_KEY_PREFIX . $key, 1, self::$COOLOFF_TTL ) &&
1528  // Don't treat failures due to I/O errors as the key being in cooloff
1529  $this->cache->getLastError() === BagOStuff::ERR_NONE
1530  ) {
1531  $this->stats->increment( "wanobjectcache.$kClass.cooloff_bounce" );
1532 
1533  return false;
1534  }
1535 
1536  return true;
1537  }
1538 
1547  private function resolveCTL( $value, $curTTL, $curInfo, $touchedCallback ) {
1548  if ( $touchedCallback === null || $value === false ) {
1549  return [ $curTTL, max( $curInfo['tombAsOf'], $curInfo['lastCKPurge'] ) ];
1550  }
1551 
1552  $touched = $touchedCallback( $value );
1553  if ( $touched !== null && $touched >= $curInfo['asOf'] ) {
1554  $curTTL = min( $curTTL, self::$TINY_NEGATIVE, $curInfo['asOf'] - $touched );
1555  }
1556 
1557  return [ $curTTL, max( $curInfo['tombAsOf'], $curInfo['lastCKPurge'], $touched ) ];
1558  }
1559 
1567  private function resolveTouched( $value, $lastPurge, $touchedCallback ) {
1568  return ( $touchedCallback === null || $value === false )
1569  ? $lastPurge // nothing to derive the "touched timestamp" from
1570  : max( $touchedCallback( $value ), $lastPurge );
1571  }
1572 
1578  private function getInterimValue( $key, $minAsOf ) {
1579  $now = $this->getCurrentTime();
1580 
1581  if ( $this->useInterimHoldOffCaching ) {
1582  $wrapped = $this->cache->get( self::$INTERIM_KEY_PREFIX . $key );
1583 
1584  list( $value, $keyInfo ) = $this->unwrap( $wrapped, $now );
1585  if ( $this->isValid( $value, $keyInfo['asOf'], $minAsOf ) ) {
1586  return [ $value, $keyInfo ];
1587  }
1588  }
1589 
1590  return $this->unwrap( false, $now );
1591  }
1592 
1600  private function setInterimValue( $key, $value, $ttl, $version, $walltime ) {
1601  $ttl = max( self::$INTERIM_KEY_TTL, (int)$ttl );
1602 
1603  $wrapped = $this->wrap( $value, $ttl, $version, $this->getCurrentTime(), $walltime );
1604  $this->cache->merge(
1605  self::$INTERIM_KEY_PREFIX . $key,
1606  function () use ( $wrapped ) {
1607  return $wrapped;
1608  },
1609  $ttl,
1610  1
1611  );
1612  }
1613 
1618  private function resolveBusyValue( $busyValue ) {
1619  return ( $busyValue instanceof Closure ) ? $busyValue() : $busyValue;
1620  }
1621 
1688  final public function getMultiWithSetCallback(
1689  ArrayIterator $keyedIds, $ttl, callable $callback, array $opts = []
1690  ) {
1691  // Load required keys into process cache in one go
1692  $this->warmupCache = $this->getRawKeysForWarmup(
1693  $this->getNonProcessCachedMultiKeys( $keyedIds, $opts ),
1694  $opts['checkKeys'] ?? []
1695  );
1696  $this->warmupKeyMisses = 0;
1697 
1698  // Wrap $callback to match the getWithSetCallback() format while passing $id to $callback
1699  $id = null; // current entity ID
1700  $func = function ( $oldValue, &$ttl, &$setOpts, $oldAsOf ) use ( $callback, &$id ) {
1701  return $callback( $id, $oldValue, $ttl, $setOpts, $oldAsOf );
1702  };
1703 
1704  $values = [];
1705  foreach ( $keyedIds as $key => $id ) { // preserve order
1706  $values[$key] = $this->getWithSetCallback( $key, $ttl, $func, $opts );
1707  }
1708 
1709  $this->warmupCache = [];
1710 
1711  return $values;
1712  }
1713 
1783  final public function getMultiWithUnionSetCallback(
1784  ArrayIterator $keyedIds, $ttl, callable $callback, array $opts = []
1785  ) {
1786  $checkKeys = $opts['checkKeys'] ?? [];
1787  unset( $opts['lockTSE'] ); // incompatible
1788  unset( $opts['busyValue'] ); // incompatible
1789 
1790  // Load required keys into process cache in one go
1791  $keysByIdGet = $this->getNonProcessCachedMultiKeys( $keyedIds, $opts );
1792  $this->warmupCache = $this->getRawKeysForWarmup( $keysByIdGet, $checkKeys );
1793  $this->warmupKeyMisses = 0;
1794 
1795  // IDs of entities known to be in need of regeneration
1796  $idsRegen = [];
1797 
1798  // Find out which keys are missing/deleted/stale
1799  $curTTLs = [];
1800  $asOfs = [];
1801  $curByKey = $this->getMulti( $keysByIdGet, $curTTLs, $checkKeys, $asOfs );
1802  foreach ( $keysByIdGet as $id => $key ) {
1803  if ( !array_key_exists( $key, $curByKey ) || $curTTLs[$key] < 0 ) {
1804  $idsRegen[] = $id;
1805  }
1806  }
1807 
1808  // Run the callback to populate the regeneration value map for all required IDs
1809  $newSetOpts = [];
1810  $newTTLsById = array_fill_keys( $idsRegen, $ttl );
1811  $newValsById = $idsRegen ? $callback( $idsRegen, $newTTLsById, $newSetOpts ) : [];
1812 
1813  // Wrap $callback to match the getWithSetCallback() format while passing $id to $callback
1814  $id = null; // current entity ID
1815  $func = function ( $oldValue, &$ttl, &$setOpts, $oldAsOf )
1816  use ( $callback, &$id, $newValsById, $newTTLsById, $newSetOpts )
1817  {
1818  if ( array_key_exists( $id, $newValsById ) ) {
1819  // Value was already regerated as expected, so use the value in $newValsById
1820  $newValue = $newValsById[$id];
1821  $ttl = $newTTLsById[$id];
1822  $setOpts = $newSetOpts;
1823  } else {
1824  // Pre-emptive/popularity refresh and version mismatch cases are not detected
1825  // above and thus $newValsById has no entry. Run $callback on this single entity.
1826  $ttls = [ $id => $ttl ];
1827  $newValue = $callback( [ $id ], $ttls, $setOpts )[$id];
1828  $ttl = $ttls[$id];
1829  }
1830 
1831  return $newValue;
1832  };
1833 
1834  // Run the cache-aside logic using warmupCache instead of persistent cache queries
1835  $values = [];
1836  foreach ( $keyedIds as $key => $id ) { // preserve order
1837  $values[$key] = $this->getWithSetCallback( $key, $ttl, $func, $opts );
1838  }
1839 
1840  $this->warmupCache = [];
1841 
1842  return $values;
1843  }
1844 
1857  final public function reap( $key, $purgeTimestamp, &$isStale = false ) {
1858  $minAsOf = $purgeTimestamp + self::HOLDOFF_TTL;
1859  $wrapped = $this->cache->get( self::$VALUE_KEY_PREFIX . $key );
1860  if ( is_array( $wrapped ) && $wrapped[self::$FLD_TIME] < $minAsOf ) {
1861  $isStale = true;
1862  $this->logger->warning( "Reaping stale value key '$key'." );
1863  $ttlReap = self::HOLDOFF_TTL; // avoids races with tombstone creation
1864  $ok = $this->cache->changeTTL( self::$VALUE_KEY_PREFIX . $key, $ttlReap );
1865  if ( !$ok ) {
1866  $this->logger->error( "Could not complete reap of key '$key'." );
1867  }
1868 
1869  return $ok;
1870  }
1871 
1872  $isStale = false;
1873 
1874  return true;
1875  }
1876 
1886  final public function reapCheckKey( $key, $purgeTimestamp, &$isStale = false ) {
1887  $purge = $this->parsePurgeValue( $this->cache->get( self::$TIME_KEY_PREFIX . $key ) );
1888  if ( $purge && $purge[self::$PURGE_TIME] < $purgeTimestamp ) {
1889  $isStale = true;
1890  $this->logger->warning( "Reaping stale check key '$key'." );
1891  $ok = $this->cache->changeTTL( self::$TIME_KEY_PREFIX . $key, self::TTL_SECOND );
1892  if ( !$ok ) {
1893  $this->logger->error( "Could not complete reap of check key '$key'." );
1894  }
1895 
1896  return $ok;
1897  }
1898 
1899  $isStale = false;
1900 
1901  return false;
1902  }
1903 
1911  public function makeKey( $class, ...$components ) {
1912  return $this->cache->makeKey( ...func_get_args() );
1913  }
1914 
1922  public function makeGlobalKey( $class, ...$components ) {
1923  return $this->cache->makeGlobalKey( ...func_get_args() );
1924  }
1925 
1933  public function hash256( $component ) {
1934  return hash_hmac( 'sha256', $component, $this->secret );
1935  }
1936 
1987  final public function makeMultiKeys( array $ids, $keyCallback ) {
1988  $idByKey = [];
1989  foreach ( $ids as $id ) {
1990  // Discourage triggering of automatic makeKey() hashing in some backends
1991  if ( strlen( $id ) > 64 ) {
1992  $this->logger->warning( __METHOD__ . ": long ID '$id'; use hash256()" );
1993  }
1994  $key = $keyCallback( $id, $this );
1995  // Edge case: ignore key collisions due to duplicate $ids like "42" and 42
1996  if ( !isset( $idByKey[$key] ) ) {
1997  $idByKey[$key] = $id;
1998  } elseif ( (string)$id !== (string)$idByKey[$key] ) {
1999  throw new UnexpectedValueException(
2000  "Cache key collision; IDs ('$id','{$idByKey[$key]}') map to '$key'"
2001  );
2002  }
2003  }
2004 
2005  return new ArrayIterator( $idByKey );
2006  }
2007 
2043  final public function multiRemap( array $ids, array $res ) {
2044  if ( count( $ids ) !== count( $res ) ) {
2045  // If makeMultiKeys() is called on a list of non-unique IDs, then the resulting
2046  // ArrayIterator will have less entries due to "first appearance" de-duplication
2047  $ids = array_keys( array_flip( $ids ) );
2048  if ( count( $ids ) !== count( $res ) ) {
2049  throw new UnexpectedValueException( "Multi-key result does not match ID list" );
2050  }
2051  }
2052 
2053  return array_combine( $ids, $res );
2054  }
2055 
2060  final public function getLastError() {
2061  $code = $this->cache->getLastError();
2062  switch ( $code ) {
2063  case BagOStuff::ERR_NONE:
2064  return self::ERR_NONE;
2066  return self::ERR_NO_RESPONSE;
2068  return self::ERR_UNREACHABLE;
2069  default:
2070  return self::ERR_UNEXPECTED;
2071  }
2072  }
2073 
2077  final public function clearLastError() {
2078  $this->cache->clearLastError();
2079  }
2080 
2086  public function clearProcessCache() {
2087  $this->processCaches = [];
2088  }
2089 
2110  final public function useInterimHoldOffCaching( $enabled ) {
2111  $this->useInterimHoldOffCaching = $enabled;
2112  }
2113 
2119  public function getQoS( $flag ) {
2120  return $this->cache->getQoS( $flag );
2121  }
2122 
2186  public function adaptiveTTL( $mtime, $maxTTL, $minTTL = 30, $factor = 0.2 ) {
2187  if ( is_float( $mtime ) || ctype_digit( $mtime ) ) {
2188  $mtime = (int)$mtime; // handle fractional seconds and string integers
2189  }
2190 
2191  if ( !is_int( $mtime ) || $mtime <= 0 ) {
2192  return $minTTL; // no last-modified time provided
2193  }
2194 
2195  $age = $this->getCurrentTime() - $mtime;
2196 
2197  return (int)min( $maxTTL, max( $minTTL, $factor * $age ) );
2198  }
2199 
2204  final public function getWarmupKeyMisses() {
2205  return $this->warmupKeyMisses;
2206  }
2207 
2218  protected function relayPurge( $key, $ttl, $holdoff ) {
2219  if ( $this->mcrouterAware ) {
2220  // See https://github.com/facebook/mcrouter/wiki/Multi-cluster-broadcast-setup
2221  // Wildcards select all matching routes, e.g. the WAN cluster on all DCs
2222  $ok = $this->cache->set(
2223  "/*/{$this->cluster}/{$key}",
2224  $this->makePurgeValue( $this->getCurrentTime(), $holdoff ),
2225  $ttl
2226  );
2227  } else {
2228  // Some other proxy handles broadcasting or there is only one datacenter
2229  $ok = $this->cache->set(
2230  $key,
2231  $this->makePurgeValue( $this->getCurrentTime(), $holdoff ),
2232  $ttl
2233  );
2234  }
2235 
2236  return $ok;
2237  }
2238 
2245  protected function relayDelete( $key ) {
2246  if ( $this->mcrouterAware ) {
2247  // See https://github.com/facebook/mcrouter/wiki/Multi-cluster-broadcast-setup
2248  // Wildcards select all matching routes, e.g. the WAN cluster on all DCs
2249  $ok = $this->cache->delete( "/*/{$this->cluster}/{$key}" );
2250  } else {
2251  // Some other proxy handles broadcasting or there is only one datacenter
2252  $ok = $this->cache->delete( $key );
2253  }
2254 
2255  return $ok;
2256  }
2257 
2266  private function scheduleAsyncRefresh( $key, $ttl, $callback, $opts ) {
2267  if ( !$this->asyncHandler ) {
2268  return false;
2269  }
2270  // Update the cache value later, such during post-send of an HTTP request
2271  $func = $this->asyncHandler;
2272  $func( function () use ( $key, $ttl, $callback, $opts ) {
2273  $opts['minAsOf'] = INF; // force a refresh
2274  $this->fetchOrRegenerate( $key, $ttl, $callback, $opts );
2275  } );
2276 
2277  return true;
2278  }
2279 
2293  private function isAliveOrInGracePeriod( $curTTL, $graceTTL ) {
2294  if ( $curTTL > 0 ) {
2295  return true;
2296  } elseif ( $graceTTL <= 0 ) {
2297  return false;
2298  }
2299 
2300  $ageStale = abs( $curTTL ); // seconds of staleness
2301  $curGTTL = ( $graceTTL - $ageStale ); // current grace-time-to-live
2302  if ( $curGTTL <= 0 ) {
2303  return false; // already out of grace period
2304  }
2305 
2306  // Chance of using a stale value is the complement of the chance of refreshing it
2307  return !$this->worthRefreshExpiring( $curGTTL, $graceTTL );
2308  }
2309 
2323  protected function worthRefreshExpiring( $curTTL, $lowTTL ) {
2324  if ( $lowTTL <= 0 ) {
2325  return false;
2326  } elseif ( $curTTL >= $lowTTL ) {
2327  return false;
2328  } elseif ( $curTTL <= 0 ) {
2329  return false;
2330  }
2331 
2332  $chance = ( 1 - $curTTL / $lowTTL );
2333 
2334  // @phan-suppress-next-line PhanTypeMismatchArgumentInternal
2335  return mt_rand( 1, 1e9 ) <= 1e9 * $chance;
2336  }
2337 
2353  protected function worthRefreshPopular( $asOf, $ageNew, $timeTillRefresh, $now ) {
2354  if ( $ageNew < 0 || $timeTillRefresh <= 0 ) {
2355  return false;
2356  }
2357 
2358  $age = $now - $asOf;
2359  $timeOld = $age - $ageNew;
2360  if ( $timeOld <= 0 ) {
2361  return false;
2362  }
2363 
2364  $popularHitsPerSec = 1;
2365  // Lifecycle is: new, ramp-up refresh chance, full refresh chance.
2366  // Note that the "expected # of refreshes" for the ramp-up time range is half
2367  // of what it would be if P(refresh) was at its full value during that time range.
2368  $refreshWindowSec = max( $timeTillRefresh - $ageNew - self::$RAMPUP_TTL / 2, 1 );
2369  // P(refresh) * (# hits in $refreshWindowSec) = (expected # of refreshes)
2370  // P(refresh) * ($refreshWindowSec * $popularHitsPerSec) = 1 (by definition)
2371  // P(refresh) = 1/($refreshWindowSec * $popularHitsPerSec)
2372  $chance = 1 / ( $popularHitsPerSec * $refreshWindowSec );
2373 
2374  // Ramp up $chance from 0 to its nominal value over RAMPUP_TTL seconds to avoid stampedes
2375  $chance *= ( $timeOld <= self::$RAMPUP_TTL ) ? $timeOld / self::$RAMPUP_TTL : 1;
2376 
2377  // @phan-suppress-next-line PhanTypeMismatchArgumentInternal
2378  return mt_rand( 1, 1e9 ) <= 1e9 * $chance;
2379  }
2380 
2390  protected function isValid( $value, $asOf, $minAsOf, $purgeTime = null ) {
2391  // Avoid reading any key not generated after the latest delete() or touch
2392  $safeMinAsOf = max( $minAsOf, $purgeTime + self::$TINY_POSTIVE );
2393 
2394  if ( $value === false ) {
2395  return false;
2396  } elseif ( $safeMinAsOf > 0 && $asOf < $minAsOf ) {
2397  return false;
2398  }
2399 
2400  return true;
2401  }
2402 
2411  private function wrap( $value, $ttl, $version, $now, $walltime ) {
2412  // Returns keys in ascending integer order for PHP7 array packing:
2413  // https://nikic.github.io/2014/12/22/PHPs-new-hashtable-implementation.html
2414  $wrapped = [
2415  self::$FLD_FORMAT_VERSION => self::$VERSION,
2416  self::$FLD_VALUE => $value,
2417  self::$FLD_TTL => $ttl,
2418  self::$FLD_TIME => $now
2419  ];
2420  if ( $version !== null ) {
2421  $wrapped[self::$FLD_VALUE_VERSION] = $version;
2422  }
2423  if ( $walltime >= self::$GENERATION_SLOW_SEC ) {
2424  $wrapped[self::$FLD_GENERATION_TIME] = $walltime;
2425  }
2426 
2427  return $wrapped;
2428  }
2429 
2441  private function unwrap( $wrapped, $now ) {
2442  $value = false;
2443  $info = [ 'asOf' => null, 'curTTL' => null, 'version' => null, 'tombAsOf' => null ];
2444 
2445  if ( is_array( $wrapped ) ) {
2446  // Entry expected to be a cached value; validate it
2447  if (
2448  ( $wrapped[self::$FLD_FORMAT_VERSION] ?? null ) === self::$VERSION &&
2449  $wrapped[self::$FLD_TIME] >= $this->epoch
2450  ) {
2451  if ( $wrapped[self::$FLD_TTL] > 0 ) {
2452  // Get the approximate time left on the key
2453  $age = $now - $wrapped[self::$FLD_TIME];
2454  $curTTL = max( $wrapped[self::$FLD_TTL] - $age, 0.0 );
2455  } else {
2456  // Key had no TTL, so the time left is unbounded
2457  $curTTL = INF;
2458  }
2459  $value = $wrapped[self::$FLD_VALUE];
2460  $info['version'] = $wrapped[self::$FLD_VALUE_VERSION] ?? null;
2461  $info['asOf'] = $wrapped[self::$FLD_TIME];
2462  $info['curTTL'] = $curTTL;
2463  }
2464  } else {
2465  // Entry expected to be a tombstone; parse it
2466  $purge = $this->parsePurgeValue( $wrapped );
2467  if ( $purge !== false ) {
2468  // Tombstoned keys should always have a negative current $ttl
2469  $info['curTTL'] = min( $purge[self::$PURGE_TIME] - $now, self::$TINY_NEGATIVE );
2470  $info['tombAsOf'] = $purge[self::$PURGE_TIME];
2471  }
2472  }
2473 
2474  return [ $value, $info ];
2475  }
2476 
2482  protected static function prefixCacheKeys( array $keys, $prefix ) {
2483  $res = [];
2484  foreach ( $keys as $key ) {
2485  $res[] = $prefix . $key;
2486  }
2487 
2488  return $res;
2489  }
2490 
2495  private function determineKeyClassForStats( $key ) {
2496  $parts = explode( ':', $key, 3 );
2497  // Sanity fallback in case the key was not made by makeKey.
2498  // Replace dots because they are special in StatsD (T232907)
2499  return strtr( $parts[1] ?? $parts[0], '.', '_' );
2500  }
2501 
2507  private function parsePurgeValue( $value ) {
2508  if ( !is_string( $value ) ) {
2509  return false;
2510  }
2511 
2512  $segments = explode( ':', $value, 3 );
2513  if (
2514  !isset( $segments[0] ) ||
2515  !isset( $segments[1] ) ||
2516  "{$segments[0]}:" !== self::$PURGE_VAL_PREFIX
2517  ) {
2518  return false;
2519  }
2520 
2521  if ( !isset( $segments[2] ) ) {
2522  // Back-compat with old purge values without holdoff
2523  $segments[2] = self::HOLDOFF_TTL;
2524  }
2525 
2526  if ( $segments[1] < $this->epoch ) {
2527  // Values this old are ignored
2528  return false;
2529  }
2530 
2531  return [
2532  self::$PURGE_TIME => (float)$segments[1],
2533  self::$PURGE_HOLDOFF => (int)$segments[2],
2534  ];
2535  }
2536 
2542  private function makePurgeValue( $timestamp, $holdoff ) {
2543  return self::$PURGE_VAL_PREFIX . (float)$timestamp . ':' . (int)$holdoff;
2544  }
2545 
2550  private function getProcessCache( $group ) {
2551  if ( !isset( $this->processCaches[$group] ) ) {
2552  list( , $size ) = explode( ':', $group );
2553  $this->processCaches[$group] = new MapCacheLRU( (int)$size );
2554  if ( $this->wallClockOverride !== null ) {
2555  $this->processCaches[$group]->setMockTime( $this->wallClockOverride );
2556  }
2557  }
2558 
2559  return $this->processCaches[$group];
2560  }
2561 
2567  private function getProcessCacheKey( $key, $version ) {
2568  return $key . ' ' . (int)$version;
2569  }
2570 
2576  private function getNonProcessCachedMultiKeys( ArrayIterator $keys, array $opts ) {
2577  $pcTTL = $opts['pcTTL'] ?? self::TTL_UNCACHEABLE;
2578 
2579  $keysMissing = [];
2580  if ( $pcTTL > 0 && $this->callbackDepth == 0 ) {
2581  $version = $opts['version'] ?? null;
2582  $pCache = $this->getProcessCache( $opts['pcGroup'] ?? self::PC_PRIMARY );
2583  foreach ( $keys as $key => $id ) {
2584  if ( !$pCache->has( $this->getProcessCacheKey( $key, $version ), $pcTTL ) ) {
2585  $keysMissing[$id] = $key;
2586  }
2587  }
2588  }
2589 
2590  return $keysMissing;
2591  }
2592 
2598  private function getRawKeysForWarmup( array $keys, array $checkKeys ) {
2599  if ( !$keys ) {
2600  return [];
2601  }
2602 
2603  $keysWarmUp = [];
2604  // Get all the value keys to fetch...
2605  foreach ( $keys as $key ) {
2606  $keysWarmUp[] = self::$VALUE_KEY_PREFIX . $key;
2607  }
2608  // Get all the check keys to fetch...
2609  foreach ( $checkKeys as $i => $checkKeyOrKeys ) {
2610  if ( is_int( $i ) ) {
2611  // Single check key that applies to all value keys
2612  $keysWarmUp[] = self::$TIME_KEY_PREFIX . $checkKeyOrKeys;
2613  } else {
2614  // List of check keys that apply to value key $i
2615  $keysWarmUp = array_merge(
2616  $keysWarmUp,
2617  self::prefixCacheKeys( $checkKeyOrKeys, self::$TIME_KEY_PREFIX )
2618  );
2619  }
2620  }
2621 
2622  $warmupCache = $this->cache->getMulti( $keysWarmUp );
2623  $warmupCache += array_fill_keys( $keysWarmUp, false );
2624 
2625  return $warmupCache;
2626  }
2627 
2632  protected function getCurrentTime() {
2633  if ( $this->wallClockOverride ) {
2634  return $this->wallClockOverride;
2635  }
2636 
2637  $clockTime = (float)time(); // call this first
2638  // microtime() uses an initial gettimeofday() call added to usage clocks.
2639  // This can severely drift from time() and the microtime() value of other threads
2640  // due to undercounting of the amount of time elapsed. Instead of seeing the current
2641  // time as being in the past, use the value of time(). This avoids setting cache values
2642  // that will immediately be seen as expired and possibly cause stampedes.
2643  return max( microtime( true ), $clockTime );
2644  }
2645 
2650  public function setMockTime( &$time ) {
2651  $this->wallClockOverride =& $time;
2652  $this->cache->setMockTime( $time );
2653  foreach ( $this->processCaches as $pCache ) {
2654  $pCache->setMockTime( $time );
2655  }
2656  }
2657 }
WANObjectCache\$RAMPUP_TTL
static int $RAMPUP_TTL
Seconds to ramp up the chance of regeneration due to expected time-till-refresh.
Definition: WANObjectCache.php:202
WANObjectCache\getQoS
getQoS( $flag)
Definition: WANObjectCache.php:2119
WANObjectCache\$VERSION
static int $VERSION
Cache format version number.
Definition: WANObjectCache.php:225
WANObjectCache\hash256
hash256( $component)
Hash a possibly long string into a suitable component for makeKey()/makeGlobalKey()
Definition: WANObjectCache.php:1933
WANObjectCache\determineKeyClassForStats
determineKeyClassForStats( $key)
Definition: WANObjectCache.php:2495
WANObjectCache\relayDelete
relayDelete( $key)
Do the actual async bus delete of a key.
Definition: WANObjectCache.php:2245
WANObjectCache\setMockTime
setMockTime(&$time)
Definition: WANObjectCache.php:2650
WANObjectCache\makePurgeValue
makePurgeValue( $timestamp, $holdoff)
Definition: WANObjectCache.php:2542
WANObjectCache\$INTERIM_KEY_TTL
static int $INTERIM_KEY_TTL
Seconds to keep interim value keys for tombstoned keys around.
Definition: WANObjectCache.php:195
IExpiringStore\ERR_UNEXPECTED
const ERR_UNEXPECTED
Definition: IExpiringStore.php:65
WANObjectCache\$warmupCache
mixed[] $warmupCache
Temporary warm-up cache.
Definition: WANObjectCache.php:144
WANObjectCache\fetchOrRegenerate
fetchOrRegenerate( $key, $ttl, $callback, array $opts)
Do the actual I/O for getWithSetCallback() when needed.
Definition: WANObjectCache.php:1315
WANObjectCache\makeGlobalKey
makeGlobalKey( $class,... $components)
Definition: WANObjectCache.php:1922
EmptyBagOStuff
A BagOStuff object with no objects in it.
Definition: EmptyBagOStuff.php:29
WANObjectCache\$cache
BagOStuff $cache
The local datacenter cache.
Definition: WANObjectCache.php:118
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:2390
IExpiringStore\ERR_NONE
const ERR_NONE
Definition: IExpiringStore.php:62
IExpiringStore\TTL_SECOND
const TTL_SECOND
Definition: IExpiringStore.php:32
WANObjectCache\checkAndSetCooloff
checkAndSetCooloff( $key, $kClass, $elapsed, $lockTSE, $hasLock)
Definition: WANObjectCache.php:1510
WANObjectCache\$FLD_GENERATION_TIME
static int $FLD_GENERATION_TIME
Key to how long it took to generate the value.
Definition: WANObjectCache.php:240
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:1688
WANObjectCache\$TINY_NEGATIVE
static float $TINY_NEGATIVE
Tiny negative float to use when CTL comes up >= 0 due to clock skew.
Definition: WANObjectCache.php:205
IExpiringStore\ERR_UNREACHABLE
const ERR_UNREACHABLE
Definition: IExpiringStore.php:64
BagOStuff
Class representing a cache/ephemeral data store.
Definition: BagOStuff.php:63
WANObjectCache\$COOLOFF_KEY_PREFIX
static $COOLOFF_KEY_PREFIX
Definition: WANObjectCache.php:246
WANObjectCache\getMultiWithUnionSetCallback
getMultiWithUnionSetCallback(ArrayIterator $keyedIds, $ttl, callable $callback, array $opts=[])
Method to fetch/regenerate multiple cache keys at once.
Definition: WANObjectCache.php:1783
WANObjectCache\resolveTouched
resolveTouched( $value, $lastPurge, $touchedCallback)
Definition: WANObjectCache.php:1567
WANObjectCache\$SET_DELAY_HIGH_MS
static int $SET_DELAY_HIGH_MS
Milliseconds of key fetch/validate/regenerate delay prone to set() stampedes.
Definition: WANObjectCache.php:210
WANObjectCache\getMulti
getMulti(array $keys, &$curTTLs=[], array $checkKeys=[], &$info=null)
Fetch the value of several keys from cache.
Definition: WANObjectCache.php:397
WANObjectCache\makeKey
makeKey( $class,... $components)
Definition: WANObjectCache.php:1911
$res
$res
Definition: testCompression.php:54
WANObjectCache\scheduleAsyncRefresh
scheduleAsyncRefresh( $key, $ttl, $callback, $opts)
Definition: WANObjectCache.php:2266
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:1886
WANObjectCache\getCheckKeyTime
getCheckKeyTime( $key)
Fetch the value of a timestamp "check" key.
Definition: WANObjectCache.php:779
WANObjectCache\resolveBusyValue
resolveBusyValue( $busyValue)
Definition: WANObjectCache.php:1618
WANObjectCache\$logger
LoggerInterface $logger
Definition: WANObjectCache.php:122
WANObjectCache\$FLD_VALUE
static int $FLD_VALUE
Key to the cached value.
Definition: WANObjectCache.php:230
WANObjectCache\$warmupKeyMisses
int $warmupKeyMisses
Key fetched.
Definition: WANObjectCache.php:146
WANObjectCache\newEmpty
static newEmpty()
Get an instance that wraps EmptyBagOStuff.
Definition: WANObjectCache.php:300
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:212
WANObjectCache\$VALUE_KEY_PREFIX
static $VALUE_KEY_PREFIX
Definition: WANObjectCache.php:242
IExpiringStore
Generic interface for lightweight expiring object stores.
Definition: IExpiringStore.php:30
WANObjectCache\clearProcessCache
clearProcessCache()
Clear the in-process caches; useful for testing.
Definition: WANObjectCache.php:2086
WANObjectCache\getInterimValue
getInterimValue( $key, $minAsOf)
Definition: WANObjectCache.php:1578
WANObjectCache\$secret
string $secret
Stable secret used for hasing long strings into key components.
Definition: WANObjectCache.php:139
WANObjectCache\clearLastError
clearLastError()
Clear the "last error" registry.
Definition: WANObjectCache.php:2077
WANObjectCache\getCurrentTime
getCurrentTime()
Definition: WANObjectCache.php:2632
WANObjectCache\getWithSetCallback
getWithSetCallback( $key, $ttl, $callback, array $opts=[])
Method to fetch/regenerate cache keys.
Definition: WANObjectCache.php:1261
WANObjectCache\prefixCacheKeys
static prefixCacheKeys(array $keys, $prefix)
Definition: WANObjectCache.php:2482
WANObjectCache\$FLD_FORMAT_VERSION
static int $FLD_FORMAT_VERSION
Key to WAN cache version number.
Definition: WANObjectCache.php:228
WANObjectCache\unwrap
unwrap( $wrapped, $now)
Definition: WANObjectCache.php:2441
WANObjectCache\getLastError
getLastError()
Get the "last error" registered; clearLastError() should be called manually.
Definition: WANObjectCache.php:2060
WANObjectCache\makeMultiKeys
makeMultiKeys(array $ids, $keyCallback)
Get an iterator of (cache key => entity ID) for a list of entity IDs.
Definition: WANObjectCache.php:1987
WANObjectCache\__construct
__construct(array $params)
Definition: WANObjectCache.php:275
WANObjectCache\useInterimHoldOffCaching
useInterimHoldOffCaching( $enabled)
Enable or disable the use of brief caching for tombstoned keys.
Definition: WANObjectCache.php:2110
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:1857
IExpiringStore\TTL_INDEFINITE
const TTL_INDEFINITE
Definition: IExpiringStore.php:44
WANObjectCache\touchCheckKey
touchCheckKey( $key, $holdoff=self::HOLDOFF_TTL)
Purge a "check" key from all datacenters, invalidating keys that use it.
Definition: WANObjectCache.php:909
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:129
WANObjectCache\wrap
wrap( $value, $ttl, $version, $now, $walltime)
Definition: WANObjectCache.php:2411
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:2186
WANObjectCache\getNonProcessCachedMultiKeys
getNonProcessCachedMultiKeys(ArrayIterator $keys, array $opts)
Definition: WANObjectCache.php:2576
WANObjectCache\$epoch
float $epoch
Unix timestamp of the oldest possible valid values.
Definition: WANObjectCache.php:137
WANObjectCache\relayPurge
relayPurge( $key, $ttl, $holdoff)
Do the actual async bus purge of a key.
Definition: WANObjectCache.php:2218
WANObjectCache\$wallClockOverride
float null $wallClockOverride
Definition: WANObjectCache.php:149
WANObjectCache\$CHECK_KEY_TTL
static int $CHECK_KEY_TTL
Seconds to keep dependency purge keys around.
Definition: WANObjectCache.php:193
WANObjectCache\isVolatileValueAgeNegligible
isVolatileValueAgeNegligible( $age)
Definition: WANObjectCache.php:1498
WANObjectCache\getMultiCheckKeyTime
getMultiCheckKeyTime(array $keys)
Fetch the values of each timestamp "check" key.
Definition: WANObjectCache.php:844
WANObjectCache\$COOLOFF_TTL
static int $COOLOFF_TTL
Seconds to no-op key set() calls to avoid large blob I/O stampedes.
Definition: WANObjectCache.php:200
WANObjectCache\worthRefreshPopular
worthRefreshPopular( $asOf, $ageNew, $timeTillRefresh, $now)
Check if a key is due for randomized regeneration due to its popularity.
Definition: WANObjectCache.php:2353
WANObjectCache\$LOCK_TTL
static int $LOCK_TTL
Seconds to keep lock keys around.
Definition: WANObjectCache.php:198
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:2293
WANObjectCache\$useInterimHoldOffCaching
bool $useInterimHoldOffCaching
Whether to use "interim" caching while keys are tombstoned.
Definition: WANObjectCache.php:135
WANObjectCache\$cluster
string $cluster
Cache cluster name for mcrouter use.
Definition: WANObjectCache.php:133
WANObjectCache\worthRefreshExpiring
worthRefreshExpiring( $curTTL, $lowTTL)
Check if a key is nearing expiration and thus due for randomized regeneration.
Definition: WANObjectCache.php:2323
WANObjectCache
Multi-datacenter aware caching interface.
Definition: WANObjectCache.php:116
WANObjectCache\getProcessCache
getProcessCache( $group)
Definition: WANObjectCache.php:2550
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:2043
WANObjectCache\getRawKeysForWarmup
getRawKeysForWarmup(array $keys, array $checkKeys)
Definition: WANObjectCache.php:2598
WANObjectCache\$FLD_FLAGS
static int $FLD_FLAGS
@noinspection PhpUnusedPrivateFieldInspection
Definition: WANObjectCache.php:236
WANObjectCache\resetCheckKey
resetCheckKey( $key)
Delete a "check" key from all datacenters, invalidating keys that use it.
Definition: WANObjectCache.php:946
WANObjectCache\claimStampedeLock
claimStampedeLock( $key)
Definition: WANObjectCache.php:1476
WANObjectCache\parsePurgeValue
parsePurgeValue( $value)
Definition: WANObjectCache.php:2507
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:214
WANObjectCache\getProcessCacheKey
getProcessCacheKey( $key, $version)
Definition: WANObjectCache.php:2567
WANObjectCache\yieldStampedeLock
yieldStampedeLock( $key, $hasLock)
Definition: WANObjectCache.php:1485
WANObjectCache\$MUTEX_KEY_PREFIX
static $MUTEX_KEY_PREFIX
Definition: WANObjectCache.php:245
WANObjectCache\$TINY_POSTIVE
static float $TINY_POSTIVE
Tiny positive float to use when using "minTime" to assert an inequality.
Definition: WANObjectCache.php:207
WANObjectCache\$TIME_KEY_PREFIX
static $TIME_KEY_PREFIX
Definition: WANObjectCache.php:244
WANObjectCache\$asyncHandler
callable null $asyncHandler
Function that takes a WAN cache callback and runs it later.
Definition: WANObjectCache.php:126
WANObjectCache\getWarmupKeyMisses
getWarmupKeyMisses()
Definition: WANObjectCache.php:2204
IExpiringStore\TTL_YEAR
const TTL_YEAR
Definition: IExpiringStore.php:38
$keys
$keys
Definition: testCompression.php:69
WANObjectCache\$GENERATION_SLOW_SEC
static int $GENERATION_SLOW_SEC
Consider value generation slow if it takes more than this many seconds.
Definition: WANObjectCache.php:217
WANObjectCache\resolveCTL
resolveCTL( $value, $curTTL, $curInfo, $touchedCallback)
Definition: WANObjectCache.php:1547
WANObjectCache\$PURGE_HOLDOFF
static int $PURGE_HOLDOFF
Key to the tombstone entry hold-off TTL.
Definition: WANObjectCache.php:222
WANObjectCache\$callbackDepth
int $callbackDepth
Callback stack depth for getWithSetCallback()
Definition: WANObjectCache.php:142
IStoreKeyEncoder
Generic interface for object stores with key encoding methods.
Definition: IStoreKeyEncoder.php:9
WANObjectCache\setLogger
setLogger(LoggerInterface $logger)
Definition: WANObjectCache.php:291
WANObjectCache\$FLD_TTL
static int $FLD_TTL
Key to the original TTL.
Definition: WANObjectCache.php:232
WANObjectCache\$PURGE_VAL_PREFIX
static $PURGE_VAL_PREFIX
Definition: WANObjectCache.php:248
WANObjectCache\$FLD_VALUE_VERSION
static int $FLD_VALUE_VERSION
Key to collection cache version number.
Definition: WANObjectCache.php:238
WANObjectCache\$FLD_TIME
static int $FLD_TIME
Key to the cache timestamp.
Definition: WANObjectCache.php:234
IExpiringStore\ERR_NO_RESPONSE
const ERR_NO_RESPONSE
Definition: IExpiringStore.php:63
WANObjectCache\$PURGE_TIME
static int $PURGE_TIME
Key to the tombstone entry timestamp.
Definition: WANObjectCache.php:220
WANObjectCache\$stats
StatsdDataFactoryInterface $stats
Definition: WANObjectCache.php:124
WANObjectCache\setInterimValue
setInterimValue( $key, $value, $ttl, $version, $walltime)
Definition: WANObjectCache.php:1600
WANObjectCache\$processCaches
MapCacheLRU[] $processCaches
Map of group PHP instance caches.
Definition: WANObjectCache.php:120
WANObjectCache\$INTERIM_KEY_PREFIX
static $INTERIM_KEY_PREFIX
Definition: WANObjectCache.php:243
WANObjectCache\processCheckKeys
processCheckKeys(array $timeKeys, array $wrappedValues, $now)
Definition: WANObjectCache.php:495
WANObjectCache\$region
string $region
Physical region for mcrouter use.
Definition: WANObjectCache.php:131