summaryrefslogtreecommitdiff
path: root/includes/libs/objectcache/BagOStuff.php
diff options
context:
space:
mode:
Diffstat (limited to 'includes/libs/objectcache/BagOStuff.php')
-rw-r--r--includes/libs/objectcache/BagOStuff.php201
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 ) {