Merge "objectcache: make BagOStuff key encoding more consistent"

This commit is contained in:
jenkins-bot 2020-12-11 20:35:23 +00:00 committed by Gerrit Code Review
commit 8a53d3c2df
17 changed files with 756 additions and 285 deletions

View file

@ -161,4 +161,12 @@ class APCUBagOStuff extends MediumSpecificBagOStuff {
return $result;
}
public function makeKeyInternal( $keyspace, $components ) {
return $this->genericKeyFromComponents( $keyspace, ...$components );
}
protected function convertGenericKey( $key ) {
return $key; // short-circuit; already uses "generic" keys
}
}

View file

@ -71,13 +71,16 @@ abstract class BagOStuff implements
{
/** @var LoggerInterface */
protected $logger;
/** @var callable|null */
protected $asyncHandler;
/** @var int[] Map of (ATTR_* class constant => QOS_* class constant) */
protected $attrMap = [];
/** @var bool */
/** @var string Default keyspace; used by makeKey() */
protected $keyspace;
/** @var bool Whether to send debug log entries to the SPI logger instance */
protected $debugMode = false;
/** @var float|null */
@ -93,15 +96,40 @@ abstract class BagOStuff implements
public const WRITE_PRUNE_SEGMENTS = 32; // delete all the segments if the value is partitioned
public const WRITE_BACKGROUND = 64; // if supported, do not block on completion until the next read
/** @var string Global keyspace; used by makeGlobalKey() */
protected const GLOBAL_KEYSPACE = 'global';
/** @var string Precomputed global cache key prefix (needs no encoding) */
protected const GLOBAL_PREFIX = 'global:';
/** @var string Precomputed global cache key prefix length */
protected const GLOBAL_PREFIX_LEN = 7;
/** @var int Item is a single cache key */
protected const ARG0_KEY = 0;
/** @var int Item is an array of cache keys */
protected const ARG0_KEYARR = 1;
/** @var int Item is an array indexed by cache keys */
protected const ARG0_KEYMAP = 2;
/** @var int Item does not involve any keys */
protected const ARG0_NONKEY = 3;
/** @var int Item is an array indexed by cache keys */
protected const RES_KEYMAP = 0;
/** @var int Item does not involve any keys */
protected const RES_NONKEY = 1;
/**
* Parameters include:
* - logger: Psr\Log\LoggerInterface instance
* - keyspace: Keyspace to use for keys in makeKey(). [Default: "local"]
* - asyncHandler: Callable to use for scheduling tasks after the web request ends.
* In CLI mode, it should run the task immediately.
* In CLI mode, it should run the task immediately. [Default: null]
* - logger: Psr\Log\LoggerInterface instance. [optional]
* @param array $params
* @phan-param array{logger?:Psr\Log\LoggerInterface,asyncHandler?:callable} $params
* @codingStandardsIgnoreStart
* @phan-param array{keyspace?:string,logger?:Psr\Log\LoggerInterface,asyncHandler?:callable} $params
* @codingStandardsIgnoreEnd
*/
public function __construct( array $params = [] ) {
$this->keyspace = $params['keyspace'] ?? 'local';
$this->setLogger( $params['logger'] ?? new NullLogger() );
$this->asyncHandler = $params['asyncHandler'] ?? null;
}
@ -310,13 +338,13 @@ abstract class BagOStuff implements
}
/**
* Delete all objects expiring before a certain date.
* Delete all objects expiring before a certain date
*
* @param string|int $timestamp The reference date in MW or TS_UNIX format
* @param callable|null $progress Optional, a function which will be called
* regularly during long-running operations with the percentage progress
* as the first parameter. [optional]
* @param int $limit Maximum number of keys to delete [default: INF]
*
* @return bool Success; false if unimplemented
*/
abstract public function deleteObjectsExpiringBefore(
@ -326,7 +354,8 @@ abstract class BagOStuff implements
);
/**
* Get an associative array containing the item for each of the keys that have items.
* Get an associative array containing the item for each of the keys that have items
*
* @param string[] $keys List of keys
* @param int $flags Bitfield; supports READ_LATEST [optional]
* @return mixed[] Map of (key => value) for existing keys
@ -340,13 +369,13 @@ abstract class BagOStuff implements
*
* WRITE_BACKGROUND can be used for bulk insertion where the response is not vital
*
* @param mixed[] $data Map of (key => value)
* @param mixed[] $valueByKey Map of (key => value)
* @param int $exptime Either an interval in seconds or a unix timestamp for expiry
* @param int $flags Bitfield of BagOStuff::WRITE_* constants (since 1.33)
* @return bool Success
* @since 1.24
*/
abstract public function setMulti( array $data, $exptime = 0, $flags = 0 );
abstract public function setMulti( array $valueByKey, $exptime = 0, $flags = 0 );
/**
* Batch deletion
@ -377,6 +406,7 @@ abstract class BagOStuff implements
/**
* Increase stored value of $key by $value while preserving its TTL
*
* @param string $key Key to increase
* @param int $value Value to add to $key (default: 1) [optional]
* @param int $flags Bit field of class WRITE_* constants [optional]
@ -386,6 +416,7 @@ abstract class BagOStuff implements
/**
* Decrease stored value of $key by $value while preserving its TTL
*
* @param string $key
* @param int $value Value to subtract from $key (default: 1) [optional]
* @param int $flags Bit field of class WRITE_* constants [optional]
@ -446,36 +477,51 @@ abstract class BagOStuff implements
abstract public function addBusyCallback( callable $workCallback );
/**
* Construct a cache key.
* Make a cache key for the given keyspace and components
*
* Long components might be converted to respective hashes due to size constraints.
* In extreme cases, all of them might be combined into a single hash component.
*
* @internal This method should not be used outside of BagOStuff (since 1.36)
*
* @param string $keyspace Keyspace component
* @param string[]|int[] $components Key components (key collection name first)
* @return string Keyspace-prepended list of encoded components as a colon-separated value
* @since 1.27
* @param string $keyspace
* @param array $components
* @return string Colon-delimited list of $keyspace followed by escaped components of $args
*/
abstract public function makeKeyInternal( $keyspace, $components );
/**
* Make a global cache key.
* Make a cache key for the default keyspace and given components
*
* @param string $class Key collection name component
* @param string|int ...$components Key components for entity IDs
* @return string Keyspace-prepended list of encoded components as a colon-separated value
* @since 1.27
* @param string $class Key class
* @param string|int ...$components Key components (starting with a key collection name)
* @return string Colon-delimited list of $keyspace followed by escaped components
*/
abstract public function makeGlobalKey( $class, ...$components );
/**
* Make a cache key, scoped to this instance's keyspace.
* Make a cache key for the global keyspace and given components
*
* @param string $class Key collection name component
* @param string|int ...$components Key components for entity IDs
* @return string Keyspace-prepended list of encoded components as a colon-separated value
* @since 1.27
* @param string $class Key class
* @param string|int ...$components Key components (starting with a key collection name)
* @return string Colon-delimited list of $keyspace followed by escaped components
*/
abstract public function makeKey( $class, ...$components );
/**
* Check whether a cache key is in the global keyspace
*
* @param string $key
* @return bool
* @since 1.35
*/
public function isKeyGlobal( $key ) {
return ( strncmp( $key, self::GLOBAL_PREFIX, self::GLOBAL_PREFIX_LEN ) === 0 );
}
/**
* @param int $flag ATTR_* class constant
* @return int QOS_* class constant
@ -533,7 +579,7 @@ abstract class BagOStuff implements
}
/**
* Prepare values for storage and get their serialized sizes, or, estimate those sizes
* Make a "generic" reversible cache key from the given components
*
* All previously prepared values will be cleared. Each of the new prepared values will be
* individually cleared as they get used by write operations for that key. This is done to
@ -559,6 +605,102 @@ abstract class BagOStuff implements
*/
abstract public function setNewPreparedValues( array $valueByKey );
/**
* At a minimum, there must be a keyspace and collection name component
*
* @param string|int ...$components Key components for keyspace, collection name, and IDs
* @return string Keyspace-prepended list of encoded components as a colon-separated value
* @since 1.35
*/
final protected function genericKeyFromComponents( ...$components ) {
if ( count( $components ) < 2 ) {
throw new InvalidArgumentException( "Missing keyspace or collection name" );
}
$key = '';
foreach ( $components as $component ) {
if ( $key !== '' ) {
$key .= ':';
}
// Escape delimiter (":") and escape ("%") characters
$key .= strtr( $component, [ '%' => '%25', ':' => '%3A' ] );
}
return $key;
}
/**
* Extract the components from a "generic" reversible cache key
*
* @see BagOStuff::genericKeyFromComponents()
*
* @param string $key Keyspace-prepended list of encoded components as a colon-separated value
* @return string[] Key components for keyspace, collection name, and IDs
* @since 1.35
*/
final protected function componentsFromGenericKey( $key ) {
// Note that the order of each corresponding search/replace pair matters
return str_replace( [ '%3A', '%25' ], [ ':', '%' ], explode( ':', $key ) );
}
/**
* Convert a "generic" reversible cache key into one for this cache
*
* @see BagOStuff::genericKeyFromComponents()
*
* @param string $key Keyspace-prepended list of encoded components as a colon-separated value
* @return string Keyspace-prepended list of encoded components as a colon-separated value
*/
abstract protected function convertGenericKey( $key );
/**
* Call a method on behalf of wrapper BagOStuff instance that uses "generic" keys
*
* @param string $method Name of a non-final public method that reads/changes keys
* @param int $arg0Sig BagOStuff::ARG0_* constant describing argument 0
* @param int $resSig BagOStuff::RES_* constant describing the return value
* @param array $genericArgs Method arguments passed to the wrapper instance
* @return mixed Method result with any resulting cache keys remapped to "generic" keys
*/
protected function proxyCall( $method, $arg0Sig, $resSig, array $genericArgs ) {
// Get the corresponding store-specific cache keys...
$storeArgs = $genericArgs;
switch ( $arg0Sig ) {
case self::ARG0_KEY:
$storeArgs[0] = $this->convertGenericKey( $genericArgs[0] );
break;
case self::ARG0_KEYARR:
foreach ( $genericArgs[0] as $i => $genericKey ) {
$storeArgs[0][$i] = $this->convertGenericKey( $genericKey );
}
break;
case self::ARG0_KEYMAP:
$storeArgs[0] = [];
foreach ( $genericArgs[0] as $genericKey => $v ) {
$storeArgs[0][$this->convertGenericKey( $genericKey )] = $v;
}
break;
}
// Result of invoking the method with the corresponding store-specific cache keys
$storeRes = $this->$method( ...$storeArgs );
// Convert any store-specific cache keys in the result back to generic cache keys
if ( $resSig === self::RES_KEYMAP ) {
// Map of (store-specific cache key => generic cache key)
$genericKeyByStoreKey = array_combine( $storeArgs[0], $genericArgs[0] );
$genericRes = [];
foreach ( $storeRes as $storeKey => $value ) {
$genericRes[$genericKeyByStoreKey[$storeKey]] = $value;
}
} else {
$genericRes = $storeRes;
}
return $genericRes;
}
/**
* @internal For testing only
* @return float UNIX timestamp

View file

@ -36,7 +36,7 @@
*/
class CachedBagOStuff extends BagOStuff {
/** @var BagOStuff */
protected $backend;
protected $store;
/** @var HashBagOStuff */
protected $procCache;
@ -46,65 +46,78 @@ class CachedBagOStuff extends BagOStuff {
* @param array $params Parameters for HashBagOStuff
*/
public function __construct( BagOStuff $backend, $params = [] ) {
$params['keyspace'] = $backend->keyspace;
parent::__construct( $params );
$this->backend = $backend;
$this->store = $backend;
$this->procCache = new HashBagOStuff( $params );
$this->attrMap = $backend->attrMap;
}
public function setDebug( $enabled ) {
parent::setDebug( $enabled );
$this->backend->setDebug( $enabled );
$this->store->setDebug( $enabled );
}
public function get( $key, $flags = 0 ) {
$value = $this->procCache->get( $key, $flags );
if ( $value === false && !$this->procCache->hasKey( $key ) ) {
$value = $this->backend->get( $key, $flags );
$this->set( $key, $value, self::TTL_INDEFINITE, self::WRITE_CACHE_ONLY );
if ( $value !== false || $this->procCache->hasKey( $key ) ) {
return $value;
}
$value = $this->store->proxyCall(
__FUNCTION__,
self::ARG0_KEY,
self::RES_NONKEY,
func_get_args()
);
$this->set( $key, $value, self::TTL_INDEFINITE, self::WRITE_CACHE_ONLY );
return $value;
}
public function getMulti( array $keys, $flags = 0 ) {
$valuesByKeyCached = [];
$valueByKeyCached = [];
$keysMissing = [];
$keysFetch = [];
foreach ( $keys as $key ) {
$value = $this->procCache->get( $key, $flags );
if ( $value === false && !$this->procCache->hasKey( $key ) ) {
$keysMissing[] = $key;
$keysFetch[] = $key;
} else {
$valuesByKeyCached[$key] = $value;
$valueByKeyCached[$key] = $value;
}
}
$valuesByKeyFetched = $this->backend->getMulti( $keysMissing, $flags );
$this->setMulti( $valuesByKeyFetched, self::TTL_INDEFINITE, self::WRITE_CACHE_ONLY );
$valueByKeyFetched = $this->store->proxyCall(
__FUNCTION__,
self::ARG0_KEYARR,
self::RES_KEYMAP,
[ $keysFetch, $flags ]
);
$this->setMulti( $valueByKeyFetched, self::TTL_INDEFINITE, self::WRITE_CACHE_ONLY );
return $valuesByKeyCached + $valuesByKeyFetched;
return $valueByKeyCached + $valueByKeyFetched;
}
public function set( $key, $value, $exptime = 0, $flags = 0 ) {
$this->procCache->set( $key, $value, $exptime, $flags );
if ( !$this->fieldHasFlags( $flags, self::WRITE_CACHE_ONLY ) ) {
$this->backend->set( $key, $value, $exptime, $flags );
if ( $this->fieldHasFlags( $flags, self::WRITE_CACHE_ONLY ) ) {
return true;
}
return true;
return $this->store->proxyCall( __FUNCTION__, self::ARG0_KEY, self::RES_NONKEY, func_get_args() );
}
public function delete( $key, $flags = 0 ) {
$this->procCache->delete( $key, $flags );
if ( !$this->fieldHasFlags( $flags, self::WRITE_CACHE_ONLY ) ) {
$this->backend->delete( $key, $flags );
if ( $this->fieldHasFlags( $flags, self::WRITE_CACHE_ONLY ) ) {
return true;
}
return true;
return $this->store->proxyCall( __FUNCTION__, self::ARG0_KEY, self::RES_NONKEY, func_get_args() );
}
public function add( $key, $value, $exptime = 0, $flags = 0 ) {
@ -121,21 +134,21 @@ class CachedBagOStuff extends BagOStuff {
public function merge( $key, callable $callback, $exptime = 0, $attempts = 10, $flags = 0 ) {
$this->procCache->delete( $key );
return $this->backend->merge( $key, $callback, $exptime, $attempts, $flags );
return $this->store->proxyCall( __FUNCTION__, self::ARG0_KEY, self::RES_NONKEY, func_get_args() );
}
public function changeTTL( $key, $exptime = 0, $flags = 0 ) {
$this->procCache->delete( $key );
return $this->backend->changeTTL( $key, $exptime, $flags );
return $this->store->proxyCall( __FUNCTION__, self::ARG0_KEY, self::RES_NONKEY, func_get_args() );
}
public function lock( $key, $timeout = 6, $expiry = 6, $rclass = '' ) {
return $this->backend->lock( $key, $timeout, $expiry, $rclass );
return $this->store->proxyCall( __FUNCTION__, self::ARG0_KEY, self::RES_NONKEY, func_get_args() );
}
public function unlock( $key ) {
return $this->backend->unlock( $key );
return $this->store->proxyCall( __FUNCTION__, self::ARG0_KEY, self::RES_NONKEY, func_get_args() );
}
public function deleteObjectsExpiringBefore(
@ -145,89 +158,93 @@ class CachedBagOStuff extends BagOStuff {
) {
$this->procCache->deleteObjectsExpiringBefore( $timestamp, $progress, $limit );
return $this->backend->deleteObjectsExpiringBefore( $timestamp, $progress, $limit );
return $this->store->proxyCall( __FUNCTION__, self::ARG0_NONKEY, self::RES_NONKEY, func_get_args() );
}
public function makeKeyInternal( $keyspace, $components ) {
return $this->backend->makeKeyInternal( $keyspace, $components );
return $this->genericKeyFromComponents( $keyspace, ...$components );
}
public function makeKey( $class, ...$components ) {
return $this->backend->makeKey( $class, ...$components );
return $this->genericKeyFromComponents( $this->keyspace, $class, ...$components );
}
public function makeGlobalKey( $class, ...$components ) {
return $this->backend->makeGlobalKey( $class, ...$components );
return $this->genericKeyFromComponents( self::GLOBAL_KEYSPACE, $class, ...$components );
}
protected function convertGenericKey( $key ) {
return $key; // short-circuit; already uses "generic" keys
}
public function getLastError() {
return $this->backend->getLastError();
return $this->store->getLastError();
}
public function clearLastError() {
return $this->backend->clearLastError();
return $this->store->clearLastError();
}
public function setMulti( array $data, $exptime = 0, $flags = 0 ) {
$this->procCache->setMulti( $data, $exptime, $flags );
public function setMulti( array $valueByKey, $exptime = 0, $flags = 0 ) {
$this->procCache->setMulti( $valueByKey, $exptime, $flags );
if ( !$this->fieldHasFlags( $flags, self::WRITE_CACHE_ONLY ) ) {
return $this->backend->setMulti( $data, $exptime, $flags );
if ( $this->fieldHasFlags( $flags, self::WRITE_CACHE_ONLY ) ) {
return true;
}
return true;
return $this->store->proxyCall( __FUNCTION__, self::ARG0_KEYMAP, self::RES_NONKEY, func_get_args() );
}
public function deleteMulti( array $keys, $flags = 0 ) {
$this->procCache->deleteMulti( $keys, $flags );
if ( !$this->fieldHasFlags( $flags, self::WRITE_CACHE_ONLY ) ) {
return $this->backend->deleteMulti( $keys, $flags );
if ( $this->fieldHasFlags( $flags, self::WRITE_CACHE_ONLY ) ) {
return true;
}
return true;
return $this->store->proxyCall( __FUNCTION__, self::ARG0_KEYARR, self::RES_NONKEY, func_get_args() );
}
public function changeTTLMulti( array $keys, $exptime, $flags = 0 ) {
$this->procCache->changeTTLMulti( $keys, $exptime, $flags );
if ( !$this->fieldHasFlags( $flags, self::WRITE_CACHE_ONLY ) ) {
return $this->backend->changeTTLMulti( $keys, $exptime, $flags );
if ( $this->fieldHasFlags( $flags, self::WRITE_CACHE_ONLY ) ) {
return true;
}
return true;
return $this->store->proxyCall( __FUNCTION__, self::ARG0_KEYARR, self::RES_NONKEY, func_get_args() );
}
public function incr( $key, $value = 1, $flags = 0 ) {
$this->procCache->delete( $key );
return $this->backend->incr( $key, $value, $flags );
return $this->store->proxyCall( __FUNCTION__, self::ARG0_KEY, self::RES_NONKEY, func_get_args() );
}
public function decr( $key, $value = 1, $flags = 0 ) {
$this->procCache->delete( $key );
return $this->backend->decr( $key, $value, $flags );
return $this->store->proxyCall( __FUNCTION__, self::ARG0_KEY, self::RES_NONKEY, func_get_args() );
}
public function incrWithInit( $key, $exptime, $value = 1, $init = null, $flags = 0 ) {
$this->procCache->delete( $key );
return $this->backend->incrWithInit( $key, $exptime, $value, $init, $flags );
return $this->store->proxyCall( __FUNCTION__, self::ARG0_KEY, self::RES_NONKEY, func_get_args() );
}
public function addBusyCallback( callable $workCallback ) {
$this->backend->addBusyCallback( $workCallback );
$this->store->addBusyCallback( $workCallback );
}
public function setNewPreparedValues( array $valueByKey ) {
return $this->backend->setNewPreparedValues( $valueByKey );
return $this->store->proxyCall( __FUNCTION__, self::ARG0_KEYMAP, self::RES_NONKEY, func_get_args() );
}
public function setMockTime( &$time ) {
parent::setMockTime( $time );
$this->procCache->setMockTime( $time );
$this->backend->setMockTime( $time );
$this->store->setMockTime( $time );
}
// @codeCoverageIgnoreEnd

View file

@ -66,4 +66,12 @@ class EmptyBagOStuff extends MediumSpecificBagOStuff {
public function merge( $key, callable $callback, $exptime = 0, $attempts = 10, $flags = 0 ) {
return true; // faster
}
public function makeKeyInternal( $keyspace, $components ) {
return $this->genericKeyFromComponents( $keyspace, ...$components );
}
protected function convertGenericKey( $key ) {
return $key; // short-circuit; already uses "generic" keys
}
}

View file

@ -177,4 +177,12 @@ class HashBagOStuff extends MediumSpecificBagOStuff {
public function hasKey( $key ) {
return isset( $this->bag[$key] );
}
public function makeKeyInternal( $keyspace, $components ) {
return $this->genericKeyFromComponents( $keyspace, ...$components );
}
protected function convertGenericKey( $key ) {
return $key; // short-circuit; already uses "generic" keys
}
}

View file

@ -8,20 +8,20 @@
*/
interface IStoreKeyEncoder {
/**
* Make a global cache key.
* Make a cache key using the "global" keyspace for the given components
*
* @param string $class Key class
* @param string|int ...$components Key components (starting with a key collection name)
* @return string Colon-delimited list of $keyspace followed by escaped components
* @param string $class Key collection name component
* @param string|int ...$components Key components for entity IDs
* @return string Keyspace-prepended list of encoded components as a colon-separated value
*/
public function makeGlobalKey( $class, ...$components );
/**
* Make a cache key, scoped to this instance's keyspace.
* Make a cache key using the default keyspace for the given components
*
* @param string $class Key class
* @param string|int ...$components Key components (starting with a key collection name)
* @return string Colon-delimited list of $keyspace followed by escaped components
* @param string $class Key collection name component
* @param string|int ...$components Key components for entity IDs
* @return string Keyspace-prepended list of encoded components as a colon-separated value
*/
public function makeKey( $class, ...$components );
}

View file

@ -36,8 +36,6 @@ abstract class MediumSpecificBagOStuff extends BagOStuff {
protected $locks = [];
/** @var int ERR_* class constant */
protected $lastError = self::ERR_NONE;
/** @var string */
protected $keyspace = 'local';
/** @var int Seconds */
protected $syncTimeout;
/** @var int Bytes; chunk size of segmented cache values */
@ -68,7 +66,6 @@ abstract class MediumSpecificBagOStuff extends BagOStuff {
* @see BagOStuff::__construct()
* Additional $params options include:
* - logger: Psr\Log\LoggerInterface instance
* - keyspace: Default keyspace for $this->makeKey()
* - reportDupes: Whether to emit warning log messages for all keys that were
* requested more than once (requires an asyncHandler).
* - syncTimeout: How long to wait with WRITE_SYNC in seconds.
@ -80,16 +77,12 @@ abstract class MediumSpecificBagOStuff extends BagOStuff {
* amount of I/O between application and cache servers that the network can handle.
* @param array $params
* @codingStandardsIgnoreStart
* @phan-param array{logger?:Psr\Log\LoggerInterface,asyncHandler?:callable,keyspace?:string,reportDupes?:bool,syncTimeout?:int,segmentationSize?:int,segmentedValueMaxSize?:int} $params
* @phan-param array{logger?:Psr\Log\LoggerInterface,asyncHandler?:callable,reportDupes?:bool,syncTimeout?:int,segmentationSize?:int,segmentedValueMaxSize?:int} $params
* @codingStandardsIgnoreEnd
*/
public function __construct( array $params = [] ) {
parent::__construct( $params );
if ( isset( $params['keyspace'] ) ) {
$this->keyspace = $params['keyspace'];
}
if ( !empty( $params['reportDupes'] ) && is_callable( $this->asyncHandler ) ) {
$this->reportDupes = true;
}
@ -600,18 +593,18 @@ abstract class MediumSpecificBagOStuff extends BagOStuff {
*
* This does not support WRITE_ALLOW_SEGMENTS to avoid excessive read I/O
*
* @param mixed[] $data Map of (key => value)
* @param mixed[] $valueByKey Map of (key => value)
* @param int $exptime Either an interval in seconds or a unix timestamp for expiry
* @param int $flags Bitfield of BagOStuff::WRITE_* constants (since 1.33)
* @return bool Success
* @since 1.24
*/
public function setMulti( array $data, $exptime = 0, $flags = 0 ) {
public function setMulti( array $valueByKey, $exptime = 0, $flags = 0 ) {
if ( $this->fieldHasFlags( $flags, self::WRITE_ALLOW_SEGMENTS ) ) {
throw new InvalidArgumentException( __METHOD__ . ' got WRITE_ALLOW_SEGMENTS' );
}
return $this->doSetMulti( $data, $exptime, $flags );
return $this->doSetMulti( $valueByKey, $exptime, $flags );
}
/**
@ -890,43 +883,26 @@ abstract class MediumSpecificBagOStuff extends BagOStuff {
return ( $value === (string)$integer );
}
public function makeKeyInternal( $keyspace, $components ) {
$key = $keyspace;
foreach ( $components as $component ) {
$key .= ':' . str_replace( ':', '%3A', $component );
}
return strtr( $key, ' ', '_' );
}
/**
* Make a global cache key.
*
* @param string $class Key class
* @param string|int ...$components Key components (starting with a key collection name)
* @return string Colon-delimited list of $keyspace followed by escaped components
* @since 1.27
*/
public function makeGlobalKey( $class, ...$components ) {
return $this->makeKeyInternal( 'global', func_get_args() );
return $this->makeKeyInternal( self::GLOBAL_KEYSPACE, func_get_args() );
}
/**
* Make a cache key, scoped to this instance's keyspace.
*
* @param string $class Key class
* @param string|int ...$components Key components (starting with a key collection name)
* @return string Colon-delimited list of $keyspace followed by escaped components
* @since 1.27
*/
public function makeKey( $class, ...$components ) {
return $this->makeKeyInternal( $this->keyspace, func_get_args() );
}
/**
* @param int $flag ATTR_* class constant
* @return int QOS_* class constant
* @since 1.28
*/
protected function convertGenericKey( $key ) {
$components = $this->componentsFromGenericKey( $key );
if ( count( $components ) < 2 ) {
// Legacy key not from makeKey()/makeGlobalKey(); keep it as-is
return $key;
}
$keyspace = array_shift( $components );
return $this->makeKeyInternal( $keyspace, $components );
}
public function getQoS( $flag ) {
return $this->attrMap[$flag] ?? self::QOS_UNKNOWN;
}

View file

@ -34,8 +34,9 @@ use Wikimedia\ObjectFactory;
* @ingroup Cache
*/
class MultiWriteBagOStuff extends BagOStuff {
/** @var BagOStuff[] */
/** @var BagOStuff[] Backing cache stores in order of highest to lowest tier */
protected $caches;
/** @var bool Use async secondary writes */
protected $asyncWrites = false;
/** @var int[] List of all backing cache indexes */
@ -111,17 +112,31 @@ class MultiWriteBagOStuff extends BagOStuff {
}
public function get( $key, $flags = 0 ) {
$args = func_get_args();
if ( $this->fieldHasFlags( $flags, self::READ_LATEST ) ) {
// If the latest write was a delete(), we do NOT want to fallback
// to the other tiers and possibly see the old value. Also, this
// is used by merge(), which only needs to hit the primary.
return $this->caches[0]->get( $key, $flags );
return $this->callKeyMethodOnTierCache(
0,
__FUNCTION__,
self::ARG0_KEY,
self::RES_NONKEY,
$args
);
}
$value = false;
$missIndexes = []; // backends checked
foreach ( $this->caches as $i => $cache ) {
$value = $cache->get( $key, $flags );
foreach ( $this->cacheIndexes as $i ) {
$value = $this->callKeyMethodOnTierCache(
$i,
__FUNCTION__,
self::ARG0_KEY,
self::RES_NONKEY,
$args
);
if ( $value !== false ) {
break;
}
@ -134,11 +149,12 @@ class MultiWriteBagOStuff extends BagOStuff {
$missIndexes
) {
// Backfill the value to the higher (and often faster/smaller) cache tiers
$this->doWrite(
$this->callKeyWriteMethodOnTierCaches(
$missIndexes,
$this->asyncWrites,
'set',
// @TODO: consider using self::WRITE_ALLOW_SEGMENTS here?
self::ARG0_KEY,
self::RES_NONKEY,
[ $key, $value, self::$UPGRADE_TTL ]
);
}
@ -147,73 +163,99 @@ class MultiWriteBagOStuff extends BagOStuff {
}
public function set( $key, $value, $exptime = 0, $flags = 0 ) {
return $this->doWrite(
return $this->callKeyWriteMethodOnTierCaches(
$this->cacheIndexes,
$this->usesAsyncWritesGivenFlags( $flags ),
$this->useAsyncSecondaryWrites( $flags ),
__FUNCTION__,
self::ARG0_KEY,
self::RES_NONKEY,
func_get_args()
);
}
public function delete( $key, $flags = 0 ) {
return $this->doWrite(
return $this->callKeyWriteMethodOnTierCaches(
$this->cacheIndexes,
$this->usesAsyncWritesGivenFlags( $flags ),
$this->useAsyncSecondaryWrites( $flags ),
__FUNCTION__,
self::ARG0_KEY,
self::RES_NONKEY,
func_get_args()
);
}
public function add( $key, $value, $exptime = 0, $flags = 0 ) {
// Try the write to the top-tier cache
$ok = $this->doWrite(
[ 0 ],
$this->usesAsyncWritesGivenFlags( $flags ),
$ok = $this->callKeyMethodOnTierCache(
0,
__FUNCTION__,
self::ARG0_KEY,
self::RES_NONKEY,
func_get_args()
);
if ( $ok ) {
// Relay the add() using set() if it succeeded. This is meant to handle certain
// migration scenarios where the same store might get written to twice for certain
// keys. In that case, it does not make sense to return false due to "self-conflicts".
return $this->doWrite(
// keys. In that case, it makes no sense to return false due to "self-conflicts".
$okSecondaries = $this->callKeyWriteMethodOnTierCaches(
array_slice( $this->cacheIndexes, 1 ),
$this->usesAsyncWritesGivenFlags( $flags ),
$this->useAsyncSecondaryWrites( $flags ),
'set',
self::ARG0_KEY,
self::RES_NONKEY,
[ $key, $value, $exptime, $flags ]
);
if ( $okSecondaries === false ) {
$ok = false;
}
}
return false;
return $ok;
}
public function merge( $key, callable $callback, $exptime = 0, $attempts = 10, $flags = 0 ) {
return $this->doWrite(
return $this->callKeyWriteMethodOnTierCaches(
$this->cacheIndexes,
$this->usesAsyncWritesGivenFlags( $flags ),
$this->useAsyncSecondaryWrites( $flags ),
__FUNCTION__,
self::ARG0_KEY,
self::RES_NONKEY,
func_get_args()
);
}
public function changeTTL( $key, $exptime = 0, $flags = 0 ) {
return $this->doWrite(
return $this->callKeyWriteMethodOnTierCaches(
$this->cacheIndexes,
$this->usesAsyncWritesGivenFlags( $flags ),
$this->useAsyncSecondaryWrites( $flags ),
__FUNCTION__,
self::ARG0_KEY,
self::RES_NONKEY,
func_get_args()
);
}
public function lock( $key, $timeout = 6, $expiry = 6, $rclass = '' ) {
// Only need to lock the first cache; also avoids deadlocks
return $this->caches[0]->lock( $key, $timeout, $expiry, $rclass );
return $this->callKeyMethodOnTierCache(
0,
__FUNCTION__,
self::ARG0_KEY,
self::RES_NONKEY,
func_get_args()
);
}
public function unlock( $key ) {
// Only the first cache is locked
return $this->caches[0]->unlock( $key );
return $this->callKeyMethodOnTierCache(
0,
__FUNCTION__,
self::ARG0_KEY,
self::RES_NONKEY,
func_get_args()
);
}
public function deleteObjectsExpiringBefore(
@ -244,127 +286,103 @@ class MultiWriteBagOStuff extends BagOStuff {
return $res;
}
public function setMulti( array $data, $exptime = 0, $flags = 0 ) {
return $this->doWrite(
public function setMulti( array $valueByKey, $exptime = 0, $flags = 0 ) {
return $this->callKeyWriteMethodOnTierCaches(
$this->cacheIndexes,
$this->usesAsyncWritesGivenFlags( $flags ),
$this->useAsyncSecondaryWrites( $flags ),
__FUNCTION__,
self::ARG0_KEYMAP,
self::RES_NONKEY,
func_get_args()
);
}
public function deleteMulti( array $data, $flags = 0 ) {
return $this->doWrite(
public function deleteMulti( array $keys, $flags = 0 ) {
return $this->callKeyWriteMethodOnTierCaches(
$this->cacheIndexes,
$this->usesAsyncWritesGivenFlags( $flags ),
$this->useAsyncSecondaryWrites( $flags ),
__FUNCTION__,
self::ARG0_KEYARR,
self::RES_NONKEY,
func_get_args()
);
}
public function changeTTLMulti( array $keys, $exptime, $flags = 0 ) {
return $this->doWrite(
return $this->callKeyWriteMethodOnTierCaches(
$this->cacheIndexes,
$this->usesAsyncWritesGivenFlags( $flags ),
$this->useAsyncSecondaryWrites( $flags ),
__FUNCTION__,
self::ARG0_KEYARR,
self::RES_NONKEY,
func_get_args()
);
}
public function incr( $key, $value = 1, $flags = 0 ) {
return $this->doWrite(
return $this->callKeyWriteMethodOnTierCaches(
$this->cacheIndexes,
$this->asyncWrites,
$this->useAsyncSecondaryWrites( $flags ),
__FUNCTION__,
self::ARG0_KEY,
self::RES_NONKEY,
func_get_args()
);
}
public function decr( $key, $value = 1, $flags = 0 ) {
return $this->doWrite(
return $this->callKeyWriteMethodOnTierCaches(
$this->cacheIndexes,
$this->asyncWrites,
$this->useAsyncSecondaryWrites( $flags ),
__FUNCTION__,
self::ARG0_KEY,
self::RES_NONKEY,
func_get_args()
);
}
public function incrWithInit( $key, $exptime, $value = 1, $init = null, $flags = 0 ) {
return $this->doWrite(
return $this->callKeyWriteMethodOnTierCaches(
$this->cacheIndexes,
$this->asyncWrites,
$this->useAsyncSecondaryWrites( $flags ),
__FUNCTION__,
self::ARG0_KEY,
self::RES_NONKEY,
func_get_args()
);
}
public function getLastError() {
return $this->caches[0]->getLastError();
}
public function clearLastError() {
$this->caches[0]->clearLastError();
}
/**
* Apply a write method to the backing caches specified by $indexes (in order)
*
* @param int[] $indexes List of backing cache indexes
* @param bool $asyncWrites
* @param string $method Method name of backing caches
* @param array $args Arguments to the method of backing caches
* @return bool
*/
protected function doWrite( $indexes, $asyncWrites, $method, array $args ) {
$ret = true;
if ( array_diff( $indexes, [ 0 ] ) && $asyncWrites && $method !== 'merge' ) {
// Deep-clone $args to prevent misbehavior when something writes an
// object to the BagOStuff then modifies it afterwards, e.g. T168040.
$args = unserialize( serialize( $args ) );
}
foreach ( $indexes as $i ) {
$cache = $this->caches[$i];
if ( $i == 0 || !$asyncWrites ) {
// First store or in sync mode: write now and get result
if ( !$cache->$method( ...$args ) ) {
$ret = false;
}
} else {
// Secondary write in async mode: do not block this HTTP request
$logger = $this->logger;
( $this->asyncHandler )(
function () use ( $cache, $method, $args, $logger ) {
if ( !$cache->$method( ...$args ) ) {
$logger->warning( "Async $method op failed" );
}
}
);
foreach ( $this->caches as $cache ) {
$error = $cache->getLastError();
if ( $error !== self::ERR_NONE ) {
return $error;
}
}
return $ret;
return self::ERR_NONE;
}
/**
* @param int $flags
* @return bool
*/
protected function usesAsyncWritesGivenFlags( $flags ) {
return $this->fieldHasFlags( $flags, self::WRITE_SYNC ) ? false : $this->asyncWrites;
public function clearLastError() {
foreach ( $this->caches as $cache ) {
$cache->clearLastError();
}
}
public function makeKeyInternal( $keyspace, $components ) {
return $this->caches[0]->makeKeyInternal( $keyspace, $components );
return $this->genericKeyFromComponents( $keyspace, ...$components );
}
public function makeKey( $class, ...$components ) {
return $this->caches[0]->makeKey( ...func_get_args() );
return $this->genericKeyFromComponents( $this->keyspace, $class, ...$components );
}
public function makeGlobalKey( $class, ...$components ) {
return $this->caches[0]->makeGlobalKey( ...func_get_args() );
return $this->genericKeyFromComponents( self::GLOBAL_KEYSPACE, $class, ...$components );
}
protected function convertGenericKey( $key ) {
return $key; // short-circuit; already uses "generic" keys
}
public function addBusyCallback( callable $workCallback ) {
@ -372,7 +390,13 @@ class MultiWriteBagOStuff extends BagOStuff {
}
public function setNewPreparedValues( array $valueByKey ) {
return $this->caches[0]->setNewPreparedValues( $valueByKey );
return $this->callKeyMethodOnTierCache(
0,
__FUNCTION__,
self::ARG0_KEYMAP,
self::RES_NONKEY,
func_get_args()
);
}
public function setMockTime( &$time ) {
@ -381,4 +405,77 @@ class MultiWriteBagOStuff extends BagOStuff {
$cache->setMockTime( $time );
}
}
/**
* Call a method on the cache instance for the given cache tier (index)
*
* @param int $index Cache tier
* @param string $method Method name
* @param int $arg0Sig BagOStuff::A0_* constant describing argument 0
* @param int $rvSig BagOStuff::RV_* constant describing the return value
* @param array $args Method arguments
* @return mixed The result of calling the given method
*/
private function callKeyMethodOnTierCache( $index, $method, $arg0Sig, $rvSig, array $args ) {
return $this->caches[$index]->proxyCall( $method, $arg0Sig, $rvSig, $args );
}
/**
* Call a write method on the cache instances, in order, for the given tiers (indexes)
*
* @param int[] $indexes List of cache tiers
* @param bool $asyncSecondary Whether to use asynchronous writes for secondary tiers
* @param string $method Method name
* @param int $arg0Sig BagOStuff::ARG0_* constant describing argument 0
* @param int $resSig BagOStuff::RES_* constant describing the return value
* @param array $args Method arguments
* @return mixed First synchronous result or false if any failed; null if all asynchronous
*/
private function callKeyWriteMethodOnTierCaches(
array $indexes,
$asyncSecondary,
$method,
$arg0Sig,
$resSig,
array $args
) {
$res = null;
if ( $asyncSecondary && array_diff( $indexes, [ 0 ] ) && $method !== 'merge' ) {
// Deep-clone $args to prevent misbehavior when something writes an
// object to the BagOStuff then modifies it afterwards, e.g. T168040.
$args = unserialize( serialize( $args ) );
}
foreach ( $indexes as $i ) {
$cache = $this->caches[$i];
if ( $i == 0 || !$asyncSecondary ) {
// Tier 0 store or in sync mode: write synchronously and get result
$storeRes = $cache->proxyCall( $method, $arg0Sig, $resSig, $args );
if ( $storeRes === false ) {
$res = false;
} elseif ( $res === null ) {
$res = $storeRes; // first synchronous result
}
} else {
// Secondary write in async mode: do not block this HTTP request
( $this->asyncHandler )(
function () use ( $cache, $method, $arg0Sig, $resSig, $args ) {
$cache->proxyCall( $method, $arg0Sig, $resSig, $args );
}
);
}
}
return $res;
}
/**
* @param int $flags
* @return bool
*/
private function useAsyncSecondaryWrites( $flags ) {
return $this->fieldHasFlags( $flags, self::WRITE_SYNC ) ? false : $this->asyncWrites;
}
}

View file

@ -222,6 +222,14 @@ class RESTBagOStuff extends MediumSpecificBagOStuff {
return $this->incr( $key, -$value, $flags );
}
public function makeKeyInternal( $keyspace, $components ) {
return $this->genericKeyFromComponents( $keyspace, ...$components );
}
protected function convertGenericKey( $key ) {
return $key; // short-circuit; already uses "generic" keys
}
/**
* Processes the response body.
*

View file

@ -521,4 +521,12 @@ class RedisBagOStuff extends MediumSpecificBagOStuff {
public function logRequest( $op, $keys, $server, $e = null ) {
$this->debug( "$op($keys) on $server: " . ( $e ? "failure" : "success" ) );
}
public function makeKeyInternal( $keyspace, $components ) {
return $this->genericKeyFromComponents( $keyspace, ...$components );
}
protected function convertGenericKey( $key ) {
return $key; // short-circuit; already uses "generic" keys
}
}

View file

@ -85,50 +85,89 @@ class ReplicatedBagOStuff extends BagOStuff {
}
public function get( $key, $flags = 0 ) {
return (
$store = (
$this->hadRecentSessionWrite( [ $key ] ) ||
$this->fieldHasFlags( $flags, self::READ_LATEST )
)
? $this->writeStore->get( $key, $flags )
: $this->readStore->get( $key, $flags );
// Try to maintain session consistency and respect READ_LATEST
? $this->writeStore
// Otherwise, just use the default "read" store
: $this->readStore;
return $store->proxyCall( __FUNCTION__, self::ARG0_KEY, self::RES_NONKEY, func_get_args() );
}
public function set( $key, $value, $exptime = 0, $flags = 0 ) {
$this->remarkRecentSessionWrite( [ $key ] );
return $this->writeStore->set( $key, $value, $exptime, $flags );
return $this->writeStore->proxyCall(
__FUNCTION__,
self::ARG0_KEY,
self::RES_NONKEY,
func_get_args()
);
}
public function delete( $key, $flags = 0 ) {
$this->remarkRecentSessionWrite( [ $key ] );
return $this->writeStore->delete( $key, $flags );
return $this->writeStore->proxyCall(
__FUNCTION__,
self::ARG0_KEY,
self::RES_NONKEY,
func_get_args()
);
}
public function add( $key, $value, $exptime = 0, $flags = 0 ) {
$this->remarkRecentSessionWrite( [ $key ] );
return $this->writeStore->add( $key, $value, $exptime, $flags );
return $this->writeStore->proxyCall(
__FUNCTION__,
self::ARG0_KEY,
self::RES_NONKEY,
func_get_args()
);
}
public function merge( $key, callable $callback, $exptime = 0, $attempts = 10, $flags = 0 ) {
$this->remarkRecentSessionWrite( [ $key ] );
return $this->writeStore->merge( $key, $callback, $exptime, $attempts, $flags );
return $this->writeStore->proxyCall(
__FUNCTION__,
self::ARG0_KEY,
self::RES_NONKEY,
func_get_args()
);
}
public function changeTTL( $key, $exptime = 0, $flags = 0 ) {
$this->remarkRecentSessionWrite( [ $key ] );
return $this->writeStore->changeTTL( $key, $exptime, $flags );
return $this->writeStore->proxyCall(
__FUNCTION__,
self::ARG0_KEY,
self::RES_NONKEY,
func_get_args()
);
}
public function lock( $key, $timeout = 6, $expiry = 6, $rclass = '' ) {
return $this->writeStore->lock( $key, $timeout, $expiry, $rclass );
return $this->writeStore->proxyCall(
__FUNCTION__,
self::ARG0_KEY,
self::RES_NONKEY,
func_get_args()
);
}
public function unlock( $key ) {
return $this->writeStore->unlock( $key );
return $this->writeStore->proxyCall(
__FUNCTION__,
self::ARG0_KEY,
self::RES_NONKEY,
func_get_args()
);
}
public function deleteObjectsExpiringBefore(
@ -136,52 +175,91 @@ class ReplicatedBagOStuff extends BagOStuff {
callable $progress = null,
$limit = INF
) {
return $this->writeStore->deleteObjectsExpiringBefore( $timestamp, $progress, $limit );
return $this->writeStore->proxyCall(
__FUNCTION__,
self::ARG0_NONKEY,
self::RES_NONKEY,
func_get_args()
);
}
public function getMulti( array $keys, $flags = 0 ) {
return (
$store = (
$this->hadRecentSessionWrite( $keys ) ||
$this->fieldHasFlags( $flags, self::READ_LATEST )
)
? $this->writeStore->getMulti( $keys, $flags )
: $this->readStore->getMulti( $keys, $flags );
// Try to maintain session consistency and respect READ_LATEST
? $this->writeStore
// Otherwise, just use the default "read" store
: $this->readStore;
return $store->proxyCall( __FUNCTION__, self::ARG0_KEYARR, self::RES_KEYMAP, func_get_args() );
}
public function setMulti( array $data, $exptime = 0, $flags = 0 ) {
$this->remarkRecentSessionWrite( array_keys( $data ) );
public function setMulti( array $valueByKey, $exptime = 0, $flags = 0 ) {
$this->remarkRecentSessionWrite( array_keys( $valueByKey ) );
return $this->writeStore->setMulti( $data, $exptime, $flags );
return $this->writeStore->proxyCall(
__FUNCTION__,
self::ARG0_KEYMAP,
self::RES_KEYMAP,
func_get_args()
);
}
public function deleteMulti( array $keys, $flags = 0 ) {
$this->remarkRecentSessionWrite( $keys );
return $this->writeStore->deleteMulti( $keys, $flags );
return $this->writeStore->proxyCall(
__FUNCTION__,
self::ARG0_KEYARR,
self::RES_NONKEY,
func_get_args()
);
}
public function changeTTLMulti( array $keys, $exptime, $flags = 0 ) {
$this->remarkRecentSessionWrite( $keys );
return $this->writeStore->changeTTLMulti( $keys, $exptime, $flags );
return $this->writeStore->proxyCall(
__FUNCTION__,
self::ARG0_KEYARR,
self::RES_NONKEY,
func_get_args()
);
}
public function incr( $key, $value = 1, $flags = 0 ) {
$this->remarkRecentSessionWrite( [ $key ] );
return $this->writeStore->incr( $key, $value, $flags );
return $this->writeStore->proxyCall(
__FUNCTION__,
self::ARG0_KEY,
self::RES_NONKEY,
func_get_args()
);
}
public function decr( $key, $value = 1, $flags = 0 ) {
$this->remarkRecentSessionWrite( [ $key ] );
return $this->writeStore->decr( $key, $value, $flags );
return $this->writeStore->proxyCall(
__FUNCTION__,
self::ARG0_KEY,
self::RES_NONKEY,
func_get_args()
);
}
public function incrWithInit( $key, $exptime, $value = 1, $init = null, $flags = 0 ) {
$this->remarkRecentSessionWrite( [ $key ] );
return $this->writeStore->incrWithInit( $key, $exptime, $value, $init, $flags );
return $this->writeStore->proxyCall(
__FUNCTION__,
self::ARG0_KEY,
self::RES_NONKEY,
func_get_args()
);
}
public function getLastError() {
@ -196,23 +274,32 @@ class ReplicatedBagOStuff extends BagOStuff {
}
public function makeKeyInternal( $keyspace, $components ) {
return $this->writeStore->makeKeyInternal( ...func_get_args() );
return $this->genericKeyFromComponents( $keyspace, ...$components );
}
public function makeKey( $class, ...$components ) {
return $this->writeStore->makeKey( ...func_get_args() );
return $this->genericKeyFromComponents( $this->keyspace, $class, ...$components );
}
public function makeGlobalKey( $class, ...$components ) {
return $this->writeStore->makeGlobalKey( ...func_get_args() );
return $this->genericKeyFromComponents( self::GLOBAL_KEYSPACE, $class, ...$components );
}
protected function convertGenericKey( $key ) {
return $key; // short-circuit; already uses "generic" keys
}
public function addBusyCallback( callable $workCallback ) {
$this->writeStore->addBusyCallback( $workCallback );
return $this->writeStore->addBusyCallback( $workCallback );
}
public function setNewPreparedValues( array $valueByKey ) {
return $this->writeStore->setNewPreparedValues( $valueByKey );
return $this->writeStore->proxyCall(
__FUNCTION__,
self::ARG0_KEYMAP,
self::RES_NONKEY,
func_get_args()
);
}
public function setMockTime( &$time ) {

View file

@ -2161,7 +2161,7 @@ class WANObjectCache implements
/**
* @see BagOStuff::makeKey()
* @param string $class Key class
* @param string $class Key collection name
* @param string|int ...$components Key components (starting with a key collection name)
* @return string Colon-delimited list of $keyspace followed by escaped components
* @since 1.27
@ -2172,7 +2172,7 @@ class WANObjectCache implements
/**
* @see BagOStuff::makeGlobalKey()
* @param string $class Key class
* @param string $class Key collection name
* @param string|int ...$components Key components (starting with a key collection name)
* @return string Colon-delimited list of $keyspace followed by escaped components
* @since 1.27

View file

@ -62,6 +62,19 @@ abstract class BagOStuffTestBase extends MediaWikiIntegrationTestCase {
);
}
/**
* @covers MediumSpecificBagOStuff::isKeyGlobal
*/
public function testKeyIsGlobal() {
$cache = new HashBagOStuff();
$localKey = $cache->makeKey( 'first', 'second', 'third' );
$globalKey = $cache->makeGlobalKey( 'first', 'second', 'third' );
$this->assertFalse( $cache->isKeyGlobal( $localKey ) );
$this->assertTrue( $cache->isKeyGlobal( $globalKey ) );
}
/**
* @covers MediumSpecificBagOStuff::merge
* @covers MediumSpecificBagOStuff::mergeViaCas

View file

@ -126,19 +126,20 @@ class CachedBagOStuffTest extends PHPUnit\Framework\TestCase {
*/
public function testMakeKey() {
$backend = $this->getMockBuilder( HashBagOStuff::class )
->setConstructorArgs( [ [ 'keyspace' => 'magic' ] ] )
->setMethods( [ 'makeKey' ] )
->getMock();
$backend->method( 'makeKey' )
->willReturn( 'special/logic' );
// CachedBagOStuff wraps any backend with a process cache
// using HashBagOStuff. Hash has no special key limitations,
// but backends often do. Make sure it uses the backend's
// makeKey() logic, not the one inherited from HashBagOStuff
$cache = new CachedBagOStuff( $backend );
$this->assertEquals( 'special/logic', $backend->makeKey( 'special', 'logic' ) );
$this->assertEquals( 'special/logic', $cache->makeKey( 'special', 'logic' ) );
$this->assertSame( 'special/logic', $backend->makeKey( 'special', 'logic' ) );
$this->assertSame(
'magic:special:logic',
$cache->makeKey( 'special', 'logic' ),
"Backend keyspace used"
);
}
/**
@ -146,6 +147,7 @@ class CachedBagOStuffTest extends PHPUnit\Framework\TestCase {
*/
public function testMakeGlobalKey() {
$backend = $this->getMockBuilder( HashBagOStuff::class )
->setConstructorArgs( [ [ 'keyspace' => 'magic' ] ] )
->setMethods( [ 'makeGlobalKey' ] )
->getMock();
$backend->method( 'makeGlobalKey' )
@ -153,7 +155,7 @@ class CachedBagOStuffTest extends PHPUnit\Framework\TestCase {
$cache = new CachedBagOStuff( $backend );
$this->assertEquals( 'special/logic', $backend->makeGlobalKey( 'special', 'logic' ) );
$this->assertEquals( 'special/logic', $cache->makeGlobalKey( 'special', 'logic' ) );
$this->assertSame( 'special/logic', $backend->makeGlobalKey( 'special', 'logic' ) );
$this->assertSame( 'global:special:logic', $cache->makeGlobalKey( 'special', 'logic' ) );
}
}

View file

@ -25,17 +25,31 @@ class MultiWriteBagOStuffTest extends MediaWikiIntegrationTestCase {
/**
* @covers MultiWriteBagOStuff::set
* @covers MultiWriteBagOStuff::doWrite
*/
public function testSetImmediate() {
public function testSet() {
$key = 'key';
$value = 'value';
$this->cache->set( $key, $value );
// Set in tier 1
$this->assertEquals( $value, $this->cache1->get( $key ), 'Written to tier 1' );
$this->assertSame( $value, $this->cache1->get( $key ), 'Written to tier 1' );
// Set in tier 2
$this->assertEquals( $value, $this->cache2->get( $key ), 'Written to tier 2' );
$this->assertSame( $value, $this->cache2->get( $key ), 'Written to tier 2' );
}
/**
* @covers MultiWriteBagOStuff::add
*/
public function testAdd() {
$key = 'key';
$value = 'value';
$ok = $this->cache->add( $key, $value );
$this->assertTrue( $ok );
// Set in tier 1
$this->assertSame( $value, $this->cache1->get( $key ), 'Written to tier 1' );
// Set in tier 2
$this->assertSame( $value, $this->cache2->get( $key ), 'Written to tier 2' );
}
/**
@ -109,15 +123,18 @@ class MultiWriteBagOStuffTest extends MediaWikiIntegrationTestCase {
public function testMakeKey() {
$cache1 = $this->getMockBuilder( HashBagOStuff::class )
->setMethods( [ 'makeKey' ] )->getMock();
$cache1->expects( $this->once() )->method( 'makeKey' )
->willReturn( 'special' );
$cache1->expects( $this->never() )->method( 'makeKey' );
$cache2 = $this->getMockBuilder( HashBagOStuff::class )
->setMethods( [ 'makeKey' ] )->getMock();
$cache2->expects( $this->never() )->method( 'makeKey' );
$cache = new MultiWriteBagOStuff( [ 'caches' => [ $cache1, $cache2 ] ] );
$this->assertSame( 'special', $cache->makeKey( 'a', 'b' ) );
$cache = new MultiWriteBagOStuff( [
'keyspace' => 'generic',
'caches' => [ $cache1, $cache2 ]
] );
$this->assertSame( 'generic:a:b', $cache->makeKey( 'a', 'b' ) );
}
/**
@ -126,8 +143,7 @@ class MultiWriteBagOStuffTest extends MediaWikiIntegrationTestCase {
public function testMakeGlobalKey() {
$cache1 = $this->getMockBuilder( HashBagOStuff::class )
->setMethods( [ 'makeGlobalKey' ] )->getMock();
$cache1->expects( $this->once() )->method( 'makeGlobalKey' )
->willReturn( 'special' );
$cache1->expects( $this->never() )->method( 'makeGlobalKey' );
$cache2 = $this->getMockBuilder( HashBagOStuff::class )
->setMethods( [ 'makeGlobalKey' ] )->getMock();
@ -135,7 +151,7 @@ class MultiWriteBagOStuffTest extends MediaWikiIntegrationTestCase {
$cache = new MultiWriteBagOStuff( [ 'caches' => [ $cache1, $cache2 ] ] );
$this->assertSame( 'special', $cache->makeGlobalKey( 'a', 'b' ) );
$this->assertSame( 'global:a:b', $cache->makeGlobalKey( 'a', 'b' ) );
}
/**
@ -149,4 +165,50 @@ class MultiWriteBagOStuffTest extends MediaWikiIntegrationTestCase {
$this->assertTrue( $cache->add( 'key', 1, 30 ) );
}
/**
* @covers MultiWriteBagOStuff::incr
*/
public function testIncr() {
$key = $this->cache->makeKey( 'key' );
$this->cache->add( $key, 7, 30 );
$value = $this->cache->incr( $key );
$this->assertSame( 8, $value, 'Value after incrementing' );
$value = $this->cache->get( $key );
$this->assertSame( 8, $value, 'Value after incrementing' );
}
/**
* @covers MultiWriteBagOStuff::decr
*/
public function testDecr() {
$key = $this->cache->makeKey( 'key' );
$this->cache->add( $key, 10, 30 );
$value = $this->cache->decr( $key );
$this->assertSame( 9, $value, 'Value after decrementing' );
$value = $this->cache->get( $key );
$this->assertSame( 9, $value, 'Value after decrementing' );
}
/**
* @covers MultiWriteBagOStuff::incrWithInit
*/
public function testIncrWithInit() {
$key = $this->cache->makeKey( 'key' );
$val = $this->cache->incrWithInit( $key, 0, 1, 3 );
$this->assertSame( 3, $val, "Correct init value" );
$val = $this->cache->incrWithInit( $key, 0, 1, 3 );
$this->assertSame( 4, $val, "Correct init value" );
$this->cache->delete( $key );
$val = $this->cache->incrWithInit( $key, 0, 5 );
$this->assertSame( 5, $val, "Correct init value" );
}
}

View file

@ -72,7 +72,7 @@ class TestBagOStuff extends CachedBagOStuff {
* @return mixed
*/
public function getSessionFromBackend( $id ) {
return $this->backend->get( $this->makeKey( 'MWSession', $id ) );
return $this->store->get( $this->makeKey( 'MWSession', $id ) );
}
/**

View file

@ -14,6 +14,7 @@ class ReplicatedBagOStuffTest extends \MediaWikiUnitTestCase {
$this->writeCache = new HashBagOStuff();
$this->readCache = new HashBagOStuff();
$this->cache = new ReplicatedBagOStuff( [
'keyspace' => 'repl_local',
'writeFactory' => $this->writeCache,
'readFactory' => $this->readCache,
] );
@ -23,7 +24,68 @@ class ReplicatedBagOStuffTest extends \MediaWikiUnitTestCase {
* @covers ReplicatedBagOStuff::set
*/
public function testSet() {
$key = 'a key';
$key = $this->cache->makeKey( 'a', 'key' );
$value = 'a value';
$this->cache->set( $key, $value );
$this->assertSame( $value, $this->writeCache->get( $key ), 'Written' );
$this->assertFalse( $this->readCache->get( $key ), 'Async replication' );
}
/**
* @covers ReplicatedBagOStuff::get
*/
public function testGet() {
$key = $this->cache->makeKey( 'a', 'key' );
$write = 'new value';
$this->writeCache->set( $key, $write );
$read = 'old value';
$this->readCache->set( $key, $read );
$this->assertSame( $read, $this->cache->get( $key ), 'Async replication' );
}
/**
* @covers ReplicatedBagOStuff::get
*/
public function testGetAbsent() {
$key = $this->cache->makeKey( 'a', 'key' );
$value = 'a value';
$this->writeCache->set( $key, $value );
$this->assertFalse( $this->cache->get( $key ), 'Async replication' );
}
/**
* @covers ReplicatedBagOStuff::setMulti
* @covers ReplicatedBagOStuff::getMulti
*/
public function testGetSetMulti() {
$keyA = $this->cache->makeKey( 'key', 'a' );
$keyB = $this->cache->makeKey( 'key', 'b' );
$valueAOld = 'one old value';
$valueBOld = 'another old value';
$valueANew = 'one new value';
$valueBNew = 'another new value';
$this->writeCache->setMulti( [ $keyA => $valueANew, $keyB => $valueBNew ] );
$this->readCache->setMulti( [ $keyA => $valueAOld, $keyB => $valueBOld ] );
$this->assertEquals(
[ $keyA => $valueAOld, $keyB => $valueBOld ],
$this->cache->getMulti( [ $keyA, $keyB ] ),
'Async replication'
);
}
/**
* @covers ReplicatedBagOStuff::get
* @covers ReplicatedBagOStuff::set
*/
public function testGetSetRaw() {
$key = 'a:key';
$value = 'a value';
$this->cache->set( $key, $value );
@ -32,31 +94,4 @@ class ReplicatedBagOStuffTest extends \MediaWikiUnitTestCase {
// Don't write to replica. Replication is deferred to backend.
$this->assertFalse( $this->readCache->get( $key ) );
}
/**
* @covers ReplicatedBagOStuff::get
*/
public function testGet() {
$key = 'a key';
$write = 'one value';
$this->writeCache->set( $key, $write );
$read = 'another value';
$this->readCache->set( $key, $read );
// Read from replica.
$this->assertEquals( $read, $this->cache->get( $key ) );
}
/**
* @covers ReplicatedBagOStuff::get
*/
public function testGetAbsent() {
$key = 'a key';
$value = 'a value';
$this->writeCache->set( $key, $value );
// Don't read from master. No failover if value is absent.
$this->assertFalse( $this->cache->get( $key ) );
}
}