29use Psr\Log\LoggerAwareInterface;
30use Psr\Log\LoggerInterface;
31use Psr\Log\NullLogger;
32use Wikimedia\ScopedCallback;
33use Wikimedia\WaitConditionLoop;
104 if ( isset(
$params[
'logger'] ) ) {
110 if ( isset(
$params[
'keyspace'] ) ) {
111 $this->keyspace =
$params[
'keyspace'];
114 $this->asyncHandler = isset(
$params[
'asyncHandler'] )
118 if ( !empty(
$params[
'reportDupes'] ) && is_callable( $this->asyncHandler ) ) {
119 $this->reportDupes =
true;
122 $this->syncTimeout = isset(
$params[
'syncTimeout'] ) ?
$params[
'syncTimeout'] : 3;
130 $this->logger = $logger;
137 $this->debugMode = $bool;
153 $value = $this->
get( $key, $flags );
156 if ( !is_callable( $callback ) ) {
157 throw new InvalidArgumentException(
"Invalid cache miss callback provided." );
159 $value = call_user_func( $callback );
161 $this->
set( $key,
$value, $ttl );
182 public function get( $key, $flags = 0, $oldFlags = null ) {
184 $flags = is_int( $oldFlags ) ? $oldFlags : $flags;
188 return $this->
doGet( $key, $flags );
196 if ( !$this->reportDupes ) {
200 if ( !isset( $this->duplicateKeyLookups[$key] ) ) {
203 $this->duplicateKeyLookups[$key] = 0;
205 $this->duplicateKeyLookups[$key] += 1;
207 if ( $this->dupeTrackScheduled ===
false ) {
208 $this->dupeTrackScheduled =
true;
210 call_user_func( $this->asyncHandler,
function () {
211 $dups = array_filter( $this->duplicateKeyLookups );
212 foreach ( $dups as $key => $count ) {
213 $this->logger->warning(
214 'Duplicate get(): "{key}" fetched {count} times',
216 [
'key' => $key,
'count' => $count + 1, ]
229 abstract protected function doGet( $key, $flags = 0 );
241 throw new Exception( __METHOD__ .
' not implemented.' );
253 abstract public function set( $key,
$value, $exptime = 0, $flags = 0 );
261 abstract public function delete( $key );
279 public function merge( $key, callable $callback, $exptime = 0, $attempts = 10, $flags = 0 ) {
280 return $this->
mergeViaLock( $key, $callback, $exptime, $attempts, $flags );
292 protected function mergeViaCas( $key, $callback, $exptime = 0, $attempts = 10 ) {
296 $this->reportDupes =
false;
298 $currentValue = $this->
getWithToken( $key, $casToken, self::READ_LATEST );
306 $value = call_user_func( $callback, $this, $key, $currentValue, $exptime );
311 } elseif ( $currentValue ===
false ) {
321 }
while ( !
$success && --$attempts );
336 protected function cas( $casToken, $key,
$value, $exptime = 0 ) {
337 throw new Exception(
"CAS is not implemented in " . __CLASS__ );
350 protected function mergeViaLock( $key, $callback, $exptime = 0, $attempts = 10, $flags = 0 ) {
351 if ( !$this->
lock( $key, 6 ) ) {
357 $this->reportDupes =
false;
358 $currentValue = $this->
get( $key, self::READ_LATEST );
365 $value = $callback( $this, $key, $currentValue, $exptime );
373 if ( !$this->
unlock( $key ) ) {
375 trigger_error(
"Could not release lock for key '$key'." );
390 $value = $this->
get( $key );
406 public function lock( $key, $timeout = 6, $expiry = 6, $rclass =
'' ) {
408 if ( isset( $this->locks[$key] ) ) {
409 if ( $rclass !=
'' && $this->locks[$key][
'class'] === $rclass ) {
410 ++$this->locks[$key][
'depth'];
417 $expiry = min( $expiry ?: INF, self::TTL_DAY );
418 $loop =
new WaitConditionLoop(
419 function () use ( $key, $timeout, $expiry ) {
421 if ( $this->
add(
"{$key}:lock", 1, $expiry ) ) {
424 return WaitConditionLoop::CONDITION_ABORTED;
427 return WaitConditionLoop::CONDITION_CONTINUE;
432 $locked = ( $loop->invoke() === $loop::CONDITION_REACHED );
434 $this->locks[$key] = [
'class' => $rclass,
'depth' => 1 ];
447 if ( isset( $this->locks[$key] ) && --$this->locks[$key][
'depth'] <= 0 ) {
448 unset( $this->locks[$key] );
450 return $this->
delete(
"{$key}:lock" );
472 final public function getScopedLock( $key, $timeout = 6, $expiry = 30, $rclass =
'' ) {
473 $expiry = min( $expiry ?: INF, self::TTL_DAY );
475 if ( !$this->
lock( $key, $timeout, $expiry, $rclass ) ) {
481 return new ScopedCallback(
function () use ( $key, $lSince, $expiry ) {
484 if ( ( $age + $latency ) >= $expiry ) {
485 $this->logger->warning(
"Lock for $key held too long ($age sec)." );
514 foreach (
$keys as $key ) {
515 $val = $this->
get( $key );
516 if ( $val !==
false ) {
530 public function setMulti( array $data, $exptime = 0 ) {
532 foreach ( $data as $key =>
$value ) {
533 if ( !$this->
set( $key,
$value, $exptime ) ) {
547 if ( $this->
get( $key ) ===
false ) {
548 return $this->
set( $key,
$value, $exptime );
560 if ( !$this->
lock( $key ) ) {
563 $n = $this->
get( $key );
566 $this->
set( $key, max( 0, $n ) );
599 if ( $newValue ===
false ) {
601 $newValue = $this->
add( $key, (
int)$init, $ttl ) ? $init :
false;
603 if ( $newValue ===
false ) {
617 return $this->lastError;
625 $this->lastError = self::ERR_NONE;
634 $this->lastError = $err;
658 $this->busyCallbacks[] = $workCallback;
683 if ( $this->debugMode ) {
684 $this->logger->debug(
"{class} debug: $text", [
685 'class' => static::class,
696 if ( $exptime != 0 && $exptime < ( 10 * self::TTL_YEAR ) ) {
711 if ( $exptime >= ( 10 * self::TTL_YEAR ) ) {
713 if ( $exptime <= 0 ) {
742 foreach (
$args as $arg ) {
743 $arg = str_replace(
':',
'%3A', $arg );
744 $key = $key .
':' . $arg;
746 return strtr( $key,
' ',
'_' );
769 public function makeKey( $class, $component =
null ) {
779 return isset( $this->attrMap[$flag] ) ? $this->attrMap[$flag] : self::QOS_UNKNOWN;
790 foreach ( $bags as $bag ) {
791 foreach ( $bag->attrMap as $attr => $rank ) {
792 if ( isset( $map[$attr] ) ) {
793 $map[$attr] = min( $map[$attr], $rank );
808 return $this->wallClockOverride ?: microtime(
true );
816 $this->wallClockOverride =&
$time;
interface is intended to be more or less compatible with the PHP memcached client.
int[] $attrMap
Map of (ATTR_* class constant => QOS_* class constant)
getWithSetCallback( $key, $ttl, $callback, $flags=0)
Get an item with the given key, regenerating and setting it if not found.
__construct(array $params=[])
$params include:
getScopedLock( $key, $timeout=6, $expiry=30, $rclass='')
Get a lightweight exclusive self-unlocking lock.
unlock( $key)
Release an advisory lock on a key string.
incrWithInit( $key, $ttl, $value=1, $init=1)
Increase stored value of $key by $value while preserving its TTL.
isInteger( $value)
Check if a value is an integer.
lock( $key, $timeout=6, $expiry=6, $rclass='')
Acquire an advisory lock on a key string.
makeKey( $class, $component=null)
Make a cache key, scoped to this instance's keyspace.
array $duplicateKeyLookups
add( $key, $value, $exptime=0)
getMulti(array $keys, $flags=0)
Get an associative array containing the item for each of the keys that have items.
decr( $key, $value=1)
Decrease stored value of $key by $value while preserving its TTL.
modifySimpleRelayEvent(array $event)
Modify a cache update operation array for EventRelayer::notify()
float null $wallClockOverride
const READ_LATEST
Bitfield constants for get()/getMulti()
deleteObjectsExpiringBefore( $date, $progressCallback=false)
Delete all objects expiring before a certain date.
trackDuplicateKeys( $key)
Track the number of times that a given key has been used.
convertExpiry( $exptime)
Convert an optionally relative time to an absolute time.
callable[] $busyCallbacks
callback null $asyncHandler
getWithToken( $key, &$casToken, $flags=0)
setMulti(array $data, $exptime=0)
Batch insertion.
const ERR_NONE
Possible values for getLastError()
convertToRelative( $exptime)
Convert an optionally absolute expiry time to a relative time.
setLogger(LoggerInterface $logger)
const WRITE_SYNC
Bitfield constants for set()/merge()
mergeFlagMaps(array $bags)
Merge the flag maps of one or more BagOStuff objects into a "lowest common denominator" map.
getLastError()
Get the "last error" registered; clearLastError() should be called manually.
setLastError( $err)
Set the "last error" registry.
changeTTL( $key, $expiry=0)
Reset the TTL on a key if it exists.
mergeViaLock( $key, $callback, $exptime=0, $attempts=10, $flags=0)
clearLastError()
Clear the "last error" registry.
addBusyCallback(callable $workCallback)
Let a callback be run to avoid wasting time on special blocking calls.
cas( $casToken, $key, $value, $exptime=0)
Check and set an item.
makeKeyInternal( $keyspace, $args)
Construct a cache key.
int $lastError
ERR_* class constant.
array[] $locks
Lock tracking.
mergeViaCas( $key, $callback, $exptime=0, $attempts=10)
incr( $key, $value=1)
Increase stored value of $key by $value while preserving its TTL.
merge( $key, callable $callback, $exptime=0, $attempts=10, $flags=0)
Merge changes into the existing cache value (possibly creating a new one)
makeGlobalKey( $class, $component=null)
Make a global cache key.
see documentation in includes Linker php for Linker::makeImageLink & $time
An extension or a local will often add custom code to the function with or without a global variable For someone wanting email notification when an article is shown may add
processing should stop and the error should be shown to the user * false
Generic base class for storage interfaces.