MediaWiki  1.27.1
BagOStuff.php
Go to the documentation of this file.
1 <?php
32 
45 abstract class BagOStuff implements IExpiringStore, LoggerAwareInterface {
47  protected $locks = [];
48 
50  protected $lastError = self::ERR_NONE;
51 
53  protected $keyspace = 'local';
54 
56  protected $logger;
57 
59  protected $asyncHandler;
60 
62  private $debugMode = false;
63 
65  private $duplicateKeyLookups = [];
66 
68  private $reportDupes = false;
69 
71  private $dupeTrackScheduled = false;
72 
74  const ERR_NONE = 0; // no error
75  const ERR_NO_RESPONSE = 1; // no response
76  const ERR_UNREACHABLE = 2; // can't connect
77  const ERR_UNEXPECTED = 3; // response gave some error
78 
80  const READ_LATEST = 1; // use latest data for replicated stores
81  const READ_VERIFIED = 2; // promise that caller can tell when keys are stale
83  const WRITE_SYNC = 1; // synchronously write to all locations for replicated stores
84  const WRITE_CACHE_ONLY = 2; // Only change state of the in-memory cache
85 
96  public function __construct( array $params = [] ) {
97  if ( isset( $params['logger'] ) ) {
98  $this->setLogger( $params['logger'] );
99  } else {
100  $this->setLogger( new NullLogger() );
101  }
102 
103  if ( isset( $params['keyspace'] ) ) {
104  $this->keyspace = $params['keyspace'];
105  }
106 
107  $this->asyncHandler = isset( $params['asyncHandler'] )
108  ? $params['asyncHandler']
109  : null;
110 
111  if ( !empty( $params['reportDupes'] ) && is_callable( $this->asyncHandler ) ) {
112  $this->reportDupes = true;
113  }
114  }
115 
120  public function setLogger( LoggerInterface $logger ) {
121  $this->logger = $logger;
122  }
123 
127  public function setDebug( $bool ) {
128  $this->debugMode = $bool;
129  }
130 
143  final public function getWithSetCallback( $key, $ttl, $callback, $flags = 0 ) {
144  $value = $this->get( $key, $flags );
145 
146  if ( $value === false ) {
147  if ( !is_callable( $callback ) ) {
148  throw new InvalidArgumentException( "Invalid cache miss callback provided." );
149  }
150  $value = call_user_func( $callback );
151  if ( $value !== false ) {
152  $this->set( $key, $value, $ttl );
153  }
154  }
155 
156  return $value;
157  }
158 
173  public function get( $key, $flags = 0, $oldFlags = null ) {
174  // B/C for ( $key, &$casToken = null, $flags = 0 )
175  $flags = is_int( $oldFlags ) ? $oldFlags : $flags;
176 
177  $this->trackDuplicateKeys( $key );
178 
179  return $this->doGet( $key, $flags );
180  }
181 
186  private function trackDuplicateKeys( $key ) {
187  if ( !$this->reportDupes ) {
188  return;
189  }
190 
191  if ( !isset( $this->duplicateKeyLookups[$key] ) ) {
192  // Track that we have seen this key. This N-1 counting style allows
193  // easy filtering with array_filter() later.
194  $this->duplicateKeyLookups[$key] = 0;
195  } else {
196  $this->duplicateKeyLookups[$key] += 1;
197 
198  if ( $this->dupeTrackScheduled === false ) {
199  $this->dupeTrackScheduled = true;
200  // Schedule a callback that logs keys processed more than once by get().
201  call_user_func( $this->asyncHandler, function () {
202  $dups = array_filter( $this->duplicateKeyLookups );
203  foreach ( $dups as $key => $count ) {
204  $this->logger->warning(
205  'Duplicate get(): "{key}" fetched {count} times',
206  // Count is N-1 of the actual lookup count
207  [ 'key' => $key, 'count' => $count + 1, ]
208  );
209  }
210  } );
211  }
212  }
213  }
214 
220  abstract protected function doGet( $key, $flags = 0 );
221 
231  protected function getWithToken( $key, &$casToken, $flags = 0 ) {
232  throw new Exception( __METHOD__ . ' not implemented.' );
233  }
234 
244  abstract public function set( $key, $value, $exptime = 0, $flags = 0 );
245 
252  abstract public function delete( $key );
253 
268  public function merge( $key, $callback, $exptime = 0, $attempts = 10, $flags = 0 ) {
269  if ( !is_callable( $callback ) ) {
270  throw new InvalidArgumentException( "Got invalid callback." );
271  }
272 
273  return $this->mergeViaLock( $key, $callback, $exptime, $attempts, $flags );
274  }
275 
285  protected function mergeViaCas( $key, $callback, $exptime = 0, $attempts = 10 ) {
286  do {
287  $this->clearLastError();
288  $casToken = null; // passed by reference
289  $currentValue = $this->getWithToken( $key, $casToken, self::READ_LATEST );
290  if ( $this->getLastError() ) {
291  return false; // don't spam retries (retry only on races)
292  }
293 
294  // Derive the new value from the old value
295  $value = call_user_func( $callback, $this, $key, $currentValue );
296 
297  $this->clearLastError();
298  if ( $value === false ) {
299  $success = true; // do nothing
300  } elseif ( $currentValue === false ) {
301  // Try to create the key, failing if it gets created in the meantime
302  $success = $this->add( $key, $value, $exptime );
303  } else {
304  // Try to update the key, failing if it gets changed in the meantime
305  $success = $this->cas( $casToken, $key, $value, $exptime );
306  }
307  if ( $this->getLastError() ) {
308  return false; // IO error; don't spam retries
309  }
310  } while ( !$success && --$attempts );
311 
312  return $success;
313  }
314 
325  protected function cas( $casToken, $key, $value, $exptime = 0 ) {
326  throw new Exception( "CAS is not implemented in " . __CLASS__ );
327  }
328 
339  protected function mergeViaLock( $key, $callback, $exptime = 0, $attempts = 10, $flags = 0 ) {
340  if ( !$this->lock( $key, 6 ) ) {
341  return false;
342  }
343 
344  $this->clearLastError();
345  $currentValue = $this->get( $key, self::READ_LATEST );
346  if ( $this->getLastError() ) {
347  $success = false;
348  } else {
349  // Derive the new value from the old value
350  $value = call_user_func( $callback, $this, $key, $currentValue );
351  if ( $value === false ) {
352  $success = true; // do nothing
353  } else {
354  $success = $this->set( $key, $value, $exptime, $flags ); // set the new value
355  }
356  }
357 
358  if ( !$this->unlock( $key ) ) {
359  // this should never happen
360  trigger_error( "Could not release lock for key '$key'." );
361  }
362 
363  return $success;
364  }
365 
377  public function lock( $key, $timeout = 6, $expiry = 6, $rclass = '' ) {
378  // Avoid deadlocks and allow lock reentry if specified
379  if ( isset( $this->locks[$key] ) ) {
380  if ( $rclass != '' && $this->locks[$key]['class'] === $rclass ) {
381  ++$this->locks[$key]['depth'];
382  return true;
383  } else {
384  return false;
385  }
386  }
387 
388  $expiry = min( $expiry ?: INF, self::TTL_DAY );
389 
390  $this->clearLastError();
391  $timestamp = microtime( true ); // starting UNIX timestamp
392  if ( $this->add( "{$key}:lock", 1, $expiry ) ) {
393  $locked = true;
394  } elseif ( $this->getLastError() || $timeout <= 0 ) {
395  $locked = false; // network partition or non-blocking
396  } else {
397  // Estimate the RTT (us); use 1ms minimum for sanity
398  $uRTT = max( 1e3, ceil( 1e6 * ( microtime( true ) - $timestamp ) ) );
399  $sleep = 2 * $uRTT; // rough time to do get()+set()
400 
401  $attempts = 0; // failed attempts
402  do {
403  if ( ++$attempts >= 3 && $sleep <= 5e5 ) {
404  // Exponentially back off after failed attempts to avoid network spam.
405  // About 2*$uRTT*(2^n-1) us of "sleep" happen for the next n attempts.
406  $sleep *= 2;
407  }
408  usleep( $sleep ); // back off
409  $this->clearLastError();
410  $locked = $this->add( "{$key}:lock", 1, $expiry );
411  if ( $this->getLastError() ) {
412  $locked = false; // network partition
413  break;
414  }
415  } while ( !$locked && ( microtime( true ) - $timestamp ) < $timeout );
416  }
417 
418  if ( $locked ) {
419  $this->locks[$key] = [ 'class' => $rclass, 'depth' => 1 ];
420  }
421 
422  return $locked;
423  }
424 
431  public function unlock( $key ) {
432  if ( isset( $this->locks[$key] ) && --$this->locks[$key]['depth'] <= 0 ) {
433  unset( $this->locks[$key] );
434 
435  return $this->delete( "{$key}:lock" );
436  }
437 
438  return true;
439  }
440 
457  final public function getScopedLock( $key, $timeout = 6, $expiry = 30, $rclass = '' ) {
458  $expiry = min( $expiry ?: INF, self::TTL_DAY );
459 
460  if ( !$this->lock( $key, $timeout, $expiry, $rclass ) ) {
461  return null;
462  }
463 
464  $lSince = microtime( true ); // lock timestamp
465 
466  return new ScopedCallback( function() use ( $key, $lSince, $expiry ) {
467  $latency = .050; // latency skew (err towards keeping lock present)
468  $age = ( microtime( true ) - $lSince + $latency );
469  if ( ( $age + $latency ) >= $expiry ) {
470  $this->logger->warning( "Lock for $key held too long ($age sec)." );
471  return; // expired; it's not "safe" to delete the key
472  }
473  $this->unlock( $key );
474  } );
475  }
476 
486  public function deleteObjectsExpiringBefore( $date, $progressCallback = false ) {
487  // stub
488  return false;
489  }
490 
497  public function getMulti( array $keys, $flags = 0 ) {
498  $res = [];
499  foreach ( $keys as $key ) {
500  $val = $this->get( $key );
501  if ( $val !== false ) {
502  $res[$key] = $val;
503  }
504  }
505  return $res;
506  }
507 
515  public function setMulti( array $data, $exptime = 0 ) {
516  $res = true;
517  foreach ( $data as $key => $value ) {
518  if ( !$this->set( $key, $value, $exptime ) ) {
519  $res = false;
520  }
521  }
522  return $res;
523  }
524 
531  public function add( $key, $value, $exptime = 0 ) {
532  if ( $this->get( $key ) === false ) {
533  return $this->set( $key, $value, $exptime );
534  }
535  return false; // key already set
536  }
537 
544  public function incr( $key, $value = 1 ) {
545  if ( !$this->lock( $key ) ) {
546  return false;
547  }
548  $n = $this->get( $key );
549  if ( $this->isInteger( $n ) ) { // key exists?
550  $n += intval( $value );
551  $this->set( $key, max( 0, $n ) ); // exptime?
552  } else {
553  $n = false;
554  }
555  $this->unlock( $key );
556 
557  return $n;
558  }
559 
566  public function decr( $key, $value = 1 ) {
567  return $this->incr( $key, - $value );
568  }
569 
582  public function incrWithInit( $key, $ttl, $value = 1, $init = 1 ) {
583  $newValue = $this->incr( $key, $value );
584  if ( $newValue === false ) {
585  // No key set; initialize
586  $newValue = $this->add( $key, (int)$init, $ttl ) ? $init : false;
587  }
588  if ( $newValue === false ) {
589  // Raced out initializing; increment
590  $newValue = $this->incr( $key, $value );
591  }
592 
593  return $newValue;
594  }
595 
601  public function getLastError() {
602  return $this->lastError;
603  }
604 
609  public function clearLastError() {
610  $this->lastError = self::ERR_NONE;
611  }
612 
618  protected function setLastError( $err ) {
619  $this->lastError = $err;
620  }
621 
636  public function modifySimpleRelayEvent( array $event ) {
637  return $event;
638  }
639 
643  protected function debug( $text ) {
644  if ( $this->debugMode ) {
645  $this->logger->debug( "{class} debug: $text", [
646  'class' => get_class( $this ),
647  ] );
648  }
649  }
650 
656  protected function convertExpiry( $exptime ) {
657  if ( $exptime != 0 && $exptime < ( 10 * self::TTL_YEAR ) ) {
658  return time() + $exptime;
659  } else {
660  return $exptime;
661  }
662  }
663 
671  protected function convertToRelative( $exptime ) {
672  if ( $exptime >= ( 10 * self::TTL_YEAR ) ) {
673  $exptime -= time();
674  if ( $exptime <= 0 ) {
675  $exptime = 1;
676  }
677  return $exptime;
678  } else {
679  return $exptime;
680  }
681  }
682 
689  protected function isInteger( $value ) {
690  return ( is_int( $value ) || ctype_digit( $value ) );
691  }
692 
701  public function makeKeyInternal( $keyspace, $args ) {
702  $key = $keyspace;
703  foreach ( $args as $arg ) {
704  $arg = str_replace( ':', '%3A', $arg );
705  $key = $key . ':' . $arg;
706  }
707  return strtr( $key, ' ', '_' );
708  }
709 
717  public function makeGlobalKey() {
718  return $this->makeKeyInternal( 'global', func_get_args() );
719  }
720 
728  public function makeKey() {
729  return $this->makeKeyInternal( $this->keyspace, func_get_args() );
730  }
731 }
clearLastError()
Clear the "last error" registry.
Definition: BagOStuff.php:609
const ERR_UNEXPECTED
Definition: BagOStuff.php:77
the array() calling protocol came about after MediaWiki 1.4rc1.
getWithToken($key, &$casToken, $flags=0)
Definition: BagOStuff.php:231
trackDuplicateKeys($key)
Track the number of times that a given key has been used.
Definition: BagOStuff.php:186
magic word the default is to use $key to get the and $key value or $key value text $key value html to format the value $key
Definition: hooks.txt:2321
bool $reportDupes
Definition: BagOStuff.php:68
$success
bool $dupeTrackScheduled
Definition: BagOStuff.php:71
Apache License January AND DISTRIBUTION Definitions License shall mean the terms and conditions for use
callback null $asyncHandler
Definition: BagOStuff.php:59
getScopedLock($key, $timeout=6, $expiry=30, $rclass= '')
Get a lightweight exclusive self-unlocking lock.
Definition: BagOStuff.php:457
lock($key, $timeout=6, $expiry=6, $rclass= '')
Acquire an advisory lock on a key string.
Definition: BagOStuff.php:377
const ERR_NO_RESPONSE
Definition: BagOStuff.php:75
$value
it s the revision text itself In either if gzip is the revision text is gzipped $flags
Definition: hooks.txt:2548
incrWithInit($key, $ttl, $value=1, $init=1)
Increase stored value of $key by $value while preserving its TTL.
Definition: BagOStuff.php:582
const ERR_UNREACHABLE
Definition: BagOStuff.php:76
doGet($key, $flags=0)
set($key, $value, $exptime=0, $flags=0)
Set an item.
getWithSetCallback($key, $ttl, $callback, $flags=0)
Get an item with the given key, regenerating and setting it if not found.
Definition: BagOStuff.php:143
deleteObjectsExpiringBefore($date, $progressCallback=false)
Delete all objects expiring before a certain date.
Definition: BagOStuff.php:486
string $keyspace
Definition: BagOStuff.php:53
modifySimpleRelayEvent(array $event)
Modify a cache update operation array for EventRelayer::notify()
Definition: BagOStuff.php:636
if($line===false) $args
Definition: cdb.php:64
getLastError()
Get the "last error" registered; clearLastError() should be called manually.
Definition: BagOStuff.php:601
Class for asserting that a callback happens when an dummy object leaves scope.
mergeViaCas($key, $callback, $exptime=0, $attempts=10)
Definition: BagOStuff.php:285
__construct(array $params=[])
$params include:
Definition: BagOStuff.php:96
bool $debugMode
Definition: BagOStuff.php:62
convertExpiry($exptime)
Convert an optionally relative time to an absolute time.
Definition: BagOStuff.php:656
const READ_VERIFIED
Definition: BagOStuff.php:81
if($limit) $timestamp
array $duplicateKeyLookups
Definition: BagOStuff.php:65
$res
Definition: database.txt:21
add($key, $value, $exptime=0)
Definition: BagOStuff.php:531
getMulti(array $keys, $flags=0)
Get an associative array containing the item for each of the keys that have items.
Definition: BagOStuff.php:497
incr($key, $value=1)
Increase stored value of $key by $value while preserving its TTL.
Definition: BagOStuff.php:544
setDebug($bool)
Definition: BagOStuff.php:127
const WRITE_SYNC
Bitfield constants for set()/merge()
Definition: BagOStuff.php:83
$params
const ERR_NONE
Possible values for getLastError()
Definition: BagOStuff.php:74
const READ_LATEST
Bitfield constants for get()/getMulti()
Definition: BagOStuff.php:80
$sleep
This document is intended to provide useful advice for parties seeking to redistribute MediaWiki to end users It s targeted particularly at maintainers for Linux since it s been observed that distribution packages of MediaWiki often break We ve consistently had to recommend that users seeking support use official tarballs instead of their distribution s and this often solves whatever problem the user is having It would be nice if this could such as
Definition: distributors.txt:9
setMulti(array $data, $exptime=0)
Batch insertion.
Definition: BagOStuff.php:515
mergeViaLock($key, $callback, $exptime=0, $attempts=10, $flags=0)
Definition: BagOStuff.php:339
const WRITE_CACHE_ONLY
Definition: BagOStuff.php:84
injection txt This is an overview of how MediaWiki makes use of dependency injection The design described here grew from the discussion of RFC T384 The term dependency this means that anything an object needs to operate should be injected from the the object itself should only know narrow no concrete implementation of the logic it relies on The requirement to inject everything typically results in an architecture that based on two main types of and essentially stateless service objects that use other service objects to operate on the value objects As of the beginning MediaWiki is only starting to use the DI approach Much of the code still relies on global state or direct resulting in a highly cyclical dependency which acts as the top level factory for services in MediaWiki which can be used to gain access to default instances of various services MediaWikiServices however also allows new services to be defined and default services to be redefined Services are defined or redefined by providing a callback the instantiator that will return a new instance of the service When it will create an instance of MediaWikiServices and populate it with the services defined in the files listed by thereby bootstrapping the DI framework Per $wgServiceWiringFiles lists includes ServiceWiring php
Definition: injection.txt:35
merge($key, $callback, $exptime=0, $attempts=10, $flags=0)
Merge changes into the existing cache value (possibly creating a new one).
Definition: BagOStuff.php:268
decr($key, $value=1)
Decrease stored value of $key by $value while preserving its TTL.
Definition: BagOStuff.php:566
LoggerInterface $logger
Definition: BagOStuff.php:56
setLogger(LoggerInterface $logger)
Definition: BagOStuff.php:120
convertToRelative($exptime)
Convert an optionally absolute expiry time to a relative time.
Definition: BagOStuff.php:671
makeKey()
Make a cache key, scoped to this instance's keyspace.
Definition: BagOStuff.php:728
$count
cas($casToken, $key, $value, $exptime=0)
Check and set an item.
Definition: BagOStuff.php:325
Generic base class for storage interfaces.
array[] $locks
Lock tracking.
Definition: BagOStuff.php:47
makeKeyInternal($keyspace, $args)
Construct a cache key.
Definition: BagOStuff.php:701
debug($text)
Definition: BagOStuff.php:643
setLastError($err)
Set the "last error" registry.
Definition: BagOStuff.php:618
unlock($key)
Release an advisory lock on a key string.
Definition: BagOStuff.php:431
isInteger($value)
Check if a value is an integer.
Definition: BagOStuff.php:689
makeGlobalKey()
Make a global cache key.
Definition: BagOStuff.php:717
integer $lastError
Definition: BagOStuff.php:50