MediaWiki REL1_32
BagOStuff.php
Go to the documentation of this file.
1<?php
29use Psr\Log\LoggerAwareInterface;
30use Psr\Log\LoggerInterface;
31use Psr\Log\NullLogger;
32use Wikimedia\ScopedCallback;
33use Wikimedia\WaitConditionLoop;
34
58abstract class BagOStuff implements IExpiringStore, LoggerAwareInterface {
60 protected $locks = [];
62 protected $lastError = self::ERR_NONE;
64 protected $keyspace = 'local';
66 protected $logger;
68 protected $asyncHandler;
70 protected $syncTimeout;
71
73 private $debugMode = false;
77 private $reportDupes = false;
79 private $dupeTrackScheduled = false;
80
82 protected $busyCallbacks = [];
83
86
88 protected $attrMap = [];
89
91 const ERR_NONE = 0; // no error
92 const ERR_NO_RESPONSE = 1; // no response
93 const ERR_UNREACHABLE = 2; // can't connect
94 const ERR_UNEXPECTED = 3; // response gave some error
95
97 const READ_LATEST = 1; // use latest data for replicated stores
98 const READ_VERIFIED = 2; // promise that caller can tell when keys are stale
100 const WRITE_SYNC = 1; // synchronously write to all locations for replicated stores
101 const WRITE_CACHE_ONLY = 2; // Only change state of the in-memory cache
102
114 public function __construct( array $params = [] ) {
115 if ( isset( $params['logger'] ) ) {
116 $this->setLogger( $params['logger'] );
117 } else {
118 $this->setLogger( new NullLogger() );
119 }
120
121 if ( isset( $params['keyspace'] ) ) {
122 $this->keyspace = $params['keyspace'];
123 }
124
125 $this->asyncHandler = $params['asyncHandler'] ?? null;
126
127 if ( !empty( $params['reportDupes'] ) && is_callable( $this->asyncHandler ) ) {
128 $this->reportDupes = true;
129 }
130
131 $this->syncTimeout = $params['syncTimeout'] ?? 3;
132 }
133
138 public function setLogger( LoggerInterface $logger ) {
139 $this->logger = $logger;
140 }
141
145 public function setDebug( $bool ) {
146 $this->debugMode = $bool;
147 }
148
161 final public function getWithSetCallback( $key, $ttl, $callback, $flags = 0 ) {
162 $value = $this->get( $key, $flags );
163
164 if ( $value === false ) {
165 if ( !is_callable( $callback ) ) {
166 throw new InvalidArgumentException( "Invalid cache miss callback provided." );
167 }
168 $value = call_user_func( $callback );
169 if ( $value !== false ) {
170 $this->set( $key, $value, $ttl );
171 }
172 }
173
174 return $value;
175 }
176
191 public function get( $key, $flags = 0, $oldFlags = null ) {
192 // B/C for ( $key, &$casToken = null, $flags = 0 )
193 $flags = is_int( $oldFlags ) ? $oldFlags : $flags;
194
195 $this->trackDuplicateKeys( $key );
196
197 return $this->doGet( $key, $flags );
198 }
199
204 private function trackDuplicateKeys( $key ) {
205 if ( !$this->reportDupes ) {
206 return;
207 }
208
209 if ( !isset( $this->duplicateKeyLookups[$key] ) ) {
210 // Track that we have seen this key. This N-1 counting style allows
211 // easy filtering with array_filter() later.
212 $this->duplicateKeyLookups[$key] = 0;
213 } else {
214 $this->duplicateKeyLookups[$key] += 1;
215
216 if ( $this->dupeTrackScheduled === false ) {
217 $this->dupeTrackScheduled = true;
218 // Schedule a callback that logs keys processed more than once by get().
219 call_user_func( $this->asyncHandler, function () {
220 $dups = array_filter( $this->duplicateKeyLookups );
221 foreach ( $dups as $key => $count ) {
222 $this->logger->warning(
223 'Duplicate get(): "{key}" fetched {count} times',
224 // Count is N-1 of the actual lookup count
225 [ 'key' => $key, 'count' => $count + 1, ]
226 );
227 }
228 } );
229 }
230 }
231 }
232
238 abstract protected function doGet( $key, $flags = 0 );
239
249 protected function getWithToken( $key, &$casToken, $flags = 0 ) {
250 throw new Exception( __METHOD__ . ' not implemented.' );
251 }
252
262 abstract public function set( $key, $value, $exptime = 0, $flags = 0 );
263
270 abstract public function delete( $key );
271
288 public function merge( $key, callable $callback, $exptime = 0, $attempts = 10, $flags = 0 ) {
289 return $this->mergeViaLock( $key, $callback, $exptime, $attempts, $flags );
290 }
291
301 protected function mergeViaCas( $key, $callback, $exptime = 0, $attempts = 10 ) {
302 do {
303 $this->clearLastError();
304 $reportDupes = $this->reportDupes;
305 $this->reportDupes = false;
306 $casToken = null; // passed by reference
307 $currentValue = $this->getWithToken( $key, $casToken, self::READ_LATEST );
308 $this->reportDupes = $reportDupes;
309
310 if ( $this->getLastError() ) {
311 $this->logger->warning(
312 __METHOD__ . ' failed due to I/O error on get() for {key}.',
313 [ 'key' => $key ]
314 );
315
316 return false; // don't spam retries (retry only on races)
317 }
318
319 // Derive the new value from the old value
320 $value = call_user_func( $callback, $this, $key, $currentValue, $exptime );
321
322 $this->clearLastError();
323 if ( $value === false ) {
324 $success = true; // do nothing
325 } elseif ( $currentValue === false ) {
326 // Try to create the key, failing if it gets created in the meantime
327 $success = $this->add( $key, $value, $exptime );
328 } else {
329 // Try to update the key, failing if it gets changed in the meantime
330 $success = $this->cas( $casToken, $key, $value, $exptime );
331 }
332 if ( $this->getLastError() ) {
333 $this->logger->warning(
334 __METHOD__ . ' failed due to I/O error for {key}.',
335 [ 'key' => $key ]
336 );
337
338 return false; // IO error; don't spam retries
339 }
340 } while ( !$success && --$attempts );
341
342 return $success;
343 }
344
355 protected function cas( $casToken, $key, $value, $exptime = 0 ) {
356 if ( !$this->lock( $key, 0 ) ) {
357 return false; // non-blocking
358 }
359
360 $curCasToken = null; // passed by reference
361 $this->getWithToken( $key, $curCasToken, self::READ_LATEST );
362 if ( $casToken === $curCasToken ) {
363 $success = $this->set( $key, $value, $exptime );
364 } else {
365 $this->logger->info(
366 __METHOD__ . ' failed due to race condition for {key}.',
367 [ 'key' => $key ]
368 );
369
370 $success = false; // mismatched or failed
371 }
372
373 $this->unlock( $key );
374
375 return $success;
376 }
377
388 protected function mergeViaLock( $key, $callback, $exptime = 0, $attempts = 10, $flags = 0 ) {
389 if ( $attempts <= 1 ) {
390 $timeout = 0; // clearly intended to be "non-blocking"
391 } else {
392 $timeout = 3;
393 }
394
395 if ( !$this->lock( $key, $timeout ) ) {
396 return false;
397 }
398
399 $this->clearLastError();
400 $reportDupes = $this->reportDupes;
401 $this->reportDupes = false;
402 $currentValue = $this->get( $key, self::READ_LATEST );
403 $this->reportDupes = $reportDupes;
404
405 if ( $this->getLastError() ) {
406 $this->logger->warning(
407 __METHOD__ . ' failed due to I/O error on get() for {key}.',
408 [ 'key' => $key ]
409 );
410
411 $success = false;
412 } else {
413 // Derive the new value from the old value
414 $value = call_user_func( $callback, $this, $key, $currentValue, $exptime );
415 if ( $value === false ) {
416 $success = true; // do nothing
417 } else {
418 $success = $this->set( $key, $value, $exptime, $flags ); // set the new value
419 }
420 }
421
422 if ( !$this->unlock( $key ) ) {
423 // this should never happen
424 trigger_error( "Could not release lock for key '$key'." );
425 }
426
427 return $success;
428 }
429
438 public function changeTTL( $key, $expiry = 0 ) {
439 $value = $this->get( $key );
440
441 return ( $value === false ) ? false : $this->set( $key, $value, $expiry );
442 }
443
455 public function lock( $key, $timeout = 6, $expiry = 6, $rclass = '' ) {
456 // Avoid deadlocks and allow lock reentry if specified
457 if ( isset( $this->locks[$key] ) ) {
458 if ( $rclass != '' && $this->locks[$key]['class'] === $rclass ) {
459 ++$this->locks[$key]['depth'];
460 return true;
461 } else {
462 return false;
463 }
464 }
465
466 $fname = __METHOD__;
467 $expiry = min( $expiry ?: INF, self::TTL_DAY );
468 $loop = new WaitConditionLoop(
469 function () use ( $key, $timeout, $expiry, $fname ) {
470 $this->clearLastError();
471 if ( $this->add( "{$key}:lock", 1, $expiry ) ) {
472 return true; // locked!
473 } elseif ( $this->getLastError() ) {
474 $this->logger->warning(
475 $fname . ' failed due to I/O error for {key}.',
476 [ 'key' => $key ]
477 );
478
479 return WaitConditionLoop::CONDITION_ABORTED; // network partition?
480 }
481
482 return WaitConditionLoop::CONDITION_CONTINUE;
483 },
484 $timeout
485 );
486
487 $code = $loop->invoke();
488 $locked = ( $code === $loop::CONDITION_REACHED );
489 if ( $locked ) {
490 $this->locks[$key] = [ 'class' => $rclass, 'depth' => 1 ];
491 } elseif ( $code === $loop::CONDITION_TIMED_OUT ) {
492 $this->logger->warning(
493 "$fname failed due to timeout for {key}.",
494 [ 'key' => $key, 'timeout' => $timeout ]
495 );
496 }
497
498 return $locked;
499 }
500
507 public function unlock( $key ) {
508 if ( isset( $this->locks[$key] ) && --$this->locks[$key]['depth'] <= 0 ) {
509 unset( $this->locks[$key] );
510
511 $ok = $this->delete( "{$key}:lock" );
512 if ( !$ok ) {
513 $this->logger->warning(
514 __METHOD__ . ' failed to release lock for {key}.',
515 [ 'key' => $key ]
516 );
517 }
518
519 return $ok;
520 }
521
522 return true;
523 }
524
541 final public function getScopedLock( $key, $timeout = 6, $expiry = 30, $rclass = '' ) {
542 $expiry = min( $expiry ?: INF, self::TTL_DAY );
543
544 if ( !$this->lock( $key, $timeout, $expiry, $rclass ) ) {
545 return null;
546 }
547
548 $lSince = $this->getCurrentTime(); // lock timestamp
549
550 return new ScopedCallback( function () use ( $key, $lSince, $expiry ) {
551 $latency = 0.050; // latency skew (err towards keeping lock present)
552 $age = ( $this->getCurrentTime() - $lSince + $latency );
553 if ( ( $age + $latency ) >= $expiry ) {
554 $this->logger->warning(
555 "Lock for {key} held too long ({age} sec).",
556 [ 'key' => $key, 'age' => $age ]
557 );
558 return; // expired; it's not "safe" to delete the key
559 }
560 $this->unlock( $key );
561 } );
562 }
563
573 public function deleteObjectsExpiringBefore( $date, $progressCallback = false ) {
574 // stub
575 return false;
576 }
577
584 public function getMulti( array $keys, $flags = 0 ) {
585 $res = [];
586 foreach ( $keys as $key ) {
587 $val = $this->get( $key );
588 if ( $val !== false ) {
589 $res[$key] = $val;
590 }
591 }
592 return $res;
593 }
594
602 public function setMulti( array $data, $exptime = 0 ) {
603 $res = true;
604 foreach ( $data as $key => $value ) {
605 if ( !$this->set( $key, $value, $exptime ) ) {
606 $res = false;
607 }
608 }
609 return $res;
610 }
611
618 public function add( $key, $value, $exptime = 0 ) {
619 // @note: avoid lock() here since that method uses *this* method by default
620 if ( $this->get( $key ) === false ) {
621 return $this->set( $key, $value, $exptime );
622 }
623 return false; // key already set
624 }
625
632 public function incr( $key, $value = 1 ) {
633 if ( !$this->lock( $key, 1 ) ) {
634 return false;
635 }
636 $n = $this->get( $key );
637 if ( $this->isInteger( $n ) ) { // key exists?
638 $n += intval( $value );
639 $this->set( $key, max( 0, $n ) ); // exptime?
640 } else {
641 $n = false;
642 }
643 $this->unlock( $key );
644
645 return $n;
646 }
647
654 public function decr( $key, $value = 1 ) {
655 return $this->incr( $key, - $value );
656 }
657
670 public function incrWithInit( $key, $ttl, $value = 1, $init = 1 ) {
671 $this->clearLastError();
672 $newValue = $this->incr( $key, $value );
673 if ( $newValue === false && !$this->getLastError() ) {
674 // No key set; initialize
675 $newValue = $this->add( $key, (int)$init, $ttl ) ? $init : false;
676 if ( $newValue === false && !$this->getLastError() ) {
677 // Raced out initializing; increment
678 $newValue = $this->incr( $key, $value );
679 }
680 }
681
682 return $newValue;
683 }
684
690 public function getLastError() {
691 return $this->lastError;
692 }
693
698 public function clearLastError() {
699 $this->lastError = self::ERR_NONE;
700 }
701
707 protected function setLastError( $err ) {
708 $this->lastError = $err;
709 }
710
731 public function addBusyCallback( callable $workCallback ) {
732 $this->busyCallbacks[] = $workCallback;
733 }
734
749 public function modifySimpleRelayEvent( array $event ) {
750 return $event;
751 }
752
756 protected function debug( $text ) {
757 if ( $this->debugMode ) {
758 $this->logger->debug( "{class} debug: $text", [
759 'class' => static::class,
760 ] );
761 }
762 }
763
769 protected function convertExpiry( $exptime ) {
770 if ( $exptime != 0 && $exptime < ( 10 * self::TTL_YEAR ) ) {
771 return (int)$this->getCurrentTime() + $exptime;
772 } else {
773 return $exptime;
774 }
775 }
776
784 protected function convertToRelative( $exptime ) {
785 if ( $exptime >= ( 10 * self::TTL_YEAR ) ) {
786 $exptime -= (int)$this->getCurrentTime();
787 if ( $exptime <= 0 ) {
788 $exptime = 1;
789 }
790 return $exptime;
791 } else {
792 return $exptime;
793 }
794 }
795
802 protected function isInteger( $value ) {
803 return ( is_int( $value ) || ctype_digit( $value ) );
804 }
805
814 public function makeKeyInternal( $keyspace, $args ) {
815 $key = $keyspace;
816 foreach ( $args as $arg ) {
817 $arg = str_replace( ':', '%3A', $arg );
818 $key = $key . ':' . $arg;
819 }
820 return strtr( $key, ' ', '_' );
821 }
822
831 public function makeGlobalKey( $class, $component = null ) {
832 return $this->makeKeyInternal( 'global', func_get_args() );
833 }
834
843 public function makeKey( $class, $component = null ) {
844 return $this->makeKeyInternal( $this->keyspace, func_get_args() );
845 }
846
852 public function getQoS( $flag ) {
853 return $this->attrMap[$flag] ?? self::QOS_UNKNOWN;
854 }
855
862 protected function mergeFlagMaps( array $bags ) {
863 $map = [];
864 foreach ( $bags as $bag ) {
865 foreach ( $bag->attrMap as $attr => $rank ) {
866 if ( isset( $map[$attr] ) ) {
867 $map[$attr] = min( $map[$attr], $rank );
868 } else {
869 $map[$attr] = $rank;
870 }
871 }
872 }
873
874 return $map;
875 }
876
881 protected function getCurrentTime() {
882 return $this->wallClockOverride ?: microtime( true );
883 }
884
889 public function setMockTime( &$time ) {
890 $this->wallClockOverride =& $time;
891 }
892}
if(defined( 'MW_SETUP_CALLBACK')) $fname
Customization point after all loading (constants, functions, classes, DefaultSettings,...
Definition Setup.php:121
if( $line===false) $args
Definition cdb.php:64
Class representing a cache/ephemeral data store.
Definition BagOStuff.php:58
const ERR_UNEXPECTED
Definition BagOStuff.php:94
int[] $attrMap
Map of (ATTR_* class constant => QOS_* class constant)
Definition BagOStuff.php:88
getWithSetCallback( $key, $ttl, $callback, $flags=0)
Get an item with the given key, regenerating and setting it if not found.
__construct(array $params=[])
$params include:
getScopedLock( $key, $timeout=6, $expiry=30, $rclass='')
Get a lightweight exclusive self-unlocking lock.
unlock( $key)
Release an advisory lock on a key string.
incrWithInit( $key, $ttl, $value=1, $init=1)
Increase stored value of $key by $value while preserving its TTL.
isInteger( $value)
Check if a value is an integer.
lock( $key, $timeout=6, $expiry=6, $rclass='')
Acquire an advisory lock on a key string.
bool $dupeTrackScheduled
Definition BagOStuff.php:79
makeKey( $class, $component=null)
Make a cache key, scoped to this instance's keyspace.
array $duplicateKeyLookups
Definition BagOStuff.php:75
add( $key, $value, $exptime=0)
getMulti(array $keys, $flags=0)
Get an associative array containing the item for each of the keys that have items.
decr( $key, $value=1)
Decrease stored value of $key by $value while preserving its TTL.
setDebug( $bool)
modifySimpleRelayEvent(array $event)
Modify a cache update operation array for EventRelayer::notify()
float null $wallClockOverride
Definition BagOStuff.php:85
getQoS( $flag)
const READ_LATEST
Bitfield constants for get()/getMulti()
Definition BagOStuff.php:97
deleteObjectsExpiringBefore( $date, $progressCallback=false)
Delete all objects expiring before a certain date.
const READ_VERIFIED
Definition BagOStuff.php:98
trackDuplicateKeys( $key)
Track the number of times that a given key has been used.
convertExpiry( $exptime)
Convert an optionally relative time to an absolute time.
callable[] $busyCallbacks
Definition BagOStuff.php:82
callback null $asyncHandler
Definition BagOStuff.php:68
getWithToken( $key, &$casToken, $flags=0)
setMulti(array $data, $exptime=0)
Batch insertion.
const ERR_NO_RESPONSE
Definition BagOStuff.php:92
const ERR_NONE
Possible values for getLastError()
Definition BagOStuff.php:91
debug( $text)
doGet( $key, $flags=0)
convertToRelative( $exptime)
Convert an optionally absolute expiry time to a relative time.
setLogger(LoggerInterface $logger)
setMockTime(&$time)
int $syncTimeout
Seconds.
Definition BagOStuff.php:70
const WRITE_SYNC
Bitfield constants for set()/merge()
mergeFlagMaps(array $bags)
Merge the flag maps of one or more BagOStuff objects into a "lowest common denominator" map.
bool $reportDupes
Definition BagOStuff.php:77
getLastError()
Get the "last error" registered; clearLastError() should be called manually.
bool $debugMode
Definition BagOStuff.php:73
LoggerInterface $logger
Definition BagOStuff.php:66
setLastError( $err)
Set the "last error" registry.
changeTTL( $key, $expiry=0)
Reset the TTL on a key if it exists.
mergeViaLock( $key, $callback, $exptime=0, $attempts=10, $flags=0)
clearLastError()
Clear the "last error" registry.
addBusyCallback(callable $workCallback)
Let a callback be run to avoid wasting time on special blocking calls.
cas( $casToken, $key, $value, $exptime=0)
Check and set an item.
makeKeyInternal( $keyspace, $args)
Construct a cache key.
const WRITE_CACHE_ONLY
string $keyspace
Definition BagOStuff.php:64
int $lastError
ERR_* class constant.
Definition BagOStuff.php:62
array[] $locks
Lock tracking.
Definition BagOStuff.php:60
mergeViaCas( $key, $callback, $exptime=0, $attempts=10)
incr( $key, $value=1)
Increase stored value of $key by $value while preserving its TTL.
merge( $key, callable $callback, $exptime=0, $attempts=10, $flags=0)
Merge changes into the existing cache value (possibly creating a new one)
makeGlobalKey( $class, $component=null)
Make a global cache key.
const ERR_UNREACHABLE
Definition BagOStuff.php:93
$res
Definition database.txt:21
see documentation in includes Linker php for Linker::makeImageLink & $time
Definition hooks.txt:1841
An extension or a local will often add custom code to the function with or without a global variable For someone wanting email notification when an article is shown may add
Definition hooks.txt:56
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:895
processing should stop and the error should be shown to the user * false
Definition hooks.txt:187
Generic base class for storage interfaces.
The wiki should then use memcached to cache various data To use multiple just add more items to the array To increase the weight of a make its entry a array("192.168.0.1:11211", 2))
$params