/** @var array[] Lock tracking */
protected $locks = [];
- /** @var integer */
+ /** @var integer ERR_* class constant */
protected $lastError = self::ERR_NONE;
/** @var string */
/** @var LoggerInterface */
protected $logger;
+ /** @var callback|null */
+ protected $asyncHandler;
+
/** @var bool */
private $debugMode = false;
+ /** @var array */
+ private $duplicateKeyLookups = [];
+
+ /** @var bool */
+ private $reportDupes = false;
+
+ /** @var bool */
+ private $dupeTrackScheduled = false;
+
+ /** @var integer[] Map of (ATTR_* class constant => QOS_* class constant) */
+ protected $attrMap = [];
+
/** Possible values for getLastError() */
const ERR_NONE = 0; // no error
const ERR_NO_RESPONSE = 1; // no response
const WRITE_SYNC = 1; // synchronously write to all locations for replicated stores
const WRITE_CACHE_ONLY = 2; // Only change state of the in-memory cache
+ /**
+ * $params include:
+ * - logger: Psr\Log\LoggerInterface instance
+ * - keyspace: Default keyspace for $this->makeKey()
+ * - asyncHandler: Callable to use for scheduling tasks after the web request ends.
+ * In CLI mode, it should run the task immediately.
+ * - reportDupes: Whether to emit warning log messages for all keys that were
+ * requested more than once (requires an asyncHandler).
+ * @param array $params
+ */
public function __construct( array $params = [] ) {
if ( isset( $params['logger'] ) ) {
$this->setLogger( $params['logger'] );
if ( isset( $params['keyspace'] ) ) {
$this->keyspace = $params['keyspace'];
}
+
+ $this->asyncHandler = isset( $params['asyncHandler'] )
+ ? $params['asyncHandler']
+ : null;
+
+ if ( !empty( $params['reportDupes'] ) && is_callable( $this->asyncHandler ) ) {
+ $this->reportDupes = true;
+ }
}
/**
// B/C for ( $key, &$casToken = null, $flags = 0 )
$flags = is_int( $oldFlags ) ? $oldFlags : $flags;
+ $this->trackDuplicateKeys( $key );
+
return $this->doGet( $key, $flags );
}
+ /**
+ * Track the number of times that a given key has been used.
+ * @param string $key
+ */
+ private function trackDuplicateKeys( $key ) {
+ if ( !$this->reportDupes ) {
+ return;
+ }
+
+ if ( !isset( $this->duplicateKeyLookups[$key] ) ) {
+ // Track that we have seen this key. This N-1 counting style allows
+ // easy filtering with array_filter() later.
+ $this->duplicateKeyLookups[$key] = 0;
+ } else {
+ $this->duplicateKeyLookups[$key] += 1;
+
+ if ( $this->dupeTrackScheduled === false ) {
+ $this->dupeTrackScheduled = true;
+ // Schedule a callback that logs keys processed more than once by get().
+ call_user_func( $this->asyncHandler, function () {
+ $dups = array_filter( $this->duplicateKeyLookups );
+ foreach ( $dups as $key => $count ) {
+ $this->logger->warning(
+ 'Duplicate get(): "{key}" fetched {count} times',
+ // Count is N-1 of the actual lookup count
+ [ 'key' => $key, 'count' => $count + 1, ]
+ );
+ }
+ } );
+ }
+ }
+ }
+
/**
* @param string $key
* @param integer $flags Bitfield of BagOStuff::READ_* constants [optional]
abstract public function delete( $key );
/**
- * Merge changes into the existing cache value (possibly creating a new one).
+ * Merge changes into the existing cache value (possibly creating a new one)
+ *
* 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).
+ * (this BagOStuff, cache key, current value, TTL).
+ * The TTL parameter is reference set to $exptime. It can be overriden in the callback.
*
* @param string $key
* @param callable $callback Callback method to be executed
* @return bool Success
* @throws InvalidArgumentException
*/
- public function merge( $key, $callback, $exptime = 0, $attempts = 10, $flags = 0 ) {
- if ( !is_callable( $callback ) ) {
- throw new InvalidArgumentException( "Got invalid callback." );
- }
-
+ public function merge( $key, callable $callback, $exptime = 0, $attempts = 10, $flags = 0 ) {
return $this->mergeViaLock( $key, $callback, $exptime, $attempts, $flags );
}
protected function mergeViaCas( $key, $callback, $exptime = 0, $attempts = 10 ) {
do {
$this->clearLastError();
+ $reportDupes = $this->reportDupes;
+ $this->reportDupes = false;
$casToken = null; // passed by reference
$currentValue = $this->getWithToken( $key, $casToken, self::READ_LATEST );
+ $this->reportDupes = $reportDupes;
+
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 );
+ $value = call_user_func( $callback, $this, $key, $currentValue, $exptime );
$this->clearLastError();
if ( $value === false ) {
}
$this->clearLastError();
+ $reportDupes = $this->reportDupes;
+ $this->reportDupes = false;
$currentValue = $this->get( $key, self::READ_LATEST );
+ $this->reportDupes = $reportDupes;
+
if ( $this->getLastError() ) {
$success = false;
} else {
// Derive the new value from the old value
- $value = call_user_func( $callback, $this, $key, $currentValue );
+ $value = call_user_func( $callback, $this, $key, $currentValue, $exptime );
if ( $value === false ) {
$success = true; // do nothing
} else {
return $success;
}
+ /**
+ * Reset the TTL on a key if it exists
+ *
+ * @param string $key
+ * @param int $expiry
+ * @return bool Success Returns false if there is no key
+ * @since 1.28
+ */
+ public function changeTTL( $key, $expiry = 0 ) {
+ $value = $this->get( $key );
+
+ return ( $value === false ) ? false : $this->set( $key, $value, $expiry );
+ }
+
/**
* Acquire an advisory lock on a key string
*
public function makeKey() {
return $this->makeKeyInternal( $this->keyspace, func_get_args() );
}
+
+ /**
+ * @param integer $flag ATTR_* class constant
+ * @return integer QOS_* class constant
+ * @since 1.28
+ */
+ public function getQoS( $flag ) {
+ return isset( $this->attrMap[$flag] ) ? $this->attrMap[$flag] : self::QOS_UNKNOWN;
+ }
+
+ /**
+ * Merge the flag maps of one or more BagOStuff objects into a "lowest common denominator" map
+ *
+ * @param BagOStuff[] $bags
+ * @return integer[] Resulting flag map (class ATTR_* constant => class QOS_* constant)
+ */
+ protected function mergeFlagMaps( array $bags ) {
+ $map = [];
+ foreach ( $bags as $bag ) {
+ foreach ( $bag->attrMap as $attr => $rank ) {
+ if ( isset( $map[$attr] ) ) {
+ $map[$attr] = min( $map[$attr], $rank );
+ } else {
+ $map[$attr] = $rank;
+ }
+ }
+ }
+
+ return $map;
+ }
}