29 use Psr\Log\LoggerAwareInterface;
30 use Psr\Log\LoggerInterface;
31 use Psr\Log\NullLogger;
32 use Wikimedia\ScopedCallback;
33 use Wikimedia\WaitConditionLoop;
101 if ( isset(
$params[
'logger'] ) ) {
107 if ( isset(
$params[
'keyspace'] ) ) {
108 $this->keyspace =
$params[
'keyspace'];
111 $this->asyncHandler = isset(
$params[
'asyncHandler'] )
115 if ( !empty(
$params[
'reportDupes'] ) && is_callable( $this->asyncHandler ) ) {
116 $this->reportDupes =
true;
119 $this->syncTimeout = isset(
$params[
'syncTimeout'] ) ?
$params[
'syncTimeout'] : 3;
134 $this->debugMode = $bool;
150 $value = $this->
get( $key, $flags );
153 if ( !is_callable( $callback ) ) {
154 throw new InvalidArgumentException(
"Invalid cache miss callback provided." );
156 $value = call_user_func( $callback );
158 $this->
set( $key,
$value, $ttl );
179 public function get( $key, $flags = 0, $oldFlags = null ) {
181 $flags = is_int( $oldFlags ) ? $oldFlags : $flags;
185 return $this->
doGet( $key, $flags );
193 if ( !$this->reportDupes ) {
197 if ( !isset( $this->duplicateKeyLookups[$key] ) ) {
200 $this->duplicateKeyLookups[$key] = 0;
202 $this->duplicateKeyLookups[$key] += 1;
204 if ( $this->dupeTrackScheduled ===
false ) {
205 $this->dupeTrackScheduled =
true;
207 call_user_func( $this->asyncHandler,
function () {
208 $dups = array_filter( $this->duplicateKeyLookups );
209 foreach ( $dups
as $key => $count ) {
210 $this->logger->warning(
211 'Duplicate get(): "{key}" fetched {count} times',
213 [
'key' => $key,
'count' => $count + 1, ]
226 abstract protected function doGet( $key, $flags = 0 );
238 throw new Exception( __METHOD__ .
' not implemented.' );
250 abstract public function set( $key,
$value, $exptime = 0, $flags = 0 );
258 abstract public function delete( $key );
276 public function merge( $key, callable $callback, $exptime = 0, $attempts = 10, $flags = 0 ) {
277 return $this->
mergeViaLock( $key, $callback, $exptime, $attempts, $flags );
289 protected function mergeViaCas( $key, $callback, $exptime = 0, $attempts = 10 ) {
293 $this->reportDupes =
false;
295 $currentValue = $this->
getWithToken( $key, $casToken, self::READ_LATEST );
303 $value = call_user_func( $callback, $this, $key, $currentValue, $exptime );
306 if ( $value ===
false ) {
308 } elseif ( $currentValue ===
false ) {
318 }
while ( !
$success && --$attempts );
333 protected function cas( $casToken, $key,
$value, $exptime = 0 ) {
334 throw new Exception(
"CAS is not implemented in " . __CLASS__ );
347 protected function mergeViaLock( $key, $callback, $exptime = 0, $attempts = 10, $flags = 0 ) {
348 if ( !$this->
lock( $key, 6 ) ) {
354 $this->reportDupes =
false;
362 $value = call_user_func( $callback, $this, $key, $currentValue, $exptime );
370 if ( !$this->
unlock( $key ) ) {
372 trigger_error(
"Could not release lock for key '$key'." );
387 $value = $this->
get( $key );
403 public function lock( $key, $timeout = 6, $expiry = 6, $rclass =
'' ) {
405 if ( isset( $this->locks[$key] ) ) {
406 if ( $rclass !=
'' && $this->locks[$key][
'class'] === $rclass ) {
407 ++$this->locks[$key][
'depth'];
414 $expiry = min( $expiry ?: INF, self::TTL_DAY );
415 $loop =
new WaitConditionLoop(
416 function ()
use ( $key, $timeout, $expiry ) {
418 if ( $this->
add(
"{$key}:lock", 1, $expiry ) ) {
421 return WaitConditionLoop::CONDITION_ABORTED;
424 return WaitConditionLoop::CONDITION_CONTINUE;
429 $locked = ( $loop->invoke() === $loop::CONDITION_REACHED );
431 $this->locks[$key] = [
'class' => $rclass,
'depth' => 1 ];
444 if ( isset( $this->locks[$key] ) && --$this->locks[$key][
'depth'] <= 0 ) {
445 unset( $this->locks[$key] );
447 return $this->
delete(
"{$key}:lock" );
469 final public function getScopedLock( $key, $timeout = 6, $expiry = 30, $rclass =
'' ) {
470 $expiry = min( $expiry ?: INF, self::TTL_DAY );
472 if ( !$this->
lock( $key, $timeout, $expiry, $rclass ) ) {
476 $lSince = microtime(
true );
478 return new ScopedCallback(
function ()
use ( $key, $lSince, $expiry ) {
480 $age = ( microtime(
true ) - $lSince + $latency );
481 if ( ( $age + $latency ) >= $expiry ) {
482 $this->logger->warning(
"Lock for $key held too long ($age sec)." );
512 $val = $this->
get( $key );
513 if ( $val !==
false ) {
529 foreach ( $data
as $key =>
$value ) {
530 if ( !$this->
set( $key,
$value, $exptime ) ) {
544 if ( $this->
get( $key ) ===
false ) {
545 return $this->
set( $key,
$value, $exptime );
557 if ( !$this->
lock( $key ) ) {
560 $n = $this->
get( $key );
563 $this->
set( $key, max( 0, $n ) );
596 if ( $newValue ===
false ) {
598 $newValue = $this->
add( $key, (
int)$init, $ttl ) ? $init :
false;
600 if ( $newValue ===
false ) {
631 $this->lastError = $err;
655 $this->busyCallbacks[] = $workCallback;
680 if ( $this->debugMode ) {
681 $this->logger->debug(
"{class} debug: $text", [
693 if ( $exptime != 0 && $exptime < ( 10 * self::TTL_YEAR ) ) {
694 return time() + $exptime;
708 if ( $exptime >= ( 10 * self::TTL_YEAR ) ) {
710 if ( $exptime <= 0 ) {
740 $arg = str_replace(
':',
'%3A', $arg );
741 $key = $key .
':' . $arg;
743 return strtr( $key,
' ',
'_' );
766 public function makeKey( $class, $component =
null ) {
776 return isset( $this->attrMap[$flag] ) ? $this->attrMap[$flag] :
self::QOS_UNKNOWN;
787 foreach ( $bags
as $bag ) {
788 foreach ( $bag->attrMap
as $attr => $rank ) {
789 if ( isset( $map[$attr] ) ) {
790 $map[$attr] = min( $map[$attr], $rank );