diff options
Diffstat (limited to 'includes/libs/objectcache/BagOStuff.php')
-rw-r--r-- | includes/libs/objectcache/BagOStuff.php | 201 |
1 files changed, 153 insertions, 48 deletions
diff --git a/includes/libs/objectcache/BagOStuff.php b/includes/libs/objectcache/BagOStuff.php index 0b791e5a..ddbe8eaa 100644 --- a/includes/libs/objectcache/BagOStuff.php +++ b/includes/libs/objectcache/BagOStuff.php @@ -1,7 +1,5 @@ <?php /** - * Classes to cache objects in PHP accelerators, SQL database or DBA files - * * Copyright © 2003-2004 Brion Vibber <brion@pobox.com> * https://www.mediawiki.org/ * @@ -37,29 +35,34 @@ use Psr\Log\NullLogger; * the PHP memcached client. * * backends for local hash array and SQL table included: - * <code> + * @code * $bag = new HashBagOStuff(); * $bag = new SqlBagOStuff(); # connect to db first - * </code> + * @endcode * * @ingroup Cache */ abstract class BagOStuff implements LoggerAwareInterface { - private $debugMode = false; - + /** @var array[] Lock tracking */ + protected $locks = array(); + /** @var integer */ protected $lastError = self::ERR_NONE; - /** - * @var LoggerInterface - */ + /** @var LoggerInterface */ protected $logger; + /** @var bool */ + private $debugMode = false; + /** Possible values for getLastError() */ const ERR_NONE = 0; // no error const ERR_NO_RESPONSE = 1; // no response const ERR_UNREACHABLE = 2; // can't connect const ERR_UNEXPECTED = 3; // response gave some error + /** Bitfield constants for get()/getMulti() */ + const READ_LATEST = 1; // use latest data for replicated stores + public function __construct( array $params = array() ) { if ( isset( $params['logger'] ) ) { $this->setLogger( $params['logger'] ); @@ -87,9 +90,10 @@ abstract class BagOStuff implements LoggerAwareInterface { * Get an item with the given key. Returns false if it does not exist. * @param string $key * @param mixed $casToken [optional] + * @param integer $flags Bitfield; supports READ_LATEST [optional] * @return mixed Returns false on failure */ - abstract public function get( $key, &$casToken = null ); + abstract public function get( $key, &$casToken = null, $flags = 0 ); /** * Set an item. @@ -109,18 +113,20 @@ abstract class BagOStuff implements LoggerAwareInterface { /** * Merge changes into the existing cache value (possibly creating a new one). - * The callback function returns the new value given the current value (possibly false), - * and takes the arguments: (this BagOStuff object, cache key, current value). + * The callback function returns the new value given the current value + * (which will be false if not present), and takes the arguments: + * (this BagOStuff, cache key, current value). * * @param string $key * @param callable $callback Callback method to be executed * @param int $exptime Either an interval in seconds or a unix timestamp for expiry * @param int $attempts The amount of times to attempt a merge in case of failure * @return bool Success + * @throws InvalidArgumentException */ public function merge( $key, $callback, $exptime = 0, $attempts = 10 ) { if ( !is_callable( $callback ) ) { - throw new Exception( "Got invalid callback." ); + throw new InvalidArgumentException( "Got invalid callback." ); } return $this->mergeViaLock( $key, $callback, $exptime, $attempts ); @@ -137,11 +143,17 @@ abstract class BagOStuff implements LoggerAwareInterface { */ protected function mergeViaCas( $key, $callback, $exptime = 0, $attempts = 10 ) { do { + $this->clearLastError(); $casToken = null; // passed by reference $currentValue = $this->get( $key, $casToken ); + if ( $this->getLastError() ) { + return false; // don't spam retries (retry only on races) + } + // Derive the new value from the old value $value = call_user_func( $callback, $this, $key, $currentValue ); + $this->clearLastError(); if ( $value === false ) { $success = true; // do nothing } elseif ( $currentValue === false ) { @@ -151,6 +163,9 @@ abstract class BagOStuff implements LoggerAwareInterface { // Try to update the key, failing if it gets changed in the meantime $success = $this->cas( $casToken, $key, $value, $exptime ); } + if ( $this->getLastError() ) { + return false; // IO error; don't spam retries + } } while ( !$success && --$attempts ); return $success; @@ -164,6 +179,7 @@ abstract class BagOStuff implements LoggerAwareInterface { * @param mixed $value * @param int $exptime Either an interval in seconds or a unix timestamp for expiry * @return bool Success + * @throws Exception */ protected function cas( $casToken, $key, $value, $exptime = 0 ) { throw new Exception( "CAS is not implemented in " . __CLASS__ ); @@ -183,14 +199,18 @@ abstract class BagOStuff implements LoggerAwareInterface { return false; } + $this->clearLastError(); $currentValue = $this->get( $key ); - // Derive the new value from the old value - $value = call_user_func( $callback, $this, $key, $currentValue ); - - if ( $value === false ) { - $success = true; // do nothing + if ( $this->getLastError() ) { + $success = false; } else { - $success = $this->set( $key, $value, $exptime ); // set the new value + // Derive the new value from the old value + $value = call_user_func( $callback, $this, $key, $currentValue ); + if ( $value === false ) { + $success = true; // do nothing + } else { + $success = $this->set( $key, $value, $exptime ); // set the new value + } } if ( !$this->unlock( $key ) ) { @@ -202,48 +222,116 @@ abstract class BagOStuff implements LoggerAwareInterface { } /** + * Acquire an advisory lock on a key string + * + * Note that if reentry is enabled, duplicate calls ignore $expiry + * * @param string $key - * @param int $timeout Lock wait timeout [optional] - * @param int $expiry Lock expiry [optional] + * @param int $timeout Lock wait timeout; 0 for non-blocking [optional] + * @param int $expiry Lock expiry [optional]; 1 day maximum + * @param string $rclass Allow reentry if set and the current lock used this value * @return bool Success */ - public function lock( $key, $timeout = 6, $expiry = 6 ) { + public function lock( $key, $timeout = 6, $expiry = 6, $rclass = '' ) { + // Avoid deadlocks and allow lock reentry if specified + if ( isset( $this->locks[$key] ) ) { + if ( $rclass != '' && $this->locks[$key]['class'] === $rclass ) { + ++$this->locks[$key]['depth']; + return true; + } else { + return false; + } + } + + $expiry = min( $expiry ?: INF, 86400 ); + $this->clearLastError(); $timestamp = microtime( true ); // starting UNIX timestamp if ( $this->add( "{$key}:lock", 1, $expiry ) ) { - return true; - } elseif ( $this->getLastError() ) { - return false; + $locked = true; + } elseif ( $this->getLastError() || $timeout <= 0 ) { + $locked = false; // network partition or non-blocking + } else { + $uRTT = ceil( 1e6 * ( microtime( true ) - $timestamp ) ); // estimate RTT (us) + $sleep = 2 * $uRTT; // rough time to do get()+set() + + $attempts = 0; // failed attempts + do { + if ( ++$attempts >= 3 && $sleep <= 5e5 ) { + // Exponentially back off after failed attempts to avoid network spam. + // About 2*$uRTT*(2^n-1) us of "sleep" happen for the next n attempts. + $sleep *= 2; + } + usleep( $sleep ); // back off + $this->clearLastError(); + $locked = $this->add( "{$key}:lock", 1, $expiry ); + if ( $this->getLastError() ) { + $locked = false; // network partition + break; + } + } while ( !$locked && ( microtime( true ) - $timestamp ) < $timeout ); } - $uRTT = ceil( 1e6 * ( microtime( true ) - $timestamp ) ); // estimate RTT (us) - $sleep = 2 * $uRTT; // rough time to do get()+set() - - $locked = false; // lock acquired - $attempts = 0; // failed attempts - do { - if ( ++$attempts >= 3 && $sleep <= 5e5 ) { - // Exponentially back off after failed attempts to avoid network spam. - // About 2*$uRTT*(2^n-1) us of "sleep" happen for the next n attempts. - $sleep *= 2; - } - usleep( $sleep ); // back off - $this->clearLastError(); - $locked = $this->add( "{$key}:lock", 1, $expiry ); - if ( $this->getLastError() ) { - return false; - } - } while ( !$locked && ( microtime( true ) - $timestamp ) < $timeout ); + if ( $locked ) { + $this->locks[$key] = array( 'class' => $rclass, 'depth' => 1 ); + } return $locked; } /** + * Release an advisory lock on a key string + * * @param string $key * @return bool Success */ public function unlock( $key ) { - return $this->delete( "{$key}:lock" ); + if ( isset( $this->locks[$key] ) && --$this->locks[$key]['depth'] <= 0 ) { + unset( $this->locks[$key] ); + + return $this->delete( "{$key}:lock" ); + } + + return true; + } + + /** + * Get a lightweight exclusive self-unlocking lock + * + * Note that the same lock cannot be acquired twice. + * + * This is useful for task de-duplication or to avoid obtrusive + * (though non-corrupting) DB errors like INSERT key conflicts + * or deadlocks when using LOCK IN SHARE MODE. + * + * @param string $key + * @param int $timeout Lock wait timeout; 0 for non-blocking [optional] + * @param int $expiry Lock expiry [optional]; 1 day maximum + * @param string $rclass Allow reentry if set and the current lock used this value + * @return ScopedCallback|null Returns null on failure + * @since 1.26 + */ + final public function getScopedLock( $key, $timeout = 6, $expiry = 30, $rclass = '' ) { + $expiry = min( $expiry ?: INF, 86400 ); + + if ( !$this->lock( $key, $timeout, $expiry, $rclass ) ) { + return null; + } + + $lSince = microtime( true ); // lock timestamp + // PHP 5.3: Can't use $this in a closure + $that = $this; + $logger = $this->logger; + + return new ScopedCallback( function() use ( $that, $logger, $key, $lSince, $expiry ) { + $latency = .050; // latency skew (err towards keeping lock present) + $age = ( microtime( true ) - $lSince + $latency ); + if ( ( $age + $latency ) >= $expiry ) { + $logger->warning( "Lock for $key held too long ($age sec)." ); + return; // expired; it's not "safe" to delete the key + } + $that->unlock( $key ); + } ); } /** @@ -260,14 +348,13 @@ abstract class BagOStuff implements LoggerAwareInterface { return false; } - /* *** Emulated functions *** */ - /** * Get an associative array containing the item for each of the keys that have items. * @param array $keys List of strings + * @param integer $flags Bitfield; supports READ_LATEST [optional] * @return array */ - public function getMulti( array $keys ) { + public function getMulti( array $keys, $flags = 0 ) { $res = array(); foreach ( $keys as $key ) { $val = $this->get( $key ); @@ -334,7 +421,7 @@ abstract class BagOStuff implements LoggerAwareInterface { * Decrease stored value of $key by $value while preserving its TTL * @param string $key * @param int $value - * @return int + * @return int|bool New value or false on failure */ public function decr( $key, $value = 1 ) { return $this->incr( $key, - $value ); @@ -384,6 +471,24 @@ abstract class BagOStuff implements LoggerAwareInterface { } /** + * Modify a cache update operation array for EventRelayer::notify() + * + * This is used for relayed writes, e.g. for broadcasting a change + * to multiple data-centers. If the array contains a 'val' field + * then the command involves setting a key to that value. Note that + * for simplicity, 'val' is always a simple scalar value. This method + * is used to possibly serialize the value and add any cache-specific + * key/values needed for the relayer daemon (e.g. memcached flags). + * + * @param array $event + * @return array + * @since 1.26 + */ + public function modifySimpleRelayEvent( array $event ) { + return $event; + } + + /** * @param string $text */ protected function debug( $text ) { |