29 use Psr\Log\LoggerAwareInterface;
30 use Psr\Log\LoggerInterface;
31 use Psr\Log\NullLogger;
32 use Wikimedia\ScopedCallback;
33 use 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;
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 );
309 if ( $value ===
false ) {
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;
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)." );
515 $val = $this->
get( $key );
516 if ( $val !==
false ) {
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 ) {
634 $this->lastError = $err;
658 $this->busyCallbacks[] = $workCallback;
683 if ( $this->debugMode ) {
684 $this->logger->debug(
"{class} debug: $text", [
696 if ( $exptime != 0 && $exptime < ( 10 * self::TTL_YEAR ) ) {
711 if ( $exptime >= ( 10 * self::TTL_YEAR ) ) {
713 if ( $exptime <= 0 ) {
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;