class APCBagOStuff extends BagOStuff {
/**
* @param $key string
+ * @param $casToken[optional] int
* @return mixed
*/
- public function get( $key ) {
+ public function get( $key, &$casToken = null ) {
$val = apc_fetch( $key );
+ $casToken = $val;
+
if ( is_string( $val ) ) {
if ( $this->isInteger( $val ) ) {
$val = intval( $val );
return true;
}
+ /**
+ * @param $casToken mixed
+ * @param $key string
+ * @param $value mixed
+ * @param $exptime int
+ * @return bool
+ */
+ public function cas( $casToken, $key, $value, $exptime = 0 ) {
+ // APC's CAS functions only work on integers
+ throw new MWException( "CAS is not implemented in " . __CLASS__ );
+ }
+
/**
* @param $key string
* @param $time int
return true;
}
+ /**
+ * @param $key string
+ * @param $callback closure Callback method to be executed
+ * @param $exptime int Either an interval in seconds or a unix timestamp for expiry
+ * @param $attempts int The amount of times to attempt a merge in case of failure
+ * @return bool success
+ */
+ public function merge( $key, closure $callback, $exptime = 0, $attempts = 10 ) {
+ return $this->mergeViaLock( $key, $callback, $exptime, $attempts );
+ }
+
public function incr( $key, $value = 1 ) {
return apc_inc( $key, $value );
}
public function decr( $key, $value = 1 ) {
return apc_dec( $key, $value );
}
-
- /**
- * @return Array
- */
- public function keys() {
- $info = apc_cache_info( 'user' );
- $list = $info['cache_list'];
- $keys = array();
-
- foreach ( $list as $entry ) {
- $keys[] = $entry['info'];
- }
-
- return $keys;
- }
}
/**
* Get an item with the given key. Returns false if it does not exist.
* @param $key string
+ * @param $casToken[optional] mixed
* @return mixed Returns false on failure
*/
- abstract public function get( $key );
+ abstract public function get( $key, &$casToken = null );
/**
* Set an item.
*/
abstract public function set( $key, $value, $exptime = 0 );
+ /**
+ * Check and set an item.
+ * @param $casToken mixed
+ * @param $key string
+ * @param $value mixed
+ * @param $exptime int Either an interval in seconds or a unix timestamp for expiry
+ * @return bool success
+ */
+ abstract public function cas( $casToken, $key, $value, $exptime = 0 );
+
/**
* Delete an item.
* @param $key string
abstract public function delete( $key, $time = 0 );
/**
+ * Merge changes into the existing cache value (possibly creating a new one)
+ *
* @param $key string
- * @param $timeout integer
+ * @param $callback closure Callback method to be executed
+ * @param $exptime int Either an interval in seconds or a unix timestamp for expiry
+ * @param $attempts int The amount of times to attempt a merge in case of failure
* @return bool success
*/
- public function lock( $key, $timeout = 0 ) {
- /* stub */
- return true;
+ public function merge( $key, closure $callback, $exptime = 0, $attempts = 10 ) {
+ return $this->mergeViaCas( $key, $callback, $exptime, $attempts );
+ }
+
+ /**
+ * @see BagOStuff::merge()
+ *
+ * @param $key string
+ * @param $callback closure Callback method to be executed
+ * @param $exptime int Either an interval in seconds or a unix timestamp for expiry
+ * @param $attempts int The amount of times to attempt a merge in case of failure
+ * @return bool success
+ */
+ protected function mergeViaCas( $key, closure $callback, $exptime = 0, $attempts = 10 ) {
+ do {
+ $casToken = null; // passed by reference
+ $currentValue = $this->get( $key, $casToken ); // get the old value
+ $value = $callback( $this, $key, $currentValue ); // derive the new value
+
+ if ( $value === false ) {
+ $success = true; // do nothing
+ } elseif ( $currentValue === false ) {
+ // Try to create the key, failing if it gets created in the meantime
+ $success = $this->add( $key, $value, $exptime );
+ } else {
+ // Try to update the key, failing if it gets changed in the meantime
+ $success = $this->cas( $casToken, $key, $value, $exptime );
+ }
+ } while ( !$success && --$attempts );
+
+ return $success;
+ }
+
+ /**
+ * @see BagOStuff::merge()
+ *
+ * @param $key string
+ * @param $callback closure Callback method to be executed
+ * @param $exptime int Either an interval in seconds or a unix timestamp for expiry
+ * @param $attempts int The amount of times to attempt a merge in case of failure
+ * @return bool success
+ */
+ protected function mergeViaLock( $key, closure $callback, $exptime = 0, $attempts = 10 ) {
+ if ( !$this->lock( $key, 60 ) ) {
+ return false;
+ }
+
+ $currentValue = $this->get( $key ); // get the old value
+ $value = $callback( $this, $key, $currentValue ); // derive the new value
+
+ if ( $value === false ) {
+ $success = true; // do nothing
+ } else {
+ $success = $this->set( $key, $value, $exptime ); // set the new value
+ }
+
+ if ( !$this->unlock( $key ) ) {
+ // this should never happen
+ trigger_error( "Could not release lock for key '$key'." );
+ }
+
+ return $success;
+ }
+
+ /**
+ * @param $key string
+ * @param $timeout integer [optional]
+ * @return bool success
+ */
+ public function lock( $key, $timeout = 60 ) {
+ $timestamp = microtime( true ); // starting UNIX timestamp
+ if ( $this->add( "{$key}:lock", $timeout ) ) {
+ return true;
+ }
+
+ $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 <= 1e6 ) {
+ // 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
+ $locked = $this->add( "{$key}:lock", $timeout );
+ } while( !$locked );
+
+ return $locked;
}
/**
* @return bool success
*/
public function unlock( $key ) {
- /* stub */
- return true;
+ return $this->delete( "{$key}:lock" );
}
- /**
- * @todo: what is this?
- * @return Array
- */
- public function keys() {
- /* stub */
- return array();
- }
-
/**
* Delete all objects expiring before a certain date.
* @param $date string The reference date in MW format
/**
* @param $key string
+ * @param $casToken[optional] mixed
* @return mixed
*/
- public function get( $key ) {
+ public function get( $key, &$casToken = null ) {
wfProfileIn( __METHOD__ );
wfDebug( __METHOD__ . "($key)\n" );
$val = false;
}
+ $casToken = $val;
+
wfProfileOut( __METHOD__ );
+
return $val;
}
return $ret;
}
+ /**
+ * @param $casToken mixed
+ * @param $key string
+ * @param $value mixed
+ * @param $exptime int
+ * @return bool
+ */
+ public function cas( $casToken, $key, $value, $exptime = 0 ) {
+ wfProfileIn( __METHOD__ );
+ wfDebug( __METHOD__ . "($key)\n" );
+
+ $blob = $this->encode( $value, $exptime );
+
+ $handle = $this->getWriter();
+ if ( !$handle ) {
+ wfProfileOut( __METHOD__ );
+ return false;
+ }
+
+ // DBA is locked to any other write connection, so we can safely
+ // compare the current & previous value before saving new value
+ $val = dba_fetch( $key, $handle );
+ list( $val, $exptime ) = $this->decode( $val );
+ if ( $casToken !== $val ) {
+ dba_close( $handle );
+ wfProfileOut( __METHOD__ );
+ return false;
+ }
+
+ $ret = dba_replace( $key, $blob, $handle );
+ dba_close( $handle );
+
+ wfProfileOut( __METHOD__ );
+ return $ret;
+ }
+
/**
* @param $key string
* @param $time int
return ( $value === false ) ? false : (int)$value;
}
-
- function keys() {
- $reader = $this->getReader();
- $k1 = dba_firstkey( $reader );
-
- if ( !$k1 ) {
- return array();
- }
-
- $result[] = $k1;
-
- $key = dba_nextkey( $reader );
- while ( $key ) {
- $result[] = $key;
- $key = dba_nextkey( $reader );
- }
-
- return $result;
- }
}
/**
* @param $key string
+ * @param $casToken[optional] mixed
* @return bool|mixed
*/
- function get( $key ) {
+ function get( $key, &$casToken = null ) {
if ( !isset( $this->bag[$key] ) ) {
return false;
}
return false;
}
+ $casToken = $this->bag[$key][0];
+
return $this->bag[$key][0];
}
return true;
}
+ /**
+ * @param $casToken mixed
+ * @param $key string
+ * @param $value mixed
+ * @param $exptime int
+ * @return bool
+ */
+ function cas( $casToken, $key, $value, $exptime = 0 ) {
+ if ( $this->get( $key ) === $casToken ) {
+ return $this->set( $key, $value, $exptime );
+ }
+
+ return false;
+ }
+
/**
* @param $key string
* @param $time int
return true;
}
-
- /**
- * @return array
- */
- function keys() {
- return array_keys( $this->bag );
- }
}
* - server: A server info structure in the format required by each
* element in $wgDBServers.
*
- * - servers: An array of server info structures describing a set of
+ * - servers: An array of server info structures describing a set of
* database servers to distribute keys to. If this is
* specified, the "server" option will be ignored.
*
*
* - tableName: The table name to use, default is "objectcache".
*
- * - shards: The number of tables to use for data storage on each server.
+ * - shards: The number of tables to use for data storage on each server.
* If this is more than 1, table names will be formed in the style
* objectcacheNNN where NNN is the shard index, between 0 and
* shards-1. The number of digits will be the minimum number
}
# Don't keep timing out trying to connect for each call if the DB is down
- if ( isset( $this->connFailureErrors[$serverIndex] )
- && ( time() - $this->connFailureTimes[$serverIndex] ) < 60 )
+ if ( isset( $this->connFailureErrors[$serverIndex] )
+ && ( time() - $this->connFailureTimes[$serverIndex] ) < 60 )
{
throw $this->connFailureErrors[$serverIndex];
}
/**
* @param $key string
+ * @param $casToken[optional] mixed
* @return mixed
*/
- public function get( $key ) {
+ public function get( $key, &$casToken = null ) {
$values = $this->getMulti( array( $key ) );
- return array_key_exists( $key, $values ) ? $values[$key] : false;
+ if ( array_key_exists( $key, $values ) ) {
+ $casToken = $values[$key];
+ return $values[$key];
+ }
+ return false;
}
/**
return true;
}
+ /**
+ * @param $casToken mixed
+ * @param $key string
+ * @param $value mixed
+ * @param $exptime int
+ * @return bool
+ */
+ public function cas( $casToken, $key, $value, $exptime = 0 ) {
+ $db = $this->getDB();
+ $exptime = intval( $exptime );
+
+ if ( $exptime < 0 ) {
+ $exptime = 0;
+ }
+
+ if ( $exptime == 0 ) {
+ $encExpiry = $this->getMaxDateTime();
+ } else {
+ if ( $exptime < 3.16e8 ) { # ~10 years
+ $exptime += time();
+ }
+
+ $encExpiry = $db->timestamp( $exptime );
+ }
+ try {
+ $db->begin( __METHOD__ );
+ // (bug 24425) use a replace if the db supports it instead of
+ // delete/insert to avoid clashes with conflicting keynames
+ $db->update(
+ $this->getTableByKey( $key ),
+ array(
+ 'keyname' => $key,
+ 'value' => $db->encodeBlob( $this->serialize( $value ) ),
+ 'exptime' => $encExpiry
+ ),
+ array(
+ 'keyname' => $key,
+ 'value' => $db->encodeBlob( $this->serialize( $casToken ) )
+ ), __METHOD__ );
+ $db->commit( __METHOD__ );
+ } catch ( DBQueryError $e ) {
+ $this->handleWriteError( $e );
+
+ return false;
+ }
+
+ return (bool) $db->affectedRows();
+ }
+
/**
* @param $key string
* @param $time int
return $newValue;
}
- /**
- * @return Array
- */
- public function keys() {
- $result = array();
-
- for ( $serverIndex = 0; $serverIndex < $this->numServers; $serverIndex++ ) {
- try {
- $db = $this->getDB( $serverIndex );
- for ( $i = 0; $i < $this->shards; $i++ ) {
- $res = $db->select( $this->getTableNameByShard( $i ),
- array( 'keyname' ), false, __METHOD__ );
- foreach ( $res as $row ) {
- $result[] = $row->keyname;
- }
- }
- } catch ( DBError $e ) {
- $this->handleReadError( $e, $serverIndex );
- }
- }
- return $result;
- }
-
/**
* @param $exptime string
* @return bool
* Get a value from the WinCache object cache
*
* @param $key String: cache key
+ * @param $casToken[optional] int: cas token
* @return mixed
*/
- public function get( $key ) {
+ public function get( $key, &$casToken = null ) {
$val = wincache_ucache_get( $key );
+ $casToken = $val;
+
if ( is_string( $val ) ) {
$val = unserialize( $val );
}
return ( is_array( $result ) && $result === array() ) || $result;
}
+ /**
+ * Store a value in the WinCache object cache, race condition-safe
+ *
+ * @param $casToken int: cas token
+ * @param $key String: cache key
+ * @param $value int: object to store
+ * @param $exptime Int: expiration time
+ * @return bool
+ */
+ public function cas( $casToken, $key, $value, $exptime = 0 ) {
+ return wincache_ucache_cas( $key, $casToken, serialize( $value ) );
+ }
+
/**
* Remove a value from the WinCache object cache
*
return true;
}
-
- /**
- * @return Array
- */
- public function keys() {
- $info = wincache_ucache_info();
- $list = $info['ucache_entries'];
- $keys = array();
-
- if ( is_null( $list ) ) {
- return array();
- }
-
- foreach ( $list as $entry ) {
- $keys[] = $entry['key_name'];
- }
-
- return $keys;
- }
}