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 
132 class WANObjectCache implements
136  LoggerAwareInterface
137 {
139  protected $cache;
141  protected $processCaches = [];
143  protected $logger;
145  protected $stats;
147  protected $asyncHandler;
148 
156  protected $broadcastRoute;
158  protected $onHostRoute;
160  protected $useInterimHoldOffCaching = true;
162  protected $epoch;
164  protected $secret;
166  protected $coalesceScheme;
167 
169  private $keyHighQps;
172 
174  private $missLog;
175 
177  private $callbackDepth = 0;
179  private $warmupCache = [];
181  private $warmupKeyMisses = 0;
182 
185 
187  private const MAX_COMMIT_DELAY = 3;
189  private const MAX_READ_LAG = 7;
191  public const HOLDOFF_TTL = self::MAX_COMMIT_DELAY + self::MAX_READ_LAG + 1;
192 
194  private const LOW_TTL = 30;
196  public const TTL_LAGGED = 30;
197 
199  private const HOT_TTR = 900;
201  private const AGE_NEW = 60;
202 
204  private const TSE_NONE = -1;
205 
207  public const STALE_TTL_NONE = 0;
209  public const GRACE_TTL_NONE = 0;
211  public const HOLDOFF_TTL_NONE = 0;
212 
214  public const MIN_TIMESTAMP_NONE = 0.0;
215 
217  private const PC_PRIMARY = 'primary:1000';
218 
220  public const PASS_BY_REF = [];
221 
223  private const SCHEME_HASH_TAG = 1;
225  private const SCHEME_HASH_STOP = 2;
226 
228  private const CHECK_KEY_TTL = self::TTL_YEAR;
230  private const INTERIM_KEY_TTL = 1;
231 
233  private const LOCK_TTL = 10;
235  private const COOLOFF_TTL = 1;
237  private const RAMPUP_TTL = 30;
238 
240  private const TINY_NEGATIVE = -0.000001;
242  private const TINY_POSTIVE = 0.000001;
243 
245  private const RECENT_SET_LOW_MS = 50;
247  private const RECENT_SET_HIGH_MS = 100;
248 
250  private const GENERATION_HIGH_SEC = 0.2;
252  private const GENERATION_SLOW_SEC = 3.0;
253 
255  private const PURGE_TIME = 0;
257  private const PURGE_HOLDOFF = 1;
258 
260  private const VERSION = 1;
261 
263  public const KEY_VERSION = 'version';
265  public const KEY_AS_OF = 'asOf';
267  public const KEY_TTL = 'ttl';
269  public const KEY_CUR_TTL = 'curTTL';
271  public const KEY_TOMB_AS_OF = 'tombAsOf';
273  public const KEY_CHECK_AS_OF = 'lastCKPurge';
274 
276  private const RES_VALUE = 0;
278  private const RES_VERSION = 1;
280  private const RES_AS_OF = 2;
282  private const RES_TTL = 3;
284  private const RES_TOMB_AS_OF = 4;
286  private const RES_CHECK_AS_OF = 5;
288  private const RES_TOUCH_AS_OF = 6;
290  private const RES_CUR_TTL = 7;
291 
293  private const FLD_FORMAT_VERSION = 0;
295  private const FLD_VALUE = 1;
297  private const FLD_TTL = 2;
299  private const FLD_TIME = 3;
301  private const FLD_FLAGS = 4;
303  private const FLD_VALUE_VERSION = 5;
305  private const FLD_GENERATION_TIME = 6;
306 
308  private const TYPE_VALUE = 'v';
310  private const TYPE_TIMESTAMP = 't';
312  private const TYPE_FLUX = 'f';
314  private const TYPE_MUTEX = 'm';
316  private const TYPE_INTERIM = 'i';
318  private const TYPE_COOLOFF = 'c';
319 
321  private const PURGE_VAL_PREFIX = 'PURGED';
322 
364  public function __construct( array $params ) {
365  $this->cache = $params['cache'];
366  $this->broadcastRoute = $params['broadcastRoutingPrefix'] ?? null;
367  $this->onHostRoute = $params['onHostRoutingPrefix'] ?? null;
368  $this->epoch = $params['epoch'] ?? 0;
369  $this->secret = $params['secret'] ?? (string)$this->epoch;
370  if ( ( $params['coalesceScheme'] ?? '' ) === 'hash_tag' ) {
371  // https://redis.io/topics/cluster-spec
372  // https://github.com/twitter/twemproxy/blob/v0.4.1/notes/recommendation.md#hash-tags
373  // https://github.com/Netflix/dynomite/blob/v0.7.0/notes/recommendation.md#hash-tags
374  $this->coalesceScheme = self::SCHEME_HASH_TAG;
375  } else {
376  // https://github.com/facebook/mcrouter/wiki/Key-syntax
377  $this->coalesceScheme = self::SCHEME_HASH_STOP;
378  }
379 
380  $this->keyHighQps = $params['keyHighQps'] ?? 100;
381  $this->keyHighUplinkBps = $params['keyHighUplinkBps'] ?? ( 1e9 / 8 / 100 );
382 
383  $this->setLogger( $params['logger'] ?? new NullLogger() );
384  $this->stats = $params['stats'] ?? new NullStatsdDataFactory();
385  $this->asyncHandler = $params['asyncHandler'] ?? null;
386 
387  $this->missLog = array_fill( 0, 10, [ '', 0.0 ] );
388 
389  $this->cache->registerWrapperInfoForStats(
390  'WANCache',
391  'wanobjectcache',
392  [ __CLASS__, 'getCollectionFromSisterKey' ]
393  );
394  }
395 
399  public function setLogger( LoggerInterface $logger ) {
400  $this->logger = $logger;
401  }
402 
408  public static function newEmpty() {
409  return new static( [ 'cache' => new EmptyBagOStuff() ] );
410  }
411 
467  final public function get( $key, &$curTTL = null, array $checkKeys = [], &$info = [] ) {
468  // Note that an undeclared variable passed as $info starts as null (not the default).
469  // Also, if no $info parameter is provided, then it doesn't matter how it changes here.
470  $legacyInfo = ( $info !== self::PASS_BY_REF );
471 
472  $res = $this->fetchKeys( [ $key ], $checkKeys )[$key];
473 
474  $curTTL = $res[self::RES_CUR_TTL];
475  $info = $legacyInfo
477  : [
478  self::KEY_VERSION => $res[self::RES_VERSION],
479  self::KEY_AS_OF => $res[self::RES_AS_OF],
480  self::KEY_TTL => $res[self::RES_TTL],
481  self::KEY_CUR_TTL => $res[self::RES_CUR_TTL],
482  self::KEY_TOMB_AS_OF => $res[self::RES_TOMB_AS_OF],
483  self::KEY_CHECK_AS_OF => $res[self::RES_CHECK_AS_OF]
484  ];
485 
486  if ( $curTTL === null || $curTTL <= 0 ) {
487  // Log the timestamp in case a corresponding set() call does not provide "walltime"
488  reset( $this->missLog );
489  unset( $this->missLog[key( $this->missLog )] );
490  $this->missLog[] = [ $key, $this->getCurrentTime() ];
491  }
492 
493  return $res[self::RES_VALUE];
494  }
495 
520  final public function getMulti(
521  array $keys,
522  &$curTTLs = [],
523  array $checkKeys = [],
524  &$info = []
525  ) {
526  // Note that an undeclared variable passed as $info starts as null (not the default).
527  // Also, if no $info parameter is provided, then it doesn't matter how it changes here.
528  $legacyInfo = ( $info !== self::PASS_BY_REF );
529 
530  $curTTLs = [];
531  $info = [];
532  $valuesByKey = [];
533 
534  $resByKey = $this->fetchKeys( $keys, $checkKeys );
535  foreach ( $resByKey as $key => $res ) {
536  if ( $res[self::RES_VALUE] !== false ) {
537  $valuesByKey[$key] = $res[self::RES_VALUE];
538  }
539 
540  if ( $res[self::RES_CUR_TTL] !== null ) {
541  $curTTLs[$key] = $res[self::RES_CUR_TTL];
542  }
543  $info[$key] = $legacyInfo
545  : [
546  self::KEY_VERSION => $res[self::RES_VERSION],
547  self::KEY_AS_OF => $res[self::RES_AS_OF],
548  self::KEY_TTL => $res[self::RES_TTL],
549  self::KEY_CUR_TTL => $res[self::RES_CUR_TTL],
550  self::KEY_TOMB_AS_OF => $res[self::RES_TOMB_AS_OF],
551  self::KEY_CHECK_AS_OF => $res[self::RES_CHECK_AS_OF]
552  ];
553  }
554 
555  return $valuesByKey;
556  }
557 
572  protected function fetchKeys( array $keys, array $checkKeys, $touchedCb = null ) {
573  $resByKey = [];
574 
575  // List of all sister keys that need to be fetched from cache
576  $allSisterKeys = [];
577  // Order-corresponding value sister key list for the base key list ($keys)
578  $valueSisterKeys = [];
579  // Order-corresponding "flux" sister key list for the base key list ($keys) or []
580  $fluxSisterKeys = [];
581  // List of "check" sister keys to compare all value sister keys against
582  $checkSisterKeysForAll = [];
583  // Map of (base key => additional "check" sister key(s) to compare against)
584  $checkSisterKeysByKey = [];
585 
586  foreach ( $keys as $key ) {
587  $sisterKey = $this->makeSisterKey( $key, self::TYPE_VALUE, $this->onHostRoute );
588  $allSisterKeys[] = $sisterKey;
589  $valueSisterKeys[] = $sisterKey;
590  if ( $this->onHostRoute !== null ) {
591  $sisterKey = $this->makeSisterKey( $key, self::TYPE_FLUX );
592  $allSisterKeys[] = $sisterKey;
593  $fluxSisterKeys[] = $sisterKey;
594  }
595  }
596 
597  foreach ( $checkKeys as $i => $checkKeyOrKeyGroup ) {
598  // Note: avoid array_merge() inside loop in case there are many keys
599  if ( is_int( $i ) ) {
600  // Single "check" key that applies to all base keys
601  $sisterKey = $this->makeSisterKey( $checkKeyOrKeyGroup, self::TYPE_TIMESTAMP );
602  $allSisterKeys[] = $sisterKey;
603  $checkSisterKeysForAll[] = $sisterKey;
604  } else {
605  // List of "check" keys that apply to a specific base key
606  foreach ( (array)$checkKeyOrKeyGroup as $checkKey ) {
607  $sisterKey = $this->makeSisterKey( $checkKey, self::TYPE_TIMESTAMP );
608  $allSisterKeys[] = $sisterKey;
609  $checkSisterKeysByKey[$i][] = $sisterKey;
610  }
611  }
612  }
613 
614  if ( $this->warmupCache ) {
615  // Get the wrapped values of the sister keys from the warmup cache
616  $wrappedBySisterKey = $this->warmupCache;
617  $sisterKeysMissing = array_diff( $allSisterKeys, array_keys( $wrappedBySisterKey ) );
618  if ( $sisterKeysMissing ) { // sanity
619  $this->warmupKeyMisses += count( $sisterKeysMissing );
620  $wrappedBySisterKey += $this->cache->getMulti( $sisterKeysMissing );
621  }
622  } else {
623  // Fetch the wrapped values of the sister keys from the backend
624  $wrappedBySisterKey = $this->cache->getMulti( $allSisterKeys );
625  }
626 
627  // Pessimistically treat the "current time" as the time when any network I/O finished
628  $now = $this->getCurrentTime();
629 
630  // List of "check" sister key purge timestamps to compare all value sister keys against
631  $ckPurgesForAll = $this->processCheckKeys(
632  $checkSisterKeysForAll,
633  $wrappedBySisterKey,
634  $now
635  );
636  // Map of (base key => extra "check" sister key purge timestamp(s) to compare against)
637  $ckPurgesByKey = [];
638  foreach ( $checkSisterKeysByKey as $keyWithCheckKeys => $checkKeysForKey ) {
639  $ckPurgesByKey[$keyWithCheckKeys] = $this->processCheckKeys(
640  $checkKeysForKey,
641  $wrappedBySisterKey,
642  $now
643  );
644  }
645 
646  // Map of (base key => "flux" key purge timestamp to compare against)
647  $fkPurgesByKey = $this->processFluxKeys( $keys, $fluxSisterKeys, $wrappedBySisterKey );
648 
649  // Unwrap and validate any value found for each base key (under the value sister key)
650  reset( $keys );
651  foreach ( $valueSisterKeys as $valueSisterKey ) {
652  // Get the corresponding base key for this value sister key
653  $key = current( $keys );
654  next( $keys );
655 
656  if ( isset( $fkPurgesByKey[$key] ) ) {
657  // An on-host tier is in use and a "flux" sister key exists for this
658  // Treat the value sister key as if it was a tombstone with this value.
659  $wrapped = $fkPurgesByKey[$key];
660  } elseif ( array_key_exists( $valueSisterKey, $wrappedBySisterKey ) ) {
661  // Key exists as either a live value or tombstone value
662  $wrapped = $wrappedBySisterKey[$valueSisterKey];
663  } else {
664  // Key does not exist
665  $wrapped = false;
666  }
667 
668  $res = $this->unwrap( $wrapped, $now );
669  $value = $res[self::RES_VALUE];
670 
671  foreach ( array_merge( $ckPurgesForAll, $ckPurgesByKey[$key] ?? [] ) as $ckPurge ) {
673  $ckPurge[self::PURGE_TIME],
674  $res[self::RES_CHECK_AS_OF]
675  );
676  // Timestamp marking the end of the hold-off period for this purge
677  $holdoffDeadline = $ckPurge[self::PURGE_TIME] + $ckPurge[self::PURGE_HOLDOFF];
678  // Check if the value was generated during the hold-off period
679  if ( $value !== false && $holdoffDeadline >= $res[self::RES_AS_OF] ) {
680  // How long ago this value was purged by *this* "check" key
681  $ago = min( $ckPurge[self::PURGE_TIME] - $now, self::TINY_NEGATIVE );
682  // How long ago this value was purged by *any* known "check" key
683  $res[self::RES_CUR_TTL] = min( $res[self::RES_CUR_TTL], $ago );
684  }
685  }
686 
687  if ( $touchedCb !== null && $value !== false ) {
688  $touched = $touchedCb( $value );
689  if ( $touched !== null && $touched >= $res[self::RES_AS_OF] ) {
690  $res[self::RES_CUR_TTL] = min(
691  $res[self::RES_CUR_TTL],
692  $res[self::RES_AS_OF] - $touched,
693  self::TINY_NEGATIVE
694  );
695  }
696  } else {
697  $touched = null;
698  }
699 
700  $res[self::RES_TOUCH_AS_OF] = max( $res[self::RES_TOUCH_AS_OF], $touched );
701 
702  $resByKey[$key] = $res;
703  }
704 
705  return $resByKey;
706  }
707 
714  private function processFluxKeys(
715  array $keys,
716  array $fluxSisterKeys,
717  array $wrappedBySisterKey
718  ) {
719  $purges = [];
720 
721  reset( $keys );
722  foreach ( $fluxSisterKeys as $fluxKey ) {
723  // Get the corresponding base key for this "flux" key
724  $key = current( $keys );
725  next( $keys );
726 
727  $purge = isset( $wrappedBySisterKey[$fluxKey] )
728  ? $this->parsePurgeValue( $wrappedBySisterKey[$fluxKey] )
729  : null;
730 
731  if ( $purge !== null ) {
732  $purges[$key] = $purge;
733  }
734  }
735 
736  return $purges;
737  }
738 
745  private function processCheckKeys(
746  array $checkSisterKeys,
747  array $wrappedBySisterKey,
748  float $now
749  ) {
750  $purges = [];
751 
752  foreach ( $checkSisterKeys as $timeKey ) {
753  $purge = isset( $wrappedBySisterKey[$timeKey] )
754  ? $this->parsePurgeValue( $wrappedBySisterKey[$timeKey] )
755  : null;
756 
757  if ( $purge === null ) {
758  $wrapped = $this->makeCheckPurgeValue( $now, self::HOLDOFF_TTL, $purge );
759  $this->cache->add( $timeKey, $wrapped, self::CHECK_KEY_TTL );
760  }
761 
762  $purges[] = $purge;
763  }
764 
765  return $purges;
766  }
767 
848  final public function set( $key, $value, $ttl = self::TTL_INDEFINITE, array $opts = [] ) {
849  $now = $this->getCurrentTime();
850  $dataReplicaLag = $opts['lag'] ?? 0;
851  $dataSnapshotLag = isset( $opts['since'] ) ? max( 0, $now - $opts['since'] ) : 0;
852  $dataCombinedLag = $dataReplicaLag + $dataSnapshotLag;
853  $dataPendingCommit = $opts['pending'] ?? null;
854  $lockTSE = $opts['lockTSE'] ?? self::TSE_NONE;
855  $staleTTL = $opts['staleTTL'] ?? self::STALE_TTL_NONE;
856  $creating = $opts['creating'] ?? false;
857  $version = $opts['version'] ?? null;
858  $walltime = $opts['walltime'] ?? $this->timeSinceLoggedMiss( $key, $now );
859 
860  if ( $ttl < 0 ) {
861  return true; // not cacheable
862  }
863 
864  // Forbid caching data that only exists within an uncommitted transaction. Also, lower
865  // the TTL when the data has a "since" time so far in the past that a delete() tombstone,
866  // made after that time, could have already expired (the key is no longer write-holed).
867  // The mitigation TTL depends on whether this data lag is assumed to systemically effect
868  // regeneration attempts in the near future. The TTL also reflects regeneration wall time.
869  if ( $dataPendingCommit ) {
870  // Case A: data comes from an uncommitted write transaction
871  $mitigated = 'pending writes';
872  // Data might never be committed; rely on a less problematic regeneration attempt
873  $mitigationTTL = self::TTL_UNCACHEABLE;
874  } elseif ( $dataSnapshotLag > self::MAX_READ_LAG ) {
875  // Case B: high snapshot lag
876  $pregenSnapshotLag = ( $walltime !== null ) ? ( $dataSnapshotLag - $walltime ) : 0;
877  if ( ( $pregenSnapshotLag + self::GENERATION_HIGH_SEC ) > self::MAX_READ_LAG ) {
878  // Case B1: generation started when transaction duration was already long
879  $mitigated = 'snapshot lag (late generation)';
880  // Probably non-systemic; rely on a less problematic regeneration attempt
881  $mitigationTTL = self::TTL_UNCACHEABLE;
882  } else {
883  // Case B2: slow generation made transaction duration long
884  $mitigated = 'snapshot lag (high generation time)';
885  // Probably systemic; use a low TTL to avoid stampedes/uncacheability
886  $mitigationTTL = self::LOW_TTL;
887  }
888  } elseif ( $dataReplicaLag === false || $dataReplicaLag > self::MAX_READ_LAG ) {
889  // Case C: low/medium snapshot lag with high replication lag
890  $mitigated = 'replication lag';
891  // Probably systemic; use a low TTL to avoid stampedes/uncacheability
892  $mitigationTTL = self::TTL_LAGGED;
893  } elseif ( $dataCombinedLag > self::MAX_READ_LAG ) {
894  $pregenCombinedLag = ( $walltime !== null ) ? ( $dataCombinedLag - $walltime ) : 0;
895  // Case D: medium snapshot lag with medium replication lag
896  if ( ( $pregenCombinedLag + self::GENERATION_HIGH_SEC ) > self::MAX_READ_LAG ) {
897  // Case D1: generation started when read lag was too high
898  $mitigated = 'read lag (late generation)';
899  // Probably non-systemic; rely on a less problematic regeneration attempt
900  $mitigationTTL = self::TTL_UNCACHEABLE;
901  } else {
902  // Case D2: slow generation made read lag too high
903  $mitigated = 'read lag (high generation time)';
904  // Probably systemic; use a low TTL to avoid stampedes/uncacheability
905  $mitigationTTL = self::LOW_TTL;
906  }
907  } else {
908  // Case E: new value generated with recent data
909  $mitigated = null;
910  // Nothing to mitigate
911  $mitigationTTL = null;
912  }
913 
914  if ( $mitigationTTL === self::TTL_UNCACHEABLE ) {
915  $this->logger->warning(
916  "Rejected set() for {cachekey} due to $mitigated.",
917  [
918  'cachekey' => $key,
919  'lag' => $dataReplicaLag,
920  'age' => $dataSnapshotLag,
921  'walltime' => $walltime
922  ]
923  );
924 
925  return true; // no-op the write for being unsafe
926  }
927 
928  // TTL to use in staleness checks (does not effect persistence layer TTL)
929  $logicalTTL = null;
930 
931  if ( $mitigationTTL !== null ) {
932  // New value was generated from data that is old enough to be risky
933  if ( $lockTSE >= 0 ) {
934  // Persist the value as long as normal, but make it count as stale sooner
935  $logicalTTL = min( $ttl ?: INF, $mitigationTTL );
936  } else {
937  // Persist the value for a shorter duration
938  $ttl = min( $ttl ?: INF, $mitigationTTL );
939  }
940 
941  $this->logger->warning(
942  "Lowered set() TTL for {cachekey} due to $mitigated.",
943  [
944  'cachekey' => $key,
945  'lag' => $dataReplicaLag,
946  'age' => $dataSnapshotLag,
947  'walltime' => $walltime
948  ]
949  );
950  }
951 
952  // Wrap that value with time/TTL/version metadata
953  $wrapped = $this->wrap( $value, $logicalTTL ?: $ttl, $version, $now, $walltime );
954  $storeTTL = $ttl + $staleTTL;
955 
956  if ( $creating ) {
957  $ok = $this->cache->add(
958  $this->makeSisterKey( $key, self::TYPE_VALUE ),
959  $wrapped,
960  $storeTTL
961  );
962  } else {
963  $ok = $this->cache->merge(
964  $this->makeSisterKey( $key, self::TYPE_VALUE ),
965  static function ( $cache, $key, $cWrapped ) use ( $wrapped ) {
966  // A string value means that it is a tombstone; do nothing in that case
967  return ( is_string( $cWrapped ) ) ? false : $wrapped;
968  },
969  $storeTTL,
970  1 // 1 attempt
971  );
972  }
973 
974  return $ok;
975  }
976 
1039  final public function delete( $key, $ttl = self::HOLDOFF_TTL ) {
1040  // Purge values must be stored under the value key so that WANObjectCache::set()
1041  // can atomically merge values without accidentally undoing a recent purge and thus
1042  // violating the holdoff TTL restriction.
1043  $valueSisterKey = $this->makeSisterKey( $key, self::TYPE_VALUE );
1044 
1045  // When an on-host tier is configured, fetchKeys() relies on "flux" keys to determine
1046  // whether a value from the on-host tier is still valid. A "flux" key is a short-lived
1047  // key that contains the last recent purge due to delete(). This approach avoids having
1048  // to purge the on-host cache service on potentially hundreds of application servers.
1049 
1050  if ( $ttl <= 0 ) {
1051  // A client or cache cleanup script is requesting a cache purge, so there is no
1052  // volatility period due to replica DB lag. Any recent change to an entity cached
1053  // in this key should have triggered an appropriate purge event.
1054 
1055  // Remove the key from all datacenters, ignoring any on-host tier. Since on-host
1056  // tier caches only use low key TTLs, setting "flux" keys here has little practical
1057  // benefit; missed purges should be rare and the on-host tier will quickly correct
1058  // itself in those rare cases.
1059  $ok = $this->relayNonVolatilePurge( $valueSisterKey );
1060  } else {
1061  // A cacheable entity recently changed, so there might be a volatility period due
1062  // to replica DB lag. Clients usually expect their actions to be reflected in any
1063  // of their subsequent web request. This is attainable if (a) purge relay lag is
1064  // lower than the time it takes for subsequent request by the client to arrive,
1065  // and, (b) DB replica queries have "read-your-writes" consistency due to DB lag
1066  // mitigation systems.
1067 
1068  $now = $this->getCurrentTime();
1069  // Set the key to the purge value in all datacenters
1070  $purgeBySisterKey = [ $valueSisterKey => $this->makeTombstonePurgeValue( $now ) ];
1071  // When an on-host tier is configured, purge it by setting "flux" keys
1072  if ( $this->onHostRoute !== null ) {
1073  $fluxSisterKey = $this->makeSisterKey( $key, self::TYPE_FLUX );
1074  $purgeBySisterKey[$fluxSisterKey] = $this->makeTombstonePurgeValue( $now );
1075  }
1076 
1077  $ok = $this->relayVolatilePurges( $purgeBySisterKey, $ttl );
1078  }
1079 
1080  $kClass = $this->determineKeyClassForStats( $key );
1081  $this->stats->increment( "wanobjectcache.$kClass.delete." . ( $ok ? 'ok' : 'error' ) );
1082 
1083  return $ok;
1084  }
1085 
1105  final public function getCheckKeyTime( $key ) {
1106  return $this->getMultiCheckKeyTime( [ $key ] )[$key];
1107  }
1108 
1170  final public function getMultiCheckKeyTime( array $keys ) {
1171  $checkSisterKeysByKey = [];
1172  foreach ( $keys as $key ) {
1173  $checkSisterKeysByKey[$key] = $this->makeSisterKey( $key, self::TYPE_TIMESTAMP );
1174  }
1175 
1176  $wrappedBySisterKey = $this->cache->getMulti( $checkSisterKeysByKey );
1177  $wrappedBySisterKey += array_fill_keys( $checkSisterKeysByKey, false );
1178 
1179  $now = $this->getCurrentTime();
1180  $times = [];
1181  foreach ( $checkSisterKeysByKey as $key => $checkSisterKey ) {
1182  $purge = $this->parsePurgeValue( $wrappedBySisterKey[$checkSisterKey] );
1183  if ( $purge === null ) {
1184  $wrapped = $this->makeCheckPurgeValue( $now, self::HOLDOFF_TTL, $purge );
1185  $this->cache->add( $checkSisterKey, $wrapped, self::CHECK_KEY_TTL );
1186  }
1187 
1188  $times[$key] = $purge[self::PURGE_TIME];
1189  }
1190 
1191  return $times;
1192  }
1193 
1227  final public function touchCheckKey( $key, $holdoff = self::HOLDOFF_TTL ) {
1228  $checkSisterKey = $this->makeSisterKey( $key, self::TYPE_TIMESTAMP );
1229 
1230  $now = $this->getCurrentTime();
1231  $purgeBySisterKey = [ $checkSisterKey => $this->makeCheckPurgeValue( $now, $holdoff ) ];
1232  $ok = $this->relayVolatilePurges( $purgeBySisterKey, self::CHECK_KEY_TTL );
1233 
1234  $kClass = $this->determineKeyClassForStats( $key );
1235  $this->stats->increment( "wanobjectcache.$kClass.ck_touch." . ( $ok ? 'ok' : 'error' ) );
1236 
1237  return $ok;
1238  }
1239 
1267  final public function resetCheckKey( $key ) {
1268  $checkSisterKey = $this->makeSisterKey( $key, self::TYPE_TIMESTAMP );
1269  $ok = $this->relayNonVolatilePurge( $checkSisterKey );
1270 
1271  $kClass = $this->determineKeyClassForStats( $key );
1272  $this->stats->increment( "wanobjectcache.$kClass.ck_reset." . ( $ok ? 'ok' : 'error' ) );
1273 
1274  return $ok;
1275  }
1276 
1580  final public function getWithSetCallback(
1581  $key, $ttl, $callback, array $opts = [], array $cbParams = []
1582  ) {
1583  $version = $opts['version'] ?? null;
1584  $pcTTL = $opts['pcTTL'] ?? self::TTL_UNCACHEABLE;
1585  $pCache = ( $pcTTL >= 0 )
1586  ? $this->getProcessCache( $opts['pcGroup'] ?? self::PC_PRIMARY )
1587  : null;
1588 
1589  // Use the process cache if requested as long as no outer cache callback is running.
1590  // Nested callback process cache use is not lag-safe with regard to HOLDOFF_TTL since
1591  // process cached values are more lagged than persistent ones as they are not purged.
1592  if ( $pCache && $this->callbackDepth == 0 ) {
1593  $cached = $pCache->get( $key, $pcTTL, false );
1594  if ( $cached !== false ) {
1595  $this->logger->debug( "getWithSetCallback($key): process cache hit" );
1596  return $cached;
1597  }
1598  }
1599 
1600  $res = $this->fetchOrRegenerate( $key, $ttl, $callback, $opts, $cbParams );
1601  list( $value, $valueVersion, $curAsOf ) = $res;
1602  if ( $valueVersion !== $version ) {
1603  // Current value has a different version; use the variant key for this version.
1604  // Regenerate the variant value if it is not newer than the main value at $key
1605  // so that purges to the main key propagate to the variant value.
1606  $this->logger->debug( "getWithSetCallback($key): using variant key" );
1607  list( $value ) = $this->fetchOrRegenerate(
1608  $this->makeGlobalKey( 'WANCache-key-variant', md5( $key ), $version ),
1609  $ttl,
1610  $callback,
1611  [ 'version' => null, 'minAsOf' => $curAsOf ] + $opts,
1612  $cbParams
1613  );
1614  }
1615 
1616  // Update the process cache if enabled
1617  if ( $pCache && $value !== false ) {
1618  $pCache->set( $key, $value );
1619  }
1620 
1621  return $value;
1622  }
1623 
1640  private function fetchOrRegenerate( $key, $ttl, $callback, array $opts, array $cbParams ) {
1641  $checkKeys = $opts['checkKeys'] ?? [];
1642  $graceTTL = $opts['graceTTL'] ?? self::GRACE_TTL_NONE;
1643  $minAsOf = $opts['minAsOf'] ?? self::MIN_TIMESTAMP_NONE;
1644  $hotTTR = $opts['hotTTR'] ?? self::HOT_TTR;
1645  $lowTTL = $opts['lowTTL'] ?? min( self::LOW_TTL, $ttl );
1646  $ageNew = $opts['ageNew'] ?? self::AGE_NEW;
1647  $touchedCb = $opts['touchedCallback'] ?? null;
1648  $startTime = $this->getCurrentTime();
1649 
1650  $kClass = $this->determineKeyClassForStats( $key );
1651 
1652  // Get the current key value and its metadata
1653  $curState = $this->fetchKeys( [ $key ], $checkKeys, $touchedCb )[$key];
1654  $curValue = $curState[self::RES_VALUE];
1655  // Use the cached value if it exists and is not due for synchronous regeneration
1656  if ( $this->isAcceptablyFreshValue( $curState, $graceTTL, $minAsOf ) ) {
1657  if ( !$this->isLotteryRefreshDue( $curState, $lowTTL, $ageNew, $hotTTR, $startTime ) ) {
1658  $this->stats->timing(
1659  "wanobjectcache.$kClass.hit.good",
1660  1e3 * ( $this->getCurrentTime() - $startTime )
1661  );
1662 
1663  return [ $curValue, $curState[self::RES_VERSION], $curState[self::RES_AS_OF] ];
1664  } elseif ( $this->scheduleAsyncRefresh( $key, $ttl, $callback, $opts, $cbParams ) ) {
1665  $this->logger->debug( "fetchOrRegenerate($key): hit with async refresh" );
1666  $this->stats->timing(
1667  "wanobjectcache.$kClass.hit.refresh",
1668  1e3 * ( $this->getCurrentTime() - $startTime )
1669  );
1670 
1671  return [ $curValue, $curState[self::RES_VERSION], $curState[self::RES_AS_OF] ];
1672  } else {
1673  $this->logger->debug( "fetchOrRegenerate($key): hit with sync refresh" );
1674  }
1675  }
1676 
1677  $isKeyTombstoned = ( $curState[self::RES_TOMB_AS_OF] !== null );
1678  // Use the interim key as an temporary alternative if the key is tombstoned
1679  if ( $isKeyTombstoned ) {
1680  $volState = $this->getInterimValue( $key, $minAsOf, $startTime, $touchedCb );
1681  $volValue = $volState[self::RES_VALUE];
1682  } else {
1683  $volState = $curState;
1684  $volValue = $curValue;
1685  }
1686 
1687  // During the volatile "hold-off" period that follows a purge of the key, the value
1688  // will be regenerated many times if frequently accessed. This is done to mitigate
1689  // the effects of backend replication lag as soon as possible. However, throttle the
1690  // overhead of locking and regeneration by reusing values recently written to cache
1691  // tens of milliseconds ago. Verify the "as of" time against the last purge event.
1692  $lastPurgeTime = max(
1693  // RES_TOUCH_AS_OF depends on the value (possibly from the interim key)
1694  $volState[self::RES_TOUCH_AS_OF],
1695  $curState[self::RES_TOMB_AS_OF],
1696  $curState[self::RES_CHECK_AS_OF]
1697  );
1698  $safeMinAsOf = max( $minAsOf, $lastPurgeTime + self::TINY_POSTIVE );
1699  if ( $this->isExtremelyNewValue( $volState, $safeMinAsOf, $startTime ) ) {
1700  $this->logger->debug( "fetchOrRegenerate($key): volatile hit" );
1701  $this->stats->timing(
1702  "wanobjectcache.$kClass.hit.volatile",
1703  1e3 * ( $this->getCurrentTime() - $startTime )
1704  );
1705 
1706  return [ $volValue, $volState[self::RES_VERSION], $curState[self::RES_AS_OF] ];
1707  }
1708 
1709  $lockTSE = $opts['lockTSE'] ?? self::TSE_NONE;
1710  $busyValue = $opts['busyValue'] ?? null;
1711  $staleTTL = $opts['staleTTL'] ?? self::STALE_TTL_NONE;
1712  $version = $opts['version'] ?? null;
1713 
1714  // Determine whether one thread per datacenter should handle regeneration at a time
1715  $useRegenerationLock =
1716  // Note that since tombstones no-op set(), $lockTSE and $curTTL cannot be used to
1717  // deduce the key hotness because |$curTTL| will always keep increasing until the
1718  // tombstone expires or is overwritten by a new tombstone. Also, even if $lockTSE
1719  // is not set, constant regeneration of a key for the tombstone lifetime might be
1720  // very expensive. Assume tombstoned keys are possibly hot in order to reduce
1721  // the risk of high regeneration load after the delete() method is called.
1722  $isKeyTombstoned ||
1723  // Assume a key is hot if requested soon ($lockTSE seconds) after purge.
1724  // This avoids stampedes when timestamps from $checkKeys/$touchedCb bump.
1725  (
1726  $curState[self::RES_CUR_TTL] !== null &&
1727  $curState[self::RES_CUR_TTL] <= 0 &&
1728  abs( $curState[self::RES_CUR_TTL] ) <= $lockTSE
1729  ) ||
1730  // Assume a key is hot if there is no value and a busy fallback is given.
1731  // This avoids stampedes on eviction or preemptive regeneration taking too long.
1732  ( $busyValue !== null && $volValue === false );
1733 
1734  // If a regeneration lock is required, threads that do not get the lock will try to use
1735  // the stale value, the interim value, or the $busyValue placeholder, in that order. If
1736  // none of those are set then all threads will bypass the lock and regenerate the value.
1737  $hasLock = $useRegenerationLock && $this->claimStampedeLock( $key );
1738  if ( $useRegenerationLock && !$hasLock ) {
1739  // Determine if there is stale or volatile cached value that is still usable
1740  if ( $this->isValid( $volValue, $volState[self::RES_AS_OF], $minAsOf ) ) {
1741  $this->logger->debug( "fetchOrRegenerate($key): returning stale value" );
1742  $this->stats->timing(
1743  "wanobjectcache.$kClass.hit.stale",
1744  1e3 * ( $this->getCurrentTime() - $startTime )
1745  );
1746 
1747  return [ $volValue, $volState[self::RES_VERSION], $curState[self::RES_AS_OF] ];
1748  } elseif ( $busyValue !== null ) {
1749  $miss = is_infinite( $minAsOf ) ? 'renew' : 'miss';
1750  $this->logger->debug( "fetchOrRegenerate($key): busy $miss" );
1751  $this->stats->timing(
1752  "wanobjectcache.$kClass.$miss.busy",
1753  1e3 * ( $this->getCurrentTime() - $startTime )
1754  );
1755  $placeholderValue = $this->resolveBusyValue( $busyValue );
1756 
1757  return [ $placeholderValue, $version, $curState[self::RES_AS_OF] ];
1758  }
1759  }
1760 
1761  // Generate the new value given any prior value with a matching version
1762  $setOpts = [];
1763  $preCallbackTime = $this->getCurrentTime();
1765  try {
1766  $value = $callback(
1767  ( $curState[self::RES_VERSION] === $version ) ? $curValue : false,
1768  $ttl,
1769  $setOpts,
1770  ( $curState[self::RES_VERSION] === $version ) ? $curState[self::RES_AS_OF] : null,
1771  $cbParams
1772  );
1773  } finally {
1775  }
1776  $postCallbackTime = $this->getCurrentTime();
1777 
1778  // How long it took to fetch, validate, and generate the value
1779  $elapsed = max( $postCallbackTime - $startTime, 0.0 );
1780 
1781  // How long it took to generate the value
1782  $walltime = max( $postCallbackTime - $preCallbackTime, 0.0 );
1783  $this->stats->timing( "wanobjectcache.$kClass.regen_walltime", 1e3 * $walltime );
1784 
1785  // Attempt to save the newly generated value if applicable
1786  if (
1787  // Callback yielded a cacheable value
1788  ( $value !== false && $ttl >= 0 ) &&
1789  // Current thread was not raced out of a regeneration lock or key is tombstoned
1790  ( !$useRegenerationLock || $hasLock || $isKeyTombstoned ) &&
1791  // Key does not appear to be undergoing a set() stampede
1792  $this->checkAndSetCooloff( $key, $kClass, $value, $elapsed, $hasLock )
1793  ) {
1794  // If the key is write-holed then use the (volatile) interim key as an alternative
1795  if ( $isKeyTombstoned ) {
1796  $this->setInterimValue( $key, $value, $lockTSE, $version, $postCallbackTime, $walltime );
1797  } else {
1798  $finalSetOpts = [
1799  // @phan-suppress-next-line PhanUselessBinaryAddRight,PhanCoalescingAlwaysNull
1800  'since' => $setOpts['since'] ?? $preCallbackTime,
1801  'version' => $version,
1802  'staleTTL' => $staleTTL,
1803  'lockTSE' => $lockTSE, // informs lag vs performance trade-offs
1804  'creating' => ( $curValue === false ), // optimization
1805  'walltime' => $walltime
1806  ] + $setOpts;
1807  $this->set( $key, $value, $ttl, $finalSetOpts );
1808  }
1809  }
1810 
1811  $this->yieldStampedeLock( $key, $hasLock );
1812 
1813  $miss = is_infinite( $minAsOf ) ? 'renew' : 'miss';
1814  $this->logger->debug( "fetchOrRegenerate($key): $miss, new value computed" );
1815  $this->stats->timing(
1816  "wanobjectcache.$kClass.$miss.compute",
1817  1e3 * ( $this->getCurrentTime() - $startTime )
1818  );
1819 
1820  return [ $value, $version, $curState[self::RES_AS_OF] ];
1821  }
1822 
1827  private function claimStampedeLock( $key ) {
1828  $checkSisterKey = $this->makeSisterKey( $key, self::TYPE_MUTEX );
1829  // Note that locking is not bypassed due to I/O errors; this avoids stampedes
1830  return $this->cache->add( $checkSisterKey, 1, self::LOCK_TTL );
1831  }
1832 
1837  private function yieldStampedeLock( $key, $hasLock ) {
1838  if ( $hasLock ) {
1839  $checkSisterKey = $this->makeSisterKey( $key, self::TYPE_MUTEX );
1840  $this->cache->changeTTL( $checkSisterKey, $this->getCurrentTime() - 60 );
1841  }
1842  }
1843 
1854  private function makeSisterKeys( array $baseKeys, string $type, string $route = null ) {
1855  $sisterKeys = [];
1856  foreach ( $baseKeys as $baseKey ) {
1857  $sisterKeys[] = $this->makeSisterKey( $baseKey, $type, $route );
1858  }
1859 
1860  return $sisterKeys;
1861  }
1862 
1873  private function makeSisterKey( string $baseKey, string $typeChar, string $route = null ) {
1874  if ( $this->coalesceScheme === self::SCHEME_HASH_STOP ) {
1875  // Key style: "WANCache:<base key>|#|<character>"
1876  $sisterKey = 'WANCache:' . $baseKey . '|#|' . $typeChar;
1877  } else {
1878  // Key style: "WANCache:{<base key>}:<character>"
1879  $sisterKey = 'WANCache:{' . $baseKey . '}:' . $typeChar;
1880  }
1881 
1882  if ( $route !== null ) {
1883  $sisterKey = $this->prependRoute( $sisterKey, $route );
1884  }
1885 
1886  return $sisterKey;
1887  }
1888 
1895  public static function getCollectionFromSisterKey( string $sisterKey ) {
1896  if ( substr( $sisterKey, -4 ) === '|#|v' ) {
1897  // Key style: "WANCache:<base key>|#|<character>"
1898  $collection = substr( $sisterKey, 9, strcspn( $sisterKey, ':|', 9 ) );
1899  } elseif ( substr( $sisterKey, -3 ) === '}:v' ) {
1900  // Key style: "WANCache:{<base key>}:<character>"
1901  $collection = substr( $sisterKey, 10, strcspn( $sisterKey, ':}', 10 ) );
1902  } else {
1903  $collection = 'internal';
1904  }
1905 
1906  return $collection;
1907  }
1908 
1921  private function isExtremelyNewValue( $res, $minAsOf, $now ) {
1922  if ( $res[self::RES_VALUE] === false || $res[self::RES_AS_OF] < $minAsOf ) {
1923  return false;
1924  }
1925 
1926  $age = $now - $res[self::RES_AS_OF];
1927 
1928  return ( $age < mt_rand( self::RECENT_SET_LOW_MS, self::RECENT_SET_HIGH_MS ) / 1e3 );
1929  }
1930 
1952  private function checkAndSetCooloff( $key, $kClass, $value, $elapsed, $hasLock ) {
1953  $valueSisterKey = $this->makeSisterKey( $key, self::TYPE_VALUE, $this->onHostRoute );
1954  list( $estimatedSize ) = $this->cache->setNewPreparedValues( [
1955  $valueSisterKey => $value
1956  ] );
1957 
1958  if ( !$hasLock ) {
1959  // Suppose that this cache key is very popular (KEY_HIGH_QPS reads/second).
1960  // After eviction, there will be cache misses until it gets regenerated and saved.
1961  // If the time window when the key is missing lasts less than one second, then the
1962  // number of misses will not reach KEY_HIGH_QPS. This window largely corresponds to
1963  // the key regeneration time. Estimate the count/rate of cache misses, e.g.:
1964  // - 100 QPS, 20ms regeneration => ~2 misses (< 1s)
1965  // - 100 QPS, 100ms regeneration => ~10 misses (< 1s)
1966  // - 100 QPS, 3000ms regeneration => ~300 misses (100/s for 3s)
1967  $missesPerSecForHighQPS = ( min( $elapsed, 1 ) * $this->keyHighQps );
1968 
1969  // Determine whether there is enough I/O stampede risk to justify throttling set().
1970  // Estimate unthrottled set() overhead, as bps, from miss count/rate and value size,
1971  // comparing it to the per-key uplink bps limit (KEY_HIGH_UPLINK_BPS), e.g.:
1972  // - 2 misses (< 1s), 10KB value, 1250000 bps limit => 160000 bits (low risk)
1973  // - 2 misses (< 1s), 100KB value, 1250000 bps limit => 1600000 bits (high risk)
1974  // - 10 misses (< 1s), 10KB value, 1250000 bps limit => 800000 bits (low risk)
1975  // - 10 misses (< 1s), 100KB value, 1250000 bps limit => 8000000 bits (high risk)
1976  // - 300 misses (100/s), 1KB value, 1250000 bps limit => 800000 bps (low risk)
1977  // - 300 misses (100/s), 10KB value, 1250000 bps limit => 8000000 bps (high risk)
1978  // - 300 misses (100/s), 100KB value, 1250000 bps limit => 80000000 bps (high risk)
1979  if ( ( $missesPerSecForHighQPS * $estimatedSize ) >= $this->keyHighUplinkBps ) {
1980  $cooloffSisterKey = $this->makeSisterKey( $key, self::TYPE_COOLOFF );
1981  $this->cache->clearLastError();
1982  if (
1983  !$this->cache->add( $cooloffSisterKey, 1, self::COOLOFF_TTL ) &&
1984  // Don't treat failures due to I/O errors as the key being in cool-off
1985  $this->cache->getLastError() === self::ERR_NONE
1986  ) {
1987  $this->stats->increment( "wanobjectcache.$kClass.cooloff_bounce" );
1988 
1989  return false;
1990  }
1991  }
1992  }
1993 
1994  // Corresponding metrics for cache writes that actually get sent over the write
1995  $this->stats->timing( "wanobjectcache.$kClass.regen_set_delay", 1e3 * $elapsed );
1996  $this->stats->updateCount( "wanobjectcache.$kClass.regen_set_bytes", $estimatedSize );
1997 
1998  return true;
1999  }
2000 
2010  private function getInterimValue( $key, $minAsOf, $now, $touchedCb ) {
2011  if ( $this->useInterimHoldOffCaching ) {
2012  $interimSisterKey = $this->makeSisterKey( $key, self::TYPE_INTERIM );
2013  $wrapped = $this->cache->get( $interimSisterKey );
2014  $res = $this->unwrap( $wrapped, $now );
2015  if ( $res[self::RES_VALUE] !== false && $res[self::RES_AS_OF] >= $minAsOf ) {
2016  if ( $touchedCb !== null ) {
2017  // Update "last purge time" since the $touchedCb timestamp depends on $value
2018  // Get the new "touched timestamp", accounting for callback-checked dependencies
2019  $res[self::RES_TOUCH_AS_OF] = max(
2020  $touchedCb( $res[self::RES_VALUE] ),
2021  $res[self::RES_TOUCH_AS_OF]
2022  );
2023  }
2024 
2025  return $res;
2026  }
2027  }
2028 
2029  return $this->unwrap( false, $now );
2030  }
2031 
2040  private function setInterimValue( $key, $value, $ttl, $version, $now, $walltime ) {
2041  $ttl = max( self::INTERIM_KEY_TTL, (int)$ttl );
2042 
2043  $wrapped = $this->wrap( $value, $ttl, $version, $now, $walltime );
2044  $this->cache->merge(
2045  $this->makeSisterKey( $key, self::TYPE_INTERIM ),
2046  static function () use ( $wrapped ) {
2047  return $wrapped;
2048  },
2049  $ttl,
2050  1
2051  );
2052  }
2053 
2058  private function resolveBusyValue( $busyValue ) {
2059  return ( $busyValue instanceof Closure ) ? $busyValue() : $busyValue;
2060  }
2061 
2126  final public function getMultiWithSetCallback(
2127  ArrayIterator $keyedIds, $ttl, callable $callback, array $opts = []
2128  ) {
2129  // Batch load required keys into the in-process warmup cache
2130  $this->warmupCache = $this->fetchWrappedValuesForWarmupCache(
2131  $this->getNonProcessCachedMultiKeys( $keyedIds, $opts ),
2132  $opts['checkKeys'] ?? []
2133  );
2134  $this->warmupKeyMisses = 0;
2135 
2136  // The required callback signature includes $id as the first argument for convenience
2137  // to distinguish different items. To reuse the code in getWithSetCallback(), wrap the
2138  // callback with a proxy callback that has the standard getWithSetCallback() signature.
2139  // This is defined only once per batch to avoid closure creation overhead.
2140  $proxyCb = static function ( $oldValue, &$ttl, &$setOpts, $oldAsOf, $params )
2141  use ( $callback )
2142  {
2143  return $callback( $params['id'], $oldValue, $ttl, $setOpts, $oldAsOf );
2144  };
2145 
2146  $values = [];
2147  foreach ( $keyedIds as $key => $id ) { // preserve order
2148  $values[$key] = $this->getWithSetCallback(
2149  $key,
2150  $ttl,
2151  $proxyCb,
2152  $opts,
2153  [ 'id' => $id ]
2154  );
2155  }
2156 
2157  $this->warmupCache = [];
2158 
2159  return $values;
2160  }
2161 
2227  final public function getMultiWithUnionSetCallback(
2228  ArrayIterator $keyedIds, $ttl, callable $callback, array $opts = []
2229  ) {
2230  $checkKeys = $opts['checkKeys'] ?? [];
2231  $minAsOf = $opts['minAsOf'] ?? self::MIN_TIMESTAMP_NONE;
2232  unset( $opts['lockTSE'] ); // incompatible
2233  unset( $opts['busyValue'] ); // incompatible
2234 
2235  // Batch load required keys into the in-process warmup cache
2236  $keysByIdGet = $this->getNonProcessCachedMultiKeys( $keyedIds, $opts );
2237  $this->warmupCache = $this->fetchWrappedValuesForWarmupCache( $keysByIdGet, $checkKeys );
2238  $this->warmupKeyMisses = 0;
2239 
2240  // IDs of entities known to be in need of generation
2241  $idsRegen = [];
2242 
2243  // Find out which keys are missing/deleted/stale
2244  $resByKey = $this->fetchKeys( $keysByIdGet, $checkKeys );
2245  foreach ( $keysByIdGet as $id => $key ) {
2246  $res = $resByKey[$key];
2247  if (
2248  $res[self::RES_VALUE] === false ||
2249  $res[self::RES_CUR_TTL] < 0 ||
2250  $res[self::RES_AS_OF] < $minAsOf
2251  ) {
2252  $idsRegen[] = $id;
2253  }
2254  }
2255 
2256  // Run the callback to populate the generation value map for all required IDs
2257  $newSetOpts = [];
2258  $newTTLsById = array_fill_keys( $idsRegen, $ttl );
2259  $newValsById = $idsRegen ? $callback( $idsRegen, $newTTLsById, $newSetOpts ) : [];
2260 
2261  // The required callback signature includes $id as the first argument for convenience
2262  // to distinguish different items. To reuse the code in getWithSetCallback(), wrap the
2263  // callback with a proxy callback that has the standard getWithSetCallback() signature.
2264  // This is defined only once per batch to avoid closure creation overhead.
2265  $proxyCb = static function ( $oldValue, &$ttl, &$setOpts, $oldAsOf, $params )
2266  use ( $callback, $newValsById, $newTTLsById, $newSetOpts )
2267  {
2268  $id = $params['id'];
2269 
2270  if ( array_key_exists( $id, $newValsById ) ) {
2271  // Value was already regerated as expected, so use the value in $newValsById
2272  $newValue = $newValsById[$id];
2273  $ttl = $newTTLsById[$id];
2274  $setOpts = $newSetOpts;
2275  } else {
2276  // Pre-emptive/popularity refresh and version mismatch cases are not detected
2277  // above and thus $newValsById has no entry. Run $callback on this single entity.
2278  $ttls = [ $id => $ttl ];
2279  $newValue = $callback( [ $id ], $ttls, $setOpts )[$id];
2280  $ttl = $ttls[$id];
2281  }
2282 
2283  return $newValue;
2284  };
2285 
2286  // Run the cache-aside logic using warmupCache instead of persistent cache queries
2287  $values = [];
2288  foreach ( $keyedIds as $key => $id ) { // preserve order
2289  $values[$key] = $this->getWithSetCallback(
2290  $key,
2291  $ttl,
2292  $proxyCb,
2293  $opts,
2294  [ 'id' => $id ]
2295  );
2296  }
2297 
2298  $this->warmupCache = [];
2299 
2300  return $values;
2301  }
2302 
2315  final public function reap( $key, $purgeTimestamp, &$isStale = false ) {
2316  $valueSisterKey = $this->makeSisterKey( $key, self::TYPE_VALUE );
2317 
2318  $minAsOf = $purgeTimestamp + self::HOLDOFF_TTL;
2319  $wrapped = $this->cache->get( $valueSisterKey );
2320  if ( is_array( $wrapped ) && $wrapped[self::FLD_TIME] < $minAsOf ) {
2321  $isStale = true;
2322  $this->logger->warning( "Reaping stale value key '$key'." );
2323  $ttlReap = self::HOLDOFF_TTL; // avoids races with tombstone creation
2324  $ok = $this->cache->changeTTL( $valueSisterKey, $ttlReap );
2325  if ( !$ok ) {
2326  $this->logger->error( "Could not complete reap of key '$key'." );
2327  }
2328 
2329  return $ok;
2330  }
2331 
2332  $isStale = false;
2333 
2334  return true;
2335  }
2336 
2346  final public function reapCheckKey( $key, $purgeTimestamp, &$isStale = false ) {
2347  $checkSisterKey = $this->makeSisterKey( $key, self::TYPE_TIMESTAMP );
2348 
2349  $wrapped = $this->cache->get( $checkSisterKey );
2350  $purge = $this->parsePurgeValue( $wrapped );
2351  if ( $purge !== null && $purge[self::PURGE_TIME] < $purgeTimestamp ) {
2352  $isStale = true;
2353  $this->logger->warning( "Reaping stale check key '$key'." );
2354  $ok = $this->cache->changeTTL( $checkSisterKey, self::TTL_SECOND );
2355  if ( !$ok ) {
2356  $this->logger->error( "Could not complete reap of check key '$key'." );
2357  }
2358 
2359  return $ok;
2360  }
2361 
2362  $isStale = false;
2363 
2364  return false;
2365  }
2366 
2377  public function makeGlobalKey( $collection, ...$components ) {
2378  return $this->cache->makeGlobalKey( ...func_get_args() );
2379  }
2380 
2391  public function makeKey( $collection, ...$components ) {
2392  return $this->cache->makeKey( ...func_get_args() );
2393  }
2394 
2402  public function hash256( $component ) {
2403  return hash_hmac( 'sha256', $component, $this->secret );
2404  }
2405 
2456  final public function makeMultiKeys( array $ids, $keyCallback ) {
2457  $idByKey = [];
2458  foreach ( $ids as $id ) {
2459  // Discourage triggering of automatic makeKey() hashing in some backends
2460  if ( strlen( $id ) > 64 ) {
2461  $this->logger->warning( __METHOD__ . ": long ID '$id'; use hash256()" );
2462  }
2463  $key = $keyCallback( $id, $this );
2464  // Edge case: ignore key collisions due to duplicate $ids like "42" and 42
2465  if ( !isset( $idByKey[$key] ) ) {
2466  $idByKey[$key] = $id;
2467  } elseif ( (string)$id !== (string)$idByKey[$key] ) {
2468  throw new UnexpectedValueException(
2469  "Cache key collision; IDs ('$id','{$idByKey[$key]}') map to '$key'"
2470  );
2471  }
2472  }
2473 
2474  return new ArrayIterator( $idByKey );
2475  }
2476 
2512  final public function multiRemap( array $ids, array $res ) {
2513  if ( count( $ids ) !== count( $res ) ) {
2514  // If makeMultiKeys() is called on a list of non-unique IDs, then the resulting
2515  // ArrayIterator will have less entries due to "first appearance" de-duplication
2516  $ids = array_keys( array_fill_keys( $ids, true ) );
2517  if ( count( $ids ) !== count( $res ) ) {
2518  throw new UnexpectedValueException( "Multi-key result does not match ID list" );
2519  }
2520  }
2521 
2522  return array_combine( $ids, $res );
2523  }
2524 
2529  final public function getLastError() {
2530  $code = $this->cache->getLastError();
2531  switch ( $code ) {
2532  case self::ERR_NONE:
2533  return self::ERR_NONE;
2534  case self::ERR_NO_RESPONSE:
2535  return self::ERR_NO_RESPONSE;
2536  case self::ERR_UNREACHABLE:
2537  return self::ERR_UNREACHABLE;
2538  default:
2539  return self::ERR_UNEXPECTED;
2540  }
2541  }
2542 
2546  final public function clearLastError() {
2547  $this->cache->clearLastError();
2548  }
2549 
2555  public function clearProcessCache() {
2556  $this->processCaches = [];
2557  }
2558 
2579  final public function useInterimHoldOffCaching( $enabled ) {
2580  $this->useInterimHoldOffCaching = $enabled;
2581  }
2582 
2588  public function getQoS( $flag ) {
2589  return $this->cache->getQoS( $flag );
2590  }
2591 
2655  public function adaptiveTTL( $mtime, $maxTTL, $minTTL = 30, $factor = 0.2 ) {
2656  if ( is_float( $mtime ) || ctype_digit( $mtime ) ) {
2657  $mtime = (int)$mtime; // handle fractional seconds and string integers
2658  }
2659 
2660  if ( !is_int( $mtime ) || $mtime <= 0 ) {
2661  return $minTTL; // no last-modified time provided
2662  }
2663 
2664  $age = $this->getCurrentTime() - $mtime;
2665 
2666  return (int)min( $maxTTL, max( $minTTL, $factor * $age ) );
2667  }
2668 
2673  final public function getWarmupKeyMisses() {
2674  return $this->warmupKeyMisses;
2675  }
2676 
2691  protected function relayVolatilePurges( array $purgeBySisterKey, int $ttl ) {
2692  $purgeByRouteKey = [];
2693  foreach ( $purgeBySisterKey as $sisterKey => $purge ) {
2694  if ( $this->broadcastRoute !== null ) {
2695  $routeKey = $this->prependRoute( $sisterKey, $this->broadcastRoute );
2696  } else {
2697  $routeKey = $sisterKey;
2698  }
2699  $purgeByRouteKey[$routeKey] = $purge;
2700  }
2701 
2702  if ( count( $purgeByRouteKey ) == 1 ) {
2703  $purge = reset( $purgeByRouteKey );
2704  $ok = $this->cache->set( key( $purgeByRouteKey ), $purge, $ttl );
2705  } else {
2706  $ok = $this->cache->setMulti( $purgeByRouteKey, $ttl );
2707  }
2708 
2709  return $ok;
2710  }
2711 
2720  protected function relayNonVolatilePurge( string $sisterKey ) {
2721  if ( $this->broadcastRoute !== null ) {
2722  $routeKey = $this->prependRoute( $sisterKey, $this->broadcastRoute );
2723  } else {
2724  $routeKey = $sisterKey;
2725  }
2726 
2727  return $this->cache->delete( $routeKey );
2728  }
2729 
2735  protected function prependRoute( string $sisterKey, string $route ) {
2736  if ( $sisterKey[0] === '/' ) {
2737  throw new RuntimeException( "Sister key '$sisterKey' already contains a route." );
2738  }
2739 
2740  return $route . $sisterKey;
2741  }
2742 
2754  private function scheduleAsyncRefresh( $key, $ttl, $callback, array $opts, array $cbParams ) {
2755  if ( !$this->asyncHandler ) {
2756  return false;
2757  }
2758  // Update the cache value later, such during post-send of an HTTP request. This forces
2759  // cache regeneration by setting "minAsOf" to infinity, meaning that no existing value
2760  // is considered valid. Furthermore, note that preemptive regeneration is not applicable
2761  // to invalid values, so there is no risk of infinite preemptive regeneration loops.
2762  $func = $this->asyncHandler;
2763  $func( function () use ( $key, $ttl, $callback, $opts, $cbParams ) {
2764  $opts['minAsOf'] = INF;
2765  try {
2766  $this->fetchOrRegenerate( $key, $ttl, $callback, $opts, $cbParams );
2767  } catch ( Exception $e ) {
2768  // Log some context for easier debugging
2769  $this->logger->error( 'Async refresh failed for {key}', [
2770  'key' => $key,
2771  'ttl' => $ttl,
2772  'exception' => $e
2773  ] );
2774  throw $e;
2775  }
2776  } );
2777 
2778  return true;
2779  }
2780 
2789  private function isAcceptablyFreshValue( $res, $graceTTL, $minAsOf ) {
2790  if ( !$this->isValid( $res[self::RES_VALUE], $res[self::RES_AS_OF], $minAsOf ) ) {
2791  // Value does not exists or is too old
2792  return false;
2793  }
2794 
2795  $curTTL = $res[self::RES_CUR_TTL];
2796  if ( $curTTL > 0 ) {
2797  // Value is definitely still fresh
2798  return true;
2799  }
2800 
2801  // Remaining seconds during which this stale value can be used
2802  $curGraceTTL = $graceTTL + $curTTL;
2803 
2804  return ( $curGraceTTL > 0 )
2805  // Chance of using the value decreases as $curTTL goes from 0 to -$graceTTL
2806  ? !$this->worthRefreshExpiring( $curGraceTTL, $graceTTL, $graceTTL )
2807  // Value is too stale to fall in the grace period
2808  : false;
2809  }
2810 
2821  protected function isLotteryRefreshDue( $res, $lowTTL, $ageNew, $hotTTR, $now ) {
2822  $curTTL = $res[self::RES_CUR_TTL];
2823  $logicalTTL = $res[self::RES_TTL];
2824  $asOf = $res[self::RES_AS_OF];
2825 
2826  return (
2827  $this->worthRefreshExpiring( $curTTL, $logicalTTL, $lowTTL ) ||
2828  $this->worthRefreshPopular( $asOf, $ageNew, $hotTTR, $now )
2829  );
2830  }
2831 
2847  protected function worthRefreshPopular( $asOf, $ageNew, $timeTillRefresh, $now ) {
2848  if ( $ageNew < 0 || $timeTillRefresh <= 0 ) {
2849  return false;
2850  }
2851 
2852  $age = $now - $asOf;
2853  $timeOld = $age - $ageNew;
2854  if ( $timeOld <= 0 ) {
2855  return false;
2856  }
2857 
2858  $popularHitsPerSec = 1;
2859  // Lifecycle is: new, ramp-up refresh chance, full refresh chance.
2860  // Note that the "expected # of refreshes" for the ramp-up time range is half
2861  // of what it would be if P(refresh) was at its full value during that time range.
2862  $refreshWindowSec = max( $timeTillRefresh - $ageNew - self::RAMPUP_TTL / 2, 1 );
2863  // P(refresh) * (# hits in $refreshWindowSec) = (expected # of refreshes)
2864  // P(refresh) * ($refreshWindowSec * $popularHitsPerSec) = 1 (by definition)
2865  // P(refresh) = 1/($refreshWindowSec * $popularHitsPerSec)
2866  $chance = 1 / ( $popularHitsPerSec * $refreshWindowSec );
2867  // Ramp up $chance from 0 to its nominal value over RAMPUP_TTL seconds to avoid stampedes
2868  $chance *= ( $timeOld <= self::RAMPUP_TTL ) ? $timeOld / self::RAMPUP_TTL : 1;
2869 
2870  $decision = ( mt_rand( 1, 1000000000 ) <= 1000000000 * $chance );
2871 
2872  return $decision;
2873  }
2874 
2893  protected function worthRefreshExpiring( $curTTL, $logicalTTL, $lowTTL ) {
2894  if ( $lowTTL <= 0 ) {
2895  return false;
2896  }
2897 
2898  // T264787: avoid having keys start off with a high chance of being refreshed;
2899  // the point where refreshing becomes possible cannot precede the key lifetime.
2900  $effectiveLowTTL = min( $lowTTL, $logicalTTL ?: INF );
2901 
2902  if ( $curTTL >= $effectiveLowTTL || $curTTL <= 0 ) {
2903  return false;
2904  }
2905 
2906  $chance = ( 1 - $curTTL / $effectiveLowTTL );
2907 
2908  $decision = ( mt_rand( 1, 1000000000 ) <= 1000000000 * $chance );
2909 
2910  return $decision;
2911  }
2912 
2921  protected function isValid( $value, $asOf, $minAsOf ) {
2922  return ( $value !== false && $asOf >= $minAsOf );
2923  }
2924 
2933  private function wrap( $value, $ttl, $version, $now, $walltime ) {
2934  // Returns keys in ascending integer order for PHP7 array packing:
2935  // https://nikic.github.io/2014/12/22/PHPs-new-hashtable-implementation.html
2936  $wrapped = [
2937  self::FLD_FORMAT_VERSION => self::VERSION,
2938  self::FLD_VALUE => $value,
2939  self::FLD_TTL => $ttl,
2940  self::FLD_TIME => $now
2941  ];
2942  if ( $version !== null ) {
2943  $wrapped[self::FLD_VALUE_VERSION] = $version;
2944  }
2945  if ( $walltime >= self::GENERATION_SLOW_SEC ) {
2946  $wrapped[self::FLD_GENERATION_TIME] = $walltime;
2947  }
2948 
2949  return $wrapped;
2950  }
2951 
2966  private function unwrap( $wrapped, $now ) {
2967  // https://nikic.github.io/2014/12/22/PHPs-new-hashtable-implementation.html
2968  $res = [
2969  // Attributes that only depend on the fetched key value
2970  self::RES_VALUE => false,
2971  self::RES_VERSION => null,
2972  self::RES_AS_OF => null,
2973  self::RES_TTL => null,
2974  self::RES_TOMB_AS_OF => null,
2975  // Attributes that depend on caller-specific "check" keys or "touched callbacks"
2976  self::RES_CHECK_AS_OF => null,
2977  self::RES_TOUCH_AS_OF => null,
2978  self::RES_CUR_TTL => null
2979  ];
2980 
2981  if ( is_array( $wrapped ) ) {
2982  // Entry expected to be a cached value; validate it
2983  if (
2984  ( $wrapped[self::FLD_FORMAT_VERSION] ?? null ) === self::VERSION &&
2985  $wrapped[self::FLD_TIME] >= $this->epoch
2986  ) {
2987  if ( $wrapped[self::FLD_TTL] > 0 ) {
2988  // Get the approximate time left on the key
2989  $age = $now - $wrapped[self::FLD_TIME];
2990  $curTTL = max( $wrapped[self::FLD_TTL] - $age, 0.0 );
2991  } else {
2992  // Key had no TTL, so the time left is unbounded
2993  $curTTL = INF;
2994  }
2995  $res[self::RES_VALUE] = $wrapped[self::FLD_VALUE];
2996  $res[self::RES_VERSION] = $wrapped[self::FLD_VALUE_VERSION] ?? null;
2997  $res[self::RES_AS_OF] = $wrapped[self::FLD_TIME];
2998  $res[self::RES_CUR_TTL] = $curTTL;
2999  $res[self::RES_TTL] = $wrapped[self::FLD_TTL];
3000  }
3001  } else {
3002  // Entry expected to be a tombstone; parse it
3003  $purge = $this->parsePurgeValue( $wrapped );
3004  if ( $purge !== null ) {
3005  // Tombstoned keys should always have a negative "current TTL"
3006  $curTTL = min( $purge[self::PURGE_TIME] - $now, self::TINY_NEGATIVE );
3007  $res[self::RES_CUR_TTL] = $curTTL;
3009  }
3010  }
3011 
3012  return $res;
3013  }
3014 
3019  private function determineKeyClassForStats( $key ) {
3020  $parts = explode( ':', $key, 3 );
3021  // Sanity fallback in case the key was not made by makeKey.
3022  // Replace dots because they are special in StatsD (T232907)
3023  return strtr( $parts[1] ?? $parts[0], '.', '_' );
3024  }
3025 
3034  private function parsePurgeValue( $value ) {
3035  if ( !is_string( $value ) ) {
3036  return null;
3037  }
3038 
3039  $segments = explode( ':', $value, 3 );
3040  $prefix = $segments[0];
3041  if ( $prefix !== self::PURGE_VAL_PREFIX ) {
3042  // Not a purge value
3043  return null;
3044  }
3045 
3046  $timestamp = (float)$segments[1];
3047  // makeTombstonePurgeValue() doesn't store hold-off TTLs
3048  $holdoff = isset( $segments[2] ) ? (int)$segments[2] : self::HOLDOFF_TTL;
3049 
3050  if ( $timestamp < $this->epoch ) {
3051  // Purge value is too old
3052  return null;
3053  }
3054 
3055  return [ self::PURGE_TIME => $timestamp, self::PURGE_HOLDOFF => $holdoff ];
3056  }
3057 
3062  private function makeTombstonePurgeValue( float $timestamp ) {
3063  return self::PURGE_VAL_PREFIX . ':' . (int)$timestamp;
3064  }
3065 
3072  private function makeCheckPurgeValue( float $timestamp, int $holdoff, array &$purge = null ) {
3073  $normalizedTime = (int)$timestamp;
3074  // Purge array that matches what parsePurgeValue() would have returned
3075  $purge = [ self::PURGE_TIME => (float)$normalizedTime, self::PURGE_HOLDOFF => $holdoff ];
3076 
3077  return self::PURGE_VAL_PREFIX . ":$normalizedTime:$holdoff";
3078  }
3079 
3084  private function getProcessCache( $group ) {
3085  if ( !isset( $this->processCaches[$group] ) ) {
3086  list( , $size ) = explode( ':', $group );
3087  $this->processCaches[$group] = new MapCacheLRU( (int)$size );
3088  if ( $this->wallClockOverride !== null ) {
3089  $this->processCaches[$group]->setMockTime( $this->wallClockOverride );
3090  }
3091  }
3092 
3093  return $this->processCaches[$group];
3094  }
3095 
3101  private function getNonProcessCachedMultiKeys( ArrayIterator $keys, array $opts ) {
3102  $pcTTL = $opts['pcTTL'] ?? self::TTL_UNCACHEABLE;
3103 
3104  $keysMissing = [];
3105  if ( $pcTTL > 0 && $this->callbackDepth == 0 ) {
3106  $pCache = $this->getProcessCache( $opts['pcGroup'] ?? self::PC_PRIMARY );
3107  foreach ( $keys as $key => $id ) {
3108  if ( !$pCache->has( $key, $pcTTL ) ) {
3109  $keysMissing[$id] = $key;
3110  }
3111  }
3112  }
3113 
3114  return $keysMissing;
3115  }
3116 
3123  private function fetchWrappedValuesForWarmupCache( array $keys, array $checkKeys ) {
3124  if ( !$keys ) {
3125  return [];
3126  }
3127 
3128  // Get all the value keys to fetch...
3129  $sisterKeys = $this->makeSisterKeys( $keys, self::TYPE_VALUE, $this->onHostRoute );
3130  // Get all the flux keys to fetch...
3131  if ( $this->onHostRoute !== null ) {
3132  foreach ( $keys as $key ) {
3133  $sisterKeys[] = $this->makeSisterKey( $key, self::TYPE_FLUX );
3134  }
3135  }
3136  // Get all the "check" keys to fetch...
3137  foreach ( $checkKeys as $i => $checkKeyOrKeyGroup ) {
3138  // Note: avoid array_merge() inside loop in case there are many keys
3139  if ( is_int( $i ) ) {
3140  // Single "check" key that applies to all value keys
3141  $sisterKeys[] = $this->makeSisterKey( $checkKeyOrKeyGroup, self::TYPE_TIMESTAMP );
3142  } else {
3143  // List of "check" keys that apply to a specific value key
3144  foreach ( (array)$checkKeyOrKeyGroup as $checkKey ) {
3145  $sisterKeys[] = $this->makeSisterKey( $checkKey, self::TYPE_TIMESTAMP );
3146  }
3147  }
3148  }
3149 
3150  $wrappedBySisterKey = $this->cache->getMulti( $sisterKeys );
3151  $wrappedBySisterKey += array_fill_keys( $sisterKeys, false );
3152 
3153  return $wrappedBySisterKey;
3154  }
3155 
3161  private function timeSinceLoggedMiss( $key, $now ) {
3162  for ( end( $this->missLog ); $miss = current( $this->missLog ); prev( $this->missLog ) ) {
3163  if ( $miss[0] === $key ) {
3164  return ( $now - $miss[1] );
3165  }
3166  }
3167 
3168  return null;
3169  }
3170 
3175  protected function getCurrentTime() {
3176  if ( $this->wallClockOverride ) {
3177  return $this->wallClockOverride;
3178  }
3179 
3180  $clockTime = (float)time(); // call this first
3181  // microtime() uses an initial gettimeofday() call added to usage clocks.
3182  // This can severely drift from time() and the microtime() value of other threads
3183  // due to undercounting of the amount of time elapsed. Instead of seeing the current
3184  // time as being in the past, use the value of time(). This avoids setting cache values
3185  // that will immediately be seen as expired and possibly cause stampedes.
3186  return max( microtime( true ), $clockTime );
3187  }
3188 
3193  public function setMockTime( &$time ) {
3194  $this->wallClockOverride =& $time;
3195  $this->cache->setMockTime( $time );
3196  foreach ( $this->processCaches as $pCache ) {
3197  $pCache->setMockTime( $time );
3198  }
3199  }
3200 }
WANObjectCache\relayVolatilePurges
relayVolatilePurges(array $purgeBySisterKey, int $ttl)
Set a sister key to a purge value in all datacenters.
Definition: WANObjectCache.php:2691
WANObjectCache\getWithSetCallback
getWithSetCallback( $key, $ttl, $callback, array $opts=[], array $cbParams=[])
Method to fetch/regenerate a cache key.
Definition: WANObjectCache.php:1580
WANObjectCache\KEY_TTL
const KEY_TTL
Logical TTL attribute for a key.
Definition: WANObjectCache.php:267
WANObjectCache\getQoS
getQoS( $flag)
Definition: WANObjectCache.php:2588
WANObjectCache\hash256
hash256( $component)
Hash a possibly long string into a suitable component for makeKey()/makeGlobalKey()
Definition: WANObjectCache.php:2402
WANObjectCache\determineKeyClassForStats
determineKeyClassForStats( $key)
Definition: WANObjectCache.php:3019
WANObjectCache\setMockTime
setMockTime(&$time)
Definition: WANObjectCache.php:3193
WANObjectCache\$warmupCache
mixed[] $warmupCache
Temporary warm-up cache.
Definition: WANObjectCache.php:179
Wikimedia\LightweightObjectStore\StorageAwareness\ERR_NO_RESPONSE
const ERR_NO_RESPONSE
Storage medium failed to yield a response.
Definition: StorageAwareness.php:36
WANObjectCache\RES_VALUE
const RES_VALUE
Value for a key.
Definition: WANObjectCache.php:276
WANObjectCache\SCHEME_HASH_TAG
const SCHEME_HASH_TAG
Use twemproxy-style Hash Tag key scheme (e.g.
Definition: WANObjectCache.php:223
EmptyBagOStuff
A BagOStuff object with no objects in it.
Definition: EmptyBagOStuff.php:29
WANObjectCache\KEY_VERSION
const KEY_VERSION
Version number attribute for a key; keep value for b/c (< 1.36)
Definition: WANObjectCache.php:263
WANObjectCache\$cache
BagOStuff $cache
The local datacenter cache.
Definition: WANObjectCache.php:139
WANObjectCache\fetchOrRegenerate
fetchOrRegenerate( $key, $ttl, $callback, array $opts, array $cbParams)
Do the actual I/O for getWithSetCallback() when needed.
Definition: WANObjectCache.php:1640
WANObjectCache\MAX_READ_LAG
const MAX_READ_LAG
Max expected seconds of combined lag from replication and "view snapshots".
Definition: WANObjectCache.php:189
WANObjectCache\TYPE_FLUX
const TYPE_FLUX
Single character component for flux keys.
Definition: WANObjectCache.php:312
WANObjectCache\FLD_VALUE
const FLD_VALUE
Key to the cached value; stored in blobs.
Definition: WANObjectCache.php:295
WANObjectCache\MAX_COMMIT_DELAY
const MAX_COMMIT_DELAY
Max expected seconds to pass between delete() and DB commit finishing.
Definition: WANObjectCache.php:187
WANObjectCache\processFluxKeys
processFluxKeys(array $keys, array $fluxSisterKeys, array $wrappedBySisterKey)
Definition: WANObjectCache.php:714
WANObjectCache\GENERATION_SLOW_SEC
const GENERATION_SLOW_SEC
Consider value generation slow if it takes this many seconds or more.
Definition: WANObjectCache.php:252
WANObjectCache\isExtremelyNewValue
isExtremelyNewValue( $res, $minAsOf, $now)
Check if a key value is non-false, new enough, and has an "as of" time almost equal to now.
Definition: WANObjectCache.php:1921
Wikimedia\LightweightObjectStore\ExpirationAwareness
Generic interface providing Time-To-Live constants for expirable object storage.
Definition: ExpirationAwareness.php:32
WANObjectCache\TYPE_INTERIM
const TYPE_INTERIM
Single character component for interium value keys.
Definition: WANObjectCache.php:316
WANObjectCache\RECENT_SET_LOW_MS
const RECENT_SET_LOW_MS
Min millisecond set() backoff during hold-off (far less than INTERIM_KEY_TTL)
Definition: WANObjectCache.php:245
WANObjectCache\isLotteryRefreshDue
isLotteryRefreshDue( $res, $lowTTL, $ageNew, $hotTTR, $now)
Check if a key is due for randomized regeneration due to near-expiration/popularity.
Definition: WANObjectCache.php:2821
WANObjectCache\KEY_CUR_TTL
const KEY_CUR_TTL
Remaining TTL attribute for a key; keep value for b/c (< 1.36)
Definition: WANObjectCache.php:269
NullStatsdDataFactory
Definition: NullStatsdDataFactory.php:10
WANObjectCache\FLD_GENERATION_TIME
const FLD_GENERATION_TIME
Key to how long it took to generate the value; stored in blobs.
Definition: WANObjectCache.php:305
WANObjectCache\FLD_FORMAT_VERSION
const FLD_FORMAT_VERSION
Key to WAN cache version number; stored in blobs.
Definition: WANObjectCache.php:293
WANObjectCache\getMultiWithSetCallback
getMultiWithSetCallback(ArrayIterator $keyedIds, $ttl, callable $callback, array $opts=[])
Method to fetch multiple cache keys at once with regeneration.
Definition: WANObjectCache.php:2126
BagOStuff
Class representing a cache/ephemeral data store.
Definition: BagOStuff.php:86
WANObjectCache\getMultiWithUnionSetCallback
getMultiWithUnionSetCallback(ArrayIterator $keyedIds, $ttl, callable $callback, array $opts=[])
Method to fetch/regenerate multiple cache keys at once.
Definition: WANObjectCache.php:2227
WANObjectCache\getCollectionFromSisterKey
static getCollectionFromSisterKey(string $sisterKey)
Definition: WANObjectCache.php:1895
WANObjectCache\PASS_BY_REF
const PASS_BY_REF
Idiom for get()/getMulti() to return extra information by reference.
Definition: WANObjectCache.php:220
WANObjectCache\FLD_VALUE_VERSION
const FLD_VALUE_VERSION
Key to collection cache version number; stored in blobs.
Definition: WANObjectCache.php:303
WANObjectCache\worthRefreshExpiring
worthRefreshExpiring( $curTTL, $logicalTTL, $lowTTL)
Check if a key is nearing expiration and thus due for randomized regeneration.
Definition: WANObjectCache.php:2893
$res
$res
Definition: testCompression.php:57
WANObjectCache\RES_CHECK_AS_OF
const RES_CHECK_AS_OF
Highest "check" key timestamp for a key.
Definition: WANObjectCache.php:286
WANObjectCache\TTL_LAGGED
const TTL_LAGGED
Max TTL, in seconds, to store keys when a data source has high replication lag.
Definition: WANObjectCache.php:196
WANObjectCache\HOLDOFF_TTL_NONE
const HOLDOFF_TTL_NONE
Idiom for delete()/touchCheckKey() meaning "no hold-off period".
Definition: WANObjectCache.php:211
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:2346
Wikimedia\LightweightObjectStore\StorageAwareness\ERR_NONE
const ERR_NONE
No storage medium error.
Definition: StorageAwareness.php:34
WANObjectCache\SCHEME_HASH_STOP
const SCHEME_HASH_STOP
Use mcrouter-style Hash Stop key scheme (e.g.
Definition: WANObjectCache.php:225
WANObjectCache\RES_TOUCH_AS_OF
const RES_TOUCH_AS_OF
Highest "touched" timestamp for a key.
Definition: WANObjectCache.php:288
WANObjectCache\fetchKeys
fetchKeys(array $keys, array $checkKeys, $touchedCb=null)
Fetch the value and key metadata of several keys from cache.
Definition: WANObjectCache.php:572
WANObjectCache\getCheckKeyTime
getCheckKeyTime( $key)
Fetch the value of a timestamp "check" key.
Definition: WANObjectCache.php:1105
WANObjectCache\resolveBusyValue
resolveBusyValue( $busyValue)
Definition: WANObjectCache.php:2058
WANObjectCache\$logger
LoggerInterface $logger
Definition: WANObjectCache.php:143
WANObjectCache\makeCheckPurgeValue
makeCheckPurgeValue(float $timestamp, int $holdoff, array &$purge=null)
Definition: WANObjectCache.php:3072
WANObjectCache\isAcceptablyFreshValue
isAcceptablyFreshValue( $res, $graceTTL, $minAsOf)
Check if a key value is non-false, new enough, and either fresh or "gracefully" stale.
Definition: WANObjectCache.php:2789
WANObjectCache\$warmupKeyMisses
int $warmupKeyMisses
Key fetched.
Definition: WANObjectCache.php:181
WANObjectCache\newEmpty
static newEmpty()
Get an instance that wraps EmptyBagOStuff.
Definition: WANObjectCache.php:408
WANObjectCache\LOW_TTL
const LOW_TTL
Consider regeneration if the key will expire within this many seconds.
Definition: WANObjectCache.php:194
WANObjectCache\FLD_TIME
const FLD_TIME
Key to the cache timestamp; stored in blobs.
Definition: WANObjectCache.php:299
WANObjectCache\clearProcessCache
clearProcessCache()
Clear the in-process caches; useful for testing.
Definition: WANObjectCache.php:2555
WANObjectCache\$broadcastRoute
string null $broadcastRoute
Routing prefix for operations that should be broadcasted to all data centers.
Definition: WANObjectCache.php:156
WANObjectCache\$secret
string $secret
Stable secret used for hasing long strings into key components.
Definition: WANObjectCache.php:164
WANObjectCache\clearLastError
clearLastError()
Clear the "last error" registry.
Definition: WANObjectCache.php:2546
WANObjectCache\getCurrentTime
getCurrentTime()
Definition: WANObjectCache.php:3175
WANObjectCache\INTERIM_KEY_TTL
const INTERIM_KEY_TTL
Seconds to keep interim value keys for tombstoned keys around.
Definition: WANObjectCache.php:230
WANObjectCache\unwrap
unwrap( $wrapped, $now)
Definition: WANObjectCache.php:2966
WANObjectCache\fetchWrappedValuesForWarmupCache
fetchWrappedValuesForWarmupCache(array $keys, array $checkKeys)
Definition: WANObjectCache.php:3123
WANObjectCache\getLastError
getLastError()
Get the "last error" registered; clearLastError() should be called manually.
Definition: WANObjectCache.php:2529
WANObjectCache\relayNonVolatilePurge
relayNonVolatilePurge(string $sisterKey)
Remove a sister key from all datacenters.
Definition: WANObjectCache.php:2720
WANObjectCache\makeMultiKeys
makeMultiKeys(array $ids, $keyCallback)
Get an iterator of (cache key => entity ID) for a list of entity IDs.
Definition: WANObjectCache.php:2456
WANObjectCache\RES_TTL
const RES_TTL
Logical TTL attribute for a key.
Definition: WANObjectCache.php:282
WANObjectCache\getMulti
getMulti(array $keys, &$curTTLs=[], array $checkKeys=[], &$info=[])
Fetch the value of several keys from cache.
Definition: WANObjectCache.php:520
WANObjectCache\__construct
__construct(array $params)
Definition: WANObjectCache.php:364
WANObjectCache\RES_AS_OF
const RES_AS_OF
Generation completion timestamp attribute for a key.
Definition: WANObjectCache.php:280
WANObjectCache\RES_CUR_TTL
const RES_CUR_TTL
Remaining TTL attribute for a key.
Definition: WANObjectCache.php:290
WANObjectCache\useInterimHoldOffCaching
useInterimHoldOffCaching( $enabled)
Enable or disable the use of brief caching for tombstoned keys.
Definition: WANObjectCache.php:2579
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:2315
WANObjectCache\$onHostRoute
string null $onHostRoute
Routing prefix for value keys that support use of an on-host tier.
Definition: WANObjectCache.php:158
WANObjectCache\$keyHighQps
int $keyHighQps
Reads/second assumed during a hypothetical cache write stampede for a key.
Definition: WANObjectCache.php:169
WANObjectCache\touchCheckKey
touchCheckKey( $key, $holdoff=self::HOLDOFF_TTL)
Increase the last-purge timestamp of a "check" key in all datacenters.
Definition: WANObjectCache.php:1227
WANObjectCache\TYPE_COOLOFF
const TYPE_COOLOFF
Single character component for cool-off bounce keys.
Definition: WANObjectCache.php:318
MapCacheLRU
Handles a simple LRU key/value map with a maximum number of entries.
Definition: MapCacheLRU.php:36
WANObjectCache\getInterimValue
getInterimValue( $key, $minAsOf, $now, $touchedCb)
Definition: WANObjectCache.php:2010
WANObjectCache\makeSisterKey
makeSisterKey(string $baseKey, string $typeChar, string $route=null)
Get a sister key that should be collocated with a base cache key.
Definition: WANObjectCache.php:1873
WANObjectCache\wrap
wrap( $value, $ttl, $version, $now, $walltime)
Definition: WANObjectCache.php:2933
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:2655
WANObjectCache\getNonProcessCachedMultiKeys
getNonProcessCachedMultiKeys(ArrayIterator $keys, array $opts)
Definition: WANObjectCache.php:3101
WANObjectCache\$epoch
float $epoch
Unix timestamp of the oldest possible valid values.
Definition: WANObjectCache.php:162
WANObjectCache\KEY_TOMB_AS_OF
const KEY_TOMB_AS_OF
Tomstone timestamp attribute for a key; keep value for b/c (< 1.36)
Definition: WANObjectCache.php:271
WANObjectCache\$wallClockOverride
float null $wallClockOverride
Definition: WANObjectCache.php:184
WANObjectCache\HOLDOFF_TTL
const HOLDOFF_TTL
Seconds to tombstone keys on delete() and to treat keys as volatile after purges.
Definition: WANObjectCache.php:191
Wikimedia\LightweightObjectStore\StorageAwareness
Generic interface providing error code and quality-of-service constants for object stores.
Definition: StorageAwareness.php:32
WANObjectCache\makeGlobalKey
makeGlobalKey( $collection,... $components)
Make a cache key for the global keyspace and given components.
Definition: WANObjectCache.php:2377
WANObjectCache\getMultiCheckKeyTime
getMultiCheckKeyTime(array $keys)
Fetch the values of each timestamp "check" key.
Definition: WANObjectCache.php:1170
WANObjectCache\worthRefreshPopular
worthRefreshPopular( $asOf, $ageNew, $timeTillRefresh, $now)
Check if a key is due for randomized regeneration due to its popularity.
Definition: WANObjectCache.php:2847
WANObjectCache\TYPE_TIMESTAMP
const TYPE_TIMESTAMP
Single character component for timestamp check keys.
Definition: WANObjectCache.php:310
WANObjectCache\RAMPUP_TTL
const RAMPUP_TTL
Seconds to ramp up the chance of regeneration due to expected time-till-refresh.
Definition: WANObjectCache.php:237
WANObjectCache\$useInterimHoldOffCaching
bool $useInterimHoldOffCaching
Whether to use "interim" caching while keys are tombstoned.
Definition: WANObjectCache.php:160
WANObjectCache\PC_PRIMARY
const PC_PRIMARY
Default process cache name and max key count.
Definition: WANObjectCache.php:217
WANObjectCache
Multi-datacenter aware caching interface.
Definition: WANObjectCache.php:137
WANObjectCache\PURGE_TIME
const PURGE_TIME
Key to the tombstone entry timestamp.
Definition: WANObjectCache.php:255
WANObjectCache\VERSION
const VERSION
Cache format version number.
Definition: WANObjectCache.php:260
WANObjectCache\makeTombstonePurgeValue
makeTombstonePurgeValue(float $timestamp)
Definition: WANObjectCache.php:3062
WANObjectCache\timeSinceLoggedMiss
timeSinceLoggedMiss( $key, $now)
Definition: WANObjectCache.php:3161
WANObjectCache\getProcessCache
getProcessCache( $group)
Definition: WANObjectCache.php:3084
WANObjectCache\KEY_AS_OF
const KEY_AS_OF
Generation completion timestamp attribute for a key; keep value for b/c (< 1.36)
Definition: WANObjectCache.php:265
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:2512
WANObjectCache\$keyHighUplinkBps
float $keyHighUplinkBps
Max tolerable bytes/second to spend on a cache write stampede for a key.
Definition: WANObjectCache.php:171
Wikimedia\LightweightObjectStore\StorageAwareness\ERR_UNEXPECTED
const ERR_UNEXPECTED
Storage medium operation failed due to usage limitations or an I/O error.
Definition: StorageAwareness.php:40
WANObjectCache\checkAndSetCooloff
checkAndSetCooloff( $key, $kClass, $value, $elapsed, $hasLock)
Check whether set() is rate-limited to avoid concurrent I/O spikes.
Definition: WANObjectCache.php:1952
WANObjectCache\processCheckKeys
processCheckKeys(array $checkSisterKeys, array $wrappedBySisterKey, float $now)
Definition: WANObjectCache.php:745
WANObjectCache\resetCheckKey
resetCheckKey( $key)
Clear the last-purge timestamp of a "check" key in all datacenters.
Definition: WANObjectCache.php:1267
WANObjectCache\TSE_NONE
const TSE_NONE
Idiom for getWithSetCallback() meaning "no cache stampede mutex".
Definition: WANObjectCache.php:204
WANObjectCache\claimStampedeLock
claimStampedeLock( $key)
Definition: WANObjectCache.php:1827
WANObjectCache\makeSisterKeys
makeSisterKeys(array $baseKeys, string $type, string $route=null)
Get sister keys that should be collocated with their corresponding base cache keys.
Definition: WANObjectCache.php:1854
WANObjectCache\parsePurgeValue
parsePurgeValue( $value)
Extract purge metadata from cached value if it is a valid purge value.
Definition: WANObjectCache.php:3034
WANObjectCache\HOT_TTR
const HOT_TTR
Expected time-till-refresh, in seconds, if the key is accessed once per second.
Definition: WANObjectCache.php:199
WANObjectCache\LOCK_TTL
const LOCK_TTL
Seconds to keep lock keys around.
Definition: WANObjectCache.php:233
WANObjectCache\TYPE_MUTEX
const TYPE_MUTEX
Single character component for mutex lock keys.
Definition: WANObjectCache.php:314
Wikimedia\LightweightObjectStore\StorageAwareness\ERR_UNREACHABLE
const ERR_UNREACHABLE
Storage medium could not be reached.
Definition: StorageAwareness.php:38
WANObjectCache\prependRoute
prependRoute(string $sisterKey, string $route)
Definition: WANObjectCache.php:2735
WANObjectCache\yieldStampedeLock
yieldStampedeLock( $key, $hasLock)
Definition: WANObjectCache.php:1837
WANObjectCache\RES_VERSION
const RES_VERSION
Version number attribute for a key.
Definition: WANObjectCache.php:278
WANObjectCache\PURGE_VAL_PREFIX
const PURGE_VAL_PREFIX
Value prefix of purge values.
Definition: WANObjectCache.php:321
WANObjectCache\$asyncHandler
callable null $asyncHandler
Function that takes a WAN cache callback and runs it later.
Definition: WANObjectCache.php:147
WANObjectCache\getWarmupKeyMisses
getWarmupKeyMisses()
Definition: WANObjectCache.php:2673
WANObjectCache\RES_TOMB_AS_OF
const RES_TOMB_AS_OF
Tomstone timestamp attribute for a key.
Definition: WANObjectCache.php:284
$keys
$keys
Definition: testCompression.php:72
WANObjectCache\KEY_CHECK_AS_OF
const KEY_CHECK_AS_OF
Highest "check" key timestamp for a key; keep value for b/c (< 1.36)
Definition: WANObjectCache.php:273
WANObjectCache\$callbackDepth
int $callbackDepth
Callback stack depth for getWithSetCallback()
Definition: WANObjectCache.php:177
WANObjectCache\FLD_FLAGS
const FLD_FLAGS
Key to the flags bit field (reserved number)
Definition: WANObjectCache.php:301
WANObjectCache\$coalesceScheme
int $coalesceScheme
Scheme to use for key coalescing (Hash Tags or Hash Stops)
Definition: WANObjectCache.php:166
WANObjectCache\setInterimValue
setInterimValue( $key, $value, $ttl, $version, $now, $walltime)
Definition: WANObjectCache.php:2040
WANObjectCache\AGE_NEW
const AGE_NEW
Minimum key age, in seconds, for expected time-till-refresh to be considered.
Definition: WANObjectCache.php:201
IStoreKeyEncoder
Generic interface for object stores with key encoding methods.
Definition: IStoreKeyEncoder.php:9
WANObjectCache\setLogger
setLogger(LoggerInterface $logger)
Definition: WANObjectCache.php:399
WANObjectCache\TYPE_VALUE
const TYPE_VALUE
Single character component for value keys.
Definition: WANObjectCache.php:308
WANObjectCache\makeKey
makeKey( $collection,... $components)
Make a cache key using the "global" keyspace for the given components.
Definition: WANObjectCache.php:2391
WANObjectCache\CHECK_KEY_TTL
const CHECK_KEY_TTL
Seconds to keep dependency purge keys around.
Definition: WANObjectCache.php:228
WANObjectCache\GRACE_TTL_NONE
const GRACE_TTL_NONE
Idiom for set()/getWithSetCallback() meaning "no post-expiration grace period".
Definition: WANObjectCache.php:209
WANObjectCache\RECENT_SET_HIGH_MS
const RECENT_SET_HIGH_MS
Max millisecond set() backoff during hold-off (far less than INTERIM_KEY_TTL)
Definition: WANObjectCache.php:247
WANObjectCache\GENERATION_HIGH_SEC
const GENERATION_HIGH_SEC
Consider value generation somewhat high if it takes this many seconds or more.
Definition: WANObjectCache.php:250
WANObjectCache\FLD_TTL
const FLD_TTL
Key to the original TTL; stored in blobs.
Definition: WANObjectCache.php:297
WANObjectCache\isValid
isValid( $value, $asOf, $minAsOf)
Check that a wrapper value exists and has an acceptable age.
Definition: WANObjectCache.php:2921
WANObjectCache\PURGE_HOLDOFF
const PURGE_HOLDOFF
Key to the tombstone entry hold-off TTL.
Definition: WANObjectCache.php:257
WANObjectCache\STALE_TTL_NONE
const STALE_TTL_NONE
Idiom for set()/getWithSetCallback() meaning "no post-expiration persistence".
Definition: WANObjectCache.php:207
WANObjectCache\$stats
StatsdDataFactoryInterface $stats
Definition: WANObjectCache.php:145
WANObjectCache\$processCaches
MapCacheLRU[] $processCaches
Map of group PHP instance caches.
Definition: WANObjectCache.php:141
WANObjectCache\scheduleAsyncRefresh
scheduleAsyncRefresh( $key, $ttl, $callback, array $opts, array $cbParams)
Schedule a deferred cache regeneration if possible.
Definition: WANObjectCache.php:2754
WANObjectCache\$missLog
array< int, array > $missLog
List of (key, UNIX timestamp) tuples for get() cache misses.
Definition: WANObjectCache.php:174
WANObjectCache\COOLOFF_TTL
const COOLOFF_TTL
Seconds to no-op key set() calls to avoid large blob I/O stampedes.
Definition: WANObjectCache.php:235
$type
$type
Definition: testCompression.php:52