/** @var int ERR_* constant for the "last error" registry */
protected $lastRelayError = self::ERR_NONE;
+ /** @var mixed[] Temporary warm-up cache */
+ private $warmupCache = [];
+
/** Max time expected to pass between delete() and DB commit finishing */
const MAX_COMMIT_DELAY = 3;
/** Max replication+snapshot lag before applying TTL_LAGGED or disallowing set() */
const TTL_LAGGED = 30;
/** Idiom for delete() for "no hold-off" */
const HOLDOFF_NONE = 0;
+ /** Idiom for getWithSetCallback() for "no minimum required as-of timestamp" */
+ const MIN_TIMESTAMP_NONE = 0.0;
/** Tiny negative float to use when CTL comes up >= 0 due to clock skew */
const TINY_NEGATIVE = -0.000001;
}
// Fetch all of the raw values
- $wrappedValues = $this->cache->getMulti( array_merge( $valueKeys, $checkKeysFlat ) );
+ $keysGet = array_merge( $valueKeys, $checkKeysFlat );
+ if ( $this->warmupCache ) {
+ $wrappedValues = array_intersect_key( $this->warmupCache, array_flip( $keysGet ) );
+ $keysGet = array_diff( $keysGet, array_keys( $wrappedValues ) ); // keys left to fetch
+ } else {
+ $wrappedValues = [];
+ }
+ $wrappedValues += $this->cache->getMulti( $keysGet );
// Time used to compare/init "check" keys (derived after getMulti() to be pessimistic)
$now = microtime( true );
* @param integer $ttl Seconds to live. Special values are:
* - WANObjectCache::TTL_INDEFINITE: Cache forever
* @param array $opts Options map:
- * - lag : Seconds of replica DB lag. Typically, this is either the replica DB lag
- * before the data was read or, if applicable, the replica DB lag before
- * the snapshot-isolated transaction the data was read from started.
- * Default: 0 seconds
- * - since : UNIX timestamp of the data in $value. Typically, this is either
- * the current time the data was read or (if applicable) the time when
- * the snapshot-isolated transaction the data was read from started.
- * Default: 0 seconds
+ * - lag : Seconds of replica DB lag. Typically, this is either the replica DB lag
+ * before the data was read or, if applicable, the replica DB lag before
+ * the snapshot-isolated transaction the data was read from started.
+ * Use false to indicate that replication is not running.
+ * Default: 0 seconds
+ * - since : UNIX timestamp of the data in $value. Typically, this is either
+ * the current time the data was read or (if applicable) the time when
+ * the snapshot-isolated transaction the data was read from started.
+ * Default: 0 seconds
* - pending : Whether this data is possibly from an uncommitted write transaction.
- * Generally, other threads should not see values from the future and
- * they certainly should not see ones that ended up getting rolled back.
- * Default: false
+ * Generally, other threads should not see values from the future and
+ * they certainly should not see ones that ended up getting rolled back.
+ * Default: false
* - lockTSE : if excessive replication/snapshot lag is detected, then store the value
- * with this TTL and flag it as stale. This is only useful if the reads for
- * this key use getWithSetCallback() with "lockTSE" set.
- * Default: WANObjectCache::TSE_NONE
+ * with this TTL and flag it as stale. This is only useful if the reads for
+ * this key use getWithSetCallback() with "lockTSE" set.
+ * Default: WANObjectCache::TSE_NONE
+ * - staleTTL : Seconds to keep the key around if it is stale. The get()/getMulti()
+ * methods return such stale values with a $curTTL of 0, and getWithSetCallback()
+ * will call the regeneration callback in such cases, passing in the old value
+ * and its as-of time to the callback. This is useful if adaptiveTTL() is used
+ * on the old value's as-of time when it is verified as still being correct.
+ * Default: 0.
+ * @note Options added in 1.28: staleTTL
* @return bool Success
*/
final public function set( $key, $value, $ttl = 0, array $opts = [] ) {
$lockTSE = isset( $opts['lockTSE'] ) ? $opts['lockTSE'] : self::TSE_NONE;
$age = isset( $opts['since'] ) ? max( 0, $now - $opts['since'] ) : 0;
$lag = isset( $opts['lag'] ) ? $opts['lag'] : 0;
+ $staleTTL = isset( $opts['staleTTL'] ) ? $opts['staleTTL'] : 0;
// Do not cache potentially uncommitted data as it might get rolled back
if ( !empty( $opts['pending'] ) ) {
: $wrapped;
};
- return $this->cache->merge( self::VALUE_KEY_PREFIX . $key, $func, $ttl, 1 );
+ return $this->cache->merge( self::VALUE_KEY_PREFIX . $key, $func, $ttl + $staleTTL, 1 );
}
/**
* versions are stored alongside older versions concurrently. Avoid storing class objects
* however, as this reduces compatibility (due to serialization).
* Default: null.
+ * - minAsOf: Reject values if they were generated before this UNIX timestamp.
+ * This is useful if the source of a key is suspected of having possibly changed
+ * recently, and the caller wants any such changes to be reflected.
+ * Default: WANObjectCache::MIN_TIMESTAMP_NONE.
* - hotTTR: Expected time-till-refresh for keys that average ~1 hit/second.
* This should be greater than "ageNew". Keys with higher hit rates will regenerate
* more often. This is useful when a popular key is changed but the cache purge was
* - ageNew: Consider popularity refreshes only once a key reaches this age in seconds.
* Default: WANObjectCache::AGE_NEW.
* @return mixed Value found or written to the key
+ * @note Options added in 1.28: version, busyValue, hotTTR, ageNew, pcGroup, minAsOf
* @note Callable type hints are not used to avoid class-autoloading
*/
final public function getWithSetCallback( $key, $ttl, $callback, array $opts = [] ) {
}
if ( $value === false ) {
- unset( $opts['minTime'] ); // not a public feature
-
// Fetch the value over the network
if ( isset( $opts['version'] ) ) {
$version = $opts['version'];
$ttl,
$callback,
// Regenerate value if not newer than $key
- [ 'version' => null, 'minTime' => $asOf ] + $opts
+ [ 'version' => null, 'minAsOf' => $asOf ] + $opts
);
}
} else {
* @param string $key
* @param integer $ttl
* @param callback $callback
- * @param array $opts Options map for getWithSetCallback() which also includes:
- * - minTime: Treat values older than this UNIX timestamp as not existing. Default: null.
+ * @param array $opts Options map for getWithSetCallback()
* @param float &$asOf Cache generation timestamp of returned value [returned]
* @return mixed
* @note Callable type hints are not used to avoid class-autoloading
$busyValue = isset( $opts['busyValue'] ) ? $opts['busyValue'] : null;
$popWindow = isset( $opts['hotTTR'] ) ? $opts['hotTTR'] : self::HOT_TTR;
$ageNew = isset( $opts['ageNew'] ) ? $opts['ageNew'] : self::AGE_NEW;
- $minTime = isset( $opts['minTime'] ) ? $opts['minTime'] : 0.0;
+ $minTime = isset( $opts['minAsOf'] ) ? $opts['minAsOf'] : self::MIN_TIMESTAMP_NONE;
$versioned = isset( $opts['version'] );
// Get the current key value
$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;
}
}
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 );
}
return $value;
}
+ /**
+ * Method to fetch/regenerate multiple cache keys at once
+ *
+ * This works the same as getWithSetCallback() except:
+ * - a) The $keys argument expects the result of WANObjectCache::makeMultiKeys()
+ * - b) The $callback argument expects a callback taking the following arguments:
+ * - $id: ID of an entity to query
+ * - $oldValue : the prior cache value or false if none was present
+ * - &$ttl : a reference to the new value TTL in seconds
+ * - &$setOpts : a reference to options for set() which can be altered
+ * - $oldAsOf : generation UNIX timestamp of $oldValue or null if not present
+ * Aside from the additional $id argument, the other arguments function the same
+ * way they do in getWithSetCallback().
+ * - c) The return value is a map of (cache key => value) in the order of $keyedIds
+ *
+ * @see WANObjectCache::getWithSetCallback()
+ *
+ * Example usage:
+ * @code
+ * $rows = $cache->getMultiWithSetCallback(
+ * // Map of cache keys to entity IDs
+ * $cache->makeMultiKeys(
+ * $this->fileVersionIds(),
+ * function ( $id, WANObjectCache $cache ) {
+ * return $cache->makeKey( 'file-version', $id );
+ * }
+ * ),
+ * // Time-to-live (in seconds)
+ * $cache::TTL_DAY,
+ * // Function that derives the new key value
+ * return function ( $id, $oldValue, &$ttl, array &$setOpts ) {
+ * $dbr = wfGetDB( DB_REPLICA );
+ * // Account for any snapshot/replica DB lag
+ * $setOpts += Database::getCacheSetOptions( $dbr );
+ *
+ * // Load the row for this file
+ * $row = $dbr->selectRow( 'file', '*', [ 'id' => $id ], __METHOD__ );
+ *
+ * return $row ? (array)$row : false;
+ * },
+ * [
+ * // Process cache for 30 seconds
+ * 'pcTTL' => 30,
+ * // Use a dedicated 500 item cache (initialized on-the-fly)
+ * 'pcGroup' => 'file-versions:500'
+ * ]
+ * );
+ * $files = array_map( [ __CLASS__, 'newFromRow' ], $rows );
+ * @endcode
+ *
+ * @param ArrayIterator $keyedIds Result of WANObjectCache::makeMultiKeys()
+ * @param integer $ttl Seconds to live for key updates
+ * @param callable $callback Callback the yields entity regeneration callbacks
+ * @param array $opts Options map
+ * @return array Map of (cache key => value) in the same order as $keyedIds
+ * @since 1.28
+ */
+ final public function getMultiWithSetCallback(
+ ArrayIterator $keyedIds, $ttl, callable $callback, array $opts = []
+ ) {
+ $keysWarmUp = iterator_to_array( $keyedIds, true );
+ $checkKeys = isset( $opts['checkKeys'] ) ? $opts['checkKeys'] : [];
+ foreach ( $checkKeys as $i => $checkKeyOrKeys ) {
+ if ( is_int( $i ) ) {
+ $keysWarmUp[] = $checkKeyOrKeys;
+ } else {
+ $keysWarmUp = array_merge( $keysWarmUp, $checkKeyOrKeys );
+ }
+ }
+
+ $this->warmupCache = $this->cache->getMulti( $keysWarmUp );
+ $this->warmupCache += array_fill_keys( $keysWarmUp, false );
+
+ // Wrap $callback to match the getWithSetCallback() format while passing $id to $callback
+ $id = null;
+ $func = function ( $oldValue, &$ttl, array $setOpts, $oldAsOf ) use ( $callback, &$id ) {
+ return $callback( $id, $oldValue, $ttl, $setOpts, $oldAsOf );
+ };
+
+ $values = [];
+ foreach ( $keyedIds as $key => $id ) {
+ $values[$key] = $this->getWithSetCallback( $key, $ttl, $func, $opts );
+ }
+
+ $this->warmupCache = [];
+
+ return $values;
+ }
+
/**
* @see BagOStuff::makeKey()
* @param string ... Key component
return call_user_func_array( [ $this->cache, __FUNCTION__ ], func_get_args() );
}
+ /**
+ * @param array $entities List of entity IDs
+ * @param callable $keyFunc Callback yielding a key from (entity ID, this WANObjectCache)
+ * @return ArrayIterator Iterator yielding (cache key => entity ID) in $entities order
+ * @since 1.28
+ */
+ public function makeMultiKeys( array $entities, callable $keyFunc ) {
+ $map = [];
+ foreach ( $entities as $entity ) {
+ $map[$keyFunc( $entity, $this )] = $entity;
+ }
+
+ return new ArrayIterator( $map );
+ }
+
/**
* Get the "last error" registered; clearLastError() should be called manually
* @return int ERR_* class constant for the "last error" registry
* @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;