MediaWiki REL1_27
BagOStuff.php
Go to the documentation of this file.
1<?php
29use Psr\Log\LoggerAwareInterface;
30use Psr\Log\LoggerInterface;
31use Psr\Log\NullLogger;
32
45abstract 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
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}
Apache License January AND DISTRIBUTION Definitions License shall mean the terms and conditions for use
lock()
Lock the current instance of the parser.
Definition Parser.php:6441
if( $line===false) $args
Definition cdb.php:64
interface is intended to be more or less compatible with the PHP memcached client.
Definition BagOStuff.php:45
const ERR_UNEXPECTED
Definition BagOStuff.php:77
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:
Definition BagOStuff.php:96
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:71
integer $lastError
Definition BagOStuff.php:50
array $duplicateKeyLookups
Definition BagOStuff.php:65
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()
const READ_LATEST
Bitfield constants for get()/getMulti()
Definition BagOStuff.php:80
deleteObjectsExpiringBefore( $date, $progressCallback=false)
Delete all objects expiring before a certain date.
const READ_VERIFIED
Definition BagOStuff.php:81
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.
callback null $asyncHandler
Definition BagOStuff.php:59
getWithToken( $key, &$casToken, $flags=0)
merge( $key, $callback, $exptime=0, $attempts=10, $flags=0)
Merge changes into the existing cache value (possibly creating a new one).
setMulti(array $data, $exptime=0)
Batch insertion.
const ERR_NO_RESPONSE
Definition BagOStuff.php:75
const ERR_NONE
Possible values for getLastError()
Definition BagOStuff.php:74
debug( $text)
doGet( $key, $flags=0)
convertToRelative( $exptime)
Convert an optionally absolute expiry time to a relative time.
setLogger(LoggerInterface $logger)
makeGlobalKey()
Make a global cache key.
const WRITE_SYNC
Bitfield constants for set()/merge()
Definition BagOStuff.php:83
bool $reportDupes
Definition BagOStuff.php:68
getLastError()
Get the "last error" registered; clearLastError() should be called manually.
bool $debugMode
Definition BagOStuff.php:62
LoggerInterface $logger
Definition BagOStuff.php:56
makeKey()
Make a cache key, scoped to this instance's keyspace.
setLastError( $err)
Set the "last error" registry.
mergeViaLock( $key, $callback, $exptime=0, $attempts=10, $flags=0)
clearLastError()
Clear the "last error" registry.
cas( $casToken, $key, $value, $exptime=0)
Check and set an item.
makeKeyInternal( $keyspace, $args)
Construct a cache key.
const WRITE_CACHE_ONLY
Definition BagOStuff.php:84
string $keyspace
Definition BagOStuff.php:53
array[] $locks
Lock tracking.
Definition BagOStuff.php:47
mergeViaCas( $key, $callback, $exptime=0, $attempts=10)
incr( $key, $value=1)
Increase stored value of $key by $value while preserving its TTL.
const ERR_UNREACHABLE
Definition BagOStuff.php:76
Class for asserting that a callback happens when an dummy object leaves scope.
$res
Definition database.txt:21
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
the array() calling protocol came about after MediaWiki 1.4rc1.
it s the revision text itself In either if gzip is the revision text is gzipped $flags
Definition hooks.txt:2555
if( $limit) $timestamp
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:37
Generic base class for storage interfaces.
$params