/** @var int ERR_* constant for the "last error" registry */
protected $lastRelayError = self::ERR_NONE;
+ /** @var integer Callback stack depth for getWithSetCallback() */
+ private $callbackDepth = 0;
/** @var mixed[] Temporary warm-up cache */
private $warmupCache = [];
$checkKeysForAll = [];
$checkKeysByKey = [];
$checkKeysFlat = [];
- foreach ( $checkKeys as $i => $keys ) {
- $prefixed = self::prefixCacheKeys( (array)$keys, self::TIME_KEY_PREFIX );
+ foreach ( $checkKeys as $i => $checkKeyGroup ) {
+ $prefixed = self::prefixCacheKeys( (array)$checkKeyGroup, self::TIME_KEY_PREFIX );
$checkKeysFlat = array_merge( $checkKeysFlat, $prefixed );
// Is this check keys for a specific cache key, or for all keys being fetched?
if ( is_int( $i ) ) {
$wrapExtra[self::FLD_FLAGS] = self::FLG_STALE; // mark as stale
// Case B: any long-running transaction; ignore this set()
} elseif ( $age > self::MAX_READ_LAG ) {
- $this->logger->warning( "Rejected set() for $key due to snapshot lag." );
+ $this->logger->info( "Rejected set() for $key due to snapshot lag." );
return true; // no-op the write for being unsafe
// Case C: high replication lag; lower TTL instead of ignoring all set()s
$this->logger->warning( "Lowered set() TTL for $key due to replication lag." );
// Case D: medium length request with medium replication lag; ignore this set()
} else {
- $this->logger->warning( "Rejected set() for $key due to high read lag." );
+ $this->logger->info( "Rejected set() for $key due to high read lag." );
return true; // no-op the write for being unsafe
}
final public function getWithSetCallback( $key, $ttl, $callback, array $opts = [] ) {
$pcTTL = isset( $opts['pcTTL'] ) ? $opts['pcTTL'] : self::TTL_UNCACHEABLE;
- // Try the process cache if enabled
- if ( $pcTTL >= 0 ) {
+ // Try the process cache if enabled and the cache callback is not within a cache callback.
+ // Process cache use in nested callbacks is not lag-safe with regard to HOLDOFF_TTL since
+ // the in-memory value is further lagged than the shared one since it uses a blind TTL.
+ if ( $pcTTL >= 0 && $this->callbackDepth == 0 ) {
$group = isset( $opts['pcGroup'] ) ? $opts['pcGroup'] : self::PC_PRIMARY;
$procCache = $this->getProcessCache( $group );
$value = $procCache->get( $key );
$cValue = $this->get( $key, $curTTL, $checkKeys, $asOf ); // current value
$value = $cValue; // return value
- // Determine if a regeneration is desired
+ $preCallbackTime = microtime( true );
+ // Determine if a cached value regeneration is needed or desired
if ( $value !== false
&& $curTTL > 0
&& $this->isValid( $value, $versioned, $asOf, $minTime )
&& !$this->worthRefreshExpiring( $curTTL, $lowTTL )
- && !$this->worthRefreshPopular( $asOf, $ageNew, $popWindow )
+ && !$this->worthRefreshPopular( $asOf, $ageNew, $popWindow, $preCallbackTime )
) {
return $value;
}
// Generate the new value from the callback...
$setOpts = [];
- $value = call_user_func_array( $callback, [ $cValue, &$ttl, &$setOpts, $asOf ] );
+ ++$this->callbackDepth;
+ try {
+ $value = call_user_func_array( $callback, [ $cValue, &$ttl, &$setOpts, $asOf ] );
+ } finally {
+ --$this->callbackDepth;
+ }
// When delete() is called, writes are write-holed by the tombstone,
// so use a special INTERIM key to pass the new value around threads.
if ( ( $isTombstone && $lockTSE > 0 ) && $value !== false && $ttl >= 0 ) {
}
if ( $value !== false && $ttl >= 0 ) {
- // Update the cache; this will fail if the key is tombstoned
$setOpts['lockTSE'] = $lockTSE;
+ // Use best known "since" timestamp if not provided
+ $setOpts += [ 'since' => $preCallbackTime ];
+ // Update the cache; this will fail if the key is tombstoned
$this->set( $key, $value, $ttl, $setOpts );
}
* @param float $asOf UNIX timestamp of the value
* @param integer $ageNew Age of key when this might recommend refreshing (seconds)
* @param integer $timeTillRefresh Age of key when it should be refreshed if popular (seconds)
+ * @param float $now The current UNIX timestamp
* @return bool
*/
- protected function worthRefreshPopular( $asOf, $ageNew, $timeTillRefresh ) {
- $age = microtime( true ) - $asOf;
+ protected function worthRefreshPopular( $asOf, $ageNew, $timeTillRefresh, $now ) {
+ $age = $now - $asOf;
$timeOld = $age - $ageNew;
if ( $timeOld <= 0 ) {
return false;
}
- // Lifecycle is: new, ramp-up refresh chance, full refresh chance
+ // Lifecycle is: new, ramp-up refresh chance, full refresh chance.
+ // Note that the "expected # of refreshes" for the ramp-up time range is half of what it
+ // would be if P(refresh) was at its full value during that time range.
$refreshWindowSec = max( $timeTillRefresh - $ageNew - self::RAMPUP_TTL / 2, 1 );
// P(refresh) * (# hits in $refreshWindowSec) = (expected # of refreshes)
// P(refresh) * ($refreshWindowSec * $popularHitsPerSec) = 1