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 
125 class WANObjectCache implements
129  LoggerAwareInterface
130 {
132  protected $cache;
134  protected $processCaches = [];
136  protected $logger;
138  protected $stats;
140  protected $asyncHandler;
141 
149  protected $broadcastRoute;
151  protected $useInterimHoldOffCaching = true;
153  protected $epoch;
155  protected $secret;
157  protected $coalesceScheme;
158 
160  private $keyHighQps;
162  private $keyHighByteSize;
164  private $keyHighUplinkBps;
165 
167  private $missLog;
168 
170  private $callbackDepth = 0;
172  private $warmupCache = [];
174  private $warmupKeyMisses = 0;
175 
177  private $wallClockOverride;
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 = 2;
224 
226  private const LOCK_TTL = 10;
228  private const COOLOFF_TTL = 2;
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 
350  public function __construct( array $params ) {
351  $this->cache = $params['cache'];
352  $this->broadcastRoute = $params['broadcastRoutingPrefix'] ?? null;
353  $this->epoch = $params['epoch'] ?? 0;
354  $this->secret = $params['secret'] ?? (string)$this->epoch;
355  if ( ( $params['coalesceScheme'] ?? '' ) === 'hash_tag' ) {
356  // https://redis.io/topics/cluster-spec
357  // https://github.com/twitter/twemproxy/blob/v0.4.1/notes/recommendation.md#hash-tags
358  // https://github.com/Netflix/dynomite/blob/v0.7.0/notes/recommendation.md#hash-tags
359  $this->coalesceScheme = self::SCHEME_HASH_TAG;
360  } else {
361  // https://github.com/facebook/mcrouter/wiki/Key-syntax
362  $this->coalesceScheme = self::SCHEME_HASH_STOP;
363  }
364 
365  $this->keyHighQps = $params['keyHighQps'] ?? 100;
366  $this->keyHighByteSize = $params['keyHighByteSize'] ?? ( 128 * 1024 );
367  $this->keyHighUplinkBps = $params['keyHighUplinkBps'] ?? ( 1e9 / 8 / 100 );
368 
369  $this->setLogger( $params['logger'] ?? new NullLogger() );
370  $this->stats = $params['stats'] ?? new NullStatsdDataFactory();
371  $this->asyncHandler = $params['asyncHandler'] ?? null;
372 
373  $this->missLog = array_fill( 0, 10, [ '', 0.0 ] );
374 
375  $this->cache->registerWrapperInfoForStats(
376  'WANCache',
377  'wanobjectcache',
378  [ __CLASS__, 'getCollectionFromSisterKey' ]
379  );
380  }
381 
385  public function setLogger( LoggerInterface $logger ) {
386  $this->logger = $logger;
387  }
388 
394  public static function newEmpty() {
395  return new static( [ 'cache' => new EmptyBagOStuff() ] );
396  }
397 
453  final public function get( $key, &$curTTL = null, array $checkKeys = [], &$info = [] ) {
454  // Note that an undeclared variable passed as $info starts as null (not the default).
455  // Also, if no $info parameter is provided, then it doesn't matter how it changes here.
456  $legacyInfo = ( $info !== self::PASS_BY_REF );
457 
458  $res = $this->fetchKeys( [ $key ], $checkKeys )[$key];
459 
460  $curTTL = $res[self::RES_CUR_TTL];
461  $info = $legacyInfo
462  ? $res[self::RES_AS_OF]
463  : [
464  self::KEY_VERSION => $res[self::RES_VERSION],
465  self::KEY_AS_OF => $res[self::RES_AS_OF],
466  self::KEY_TTL => $res[self::RES_TTL],
467  self::KEY_CUR_TTL => $res[self::RES_CUR_TTL],
468  self::KEY_TOMB_AS_OF => $res[self::RES_TOMB_AS_OF],
469  self::KEY_CHECK_AS_OF => $res[self::RES_CHECK_AS_OF]
470  ];
471 
472  if ( $curTTL === null || $curTTL <= 0 ) {
473  // Log the timestamp in case a corresponding set() call does not provide "walltime"
474  unset( $this->missLog[array_key_first( $this->missLog )] );
475  $this->missLog[] = [ $key, $this->getCurrentTime() ];
476  }
477 
478  return $res[self::RES_VALUE];
479  }
480 
505  final public function getMulti(
506  array $keys,
507  &$curTTLs = [],
508  array $checkKeys = [],
509  &$info = []
510  ) {
511  // Note that an undeclared variable passed as $info starts as null (not the default).
512  // Also, if no $info parameter is provided, then it doesn't matter how it changes here.
513  $legacyInfo = ( $info !== self::PASS_BY_REF );
514 
515  $curTTLs = [];
516  $info = [];
517  $valuesByKey = [];
518 
519  $resByKey = $this->fetchKeys( $keys, $checkKeys );
520  foreach ( $resByKey as $key => $res ) {
521  if ( $res[self::RES_VALUE] !== false ) {
522  $valuesByKey[$key] = $res[self::RES_VALUE];
523  }
524 
525  if ( $res[self::RES_CUR_TTL] !== null ) {
526  $curTTLs[$key] = $res[self::RES_CUR_TTL];
527  }
528  $info[$key] = $legacyInfo
529  ? $res[self::RES_AS_OF]
530  : [
531  self::KEY_VERSION => $res[self::RES_VERSION],
532  self::KEY_AS_OF => $res[self::RES_AS_OF],
533  self::KEY_TTL => $res[self::RES_TTL],
534  self::KEY_CUR_TTL => $res[self::RES_CUR_TTL],
535  self::KEY_TOMB_AS_OF => $res[self::RES_TOMB_AS_OF],
536  self::KEY_CHECK_AS_OF => $res[self::RES_CHECK_AS_OF]
537  ];
538  }
539 
540  return $valuesByKey;
541  }
542 
557  protected function fetchKeys( array $keys, array $checkKeys, $touchedCb = null ) {
558  $resByKey = [];
559 
560  // List of all sister keys that need to be fetched from cache
561  $allSisterKeys = [];
562  // Order-corresponding value sister key list for the base key list ($keys)
563  $valueSisterKeys = [];
564  // List of "check" sister keys to compare all value sister keys against
565  $checkSisterKeysForAll = [];
566  // Map of (base key => additional "check" sister key(s) to compare against)
567  $checkSisterKeysByKey = [];
568 
569  foreach ( $keys as $key ) {
570  $sisterKey = $this->makeSisterKey( $key, self::TYPE_VALUE );
571  $allSisterKeys[] = $sisterKey;
572  $valueSisterKeys[] = $sisterKey;
573  }
574 
575  foreach ( $checkKeys as $i => $checkKeyOrKeyGroup ) {
576  // Note: avoid array_merge() inside loop in case there are many keys
577  if ( is_int( $i ) ) {
578  // Single "check" key that applies to all base keys
579  $sisterKey = $this->makeSisterKey( $checkKeyOrKeyGroup, self::TYPE_TIMESTAMP );
580  $allSisterKeys[] = $sisterKey;
581  $checkSisterKeysForAll[] = $sisterKey;
582  } else {
583  // List of "check" keys that apply to a specific base key
584  foreach ( (array)$checkKeyOrKeyGroup as $checkKey ) {
585  $sisterKey = $this->makeSisterKey( $checkKey, self::TYPE_TIMESTAMP );
586  $allSisterKeys[] = $sisterKey;
587  $checkSisterKeysByKey[$i][] = $sisterKey;
588  }
589  }
590  }
591 
592  if ( $this->warmupCache ) {
593  // Get the wrapped values of the sister keys from the warmup cache
594  $wrappedBySisterKey = $this->warmupCache;
595  $sisterKeysMissing = array_diff( $allSisterKeys, array_keys( $wrappedBySisterKey ) );
596  if ( $sisterKeysMissing ) {
597  $this->warmupKeyMisses += count( $sisterKeysMissing );
598  $wrappedBySisterKey += $this->cache->getMulti( $sisterKeysMissing );
599  }
600  } else {
601  // Fetch the wrapped values of the sister keys from the backend
602  $wrappedBySisterKey = $this->cache->getMulti( $allSisterKeys );
603  }
604 
605  // Pessimistically treat the "current time" as the time when any network I/O finished
606  $now = $this->getCurrentTime();
607 
608  // List of "check" sister key purge timestamps to compare all value sister keys against
609  $ckPurgesForAll = $this->processCheckKeys(
610  $checkSisterKeysForAll,
611  $wrappedBySisterKey,
612  $now
613  );
614  // Map of (base key => extra "check" sister key purge timestamp(s) to compare against)
615  $ckPurgesByKey = [];
616  foreach ( $checkSisterKeysByKey as $keyWithCheckKeys => $checkKeysForKey ) {
617  $ckPurgesByKey[$keyWithCheckKeys] = $this->processCheckKeys(
618  $checkKeysForKey,
619  $wrappedBySisterKey,
620  $now
621  );
622  }
623 
624  // Unwrap and validate any value found for each base key (under the value sister key)
625  reset( $keys );
626  foreach ( $valueSisterKeys as $valueSisterKey ) {
627  // Get the corresponding base key for this value sister key
628  $key = current( $keys );
629  next( $keys );
630 
631  if ( array_key_exists( $valueSisterKey, $wrappedBySisterKey ) ) {
632  // Key exists as either a live value or tombstone value
633  $wrapped = $wrappedBySisterKey[$valueSisterKey];
634  } else {
635  // Key does not exist
636  $wrapped = false;
637  }
638 
639  $res = $this->unwrap( $wrapped, $now );
640  $value = $res[self::RES_VALUE];
641 
642  foreach ( array_merge( $ckPurgesForAll, $ckPurgesByKey[$key] ?? [] ) as $ckPurge ) {
643  $res[self::RES_CHECK_AS_OF] = max(
644  $ckPurge[self::PURGE_TIME],
645  $res[self::RES_CHECK_AS_OF]
646  );
647  // Timestamp marking the end of the hold-off period for this purge
648  $holdoffDeadline = $ckPurge[self::PURGE_TIME] + $ckPurge[self::PURGE_HOLDOFF];
649  // Check if the value was generated during the hold-off period
650  if ( $value !== false && $holdoffDeadline >= $res[self::RES_AS_OF] ) {
651  // How long ago this value was purged by *this* "check" key
652  $ago = min( $ckPurge[self::PURGE_TIME] - $now, self::TINY_NEGATIVE );
653  // How long ago this value was purged by *any* known "check" key
654  $res[self::RES_CUR_TTL] = min( $res[self::RES_CUR_TTL], $ago );
655  }
656  }
657 
658  if ( $touchedCb !== null && $value !== false ) {
659  $touched = $touchedCb( $value );
660  if ( $touched !== null && $touched >= $res[self::RES_AS_OF] ) {
661  $res[self::RES_CUR_TTL] = min(
662  $res[self::RES_CUR_TTL],
663  $res[self::RES_AS_OF] - $touched,
664  self::TINY_NEGATIVE
665  );
666  }
667  } else {
668  $touched = null;
669  }
670 
671  $res[self::RES_TOUCH_AS_OF] = max( $res[self::RES_TOUCH_AS_OF], $touched );
672 
673  $resByKey[$key] = $res;
674  }
675 
676  return $resByKey;
677  }
678 
685  private function processCheckKeys(
686  array $checkSisterKeys,
687  array $wrappedBySisterKey,
688  float $now
689  ) {
690  $purges = [];
691 
692  foreach ( $checkSisterKeys as $timeKey ) {
693  $purge = isset( $wrappedBySisterKey[$timeKey] )
694  ? $this->parsePurgeValue( $wrappedBySisterKey[$timeKey] )
695  : null;
696 
697  if ( $purge === null ) {
698  $wrapped = $this->makeCheckPurgeValue( $now, self::HOLDOFF_TTL, $purge );
699  $this->cache->add(
700  $timeKey,
701  $wrapped,
702  self::CHECK_KEY_TTL,
703  $this->cache::WRITE_BACKGROUND
704  );
705  }
706 
707  $purges[] = $purge;
708  }
709 
710  return $purges;
711  }
712 
796  final public function set( $key, $value, $ttl = self::TTL_INDEFINITE, array $opts = [] ) {
797  $kClass = $this->determineKeyClassForStats( $key );
798 
799  $ok = $this->setMainValue(
800  $key,
801  $value,
802  $ttl,
803  $opts['version'] ?? null,
804  $opts['walltime'] ?? null,
805  $opts['lag'] ?? 0,
806  $opts['since'] ?? null,
807  $opts['pending'] ?? false,
808  $opts['lockTSE'] ?? self::TSE_NONE,
809  $opts['staleTTL'] ?? self::STALE_TTL_NONE,
810  $opts['segmentable'] ?? false,
811  $opts['creating'] ?? false
812  );
813 
814  $this->stats->increment( "wanobjectcache.$kClass.set." . ( $ok ? 'ok' : 'error' ) );
815 
816  return $ok;
817  }
818 
834  private function setMainValue(
835  $key,
836  $value,
837  $ttl,
838  ?int $version,
839  ?float $walltime,
840  $dataReplicaLag,
841  $dataReadSince,
842  bool $dataPendingCommit,
843  int $lockTSE,
844  int $staleTTL,
845  bool $segmentable,
846  bool $creating
847  ) {
848  if ( $ttl < 0 ) {
849  // not cacheable
850  return true;
851  }
852 
853  $now = $this->getCurrentTime();
854  $ttl = (int)$ttl;
855  $walltime = $walltime ?? $this->timeSinceLoggedMiss( $key, $now );
856  $dataSnapshotLag = ( $dataReadSince !== null ) ? max( 0, $now - $dataReadSince ) : 0;
857  $dataCombinedLag = $dataReplicaLag + $dataSnapshotLag;
858 
859  // Forbid caching data that only exists within an uncommitted transaction. Also, lower
860  // the TTL when the data has a "since" time so far in the past that a delete() tombstone,
861  // made after that time, could have already expired (the key is no longer write-holed).
862  // The mitigation TTL depends on whether this data lag is assumed to systemically effect
863  // regeneration attempts in the near future. The TTL also reflects regeneration wall time.
864  if ( $dataPendingCommit ) {
865  // Case A: data comes from an uncommitted write transaction
866  $mitigated = 'pending writes';
867  // Data might never be committed; rely on a less problematic regeneration attempt
868  $mitigationTTL = self::TTL_UNCACHEABLE;
869  } elseif ( $dataSnapshotLag > self::MAX_READ_LAG ) {
870  // Case B: high snapshot lag
871  $pregenSnapshotLag = ( $walltime !== null ) ? ( $dataSnapshotLag - $walltime ) : 0;
872  if ( ( $pregenSnapshotLag + self::GENERATION_HIGH_SEC ) > self::MAX_READ_LAG ) {
873  // Case B1: generation started when transaction duration was already long
874  $mitigated = 'snapshot lag (late generation)';
875  // Probably non-systemic; rely on a less problematic regeneration attempt
876  $mitigationTTL = self::TTL_UNCACHEABLE;
877  } else {
878  // Case B2: slow generation made transaction duration long
879  $mitigated = 'snapshot lag (high generation time)';
880  // Probably systemic; use a low TTL to avoid stampedes/uncacheability
881  $mitigationTTL = self::LOW_TTL;
882  }
883  } elseif ( $dataReplicaLag === false || $dataReplicaLag > self::MAX_READ_LAG ) {
884  // Case C: low/medium snapshot lag with high replication lag
885  $mitigated = 'replication lag';
886  // Probably systemic; use a low TTL to avoid stampedes/uncacheability
887  $mitigationTTL = self::TTL_LAGGED;
888  } elseif ( $dataCombinedLag > self::MAX_READ_LAG ) {
889  $pregenCombinedLag = ( $walltime !== null ) ? ( $dataCombinedLag - $walltime ) : 0;
890  // Case D: medium snapshot lag with medium replication lag
891  if ( ( $pregenCombinedLag + self::GENERATION_HIGH_SEC ) > self::MAX_READ_LAG ) {
892  // Case D1: generation started when read lag was too high
893  $mitigated = 'read lag (late generation)';
894  // Probably non-systemic; rely on a less problematic regeneration attempt
895  $mitigationTTL = self::TTL_UNCACHEABLE;
896  } else {
897  // Case D2: slow generation made read lag too high
898  $mitigated = 'read lag (high generation time)';
899  // Probably systemic; use a low TTL to avoid stampedes/uncacheability
900  $mitigationTTL = self::LOW_TTL;
901  }
902  } else {
903  // Case E: new value generated with recent data
904  $mitigated = null;
905  // Nothing to mitigate
906  $mitigationTTL = null;
907  }
908 
909  if ( $mitigationTTL === self::TTL_UNCACHEABLE ) {
910  $this->logger->warning(
911  "Rejected set() for {cachekey} due to $mitigated.",
912  [
913  'cachekey' => $key,
914  'lag' => $dataReplicaLag,
915  'age' => $dataSnapshotLag,
916  'walltime' => $walltime
917  ]
918  );
919 
920  // no-op the write for being unsafe
921  return true;
922  }
923 
924  // TTL to use in staleness checks (does not effect persistence layer TTL)
925  $logicalTTL = null;
926 
927  if ( $mitigationTTL !== null ) {
928  // New value was generated from data that is old enough to be risky
929  if ( $lockTSE >= 0 ) {
930  // Persist the value as long as normal, but make it count as stale sooner
931  $logicalTTL = min( $ttl ?: INF, $mitigationTTL );
932  } else {
933  // Persist the value for a shorter duration
934  $ttl = min( $ttl ?: INF, $mitigationTTL );
935  }
936 
937  $this->logger->warning(
938  "Lowered set() TTL for {cachekey} due to $mitigated.",
939  [
940  'cachekey' => $key,
941  'lag' => $dataReplicaLag,
942  'age' => $dataSnapshotLag,
943  'walltime' => $walltime
944  ]
945  );
946  }
947 
948  // Wrap that value with time/TTL/version metadata
949  $wrapped = $this->wrap( $value, $logicalTTL ?: $ttl, $version, $now, $walltime );
950  $storeTTL = $ttl + $staleTTL;
951 
952  $flags = $this->cache::WRITE_BACKGROUND;
953  if ( $segmentable ) {
954  $flags |= $this->cache::WRITE_ALLOW_SEGMENTS;
955  }
956 
957  if ( $creating ) {
958  $ok = $this->cache->add(
959  $this->makeSisterKey( $key, self::TYPE_VALUE ),
960  $wrapped,
961  $storeTTL,
962  $flags
963  );
964  } else {
965  $ok = $this->cache->merge(
966  $this->makeSisterKey( $key, self::TYPE_VALUE ),
967  static function ( $cache, $key, $cWrapped ) use ( $wrapped ) {
968  // A string value means that it is a tombstone; do nothing in that case
969  return ( is_string( $cWrapped ) ) ? false : $wrapped;
970  },
971  $storeTTL,
972  $this->cache::MAX_CONFLICTS_ONE,
973  $flags
974  );
975  }
976 
977  return $ok;
978  }
979 
1042  final public function delete( $key, $ttl = self::HOLDOFF_TTL ) {
1043  // Purge values must be stored under the value key so that WANObjectCache::set()
1044  // can atomically merge values without accidentally undoing a recent purge and thus
1045  // violating the holdoff TTL restriction.
1046  $valueSisterKey = $this->makeSisterKey( $key, self::TYPE_VALUE );
1047 
1048  if ( $ttl <= 0 ) {
1049  // A client or cache cleanup script is requesting a cache purge, so there is no
1050  // volatility period due to replica DB lag. Any recent change to an entity cached
1051  // in this key should have triggered an appropriate purge event.
1052  $ok = $this->relayNonVolatilePurge( $valueSisterKey );
1053  } else {
1054  // A cacheable entity recently changed, so there might be a volatility period due
1055  // to replica DB lag. Clients usually expect their actions to be reflected in any
1056  // of their subsequent web request. This is attainable if (a) purge relay lag is
1057  // lower than the time it takes for subsequent request by the client to arrive,
1058  // and, (b) DB replica queries have "read-your-writes" consistency due to DB lag
1059  // mitigation systems.
1060  $now = $this->getCurrentTime();
1061  // Set the key to the purge value in all datacenters
1062  $purge = $this->makeTombstonePurgeValue( $now );
1063  $ok = $this->relayVolatilePurge( $valueSisterKey, $purge, $ttl );
1064  }
1065 
1066  $kClass = $this->determineKeyClassForStats( $key );
1067  $this->stats->increment( "wanobjectcache.$kClass.delete." . ( $ok ? 'ok' : 'error' ) );
1068 
1069  return $ok;
1070  }
1071 
1091  final public function getCheckKeyTime( $key ) {
1092  return $this->getMultiCheckKeyTime( [ $key ] )[$key];
1093  }
1094 
1156  final public function getMultiCheckKeyTime( array $keys ) {
1157  $checkSisterKeysByKey = [];
1158  foreach ( $keys as $key ) {
1159  $checkSisterKeysByKey[$key] = $this->makeSisterKey( $key, self::TYPE_TIMESTAMP );
1160  }
1161 
1162  $wrappedBySisterKey = $this->cache->getMulti( $checkSisterKeysByKey );
1163  $wrappedBySisterKey += array_fill_keys( $checkSisterKeysByKey, false );
1164 
1165  $now = $this->getCurrentTime();
1166  $times = [];
1167  foreach ( $checkSisterKeysByKey as $key => $checkSisterKey ) {
1168  $purge = $this->parsePurgeValue( $wrappedBySisterKey[$checkSisterKey] );
1169  if ( $purge === null ) {
1170  $wrapped = $this->makeCheckPurgeValue( $now, self::HOLDOFF_TTL, $purge );
1171  $this->cache->add(
1172  $checkSisterKey,
1173  $wrapped,
1174  self::CHECK_KEY_TTL,
1175  $this->cache::WRITE_BACKGROUND
1176  );
1177  }
1178 
1179  $times[$key] = $purge[self::PURGE_TIME];
1180  }
1181 
1182  return $times;
1183  }
1184 
1218  final public function touchCheckKey( $key, $holdoff = self::HOLDOFF_TTL ) {
1219  $checkSisterKey = $this->makeSisterKey( $key, self::TYPE_TIMESTAMP );
1220 
1221  $now = $this->getCurrentTime();
1222  $purge = $this->makeCheckPurgeValue( $now, $holdoff );
1223  $ok = $this->relayVolatilePurge( $checkSisterKey, $purge, self::CHECK_KEY_TTL );
1224 
1225  $kClass = $this->determineKeyClassForStats( $key );
1226  $this->stats->increment( "wanobjectcache.$kClass.ck_touch." . ( $ok ? 'ok' : 'error' ) );
1227 
1228  return $ok;
1229  }
1230 
1258  final public function resetCheckKey( $key ) {
1259  $checkSisterKey = $this->makeSisterKey( $key, self::TYPE_TIMESTAMP );
1260  $ok = $this->relayNonVolatilePurge( $checkSisterKey );
1261 
1262  $kClass = $this->determineKeyClassForStats( $key );
1263  $this->stats->increment( "wanobjectcache.$kClass.ck_reset." . ( $ok ? 'ok' : 'error' ) );
1264 
1265  return $ok;
1266  }
1267 
1571  final public function getWithSetCallback(
1572  $key, $ttl, $callback, array $opts = [], array $cbParams = []
1573  ) {
1574  $version = $opts['version'] ?? null;
1575  $pcTTL = $opts['pcTTL'] ?? self::TTL_UNCACHEABLE;
1576  $pCache = ( $pcTTL >= 0 )
1577  ? $this->getProcessCache( $opts['pcGroup'] ?? self::PC_PRIMARY )
1578  : null;
1579 
1580  // Use the process cache if requested as long as no outer cache callback is running.
1581  // Nested callback process cache use is not lag-safe with regard to HOLDOFF_TTL since
1582  // process cached values are more lagged than persistent ones as they are not purged.
1583  if ( $pCache && $this->callbackDepth == 0 ) {
1584  $cached = $pCache->get( $key, $pcTTL, false );
1585  if ( $cached !== false ) {
1586  $this->logger->debug( "getWithSetCallback($key): process cache hit" );
1587  return $cached;
1588  }
1589  }
1590 
1591  [ $value, $valueVersion, $curAsOf ] = $this->fetchOrRegenerate( $key, $ttl, $callback, $opts, $cbParams );
1592  if ( $valueVersion !== $version ) {
1593  // Current value has a different version; use the variant key for this version.
1594  // Regenerate the variant value if it is not newer than the main value at $key
1595  // so that purges to the main key propagate to the variant value.
1596  $this->logger->debug( "getWithSetCallback($key): using variant key" );
1597  [ $value ] = $this->fetchOrRegenerate(
1598  $this->makeGlobalKey( 'WANCache-key-variant', md5( $key ), (string)$version ),
1599  $ttl,
1600  $callback,
1601  [ 'version' => null, 'minAsOf' => $curAsOf ] + $opts,
1602  $cbParams
1603  );
1604  }
1605 
1606  // Update the process cache if enabled
1607  if ( $pCache && $value !== false ) {
1608  $pCache->set( $key, $value );
1609  }
1610 
1611  return $value;
1612  }
1613 
1630  private function fetchOrRegenerate( $key, $ttl, $callback, array $opts, array $cbParams ) {
1631  $checkKeys = $opts['checkKeys'] ?? [];
1632  $graceTTL = $opts['graceTTL'] ?? self::GRACE_TTL_NONE;
1633  $minAsOf = $opts['minAsOf'] ?? self::MIN_TIMESTAMP_NONE;
1634  $hotTTR = $opts['hotTTR'] ?? self::HOT_TTR;
1635  $lowTTL = $opts['lowTTL'] ?? min( self::LOW_TTL, $ttl );
1636  $ageNew = $opts['ageNew'] ?? self::AGE_NEW;
1637  $touchedCb = $opts['touchedCallback'] ?? null;
1638  $startTime = $this->getCurrentTime();
1639 
1640  $kClass = $this->determineKeyClassForStats( $key );
1641 
1642  // Get the current key value and its metadata
1643  $curState = $this->fetchKeys( [ $key ], $checkKeys, $touchedCb )[$key];
1644  $curValue = $curState[self::RES_VALUE];
1645  // Use the cached value if it exists and is not due for synchronous regeneration
1646  if ( $this->isAcceptablyFreshValue( $curState, $graceTTL, $minAsOf ) ) {
1647  if ( !$this->isLotteryRefreshDue( $curState, $lowTTL, $ageNew, $hotTTR, $startTime ) ) {
1648  $this->stats->timing(
1649  "wanobjectcache.$kClass.hit.good",
1650  1e3 * ( $this->getCurrentTime() - $startTime )
1651  );
1652 
1653  return [ $curValue, $curState[self::RES_VERSION], $curState[self::RES_AS_OF] ];
1654  } elseif ( $this->scheduleAsyncRefresh( $key, $ttl, $callback, $opts, $cbParams ) ) {
1655  $this->logger->debug( "fetchOrRegenerate($key): hit with async refresh" );
1656  $this->stats->timing(
1657  "wanobjectcache.$kClass.hit.refresh",
1658  1e3 * ( $this->getCurrentTime() - $startTime )
1659  );
1660 
1661  return [ $curValue, $curState[self::RES_VERSION], $curState[self::RES_AS_OF] ];
1662  } else {
1663  $this->logger->debug( "fetchOrRegenerate($key): hit with sync refresh" );
1664  }
1665  }
1666 
1667  $isKeyTombstoned = ( $curState[self::RES_TOMB_AS_OF] !== null );
1668  // Use the interim key as an temporary alternative if the key is tombstoned
1669  if ( $isKeyTombstoned ) {
1670  $volState = $this->getInterimValue( $key, $minAsOf, $startTime, $touchedCb );
1671  $volValue = $volState[self::RES_VALUE];
1672  } else {
1673  $volState = $curState;
1674  $volValue = $curValue;
1675  }
1676 
1677  // During the volatile "hold-off" period that follows a purge of the key, the value
1678  // will be regenerated many times if frequently accessed. This is done to mitigate
1679  // the effects of backend replication lag as soon as possible. However, throttle the
1680  // overhead of locking and regeneration by reusing values recently written to cache
1681  // tens of milliseconds ago. Verify the "as of" time against the last purge event.
1682  $lastPurgeTime = max(
1683  // RES_TOUCH_AS_OF depends on the value (possibly from the interim key)
1684  $volState[self::RES_TOUCH_AS_OF],
1685  $curState[self::RES_TOMB_AS_OF],
1686  $curState[self::RES_CHECK_AS_OF]
1687  );
1688  $safeMinAsOf = max( $minAsOf, $lastPurgeTime + self::TINY_POSTIVE );
1689  if ( $this->isExtremelyNewValue( $volState, $safeMinAsOf, $startTime ) ) {
1690  $this->logger->debug( "fetchOrRegenerate($key): volatile hit" );
1691  $this->stats->timing(
1692  "wanobjectcache.$kClass.hit.volatile",
1693  1e3 * ( $this->getCurrentTime() - $startTime )
1694  );
1695 
1696  return [ $volValue, $volState[self::RES_VERSION], $curState[self::RES_AS_OF] ];
1697  }
1698 
1699  $lockTSE = $opts['lockTSE'] ?? self::TSE_NONE;
1700  $busyValue = $opts['busyValue'] ?? null;
1701  $staleTTL = $opts['staleTTL'] ?? self::STALE_TTL_NONE;
1702  $segmentable = $opts['segmentable'] ?? false;
1703  $version = $opts['version'] ?? null;
1704 
1705  // Determine whether one thread per datacenter should handle regeneration at a time
1706  $useRegenerationLock =
1707  // Note that since tombstones no-op set(), $lockTSE and $curTTL cannot be used to
1708  // deduce the key hotness because |$curTTL| will always keep increasing until the
1709  // tombstone expires or is overwritten by a new tombstone. Also, even if $lockTSE
1710  // is not set, constant regeneration of a key for the tombstone lifetime might be
1711  // very expensive. Assume tombstoned keys are possibly hot in order to reduce
1712  // the risk of high regeneration load after the delete() method is called.
1713  $isKeyTombstoned ||
1714  // Assume a key is hot if requested soon ($lockTSE seconds) after purge.
1715  // This avoids stampedes when timestamps from $checkKeys/$touchedCb bump.
1716  (
1717  $curState[self::RES_CUR_TTL] !== null &&
1718  $curState[self::RES_CUR_TTL] <= 0 &&
1719  abs( $curState[self::RES_CUR_TTL] ) <= $lockTSE
1720  ) ||
1721  // Assume a key is hot if there is no value and a busy fallback is given.
1722  // This avoids stampedes on eviction or preemptive regeneration taking too long.
1723  ( $busyValue !== null && $volValue === false );
1724 
1725  // If a regeneration lock is required, threads that do not get the lock will try to use
1726  // the stale value, the interim value, or the $busyValue placeholder, in that order. If
1727  // none of those are set then all threads will bypass the lock and regenerate the value.
1728  $hasLock = $useRegenerationLock && $this->claimStampedeLock( $key );
1729  if ( $useRegenerationLock && !$hasLock ) {
1730  // Determine if there is stale or volatile cached value that is still usable
1731  // @phan-suppress-next-line PhanTypeMismatchArgumentNullable False positive
1732  if ( $this->isValid( $volValue, $volState[self::RES_AS_OF], $minAsOf ) ) {
1733  $this->logger->debug( "fetchOrRegenerate($key): returning stale value" );
1734  $this->stats->timing(
1735  "wanobjectcache.$kClass.hit.stale",
1736  1e3 * ( $this->getCurrentTime() - $startTime )
1737  );
1738 
1739  return [ $volValue, $volState[self::RES_VERSION], $curState[self::RES_AS_OF] ];
1740  } elseif ( $busyValue !== null ) {
1741  $miss = is_infinite( $minAsOf ) ? 'renew' : 'miss';
1742  $this->logger->debug( "fetchOrRegenerate($key): busy $miss" );
1743  $this->stats->timing(
1744  "wanobjectcache.$kClass.$miss.busy",
1745  1e3 * ( $this->getCurrentTime() - $startTime )
1746  );
1747  $placeholderValue = $this->resolveBusyValue( $busyValue );
1748 
1749  return [ $placeholderValue, $version, $curState[self::RES_AS_OF] ];
1750  }
1751  }
1752 
1753  // Generate the new value given any prior value with a matching version
1754  $setOpts = [];
1755  $preCallbackTime = $this->getCurrentTime();
1756  ++$this->callbackDepth;
1757  // https://github.com/phan/phan/issues/4419
1758  $value = null;
1759  try {
1760  $value = $callback(
1761  ( $curState[self::RES_VERSION] === $version ) ? $curValue : false,
1762  $ttl,
1763  $setOpts,
1764  ( $curState[self::RES_VERSION] === $version ) ? $curState[self::RES_AS_OF] : null,
1765  $cbParams
1766  );
1767  } finally {
1768  --$this->callbackDepth;
1769  }
1770  $postCallbackTime = $this->getCurrentTime();
1771 
1772  // How long it took to fetch, validate, and generate the value
1773  $elapsed = max( $postCallbackTime - $startTime, 0.0 );
1774 
1775  // How long it took to generate the value
1776  $walltime = max( $postCallbackTime - $preCallbackTime, 0.0 );
1777  $this->stats->timing( "wanobjectcache.$kClass.regen_walltime", 1e3 * $walltime );
1778 
1779  // Attempt to save the newly generated value if applicable
1780  if (
1781  // Callback yielded a cacheable value
1782  ( $value !== false && $ttl >= 0 ) &&
1783  // Current thread was not raced out of a regeneration lock or key is tombstoned
1784  ( !$useRegenerationLock || $hasLock || $isKeyTombstoned ) &&
1785  // Key does not appear to be undergoing a set() stampede
1786  $this->checkAndSetCooloff( $key, $kClass, $value, $elapsed, $hasLock )
1787  ) {
1788  // If the key is write-holed then use the (volatile) interim key as an alternative
1789  if ( $isKeyTombstoned ) {
1790  $this->setInterimValue(
1791  $key,
1792  $value,
1793  $lockTSE,
1794  $version,
1795  $walltime,
1796  $segmentable
1797  );
1798  } else {
1799  $this->setMainValue(
1800  $key,
1801  $value,
1802  $ttl,
1803  $version,
1804  $walltime,
1805  // @phan-suppress-next-line PhanCoalescingAlwaysNull
1806  $setOpts['lag'] ?? 0,
1807  // @phan-suppress-next-line PhanCoalescingAlwaysNull
1808  $setOpts['since'] ?? $preCallbackTime,
1809  // @phan-suppress-next-line PhanCoalescingAlwaysNull
1810  $setOpts['pending'] ?? false,
1811  $lockTSE,
1812  $staleTTL,
1813  $segmentable,
1814  ( $curValue === false )
1815  );
1816  }
1817  }
1818 
1819  $this->yieldStampedeLock( $key, $hasLock );
1820 
1821  $miss = is_infinite( $minAsOf ) ? 'renew' : 'miss';
1822  $this->logger->debug( "fetchOrRegenerate($key): $miss, new value computed" );
1823  $this->stats->timing(
1824  "wanobjectcache.$kClass.$miss.compute",
1825  1e3 * ( $this->getCurrentTime() - $startTime )
1826  );
1827 
1828  return [ $value, $version, $curState[self::RES_AS_OF] ];
1829  }
1830 
1835  private function claimStampedeLock( $key ) {
1836  $checkSisterKey = $this->makeSisterKey( $key, self::TYPE_MUTEX );
1837  // Note that locking is not bypassed due to I/O errors; this avoids stampedes
1838  return $this->cache->add( $checkSisterKey, 1, self::LOCK_TTL );
1839  }
1840 
1845  private function yieldStampedeLock( $key, $hasLock ) {
1846  if ( $hasLock ) {
1847  $checkSisterKey = $this->makeSisterKey( $key, self::TYPE_MUTEX );
1848  $this->cache->delete( $checkSisterKey, $this->cache::WRITE_BACKGROUND );
1849  }
1850  }
1851 
1862  private function makeSisterKeys( array $baseKeys, string $type, string $route = null ) {
1863  $sisterKeys = [];
1864  foreach ( $baseKeys as $baseKey ) {
1865  $sisterKeys[] = $this->makeSisterKey( $baseKey, $type, $route );
1866  }
1867 
1868  return $sisterKeys;
1869  }
1870 
1881  private function makeSisterKey( string $baseKey, string $typeChar, string $route = null ) {
1882  if ( $this->coalesceScheme === self::SCHEME_HASH_STOP ) {
1883  // Key style: "WANCache:<base key>|#|<character>"
1884  $sisterKey = 'WANCache:' . $baseKey . '|#|' . $typeChar;
1885  } else {
1886  // Key style: "WANCache:{<base key>}:<character>"
1887  $sisterKey = 'WANCache:{' . $baseKey . '}:' . $typeChar;
1888  }
1889 
1890  if ( $route !== null ) {
1891  $sisterKey = $this->prependRoute( $sisterKey, $route );
1892  }
1893 
1894  return $sisterKey;
1895  }
1896 
1903  public static function getCollectionFromSisterKey( string $sisterKey ) {
1904  if ( substr( $sisterKey, -4 ) === '|#|v' ) {
1905  // Key style: "WANCache:<base key>|#|<character>"
1906  $collection = substr( $sisterKey, 9, strcspn( $sisterKey, ':|', 9 ) );
1907  } elseif ( substr( $sisterKey, -3 ) === '}:v' ) {
1908  // Key style: "WANCache:{<base key>}:<character>"
1909  $collection = substr( $sisterKey, 10, strcspn( $sisterKey, ':}', 10 ) );
1910  } else {
1911  $collection = 'internal';
1912  }
1913 
1914  return $collection;
1915  }
1916 
1929  private function isExtremelyNewValue( $res, $minAsOf, $now ) {
1930  if ( $res[self::RES_VALUE] === false || $res[self::RES_AS_OF] < $minAsOf ) {
1931  return false;
1932  }
1933 
1934  $age = $now - $res[self::RES_AS_OF];
1935 
1936  return ( $age < mt_rand( self::RECENT_SET_LOW_MS, self::RECENT_SET_HIGH_MS ) / 1e3 );
1937  }
1938 
1960  private function checkAndSetCooloff( $key, $kClass, $value, $elapsed, $hasLock ) {
1961  if ( is_scalar( $value ) ) {
1962  // Roughly estimate the size of the value once serialized
1963  $hypotheticalSize = strlen( (string)$value );
1964  } else {
1965  // Treat the value is a generic sizable object
1966  $hypotheticalSize = $this->keyHighByteSize;
1967  }
1968 
1969  if ( !$hasLock ) {
1970  // Suppose that this cache key is very popular (KEY_HIGH_QPS reads/second).
1971  // After eviction, there will be cache misses until it gets regenerated and saved.
1972  // If the time window when the key is missing lasts less than one second, then the
1973  // number of misses will not reach KEY_HIGH_QPS. This window largely corresponds to
1974  // the key regeneration time. Estimate the count/rate of cache misses, e.g.:
1975  // - 100 QPS, 20ms regeneration => ~2 misses (< 1s)
1976  // - 100 QPS, 100ms regeneration => ~10 misses (< 1s)
1977  // - 100 QPS, 3000ms regeneration => ~300 misses (100/s for 3s)
1978  $missesPerSecForHighQPS = ( min( $elapsed, 1 ) * $this->keyHighQps );
1979 
1980  // Determine whether there is enough I/O stampede risk to justify throttling set().
1981  // Estimate unthrottled set() overhead, as bps, from miss count/rate and value size,
1982  // comparing it to the per-key uplink bps limit (KEY_HIGH_UPLINK_BPS), e.g.:
1983  // - 2 misses (< 1s), 10KB value, 1250000 bps limit => 160000 bits (low risk)
1984  // - 2 misses (< 1s), 100KB value, 1250000 bps limit => 1600000 bits (high risk)
1985  // - 10 misses (< 1s), 10KB value, 1250000 bps limit => 800000 bits (low risk)
1986  // - 10 misses (< 1s), 100KB value, 1250000 bps limit => 8000000 bits (high risk)
1987  // - 300 misses (100/s), 1KB value, 1250000 bps limit => 800000 bps (low risk)
1988  // - 300 misses (100/s), 10KB value, 1250000 bps limit => 8000000 bps (high risk)
1989  // - 300 misses (100/s), 100KB value, 1250000 bps limit => 80000000 bps (high risk)
1990  if ( ( $missesPerSecForHighQPS * $hypotheticalSize ) >= $this->keyHighUplinkBps ) {
1991  $cooloffSisterKey = $this->makeSisterKey( $key, self::TYPE_COOLOFF );
1992  $watchPoint = $this->cache->watchErrors();
1993  if (
1994  !$this->cache->add( $cooloffSisterKey, 1, self::COOLOFF_TTL ) &&
1995  // Don't treat failures due to I/O errors as the key being in cool-off
1996  $this->cache->getLastError( $watchPoint ) === self::ERR_NONE
1997  ) {
1998  $this->logger->debug( "checkAndSetCooloff($key): bounced; ${elapsed}s" );
1999  $this->stats->increment( "wanobjectcache.$kClass.cooloff_bounce" );
2000 
2001  return false;
2002  }
2003  }
2004  }
2005 
2006  // Corresponding metrics for cache writes that actually get sent over the write
2007  $this->stats->timing( "wanobjectcache.$kClass.regen_set_delay", 1e3 * $elapsed );
2008  $this->stats->updateCount( "wanobjectcache.$kClass.regen_set_bytes", $hypotheticalSize );
2009 
2010  return true;
2011  }
2012 
2022  private function getInterimValue( $key, $minAsOf, $now, $touchedCb ) {
2023  if ( $this->useInterimHoldOffCaching ) {
2024  $interimSisterKey = $this->makeSisterKey( $key, self::TYPE_INTERIM );
2025  $wrapped = $this->cache->get( $interimSisterKey );
2026  $res = $this->unwrap( $wrapped, $now );
2027  if ( $res[self::RES_VALUE] !== false && $res[self::RES_AS_OF] >= $minAsOf ) {
2028  if ( $touchedCb !== null ) {
2029  // Update "last purge time" since the $touchedCb timestamp depends on $value
2030  // Get the new "touched timestamp", accounting for callback-checked dependencies
2031  $res[self::RES_TOUCH_AS_OF] = max(
2032  $touchedCb( $res[self::RES_VALUE] ),
2033  $res[self::RES_TOUCH_AS_OF]
2034  );
2035  }
2036 
2037  return $res;
2038  }
2039  }
2040 
2041  return $this->unwrap( false, $now );
2042  }
2043 
2053  private function setInterimValue(
2054  $key,
2055  $value,
2056  $ttl,
2057  ?int $version,
2058  float $walltime,
2059  bool $segmentable
2060  ) {
2061  $now = $this->getCurrentTime();
2062  $ttl = max( self::INTERIM_KEY_TTL, (int)$ttl );
2063 
2064  // Wrap that value with time/TTL/version metadata
2065  $wrapped = $this->wrap( $value, $ttl, $version, $now, $walltime );
2066 
2067  $flags = $this->cache::WRITE_BACKGROUND;
2068  if ( $segmentable ) {
2069  $flags |= $this->cache::WRITE_ALLOW_SEGMENTS;
2070  }
2071 
2072  return $this->cache->set(
2073  $this->makeSisterKey( $key, self::TYPE_INTERIM ),
2074  $wrapped,
2075  $ttl,
2076  $flags
2077  );
2078  }
2079 
2084  private function resolveBusyValue( $busyValue ) {
2085  return ( $busyValue instanceof Closure ) ? $busyValue() : $busyValue;
2086  }
2087 
2153  final public function getMultiWithSetCallback(
2154  ArrayIterator $keyedIds, $ttl, callable $callback, array $opts = []
2155  ) {
2156  // Batch load required keys into the in-process warmup cache
2157  $this->warmupCache = $this->fetchWrappedValuesForWarmupCache(
2158  $this->getNonProcessCachedMultiKeys( $keyedIds, $opts ),
2159  $opts['checkKeys'] ?? []
2160  );
2161  $this->warmupKeyMisses = 0;
2162 
2163  // The required callback signature includes $id as the first argument for convenience
2164  // to distinguish different items. To reuse the code in getWithSetCallback(), wrap the
2165  // callback with a proxy callback that has the standard getWithSetCallback() signature.
2166  // This is defined only once per batch to avoid closure creation overhead.
2167  $proxyCb = static function ( $oldValue, &$ttl, &$setOpts, $oldAsOf, $params )
2168  use ( $callback )
2169  {
2170  return $callback( $params['id'], $oldValue, $ttl, $setOpts, $oldAsOf );
2171  };
2172 
2173  // Get the order-preserved result map using the warm-up cache
2174  $values = [];
2175  foreach ( $keyedIds as $key => $id ) {
2176  $values[$key] = $this->getWithSetCallback(
2177  $key,
2178  $ttl,
2179  $proxyCb,
2180  $opts,
2181  [ 'id' => $id ]
2182  );
2183  }
2184 
2185  $this->warmupCache = [];
2186 
2187  return $values;
2188  }
2189 
2256  final public function getMultiWithUnionSetCallback(
2257  ArrayIterator $keyedIds, $ttl, callable $callback, array $opts = []
2258  ) {
2259  $checkKeys = $opts['checkKeys'] ?? [];
2260  $minAsOf = $opts['minAsOf'] ?? self::MIN_TIMESTAMP_NONE;
2261 
2262  // unset incompatible keys
2263  unset( $opts['lockTSE'] );
2264  unset( $opts['busyValue'] );
2265 
2266  // Batch load required keys into the in-process warmup cache
2267  $keysByIdGet = $this->getNonProcessCachedMultiKeys( $keyedIds, $opts );
2268  $this->warmupCache = $this->fetchWrappedValuesForWarmupCache( $keysByIdGet, $checkKeys );
2269  $this->warmupKeyMisses = 0;
2270 
2271  // IDs of entities known to be in need of generation
2272  $idsRegen = [];
2273 
2274  // Find out which keys are missing/deleted/stale
2275  $resByKey = $this->fetchKeys( $keysByIdGet, $checkKeys );
2276  foreach ( $keysByIdGet as $id => $key ) {
2277  $res = $resByKey[$key];
2278  if (
2279  $res[self::RES_VALUE] === false ||
2280  $res[self::RES_CUR_TTL] < 0 ||
2281  $res[self::RES_AS_OF] < $minAsOf
2282  ) {
2283  $idsRegen[] = $id;
2284  }
2285  }
2286 
2287  // Run the callback to populate the generation value map for all required IDs
2288  $newSetOpts = [];
2289  $newTTLsById = array_fill_keys( $idsRegen, $ttl );
2290  $newValsById = $idsRegen ? $callback( $idsRegen, $newTTLsById, $newSetOpts ) : [];
2291 
2292  $method = __METHOD__;
2293  // The required callback signature includes $id as the first argument for convenience
2294  // to distinguish different items. To reuse the code in getWithSetCallback(), wrap the
2295  // callback with a proxy callback that has the standard getWithSetCallback() signature.
2296  // This is defined only once per batch to avoid closure creation overhead.
2297  $proxyCb = function ( $oldValue, &$ttl, &$setOpts, $oldAsOf, $params )
2298  use ( $callback, $newValsById, $newTTLsById, $newSetOpts, $method )
2299  {
2300  $id = $params['id'];
2301 
2302  if ( array_key_exists( $id, $newValsById ) ) {
2303  // Value was already regenerated as expected, so use the value in $newValsById
2304  $newValue = $newValsById[$id];
2305  $ttl = $newTTLsById[$id];
2306  $setOpts = $newSetOpts;
2307  } else {
2308  // Pre-emptive/popularity refresh and version mismatch cases are not detected
2309  // above and thus $newValsById has no entry. Run $callback on this single entity.
2310  $ttls = [ $id => $ttl ];
2311  $result = $callback( [ $id ], $ttls, $setOpts );
2312  if ( !isset( $result[$id] ) ) {
2313  // T303092
2314  $this->logger->warning(
2315  $method . ' failed due to {id} not set in result {result}', [
2316  'id' => $id,
2317  'result' => json_encode( $result )
2318  ] );
2319  }
2320  $newValue = $result[$id];
2321  $ttl = $ttls[$id];
2322  }
2323 
2324  return $newValue;
2325  };
2326 
2327  // Get the order-preserved result map using the warm-up cache
2328  $values = [];
2329  foreach ( $keyedIds as $key => $id ) {
2330  $values[$key] = $this->getWithSetCallback(
2331  $key,
2332  $ttl,
2333  $proxyCb,
2334  $opts,
2335  [ 'id' => $id ]
2336  );
2337  }
2338 
2339  $this->warmupCache = [];
2340 
2341  return $values;
2342  }
2343 
2354  public function makeGlobalKey( $collection, ...$components ) {
2355  // @phan-suppress-next-line PhanParamTooFewUnpack Should infer non-emptiness
2356  return $this->cache->makeGlobalKey( ...func_get_args() );
2357  }
2358 
2369  public function makeKey( $collection, ...$components ) {
2370  // @phan-suppress-next-line PhanParamTooFewUnpack Should infer non-emptiness
2371  return $this->cache->makeKey( ...func_get_args() );
2372  }
2373 
2381  public function hash256( $component ) {
2382  return hash_hmac( 'sha256', $component, $this->secret );
2383  }
2384 
2436  final public function makeMultiKeys( array $ids, $keyCallback ) {
2437  $idByKey = [];
2438  foreach ( $ids as $id ) {
2439  // Discourage triggering of automatic makeKey() hashing in some backends
2440  if ( strlen( $id ) > 64 ) {
2441  $this->logger->warning( __METHOD__ . ": long ID '$id'; use hash256()" );
2442  }
2443  $key = $keyCallback( $id, $this );
2444  // Edge case: ignore key collisions due to duplicate $ids like "42" and 42
2445  if ( !isset( $idByKey[$key] ) ) {
2446  $idByKey[$key] = $id;
2447  } elseif ( (string)$id !== (string)$idByKey[$key] ) {
2448  throw new UnexpectedValueException(
2449  "Cache key collision; IDs ('$id','{$idByKey[$key]}') map to '$key'"
2450  );
2451  }
2452  }
2453 
2454  return new ArrayIterator( $idByKey );
2455  }
2456 
2492  final public function multiRemap( array $ids, array $res ) {
2493  if ( count( $ids ) !== count( $res ) ) {
2494  // If makeMultiKeys() is called on a list of non-unique IDs, then the resulting
2495  // ArrayIterator will have less entries due to "first appearance" de-duplication
2496  $ids = array_keys( array_fill_keys( $ids, true ) );
2497  if ( count( $ids ) !== count( $res ) ) {
2498  throw new UnexpectedValueException( "Multi-key result does not match ID list" );
2499  }
2500  }
2501 
2502  return array_combine( $ids, $res );
2503  }
2504 
2511  public function watchErrors() {
2512  return $this->cache->watchErrors();
2513  }
2514 
2532  final public function getLastError( $watchPoint = 0 ) {
2533  $code = $this->cache->getLastError( $watchPoint );
2534  switch ( $code ) {
2535  case self::ERR_NONE:
2536  return self::ERR_NONE;
2537  case self::ERR_NO_RESPONSE:
2538  return self::ERR_NO_RESPONSE;
2539  case self::ERR_UNREACHABLE:
2540  return self::ERR_UNREACHABLE;
2541  default:
2542  return self::ERR_UNEXPECTED;
2543  }
2544  }
2545 
2550  final public function clearLastError() {
2551  $this->cache->clearLastError();
2552  }
2553 
2559  public function clearProcessCache() {
2560  $this->processCaches = [];
2561  }
2562 
2583  final public function useInterimHoldOffCaching( $enabled ) {
2584  $this->useInterimHoldOffCaching = $enabled;
2585  }
2586 
2592  public function getQoS( $flag ) {
2593  return $this->cache->getQoS( $flag );
2594  }
2595 
2659  public function adaptiveTTL( $mtime, $maxTTL, $minTTL = 30, $factor = 0.2 ) {
2660  // handle fractional seconds and string integers
2661  $mtime = (int)$mtime;
2662  if ( $mtime <= 0 ) {
2663  // no last-modified time provided
2664  return $minTTL;
2665  }
2666 
2667  $age = (int)$this->getCurrentTime() - $mtime;
2668 
2669  return (int)min( $maxTTL, max( $minTTL, $factor * $age ) );
2670  }
2671 
2677  final public function getWarmupKeyMisses() {
2678  // Number of misses in $this->warmupCache during the last call to certain methods
2679  return $this->warmupKeyMisses;
2680  }
2681 
2696  protected function relayVolatilePurge( string $sisterKey, string $purgeValue, int $ttl ) {
2697  if ( $this->broadcastRoute !== null ) {
2698  $routeKey = $this->prependRoute( $sisterKey, $this->broadcastRoute );
2699  } else {
2700  $routeKey = $sisterKey;
2701  }
2702 
2703  return $this->cache->set(
2704  $routeKey,
2705  $purgeValue,
2706  $ttl,
2707  $this->cache::WRITE_BACKGROUND
2708  );
2709  }
2710 
2719  protected function relayNonVolatilePurge( string $sisterKey ) {
2720  if ( $this->broadcastRoute !== null ) {
2721  $routeKey = $this->prependRoute( $sisterKey, $this->broadcastRoute );
2722  } else {
2723  $routeKey = $sisterKey;
2724  }
2725 
2726  return $this->cache->delete( $routeKey, $this->cache::WRITE_BACKGROUND );
2727  }
2728 
2734  protected function prependRoute( string $sisterKey, string $route ) {
2735  if ( $sisterKey[0] === '/' ) {
2736  throw new RuntimeException( "Sister key '$sisterKey' already contains a route." );
2737  }
2738 
2739  return $route . $sisterKey;
2740  }
2741 
2753  private function scheduleAsyncRefresh( $key, $ttl, $callback, array $opts, array $cbParams ) {
2754  if ( !$this->asyncHandler ) {
2755  return false;
2756  }
2757  // Update the cache value later, such during post-send of an HTTP request. This forces
2758  // cache regeneration by setting "minAsOf" to infinity, meaning that no existing value
2759  // is considered valid. Furthermore, note that preemptive regeneration is not applicable
2760  // to invalid values, so there is no risk of infinite preemptive regeneration loops.
2761  $func = $this->asyncHandler;
2762  $func( function () use ( $key, $ttl, $callback, $opts, $cbParams ) {
2763  $opts['minAsOf'] = INF;
2764  try {
2765  $this->fetchOrRegenerate( $key, $ttl, $callback, $opts, $cbParams );
2766  } catch ( Exception $e ) {
2767  // Log some context for easier debugging
2768  $this->logger->error( 'Async refresh failed for {key}', [
2769  'key' => $key,
2770  'ttl' => $ttl,
2771  'exception' => $e
2772  ] );
2773  throw $e;
2774  }
2775  } );
2776 
2777  return true;
2778  }
2779 
2788  private function isAcceptablyFreshValue( $res, $graceTTL, $minAsOf ) {
2789  if ( !$this->isValid( $res[self::RES_VALUE], $res[self::RES_AS_OF], $minAsOf ) ) {
2790  // Value does not exists or is too old
2791  return false;
2792  }
2793 
2794  $curTTL = $res[self::RES_CUR_TTL];
2795  if ( $curTTL > 0 ) {
2796  // Value is definitely still fresh
2797  return true;
2798  }
2799 
2800  // Remaining seconds during which this stale value can be used
2801  $curGraceTTL = $graceTTL + $curTTL;
2802 
2803  return ( $curGraceTTL > 0 )
2804  // Chance of using the value decreases as $curTTL goes from 0 to -$graceTTL
2805  ? !$this->worthRefreshExpiring( $curGraceTTL, $graceTTL, $graceTTL )
2806  // Value is too stale to fall in the grace period
2807  : false;
2808  }
2809 
2820  protected function isLotteryRefreshDue( $res, $lowTTL, $ageNew, $hotTTR, $now ) {
2821  $curTTL = $res[self::RES_CUR_TTL];
2822  $logicalTTL = $res[self::RES_TTL];
2823  $asOf = $res[self::RES_AS_OF];
2824 
2825  return (
2826  $this->worthRefreshExpiring( $curTTL, $logicalTTL, $lowTTL ) ||
2827  $this->worthRefreshPopular( $asOf, $ageNew, $hotTTR, $now )
2828  );
2829  }
2830 
2846  protected function worthRefreshPopular( $asOf, $ageNew, $timeTillRefresh, $now ) {
2847  if ( $ageNew < 0 || $timeTillRefresh <= 0 ) {
2848  return false;
2849  }
2850 
2851  $age = $now - $asOf;
2852  $timeOld = $age - $ageNew;
2853  if ( $timeOld <= 0 ) {
2854  return false;
2855  }
2856 
2857  $popularHitsPerSec = 1;
2858  // Lifecycle is: new, ramp-up refresh chance, full refresh chance.
2859  // Note that the "expected # of refreshes" for the ramp-up time range is half
2860  // of what it would be if P(refresh) was at its full value during that time range.
2861  $refreshWindowSec = max( $timeTillRefresh - $ageNew - self::RAMPUP_TTL / 2, 1 );
2862  // P(refresh) * (# hits in $refreshWindowSec) = (expected # of refreshes)
2863  // P(refresh) * ($refreshWindowSec * $popularHitsPerSec) = 1 (by definition)
2864  // P(refresh) = 1/($refreshWindowSec * $popularHitsPerSec)
2865  $chance = 1 / ( $popularHitsPerSec * $refreshWindowSec );
2866  // Ramp up $chance from 0 to its nominal value over RAMPUP_TTL seconds to avoid stampedes
2867  $chance *= ( $timeOld <= self::RAMPUP_TTL ) ? $timeOld / self::RAMPUP_TTL : 1;
2868 
2869  return ( mt_rand( 1, 1000000000 ) <= 1000000000 * $chance );
2870  }
2871 
2890  protected function worthRefreshExpiring( $curTTL, $logicalTTL, $lowTTL ) {
2891  if ( $lowTTL <= 0 ) {
2892  return false;
2893  }
2894 
2895  // T264787: avoid having keys start off with a high chance of being refreshed;
2896  // the point where refreshing becomes possible cannot precede the key lifetime.
2897  $effectiveLowTTL = min( $lowTTL, $logicalTTL ?: INF );
2898 
2899  if ( $curTTL >= $effectiveLowTTL || $curTTL <= 0 ) {
2900  return false;
2901  }
2902 
2903  $chance = ( 1 - $curTTL / $effectiveLowTTL );
2904 
2905  return ( mt_rand( 1, 1000000000 ) <= 1000000000 * $chance );
2906  }
2907 
2916  protected function isValid( $value, $asOf, $minAsOf ) {
2917  return ( $value !== false && $asOf >= $minAsOf );
2918  }
2919 
2928  private function wrap( $value, $ttl, $version, $now, $walltime ) {
2929  // Returns keys in ascending integer order for PHP7 array packing:
2930  // https://nikic.github.io/2014/12/22/PHPs-new-hashtable-implementation.html
2931  $wrapped = [
2932  self::FLD_FORMAT_VERSION => self::VERSION,
2933  self::FLD_VALUE => $value,
2934  self::FLD_TTL => $ttl,
2935  self::FLD_TIME => $now
2936  ];
2937  if ( $version !== null ) {
2938  $wrapped[self::FLD_VALUE_VERSION] = $version;
2939  }
2940  if ( $walltime >= self::GENERATION_SLOW_SEC ) {
2941  $wrapped[self::FLD_GENERATION_TIME] = $walltime;
2942  }
2943 
2944  return $wrapped;
2945  }
2946 
2961  private function unwrap( $wrapped, $now ) {
2962  // https://nikic.github.io/2014/12/22/PHPs-new-hashtable-implementation.html
2963  $res = [
2964  // Attributes that only depend on the fetched key value
2965  self::RES_VALUE => false,
2966  self::RES_VERSION => null,
2967  self::RES_AS_OF => null,
2968  self::RES_TTL => null,
2969  self::RES_TOMB_AS_OF => null,
2970  // Attributes that depend on caller-specific "check" keys or "touched callbacks"
2971  self::RES_CHECK_AS_OF => null,
2972  self::RES_TOUCH_AS_OF => null,
2973  self::RES_CUR_TTL => null
2974  ];
2975 
2976  if ( is_array( $wrapped ) ) {
2977  // Entry expected to be a cached value; validate it
2978  if (
2979  ( $wrapped[self::FLD_FORMAT_VERSION] ?? null ) === self::VERSION &&
2980  $wrapped[self::FLD_TIME] >= $this->epoch
2981  ) {
2982  if ( $wrapped[self::FLD_TTL] > 0 ) {
2983  // Get the approximate time left on the key
2984  $age = $now - $wrapped[self::FLD_TIME];
2985  $curTTL = max( $wrapped[self::FLD_TTL] - $age, 0.0 );
2986  } else {
2987  // Key had no TTL, so the time left is unbounded
2988  $curTTL = INF;
2989  }
2990  $res[self::RES_VALUE] = $wrapped[self::FLD_VALUE];
2991  $res[self::RES_VERSION] = $wrapped[self::FLD_VALUE_VERSION] ?? null;
2992  $res[self::RES_AS_OF] = $wrapped[self::FLD_TIME];
2993  $res[self::RES_CUR_TTL] = $curTTL;
2994  $res[self::RES_TTL] = $wrapped[self::FLD_TTL];
2995  }
2996  } else {
2997  // Entry expected to be a tombstone; parse it
2998  $purge = $this->parsePurgeValue( $wrapped );
2999  if ( $purge !== null ) {
3000  // Tombstoned keys should always have a negative "current TTL"
3001  $curTTL = min( $purge[self::PURGE_TIME] - $now, self::TINY_NEGATIVE );
3002  $res[self::RES_CUR_TTL] = $curTTL;
3003  $res[self::RES_TOMB_AS_OF] = $purge[self::PURGE_TIME];
3004  }
3005  }
3006 
3007  return $res;
3008  }
3009 
3014  private function determineKeyClassForStats( $key ) {
3015  $parts = explode( ':', $key, 3 );
3016  // Fallback in case the key was not made by makeKey.
3017  // Replace dots because they are special in StatsD (T232907)
3018  return strtr( $parts[1] ?? $parts[0], '.', '_' );
3019  }
3020 
3029  private function parsePurgeValue( $value ) {
3030  if ( !is_string( $value ) ) {
3031  return null;
3032  }
3033 
3034  $segments = explode( ':', $value, 3 );
3035  $prefix = $segments[0];
3036  if ( $prefix !== self::PURGE_VAL_PREFIX ) {
3037  // Not a purge value
3038  return null;
3039  }
3040 
3041  $timestamp = (float)$segments[1];
3042  // makeTombstonePurgeValue() doesn't store hold-off TTLs
3043  $holdoff = isset( $segments[2] ) ? (int)$segments[2] : self::HOLDOFF_TTL;
3044 
3045  if ( $timestamp < $this->epoch ) {
3046  // Purge value is too old
3047  return null;
3048  }
3049 
3050  return [ self::PURGE_TIME => $timestamp, self::PURGE_HOLDOFF => $holdoff ];
3051  }
3052 
3057  private function makeTombstonePurgeValue( float $timestamp ) {
3058  return self::PURGE_VAL_PREFIX . ':' . (int)$timestamp;
3059  }
3060 
3067  private function makeCheckPurgeValue( float $timestamp, int $holdoff, array &$purge = null ) {
3068  $normalizedTime = (int)$timestamp;
3069  // Purge array that matches what parsePurgeValue() would have returned
3070  $purge = [ self::PURGE_TIME => (float)$normalizedTime, self::PURGE_HOLDOFF => $holdoff ];
3071 
3072  return self::PURGE_VAL_PREFIX . ":$normalizedTime:$holdoff";
3073  }
3074 
3079  private function getProcessCache( $group ) {
3080  if ( !isset( $this->processCaches[$group] ) ) {
3081  [ , $size ] = explode( ':', $group );
3082  $this->processCaches[$group] = new MapCacheLRU( (int)$size );
3083  if ( $this->wallClockOverride !== null ) {
3084  $this->processCaches[$group]->setMockTime( $this->wallClockOverride );
3085  }
3086  }
3087 
3088  return $this->processCaches[$group];
3089  }
3090 
3096  private function getNonProcessCachedMultiKeys( ArrayIterator $keys, array $opts ) {
3097  $pcTTL = $opts['pcTTL'] ?? self::TTL_UNCACHEABLE;
3098 
3099  $keysMissing = [];
3100  if ( $pcTTL > 0 && $this->callbackDepth == 0 ) {
3101  $pCache = $this->getProcessCache( $opts['pcGroup'] ?? self::PC_PRIMARY );
3102  foreach ( $keys as $key => $id ) {
3103  if ( !$pCache->has( $key, $pcTTL ) ) {
3104  $keysMissing[$id] = $key;
3105  }
3106  }
3107  }
3108 
3109  return $keysMissing;
3110  }
3111 
3118  private function fetchWrappedValuesForWarmupCache( array $keys, array $checkKeys ) {
3119  if ( !$keys ) {
3120  return [];
3121  }
3122 
3123  // Get all the value keys to fetch...
3124  $sisterKeys = $this->makeSisterKeys( $keys, self::TYPE_VALUE );
3125  // Get all the "check" keys to fetch...
3126  foreach ( $checkKeys as $i => $checkKeyOrKeyGroup ) {
3127  // Note: avoid array_merge() inside loop in case there are many keys
3128  if ( is_int( $i ) ) {
3129  // Single "check" key that applies to all value keys
3130  $sisterKeys[] = $this->makeSisterKey( $checkKeyOrKeyGroup, self::TYPE_TIMESTAMP );
3131  } else {
3132  // List of "check" keys that apply to a specific value key
3133  foreach ( (array)$checkKeyOrKeyGroup as $checkKey ) {
3134  $sisterKeys[] = $this->makeSisterKey( $checkKey, self::TYPE_TIMESTAMP );
3135  }
3136  }
3137  }
3138 
3139  $wrappedBySisterKey = $this->cache->getMulti( $sisterKeys );
3140  $wrappedBySisterKey += array_fill_keys( $sisterKeys, false );
3141 
3142  return $wrappedBySisterKey;
3143  }
3144 
3150  private function timeSinceLoggedMiss( $key, $now ) {
3151  for ( end( $this->missLog ); $miss = current( $this->missLog ); prev( $this->missLog ) ) {
3152  if ( $miss[0] === $key ) {
3153  return ( $now - $miss[1] );
3154  }
3155  }
3156 
3157  return null;
3158  }
3159 
3164  protected function getCurrentTime() {
3165  return $this->wallClockOverride ?: microtime( true );
3166  }
3167 
3172  public function setMockTime( &$time ) {
3173  $this->wallClockOverride =& $time;
3174  $this->cache->setMockTime( $time );
3175  foreach ( $this->processCaches as $pCache ) {
3176  $pCache->setMockTime( $time );
3177  }
3178  }
3179 }
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.
const HOLDOFF_TTL
Seconds to tombstone keys on delete() and to treat keys as volatile after purges.
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.
worthRefreshPopular( $asOf, $ageNew, $timeTillRefresh, $now)
Check if a key is due for randomized regeneration due to its popularity.
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...
relayVolatilePurge(string $sisterKey, string $purgeValue, int $ttl)
Set a sister key to a purge value in all datacenters.
prependRoute(string $sisterKey, string $route)
touchCheckKey( $key, $holdoff=self::HOLDOFF_TTL)
Increase the last-purge timestamp of a "check" key in all datacenters.
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".
BagOStuff $cache
The local datacenter cache.
fetchKeys(array $keys, array $checkKeys, $touchedCb=null)
Fetch the value and key metadata of several keys from cache.
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.
getMulti(array $keys, &$curTTLs=[], array $checkKeys=[], &$info=[])
Fetch the value of several keys from cache.
getMultiWithUnionSetCallback(ArrayIterator $keyedIds, $ttl, callable $callback, array $opts=[])
Method to fetch/regenerate multiple cache keys at once.
LoggerInterface $logger
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.
makeMultiKeys(array $ids, $keyCallback)
Get an iterator of (cache key => entity ID) for a list of entity IDs.
static newEmpty()
Get an instance that wraps EmptyBagOStuff.
int $coalesceScheme
Scheme to use for key coalescing (Hash Tags or Hash Stops)
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 HOLDOFF_TTL_NONE
Idiom for delete()/touchCheckKey() meaning "no hold-off period".
useInterimHoldOffCaching( $enabled)
Enable or disable the use of brief caching for tombstoned keys.
StatsdDataFactoryInterface $stats
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)
getLastError( $watchPoint=0)
Get the "last error" registry.
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.
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.
setLogger(LoggerInterface $logger)
static getCollectionFromSisterKey(string $sisterKey)
const PASS_BY_REF
Idiom for get()/getMulti() to return extra information by reference.
const KEY_CHECK_AS_OF
Highest "check" key timestamp for a key; keep value for b/c (< 1.36)
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.
string $secret
Stable secret used for hashing long strings into key components.
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)
const KEY_CUR_TTL
Remaining TTL attribute for a key; keep value for b/c (< 1.36)
const TTL_LAGGED
Max TTL, in seconds, to store keys when a data source has high replication lag.
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.
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.