MediaWiki  master
WANObjectCache.php
Go to the documentation of this file.
1 <?php
22 use Liuggio\StatsdClient\Factory\StatsdDataFactoryInterface;
23 use Psr\Log\LoggerAwareInterface;
24 use Psr\Log\LoggerInterface;
25 use Psr\Log\NullLogger;
28 
127 class WANObjectCache implements
131  LoggerAwareInterface
132 {
134  protected $cache;
136  protected $processCaches = [];
138  protected $logger;
140  protected $stats;
142  protected $asyncHandler;
143 
151  protected $broadcastRoute;
153  protected $useInterimHoldOffCaching = true;
155  protected $epoch;
157  protected $secret;
159  protected $coalesceScheme;
160 
162  private $keyHighQps;
165 
167  private $missLog;
168 
170  private $callbackDepth = 0;
172  private $warmupCache = [];
174  private $warmupKeyMisses = 0;
175 
178 
180  private const MAX_COMMIT_DELAY = 3;
182  private const MAX_READ_LAG = 7;
184  public const HOLDOFF_TTL = self::MAX_COMMIT_DELAY + self::MAX_READ_LAG + 1;
185 
187  private const LOW_TTL = 30;
189  public const TTL_LAGGED = 30;
190 
192  private const HOT_TTR = 900;
194  private const AGE_NEW = 60;
195 
197  private const TSE_NONE = -1;
198 
200  public const STALE_TTL_NONE = 0;
202  public const GRACE_TTL_NONE = 0;
204  public const HOLDOFF_TTL_NONE = 0;
205 
207  public const MIN_TIMESTAMP_NONE = 0.0;
208 
210  private const PC_PRIMARY = 'primary:1000';
211 
213  public const PASS_BY_REF = [];
214 
216  private const SCHEME_HASH_TAG = 1;
218  private const SCHEME_HASH_STOP = 2;
219 
221  private const CHECK_KEY_TTL = self::TTL_YEAR;
223  private const INTERIM_KEY_TTL = 1;
224 
226  private const LOCK_TTL = 10;
228  private const COOLOFF_TTL = 1;
230  private const RAMPUP_TTL = 30;
231 
233  private const TINY_NEGATIVE = -0.000001;
235  private const TINY_POSTIVE = 0.000001;
236 
238  private const RECENT_SET_LOW_MS = 50;
240  private const RECENT_SET_HIGH_MS = 100;
241 
243  private const GENERATION_HIGH_SEC = 0.2;
245  private const GENERATION_SLOW_SEC = 3.0;
246 
248  private const PURGE_TIME = 0;
250  private const PURGE_HOLDOFF = 1;
251 
253  private const VERSION = 1;
254 
256  public const KEY_VERSION = 'version';
258  public const KEY_AS_OF = 'asOf';
260  public const KEY_TTL = 'ttl';
262  public const KEY_CUR_TTL = 'curTTL';
264  public const KEY_TOMB_AS_OF = 'tombAsOf';
266  public const KEY_CHECK_AS_OF = 'lastCKPurge';
267 
269  private const RES_VALUE = 0;
271  private const RES_VERSION = 1;
273  private const RES_AS_OF = 2;
275  private const RES_TTL = 3;
277  private const RES_TOMB_AS_OF = 4;
279  private const RES_CHECK_AS_OF = 5;
281  private const RES_TOUCH_AS_OF = 6;
283  private const RES_CUR_TTL = 7;
284 
286  private const FLD_FORMAT_VERSION = 0;
288  private const FLD_VALUE = 1;
290  private const FLD_TTL = 2;
292  private const FLD_TIME = 3;
294  private const FLD_FLAGS = 4;
296  private const FLD_VALUE_VERSION = 5;
298  private const FLD_GENERATION_TIME = 6;
299 
301  private const TYPE_VALUE = 'v';
303  private const TYPE_TIMESTAMP = 't';
305  private const TYPE_MUTEX = 'm';
307  private const TYPE_INTERIM = 'i';
309  private const TYPE_COOLOFF = 'c';
310 
312  private const PURGE_VAL_PREFIX = 'PURGED';
313 
347  public function __construct( array $params ) {
348  $this->cache = $params['cache'];
349  $this->broadcastRoute = $params['broadcastRoutingPrefix'] ?? null;
350  $this->epoch = $params['epoch'] ?? 0;
351  $this->secret = $params['secret'] ?? (string)$this->epoch;
352  if ( ( $params['coalesceScheme'] ?? '' ) === 'hash_tag' ) {
353  // https://redis.io/topics/cluster-spec
354  // https://github.com/twitter/twemproxy/blob/v0.4.1/notes/recommendation.md#hash-tags
355  // https://github.com/Netflix/dynomite/blob/v0.7.0/notes/recommendation.md#hash-tags
356  $this->coalesceScheme = self::SCHEME_HASH_TAG;
357  } else {
358  // https://github.com/facebook/mcrouter/wiki/Key-syntax
359  $this->coalesceScheme = self::SCHEME_HASH_STOP;
360  }
361 
362  $this->keyHighQps = $params['keyHighQps'] ?? 100;
363  $this->keyHighUplinkBps = $params['keyHighUplinkBps'] ?? ( 1e9 / 8 / 100 );
364 
365  $this->setLogger( $params['logger'] ?? new NullLogger() );
366  $this->stats = $params['stats'] ?? new NullStatsdDataFactory();
367  $this->asyncHandler = $params['asyncHandler'] ?? null;
368 
369  $this->missLog = array_fill( 0, 10, [ '', 0.0 ] );
370 
371  $this->cache->registerWrapperInfoForStats(
372  'WANCache',
373  'wanobjectcache',
374  [ __CLASS__, 'getCollectionFromSisterKey' ]
375  );
376  }
377 
381  public function setLogger( LoggerInterface $logger ) {
382  $this->logger = $logger;
383  }
384 
390  public static function newEmpty() {
391  return new static( [ 'cache' => new EmptyBagOStuff() ] );
392  }
393 
449  final public function get( $key, &$curTTL = null, array $checkKeys = [], &$info = [] ) {
450  // Note that an undeclared variable passed as $info starts as null (not the default).
451  // Also, if no $info parameter is provided, then it doesn't matter how it changes here.
452  $legacyInfo = ( $info !== self::PASS_BY_REF );
453 
454  $res = $this->fetchKeys( [ $key ], $checkKeys )[$key];
455 
456  $curTTL = $res[self::RES_CUR_TTL];
457  $info = $legacyInfo
459  : [
460  self::KEY_VERSION => $res[self::RES_VERSION],
461  self::KEY_AS_OF => $res[self::RES_AS_OF],
462  self::KEY_TTL => $res[self::RES_TTL],
463  self::KEY_CUR_TTL => $res[self::RES_CUR_TTL],
464  self::KEY_TOMB_AS_OF => $res[self::RES_TOMB_AS_OF],
465  self::KEY_CHECK_AS_OF => $res[self::RES_CHECK_AS_OF]
466  ];
467 
468  if ( $curTTL === null || $curTTL <= 0 ) {
469  // Log the timestamp in case a corresponding set() call does not provide "walltime"
470  reset( $this->missLog );
471  unset( $this->missLog[key( $this->missLog )] );
472  $this->missLog[] = [ $key, $this->getCurrentTime() ];
473  }
474 
475  return $res[self::RES_VALUE];
476  }
477 
502  final public function getMulti(
503  array $keys,
504  &$curTTLs = [],
505  array $checkKeys = [],
506  &$info = []
507  ) {
508  // Note that an undeclared variable passed as $info starts as null (not the default).
509  // Also, if no $info parameter is provided, then it doesn't matter how it changes here.
510  $legacyInfo = ( $info !== self::PASS_BY_REF );
511 
512  $curTTLs = [];
513  $info = [];
514  $valuesByKey = [];
515 
516  $resByKey = $this->fetchKeys( $keys, $checkKeys );
517  foreach ( $resByKey as $key => $res ) {
518  if ( $res[self::RES_VALUE] !== false ) {
519  $valuesByKey[$key] = $res[self::RES_VALUE];
520  }
521 
522  if ( $res[self::RES_CUR_TTL] !== null ) {
523  $curTTLs[$key] = $res[self::RES_CUR_TTL];
524  }
525  $info[$key] = $legacyInfo
527  : [
528  self::KEY_VERSION => $res[self::RES_VERSION],
529  self::KEY_AS_OF => $res[self::RES_AS_OF],
530  self::KEY_TTL => $res[self::RES_TTL],
531  self::KEY_CUR_TTL => $res[self::RES_CUR_TTL],
532  self::KEY_TOMB_AS_OF => $res[self::RES_TOMB_AS_OF],
533  self::KEY_CHECK_AS_OF => $res[self::RES_CHECK_AS_OF]
534  ];
535  }
536 
537  return $valuesByKey;
538  }
539 
554  protected function fetchKeys( array $keys, array $checkKeys, $touchedCb = null ) {
555  $resByKey = [];
556 
557  // List of all sister keys that need to be fetched from cache
558  $allSisterKeys = [];
559  // Order-corresponding value sister key list for the base key list ($keys)
560  $valueSisterKeys = [];
561  // List of "check" sister keys to compare all value sister keys against
562  $checkSisterKeysForAll = [];
563  // Map of (base key => additional "check" sister key(s) to compare against)
564  $checkSisterKeysByKey = [];
565 
566  foreach ( $keys as $key ) {
567  $sisterKey = $this->makeSisterKey( $key, self::TYPE_VALUE );
568  $allSisterKeys[] = $sisterKey;
569  $valueSisterKeys[] = $sisterKey;
570  }
571 
572  foreach ( $checkKeys as $i => $checkKeyOrKeyGroup ) {
573  // Note: avoid array_merge() inside loop in case there are many keys
574  if ( is_int( $i ) ) {
575  // Single "check" key that applies to all base keys
576  $sisterKey = $this->makeSisterKey( $checkKeyOrKeyGroup, self::TYPE_TIMESTAMP );
577  $allSisterKeys[] = $sisterKey;
578  $checkSisterKeysForAll[] = $sisterKey;
579  } else {
580  // List of "check" keys that apply to a specific base key
581  foreach ( (array)$checkKeyOrKeyGroup as $checkKey ) {
582  $sisterKey = $this->makeSisterKey( $checkKey, self::TYPE_TIMESTAMP );
583  $allSisterKeys[] = $sisterKey;
584  $checkSisterKeysByKey[$i][] = $sisterKey;
585  }
586  }
587  }
588 
589  if ( $this->warmupCache ) {
590  // Get the wrapped values of the sister keys from the warmup cache
591  $wrappedBySisterKey = $this->warmupCache;
592  $sisterKeysMissing = array_diff( $allSisterKeys, array_keys( $wrappedBySisterKey ) );
593  if ( $sisterKeysMissing ) {
594  $this->warmupKeyMisses += count( $sisterKeysMissing );
595  $wrappedBySisterKey += $this->cache->getMulti( $sisterKeysMissing );
596  }
597  } else {
598  // Fetch the wrapped values of the sister keys from the backend
599  $wrappedBySisterKey = $this->cache->getMulti( $allSisterKeys );
600  }
601 
602  // Pessimistically treat the "current time" as the time when any network I/O finished
603  $now = $this->getCurrentTime();
604 
605  // List of "check" sister key purge timestamps to compare all value sister keys against
606  $ckPurgesForAll = $this->processCheckKeys(
607  $checkSisterKeysForAll,
608  $wrappedBySisterKey,
609  $now
610  );
611  // Map of (base key => extra "check" sister key purge timestamp(s) to compare against)
612  $ckPurgesByKey = [];
613  foreach ( $checkSisterKeysByKey as $keyWithCheckKeys => $checkKeysForKey ) {
614  $ckPurgesByKey[$keyWithCheckKeys] = $this->processCheckKeys(
615  $checkKeysForKey,
616  $wrappedBySisterKey,
617  $now
618  );
619  }
620 
621  // Unwrap and validate any value found for each base key (under the value sister key)
622  reset( $keys );
623  foreach ( $valueSisterKeys as $valueSisterKey ) {
624  // Get the corresponding base key for this value sister key
625  $key = current( $keys );
626  next( $keys );
627 
628  if ( array_key_exists( $valueSisterKey, $wrappedBySisterKey ) ) {
629  // Key exists as either a live value or tombstone value
630  $wrapped = $wrappedBySisterKey[$valueSisterKey];
631  } else {
632  // Key does not exist
633  $wrapped = false;
634  }
635 
636  $res = $this->unwrap( $wrapped, $now );
637  $value = $res[self::RES_VALUE];
638 
639  foreach ( array_merge( $ckPurgesForAll, $ckPurgesByKey[$key] ?? [] ) as $ckPurge ) {
641  $ckPurge[self::PURGE_TIME],
642  $res[self::RES_CHECK_AS_OF]
643  );
644  // Timestamp marking the end of the hold-off period for this purge
645  $holdoffDeadline = $ckPurge[self::PURGE_TIME] + $ckPurge[self::PURGE_HOLDOFF];
646  // Check if the value was generated during the hold-off period
647  if ( $value !== false && $holdoffDeadline >= $res[self::RES_AS_OF] ) {
648  // How long ago this value was purged by *this* "check" key
649  $ago = min( $ckPurge[self::PURGE_TIME] - $now, self::TINY_NEGATIVE );
650  // How long ago this value was purged by *any* known "check" key
651  $res[self::RES_CUR_TTL] = min( $res[self::RES_CUR_TTL], $ago );
652  }
653  }
654 
655  if ( $touchedCb !== null && $value !== false ) {
656  $touched = $touchedCb( $value );
657  if ( $touched !== null && $touched >= $res[self::RES_AS_OF] ) {
658  $res[self::RES_CUR_TTL] = min(
659  $res[self::RES_CUR_TTL],
660  $res[self::RES_AS_OF] - $touched,
661  self::TINY_NEGATIVE
662  );
663  }
664  } else {
665  $touched = null;
666  }
667 
668  $res[self::RES_TOUCH_AS_OF] = max( $res[self::RES_TOUCH_AS_OF], $touched );
669 
670  $resByKey[$key] = $res;
671  }
672 
673  return $resByKey;
674  }
675 
682  private function processCheckKeys(
683  array $checkSisterKeys,
684  array $wrappedBySisterKey,
685  float $now
686  ) {
687  $purges = [];
688 
689  foreach ( $checkSisterKeys as $timeKey ) {
690  $purge = isset( $wrappedBySisterKey[$timeKey] )
691  ? $this->parsePurgeValue( $wrappedBySisterKey[$timeKey] )
692  : null;
693 
694  if ( $purge === null ) {
695  $wrapped = $this->makeCheckPurgeValue( $now, self::HOLDOFF_TTL, $purge );
696  $this->cache->add( $timeKey, $wrapped, self::CHECK_KEY_TTL );
697  }
698 
699  $purges[] = $purge;
700  }
701 
702  return $purges;
703  }
704 
785  final public function set( $key, $value, $ttl = self::TTL_INDEFINITE, array $opts = [] ) {
786  $now = $this->getCurrentTime();
787  $dataReplicaLag = $opts['lag'] ?? 0;
788  $dataSnapshotLag = isset( $opts['since'] ) ? max( 0, $now - $opts['since'] ) : 0;
789  $dataCombinedLag = $dataReplicaLag + $dataSnapshotLag;
790  $dataPendingCommit = $opts['pending'] ?? null;
791  $lockTSE = $opts['lockTSE'] ?? self::TSE_NONE;
792  $staleTTL = $opts['staleTTL'] ?? self::STALE_TTL_NONE;
793  $creating = $opts['creating'] ?? false;
794  $version = $opts['version'] ?? null;
795  $walltime = $opts['walltime'] ?? $this->timeSinceLoggedMiss( $key, $now );
796 
797  if ( $ttl < 0 ) {
798  // not cacheable
799  return true;
800  }
801 
802  // Forbid caching data that only exists within an uncommitted transaction. Also, lower
803  // the TTL when the data has a "since" time so far in the past that a delete() tombstone,
804  // made after that time, could have already expired (the key is no longer write-holed).
805  // The mitigation TTL depends on whether this data lag is assumed to systemically effect
806  // regeneration attempts in the near future. The TTL also reflects regeneration wall time.
807  if ( $dataPendingCommit ) {
808  // Case A: data comes from an uncommitted write transaction
809  $mitigated = 'pending writes';
810  // Data might never be committed; rely on a less problematic regeneration attempt
811  $mitigationTTL = self::TTL_UNCACHEABLE;
812  } elseif ( $dataSnapshotLag > self::MAX_READ_LAG ) {
813  // Case B: high snapshot lag
814  $pregenSnapshotLag = ( $walltime !== null ) ? ( $dataSnapshotLag - $walltime ) : 0;
815  if ( ( $pregenSnapshotLag + self::GENERATION_HIGH_SEC ) > self::MAX_READ_LAG ) {
816  // Case B1: generation started when transaction duration was already long
817  $mitigated = 'snapshot lag (late generation)';
818  // Probably non-systemic; rely on a less problematic regeneration attempt
819  $mitigationTTL = self::TTL_UNCACHEABLE;
820  } else {
821  // Case B2: slow generation made transaction duration long
822  $mitigated = 'snapshot lag (high generation time)';
823  // Probably systemic; use a low TTL to avoid stampedes/uncacheability
824  $mitigationTTL = self::LOW_TTL;
825  }
826  } elseif ( $dataReplicaLag === false || $dataReplicaLag > self::MAX_READ_LAG ) {
827  // Case C: low/medium snapshot lag with high replication lag
828  $mitigated = 'replication lag';
829  // Probably systemic; use a low TTL to avoid stampedes/uncacheability
830  $mitigationTTL = self::TTL_LAGGED;
831  } elseif ( $dataCombinedLag > self::MAX_READ_LAG ) {
832  $pregenCombinedLag = ( $walltime !== null ) ? ( $dataCombinedLag - $walltime ) : 0;
833  // Case D: medium snapshot lag with medium replication lag
834  if ( ( $pregenCombinedLag + self::GENERATION_HIGH_SEC ) > self::MAX_READ_LAG ) {
835  // Case D1: generation started when read lag was too high
836  $mitigated = 'read lag (late generation)';
837  // Probably non-systemic; rely on a less problematic regeneration attempt
838  $mitigationTTL = self::TTL_UNCACHEABLE;
839  } else {
840  // Case D2: slow generation made read lag too high
841  $mitigated = 'read lag (high generation time)';
842  // Probably systemic; use a low TTL to avoid stampedes/uncacheability
843  $mitigationTTL = self::LOW_TTL;
844  }
845  } else {
846  // Case E: new value generated with recent data
847  $mitigated = null;
848  // Nothing to mitigate
849  $mitigationTTL = null;
850  }
851 
852  if ( $mitigationTTL === self::TTL_UNCACHEABLE ) {
853  $this->logger->warning(
854  "Rejected set() for {cachekey} due to $mitigated.",
855  [
856  'cachekey' => $key,
857  'lag' => $dataReplicaLag,
858  'age' => $dataSnapshotLag,
859  'walltime' => $walltime
860  ]
861  );
862 
863  // no-op the write for being unsafe
864  return true;
865  }
866 
867  // TTL to use in staleness checks (does not effect persistence layer TTL)
868  $logicalTTL = null;
869 
870  if ( $mitigationTTL !== null ) {
871  // New value was generated from data that is old enough to be risky
872  if ( $lockTSE >= 0 ) {
873  // Persist the value as long as normal, but make it count as stale sooner
874  $logicalTTL = min( $ttl ?: INF, $mitigationTTL );
875  } else {
876  // Persist the value for a shorter duration
877  $ttl = min( $ttl ?: INF, $mitigationTTL );
878  }
879 
880  $this->logger->warning(
881  "Lowered set() TTL for {cachekey} due to $mitigated.",
882  [
883  'cachekey' => $key,
884  'lag' => $dataReplicaLag,
885  'age' => $dataSnapshotLag,
886  'walltime' => $walltime
887  ]
888  );
889  }
890 
891  // Wrap that value with time/TTL/version metadata
892  $wrapped = $this->wrap( $value, $logicalTTL ?: $ttl, $version, $now, $walltime );
893  $storeTTL = $ttl + $staleTTL;
894 
895  if ( $creating ) {
896  $ok = $this->cache->add(
897  $this->makeSisterKey( $key, self::TYPE_VALUE ),
898  $wrapped,
899  $storeTTL
900  );
901  } else {
902  $ok = $this->cache->merge(
903  $this->makeSisterKey( $key, self::TYPE_VALUE ),
904  static function ( $cache, $key, $cWrapped ) use ( $wrapped ) {
905  // A string value means that it is a tombstone; do nothing in that case
906  return ( is_string( $cWrapped ) ) ? false : $wrapped;
907  },
908  $storeTTL,
909  // 1 attempt
910  1
911  );
912  }
913 
914  return $ok;
915  }
916 
979  final public function delete( $key, $ttl = self::HOLDOFF_TTL ) {
980  // Purge values must be stored under the value key so that WANObjectCache::set()
981  // can atomically merge values without accidentally undoing a recent purge and thus
982  // violating the holdoff TTL restriction.
983  $valueSisterKey = $this->makeSisterKey( $key, self::TYPE_VALUE );
984 
985  if ( $ttl <= 0 ) {
986  // A client or cache cleanup script is requesting a cache purge, so there is no
987  // volatility period due to replica DB lag. Any recent change to an entity cached
988  // in this key should have triggered an appropriate purge event.
989  $ok = $this->relayNonVolatilePurge( $valueSisterKey );
990  } else {
991  // A cacheable entity recently changed, so there might be a volatility period due
992  // to replica DB lag. Clients usually expect their actions to be reflected in any
993  // of their subsequent web request. This is attainable if (a) purge relay lag is
994  // lower than the time it takes for subsequent request by the client to arrive,
995  // and, (b) DB replica queries have "read-your-writes" consistency due to DB lag
996  // mitigation systems.
997  $now = $this->getCurrentTime();
998  // Set the key to the purge value in all datacenters
999  $purgeBySisterKey = [ $valueSisterKey => $this->makeTombstonePurgeValue( $now ) ];
1000  $ok = $this->relayVolatilePurges( $purgeBySisterKey, $ttl );
1001  }
1002 
1003  $kClass = $this->determineKeyClassForStats( $key );
1004  $this->stats->increment( "wanobjectcache.$kClass.delete." . ( $ok ? 'ok' : 'error' ) );
1005 
1006  return $ok;
1007  }
1008 
1028  final public function getCheckKeyTime( $key ) {
1029  return $this->getMultiCheckKeyTime( [ $key ] )[$key];
1030  }
1031 
1093  final public function getMultiCheckKeyTime( array $keys ) {
1094  $checkSisterKeysByKey = [];
1095  foreach ( $keys as $key ) {
1096  $checkSisterKeysByKey[$key] = $this->makeSisterKey( $key, self::TYPE_TIMESTAMP );
1097  }
1098 
1099  $wrappedBySisterKey = $this->cache->getMulti( $checkSisterKeysByKey );
1100  $wrappedBySisterKey += array_fill_keys( $checkSisterKeysByKey, false );
1101 
1102  $now = $this->getCurrentTime();
1103  $times = [];
1104  foreach ( $checkSisterKeysByKey as $key => $checkSisterKey ) {
1105  $purge = $this->parsePurgeValue( $wrappedBySisterKey[$checkSisterKey] );
1106  if ( $purge === null ) {
1107  $wrapped = $this->makeCheckPurgeValue( $now, self::HOLDOFF_TTL, $purge );
1108  $this->cache->add( $checkSisterKey, $wrapped, self::CHECK_KEY_TTL );
1109  }
1110 
1111  $times[$key] = $purge[self::PURGE_TIME];
1112  }
1113 
1114  return $times;
1115  }
1116 
1150  final public function touchCheckKey( $key, $holdoff = self::HOLDOFF_TTL ) {
1151  $checkSisterKey = $this->makeSisterKey( $key, self::TYPE_TIMESTAMP );
1152 
1153  $now = $this->getCurrentTime();
1154  $purgeBySisterKey = [ $checkSisterKey => $this->makeCheckPurgeValue( $now, $holdoff ) ];
1155  $ok = $this->relayVolatilePurges( $purgeBySisterKey, self::CHECK_KEY_TTL );
1156 
1157  $kClass = $this->determineKeyClassForStats( $key );
1158  $this->stats->increment( "wanobjectcache.$kClass.ck_touch." . ( $ok ? 'ok' : 'error' ) );
1159 
1160  return $ok;
1161  }
1162 
1190  final public function resetCheckKey( $key ) {
1191  $checkSisterKey = $this->makeSisterKey( $key, self::TYPE_TIMESTAMP );
1192  $ok = $this->relayNonVolatilePurge( $checkSisterKey );
1193 
1194  $kClass = $this->determineKeyClassForStats( $key );
1195  $this->stats->increment( "wanobjectcache.$kClass.ck_reset." . ( $ok ? 'ok' : 'error' ) );
1196 
1197  return $ok;
1198  }
1199 
1503  final public function getWithSetCallback(
1504  $key, $ttl, $callback, array $opts = [], array $cbParams = []
1505  ) {
1506  $version = $opts['version'] ?? null;
1507  $pcTTL = $opts['pcTTL'] ?? self::TTL_UNCACHEABLE;
1508  $pCache = ( $pcTTL >= 0 )
1509  ? $this->getProcessCache( $opts['pcGroup'] ?? self::PC_PRIMARY )
1510  : null;
1511 
1512  // Use the process cache if requested as long as no outer cache callback is running.
1513  // Nested callback process cache use is not lag-safe with regard to HOLDOFF_TTL since
1514  // process cached values are more lagged than persistent ones as they are not purged.
1515  if ( $pCache && $this->callbackDepth == 0 ) {
1516  $cached = $pCache->get( $key, $pcTTL, false );
1517  if ( $cached !== false ) {
1518  $this->logger->debug( "getWithSetCallback($key): process cache hit" );
1519  return $cached;
1520  }
1521  }
1522 
1523  [ $value, $valueVersion, $curAsOf ] = $this->fetchOrRegenerate( $key, $ttl, $callback, $opts, $cbParams );
1524  if ( $valueVersion !== $version ) {
1525  // Current value has a different version; use the variant key for this version.
1526  // Regenerate the variant value if it is not newer than the main value at $key
1527  // so that purges to the main key propagate to the variant value.
1528  $this->logger->debug( "getWithSetCallback($key): using variant key" );
1529  list( $value ) = $this->fetchOrRegenerate(
1530  $this->makeGlobalKey( 'WANCache-key-variant', md5( $key ), (string)$version ),
1531  $ttl,
1532  $callback,
1533  [ 'version' => null, 'minAsOf' => $curAsOf ] + $opts,
1534  $cbParams
1535  );
1536  }
1537 
1538  // Update the process cache if enabled
1539  if ( $pCache && $value !== false ) {
1540  $pCache->set( $key, $value );
1541  }
1542 
1543  return $value;
1544  }
1545 
1562  private function fetchOrRegenerate( $key, $ttl, $callback, array $opts, array $cbParams ) {
1563  $checkKeys = $opts['checkKeys'] ?? [];
1564  $graceTTL = $opts['graceTTL'] ?? self::GRACE_TTL_NONE;
1565  $minAsOf = $opts['minAsOf'] ?? self::MIN_TIMESTAMP_NONE;
1566  $hotTTR = $opts['hotTTR'] ?? self::HOT_TTR;
1567  $lowTTL = $opts['lowTTL'] ?? min( self::LOW_TTL, $ttl );
1568  $ageNew = $opts['ageNew'] ?? self::AGE_NEW;
1569  $touchedCb = $opts['touchedCallback'] ?? null;
1570  $startTime = $this->getCurrentTime();
1571 
1572  $kClass = $this->determineKeyClassForStats( $key );
1573 
1574  // Get the current key value and its metadata
1575  $curState = $this->fetchKeys( [ $key ], $checkKeys, $touchedCb )[$key];
1576  $curValue = $curState[self::RES_VALUE];
1577  // Use the cached value if it exists and is not due for synchronous regeneration
1578  if ( $this->isAcceptablyFreshValue( $curState, $graceTTL, $minAsOf ) ) {
1579  if ( !$this->isLotteryRefreshDue( $curState, $lowTTL, $ageNew, $hotTTR, $startTime ) ) {
1580  $this->stats->timing(
1581  "wanobjectcache.$kClass.hit.good",
1582  1e3 * ( $this->getCurrentTime() - $startTime )
1583  );
1584 
1585  return [ $curValue, $curState[self::RES_VERSION], $curState[self::RES_AS_OF] ];
1586  } elseif ( $this->scheduleAsyncRefresh( $key, $ttl, $callback, $opts, $cbParams ) ) {
1587  $this->logger->debug( "fetchOrRegenerate($key): hit with async refresh" );
1588  $this->stats->timing(
1589  "wanobjectcache.$kClass.hit.refresh",
1590  1e3 * ( $this->getCurrentTime() - $startTime )
1591  );
1592 
1593  return [ $curValue, $curState[self::RES_VERSION], $curState[self::RES_AS_OF] ];
1594  } else {
1595  $this->logger->debug( "fetchOrRegenerate($key): hit with sync refresh" );
1596  }
1597  }
1598 
1599  $isKeyTombstoned = ( $curState[self::RES_TOMB_AS_OF] !== null );
1600  // Use the interim key as an temporary alternative if the key is tombstoned
1601  if ( $isKeyTombstoned ) {
1602  $volState = $this->getInterimValue( $key, $minAsOf, $startTime, $touchedCb );
1603  $volValue = $volState[self::RES_VALUE];
1604  } else {
1605  $volState = $curState;
1606  $volValue = $curValue;
1607  }
1608 
1609  // During the volatile "hold-off" period that follows a purge of the key, the value
1610  // will be regenerated many times if frequently accessed. This is done to mitigate
1611  // the effects of backend replication lag as soon as possible. However, throttle the
1612  // overhead of locking and regeneration by reusing values recently written to cache
1613  // tens of milliseconds ago. Verify the "as of" time against the last purge event.
1614  $lastPurgeTime = max(
1615  // RES_TOUCH_AS_OF depends on the value (possibly from the interim key)
1616  $volState[self::RES_TOUCH_AS_OF],
1617  $curState[self::RES_TOMB_AS_OF],
1618  $curState[self::RES_CHECK_AS_OF]
1619  );
1620  $safeMinAsOf = max( $minAsOf, $lastPurgeTime + self::TINY_POSTIVE );
1621  if ( $this->isExtremelyNewValue( $volState, $safeMinAsOf, $startTime ) ) {
1622  $this->logger->debug( "fetchOrRegenerate($key): volatile hit" );
1623  $this->stats->timing(
1624  "wanobjectcache.$kClass.hit.volatile",
1625  1e3 * ( $this->getCurrentTime() - $startTime )
1626  );
1627 
1628  return [ $volValue, $volState[self::RES_VERSION], $curState[self::RES_AS_OF] ];
1629  }
1630 
1631  $lockTSE = $opts['lockTSE'] ?? self::TSE_NONE;
1632  $busyValue = $opts['busyValue'] ?? null;
1633  $staleTTL = $opts['staleTTL'] ?? self::STALE_TTL_NONE;
1634  $version = $opts['version'] ?? null;
1635 
1636  // Determine whether one thread per datacenter should handle regeneration at a time
1637  $useRegenerationLock =
1638  // Note that since tombstones no-op set(), $lockTSE and $curTTL cannot be used to
1639  // deduce the key hotness because |$curTTL| will always keep increasing until the
1640  // tombstone expires or is overwritten by a new tombstone. Also, even if $lockTSE
1641  // is not set, constant regeneration of a key for the tombstone lifetime might be
1642  // very expensive. Assume tombstoned keys are possibly hot in order to reduce
1643  // the risk of high regeneration load after the delete() method is called.
1644  $isKeyTombstoned ||
1645  // Assume a key is hot if requested soon ($lockTSE seconds) after purge.
1646  // This avoids stampedes when timestamps from $checkKeys/$touchedCb bump.
1647  (
1648  $curState[self::RES_CUR_TTL] !== null &&
1649  $curState[self::RES_CUR_TTL] <= 0 &&
1650  abs( $curState[self::RES_CUR_TTL] ) <= $lockTSE
1651  ) ||
1652  // Assume a key is hot if there is no value and a busy fallback is given.
1653  // This avoids stampedes on eviction or preemptive regeneration taking too long.
1654  ( $busyValue !== null && $volValue === false );
1655 
1656  // If a regeneration lock is required, threads that do not get the lock will try to use
1657  // the stale value, the interim value, or the $busyValue placeholder, in that order. If
1658  // none of those are set then all threads will bypass the lock and regenerate the value.
1659  $hasLock = $useRegenerationLock && $this->claimStampedeLock( $key );
1660  if ( $useRegenerationLock && !$hasLock ) {
1661  // Determine if there is stale or volatile cached value that is still usable
1662  // @phan-suppress-next-line PhanTypeMismatchArgumentNullable False positive
1663  if ( $this->isValid( $volValue, $volState[self::RES_AS_OF], $minAsOf ) ) {
1664  $this->logger->debug( "fetchOrRegenerate($key): returning stale value" );
1665  $this->stats->timing(
1666  "wanobjectcache.$kClass.hit.stale",
1667  1e3 * ( $this->getCurrentTime() - $startTime )
1668  );
1669 
1670  return [ $volValue, $volState[self::RES_VERSION], $curState[self::RES_AS_OF] ];
1671  } elseif ( $busyValue !== null ) {
1672  $miss = is_infinite( $minAsOf ) ? 'renew' : 'miss';
1673  $this->logger->debug( "fetchOrRegenerate($key): busy $miss" );
1674  $this->stats->timing(
1675  "wanobjectcache.$kClass.$miss.busy",
1676  1e3 * ( $this->getCurrentTime() - $startTime )
1677  );
1678  $placeholderValue = $this->resolveBusyValue( $busyValue );
1679 
1680  return [ $placeholderValue, $version, $curState[self::RES_AS_OF] ];
1681  }
1682  }
1683 
1684  // Generate the new value given any prior value with a matching version
1685  $setOpts = [];
1686  $preCallbackTime = $this->getCurrentTime();
1688  try {
1689  $value = $callback(
1690  ( $curState[self::RES_VERSION] === $version ) ? $curValue : false,
1691  $ttl,
1692  $setOpts,
1693  ( $curState[self::RES_VERSION] === $version ) ? $curState[self::RES_AS_OF] : null,
1694  $cbParams
1695  );
1696  } finally {
1698  }
1699  $postCallbackTime = $this->getCurrentTime();
1700 
1701  // How long it took to fetch, validate, and generate the value
1702  $elapsed = max( $postCallbackTime - $startTime, 0.0 );
1703 
1704  // How long it took to generate the value
1705  $walltime = max( $postCallbackTime - $preCallbackTime, 0.0 );
1706  $this->stats->timing( "wanobjectcache.$kClass.regen_walltime", 1e3 * $walltime );
1707 
1708  // Attempt to save the newly generated value if applicable
1709  if (
1710  // Callback yielded a cacheable value
1711  // @phan-suppress-next-line PhanPossiblyUndeclaredVariable False positive
1712  ( $value !== false && $ttl >= 0 ) &&
1713  // Current thread was not raced out of a regeneration lock or key is tombstoned
1714  ( !$useRegenerationLock || $hasLock || $isKeyTombstoned ) &&
1715  // Key does not appear to be undergoing a set() stampede
1716  // @phan-suppress-next-line PhanPossiblyUndeclaredVariable False positive
1717  $this->checkAndSetCooloff( $key, $kClass, $value, $elapsed, $hasLock )
1718  ) {
1719  // If the key is write-holed then use the (volatile) interim key as an alternative
1720  if ( $isKeyTombstoned ) {
1721  // @phan-suppress-next-line PhanPossiblyUndeclaredVariable False positive
1722  $this->setInterimValue( $key, $value, $lockTSE, $version, $postCallbackTime, $walltime );
1723  } else {
1724  $finalSetOpts = [
1725  // @phan-suppress-next-line PhanUselessBinaryAddRight,PhanCoalescingAlwaysNull
1726  'since' => $setOpts['since'] ?? $preCallbackTime,
1727  'version' => $version,
1728  'staleTTL' => $staleTTL,
1729  // informs lag vs performance trade-offs
1730  'lockTSE' => $lockTSE,
1731  // optimization
1732  'creating' => ( $curValue === false ),
1733  'walltime' => $walltime
1734  ] + $setOpts;
1735  // @phan-suppress-next-line PhanTypeMismatchArgument,PhanPossiblyUndeclaredVariable False positive
1736  $this->set( $key, $value, $ttl, $finalSetOpts );
1737  }
1738  }
1739 
1740  $this->yieldStampedeLock( $key, $hasLock );
1741 
1742  $miss = is_infinite( $minAsOf ) ? 'renew' : 'miss';
1743  $this->logger->debug( "fetchOrRegenerate($key): $miss, new value computed" );
1744  $this->stats->timing(
1745  "wanobjectcache.$kClass.$miss.compute",
1746  1e3 * ( $this->getCurrentTime() - $startTime )
1747  );
1748 
1749  // @phan-suppress-next-line PhanPossiblyUndeclaredVariable False positive
1750  return [ $value, $version, $curState[self::RES_AS_OF] ];
1751  }
1752 
1757  private function claimStampedeLock( $key ) {
1758  $checkSisterKey = $this->makeSisterKey( $key, self::TYPE_MUTEX );
1759  // Note that locking is not bypassed due to I/O errors; this avoids stampedes
1760  return $this->cache->add( $checkSisterKey, 1, self::LOCK_TTL );
1761  }
1762 
1767  private function yieldStampedeLock( $key, $hasLock ) {
1768  if ( $hasLock ) {
1769  $checkSisterKey = $this->makeSisterKey( $key, self::TYPE_MUTEX );
1770  $this->cache->changeTTL( $checkSisterKey, (int)$this->getCurrentTime() - 60 );
1771  }
1772  }
1773 
1784  private function makeSisterKeys( array $baseKeys, string $type, string $route = null ) {
1785  $sisterKeys = [];
1786  foreach ( $baseKeys as $baseKey ) {
1787  $sisterKeys[] = $this->makeSisterKey( $baseKey, $type, $route );
1788  }
1789 
1790  return $sisterKeys;
1791  }
1792 
1803  private function makeSisterKey( string $baseKey, string $typeChar, string $route = null ) {
1804  if ( $this->coalesceScheme === self::SCHEME_HASH_STOP ) {
1805  // Key style: "WANCache:<base key>|#|<character>"
1806  $sisterKey = 'WANCache:' . $baseKey . '|#|' . $typeChar;
1807  } else {
1808  // Key style: "WANCache:{<base key>}:<character>"
1809  $sisterKey = 'WANCache:{' . $baseKey . '}:' . $typeChar;
1810  }
1811 
1812  if ( $route !== null ) {
1813  $sisterKey = $this->prependRoute( $sisterKey, $route );
1814  }
1815 
1816  return $sisterKey;
1817  }
1818 
1825  public static function getCollectionFromSisterKey( string $sisterKey ) {
1826  if ( substr( $sisterKey, -4 ) === '|#|v' ) {
1827  // Key style: "WANCache:<base key>|#|<character>"
1828  $collection = substr( $sisterKey, 9, strcspn( $sisterKey, ':|', 9 ) );
1829  } elseif ( substr( $sisterKey, -3 ) === '}:v' ) {
1830  // Key style: "WANCache:{<base key>}:<character>"
1831  $collection = substr( $sisterKey, 10, strcspn( $sisterKey, ':}', 10 ) );
1832  } else {
1833  $collection = 'internal';
1834  }
1835 
1836  return $collection;
1837  }
1838 
1851  private function isExtremelyNewValue( $res, $minAsOf, $now ) {
1852  if ( $res[self::RES_VALUE] === false || $res[self::RES_AS_OF] < $minAsOf ) {
1853  return false;
1854  }
1855 
1856  $age = $now - $res[self::RES_AS_OF];
1857 
1858  return ( $age < mt_rand( self::RECENT_SET_LOW_MS, self::RECENT_SET_HIGH_MS ) / 1e3 );
1859  }
1860 
1882  private function checkAndSetCooloff( $key, $kClass, $value, $elapsed, $hasLock ) {
1883  $valueSisterKey = $this->makeSisterKey( $key, self::TYPE_VALUE );
1884  list( $estimatedSize ) = $this->cache->setNewPreparedValues( [
1885  $valueSisterKey => $value
1886  ] );
1887 
1888  if ( !$hasLock ) {
1889  // Suppose that this cache key is very popular (KEY_HIGH_QPS reads/second).
1890  // After eviction, there will be cache misses until it gets regenerated and saved.
1891  // If the time window when the key is missing lasts less than one second, then the
1892  // number of misses will not reach KEY_HIGH_QPS. This window largely corresponds to
1893  // the key regeneration time. Estimate the count/rate of cache misses, e.g.:
1894  // - 100 QPS, 20ms regeneration => ~2 misses (< 1s)
1895  // - 100 QPS, 100ms regeneration => ~10 misses (< 1s)
1896  // - 100 QPS, 3000ms regeneration => ~300 misses (100/s for 3s)
1897  $missesPerSecForHighQPS = ( min( $elapsed, 1 ) * $this->keyHighQps );
1898 
1899  // Determine whether there is enough I/O stampede risk to justify throttling set().
1900  // Estimate unthrottled set() overhead, as bps, from miss count/rate and value size,
1901  // comparing it to the per-key uplink bps limit (KEY_HIGH_UPLINK_BPS), e.g.:
1902  // - 2 misses (< 1s), 10KB value, 1250000 bps limit => 160000 bits (low risk)
1903  // - 2 misses (< 1s), 100KB value, 1250000 bps limit => 1600000 bits (high risk)
1904  // - 10 misses (< 1s), 10KB value, 1250000 bps limit => 800000 bits (low risk)
1905  // - 10 misses (< 1s), 100KB value, 1250000 bps limit => 8000000 bits (high risk)
1906  // - 300 misses (100/s), 1KB value, 1250000 bps limit => 800000 bps (low risk)
1907  // - 300 misses (100/s), 10KB value, 1250000 bps limit => 8000000 bps (high risk)
1908  // - 300 misses (100/s), 100KB value, 1250000 bps limit => 80000000 bps (high risk)
1909  if ( ( $missesPerSecForHighQPS * $estimatedSize ) >= $this->keyHighUplinkBps ) {
1910  $cooloffSisterKey = $this->makeSisterKey( $key, self::TYPE_COOLOFF );
1911  $watchPoint = $this->cache->watchErrors();
1912  if (
1913  !$this->cache->add( $cooloffSisterKey, 1, self::COOLOFF_TTL ) &&
1914  // Don't treat failures due to I/O errors as the key being in cool-off
1915  $this->cache->getLastError( $watchPoint ) === self::ERR_NONE
1916  ) {
1917  $this->stats->increment( "wanobjectcache.$kClass.cooloff_bounce" );
1918 
1919  return false;
1920  }
1921  }
1922  }
1923 
1924  // Corresponding metrics for cache writes that actually get sent over the write
1925  $this->stats->timing( "wanobjectcache.$kClass.regen_set_delay", 1e3 * $elapsed );
1926  $this->stats->updateCount( "wanobjectcache.$kClass.regen_set_bytes", $estimatedSize );
1927 
1928  return true;
1929  }
1930 
1940  private function getInterimValue( $key, $minAsOf, $now, $touchedCb ) {
1941  if ( $this->useInterimHoldOffCaching ) {
1942  $interimSisterKey = $this->makeSisterKey( $key, self::TYPE_INTERIM );
1943  $wrapped = $this->cache->get( $interimSisterKey );
1944  $res = $this->unwrap( $wrapped, $now );
1945  if ( $res[self::RES_VALUE] !== false && $res[self::RES_AS_OF] >= $minAsOf ) {
1946  if ( $touchedCb !== null ) {
1947  // Update "last purge time" since the $touchedCb timestamp depends on $value
1948  // Get the new "touched timestamp", accounting for callback-checked dependencies
1949  $res[self::RES_TOUCH_AS_OF] = max(
1950  $touchedCb( $res[self::RES_VALUE] ),
1951  $res[self::RES_TOUCH_AS_OF]
1952  );
1953  }
1954 
1955  return $res;
1956  }
1957  }
1958 
1959  return $this->unwrap( false, $now );
1960  }
1961 
1970  private function setInterimValue( $key, $value, $ttl, $version, $now, $walltime ) {
1971  $ttl = max( self::INTERIM_KEY_TTL, (int)$ttl );
1972 
1973  $wrapped = $this->wrap( $value, $ttl, $version, $now, $walltime );
1974  $this->cache->set(
1975  $this->makeSisterKey( $key, self::TYPE_INTERIM ),
1976  $wrapped,
1977  $ttl
1978  );
1979  }
1980 
1985  private function resolveBusyValue( $busyValue ) {
1986  return ( $busyValue instanceof Closure ) ? $busyValue() : $busyValue;
1987  }
1988 
2054  final public function getMultiWithSetCallback(
2055  ArrayIterator $keyedIds, $ttl, callable $callback, array $opts = []
2056  ) {
2057  // Batch load required keys into the in-process warmup cache
2058  $this->warmupCache = $this->fetchWrappedValuesForWarmupCache(
2059  $this->getNonProcessCachedMultiKeys( $keyedIds, $opts ),
2060  $opts['checkKeys'] ?? []
2061  );
2062  $this->warmupKeyMisses = 0;
2063 
2064  // The required callback signature includes $id as the first argument for convenience
2065  // to distinguish different items. To reuse the code in getWithSetCallback(), wrap the
2066  // callback with a proxy callback that has the standard getWithSetCallback() signature.
2067  // This is defined only once per batch to avoid closure creation overhead.
2068  $proxyCb = static function ( $oldValue, &$ttl, &$setOpts, $oldAsOf, $params )
2069  use ( $callback )
2070  {
2071  return $callback( $params['id'], $oldValue, $ttl, $setOpts, $oldAsOf );
2072  };
2073 
2074  // Get the order-preserved result map using the warm-up cache
2075  $values = [];
2076  foreach ( $keyedIds as $key => $id ) {
2077  $values[$key] = $this->getWithSetCallback(
2078  $key,
2079  $ttl,
2080  $proxyCb,
2081  $opts,
2082  [ 'id' => $id ]
2083  );
2084  }
2085 
2086  $this->warmupCache = [];
2087 
2088  return $values;
2089  }
2090 
2157  final public function getMultiWithUnionSetCallback(
2158  ArrayIterator $keyedIds, $ttl, callable $callback, array $opts = []
2159  ) {
2160  $checkKeys = $opts['checkKeys'] ?? [];
2161  $minAsOf = $opts['minAsOf'] ?? self::MIN_TIMESTAMP_NONE;
2162 
2163  // unset incompatible keys
2164  unset( $opts['lockTSE'] );
2165  unset( $opts['busyValue'] );
2166 
2167  // Batch load required keys into the in-process warmup cache
2168  $keysByIdGet = $this->getNonProcessCachedMultiKeys( $keyedIds, $opts );
2169  $this->warmupCache = $this->fetchWrappedValuesForWarmupCache( $keysByIdGet, $checkKeys );
2170  $this->warmupKeyMisses = 0;
2171 
2172  // IDs of entities known to be in need of generation
2173  $idsRegen = [];
2174 
2175  // Find out which keys are missing/deleted/stale
2176  $resByKey = $this->fetchKeys( $keysByIdGet, $checkKeys );
2177  foreach ( $keysByIdGet as $id => $key ) {
2178  $res = $resByKey[$key];
2179  if (
2180  $res[self::RES_VALUE] === false ||
2181  $res[self::RES_CUR_TTL] < 0 ||
2182  $res[self::RES_AS_OF] < $minAsOf
2183  ) {
2184  $idsRegen[] = $id;
2185  }
2186  }
2187 
2188  // Run the callback to populate the generation value map for all required IDs
2189  $newSetOpts = [];
2190  $newTTLsById = array_fill_keys( $idsRegen, $ttl );
2191  $newValsById = $idsRegen ? $callback( $idsRegen, $newTTLsById, $newSetOpts ) : [];
2192 
2193  $method = __METHOD__;
2194  // The required callback signature includes $id as the first argument for convenience
2195  // to distinguish different items. To reuse the code in getWithSetCallback(), wrap the
2196  // callback with a proxy callback that has the standard getWithSetCallback() signature.
2197  // This is defined only once per batch to avoid closure creation overhead.
2198  $proxyCb = function ( $oldValue, &$ttl, &$setOpts, $oldAsOf, $params )
2199  use ( $callback, $newValsById, $newTTLsById, $newSetOpts, $method )
2200  {
2201  $id = $params['id'];
2202 
2203  if ( array_key_exists( $id, $newValsById ) ) {
2204  // Value was already regenerated as expected, so use the value in $newValsById
2205  $newValue = $newValsById[$id];
2206  $ttl = $newTTLsById[$id];
2207  $setOpts = $newSetOpts;
2208  } else {
2209  // Pre-emptive/popularity refresh and version mismatch cases are not detected
2210  // above and thus $newValsById has no entry. Run $callback on this single entity.
2211  $ttls = [ $id => $ttl ];
2212  $result = $callback( [ $id ], $ttls, $setOpts );
2213  if ( !isset( $result[$id] ) ) {
2214  // T303092
2215  $this->logger->warning(
2216  $method . ' failed due to {id} not set in result {result}', [
2217  'id' => $id,
2218  'result' => json_encode( $result )
2219  ] );
2220  }
2221  $newValue = $result[$id];
2222  $ttl = $ttls[$id];
2223  }
2224 
2225  return $newValue;
2226  };
2227 
2228  // Get the order-preserved result map using the warm-up cache
2229  $values = [];
2230  foreach ( $keyedIds as $key => $id ) {
2231  $values[$key] = $this->getWithSetCallback(
2232  $key,
2233  $ttl,
2234  $proxyCb,
2235  $opts,
2236  [ 'id' => $id ]
2237  );
2238  }
2239 
2240  $this->warmupCache = [];
2241 
2242  return $values;
2243  }
2244 
2257  final public function reap( $key, $purgeTimestamp, &$isStale = false ) {
2258  $valueSisterKey = $this->makeSisterKey( $key, self::TYPE_VALUE );
2259 
2260  $minAsOf = $purgeTimestamp + self::HOLDOFF_TTL;
2261  $wrapped = $this->cache->get( $valueSisterKey );
2262  if ( is_array( $wrapped ) && $wrapped[self::FLD_TIME] < $minAsOf ) {
2263  $isStale = true;
2264  $this->logger->warning( "Reaping stale value key '$key'." );
2265  // avoids races with tombstone creation
2266  $ttlReap = self::HOLDOFF_TTL;
2267  $ok = $this->cache->changeTTL( $valueSisterKey, $ttlReap );
2268  if ( !$ok ) {
2269  $this->logger->error( "Could not complete reap of key '$key'." );
2270  }
2271 
2272  return $ok;
2273  }
2274 
2275  $isStale = false;
2276 
2277  return true;
2278  }
2279 
2289  final public function reapCheckKey( $key, $purgeTimestamp, &$isStale = false ) {
2290  $checkSisterKey = $this->makeSisterKey( $key, self::TYPE_TIMESTAMP );
2291 
2292  $wrapped = $this->cache->get( $checkSisterKey );
2293  $purge = $this->parsePurgeValue( $wrapped );
2294  if ( $purge !== null && $purge[self::PURGE_TIME] < $purgeTimestamp ) {
2295  $isStale = true;
2296  $this->logger->warning( "Reaping stale check key '$key'." );
2297  $ok = $this->cache->changeTTL( $checkSisterKey, self::TTL_SECOND );
2298  if ( !$ok ) {
2299  $this->logger->error( "Could not complete reap of check key '$key'." );
2300  }
2301 
2302  return $ok;
2303  }
2304 
2305  $isStale = false;
2306 
2307  return false;
2308  }
2309 
2320  public function makeGlobalKey( $collection, ...$components ) {
2321  return $this->cache->makeGlobalKey( ...func_get_args() );
2322  }
2323 
2334  public function makeKey( $collection, ...$components ) {
2335  return $this->cache->makeKey( ...func_get_args() );
2336  }
2337 
2345  public function hash256( $component ) {
2346  return hash_hmac( 'sha256', $component, $this->secret );
2347  }
2348 
2398  final public function makeMultiKeys( array $ids, $keyCallback ) {
2399  $idByKey = [];
2400  foreach ( $ids as $id ) {
2401  // Discourage triggering of automatic makeKey() hashing in some backends
2402  if ( strlen( $id ) > 64 ) {
2403  $this->logger->warning( __METHOD__ . ": long ID '$id'; use hash256()" );
2404  }
2405  $key = $keyCallback( $id, $this );
2406  // Edge case: ignore key collisions due to duplicate $ids like "42" and 42
2407  if ( !isset( $idByKey[$key] ) ) {
2408  $idByKey[$key] = $id;
2409  } elseif ( (string)$id !== (string)$idByKey[$key] ) {
2410  throw new UnexpectedValueException(
2411  "Cache key collision; IDs ('$id','{$idByKey[$key]}') map to '$key'"
2412  );
2413  }
2414  }
2415 
2416  return new ArrayIterator( $idByKey );
2417  }
2418 
2454  final public function multiRemap( array $ids, array $res ) {
2455  if ( count( $ids ) !== count( $res ) ) {
2456  // If makeMultiKeys() is called on a list of non-unique IDs, then the resulting
2457  // ArrayIterator will have less entries due to "first appearance" de-duplication
2458  $ids = array_keys( array_fill_keys( $ids, true ) );
2459  if ( count( $ids ) !== count( $res ) ) {
2460  throw new UnexpectedValueException( "Multi-key result does not match ID list" );
2461  }
2462  }
2463 
2464  return array_combine( $ids, $res );
2465  }
2466 
2473  public function watchErrors() {
2474  return $this->cache->watchErrors();
2475  }
2476 
2494  final public function getLastError( $watchPoint = 0 ) {
2495  $code = $this->cache->getLastError( $watchPoint );
2496  switch ( $code ) {
2497  case self::ERR_NONE:
2498  return self::ERR_NONE;
2499  case self::ERR_NO_RESPONSE:
2500  return self::ERR_NO_RESPONSE;
2501  case self::ERR_UNREACHABLE:
2502  return self::ERR_UNREACHABLE;
2503  default:
2504  return self::ERR_UNEXPECTED;
2505  }
2506  }
2507 
2512  final public function clearLastError() {
2513  $this->cache->clearLastError();
2514  }
2515 
2521  public function clearProcessCache() {
2522  $this->processCaches = [];
2523  }
2524 
2545  final public function useInterimHoldOffCaching( $enabled ) {
2546  $this->useInterimHoldOffCaching = $enabled;
2547  }
2548 
2554  public function getQoS( $flag ) {
2555  return $this->cache->getQoS( $flag );
2556  }
2557 
2621  public function adaptiveTTL( $mtime, $maxTTL, $minTTL = 30, $factor = 0.2 ) {
2622  // handle fractional seconds and string integers
2623  $mtime = (int)$mtime;
2624  if ( $mtime <= 0 ) {
2625  // no last-modified time provided
2626  return $minTTL;
2627  }
2628 
2629  $age = (int)$this->getCurrentTime() - $mtime;
2630 
2631  return (int)min( $maxTTL, max( $minTTL, $factor * $age ) );
2632  }
2633 
2639  final public function getWarmupKeyMisses() {
2640  // Number of misses in $this->warmupCache during the last call to certain methods
2641  return $this->warmupKeyMisses;
2642  }
2643 
2658  protected function relayVolatilePurges( array $purgeBySisterKey, int $ttl ) {
2659  $purgeByRouteKey = [];
2660  foreach ( $purgeBySisterKey as $sisterKey => $purge ) {
2661  if ( $this->broadcastRoute !== null ) {
2662  $routeKey = $this->prependRoute( $sisterKey, $this->broadcastRoute );
2663  } else {
2664  $routeKey = $sisterKey;
2665  }
2666  $purgeByRouteKey[$routeKey] = $purge;
2667  }
2668 
2669  if ( count( $purgeByRouteKey ) == 1 ) {
2670  $purge = reset( $purgeByRouteKey );
2671  $ok = $this->cache->set( key( $purgeByRouteKey ), $purge, $ttl );
2672  } else {
2673  $ok = $this->cache->setMulti( $purgeByRouteKey, $ttl );
2674  }
2675 
2676  return $ok;
2677  }
2678 
2687  protected function relayNonVolatilePurge( string $sisterKey ) {
2688  if ( $this->broadcastRoute !== null ) {
2689  $routeKey = $this->prependRoute( $sisterKey, $this->broadcastRoute );
2690  } else {
2691  $routeKey = $sisterKey;
2692  }
2693 
2694  return $this->cache->delete( $routeKey );
2695  }
2696 
2702  protected function prependRoute( string $sisterKey, string $route ) {
2703  if ( $sisterKey[0] === '/' ) {
2704  throw new RuntimeException( "Sister key '$sisterKey' already contains a route." );
2705  }
2706 
2707  return $route . $sisterKey;
2708  }
2709 
2721  private function scheduleAsyncRefresh( $key, $ttl, $callback, array $opts, array $cbParams ) {
2722  if ( !$this->asyncHandler ) {
2723  return false;
2724  }
2725  // Update the cache value later, such during post-send of an HTTP request. This forces
2726  // cache regeneration by setting "minAsOf" to infinity, meaning that no existing value
2727  // is considered valid. Furthermore, note that preemptive regeneration is not applicable
2728  // to invalid values, so there is no risk of infinite preemptive regeneration loops.
2729  $func = $this->asyncHandler;
2730  $func( function () use ( $key, $ttl, $callback, $opts, $cbParams ) {
2731  $opts['minAsOf'] = INF;
2732  try {
2733  $this->fetchOrRegenerate( $key, $ttl, $callback, $opts, $cbParams );
2734  } catch ( Exception $e ) {
2735  // Log some context for easier debugging
2736  $this->logger->error( 'Async refresh failed for {key}', [
2737  'key' => $key,
2738  'ttl' => $ttl,
2739  'exception' => $e
2740  ] );
2741  throw $e;
2742  }
2743  } );
2744 
2745  return true;
2746  }
2747 
2756  private function isAcceptablyFreshValue( $res, $graceTTL, $minAsOf ) {
2757  if ( !$this->isValid( $res[self::RES_VALUE], $res[self::RES_AS_OF], $minAsOf ) ) {
2758  // Value does not exists or is too old
2759  return false;
2760  }
2761 
2762  $curTTL = $res[self::RES_CUR_TTL];
2763  if ( $curTTL > 0 ) {
2764  // Value is definitely still fresh
2765  return true;
2766  }
2767 
2768  // Remaining seconds during which this stale value can be used
2769  $curGraceTTL = $graceTTL + $curTTL;
2770 
2771  return ( $curGraceTTL > 0 )
2772  // Chance of using the value decreases as $curTTL goes from 0 to -$graceTTL
2773  ? !$this->worthRefreshExpiring( $curGraceTTL, $graceTTL, $graceTTL )
2774  // Value is too stale to fall in the grace period
2775  : false;
2776  }
2777 
2788  protected function isLotteryRefreshDue( $res, $lowTTL, $ageNew, $hotTTR, $now ) {
2789  $curTTL = $res[self::RES_CUR_TTL];
2790  $logicalTTL = $res[self::RES_TTL];
2791  $asOf = $res[self::RES_AS_OF];
2792 
2793  return (
2794  $this->worthRefreshExpiring( $curTTL, $logicalTTL, $lowTTL ) ||
2795  $this->worthRefreshPopular( $asOf, $ageNew, $hotTTR, $now )
2796  );
2797  }
2798 
2814  protected function worthRefreshPopular( $asOf, $ageNew, $timeTillRefresh, $now ) {
2815  if ( $ageNew < 0 || $timeTillRefresh <= 0 ) {
2816  return false;
2817  }
2818 
2819  $age = $now - $asOf;
2820  $timeOld = $age - $ageNew;
2821  if ( $timeOld <= 0 ) {
2822  return false;
2823  }
2824 
2825  $popularHitsPerSec = 1;
2826  // Lifecycle is: new, ramp-up refresh chance, full refresh chance.
2827  // Note that the "expected # of refreshes" for the ramp-up time range is half
2828  // of what it would be if P(refresh) was at its full value during that time range.
2829  $refreshWindowSec = max( $timeTillRefresh - $ageNew - self::RAMPUP_TTL / 2, 1 );
2830  // P(refresh) * (# hits in $refreshWindowSec) = (expected # of refreshes)
2831  // P(refresh) * ($refreshWindowSec * $popularHitsPerSec) = 1 (by definition)
2832  // P(refresh) = 1/($refreshWindowSec * $popularHitsPerSec)
2833  $chance = 1 / ( $popularHitsPerSec * $refreshWindowSec );
2834  // Ramp up $chance from 0 to its nominal value over RAMPUP_TTL seconds to avoid stampedes
2835  $chance *= ( $timeOld <= self::RAMPUP_TTL ) ? $timeOld / self::RAMPUP_TTL : 1;
2836 
2837  return ( mt_rand( 1, 1000000000 ) <= 1000000000 * $chance );
2838  }
2839 
2858  protected function worthRefreshExpiring( $curTTL, $logicalTTL, $lowTTL ) {
2859  if ( $lowTTL <= 0 ) {
2860  return false;
2861  }
2862 
2863  // T264787: avoid having keys start off with a high chance of being refreshed;
2864  // the point where refreshing becomes possible cannot precede the key lifetime.
2865  $effectiveLowTTL = min( $lowTTL, $logicalTTL ?: INF );
2866 
2867  if ( $curTTL >= $effectiveLowTTL || $curTTL <= 0 ) {
2868  return false;
2869  }
2870 
2871  $chance = ( 1 - $curTTL / $effectiveLowTTL );
2872 
2873  return ( mt_rand( 1, 1000000000 ) <= 1000000000 * $chance );
2874  }
2875 
2884  protected function isValid( $value, $asOf, $minAsOf ) {
2885  return ( $value !== false && $asOf >= $minAsOf );
2886  }
2887 
2896  private function wrap( $value, $ttl, $version, $now, $walltime ) {
2897  // Returns keys in ascending integer order for PHP7 array packing:
2898  // https://nikic.github.io/2014/12/22/PHPs-new-hashtable-implementation.html
2899  $wrapped = [
2900  self::FLD_FORMAT_VERSION => self::VERSION,
2901  self::FLD_VALUE => $value,
2902  self::FLD_TTL => $ttl,
2903  self::FLD_TIME => $now
2904  ];
2905  if ( $version !== null ) {
2906  $wrapped[self::FLD_VALUE_VERSION] = $version;
2907  }
2908  if ( $walltime >= self::GENERATION_SLOW_SEC ) {
2909  $wrapped[self::FLD_GENERATION_TIME] = $walltime;
2910  }
2911 
2912  return $wrapped;
2913  }
2914 
2929  private function unwrap( $wrapped, $now ) {
2930  // https://nikic.github.io/2014/12/22/PHPs-new-hashtable-implementation.html
2931  $res = [
2932  // Attributes that only depend on the fetched key value
2933  self::RES_VALUE => false,
2934  self::RES_VERSION => null,
2935  self::RES_AS_OF => null,
2936  self::RES_TTL => null,
2937  self::RES_TOMB_AS_OF => null,
2938  // Attributes that depend on caller-specific "check" keys or "touched callbacks"
2939  self::RES_CHECK_AS_OF => null,
2940  self::RES_TOUCH_AS_OF => null,
2941  self::RES_CUR_TTL => null
2942  ];
2943 
2944  if ( is_array( $wrapped ) ) {
2945  // Entry expected to be a cached value; validate it
2946  if (
2947  ( $wrapped[self::FLD_FORMAT_VERSION] ?? null ) === self::VERSION &&
2948  $wrapped[self::FLD_TIME] >= $this->epoch
2949  ) {
2950  if ( $wrapped[self::FLD_TTL] > 0 ) {
2951  // Get the approximate time left on the key
2952  $age = $now - $wrapped[self::FLD_TIME];
2953  $curTTL = max( $wrapped[self::FLD_TTL] - $age, 0.0 );
2954  } else {
2955  // Key had no TTL, so the time left is unbounded
2956  $curTTL = INF;
2957  }
2958  $res[self::RES_VALUE] = $wrapped[self::FLD_VALUE];
2959  $res[self::RES_VERSION] = $wrapped[self::FLD_VALUE_VERSION] ?? null;
2960  $res[self::RES_AS_OF] = $wrapped[self::FLD_TIME];
2961  $res[self::RES_CUR_TTL] = $curTTL;
2962  $res[self::RES_TTL] = $wrapped[self::FLD_TTL];
2963  }
2964  } else {
2965  // Entry expected to be a tombstone; parse it
2966  $purge = $this->parsePurgeValue( $wrapped );
2967  if ( $purge !== null ) {
2968  // Tombstoned keys should always have a negative "current TTL"
2969  $curTTL = min( $purge[self::PURGE_TIME] - $now, self::TINY_NEGATIVE );
2970  $res[self::RES_CUR_TTL] = $curTTL;
2972  }
2973  }
2974 
2975  return $res;
2976  }
2977 
2982  private function determineKeyClassForStats( $key ) {
2983  $parts = explode( ':', $key, 3 );
2984  // Fallback in case the key was not made by makeKey.
2985  // Replace dots because they are special in StatsD (T232907)
2986  return strtr( $parts[1] ?? $parts[0], '.', '_' );
2987  }
2988 
2997  private function parsePurgeValue( $value ) {
2998  if ( !is_string( $value ) ) {
2999  return null;
3000  }
3001 
3002  $segments = explode( ':', $value, 3 );
3003  $prefix = $segments[0];
3004  if ( $prefix !== self::PURGE_VAL_PREFIX ) {
3005  // Not a purge value
3006  return null;
3007  }
3008 
3009  $timestamp = (float)$segments[1];
3010  // makeTombstonePurgeValue() doesn't store hold-off TTLs
3011  $holdoff = isset( $segments[2] ) ? (int)$segments[2] : self::HOLDOFF_TTL;
3012 
3013  if ( $timestamp < $this->epoch ) {
3014  // Purge value is too old
3015  return null;
3016  }
3017 
3018  return [ self::PURGE_TIME => $timestamp, self::PURGE_HOLDOFF => $holdoff ];
3019  }
3020 
3025  private function makeTombstonePurgeValue( float $timestamp ) {
3026  return self::PURGE_VAL_PREFIX . ':' . (int)$timestamp;
3027  }
3028 
3035  private function makeCheckPurgeValue( float $timestamp, int $holdoff, array &$purge = null ) {
3036  $normalizedTime = (int)$timestamp;
3037  // Purge array that matches what parsePurgeValue() would have returned
3038  $purge = [ self::PURGE_TIME => (float)$normalizedTime, self::PURGE_HOLDOFF => $holdoff ];
3039 
3040  return self::PURGE_VAL_PREFIX . ":$normalizedTime:$holdoff";
3041  }
3042 
3047  private function getProcessCache( $group ) {
3048  if ( !isset( $this->processCaches[$group] ) ) {
3049  list( , $size ) = explode( ':', $group );
3050  $this->processCaches[$group] = new MapCacheLRU( (int)$size );
3051  if ( $this->wallClockOverride !== null ) {
3052  $this->processCaches[$group]->setMockTime( $this->wallClockOverride );
3053  }
3054  }
3055 
3056  return $this->processCaches[$group];
3057  }
3058 
3064  private function getNonProcessCachedMultiKeys( ArrayIterator $keys, array $opts ) {
3065  $pcTTL = $opts['pcTTL'] ?? self::TTL_UNCACHEABLE;
3066 
3067  $keysMissing = [];
3068  if ( $pcTTL > 0 && $this->callbackDepth == 0 ) {
3069  $pCache = $this->getProcessCache( $opts['pcGroup'] ?? self::PC_PRIMARY );
3070  foreach ( $keys as $key => $id ) {
3071  if ( !$pCache->has( $key, $pcTTL ) ) {
3072  $keysMissing[$id] = $key;
3073  }
3074  }
3075  }
3076 
3077  return $keysMissing;
3078  }
3079 
3086  private function fetchWrappedValuesForWarmupCache( array $keys, array $checkKeys ) {
3087  if ( !$keys ) {
3088  return [];
3089  }
3090 
3091  // Get all the value keys to fetch...
3092  $sisterKeys = $this->makeSisterKeys( $keys, self::TYPE_VALUE );
3093  // Get all the "check" keys to fetch...
3094  foreach ( $checkKeys as $i => $checkKeyOrKeyGroup ) {
3095  // Note: avoid array_merge() inside loop in case there are many keys
3096  if ( is_int( $i ) ) {
3097  // Single "check" key that applies to all value keys
3098  $sisterKeys[] = $this->makeSisterKey( $checkKeyOrKeyGroup, self::TYPE_TIMESTAMP );
3099  } else {
3100  // List of "check" keys that apply to a specific value key
3101  foreach ( (array)$checkKeyOrKeyGroup as $checkKey ) {
3102  $sisterKeys[] = $this->makeSisterKey( $checkKey, self::TYPE_TIMESTAMP );
3103  }
3104  }
3105  }
3106 
3107  $wrappedBySisterKey = $this->cache->getMulti( $sisterKeys );
3108  $wrappedBySisterKey += array_fill_keys( $sisterKeys, false );
3109 
3110  return $wrappedBySisterKey;
3111  }
3112 
3118  private function timeSinceLoggedMiss( $key, $now ) {
3119  for ( end( $this->missLog ); $miss = current( $this->missLog ); prev( $this->missLog ) ) {
3120  if ( $miss[0] === $key ) {
3121  return ( $now - $miss[1] );
3122  }
3123  }
3124 
3125  return null;
3126  }
3127 
3132  protected function getCurrentTime() {
3133  if ( $this->wallClockOverride ) {
3134  return $this->wallClockOverride;
3135  }
3136 
3137  // call this first
3138  $clockTime = (float)time();
3139  // microtime() uses an initial gettimeofday() call added to usage clocks.
3140  // This can severely drift from time() and the microtime() value of other threads
3141  // due to undercounting of the amount of time elapsed. Instead of seeing the current
3142  // time as being in the past, use the value of time(). This avoids setting cache values
3143  // that will immediately be seen as expired and possibly cause stampedes.
3144  return max( microtime( true ), $clockTime );
3145  }
3146 
3151  public function setMockTime( &$time ) {
3152  $this->wallClockOverride =& $time;
3153  $this->cache->setMockTime( $time );
3154  foreach ( $this->processCaches as $pCache ) {
3155  $pCache->setMockTime( $time );
3156  }
3157  }
3158 }
A BagOStuff object with no objects in it.
Handles a simple LRU key/value map with a maximum number of entries.
Definition: MapCacheLRU.php:36
Multi-datacenter aware caching interface.
makeGlobalKey( $collection,... $components)
Make a cache key for the global keyspace and given components.
int $callbackDepth
Callback stack depth for getWithSetCallback()
const PURGE_TIME
Key to the tombstone entry timestamp.
const RES_TOUCH_AS_OF
Highest "touched" timestamp for a key.
const HOLDOFF_TTL
Seconds to tombstone keys on delete() and to treat keys as volatile after purges.
const HOT_TTR
Expected time-till-refresh, in seconds, if the key is accessed once per second.
const KEY_VERSION
Version number attribute for a key; keep value for b/c (< 1.36)
__construct(array $params)
isValid( $value, $asOf, $minAsOf)
Check that a wrapper value exists and has an acceptable age.
unwrap( $wrapped, $now)
const TYPE_TIMESTAMP
Single character component for timestamp check keys.
const RES_AS_OF
Generation completion timestamp attribute for a key.
worthRefreshPopular( $asOf, $ageNew, $timeTillRefresh, $now)
Check if a key is due for randomized regeneration due to its popularity.
fetchOrRegenerate( $key, $ttl, $callback, array $opts, array $cbParams)
Do the actual I/O for getWithSetCallback() when needed.
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...
const FLD_FORMAT_VERSION
Key to WAN cache version number; stored in blobs.
determineKeyClassForStats( $key)
const SCHEME_HASH_STOP
Use mcrouter-style Hash Stop key scheme (e.g.
const RES_VALUE
Value for a key.
const RES_VERSION
Version number attribute for a key.
prependRoute(string $sisterKey, string $route)
touchCheckKey( $key, $holdoff=self::HOLDOFF_TTL)
Increase the last-purge timestamp of a "check" key in all datacenters.
const FLD_VALUE
Key to the cached value; stored in blobs.
const PURGE_HOLDOFF
Key to the tombstone entry hold-off TTL.
adaptiveTTL( $mtime, $maxTTL, $minTTL=30, $factor=0.2)
Get a TTL that is higher for objects that have not changed recently.
const GRACE_TTL_NONE
Idiom for set()/getWithSetCallback() meaning "no post-expiration grace period".
int $warmupKeyMisses
Key fetched.
float null $wallClockOverride
relayVolatilePurges(array $purgeBySisterKey, int $ttl)
Set a sister key to a purge value in all datacenters.
mixed[] $warmupCache
Temporary warm-up cache.
const VERSION
Cache format version number.
const LOW_TTL
Consider regeneration if the key will expire within this many seconds.
BagOStuff $cache
The local datacenter cache.
fetchKeys(array $keys, array $checkKeys, $touchedCb=null)
Fetch the value and key metadata of several keys from cache.
parsePurgeValue( $value)
Extract purge metadata from cached value if it is a valid purge value.
const RES_TOMB_AS_OF
Tomstone timestamp attribute for a key.
scheduleAsyncRefresh( $key, $ttl, $callback, array $opts, array $cbParams)
Schedule a deferred cache regeneration if possible.
const RES_TTL
Logical TTL attribute for a key.
const GENERATION_HIGH_SEC
Consider value generation somewhat high if it takes this many seconds or more.
const GENERATION_SLOW_SEC
Consider value generation slow if it takes this many seconds or more.
const COOLOFF_TTL
Seconds to no-op key set() calls to avoid large blob I/O stampedes.
getWithSetCallback( $key, $ttl, $callback, array $opts=[], array $cbParams=[])
Method to fetch/regenerate a cache key.
getCheckKeyTime( $key)
Fetch the value of a timestamp "check" key.
getNonProcessCachedMultiKeys(ArrayIterator $keys, array $opts)
const SCHEME_HASH_TAG
Use twemproxy-style Hash Tag key scheme (e.g.
const RECENT_SET_HIGH_MS
Max millisecond set() backoff during hold-off (far less than INTERIM_KEY_TTL)
const LOCK_TTL
Seconds to keep lock keys around.
getMulti(array $keys, &$curTTLs=[], array $checkKeys=[], &$info=[])
Fetch the value of several keys from cache.
const PC_PRIMARY
Default process cache name and max key count.
getMultiWithUnionSetCallback(ArrayIterator $keyedIds, $ttl, callable $callback, array $opts=[])
Method to fetch/regenerate multiple cache keys at once.
LoggerInterface $logger
const TYPE_MUTEX
Single character component for mutex lock keys.
relayNonVolatilePurge(string $sisterKey)
Remove a sister key from all datacenters.
getMultiWithSetCallback(ArrayIterator $keyedIds, $ttl, callable $callback, array $opts=[])
Method to fetch multiple cache keys at once with regeneration.
timeSinceLoggedMiss( $key, $now)
isExtremelyNewValue( $res, $minAsOf, $now)
Check if a key value is non-false, new enough, and has an "as of" time almost equal to now.
getProcessCache( $group)
wrap( $value, $ttl, $version, $now, $walltime)
const PURGE_VAL_PREFIX
Value prefix of purge values.
const INTERIM_KEY_TTL
Seconds to keep interim value keys for tombstoned keys around.
makeMultiKeys(array $ids, $keyCallback)
Get an iterator of (cache key => entity ID) for a list of entity IDs.
makeTombstonePurgeValue(float $timestamp)
array< int, array > $missLog
List of (key, UNIX timestamp) tuples for get() cache misses.
static newEmpty()
Get an instance that wraps EmptyBagOStuff.
int $coalesceScheme
Scheme to use for key coalescing (Hash Tags or Hash Stops)
const FLD_TTL
Key to the original TTL; stored in blobs.
isLotteryRefreshDue( $res, $lowTTL, $ageNew, $hotTTR, $now)
Check if a key is due for randomized regeneration due to near-expiration/popularity.
watchErrors()
Get a "watch point" token that can be used to get the "last error" to occur after now.
bool $useInterimHoldOffCaching
Whether to use "interim" caching while keys are tombstoned.
worthRefreshExpiring( $curTTL, $logicalTTL, $lowTTL)
Check if a key is nearing expiration and thus due for randomized regeneration.
const FLD_FLAGS
Key to the flags bit field (reserved number)
const HOLDOFF_TTL_NONE
Idiom for delete()/touchCheckKey() meaning "no hold-off period".
const MAX_READ_LAG
Max expected seconds of combined lag from replication and "view snapshots".
const RES_CUR_TTL
Remaining TTL attribute for a key.
const FLD_TIME
Key to the cache timestamp; stored in blobs.
const CHECK_KEY_TTL
Seconds to keep dependency purge keys around.
useInterimHoldOffCaching( $enabled)
Enable or disable the use of brief caching for tombstoned keys.
StatsdDataFactoryInterface $stats
makeSisterKeys(array $baseKeys, string $type, string $route=null)
Get sister keys that should be collocated with their corresponding base cache keys.
clearProcessCache()
Clear the in-process caches; useful for testing.
const KEY_AS_OF
Generation completion timestamp attribute for a key; keep value for b/c (< 1.36)
const TYPE_COOLOFF
Single character component for cool-off bounce keys.
const FLD_GENERATION_TIME
Key to how long it took to generate the value; stored in blobs.
getLastError( $watchPoint=0)
Get the "last error" registry.
makeSisterKey(string $baseKey, string $typeChar, string $route=null)
Get a sister key that should be collocated with a base cache key.
makeKey( $collection,... $components)
Make a cache key using the "global" keyspace for the given components.
float $epoch
Unix timestamp of the oldest possible valid values.
fetchWrappedValuesForWarmupCache(array $keys, array $checkKeys)
callable null $asyncHandler
Function that takes a WAN cache callback and runs it later.
string null $broadcastRoute
Routing prefix for operations that should be broadcasted to all data centers.
resolveBusyValue( $busyValue)
reap( $key, $purgeTimestamp, &$isStale=false)
Set a key to soon expire in the local cluster if it pre-dates $purgeTimestamp.
const RECENT_SET_LOW_MS
Min millisecond set() backoff during hold-off (far less than INTERIM_KEY_TTL)
setLogger(LoggerInterface $logger)
static getCollectionFromSisterKey(string $sisterKey)
reapCheckKey( $key, $purgeTimestamp, &$isStale=false)
Set a "check" key to soon expire in the local cluster if it pre-dates $purgeTimestamp.
const TYPE_INTERIM
Single character component for interium value keys.
const PASS_BY_REF
Idiom for get()/getMulti() to return extra information by reference.
checkAndSetCooloff( $key, $kClass, $value, $elapsed, $hasLock)
Check whether set() is rate-limited to avoid concurrent I/O spikes.
float $keyHighUplinkBps
Max tolerable bytes/second to spend on a cache write stampede for a key.
getInterimValue( $key, $minAsOf, $now, $touchedCb)
const KEY_CHECK_AS_OF
Highest "check" key timestamp for a key; keep value for b/c (< 1.36)
processCheckKeys(array $checkSisterKeys, array $wrappedBySisterKey, float $now)
setInterimValue( $key, $value, $ttl, $version, $now, $walltime)
isAcceptablyFreshValue( $res, $graceTTL, $minAsOf)
Check if a key value is non-false, new enough, and either fresh or "gracefully" stale.
clearLastError()
Clear the "last error" registry.
const STALE_TTL_NONE
Idiom for set()/getWithSetCallback() meaning "no post-expiration persistence".
MapCacheLRU[] $processCaches
Map of group PHP instance caches.
const TSE_NONE
Idiom for getWithSetCallback() meaning "no cache stampede mutex".
string $secret
Stable secret used for hashing long strings into key components.
const RES_CHECK_AS_OF
Highest "check" key timestamp for a key.
const TYPE_VALUE
Single character component for value keys.
resetCheckKey( $key)
Clear the last-purge timestamp of a "check" key in all datacenters.
const KEY_TOMB_AS_OF
Tomstone timestamp attribute for a key; keep value for b/c (< 1.36)
int $keyHighQps
Reads/second assumed during a hypothetical cache write stampede for a key.
const MAX_COMMIT_DELAY
Max expected seconds to pass between delete() and DB commit finishing.
const KEY_CUR_TTL
Remaining TTL attribute for a key; keep value for b/c (< 1.36)
const AGE_NEW
Minimum key age, in seconds, for expected time-till-refresh to be considered.
yieldStampedeLock( $key, $hasLock)
const RAMPUP_TTL
Seconds to ramp up the chance of regeneration due to expected time-till-refresh.
const TTL_LAGGED
Max TTL, in seconds, to store keys when a data source has high replication lag.
const FLD_VALUE_VERSION
Key to collection cache version number; stored in blobs.
hash256( $component)
Hash a possibly long string into a suitable component for makeKey()/makeGlobalKey()
getMultiCheckKeyTime(array $keys)
Fetch the values of each timestamp "check" key.
makeCheckPurgeValue(float $timestamp, int $holdoff, array &$purge=null)
const KEY_TTL
Logical TTL attribute for a key.
Generic interface for object stores with key encoding methods.
Generic interface providing Time-To-Live constants for expirable object storage.
Generic interface providing error code and quality-of-service constants for object stores.
const ERR_UNREACHABLE
Storage medium could not be reached to establish a connection.
const ERR_UNEXPECTED
Storage medium operation failed due to usage limitations or an I/O error.
const ERR_NO_RESPONSE
Storage medium failed to yield a complete response to an operation.