MediaWiki REL1_37
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
123class WANObjectCache implements
127 LoggerAwareInterface
128{
130 protected $cache;
132 protected $processCaches = [];
134 protected $logger;
136 protected $stats;
138 protected $asyncHandler;
139
149 protected $onHostRoute;
153 protected $epoch;
155 protected $secret;
158
160 private $keyHighQps;
163
165 private $callbackDepth = 0;
167 private $warmupCache = [];
169 private $warmupKeyMisses = 0;
170
173
175 private const MAX_COMMIT_DELAY = 3;
177 private const MAX_READ_LAG = 7;
179 public const HOLDOFF_TTL = self::MAX_COMMIT_DELAY + self::MAX_READ_LAG + 1;
180
182 private const LOW_TTL = 30;
184 public const TTL_LAGGED = 30;
185
187 private const HOT_TTR = 900;
189 private const AGE_NEW = 60;
190
192 private const TSE_NONE = -1;
193
195 public const STALE_TTL_NONE = 0;
197 public const GRACE_TTL_NONE = 0;
199 public const HOLDOFF_TTL_NONE = 0;
200
202 public const MIN_TIMESTAMP_NONE = 0.0;
203
205 private const PC_PRIMARY = 'primary:1000';
206
208 public const PASS_BY_REF = [];
209
211 private const SCHEME_HASH_TAG = 1;
213 private const SCHEME_HASH_STOP = 2;
214
216 private const CHECK_KEY_TTL = self::TTL_YEAR;
218 private const INTERIM_KEY_TTL = 1;
219
221 private const LOCK_TTL = 10;
223 private const COOLOFF_TTL = 1;
225 private const RAMPUP_TTL = 30;
226
228 private const TINY_NEGATIVE = -0.000001;
230 private const TINY_POSTIVE = 0.000001;
231
233 private const RECENT_SET_LOW_MS = 50;
235 private const RECENT_SET_HIGH_MS = 100;
236
238 private const GENERATION_SLOW_SEC = 3;
239
241 private const PURGE_TIME = 0;
243 private const PURGE_HOLDOFF = 1;
244
246 private const VERSION = 1;
247
249 private const RES_VALUE = 0;
251 private const RES_METADATA = 1;
252
254 public const KEY_VERSION = 'version';
256 public const KEY_AS_OF = 'asOf';
258 public const KEY_TTL = 'ttl';
260 public const KEY_CUR_TTL = 'curTTL';
262 public const KEY_TOMB_AS_OF = 'tombAsOf';
264 public const KEY_CHECK_AS_OF = 'lastCKPurge';
265
267 private const FLD_FORMAT_VERSION = 0;
269 private const FLD_VALUE = 1;
271 private const FLD_TTL = 2;
273 private const FLD_TIME = 3;
275 private const FLD_FLAGS = 4;
277 private const FLD_VALUE_VERSION = 5;
279 private const FLD_GENERATION_TIME = 6;
280
282 private const TYPE_VALUE = 'v';
284 private const TYPE_TIMESTAMP = 't';
286 private const TYPE_FLUX = 'f';
288 private const TYPE_MUTEX = 'm';
290 private const TYPE_INTERIM = 'i';
292 private const TYPE_COOLOFF = 'c';
293
295 private const PURGE_VAL_PREFIX = 'PURGED:';
296
340 public function __construct( array $params ) {
341 $this->cache = $params['cache'];
342 $this->broadcastRoute = $params['broadcastRoutingPrefix'] ?? null;
343 $this->onHostRoute = $params['onHostRoutingPrefix'] ?? null;
344 $this->epoch = $params['epoch'] ?? 0;
345 $this->secret = $params['secret'] ?? (string)$this->epoch;
346 if ( ( $params['coalesceScheme'] ?? '' ) === 'hash_tag' ) {
347 // https://redis.io/topics/cluster-spec
348 // https://github.com/twitter/twemproxy/blob/v0.4.1/notes/recommendation.md#hash-tags
349 // https://github.com/Netflix/dynomite/blob/v0.7.0/notes/recommendation.md#hash-tags
350 $this->coalesceScheme = self::SCHEME_HASH_TAG;
351 } else {
352 // https://github.com/facebook/mcrouter/wiki/Key-syntax
353 $this->coalesceScheme = self::SCHEME_HASH_STOP;
354 }
355
356 $this->keyHighQps = $params['keyHighQps'] ?? 100;
357 $this->keyHighUplinkBps = $params['keyHighUplinkBps'] ?? ( 1e9 / 8 / 100 );
358
359 $this->setLogger( $params['logger'] ?? new NullLogger() );
360 $this->stats = $params['stats'] ?? new NullStatsdDataFactory();
361 $this->asyncHandler = $params['asyncHandler'] ?? null;
362
363 $this->cache->registerWrapperInfoForStats(
364 'WANCache',
365 'wanobjectcache',
366 [ __CLASS__, 'getCollectionFromSisterKey' ]
367 );
368 }
369
373 public function setLogger( LoggerInterface $logger ) {
374 $this->logger = $logger;
375 }
376
382 public static function newEmpty() {
383 return new static( [ 'cache' => new EmptyBagOStuff() ] );
384 }
385
441 final public function get( $key, &$curTTL = null, array $checkKeys = [], &$info = [] ) {
442 // Note that an undeclared variable passed as $info starts as null (not the default).
443 // Also, if no $info parameter is provided, then it doesn't matter how it changes here.
444 $legacyInfo = ( $info !== self::PASS_BY_REF );
445
446 $res = $this->fetchKeys( [ $key ], $checkKeys )[$key];
447 $value = $res[self::RES_VALUE];
448 $metadata = $res[self::RES_METADATA];
449
450 $curTTL = $metadata[self::KEY_CUR_TTL];
451 $info = $legacyInfo ? $metadata[self::KEY_AS_OF] : $metadata;
452
453 return $value;
454 }
455
480 final public function getMulti(
481 array $keys,
482 &$curTTLs = [],
483 array $checkKeys = [],
484 &$info = []
485 ) {
486 // Note that an undeclared variable passed as $info starts as null (not the default).
487 // Also, if no $info parameter is provided, then it doesn't matter how it changes here.
488 $legacyInfo = ( $info !== self::PASS_BY_REF );
489
490 $curTTLs = [];
491 $info = [];
492 $valuesByKey = [];
493
494 $resByKey = $this->fetchKeys( $keys, $checkKeys );
495 foreach ( $resByKey as $key => $res ) {
496 $value = $res[self::RES_VALUE];
497 $metadata = $res[self::RES_METADATA];
498
499 if ( $value !== false ) {
500 $valuesByKey[$key] = $value;
501 }
502
503 if ( $metadata[self::KEY_CUR_TTL] !== null ) {
504 $curTTLs[$key] = $metadata[self::KEY_CUR_TTL];
505 }
506
507 $info[$key] = $legacyInfo ? $metadata[self::KEY_AS_OF] : $metadata;
508 }
509
510 return $valuesByKey;
511 }
512
530 protected function fetchKeys( array $keys, array $checkKeys ) {
531 $resByKey = [];
532
533 // List of all sister keys that need to be fetched from cache
534 $allSisterKeys = [];
535 // Order-corresponding value sister key list for the base key list ($keys)
536 $valueSisterKeys = [];
537 // Order-corresponding "flux" sister key list for the base key list ($keys) or []
538 $fluxSisterKeys = [];
539 // List of "check" sister keys to compare all value sister keys against
540 $checkSisterKeysForAll = [];
541 // Map of (base key => additional "check" sister key(s) to compare against)
542 $checkSisterKeysByKey = [];
543
544 foreach ( $keys as $key ) {
545 $sisterKey = $this->makeSisterKey( $key, self::TYPE_VALUE, $this->onHostRoute );
546 $allSisterKeys[] = $sisterKey;
547 $valueSisterKeys[] = $sisterKey;
548 if ( $this->onHostRoute !== null ) {
549 $sisterKey = $this->makeSisterKey( $key, self::TYPE_FLUX );
550 $allSisterKeys[] = $sisterKey;
551 $fluxSisterKeys[] = $sisterKey;
552 }
553 }
554
555 foreach ( $checkKeys as $i => $checkKeyOrKeyGroup ) {
556 // Note: avoid array_merge() inside loop in case there are many keys
557 if ( is_int( $i ) ) {
558 // Single "check" key that applies to all base keys
559 $sisterKey = $this->makeSisterKey( $checkKeyOrKeyGroup, self::TYPE_TIMESTAMP );
560 $allSisterKeys[] = $sisterKey;
561 $checkSisterKeysForAll[] = $sisterKey;
562 } else {
563 // List of "check" keys that apply to a specific base key
564 foreach ( (array)$checkKeyOrKeyGroup as $checkKey ) {
565 $sisterKey = $this->makeSisterKey( $checkKey, self::TYPE_TIMESTAMP );
566 $allSisterKeys[] = $sisterKey;
567 $checkSisterKeysByKey[$i][] = $sisterKey;
568 }
569 }
570 }
571
572 if ( $this->warmupCache ) {
573 // Get the wrapped values of the sister keys from the warmup cache
574 $wrappedBySisterKey = $this->warmupCache;
575 $sisterKeysMissing = array_diff( $allSisterKeys, array_keys( $wrappedBySisterKey ) );
576 if ( $sisterKeysMissing ) { // sanity
577 $this->warmupKeyMisses += count( $sisterKeysMissing );
578 $wrappedBySisterKey += $this->cache->getMulti( $sisterKeysMissing );
579 }
580 } else {
581 // Fetch the wrapped values of the sister keys from the backend
582 $wrappedBySisterKey = $this->cache->getMulti( $allSisterKeys );
583 }
584
585 // Pessimistically treat the "current time" as the time when any network I/O finished
586 $now = $this->getCurrentTime();
587
588 // List of "check" sister key purge timestamps to compare all value sister keys against
589 $ckPurgesForAll = $this->processCheckKeys(
590 $checkSisterKeysForAll,
591 $wrappedBySisterKey,
592 $now
593 );
594 // Map of (base key => extra "check" sister key purge timestamp(s) to compare against)
595 $ckPurgesByKey = [];
596 foreach ( $checkSisterKeysByKey as $keyWithCheckKeys => $checkKeysForKey ) {
597 $ckPurgesByKey[$keyWithCheckKeys] = $this->processCheckKeys(
598 $checkKeysForKey,
599 $wrappedBySisterKey,
600 $now
601 );
602 }
603
604 // Map of (base key => "flux" key purge timestamp to compare against)
605 $fkPurgesByKey = $this->processFluxKeys( $keys, $fluxSisterKeys, $wrappedBySisterKey );
606
607 // Unwrap and validate any value found for each base key (under the value sister key)
608 reset( $keys );
609 foreach ( $valueSisterKeys as $valueSisterKey ) {
610 // Get the corresponding base key for this value sister key
611 $key = current( $keys );
612 next( $keys );
613
614 if ( isset( $fkPurgesByKey[$key] ) ) {
615 // An on-host tier is in use and a "flux" sister key exists for this
616 // Treat the value sister key as if it was a tombstone with this value.
617 $wrapped = $fkPurgesByKey[$key];
618 } elseif ( array_key_exists( $valueSisterKey, $wrappedBySisterKey ) ) {
619 // Key exists as either a live value or tombstone value
620 $wrapped = $wrappedBySisterKey[$valueSisterKey];
621 } else {
622 // Key does not exist
623 $wrapped = false;
624 }
625
626 list( $value, $metadata ) = $this->unwrap( $wrapped, $now );
627 // Include the timestamp of the newest "check" key purge/initialization
628 $metadata[self::KEY_CHECK_AS_OF] = null;
629
630 foreach ( array_merge( $ckPurgesForAll, $ckPurgesByKey[$key] ?? [] ) as $ckPurge ) {
631 $metadata[self::KEY_CHECK_AS_OF] = max(
632 $ckPurge[self::PURGE_TIME],
633 $metadata[self::KEY_CHECK_AS_OF]
634 );
635 // Timestamp marking the end of the hold-off period for this purge
636 $holdoffDeadline = $ckPurge[self::PURGE_TIME] + $ckPurge[self::PURGE_HOLDOFF];
637 // Check if the value was generated during the hold-off period
638 if ( $value !== false && $holdoffDeadline >= $metadata[self::KEY_AS_OF] ) {
639 // How long ago this value was invalidated by *this* "check" key
640 $ago = min( $ckPurge[self::PURGE_TIME] - $now, self::TINY_NEGATIVE );
641 // How long ago this value was invalidated by *any* known "check" key
642 $metadata[self::KEY_CUR_TTL] = min( $metadata[self::KEY_CUR_TTL], $ago );
643 }
644 }
645
646 $resByKey[$key] = [ self::RES_VALUE => $value, self::RES_METADATA => $metadata ];
647 }
648
649 return $resByKey;
650 }
651
658 private function processFluxKeys(
659 array $keys,
660 array $fluxSisterKeys,
661 array $wrappedBySisterKey
662 ) {
663 $purges = [];
664
665 reset( $keys );
666 foreach ( $fluxSisterKeys as $fluxKey ) {
667 // Get the corresponding base key for this "flux" key
668 $key = current( $keys );
669 next( $keys );
670
671 $purge = isset( $wrappedBySisterKey[$fluxKey] )
672 ? $this->parsePurgeValue( $wrappedBySisterKey[$fluxKey] )
673 : null;
674
675 if ( $purge !== null ) {
676 $purges[$key] = $purge;
677 }
678 }
679
680 return $purges;
681 }
682
689 private function processCheckKeys(
690 array $checkSisterKeys,
691 array $wrappedBySisterKey,
692 float $now
693 ) {
694 $purges = [];
695
696 foreach ( $checkSisterKeys as $timeKey ) {
697 $purge = isset( $wrappedBySisterKey[$timeKey] )
698 ? $this->parsePurgeValue( $wrappedBySisterKey[$timeKey] )
699 : null;
700
701 if ( $purge === null ) {
702 $wrapped = $this->makeCheckPurgeValue( $now, self::HOLDOFF_TTL, $purge );
703 $this->cache->add( $timeKey, $wrapped, self::CHECK_KEY_TTL );
704 }
705
706 $purges[] = $purge;
707 }
708
709 return $purges;
710 }
711
790 final public function set( $key, $value, $ttl = self::TTL_INDEFINITE, array $opts = [] ) {
791 $now = $this->getCurrentTime();
792 $lag = $opts['lag'] ?? 0;
793 $age = isset( $opts['since'] ) ? max( 0, $now - $opts['since'] ) : 0;
794 $pending = $opts['pending'] ?? false;
795 $lockTSE = $opts['lockTSE'] ?? self::TSE_NONE;
796 $staleTTL = $opts['staleTTL'] ?? self::STALE_TTL_NONE;
797 $creating = $opts['creating'] ?? false;
798 $version = $opts['version'] ?? null;
799 $walltime = $opts['walltime'] ?? null;
800
801 if ( $ttl < 0 ) {
802 return true; // not cacheable
803 }
804
805 // Do not cache potentially uncommitted data as it might get rolled back
806 if ( $pending ) {
807 $this->logger->info(
808 'Rejected set() for {cachekey} due to pending writes.',
809 [ 'cachekey' => $key ]
810 );
811
812 return true; // no-op the write for being unsafe
813 }
814
815 // Check if there is a risk of caching (stale) data that predates the last delete()
816 // tombstone due to the tombstone having expired. If so, then the behavior should depend
817 // on whether the problem is specific to this regeneration attempt or systemically affects
818 // attempts to regenerate this key. For systemic cases, the cache writes should set a low
819 // TTL so that the value at least remains cacheable. For non-systemic cases, the cache
820 // write can simply be rejected.
821 if ( $age > self::MAX_READ_LAG ) {
822 // Case A: high snapshot lag
823 if ( $walltime === null ) {
824 // Case A0: high snapshot lag without regeneration wall time info.
825 // Probably systemic; use a low TTL to avoid stampedes/uncacheability.
826 $mitigated = 'snapshot lag';
827 $mitigationTTL = self::TTL_SECOND;
828 } elseif ( ( $age - $walltime ) > self::MAX_READ_LAG ) {
829 // Case A1: value regeneration during an already long-running transaction.
830 // Probably non-systemic; rely on a less problematic regeneration attempt.
831 $mitigated = 'snapshot lag (late regeneration)';
832 $mitigationTTL = self::TTL_UNCACHEABLE;
833 } else {
834 // Case A2: value regeneration takes a long time.
835 // Probably systemic; use a low TTL to avoid stampedes/uncacheability.
836 $mitigated = 'snapshot lag (high regeneration time)';
837 $mitigationTTL = self::TTL_SECOND;
838 }
839 } elseif ( $lag === false || $lag > self::MAX_READ_LAG ) {
840 // Case B: high replication lag without high snapshot lag
841 // Probably systemic; use a low TTL to avoid stampedes/uncacheability
842 $mitigated = 'replication lag';
843 $mitigationTTL = self::TTL_LAGGED;
844 } elseif ( ( $lag + $age ) > self::MAX_READ_LAG ) {
845 // Case C: medium length request with medium replication lag
846 // Probably non-systemic; rely on a less problematic regeneration attempt
847 $mitigated = 'read lag';
848 $mitigationTTL = self::TTL_UNCACHEABLE;
849 } else {
850 // New value generated with recent enough data
851 $mitigated = null;
852 $mitigationTTL = null;
853 }
854
855 if ( $mitigationTTL === self::TTL_UNCACHEABLE ) {
856 $this->logger->warning(
857 "Rejected set() for {cachekey} due to $mitigated.",
858 [ 'cachekey' => $key, 'lag' => $lag, 'age' => $age, 'walltime' => $walltime ]
859 );
860
861 return true; // no-op the write for being unsafe
862 }
863
864 // TTL to use in staleness checks (does not effect persistence layer TTL)
865 $logicalTTL = null;
866
867 if ( $mitigationTTL !== null ) {
868 // New value generated from data that is old enough to be risky
869 if ( $lockTSE >= 0 ) {
870 // Value will have the normal expiry but will be seen as stale sooner
871 $logicalTTL = min( $ttl ?: INF, $mitigationTTL );
872 } else {
873 // Value expires sooner (leaving enough TTL for preemptive refresh)
874 $ttl = min( $ttl ?: INF, max( $mitigationTTL, self::LOW_TTL ) );
875 }
876
877 $this->logger->warning(
878 "Lowered set() TTL for {cachekey} due to $mitigated.",
879 [ 'cachekey' => $key, 'lag' => $lag, 'age' => $age, 'walltime' => $walltime ]
880 );
881 }
882
883 // Wrap that value with time/TTL/version metadata
884 $wrapped = $this->wrap( $value, $logicalTTL ?: $ttl, $version, $now, $walltime );
885 $storeTTL = $ttl + $staleTTL;
886
887 if ( $creating ) {
888 $ok = $this->cache->add(
889 $this->makeSisterKey( $key, self::TYPE_VALUE ),
890 $wrapped,
891 $storeTTL
892 );
893 } else {
894 $ok = $this->cache->merge(
895 $this->makeSisterKey( $key, self::TYPE_VALUE ),
896 static function ( $cache, $key, $cWrapped ) use ( $wrapped ) {
897 // A string value means that it is a tombstone; do nothing in that case
898 return ( is_string( $cWrapped ) ) ? false : $wrapped;
899 },
900 $storeTTL,
901 1 // 1 attempt
902 );
903 }
904
905 return $ok;
906 }
907
970 final public function delete( $key, $ttl = self::HOLDOFF_TTL ) {
971 // Purge values must be stored under the value key so that WANObjectCache::set()
972 // can atomically merge values without accidentally undoing a recent purge and thus
973 // violating the holdoff TTL restriction.
974 $valueSisterKey = $this->makeSisterKey( $key, self::TYPE_VALUE );
975
976 // When an on-host tier is configured, fetchKeys() relies on "flux" keys to determine
977 // whether a value from the on-host tier is still valid. A "flux" key is a short-lived
978 // key that contains the last recent purge due to delete(). This approach avoids having
979 // to purge the on-host cache service on potentially hundreds of application servers.
980
981 if ( $ttl <= 0 ) {
982 // A client or cache cleanup script is requesting a cache purge, so there is no
983 // volatility period due to replica DB lag. Any recent change to an entity cached
984 // in this key should have triggered an appropriate purge event.
985
986 // Remove the key from all datacenters, ignoring any on-host tier. Since on-host
987 // tier caches only use low key TTLs, setting "flux" keys here has little practical
988 // benefit; missed purges should be rare and the on-host tier will quickly correct
989 // itself in those rare cases.
990 $ok = $this->relayNonVolatilePurge( $valueSisterKey );
991 } else {
992 // A cacheable entity recently changed, so there might be a volatility period due
993 // to replica DB lag. Clients usually expect their actions to be reflected in any
994 // of their subsequent web request. This is attainable if (a) purge relay lag is
995 // lower than the time it takes for subsequent request by the client to arrive,
996 // and, (b) DB replica queries have "read-your-writes" consistency due to DB lag
997 // mitigation systems.
998
999 $now = $this->getCurrentTime();
1000 // Set the key to the purge value in all datacenters
1001 $purgeBySisterKey = [ $valueSisterKey => $this->makeTombstonePurgeValue( $now ) ];
1002 // When an on-host tier is configured, invalidate it by setting "flux" keys
1003 if ( $this->onHostRoute !== null ) {
1004 $fluxSisterKey = $this->makeSisterKey( $key, self::TYPE_FLUX );
1005 $purgeBySisterKey[$fluxSisterKey] = $this->makeTombstonePurgeValue( $now );
1006 }
1007
1008 $ok = $this->relayVolatilePurges( $purgeBySisterKey, $ttl );
1009 }
1010
1011 $kClass = $this->determineKeyClassForStats( $key );
1012 $this->stats->increment( "wanobjectcache.$kClass.delete." . ( $ok ? 'ok' : 'error' ) );
1013
1014 return $ok;
1015 }
1016
1036 final public function getCheckKeyTime( $key ) {
1037 return $this->getMultiCheckKeyTime( [ $key ] )[$key];
1038 }
1039
1101 final public function getMultiCheckKeyTime( array $keys ) {
1102 $checkSisterKeysByKey = [];
1103 foreach ( $keys as $key ) {
1104 $checkSisterKeysByKey[$key] = $this->makeSisterKey( $key, self::TYPE_TIMESTAMP );
1105 }
1106
1107 $wrappedBySisterKey = $this->cache->getMulti( $checkSisterKeysByKey );
1108 $wrappedBySisterKey += array_fill_keys( $checkSisterKeysByKey, false );
1109
1110 $now = $this->getCurrentTime();
1111 $times = [];
1112 foreach ( $checkSisterKeysByKey as $key => $checkSisterKey ) {
1113 $purge = $this->parsePurgeValue( $wrappedBySisterKey[$checkSisterKey] );
1114 if ( $purge === null ) {
1115 $wrapped = $this->makeCheckPurgeValue( $now, self::HOLDOFF_TTL, $purge );
1116 $this->cache->add( $checkSisterKey, $wrapped, self::CHECK_KEY_TTL );
1117 }
1118
1119 $times[$key] = $purge[self::PURGE_TIME];
1120 }
1121
1122 return $times;
1123 }
1124
1159 final public function touchCheckKey( $key, $holdoff = self::HOLDOFF_TTL ) {
1160 $checkSisterKey = $this->makeSisterKey( $key, self::TYPE_TIMESTAMP );
1161
1162 $now = $this->getCurrentTime();
1163 $purgeBySisterKey = [ $checkSisterKey => $this->makeCheckPurgeValue( $now, $holdoff ) ];
1164 $ok = $this->relayVolatilePurges( $purgeBySisterKey, self::CHECK_KEY_TTL );
1165
1166 $kClass = $this->determineKeyClassForStats( $key );
1167 $this->stats->increment( "wanobjectcache.$kClass.ck_touch." . ( $ok ? 'ok' : 'error' ) );
1168
1169 return $ok;
1170 }
1171
1199 final public function resetCheckKey( $key ) {
1200 $checkSisterKey = $this->makeSisterKey( $key, self::TYPE_TIMESTAMP );
1201 $ok = $this->relayNonVolatilePurge( $checkSisterKey );
1202
1203 $kClass = $this->determineKeyClassForStats( $key );
1204 $this->stats->increment( "wanobjectcache.$kClass.ck_reset." . ( $ok ? 'ok' : 'error' ) );
1205
1206 return $ok;
1207 }
1208
1512 final public function getWithSetCallback(
1513 $key, $ttl, $callback, array $opts = [], array $cbParams = []
1514 ) {
1515 $version = $opts['version'] ?? null;
1516 $pcTTL = $opts['pcTTL'] ?? self::TTL_UNCACHEABLE;
1517 $pCache = ( $pcTTL >= 0 )
1518 ? $this->getProcessCache( $opts['pcGroup'] ?? self::PC_PRIMARY )
1519 : null;
1520
1521 // Use the process cache if requested as long as no outer cache callback is running.
1522 // Nested callback process cache use is not lag-safe with regard to HOLDOFF_TTL since
1523 // process cached values are more lagged than persistent ones as they are not purged.
1524 if ( $pCache && $this->callbackDepth == 0 ) {
1525 $cached = $pCache->get( $key, $pcTTL, false );
1526 if ( $cached !== false ) {
1527 $this->logger->debug( "getWithSetCallback($key): process cache hit" );
1528 return $cached;
1529 }
1530 }
1531
1532 $res = $this->fetchOrRegenerate( $key, $ttl, $callback, $opts, $cbParams );
1533 list( $value, $valueVersion, $curAsOf ) = $res;
1534 if ( $valueVersion !== $version ) {
1535 // Current value has a different version; use the variant key for this version.
1536 // Regenerate the variant value if it is not newer than the main value at $key
1537 // so that purges to the main key propagate to the variant value.
1538 $this->logger->debug( "getWithSetCallback($key): using variant key" );
1539 list( $value ) = $this->fetchOrRegenerate(
1540 $this->makeGlobalKey( 'WANCache-key-variant', md5( $key ), $version ),
1541 $ttl,
1542 $callback,
1543 [ 'version' => null, 'minAsOf' => $curAsOf ] + $opts,
1544 $cbParams
1545 );
1546 }
1547
1548 // Update the process cache if enabled
1549 if ( $pCache && $value !== false ) {
1550 $pCache->set( $key, $value );
1551 }
1552
1553 return $value;
1554 }
1555
1572 private function fetchOrRegenerate( $key, $ttl, $callback, array $opts, array $cbParams ) {
1573 $checkKeys = $opts['checkKeys'] ?? [];
1574 $graceTTL = $opts['graceTTL'] ?? self::GRACE_TTL_NONE;
1575 $minAsOf = $opts['minAsOf'] ?? self::MIN_TIMESTAMP_NONE;
1576 $hotTTR = $opts['hotTTR'] ?? self::HOT_TTR;
1577 $lowTTL = $opts['lowTTL'] ?? min( self::LOW_TTL, $ttl );
1578 $ageNew = $opts['ageNew'] ?? self::AGE_NEW;
1579 $touchedCb = $opts['touchedCallback'] ?? null;
1580 $initialTime = $this->getCurrentTime();
1581
1582 $kClass = $this->determineKeyClassForStats( $key );
1583
1584 // Get the current key value and its metadata
1585 $res = $this->fetchKeys( [ $key ], $checkKeys )[$key];
1586 $curValue = $res[self::RES_VALUE];
1587 $curInfo = $res[self::RES_METADATA];
1588 $curTTL = $curInfo[self::KEY_CUR_TTL];
1589 // Apply any $touchedCb invalidation timestamp to get the "last purge timestamp"
1590 list( $curTTL, $LPT ) = $this->resolveCTL( $curValue, $curTTL, $curInfo, $touchedCb );
1591 // Use the cached value if it exists and is not due for synchronous regeneration
1592 if (
1593 $this->isValid( $curValue, $curInfo[self::KEY_AS_OF], $minAsOf ) &&
1594 $this->isAliveOrInGracePeriod( $curTTL, $graceTTL )
1595 ) {
1596 $preemptiveRefresh = (
1597 $this->worthRefreshExpiring( $curTTL, $curInfo[self::KEY_TTL], $lowTTL ) ||
1598 $this->worthRefreshPopular( $curInfo['asOf'], $ageNew, $hotTTR, $initialTime )
1599 );
1600 if ( !$preemptiveRefresh ) {
1601 $this->stats->timing(
1602 "wanobjectcache.$kClass.hit.good",
1603 1e3 * ( $this->getCurrentTime() - $initialTime )
1604 );
1605
1606 return [ $curValue, $curInfo[self::KEY_VERSION], $curInfo[self::KEY_AS_OF] ];
1607 } elseif ( $this->scheduleAsyncRefresh( $key, $ttl, $callback, $opts, $cbParams ) ) {
1608 $this->logger->debug( "fetchOrRegenerate($key): hit with async refresh" );
1609 $this->stats->timing(
1610 "wanobjectcache.$kClass.hit.refresh",
1611 1e3 * ( $this->getCurrentTime() - $initialTime )
1612 );
1613
1614 return [ $curValue, $curInfo[self::KEY_VERSION], $curInfo[self::KEY_AS_OF] ];
1615 } else {
1616 $this->logger->debug( "fetchOrRegenerate($key): hit with sync refresh" );
1617 }
1618 }
1619
1620 // Determine if there is stale or volatile cached value that is still usable
1621 $isKeyTombstoned = ( $curInfo[self::KEY_TOMB_AS_OF] !== null );
1622 if ( $isKeyTombstoned ) {
1623 // Key is write-holed; use the (volatile) interim key as an alternative
1624 list( $possValue, $possInfo ) = $this->getInterimValue( $key, $minAsOf );
1625 // Update the "last purge time" since the $touchedCb timestamp depends on $value
1626 $LPT = $this->resolveTouched( $possValue, $LPT, $touchedCb );
1627 } else {
1628 $possValue = $curValue;
1629 $possInfo = $curInfo;
1630 }
1631
1632 // Avoid overhead from callback runs, regeneration locks, and cache sets during
1633 // hold-off periods for the key by reusing very recently generated cached values
1634 if (
1635 $this->isValid( $possValue, $possInfo[self::KEY_AS_OF], $minAsOf, $LPT ) &&
1636 $this->isVolatileValueAgeNegligible( $initialTime - $possInfo[self::KEY_AS_OF] )
1637 ) {
1638 $this->logger->debug( "fetchOrRegenerate($key): volatile hit" );
1639 $this->stats->timing(
1640 "wanobjectcache.$kClass.hit.volatile",
1641 1e3 * ( $this->getCurrentTime() - $initialTime )
1642 );
1643
1644 return [ $possValue, $possInfo[self::KEY_VERSION], $curInfo[self::KEY_AS_OF] ];
1645 }
1646
1647 $lockTSE = $opts['lockTSE'] ?? self::TSE_NONE;
1648 $busyValue = $opts['busyValue'] ?? null;
1649 $staleTTL = $opts['staleTTL'] ?? self::STALE_TTL_NONE;
1650 $version = $opts['version'] ?? null;
1651
1652 // Determine whether one thread per datacenter should handle regeneration at a time
1653 $useRegenerationLock =
1654 // Note that since tombstones no-op set(), $lockTSE and $curTTL cannot be used to
1655 // deduce the key hotness because |$curTTL| will always keep increasing until the
1656 // tombstone expires or is overwritten by a new tombstone. Also, even if $lockTSE
1657 // is not set, constant regeneration of a key for the tombstone lifetime might be
1658 // very expensive. Assume tombstoned keys are possibly hot in order to reduce
1659 // the risk of high regeneration load after the delete() method is called.
1660 $isKeyTombstoned ||
1661 // Assume a key is hot if requested soon ($lockTSE seconds) after invalidation.
1662 // This avoids stampedes when timestamps from $checkKeys/$touchedCb bump.
1663 ( $curTTL !== null && $curTTL <= 0 && abs( $curTTL ) <= $lockTSE ) ||
1664 // Assume a key is hot if there is no value and a busy fallback is given.
1665 // This avoids stampedes on eviction or preemptive regeneration taking too long.
1666 ( $busyValue !== null && $possValue === false );
1667
1668 // If a regeneration lock is required, threads that do not get the lock will try to use
1669 // the stale value, the interim value, or the $busyValue placeholder, in that order. If
1670 // none of those are set then all threads will bypass the lock and regenerate the value.
1671 $hasLock = $useRegenerationLock && $this->claimStampedeLock( $key );
1672 if ( $useRegenerationLock && !$hasLock ) {
1673 if ( $this->isValid( $possValue, $possInfo[self::KEY_AS_OF], $minAsOf ) ) {
1674 $this->logger->debug( "fetchOrRegenerate($key): returning stale value" );
1675 $this->stats->timing(
1676 "wanobjectcache.$kClass.hit.stale",
1677 1e3 * ( $this->getCurrentTime() - $initialTime )
1678 );
1679
1680 return [ $possValue, $possInfo[self::KEY_VERSION], $curInfo[self::KEY_AS_OF] ];
1681 } elseif ( $busyValue !== null ) {
1682 $miss = is_infinite( $minAsOf ) ? 'renew' : 'miss';
1683 $this->logger->debug( "fetchOrRegenerate($key): busy $miss" );
1684 $this->stats->timing(
1685 "wanobjectcache.$kClass.$miss.busy",
1686 1e3 * ( $this->getCurrentTime() - $initialTime )
1687 );
1688 $placeholderValue = $this->resolveBusyValue( $busyValue );
1689
1690 return [ $placeholderValue, $version, $curInfo[self::KEY_AS_OF] ];
1691 }
1692 }
1693
1694 // Generate the new value given any prior value with a matching version
1695 $setOpts = [];
1696 $preCallbackTime = $this->getCurrentTime();
1697 ++$this->callbackDepth;
1698 try {
1699 $value = $callback(
1700 ( $curInfo[self::KEY_VERSION] === $version ) ? $curValue : false,
1701 $ttl,
1702 $setOpts,
1703 ( $curInfo[self::KEY_VERSION] === $version ) ? $curInfo[self::KEY_AS_OF] : null,
1704 $cbParams
1705 );
1706 } finally {
1707 --$this->callbackDepth;
1708 }
1709 $postCallbackTime = $this->getCurrentTime();
1710
1711 // How long it took to fetch, validate, and generate the value
1712 $elapsed = max( $postCallbackTime - $initialTime, 0.0 );
1713
1714 // How long it took to generate the value
1715 $walltime = max( $postCallbackTime - $preCallbackTime, 0.0 );
1716 $this->stats->timing( "wanobjectcache.$kClass.regen_walltime", 1e3 * $walltime );
1717
1718 // Attempt to save the newly generated value if applicable
1719 if (
1720 // Callback yielded a cacheable value
1721 ( $value !== false && $ttl >= 0 ) &&
1722 // Current thread was not raced out of a regeneration lock or key is tombstoned
1723 ( !$useRegenerationLock || $hasLock || $isKeyTombstoned ) &&
1724 // Key does not appear to be undergoing a set() stampede
1725 $this->checkAndSetCooloff( $key, $kClass, $value, $elapsed, $hasLock )
1726 ) {
1727 // If the key is write-holed then use the (volatile) interim key as an alternative
1728 if ( $isKeyTombstoned ) {
1729 $this->setInterimValue( $key, $value, $lockTSE, $version, $walltime );
1730 } else {
1731 $finalSetOpts = [
1732 // @phan-suppress-next-line PhanUselessBinaryAddRight,PhanCoalescingAlwaysNull
1733 'since' => $setOpts['since'] ?? $preCallbackTime,
1734 'version' => $version,
1735 'staleTTL' => $staleTTL,
1736 'lockTSE' => $lockTSE, // informs lag vs performance trade-offs
1737 'creating' => ( $curValue === false ), // optimization
1738 'walltime' => $walltime
1739 ] + $setOpts;
1740 $this->set( $key, $value, $ttl, $finalSetOpts );
1741 }
1742 }
1743
1744 $this->yieldStampedeLock( $key, $hasLock );
1745
1746 $miss = is_infinite( $minAsOf ) ? 'renew' : 'miss';
1747 $this->logger->debug( "fetchOrRegenerate($key): $miss, new value computed" );
1748 $this->stats->timing(
1749 "wanobjectcache.$kClass.$miss.compute",
1750 1e3 * ( $this->getCurrentTime() - $initialTime )
1751 );
1752
1753 return [ $value, $version, $curInfo[self::KEY_AS_OF] ];
1754 }
1755
1760 private function claimStampedeLock( $key ) {
1761 $checkSisterKey = $this->makeSisterKey( $key, self::TYPE_MUTEX );
1762 // Note that locking is not bypassed due to I/O errors; this avoids stampedes
1763 return $this->cache->add( $checkSisterKey, 1, self::LOCK_TTL );
1764 }
1765
1770 private function yieldStampedeLock( $key, $hasLock ) {
1771 if ( $hasLock ) {
1772 $checkSisterKey = $this->makeSisterKey( $key, self::TYPE_MUTEX );
1773 // The backend might be a mcrouter proxy set to broadcast DELETE to *all* the local
1774 // datacenter cache servers via OperationSelectorRoute (for increased consistency).
1775 // Since that would be excessive for these locks, use TOUCH to expire the key.
1776 $this->cache->changeTTL( $checkSisterKey, $this->getCurrentTime() - 60 );
1777 }
1778 }
1779
1790 private function makeSisterKeys( array $baseKeys, string $type, string $route = null ) {
1791 $sisterKeys = [];
1792 foreach ( $baseKeys as $baseKey ) {
1793 $sisterKeys[] = $this->makeSisterKey( $baseKey, $type, $route );
1794 }
1795
1796 return $sisterKeys;
1797 }
1798
1809 private function makeSisterKey( string $baseKey, string $typeChar, string $route = null ) {
1810 if ( $this->coalesceScheme === self::SCHEME_HASH_STOP ) {
1811 // Key style: "WANCache:<base key>|#|<character>"
1812 $sisterKey = 'WANCache:' . $baseKey . '|#|' . $typeChar;
1813 } else {
1814 // Key style: "WANCache:{<base key>}:<character>"
1815 $sisterKey = 'WANCache:{' . $baseKey . '}:' . $typeChar;
1816 }
1817
1818 if ( $route !== null ) {
1819 $sisterKey = $this->prependRoute( $sisterKey, $route );
1820 }
1821
1822 return $sisterKey;
1823 }
1824
1831 public static function getCollectionFromSisterKey( string $sisterKey ) {
1832 if ( substr( $sisterKey, -4 ) === '|#|v' ) {
1833 // Key style: "WANCache:<base key>|#|<character>"
1834 $collection = substr( $sisterKey, 9, strcspn( $sisterKey, ':|', 9 ) );
1835 } elseif ( substr( $sisterKey, -3 ) === '}:v' ) {
1836 // Key style: "WANCache:{<base key>}:<character>"
1837 $collection = substr( $sisterKey, 10, strcspn( $sisterKey, ':}', 10 ) );
1838 } else {
1839 $collection = 'internal';
1840 }
1841
1842 return $collection;
1843 }
1844
1849 private function isVolatileValueAgeNegligible( $age ) {
1850 return ( $age < mt_rand( self::RECENT_SET_LOW_MS, self::RECENT_SET_HIGH_MS ) / 1e3 );
1851 }
1852
1874 private function checkAndSetCooloff( $key, $kClass, $value, $elapsed, $hasLock ) {
1875 $valueSisterKey = $this->makeSisterKey( $key, self::TYPE_VALUE, $this->onHostRoute );
1876 list( $estimatedSize ) = $this->cache->setNewPreparedValues( [
1877 $valueSisterKey => $value
1878 ] );
1879
1880 if ( !$hasLock ) {
1881 // Suppose that this cache key is very popular (KEY_HIGH_QPS reads/second).
1882 // After eviction, there will be cache misses until it gets regenerated and saved.
1883 // If the time window when the key is missing lasts less than one second, then the
1884 // number of misses will not reach KEY_HIGH_QPS. This window largely corresponds to
1885 // the key regeneration time. Estimate the count/rate of cache misses, e.g.:
1886 // - 100 QPS, 20ms regeneration => ~2 misses (< 1s)
1887 // - 100 QPS, 100ms regeneration => ~10 misses (< 1s)
1888 // - 100 QPS, 3000ms regeneration => ~300 misses (100/s for 3s)
1889 $missesPerSecForHighQPS = ( min( $elapsed, 1 ) * $this->keyHighQps );
1890
1891 // Determine whether there is enough I/O stampede risk to justify throttling set().
1892 // Estimate unthrottled set() overhead, as bps, from miss count/rate and value size,
1893 // comparing it to the per-key uplink bps limit (KEY_HIGH_UPLINK_BPS), e.g.:
1894 // - 2 misses (< 1s), 10KB value, 1250000 bps limit => 160000 bits (low risk)
1895 // - 2 misses (< 1s), 100KB value, 1250000 bps limit => 1600000 bits (high risk)
1896 // - 10 misses (< 1s), 10KB value, 1250000 bps limit => 800000 bits (low risk)
1897 // - 10 misses (< 1s), 100KB value, 1250000 bps limit => 8000000 bits (high risk)
1898 // - 300 misses (100/s), 1KB value, 1250000 bps limit => 800000 bps (low risk)
1899 // - 300 misses (100/s), 10KB value, 1250000 bps limit => 8000000 bps (high risk)
1900 // - 300 misses (100/s), 100KB value, 1250000 bps limit => 80000000 bps (high risk)
1901 if ( ( $missesPerSecForHighQPS * $estimatedSize ) >= $this->keyHighUplinkBps ) {
1902 $cooloffSisterKey = $this->makeSisterKey( $key, self::TYPE_COOLOFF );
1903 $this->cache->clearLastError();
1904 if (
1905 !$this->cache->add( $cooloffSisterKey, 1, self::COOLOFF_TTL ) &&
1906 // Don't treat failures due to I/O errors as the key being in cool-off
1907 $this->cache->getLastError() === BagOStuff::ERR_NONE
1908 ) {
1909 $this->stats->increment( "wanobjectcache.$kClass.cooloff_bounce" );
1910
1911 return false;
1912 }
1913 }
1914 }
1915
1916 // Corresponding metrics for cache writes that actually get sent over the write
1917 $this->stats->timing( "wanobjectcache.$kClass.regen_set_delay", 1e3 * $elapsed );
1918 $this->stats->updateCount( "wanobjectcache.$kClass.regen_set_bytes", $estimatedSize );
1919
1920 return true;
1921 }
1922
1931 private function resolveCTL( $value, $curTTL, $curInfo, $touchedCallback ) {
1932 if ( $touchedCallback === null || $value === false ) {
1933 return [
1934 $curTTL,
1935 max( $curInfo[self::KEY_TOMB_AS_OF], $curInfo[self::KEY_CHECK_AS_OF] )
1936 ];
1937 }
1938
1939 $touched = $touchedCallback( $value );
1940 if ( $touched !== null && $touched >= $curInfo[self::KEY_AS_OF] ) {
1941 $curTTL = min( $curTTL, self::TINY_NEGATIVE, $curInfo[self::KEY_AS_OF] - $touched );
1942 }
1943
1944 return [
1945 $curTTL,
1946 max(
1947 $curInfo[self::KEY_TOMB_AS_OF],
1948 $curInfo[self::KEY_CHECK_AS_OF],
1949 $touched
1950 )
1951 ];
1952 }
1953
1961 private function resolveTouched( $value, $lastPurge, $touchedCallback ) {
1962 return ( $touchedCallback === null || $value === false )
1963 ? $lastPurge // nothing to derive the "touched timestamp" from
1964 : max( $touchedCallback( $value ), $lastPurge );
1965 }
1966
1972 private function getInterimValue( $key, $minAsOf ) {
1973 $now = $this->getCurrentTime();
1974
1975 if ( $this->useInterimHoldOffCaching ) {
1976 $wrapped = $this->cache->get(
1977 $this->makeSisterKey( $key, self::TYPE_INTERIM )
1978 );
1979
1980 list( $value, $keyInfo ) = $this->unwrap( $wrapped, $now );
1981 if ( $this->isValid( $value, $keyInfo[self::KEY_AS_OF], $minAsOf ) ) {
1982 return [ $value, $keyInfo ];
1983 }
1984 }
1985
1986 return $this->unwrap( false, $now );
1987 }
1988
1996 private function setInterimValue( $key, $value, $ttl, $version, $walltime ) {
1997 $ttl = max( self::INTERIM_KEY_TTL, (int)$ttl );
1998
1999 $wrapped = $this->wrap( $value, $ttl, $version, $this->getCurrentTime(), $walltime );
2000 $this->cache->merge(
2001 $this->makeSisterKey( $key, self::TYPE_INTERIM ),
2002 static function () use ( $wrapped ) {
2003 return $wrapped;
2004 },
2005 $ttl,
2006 1
2007 );
2008 }
2009
2014 private function resolveBusyValue( $busyValue ) {
2015 return ( $busyValue instanceof Closure ) ? $busyValue() : $busyValue;
2016 }
2017
2082 final public function getMultiWithSetCallback(
2083 ArrayIterator $keyedIds, $ttl, callable $callback, array $opts = []
2084 ) {
2085 // Batch load required keys into the in-process warmup cache
2086 $this->warmupCache = $this->fetchWrappedValuesForWarmupCache(
2087 $this->getNonProcessCachedMultiKeys( $keyedIds, $opts ),
2088 $opts['checkKeys'] ?? []
2089 );
2090 $this->warmupKeyMisses = 0;
2091
2092 // The required callback signature includes $id as the first argument for convenience
2093 // to distinguish different items. To reuse the code in getWithSetCallback(), wrap the
2094 // callback with a proxy callback that has the standard getWithSetCallback() signature.
2095 // This is defined only once per batch to avoid closure creation overhead.
2096 $proxyCb = static function ( $oldValue, &$ttl, &$setOpts, $oldAsOf, $params )
2097 use ( $callback )
2098 {
2099 return $callback( $params['id'], $oldValue, $ttl, $setOpts, $oldAsOf );
2100 };
2101
2102 $values = [];
2103 foreach ( $keyedIds as $key => $id ) { // preserve order
2104 $values[$key] = $this->getWithSetCallback(
2105 $key,
2106 $ttl,
2107 $proxyCb,
2108 $opts,
2109 [ 'id' => $id ]
2110 );
2111 }
2112
2113 $this->warmupCache = [];
2114
2115 return $values;
2116 }
2117
2183 final public function getMultiWithUnionSetCallback(
2184 ArrayIterator $keyedIds, $ttl, callable $callback, array $opts = []
2185 ) {
2186 $checkKeys = $opts['checkKeys'] ?? [];
2187 unset( $opts['lockTSE'] ); // incompatible
2188 unset( $opts['busyValue'] ); // incompatible
2189
2190 // Batch load required keys into the in-process warmup cache
2191 $keysByIdGet = $this->getNonProcessCachedMultiKeys( $keyedIds, $opts );
2192 $this->warmupCache = $this->fetchWrappedValuesForWarmupCache( $keysByIdGet, $checkKeys );
2193 $this->warmupKeyMisses = 0;
2194
2195 // IDs of entities known to be in need of regeneration
2196 $idsRegen = [];
2197
2198 // Find out which keys are missing/deleted/stale
2199 $resByKey = $this->fetchKeys( $keysByIdGet, $checkKeys );
2200 foreach ( $keysByIdGet as $id => $key ) {
2201 $res = $resByKey[$key];
2202 $value = $res[self::RES_VALUE];
2203 $metadata = $res[self::RES_METADATA];
2204 if ( $value === false || $metadata[self::KEY_CUR_TTL] < 0 ) {
2205 $idsRegen[] = $id;
2206 }
2207 }
2208
2209 // Run the callback to populate the regeneration value map for all required IDs
2210 $newSetOpts = [];
2211 $newTTLsById = array_fill_keys( $idsRegen, $ttl );
2212 $newValsById = $idsRegen ? $callback( $idsRegen, $newTTLsById, $newSetOpts ) : [];
2213
2214 // The required callback signature includes $id as the first argument for convenience
2215 // to distinguish different items. To reuse the code in getWithSetCallback(), wrap the
2216 // callback with a proxy callback that has the standard getWithSetCallback() signature.
2217 // This is defined only once per batch to avoid closure creation overhead.
2218 $proxyCb = static function ( $oldValue, &$ttl, &$setOpts, $oldAsOf, $params )
2219 use ( $callback, $newValsById, $newTTLsById, $newSetOpts )
2220 {
2221 $id = $params['id'];
2222
2223 if ( array_key_exists( $id, $newValsById ) ) {
2224 // Value was already regerated as expected, so use the value in $newValsById
2225 $newValue = $newValsById[$id];
2226 $ttl = $newTTLsById[$id];
2227 $setOpts = $newSetOpts;
2228 } else {
2229 // Pre-emptive/popularity refresh and version mismatch cases are not detected
2230 // above and thus $newValsById has no entry. Run $callback on this single entity.
2231 $ttls = [ $id => $ttl ];
2232 $newValue = $callback( [ $id ], $ttls, $setOpts )[$id];
2233 $ttl = $ttls[$id];
2234 }
2235
2236 return $newValue;
2237 };
2238
2239 // Run the cache-aside logic using warmupCache instead of persistent cache queries
2240 $values = [];
2241 foreach ( $keyedIds as $key => $id ) { // preserve order
2242 $values[$key] = $this->getWithSetCallback(
2243 $key,
2244 $ttl,
2245 $proxyCb,
2246 $opts,
2247 [ 'id' => $id ]
2248 );
2249 }
2250
2251 $this->warmupCache = [];
2252
2253 return $values;
2254 }
2255
2268 final public function reap( $key, $purgeTimestamp, &$isStale = false ) {
2269 $valueSisterKey = $this->makeSisterKey( $key, self::TYPE_VALUE );
2270
2271 $minAsOf = $purgeTimestamp + self::HOLDOFF_TTL;
2272 $wrapped = $this->cache->get( $valueSisterKey );
2273 if ( is_array( $wrapped ) && $wrapped[self::FLD_TIME] < $minAsOf ) {
2274 $isStale = true;
2275 $this->logger->warning( "Reaping stale value key '$key'." );
2276 $ttlReap = self::HOLDOFF_TTL; // avoids races with tombstone creation
2277 $ok = $this->cache->changeTTL( $valueSisterKey, $ttlReap );
2278 if ( !$ok ) {
2279 $this->logger->error( "Could not complete reap of key '$key'." );
2280 }
2281
2282 return $ok;
2283 }
2284
2285 $isStale = false;
2286
2287 return true;
2288 }
2289
2299 final public function reapCheckKey( $key, $purgeTimestamp, &$isStale = false ) {
2300 $checkSisterKey = $this->makeSisterKey( $key, self::TYPE_TIMESTAMP );
2301
2302 $wrapped = $this->cache->get( $checkSisterKey );
2303 $purge = $this->parsePurgeValue( $wrapped );
2304 if ( $purge !== null && $purge[self::PURGE_TIME] < $purgeTimestamp ) {
2305 $isStale = true;
2306 $this->logger->warning( "Reaping stale check key '$key'." );
2307 $ok = $this->cache->changeTTL( $checkSisterKey, self::TTL_SECOND );
2308 if ( !$ok ) {
2309 $this->logger->error( "Could not complete reap of check key '$key'." );
2310 }
2311
2312 return $ok;
2313 }
2314
2315 $isStale = false;
2316
2317 return false;
2318 }
2319
2330 public function makeGlobalKey( $collection, ...$components ) {
2331 return $this->cache->makeGlobalKey( ...func_get_args() );
2332 }
2333
2344 public function makeKey( $collection, ...$components ) {
2345 return $this->cache->makeKey( ...func_get_args() );
2346 }
2347
2355 public function hash256( $component ) {
2356 return hash_hmac( 'sha256', $component, $this->secret );
2357 }
2358
2409 final public function makeMultiKeys( array $ids, $keyCallback ) {
2410 $idByKey = [];
2411 foreach ( $ids as $id ) {
2412 // Discourage triggering of automatic makeKey() hashing in some backends
2413 if ( strlen( $id ) > 64 ) {
2414 $this->logger->warning( __METHOD__ . ": long ID '$id'; use hash256()" );
2415 }
2416 $key = $keyCallback( $id, $this );
2417 // Edge case: ignore key collisions due to duplicate $ids like "42" and 42
2418 if ( !isset( $idByKey[$key] ) ) {
2419 $idByKey[$key] = $id;
2420 } elseif ( (string)$id !== (string)$idByKey[$key] ) {
2421 throw new UnexpectedValueException(
2422 "Cache key collision; IDs ('$id','{$idByKey[$key]}') map to '$key'"
2423 );
2424 }
2425 }
2426
2427 return new ArrayIterator( $idByKey );
2428 }
2429
2465 final public function multiRemap( array $ids, array $res ) {
2466 if ( count( $ids ) !== count( $res ) ) {
2467 // If makeMultiKeys() is called on a list of non-unique IDs, then the resulting
2468 // ArrayIterator will have less entries due to "first appearance" de-duplication
2469 $ids = array_keys( array_fill_keys( $ids, true ) );
2470 if ( count( $ids ) !== count( $res ) ) {
2471 throw new UnexpectedValueException( "Multi-key result does not match ID list" );
2472 }
2473 }
2474
2475 return array_combine( $ids, $res );
2476 }
2477
2482 final public function getLastError() {
2483 $code = $this->cache->getLastError();
2484 switch ( $code ) {
2486 return self::ERR_NONE;
2488 return self::ERR_NO_RESPONSE;
2490 return self::ERR_UNREACHABLE;
2491 default:
2492 return self::ERR_UNEXPECTED;
2493 }
2494 }
2495
2499 final public function clearLastError() {
2500 $this->cache->clearLastError();
2501 }
2502
2508 public function clearProcessCache() {
2509 $this->processCaches = [];
2510 }
2511
2532 final public function useInterimHoldOffCaching( $enabled ) {
2533 $this->useInterimHoldOffCaching = $enabled;
2534 }
2535
2541 public function getQoS( $flag ) {
2542 return $this->cache->getQoS( $flag );
2543 }
2544
2608 public function adaptiveTTL( $mtime, $maxTTL, $minTTL = 30, $factor = 0.2 ) {
2609 if ( is_float( $mtime ) || ctype_digit( $mtime ) ) {
2610 $mtime = (int)$mtime; // handle fractional seconds and string integers
2611 }
2612
2613 if ( !is_int( $mtime ) || $mtime <= 0 ) {
2614 return $minTTL; // no last-modified time provided
2615 }
2616
2617 $age = $this->getCurrentTime() - $mtime;
2618
2619 return (int)min( $maxTTL, max( $minTTL, $factor * $age ) );
2620 }
2621
2626 final public function getWarmupKeyMisses() {
2627 return $this->warmupKeyMisses;
2628 }
2629
2644 protected function relayVolatilePurges( array $purgeBySisterKey, int $ttl ) {
2645 $purgeByRouteKey = [];
2646 foreach ( $purgeBySisterKey as $sisterKey => $purge ) {
2647 if ( $this->broadcastRoute !== null ) {
2648 $routeKey = $this->prependRoute( $sisterKey, $this->broadcastRoute );
2649 } else {
2650 $routeKey = $sisterKey;
2651 }
2652 $purgeByRouteKey[$routeKey] = $purge;
2653 }
2654
2655 if ( count( $purgeByRouteKey ) == 1 ) {
2656 $purge = reset( $purgeByRouteKey );
2657 $ok = $this->cache->set( key( $purgeByRouteKey ), $purge, $ttl );
2658 } else {
2659 $ok = $this->cache->setMulti( $purgeByRouteKey, $ttl );
2660 }
2661
2662 return $ok;
2663 }
2664
2673 protected function relayNonVolatilePurge( string $sisterKey ) {
2674 if ( $this->broadcastRoute !== null ) {
2675 $routeKey = $this->prependRoute( $sisterKey, $this->broadcastRoute );
2676 } else {
2677 $routeKey = $sisterKey;
2678 }
2679
2680 return $this->cache->delete( $routeKey );
2681 }
2682
2688 protected function prependRoute( string $sisterKey, string $route ) {
2689 if ( $sisterKey[0] === '/' ) {
2690 throw new RuntimeException( "Sister key '$sisterKey' already contains a route." );
2691 }
2692
2693 return $route . $sisterKey;
2694 }
2695
2707 private function scheduleAsyncRefresh( $key, $ttl, $callback, array $opts, array $cbParams ) {
2708 if ( !$this->asyncHandler ) {
2709 return false;
2710 }
2711 // Update the cache value later, such during post-send of an HTTP request. This forces
2712 // cache regeneration by setting "minAsOf" to infinity, meaning that no existing value
2713 // is considered valid. Furthermore, note that preemptive regeneration is not applicable
2714 // to invalid values, so there is no risk of infinite preemptive regeneration loops.
2715 $func = $this->asyncHandler;
2716 $func( function () use ( $key, $ttl, $callback, $opts, $cbParams ) {
2717 $opts['minAsOf'] = INF;
2718 try {
2719 $this->fetchOrRegenerate( $key, $ttl, $callback, $opts, $cbParams );
2720 } catch ( Exception $e ) {
2721 // Log some context for easier debugging
2722 $this->logger->error( 'Async refresh failed for {key}', [
2723 'key' => $key,
2724 'ttl' => $ttl,
2725 'exception' => $e
2726 ] );
2727 throw $e;
2728 }
2729 } );
2730
2731 return true;
2732 }
2733
2748 private function isAliveOrInGracePeriod( $curTTL, $graceTTL ) {
2749 if ( $curTTL > 0 ) {
2750 return true;
2751 } elseif ( $graceTTL <= 0 ) {
2752 return false;
2753 }
2754
2755 $ageStale = abs( $curTTL ); // seconds of staleness
2756 $curGraceTTL = ( $graceTTL - $ageStale ); // current grace-time-to-live
2757 if ( $curGraceTTL <= 0 ) {
2758 return false; // already out of grace period
2759 }
2760
2761 // Chance of using a stale value is the complement of the chance of refreshing it
2762 return !$this->worthRefreshExpiring( $curGraceTTL, $graceTTL, $graceTTL );
2763 }
2764
2782 protected function worthRefreshExpiring( $curTTL, $logicalTTL, $lowTTL ) {
2783 if ( $lowTTL <= 0 ) {
2784 return false;
2785 }
2786
2787 // T264787: avoid having keys start off with a high chance of being refreshed;
2788 // the point where refreshing becomes possible cannot precede the key lifetime.
2789 $effectiveLowTTL = min( $lowTTL, $logicalTTL ?: INF );
2790
2791 if ( $curTTL >= $effectiveLowTTL || $curTTL <= 0 ) {
2792 return false;
2793 }
2794
2795 $chance = ( 1 - $curTTL / $effectiveLowTTL );
2796
2797 // @phan-suppress-next-line PhanTypeMismatchArgumentInternal
2798 $decision = ( mt_rand( 1, 1e9 ) <= 1e9 * $chance );
2799
2800 $this->logger->debug(
2801 "worthRefreshExpiring($curTTL, $logicalTTL, $lowTTL): " .
2802 "p = $chance; refresh = " . ( $decision ? 'Y' : 'N' )
2803 );
2804
2805 return $decision;
2806 }
2807
2823 protected function worthRefreshPopular( $asOf, $ageNew, $timeTillRefresh, $now ) {
2824 if ( $ageNew < 0 || $timeTillRefresh <= 0 ) {
2825 return false;
2826 }
2827
2828 $age = $now - $asOf;
2829 $timeOld = $age - $ageNew;
2830 if ( $timeOld <= 0 ) {
2831 return false;
2832 }
2833
2834 $popularHitsPerSec = 1;
2835 // Lifecycle is: new, ramp-up refresh chance, full refresh chance.
2836 // Note that the "expected # of refreshes" for the ramp-up time range is half
2837 // of what it would be if P(refresh) was at its full value during that time range.
2838 $refreshWindowSec = max( $timeTillRefresh - $ageNew - self::RAMPUP_TTL / 2, 1 );
2839 // P(refresh) * (# hits in $refreshWindowSec) = (expected # of refreshes)
2840 // P(refresh) * ($refreshWindowSec * $popularHitsPerSec) = 1 (by definition)
2841 // P(refresh) = 1/($refreshWindowSec * $popularHitsPerSec)
2842 $chance = 1 / ( $popularHitsPerSec * $refreshWindowSec );
2843 // Ramp up $chance from 0 to its nominal value over RAMPUP_TTL seconds to avoid stampedes
2844 $chance *= ( $timeOld <= self::RAMPUP_TTL ) ? $timeOld / self::RAMPUP_TTL : 1;
2845
2846 // @phan-suppress-next-line PhanTypeMismatchArgumentInternal
2847 $decision = ( mt_rand( 1, 1e9 ) <= 1e9 * $chance );
2848
2849 $this->logger->debug(
2850 "worthRefreshPopular($asOf, $ageNew, $timeTillRefresh, $now): " .
2851 "p = $chance; refresh = " . ( $decision ? 'Y' : 'N' )
2852 );
2853
2854 return $decision;
2855 }
2856
2866 protected function isValid( $value, $asOf, $minAsOf, $purgeTime = null ) {
2867 // Avoid reading any key not generated after the latest delete() or touch
2868 $safeMinAsOf = max( $minAsOf, $purgeTime + self::TINY_POSTIVE );
2869
2870 if ( $value === false ) {
2871 return false;
2872 } elseif ( $safeMinAsOf > 0 && $asOf < $minAsOf ) {
2873 return false;
2874 }
2875
2876 return true;
2877 }
2878
2887 private function wrap( $value, $ttl, $version, $now, $walltime ) {
2888 // Returns keys in ascending integer order for PHP7 array packing:
2889 // https://nikic.github.io/2014/12/22/PHPs-new-hashtable-implementation.html
2890 $wrapped = [
2891 self::FLD_FORMAT_VERSION => self::VERSION,
2892 self::FLD_VALUE => $value,
2893 self::FLD_TTL => $ttl,
2894 self::FLD_TIME => $now
2895 ];
2896 if ( $version !== null ) {
2897 $wrapped[self::FLD_VALUE_VERSION] = $version;
2898 }
2899 if ( $walltime >= self::GENERATION_SLOW_SEC ) {
2900 $wrapped[self::FLD_GENERATION_TIME] = $walltime;
2901 }
2902
2903 return $wrapped;
2904 }
2905
2918 private function unwrap( $wrapped, $now ) {
2919 $value = false;
2920 $info = $this->newKeyInfoPlaceholder();
2921
2922 if ( is_array( $wrapped ) ) {
2923 // Entry expected to be a cached value; validate it
2924 if (
2925 ( $wrapped[self::FLD_FORMAT_VERSION] ?? null ) === self::VERSION &&
2926 $wrapped[self::FLD_TIME] >= $this->epoch
2927 ) {
2928 if ( $wrapped[self::FLD_TTL] > 0 ) {
2929 // Get the approximate time left on the key
2930 $age = $now - $wrapped[self::FLD_TIME];
2931 $curTTL = max( $wrapped[self::FLD_TTL] - $age, 0.0 );
2932 } else {
2933 // Key had no TTL, so the time left is unbounded
2934 $curTTL = INF;
2935 }
2936 $value = $wrapped[self::FLD_VALUE];
2937 $info[self::KEY_VERSION] = $wrapped[self::FLD_VALUE_VERSION] ?? null;
2938 $info[self::KEY_AS_OF] = $wrapped[self::FLD_TIME];
2939 $info[self::KEY_CUR_TTL] = $curTTL;
2940 $info[self::KEY_TTL] = $wrapped[self::FLD_TTL];
2941 }
2942 } else {
2943 // Entry expected to be a tombstone; parse it
2944 $purge = $this->parsePurgeValue( $wrapped );
2945 if ( $purge !== null ) {
2946 // Tombstoned keys should always have a negative current $ttl
2947 $info[self::KEY_CUR_TTL] =
2948 min( $purge[self::PURGE_TIME] - $now, self::TINY_NEGATIVE );
2949 $info[self::KEY_TOMB_AS_OF] = $purge[self::PURGE_TIME];
2950 }
2951 }
2952
2953 return [ $value, $info ];
2954 }
2955
2959 private function newKeyInfoPlaceholder() {
2960 return [
2961 self::KEY_VERSION => null,
2962 self::KEY_AS_OF => null,
2963 self::KEY_TTL => null,
2964 self::KEY_CUR_TTL => null,
2965 self::KEY_TOMB_AS_OF => null
2966 ];
2967 }
2968
2973 private function determineKeyClassForStats( $key ) {
2974 $parts = explode( ':', $key, 3 );
2975 // Sanity fallback in case the key was not made by makeKey.
2976 // Replace dots because they are special in StatsD (T232907)
2977 return strtr( $parts[1] ?? $parts[0], '.', '_' );
2978 }
2979
2988 private function parsePurgeValue( $value ) {
2989 if ( !is_string( $value ) ) {
2990 return null;
2991 }
2992
2993 $segments = explode( ':', $value, 3 );
2994 if ( isset( $segments[2] ) ) {
2995 $prefix = $segments[0];
2996 $timestamp = (float)$segments[1];
2997 $holdoff = (int)$segments[2];
2998 } elseif ( isset( $segments[1] ) ) {
2999 $prefix = $segments[0];
3000 $timestamp = (float)$segments[1];
3001 // Value tombstones don't store hold-off TTLs
3002 $holdoff = self::HOLDOFF_TTL;
3003 } else {
3004 return null;
3005 }
3006
3007 if ( "{$prefix}:" !== self::PURGE_VAL_PREFIX || $timestamp < $this->epoch ) {
3008 // Not a purge value or the purge value is too old
3009 return null;
3010 }
3011
3012 return [ self::PURGE_TIME => $timestamp, self::PURGE_HOLDOFF => $holdoff ];
3013 }
3014
3019 private function makeTombstonePurgeValue( float $timestamp ) {
3020 return self::PURGE_VAL_PREFIX . number_format( $timestamp, 4, '.', '' );
3021 }
3022
3029 private function makeCheckPurgeValue( float $timestamp, int $holdoff, array &$purge = null ) {
3030 $normalizedTime = number_format( $timestamp, 4, '.', '' );
3031 // Purge array that matches what parsePurgeValue() would have returned
3032 $purge = [ self::PURGE_TIME => (float)$normalizedTime, self::PURGE_HOLDOFF => $holdoff ];
3033
3034 return self::PURGE_VAL_PREFIX . "$normalizedTime:$holdoff";
3035 }
3036
3041 private function getProcessCache( $group ) {
3042 if ( !isset( $this->processCaches[$group] ) ) {
3043 list( , $size ) = explode( ':', $group );
3044 $this->processCaches[$group] = new MapCacheLRU( (int)$size );
3045 if ( $this->wallClockOverride !== null ) {
3046 $this->processCaches[$group]->setMockTime( $this->wallClockOverride );
3047 }
3048 }
3049
3050 return $this->processCaches[$group];
3051 }
3052
3058 private function getNonProcessCachedMultiKeys( ArrayIterator $keys, array $opts ) {
3059 $pcTTL = $opts['pcTTL'] ?? self::TTL_UNCACHEABLE;
3060
3061 $keysMissing = [];
3062 if ( $pcTTL > 0 && $this->callbackDepth == 0 ) {
3063 $version = $opts['version'] ?? null;
3064 $pCache = $this->getProcessCache( $opts['pcGroup'] ?? self::PC_PRIMARY );
3065 foreach ( $keys as $key => $id ) {
3066 if ( !$pCache->has( $key, $pcTTL ) ) {
3067 $keysMissing[$id] = $key;
3068 }
3069 }
3070 }
3071
3072 return $keysMissing;
3073 }
3074
3081 private function fetchWrappedValuesForWarmupCache( array $keys, array $checkKeys ) {
3082 if ( !$keys ) {
3083 return [];
3084 }
3085
3086 // Get all the value keys to fetch...
3087 $sisterKeys = $this->makeSisterKeys( $keys, self::TYPE_VALUE, $this->onHostRoute );
3088 // Get all the flux keys to fetch...
3089 if ( $this->onHostRoute !== null ) {
3090 foreach ( $keys as $key ) {
3091 $sisterKeys[] = $this->makeSisterKey( $key, self::TYPE_FLUX );
3092 }
3093 }
3094 // Get all the "check" keys to fetch...
3095 foreach ( $checkKeys as $i => $checkKeyOrKeyGroup ) {
3096 // Note: avoid array_merge() inside loop in case there are many keys
3097 if ( is_int( $i ) ) {
3098 // Single "check" key that applies to all value keys
3099 $sisterKeys[] = $this->makeSisterKey( $checkKeyOrKeyGroup, self::TYPE_TIMESTAMP );
3100 } else {
3101 // List of "check" keys that apply to a specific value key
3102 foreach ( (array)$checkKeyOrKeyGroup as $checkKey ) {
3103 $sisterKeys[] = $this->makeSisterKey( $checkKey, self::TYPE_TIMESTAMP );
3104 }
3105 }
3106 }
3107
3108 $wrappedBySisterKey = $this->cache->getMulti( $sisterKeys );
3109 $wrappedBySisterKey += array_fill_keys( $sisterKeys, false );
3110
3111 return $wrappedBySisterKey;
3112 }
3113
3118 protected function getCurrentTime() {
3119 if ( $this->wallClockOverride ) {
3120 return $this->wallClockOverride;
3121 }
3122
3123 $clockTime = (float)time(); // call this first
3124 // microtime() uses an initial gettimeofday() call added to usage clocks.
3125 // This can severely drift from time() and the microtime() value of other threads
3126 // due to undercounting of the amount of time elapsed. Instead of seeing the current
3127 // time as being in the past, use the value of time(). This avoids setting cache values
3128 // that will immediately be seen as expired and possibly cause stampedes.
3129 return max( microtime( true ), $clockTime );
3130 }
3131
3136 public function setMockTime( &$time ) {
3137 $this->wallClockOverride =& $time;
3138 $this->cache->setMockTime( $time );
3139 foreach ( $this->processCaches as $pCache ) {
3140 $pCache->setMockTime( $time );
3141 }
3142 }
3143}
Class representing a cache/ephemeral data store.
Definition BagOStuff.php:86
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.
int $callbackDepth
Callback stack depth for getWithSetCallback()
const PURGE_TIME
Key to the tombstone entry timestamp.
const HOLDOFF_TTL
Seconds to tombstone keys on delete() and treat as volatile after invalidation.
const HOT_TTR
Expected time-till-refresh, in seconds, if the key is accessed once per second.
const KEY_VERSION
Version number attribute for a key; keep value for b/c (< 1.36)
const RES_METADATA
The key metadata component of a fetchMulti() result.
__construct(array $params)
resolveCTL( $value, $curTTL, $curInfo, $touchedCallback)
unwrap( $wrapped, $now)
const TYPE_TIMESTAMP
Single character component for timestamp check keys.
worthRefreshPopular( $asOf, $ageNew, $timeTillRefresh, $now)
Check if a key is due for randomized regeneration due to its popularity.
fetchOrRegenerate( $key, $ttl, $callback, array $opts, array $cbParams)
Do the actual I/O for getWithSetCallback() when needed.
multiRemap(array $ids, array $res)
Get an (ID => value) map from (i) a non-unique list of entity IDs, and (ii) the list of corresponding...
const FLD_FORMAT_VERSION
Key to WAN cache version number.
determineKeyClassForStats( $key)
const SCHEME_HASH_STOP
Use mcrouter-style Hash Stop key scheme (e.g.
const RES_VALUE
The key value component of a fetchMulti() result.
prependRoute(string $sisterKey, string $route)
touchCheckKey( $key, $holdoff=self::HOLDOFF_TTL)
Purge a "check" key from all datacenters, invalidating keys that use it.
const FLD_VALUE
Key to the cached value.
const PURGE_HOLDOFF
Key to the tombstone entry hold-off TTL.
isValid( $value, $asOf, $minAsOf, $purgeTime=null)
Check if $value is not false, versioned (if needed), and not older than $minTime (if set)
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".
isVolatileValueAgeNegligible( $age)
string null $onHostRoute
Routing prefix for value keys that support use of an on-host tier.
int $warmupKeyMisses
Key fetched.
float null $wallClockOverride
relayVolatilePurges(array $purgeBySisterKey, int $ttl)
Set a sister key to a purge value in all datacenters.
mixed[] $warmupCache
Temporary warm-up cache.
const VERSION
Cache format version number.
const LOW_TTL
Consider regeneration if the key will expire within this many seconds.
getLastError()
Get the "last error" registered; clearLastError() should be called manually.
BagOStuff $cache
The local datacenter cache.
parsePurgeValue( $value)
Extract purge metadata from cached value if it is a valid purge value.
scheduleAsyncRefresh( $key, $ttl, $callback, array $opts, array $cbParams)
Schedule a deferred cache regeneration if possible.
const GENERATION_SLOW_SEC
Consider value generation slow if it takes more than this many seconds.
const COOLOFF_TTL
Seconds to no-op key set() calls to avoid large blob I/O stampedes.
getWithSetCallback( $key, $ttl, $callback, array $opts=[], array $cbParams=[])
Method to fetch/regenerate a cache key.
getCheckKeyTime( $key)
Fetch the value of a timestamp "check" key.
getNonProcessCachedMultiKeys(ArrayIterator $keys, array $opts)
const SCHEME_HASH_TAG
Use twemproxy-style Hash Tag key scheme (e.g.
const RECENT_SET_HIGH_MS
Max millisecond set() backoff during hold-off (far less than INTERIM_KEY_TTL)
const LOCK_TTL
Seconds to keep lock keys around.
getMulti(array $keys, &$curTTLs=[], array $checkKeys=[], &$info=[])
Fetch the value of several keys from cache.
const PC_PRIMARY
Default process cache name and max key count.
getMultiWithUnionSetCallback(ArrayIterator $keyedIds, $ttl, callable $callback, array $opts=[])
Method to fetch/regenerate multiple cache keys at once.
LoggerInterface $logger
const TYPE_MUTEX
Single character component for mutex lock keys.
relayNonVolatilePurge(string $sisterKey)
Remove a sister key from all datacenters.
getMultiWithSetCallback(ArrayIterator $keyedIds, $ttl, callable $callback, array $opts=[])
Method to fetch multiple cache keys at once with regeneration.
wrap( $value, $ttl, $version, $now, $walltime)
const PURGE_VAL_PREFIX
Value prefix of purge values.
const INTERIM_KEY_TTL
Seconds to keep interim value keys for tombstoned keys around.
makeMultiKeys(array $ids, $keyCallback)
Get an iterator of (cache key => entity ID) for a list of entity IDs.
makeTombstonePurgeValue(float $timestamp)
static newEmpty()
Get an instance that wraps EmptyBagOStuff.
int $coalesceScheme
Scheme to use for key coalescing (Hash Tags or Hash Stops)
const FLD_TTL
Key to the original TTL.
bool $useInterimHoldOffCaching
Whether to use "interim" caching while keys are tombstoned.
worthRefreshExpiring( $curTTL, $logicalTTL, $lowTTL)
Check if a key is nearing expiration and thus due for randomized regeneration.
const FLD_FLAGS
Key to the flags bit field (reserved number)
const HOLDOFF_TTL_NONE
Idiom for delete()/touchCheckKey() meaning "no hold-off period".
const TYPE_FLUX
Single character component for flux keys.
const MAX_READ_LAG
Max expected seconds of combined lag from replication and view snapshots.
resolveTouched( $value, $lastPurge, $touchedCallback)
setInterimValue( $key, $value, $ttl, $version, $walltime)
const FLD_TIME
Key to the cache timestamp.
const CHECK_KEY_TTL
Seconds to keep dependency purge keys around.
useInterimHoldOffCaching( $enabled)
Enable or disable the use of brief caching for tombstoned keys.
StatsdDataFactoryInterface $stats
makeSisterKeys(array $baseKeys, string $type, string $route=null)
Get sister keys that should be collocated with their corresponding base cache keys.
clearProcessCache()
Clear the in-process caches; useful for testing.
const KEY_AS_OF
Generation timestamp attribute for a key; keep value for b/c (< 1.36)
const TYPE_COOLOFF
Single character component for cool-off bounce keys.
const FLD_GENERATION_TIME
Key to how long it took to generate the value.
makeSisterKey(string $baseKey, string $typeChar, string $route=null)
Get a sister key that should be collocated with a base cache key.
makeKey( $collection,... $components)
Make a cache key using the "global" keyspace for the given components.
float $epoch
Unix timestamp of the oldest possible valid values.
fetchWrappedValuesForWarmupCache(array $keys, array $checkKeys)
callable null $asyncHandler
Function that takes a WAN cache callback and runs it later.
string null $broadcastRoute
Routing prefix for values that should be broadcasted to all data centers.
resolveBusyValue( $busyValue)
reap( $key, $purgeTimestamp, &$isStale=false)
Set a key to soon expire in the local cluster if it pre-dates $purgeTimestamp.
const RECENT_SET_LOW_MS
Min millisecond set() backoff during hold-off (far less than INTERIM_KEY_TTL)
setLogger(LoggerInterface $logger)
processFluxKeys(array $keys, array $fluxSisterKeys, array $wrappedBySisterKey)
static getCollectionFromSisterKey(string $sisterKey)
reapCheckKey( $key, $purgeTimestamp, &$isStale=false)
Set a "check" key to soon expire in the local cluster if it pre-dates $purgeTimestamp.
const TYPE_INTERIM
Single character component for interium value keys.
const PASS_BY_REF
Idiom for get()/getMulti() to return extra information by reference.
checkAndSetCooloff( $key, $kClass, $value, $elapsed, $hasLock)
Check whether set() is rate-limited to avoid concurrent I/O spikes.
float $keyHighUplinkBps
Max tolerable bytes/second to spend on a cache write stampede for a key.
const KEY_CHECK_AS_OF
Highest "check" key timestamp for a key; keep value for b/c (< 1.36)
processCheckKeys(array $checkSisterKeys, array $wrappedBySisterKey, float $now)
clearLastError()
Clear the "last error" registry.
const STALE_TTL_NONE
Idiom for set()/getWithSetCallback() meaning "no post-expiration persistence".
MapCacheLRU[] $processCaches
Map of group PHP instance caches.
const TSE_NONE
Idiom for getWithSetCallback() meaning "no cache stampede mutex".
string $secret
Stable secret used for hasing long strings into key components.
fetchKeys(array $keys, array $checkKeys)
Fetch the value and key metadata of several keys from cache.
const TYPE_VALUE
Single character component for value keys.
resetCheckKey( $key)
Delete a "check" key from all datacenters, invalidating keys that use it.
const KEY_TOMB_AS_OF
Tomstone timestamp attribute for a key; keep value for b/c (< 1.36)
int $keyHighQps
Reads/second assumed during a hypothetical cache write stampede for a key.
const MAX_COMMIT_DELAY
Max expected seconds to pass between delete() and DB commit finishing.
const KEY_CUR_TTL
Remaining TTL attribute for a key; keep value for b/c (< 1.36)
const AGE_NEW
Minimum key age, in seconds, for expected time-till-refresh to be considered.
getInterimValue( $key, $minAsOf)
yieldStampedeLock( $key, $hasLock)
const RAMPUP_TTL
Seconds to ramp up the chance of regeneration due to expected time-till-refresh.
const TTL_LAGGED
Max TTL, in seconds, to store keys when a data sourced is lagged.
const FLD_VALUE_VERSION
Key to collection cache version number.
isAliveOrInGracePeriod( $curTTL, $graceTTL)
Check if a key is fresh or in the grace window and thus due for randomized reuse.
hash256( $component)
Hash a possibly long string into a suitable component for makeKey()/makeGlobalKey()
getMultiCheckKeyTime(array $keys)
Fetch the values of each timestamp "check" key.
makeCheckPurgeValue(float $timestamp, int $holdoff, array &$purge=null)
const KEY_TTL
Logical TTL attribute for a key.
Generic interface for object stores with key encoding methods.
Generic interface providing Time-To-Live constants for expirable object storage.
Generic interface providing error code and quality-of-service constants for object stores.
const ERR_UNREACHABLE
Storage medium could not be reached.
const ERR_NO_RESPONSE
Storage medium failed to yield a response.
$cache
Definition mcc.php:33