MediaWiki REL1_30
WANObjectCache.php
Go to the documentation of this file.
1<?php
22use Psr\Log\LoggerAwareInterface;
23use Psr\Log\LoggerInterface;
24use Psr\Log\NullLogger;
25
80class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
82 protected $cache;
84 protected $processCaches = [];
86 protected $purgeChannel;
88 protected $purgeRelayer;
90 protected $logger;
91
93 protected $lastRelayError = self::ERR_NONE;
94
96 private $callbackDepth = 0;
98 private $warmupCache = [];
100 private $warmupKeyMisses = 0;
101
105 const MAX_READ_LAG = 7;
107 const HOLDOFF_TTL = 11; // MAX_COMMIT_DELAY + MAX_READ_LAG + 1
108
110 const CHECK_KEY_TTL = self::TTL_YEAR;
112 const LOCK_TTL = 10;
114 const LOW_TTL = 30;
116 const LOCK_TSE = 1;
117
119 const AGE_NEW = 60;
121 const HOT_TTR = 900;
123 const HIT_RATE_HIGH = 1;
125 const RAMPUP_TTL = 30;
126
128 const TTL_UNCACHEABLE = -1;
130 const TSE_NONE = -1;
132 const TTL_LAGGED = 30;
134 const HOLDOFF_NONE = 0;
137
139 const TINY_NEGATIVE = -0.000001;
140
142 const VERSION = 1;
143
144 const FLD_VERSION = 0; // key to cache version number
145 const FLD_VALUE = 1; // key to the cached value
146 const FLD_TTL = 2; // key to the original TTL
147 const FLD_TIME = 3; // key to the cache time
148 const FLD_FLAGS = 4; // key to the flags bitfield
149 const FLD_HOLDOFF = 5; // key to any hold-off TTL
150
152 const FLG_STALE = 1;
153
154 const ERR_NONE = 0; // no error
155 const ERR_NO_RESPONSE = 1; // no response
156 const ERR_UNREACHABLE = 2; // can't connect
157 const ERR_UNEXPECTED = 3; // response gave some error
158 const ERR_RELAY = 4; // relay broadcast failed
159
160 const VALUE_KEY_PREFIX = 'WANCache:v:';
161 const INTERIM_KEY_PREFIX = 'WANCache:i:';
162 const TIME_KEY_PREFIX = 'WANCache:t:';
163 const MUTEX_KEY_PREFIX = 'WANCache:m:';
164
165 const PURGE_VAL_PREFIX = 'PURGED:';
166
167 const VFLD_DATA = 'WOC:d'; // key to the value of versioned data
168 const VFLD_VERSION = 'WOC:v'; // key to the version of the value present
169
170 const PC_PRIMARY = 'primary:1000'; // process cache name and max key count
171
172 const DEFAULT_PURGE_CHANNEL = 'wancache-purge';
173
181 public function __construct( array $params ) {
182 $this->cache = $params['cache'];
183 $this->purgeChannel = isset( $params['channels']['purge'] )
184 ? $params['channels']['purge']
185 : self::DEFAULT_PURGE_CHANNEL;
186 $this->purgeRelayer = isset( $params['relayers']['purge'] )
187 ? $params['relayers']['purge']
188 : new EventRelayerNull( [] );
189 $this->setLogger( isset( $params['logger'] ) ? $params['logger'] : new NullLogger() );
190 }
191
192 public function setLogger( LoggerInterface $logger ) {
193 $this->logger = $logger;
194 }
195
201 public static function newEmpty() {
202 return new self( [
203 'cache' => new EmptyBagOStuff(),
204 'pool' => 'empty'
205 ] );
206 }
207
248 final public function get( $key, &$curTTL = null, array $checkKeys = [], &$asOf = null ) {
249 $curTTLs = [];
250 $asOfs = [];
251 $values = $this->getMulti( [ $key ], $curTTLs, $checkKeys, $asOfs );
252 $curTTL = isset( $curTTLs[$key] ) ? $curTTLs[$key] : null;
253 $asOf = isset( $asOfs[$key] ) ? $asOfs[$key] : null;
254
255 return isset( $values[$key] ) ? $values[$key] : false;
256 }
257
270 final public function getMulti(
271 array $keys, &$curTTLs = [], array $checkKeys = [], array &$asOfs = []
272 ) {
273 $result = [];
274 $curTTLs = [];
275 $asOfs = [];
276
277 $vPrefixLen = strlen( self::VALUE_KEY_PREFIX );
278 $valueKeys = self::prefixCacheKeys( $keys, self::VALUE_KEY_PREFIX );
279
280 $checkKeysForAll = [];
281 $checkKeysByKey = [];
282 $checkKeysFlat = [];
283 foreach ( $checkKeys as $i => $checkKeyGroup ) {
284 $prefixed = self::prefixCacheKeys( (array)$checkKeyGroup, self::TIME_KEY_PREFIX );
285 $checkKeysFlat = array_merge( $checkKeysFlat, $prefixed );
286 // Is this check keys for a specific cache key, or for all keys being fetched?
287 if ( is_int( $i ) ) {
288 $checkKeysForAll = array_merge( $checkKeysForAll, $prefixed );
289 } else {
290 $checkKeysByKey[$i] = isset( $checkKeysByKey[$i] )
291 ? array_merge( $checkKeysByKey[$i], $prefixed )
292 : $prefixed;
293 }
294 }
295
296 // Fetch all of the raw values
297 $keysGet = array_merge( $valueKeys, $checkKeysFlat );
298 if ( $this->warmupCache ) {
299 $wrappedValues = array_intersect_key( $this->warmupCache, array_flip( $keysGet ) );
300 $keysGet = array_diff( $keysGet, array_keys( $wrappedValues ) ); // keys left to fetch
301 $this->warmupKeyMisses += count( $keysGet );
302 } else {
303 $wrappedValues = [];
304 }
305 if ( $keysGet ) {
306 $wrappedValues += $this->cache->getMulti( $keysGet );
307 }
308 // Time used to compare/init "check" keys (derived after getMulti() to be pessimistic)
309 $now = microtime( true );
310
311 // Collect timestamps from all "check" keys
312 $purgeValuesForAll = $this->processCheckKeys( $checkKeysForAll, $wrappedValues, $now );
313 $purgeValuesByKey = [];
314 foreach ( $checkKeysByKey as $cacheKey => $checks ) {
315 $purgeValuesByKey[$cacheKey] =
316 $this->processCheckKeys( $checks, $wrappedValues, $now );
317 }
318
319 // Get the main cache value for each key and validate them
320 foreach ( $valueKeys as $vKey ) {
321 if ( !isset( $wrappedValues[$vKey] ) ) {
322 continue; // not found
323 }
324
325 $key = substr( $vKey, $vPrefixLen ); // unprefix
326
327 list( $value, $curTTL ) = $this->unwrap( $wrappedValues[$vKey], $now );
328 if ( $value !== false ) {
329 $result[$key] = $value;
330
331 // Force dependant keys to be invalid for a while after purging
332 // to reduce race conditions involving stale data getting cached
333 $purgeValues = $purgeValuesForAll;
334 if ( isset( $purgeValuesByKey[$key] ) ) {
335 $purgeValues = array_merge( $purgeValues, $purgeValuesByKey[$key] );
336 }
337 foreach ( $purgeValues as $purge ) {
338 $safeTimestamp = $purge[self::FLD_TIME] + $purge[self::FLD_HOLDOFF];
339 if ( $safeTimestamp >= $wrappedValues[$vKey][self::FLD_TIME] ) {
340 // How long ago this value was expired by *this* check key
341 $ago = min( $purge[self::FLD_TIME] - $now, self::TINY_NEGATIVE );
342 // How long ago this value was expired by *any* known check key
343 $curTTL = min( $curTTL, $ago );
344 }
345 }
346 }
347 $curTTLs[$key] = $curTTL;
348 $asOfs[$key] = ( $value !== false ) ? $wrappedValues[$vKey][self::FLD_TIME] : null;
349 }
350
351 return $result;
352 }
353
361 private function processCheckKeys( array $timeKeys, array $wrappedValues, $now ) {
362 $purgeValues = [];
363 foreach ( $timeKeys as $timeKey ) {
364 $purge = isset( $wrappedValues[$timeKey] )
365 ? self::parsePurgeValue( $wrappedValues[$timeKey] )
366 : false;
367 if ( $purge === false ) {
368 // Key is not set or invalid; regenerate
369 $newVal = $this->makePurgeValue( $now, self::HOLDOFF_TTL );
370 $this->cache->add( $timeKey, $newVal, self::CHECK_KEY_TTL );
371 $purge = self::parsePurgeValue( $newVal );
372 }
373 $purgeValues[] = $purge;
374 }
375 return $purgeValues;
376 }
377
436 final public function set( $key, $value, $ttl = 0, array $opts = [] ) {
437 $now = microtime( true );
438 $lockTSE = isset( $opts['lockTSE'] ) ? $opts['lockTSE'] : self::TSE_NONE;
439 $age = isset( $opts['since'] ) ? max( 0, $now - $opts['since'] ) : 0;
440 $lag = isset( $opts['lag'] ) ? $opts['lag'] : 0;
441 $staleTTL = isset( $opts['staleTTL'] ) ? $opts['staleTTL'] : 0;
442
443 // Do not cache potentially uncommitted data as it might get rolled back
444 if ( !empty( $opts['pending'] ) ) {
445 $this->logger->info( "Rejected set() for $key due to pending writes." );
446
447 return true; // no-op the write for being unsafe
448 }
449
450 $wrapExtra = []; // additional wrapped value fields
451 // Check if there's a risk of writing stale data after the purge tombstone expired
452 if ( $lag === false || ( $lag + $age ) > self::MAX_READ_LAG ) {
453 // Case A: read lag with "lockTSE"; save but record value as stale
454 if ( $lockTSE >= 0 ) {
455 $ttl = max( 1, (int)$lockTSE ); // set() expects seconds
456 $wrapExtra[self::FLD_FLAGS] = self::FLG_STALE; // mark as stale
457 // Case B: any long-running transaction; ignore this set()
458 } elseif ( $age > self::MAX_READ_LAG ) {
459 $this->logger->info( "Rejected set() for $key due to snapshot lag." );
460
461 return true; // no-op the write for being unsafe
462 // Case C: high replication lag; lower TTL instead of ignoring all set()s
463 } elseif ( $lag === false || $lag > self::MAX_READ_LAG ) {
464 $ttl = $ttl ? min( $ttl, self::TTL_LAGGED ) : self::TTL_LAGGED;
465 $this->logger->warning( "Lowered set() TTL for $key due to replication lag." );
466 // Case D: medium length request with medium replication lag; ignore this set()
467 } else {
468 $this->logger->info( "Rejected set() for $key due to high read lag." );
469
470 return true; // no-op the write for being unsafe
471 }
472 }
473
474 // Wrap that value with time/TTL/version metadata
475 $wrapped = $this->wrap( $value, $ttl, $now ) + $wrapExtra;
476
477 $func = function ( $cache, $key, $cWrapped ) use ( $wrapped ) {
478 return ( is_string( $cWrapped ) )
479 ? false // key is tombstoned; do nothing
480 : $wrapped;
481 };
482
483 return $this->cache->merge( self::VALUE_KEY_PREFIX . $key, $func, $ttl + $staleTTL, 1 );
484 }
485
543 final public function delete( $key, $ttl = self::HOLDOFF_TTL ) {
544 $key = self::VALUE_KEY_PREFIX . $key;
545
546 if ( $ttl <= 0 ) {
547 // Publish the purge to all datacenters
548 $ok = $this->relayDelete( $key );
549 } else {
550 // Publish the purge to all datacenters
551 $ok = $this->relayPurge( $key, $ttl, self::HOLDOFF_NONE );
552 }
553
554 return $ok;
555 }
556
576 final public function getCheckKeyTime( $key ) {
577 $key = self::TIME_KEY_PREFIX . $key;
578
579 $purge = self::parsePurgeValue( $this->cache->get( $key ) );
580 if ( $purge !== false ) {
581 $time = $purge[self::FLD_TIME];
582 } else {
583 // Casting assures identical floats for the next getCheckKeyTime() calls
584 $now = (string)microtime( true );
585 $this->cache->add( $key,
586 $this->makePurgeValue( $now, self::HOLDOFF_TTL ),
587 self::CHECK_KEY_TTL
588 );
589 $time = (float)$now;
590 }
591
592 return $time;
593 }
594
628 final public function touchCheckKey( $key, $holdoff = self::HOLDOFF_TTL ) {
629 // Publish the purge to all datacenters
630 return $this->relayPurge( self::TIME_KEY_PREFIX . $key, self::CHECK_KEY_TTL, $holdoff );
631 }
632
663 final public function resetCheckKey( $key ) {
664 // Publish the purge to all datacenters
665 return $this->relayDelete( self::TIME_KEY_PREFIX . $key );
666 }
667
857 final public function getWithSetCallback( $key, $ttl, $callback, array $opts = [] ) {
858 $pcTTL = isset( $opts['pcTTL'] ) ? $opts['pcTTL'] : self::TTL_UNCACHEABLE;
859
860 // Try the process cache if enabled and the cache callback is not within a cache callback.
861 // Process cache use in nested callbacks is not lag-safe with regard to HOLDOFF_TTL since
862 // the in-memory value is further lagged than the shared one since it uses a blind TTL.
863 if ( $pcTTL >= 0 && $this->callbackDepth == 0 ) {
864 $group = isset( $opts['pcGroup'] ) ? $opts['pcGroup'] : self::PC_PRIMARY;
865 $procCache = $this->getProcessCache( $group );
866 $value = $procCache->get( $key );
867 } else {
868 $procCache = false;
869 $value = false;
870 }
871
872 if ( $value === false ) {
873 // Fetch the value over the network
874 if ( isset( $opts['version'] ) ) {
875 $version = $opts['version'];
876 $asOf = null;
877 $cur = $this->doGetWithSetCallback(
878 $key,
879 $ttl,
880 function ( $oldValue, &$ttl, &$setOpts, $oldAsOf )
881 use ( $callback, $version ) {
882 if ( is_array( $oldValue )
883 && array_key_exists( self::VFLD_DATA, $oldValue )
884 ) {
885 $oldData = $oldValue[self::VFLD_DATA];
886 } else {
887 // VFLD_DATA is not set if an old, unversioned, key is present
888 $oldData = false;
889 }
890
891 return [
892 self::VFLD_DATA => $callback( $oldData, $ttl, $setOpts, $oldAsOf ),
893 self::VFLD_VERSION => $version
894 ];
895 },
896 $opts,
897 $asOf
898 );
899 if ( $cur[self::VFLD_VERSION] === $version ) {
900 // Value created or existed before with version; use it
901 $value = $cur[self::VFLD_DATA];
902 } else {
903 // Value existed before with a different version; use variant key.
904 // Reflect purges to $key by requiring that this key value be newer.
906 'cache-variant:' . md5( $key ) . ":$version",
907 $ttl,
908 $callback,
909 // Regenerate value if not newer than $key
910 [ 'version' => null, 'minAsOf' => $asOf ] + $opts
911 );
912 }
913 } else {
914 $value = $this->doGetWithSetCallback( $key, $ttl, $callback, $opts );
915 }
916
917 // Update the process cache if enabled
918 if ( $procCache && $value !== false ) {
919 $procCache->set( $key, $value, $pcTTL );
920 }
921 }
922
923 return $value;
924 }
925
939 protected function doGetWithSetCallback( $key, $ttl, $callback, array $opts, &$asOf = null ) {
940 $lowTTL = isset( $opts['lowTTL'] ) ? $opts['lowTTL'] : min( self::LOW_TTL, $ttl );
941 $lockTSE = isset( $opts['lockTSE'] ) ? $opts['lockTSE'] : self::TSE_NONE;
942 $checkKeys = isset( $opts['checkKeys'] ) ? $opts['checkKeys'] : [];
943 $busyValue = isset( $opts['busyValue'] ) ? $opts['busyValue'] : null;
944 $popWindow = isset( $opts['hotTTR'] ) ? $opts['hotTTR'] : self::HOT_TTR;
945 $ageNew = isset( $opts['ageNew'] ) ? $opts['ageNew'] : self::AGE_NEW;
946 $minTime = isset( $opts['minAsOf'] ) ? $opts['minAsOf'] : self::MIN_TIMESTAMP_NONE;
947 $versioned = isset( $opts['version'] );
948
949 // Get the current key value
950 $curTTL = null;
951 $cValue = $this->get( $key, $curTTL, $checkKeys, $asOf ); // current value
952 $value = $cValue; // return value
953
954 $preCallbackTime = microtime( true );
955 // Determine if a cached value regeneration is needed or desired
956 if ( $value !== false
957 && $curTTL > 0
958 && $this->isValid( $value, $versioned, $asOf, $minTime )
959 && !$this->worthRefreshExpiring( $curTTL, $lowTTL )
960 && !$this->worthRefreshPopular( $asOf, $ageNew, $popWindow, $preCallbackTime )
961 ) {
962 return $value;
963 }
964
965 // A deleted key with a negative TTL left must be tombstoned
966 $isTombstone = ( $curTTL !== null && $value === false );
967 // Assume a key is hot if requested soon after invalidation
968 $isHot = ( $curTTL !== null && $curTTL <= 0 && abs( $curTTL ) <= $lockTSE );
969 // Use the mutex if there is no value and a busy fallback is given
970 $checkBusy = ( $busyValue !== null && $value === false );
971 // Decide whether a single thread should handle regenerations.
972 // This avoids stampedes when $checkKeys are bumped and when preemptive
973 // renegerations take too long. It also reduces regenerations while $key
974 // is tombstoned. This balances cache freshness with avoiding DB load.
975 $useMutex = ( $isHot || ( $isTombstone && $lockTSE > 0 ) || $checkBusy );
976
977 $lockAcquired = false;
978 if ( $useMutex ) {
979 // Acquire a datacenter-local non-blocking lock
980 if ( $this->cache->add( self::MUTEX_KEY_PREFIX . $key, 1, self::LOCK_TTL ) ) {
981 // Lock acquired; this thread should update the key
982 $lockAcquired = true;
983 } elseif ( $value !== false && $this->isValid( $value, $versioned, $asOf, $minTime ) ) {
984 // If it cannot be acquired; then the stale value can be used
985 return $value;
986 } else {
987 // Use the INTERIM value for tombstoned keys to reduce regeneration load.
988 // For hot keys, either another thread has the lock or the lock failed;
989 // use the INTERIM value from the last thread that regenerated it.
990 $wrapped = $this->cache->get( self::INTERIM_KEY_PREFIX . $key );
991 list( $value ) = $this->unwrap( $wrapped, microtime( true ) );
992 if ( $value !== false && $this->isValid( $value, $versioned, $asOf, $minTime ) ) {
993 $asOf = $wrapped[self::FLD_TIME];
994
995 return $value;
996 }
997 // Use the busy fallback value if nothing else
998 if ( $busyValue !== null ) {
999 return is_callable( $busyValue ) ? $busyValue() : $busyValue;
1000 }
1001 }
1002 }
1003
1004 if ( !is_callable( $callback ) ) {
1005 throw new InvalidArgumentException( "Invalid cache miss callback provided." );
1006 }
1007
1008 // Generate the new value from the callback...
1009 $setOpts = [];
1010 ++$this->callbackDepth;
1011 try {
1012 $value = call_user_func_array( $callback, [ $cValue, &$ttl, &$setOpts, $asOf ] );
1013 } finally {
1014 --$this->callbackDepth;
1015 }
1016 // When delete() is called, writes are write-holed by the tombstone,
1017 // so use a special INTERIM key to pass the new value around threads.
1018 if ( ( $isTombstone && $lockTSE > 0 ) && $value !== false && $ttl >= 0 ) {
1019 $tempTTL = max( 1, (int)$lockTSE ); // set() expects seconds
1020 $newAsOf = microtime( true );
1021 $wrapped = $this->wrap( $value, $tempTTL, $newAsOf );
1022 // Avoid using set() to avoid pointless mcrouter broadcasting
1023 $this->cache->merge(
1024 self::INTERIM_KEY_PREFIX . $key,
1025 function () use ( $wrapped ) {
1026 return $wrapped;
1027 },
1028 $tempTTL,
1029 1
1030 );
1031 }
1032
1033 if ( $value !== false && $ttl >= 0 ) {
1034 $setOpts['lockTSE'] = $lockTSE;
1035 // Use best known "since" timestamp if not provided
1036 $setOpts += [ 'since' => $preCallbackTime ];
1037 // Update the cache; this will fail if the key is tombstoned
1038 $this->set( $key, $value, $ttl, $setOpts );
1039 }
1040
1041 if ( $lockAcquired ) {
1042 // Avoid using delete() to avoid pointless mcrouter broadcasting
1043 $this->cache->changeTTL( self::MUTEX_KEY_PREFIX . $key, 1 );
1044 }
1045
1046 return $value;
1047 }
1048
1107 final public function getMultiWithSetCallback(
1108 ArrayIterator $keyedIds, $ttl, callable $callback, array $opts = []
1109 ) {
1110 $valueKeys = array_keys( $keyedIds->getArrayCopy() );
1111 $checkKeys = isset( $opts['checkKeys'] ) ? $opts['checkKeys'] : [];
1112
1113 // Load required keys into process cache in one go
1114 $this->warmupCache = $this->getRawKeysForWarmup(
1115 $this->getNonProcessCachedKeys( $valueKeys, $opts ),
1116 $checkKeys
1117 );
1118 $this->warmupKeyMisses = 0;
1119
1120 // Wrap $callback to match the getWithSetCallback() format while passing $id to $callback
1121 $id = null; // current entity ID
1122 $func = function ( $oldValue, &$ttl, &$setOpts, $oldAsOf ) use ( $callback, &$id ) {
1123 return $callback( $id, $oldValue, $ttl, $setOpts, $oldAsOf );
1124 };
1125
1126 $values = [];
1127 foreach ( $keyedIds as $key => $id ) { // preserve order
1128 $values[$key] = $this->getWithSetCallback( $key, $ttl, $func, $opts );
1129 }
1130
1131 $this->warmupCache = [];
1132
1133 return $values;
1134 }
1135
1193 final public function getMultiWithUnionSetCallback(
1194 ArrayIterator $keyedIds, $ttl, callable $callback, array $opts = []
1195 ) {
1196 $idsByValueKey = $keyedIds->getArrayCopy();
1197 $valueKeys = array_keys( $idsByValueKey );
1198 $checkKeys = isset( $opts['checkKeys'] ) ? $opts['checkKeys'] : [];
1199 unset( $opts['lockTSE'] ); // incompatible
1200 unset( $opts['busyValue'] ); // incompatible
1201
1202 // Load required keys into process cache in one go
1203 $keysGet = $this->getNonProcessCachedKeys( $valueKeys, $opts );
1204 $this->warmupCache = $this->getRawKeysForWarmup( $keysGet, $checkKeys );
1205 $this->warmupKeyMisses = 0;
1206
1207 // IDs of entities known to be in need of regeneration
1208 $idsRegen = [];
1209
1210 // Find out which keys are missing/deleted/stale
1211 $curTTLs = [];
1212 $asOfs = [];
1213 $curByKey = $this->getMulti( $keysGet, $curTTLs, $checkKeys, $asOfs );
1214 foreach ( $keysGet as $key ) {
1215 if ( !array_key_exists( $key, $curByKey ) || $curTTLs[$key] < 0 ) {
1216 $idsRegen[] = $idsByValueKey[$key];
1217 }
1218 }
1219
1220 // Run the callback to populate the regeneration value map for all required IDs
1221 $newSetOpts = [];
1222 $newTTLsById = array_fill_keys( $idsRegen, $ttl );
1223 $newValsById = $idsRegen ? $callback( $idsRegen, $newTTLsById, $newSetOpts ) : [];
1224
1225 // Wrap $callback to match the getWithSetCallback() format while passing $id to $callback
1226 $id = null; // current entity ID
1227 $func = function ( $oldValue, &$ttl, &$setOpts, $oldAsOf )
1228 use ( $callback, &$id, $newValsById, $newTTLsById, $newSetOpts )
1229 {
1230 if ( array_key_exists( $id, $newValsById ) ) {
1231 // Value was already regerated as expected, so use the value in $newValsById
1232 $newValue = $newValsById[$id];
1233 $ttl = $newTTLsById[$id];
1234 $setOpts = $newSetOpts;
1235 } else {
1236 // Pre-emptive/popularity refresh and version mismatch cases are not detected
1237 // above and thus $newValsById has no entry. Run $callback on this single entity.
1238 $ttls = [ $id => $ttl ];
1239 $newValue = $callback( [ $id ], $ttls, $setOpts )[$id];
1240 $ttl = $ttls[$id];
1241 }
1242
1243 return $newValue;
1244 };
1245
1246 // Run the cache-aside logic using warmupCache instead of persistent cache queries
1247 $values = [];
1248 foreach ( $idsByValueKey as $key => $id ) { // preserve order
1249 $values[$key] = $this->getWithSetCallback( $key, $ttl, $func, $opts );
1250 }
1251
1252 $this->warmupCache = [];
1253
1254 return $values;
1255 }
1256
1269 public function reap( $key, $purgeTimestamp, &$isStale = false ) {
1270 $minAsOf = $purgeTimestamp + self::HOLDOFF_TTL;
1271 $wrapped = $this->cache->get( self::VALUE_KEY_PREFIX . $key );
1272 if ( is_array( $wrapped ) && $wrapped[self::FLD_TIME] < $minAsOf ) {
1273 $isStale = true;
1274 $this->logger->warning( "Reaping stale value key '$key'." );
1275 $ttlReap = self::HOLDOFF_TTL; // avoids races with tombstone creation
1276 $ok = $this->cache->changeTTL( self::VALUE_KEY_PREFIX . $key, $ttlReap );
1277 if ( !$ok ) {
1278 $this->logger->error( "Could not complete reap of key '$key'." );
1279 }
1280
1281 return $ok;
1282 }
1283
1284 $isStale = false;
1285
1286 return true;
1287 }
1288
1298 public function reapCheckKey( $key, $purgeTimestamp, &$isStale = false ) {
1299 $purge = $this->parsePurgeValue( $this->cache->get( self::TIME_KEY_PREFIX . $key ) );
1300 if ( $purge && $purge[self::FLD_TIME] < $purgeTimestamp ) {
1301 $isStale = true;
1302 $this->logger->warning( "Reaping stale check key '$key'." );
1303 $ok = $this->cache->changeTTL( self::TIME_KEY_PREFIX . $key, 1 );
1304 if ( !$ok ) {
1305 $this->logger->error( "Could not complete reap of check key '$key'." );
1306 }
1307
1308 return $ok;
1309 }
1310
1311 $isStale = false;
1312
1313 return false;
1314 }
1315
1322 public function makeKey() {
1323 return call_user_func_array( [ $this->cache, __FUNCTION__ ], func_get_args() );
1324 }
1325
1332 public function makeGlobalKey() {
1333 return call_user_func_array( [ $this->cache, __FUNCTION__ ], func_get_args() );
1334 }
1335
1342 public function makeMultiKeys( array $entities, callable $keyFunc ) {
1343 $map = [];
1344 foreach ( $entities as $entity ) {
1345 $map[$keyFunc( $entity, $this )] = $entity;
1346 }
1347
1348 return new ArrayIterator( $map );
1349 }
1350
1355 final public function getLastError() {
1356 if ( $this->lastRelayError ) {
1357 // If the cache and the relayer failed, focus on the latter.
1358 // An update not making it to the relayer means it won't show up
1359 // in other DCs (nor will consistent re-hashing see up-to-date values).
1360 // On the other hand, if just the cache update failed, then it should
1361 // eventually be applied by the relayer.
1362 return $this->lastRelayError;
1363 }
1364
1365 $code = $this->cache->getLastError();
1366 switch ( $code ) {
1367 case BagOStuff::ERR_NONE:
1368 return self::ERR_NONE;
1369 case BagOStuff::ERR_NO_RESPONSE:
1370 return self::ERR_NO_RESPONSE;
1371 case BagOStuff::ERR_UNREACHABLE:
1372 return self::ERR_UNREACHABLE;
1373 default:
1374 return self::ERR_UNEXPECTED;
1375 }
1376 }
1377
1381 final public function clearLastError() {
1382 $this->cache->clearLastError();
1383 $this->lastRelayError = self::ERR_NONE;
1384 }
1385
1391 public function clearProcessCache() {
1392 $this->processCaches = [];
1393 }
1394
1400 public function getQoS( $flag ) {
1401 return $this->cache->getQoS( $flag );
1402 }
1403
1427 public function adaptiveTTL( $mtime, $maxTTL, $minTTL = 30, $factor = 0.2 ) {
1428 if ( is_float( $mtime ) || ctype_digit( $mtime ) ) {
1429 $mtime = (int)$mtime; // handle fractional seconds and string integers
1430 }
1431
1432 if ( !is_int( $mtime ) || $mtime <= 0 ) {
1433 return $minTTL; // no last-modified time provided
1434 }
1435
1436 $age = time() - $mtime;
1437
1438 return (int)min( $maxTTL, max( $minTTL, $factor * $age ) );
1439 }
1440
1445 public function getWarmupKeyMisses() {
1446 return $this->warmupKeyMisses;
1447 }
1448
1459 protected function relayPurge( $key, $ttl, $holdoff ) {
1460 if ( $this->purgeRelayer instanceof EventRelayerNull ) {
1461 // This handles the mcrouter and the single-DC case
1462 $ok = $this->cache->set( $key,
1463 $this->makePurgeValue( microtime( true ), self::HOLDOFF_NONE ),
1464 $ttl
1465 );
1466 } else {
1467 $event = $this->cache->modifySimpleRelayEvent( [
1468 'cmd' => 'set',
1469 'key' => $key,
1470 'val' => 'PURGED:$UNIXTIME$:' . (int)$holdoff,
1471 'ttl' => max( $ttl, 1 ),
1472 'sbt' => true, // substitute $UNIXTIME$ with actual microtime
1473 ] );
1474
1475 $ok = $this->purgeRelayer->notify( $this->purgeChannel, $event );
1476 if ( !$ok ) {
1477 $this->lastRelayError = self::ERR_RELAY;
1478 }
1479 }
1480
1481 return $ok;
1482 }
1483
1490 protected function relayDelete( $key ) {
1491 if ( $this->purgeRelayer instanceof EventRelayerNull ) {
1492 // This handles the mcrouter and the single-DC case
1493 $ok = $this->cache->delete( $key );
1494 } else {
1495 $event = $this->cache->modifySimpleRelayEvent( [
1496 'cmd' => 'delete',
1497 'key' => $key,
1498 ] );
1499
1500 $ok = $this->purgeRelayer->notify( $this->purgeChannel, $event );
1501 if ( !$ok ) {
1502 $this->lastRelayError = self::ERR_RELAY;
1503 }
1504 }
1505
1506 return $ok;
1507 }
1508
1521 protected function worthRefreshExpiring( $curTTL, $lowTTL ) {
1522 if ( $curTTL >= $lowTTL ) {
1523 return false;
1524 } elseif ( $curTTL <= 0 ) {
1525 return true;
1526 }
1527
1528 $chance = ( 1 - $curTTL / $lowTTL );
1529
1530 return mt_rand( 1, 1e9 ) <= 1e9 * $chance;
1531 }
1532
1548 protected function worthRefreshPopular( $asOf, $ageNew, $timeTillRefresh, $now ) {
1549 $age = $now - $asOf;
1550 $timeOld = $age - $ageNew;
1551 if ( $timeOld <= 0 ) {
1552 return false;
1553 }
1554
1555 // Lifecycle is: new, ramp-up refresh chance, full refresh chance.
1556 // Note that the "expected # of refreshes" for the ramp-up time range is half of what it
1557 // would be if P(refresh) was at its full value during that time range.
1558 $refreshWindowSec = max( $timeTillRefresh - $ageNew - self::RAMPUP_TTL / 2, 1 );
1559 // P(refresh) * (# hits in $refreshWindowSec) = (expected # of refreshes)
1560 // P(refresh) * ($refreshWindowSec * $popularHitsPerSec) = 1
1561 // P(refresh) = 1/($refreshWindowSec * $popularHitsPerSec)
1562 $chance = 1 / ( self::HIT_RATE_HIGH * $refreshWindowSec );
1563
1564 // Ramp up $chance from 0 to its nominal value over RAMPUP_TTL seconds to avoid stampedes
1565 $chance *= ( $timeOld <= self::RAMPUP_TTL ) ? $timeOld / self::RAMPUP_TTL : 1;
1566
1567 return mt_rand( 1, 1e9 ) <= 1e9 * $chance;
1568 }
1569
1579 protected function isValid( $value, $versioned, $asOf, $minTime ) {
1580 if ( $versioned && !isset( $value[self::VFLD_VERSION] ) ) {
1581 return false;
1582 } elseif ( $minTime > 0 && $asOf < $minTime ) {
1583 return false;
1584 }
1585
1586 return true;
1587 }
1588
1597 protected function wrap( $value, $ttl, $now ) {
1598 return [
1599 self::FLD_VERSION => self::VERSION,
1600 self::FLD_VALUE => $value,
1601 self::FLD_TTL => $ttl,
1602 self::FLD_TIME => $now
1603 ];
1604 }
1605
1613 protected function unwrap( $wrapped, $now ) {
1614 // Check if the value is a tombstone
1615 $purge = self::parsePurgeValue( $wrapped );
1616 if ( $purge !== false ) {
1617 // Purged values should always have a negative current $ttl
1618 $curTTL = min( $purge[self::FLD_TIME] - $now, self::TINY_NEGATIVE );
1619 return [ false, $curTTL ];
1620 }
1621
1622 if ( !is_array( $wrapped ) // not found
1623 || !isset( $wrapped[self::FLD_VERSION] ) // wrong format
1624 || $wrapped[self::FLD_VERSION] !== self::VERSION // wrong version
1625 ) {
1626 return [ false, null ];
1627 }
1628
1629 $flags = isset( $wrapped[self::FLD_FLAGS] ) ? $wrapped[self::FLD_FLAGS] : 0;
1630 if ( ( $flags & self::FLG_STALE ) == self::FLG_STALE ) {
1631 // Treat as expired, with the cache time as the expiration
1632 $age = $now - $wrapped[self::FLD_TIME];
1633 $curTTL = min( -$age, self::TINY_NEGATIVE );
1634 } elseif ( $wrapped[self::FLD_TTL] > 0 ) {
1635 // Get the approximate time left on the key
1636 $age = $now - $wrapped[self::FLD_TIME];
1637 $curTTL = max( $wrapped[self::FLD_TTL] - $age, 0.0 );
1638 } else {
1639 // Key had no TTL, so the time left is unbounded
1640 $curTTL = INF;
1641 }
1642
1643 return [ $wrapped[self::FLD_VALUE], $curTTL ];
1644 }
1645
1651 protected static function prefixCacheKeys( array $keys, $prefix ) {
1652 $res = [];
1653 foreach ( $keys as $key ) {
1654 $res[] = $prefix . $key;
1655 }
1656
1657 return $res;
1658 }
1659
1665 protected static function parsePurgeValue( $value ) {
1666 if ( !is_string( $value ) ) {
1667 return false;
1668 }
1669 $segments = explode( ':', $value, 3 );
1670 if ( !isset( $segments[0] ) || !isset( $segments[1] )
1671 || "{$segments[0]}:" !== self::PURGE_VAL_PREFIX
1672 ) {
1673 return false;
1674 }
1675 if ( !isset( $segments[2] ) ) {
1676 // Back-compat with old purge values without holdoff
1677 $segments[2] = self::HOLDOFF_TTL;
1678 }
1679 return [
1680 self::FLD_TIME => (float)$segments[1],
1681 self::FLD_HOLDOFF => (int)$segments[2],
1682 ];
1683 }
1684
1690 protected function makePurgeValue( $timestamp, $holdoff ) {
1691 return self::PURGE_VAL_PREFIX . (float)$timestamp . ':' . (int)$holdoff;
1692 }
1693
1698 protected function getProcessCache( $group ) {
1699 if ( !isset( $this->processCaches[$group] ) ) {
1700 list( , $n ) = explode( ':', $group );
1701 $this->processCaches[$group] = new HashBagOStuff( [ 'maxKeys' => (int)$n ] );
1702 }
1703
1704 return $this->processCaches[$group];
1705 }
1706
1712 private function getNonProcessCachedKeys( array $keys, array $opts ) {
1713 $keysFound = [];
1714 if ( isset( $opts['pcTTL'] ) && $opts['pcTTL'] > 0 && $this->callbackDepth == 0 ) {
1715 $pcGroup = isset( $opts['pcGroup'] ) ? $opts['pcGroup'] : self::PC_PRIMARY;
1716 $procCache = $this->getProcessCache( $pcGroup );
1717 foreach ( $keys as $key ) {
1718 if ( $procCache->get( $key ) !== false ) {
1719 $keysFound[] = $key;
1720 }
1721 }
1722 }
1723
1724 return array_diff( $keys, $keysFound );
1725 }
1726
1732 private function getRawKeysForWarmup( array $keys, array $checkKeys ) {
1733 if ( !$keys ) {
1734 return [];
1735 }
1736
1737 $keysWarmUp = [];
1738 // Get all the value keys to fetch...
1739 foreach ( $keys as $key ) {
1740 $keysWarmUp[] = self::VALUE_KEY_PREFIX . $key;
1741 }
1742 // Get all the check keys to fetch...
1743 foreach ( $checkKeys as $i => $checkKeyOrKeys ) {
1744 if ( is_int( $i ) ) {
1745 // Single check key that applies to all value keys
1746 $keysWarmUp[] = self::TIME_KEY_PREFIX . $checkKeyOrKeys;
1747 } else {
1748 // List of check keys that apply to value key $i
1749 $keysWarmUp = array_merge(
1750 $keysWarmUp,
1751 self::prefixCacheKeys( $checkKeyOrKeys, self::TIME_KEY_PREFIX )
1752 );
1753 }
1754 }
1755
1756 $warmupCache = $this->cache->getMulti( $keysWarmUp );
1757 $warmupCache += array_fill_keys( $keysWarmUp, false );
1758
1759 return $warmupCache;
1760 }
1761}
interface is intended to be more or less compatible with the PHP memcached client.
Definition BagOStuff.php:47
A BagOStuff object with no objects in it.
No-op class for publishing messages into a PubSub system.
Base class for reliable event relays.
Simple store for keeping values in an associative array for the current process.
Multi-datacenter aware caching interface.
int $callbackDepth
Callback stack depth for getWithSetCallback()
const TINY_NEGATIVE
Tiny negative float to use when CTL comes up >= 0 due to clock skew.
const HOLDOFF_TTL
Seconds to tombstone keys on delete()
const HOT_TTR
The time length of the "popularity" refresh window for hot keys.
__construct(array $params)
unwrap( $wrapped, $now)
Do not use this method outside WANObjectCache.
worthRefreshPopular( $asOf, $ageNew, $timeTillRefresh, $now)
Check if a key is due for randomized regeneration due to its popularity.
touchCheckKey( $key, $holdoff=self::HOLDOFF_TTL)
Purge a "check" key from all datacenters, invalidating keys that use it.
const LOCK_TSE
Default time-since-expiry on a miss that makes a key "hot".
adaptiveTTL( $mtime, $maxTTL, $minTTL=30, $factor=0.2)
Get a TTL that is higher for objects that have not changed recently.
int $warmupKeyMisses
Key fetched.
mixed[] $warmupCache
Temporary warm-up cache.
const VERSION
Cache format version number.
const TTL_UNCACHEABLE
Idiom for getWithSetCallback() callbacks to avoid calling set()
const LOW_TTL
Default remaining TTL at which to consider pre-emptive regeneration.
relayPurge( $key, $ttl, $holdoff)
Do the actual async bus purge of a key.
getLastError()
Get the "last error" registered; clearLastError() should be called manually.
BagOStuff $cache
The local datacenter cache.
isValid( $value, $versioned, $asOf, $minTime)
Check whether $value is appropriately versioned and not older than $minTime (if set)
processCheckKeys(array $timeKeys, array $wrappedValues, $now)
doGetWithSetCallback( $key, $ttl, $callback, array $opts, &$asOf=null)
Do the actual I/O for getWithSetCallback() when needed.
const HOLDOFF_NONE
Idiom for delete() for "no hold-off".
getMulti(array $keys, &$curTTLs=[], array $checkKeys=[], array &$asOfs=[])
Fetch the value of several keys from cache.
getCheckKeyTime( $key)
Fetch the value of a timestamp "check" key.
HashBagOStuff[] $processCaches
Map of group PHP instance caches.
relayDelete( $key)
Do the actual async bus delete of a key.
static parsePurgeValue( $value)
const LOCK_TTL
Seconds to keep lock keys around.
getMultiWithUnionSetCallback(ArrayIterator $keyedIds, $ttl, callable $callback, array $opts=[])
Method to fetch/regenerate multiple cache keys at once.
LoggerInterface $logger
static prefixCacheKeys(array $keys, $prefix)
getMultiWithSetCallback(ArrayIterator $keyedIds, $ttl, callable $callback, array $opts=[])
Method to fetch multiple cache keys at once with regeneration.
const HIT_RATE_HIGH
Hits/second for a refresh to be expected within the "popularity" window.
EventRelayer $purgeRelayer
Bus that handles purge broadcasts.
static newEmpty()
Get an instance that wraps EmptyBagOStuff.
worthRefreshExpiring( $curTTL, $lowTTL)
Check if a key should be regenerated (using random probability)
getWithSetCallback( $key, $ttl, $callback, array $opts=[])
Method to fetch/regenerate cache keys.
makeMultiKeys(array $entities, callable $keyFunc)
const MAX_READ_LAG
Max replication+snapshot lag before applying TTL_LAGGED or disallowing set()
const CHECK_KEY_TTL
Seconds to keep dependency purge keys around.
const MIN_TIMESTAMP_NONE
Idiom for getWithSetCallback() for "no minimum required as-of timestamp".
clearProcessCache()
Clear the in-process caches; useful for testing.
getNonProcessCachedKeys(array $keys, array $opts)
wrap( $value, $ttl, $now)
Do not use this method outside WANObjectCache.
int $lastRelayError
ERR_* constant for the "last error" registry.
string $purgeChannel
Purge channel name.
reap( $key, $purgeTimestamp, &$isStale=false)
Locally set a key to expire soon if it is stale based on $purgeTimestamp.
makePurgeValue( $timestamp, $holdoff)
getRawKeysForWarmup(array $keys, array $checkKeys)
setLogger(LoggerInterface $logger)
reapCheckKey( $key, $purgeTimestamp, &$isStale=false)
Locally set a "check" key to expire soon if it is stale based on $purgeTimestamp.
clearLastError()
Clear the "last error" registry.
const TSE_NONE
Idiom for getWithSetCallback() callbacks to 'lockTSE' logic.
resetCheckKey( $key)
Delete a "check" key from all datacenters, invalidating keys that use it.
const MAX_COMMIT_DELAY
Max time expected to pass between delete() and DB commit finishing.
const AGE_NEW
Never consider performing "popularity" refreshes until a key reaches this age.
const RAMPUP_TTL
Seconds to ramp up to the "popularity" refresh chance after a key is no longer new.
const TTL_LAGGED
Max TTL to store keys when a data sourced is lagged.
$res
Definition database.txt:21
deferred txt A few of the database updates required by various functions here can be deferred until after the result page is displayed to the user For updating the view updating the linked to tables after a etc PHP does not yet have any way to tell the server to actually return and disconnect while still running these but it might have such a feature in the future We handle these by creating a deferred update object and putting those objects on a global list
Definition deferred.txt:11
namespace being checked & $result
Definition hooks.txt:2293
see documentation in includes Linker php for Linker::makeImageLink & $time
Definition hooks.txt:1778
This code would result in ircNotify being run twice when an article is and once for brion Hooks can return three possible true was required This is the default since MediaWiki *some string
Definition hooks.txt:181
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that probably a stub it is not rendered in wiki pages or galleries in category pages allow injecting custom HTML after the section Any uses of the hook need to handle escaping see BaseTemplate::getToolbox and BaseTemplate::makeListItem for details on the format of individual items inside of this array or by returning and letting standard HTTP rendering take place modifiable or by returning false and taking over the output modifiable & $code
Definition hooks.txt:863
it s the revision text itself In either if gzip is the revision text is gzipped $flags
Definition hooks.txt:2805
processing should stop and the error should be shown to the user * false
Definition hooks.txt:187
Generic base class for storage interfaces.
you have access to all of the normal MediaWiki so you can get a DB use the cache
$cache
Definition mcc.php:33
$params