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