protected $lastRelayError = self::ERR_NONE;
/** Max time expected to pass between delete() and DB commit finishing */
- const MAX_COMMIT_DELAY = 1;
- /** Max expected replication lag for a reasonable storage setup */
- const MAX_REPLICA_LAG = 7;
+ const MAX_COMMIT_DELAY = 3;
+ /** Max replication lag before applying TTL_LAGGED to set() */
+ const MAX_REPLICA_LAG = 5;
/** Max time since snapshot transaction start to avoid no-op of set() */
- const MAX_SNAPSHOT_LAG = 6;
+ const MAX_SNAPSHOT_LAG = 5;
/** Seconds to tombstone keys on delete() */
- const HOLDOFF_TTL = 14; // MAX_COMMIT_DELAY + MAX_REPLICA_LAG + MAX_SNAPSHOT_LAG
+ const HOLDOFF_TTL = 14; // MAX_COMMIT_DELAY + MAX_REPLICA_LAG + MAX_SNAPSHOT_LAG + 1
/** Seconds to keep dependency purge keys around */
const CHECK_KEY_TTL = 31536000; // 1 year
/** Seconds to keep lock keys around */
const LOCK_TTL = 5;
/** Default remaining TTL at which to consider pre-emptive regeneration */
- const LOW_TTL = 10;
+ const LOW_TTL = 30;
/** Default time-since-expiry on a miss that makes a key "hot" */
const LOCK_TSE = 1;
const TTL_UNCACHEABLE = -1;
/** Idiom for getWithSetCallback() callbacks to 'lockTSE' logic */
const TSE_NONE = -1;
+ /** Max TTL to store keys when a data sourced is lagged */
+ const TTL_LAGGED = 30;
/** Cache format version number */
const VERSION = 1;
* isolation can largely be maintained by doing the following:
* - a) Calling delete() on entity change *and* creation, before DB commit
* - b) Keeping transaction duration shorter than delete() hold-off TTL
+ *
* However, pre-snapshot values might still be seen if an update was made
* in a remote datacenter but the purge from delete() didn't relay yet.
*
* - d) T1 reads the row and calls set() due to a cache miss
* - e) Stale value is stuck in cache
*
+ * Setting 'lag' helps avoids keys getting stuck in long-term stale states.
+ *
* Example usage:
* @code
* $dbr = wfGetDB( DB_SLAVE );
+ * $setOpts = Database::getCacheSetOptions( $dbr );
* // Fetch the row from the DB
* $row = $dbr->selectRow( ... );
* $key = wfMemcKey( 'building', $buildingId );
- * // Give the age of the transaction snapshot the data came from
- * $opts = array( 'since' => $dbr->trxTimestamp() );
- * $cache->set( $key, $row, 86400, $opts );
+ * $cache->set( $key, $row, 86400, $setOpts );
* @endcode
*
* @param string $key Cache key
* @param mixed $value
* @param integer $ttl Seconds to live [0=forever]
* @param array $opts Options map:
+ * - lag : Seconds of slave lag. Typically, this is either the slave lag
+ * before the data was read or, if applicable, the slave 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.
final public function set( $key, $value, $ttl = 0, array $opts = array() ) {
$lockTSE = isset( $opts['lockTSE'] ) ? $opts['lockTSE'] : self::TSE_NONE;
$age = isset( $opts['since'] ) ? max( 0, microtime( true ) - $opts['since'] ) : 0;
+ $lag = isset( $opts['lag'] ) ? $opts['lag'] : 0;
+
+ if ( $lag > self::MAX_REPLICA_LAG ) {
+ // Too much lag detected; lower TTL so it converges faster
+ $ttl = $ttl ? min( $ttl, self::TTL_LAGGED ) : self::TTL_LAGGED;
+ }
if ( $age > self::MAX_SNAPSHOT_LAG ) {
if ( $lockTSE >= 0 ) {
* - b) Thus, dependent keys will be known to be invalid, but not
* for how long (they are treated as "just" purged), which
* effects any lockTSE logic in getWithSetCallback()
+ *
* The advantage is that this does not place high TTL keys on every cache
* server, making it better for code that will cache many different keys
* and either does not use lockTSE or uses a low enough TTL anyway.
* can be set dynamically by altering $ttl in the callback (by reference).
* The $setOpts array can be altered and is given to set() when called;
* it is recommended to set the 'since' field to avoid race conditions.
+ * Setting 'lag' helps avoids keys getting stuck in long-term stale states.
*
* Usually, callbacks ignore the current value, but it can be used
* to maintain "most recent X" values that come from time or sequence
* the 'lockTSE' option in $opts. If cache purges are needed, also:
* - a) Pass $key into $checkKeys
* - b) Use touchCheckKey( $key ) instead of delete( $key )
- * Following this pattern lets the old cache be used until a
- * single thread updates it as needed. Also consider tweaking
- * the 'lowTTL' parameter.
*
* Example usage (typical key):
* @code
* // Key to store the cached value under
* wfMemcKey( 'cat-attributes', $catId ),
* // Function that derives the new key value
- * function( $oldValue, &$ttl, array &$setOpts ) {
- * // Fetch row from the DB
+ * function ( $oldValue, &$ttl, array &$setOpts ) {
* $dbr = wfGetDB( DB_SLAVE );
- * $row = $dbr->selectRow( ... );
- *
- * // Set age of the transaction snapshot the data came from
- * $setOpts = array( 'since' => $dbr->trxTimestamp() );
+ * // Account for any snapshot/slave lag
+ * $setOpts += Database::getCacheSetOptions( $dbr );
*
- * return $row;
+ * return $dbr->selectRow( ... );
* },
* // Time-to-live (seconds)
* 60
* // Key to store the cached value under
* wfMemcKey( 'site-cat-config' ),
* // Function that derives the new key value
- * function( $oldValue, &$ttl, array &$setOpts ) {
- * // Fetch row from the DB
+ * function ( $oldValue, &$ttl, array &$setOpts ) {
* $dbr = wfGetDB( DB_SLAVE );
- * $config = CatConfig::newFromRow( $dbr->selectRow( ... ) );
- *
- * // Set age of the transaction snapshot the data came from
- * $setOpts = array( 'since' => $dbr->trxTimestamp() );
+ * // Account for any snapshot/slave lag
+ * $setOpts += Database::getCacheSetOptions( $dbr );
*
- * return $config;
+ * return CatConfig::newFromRow( $dbr->selectRow( ... ) );
* },
* // Time-to-live (seconds)
* 86400,
* // Key to store the cached value under
* wfMemcKey( 'cat-state', $cat->getId() ),
* // Function that derives the new key value
- * function( $oldValue, &$ttl, array &$setOpts ) {
+ * function ( $oldValue, &$ttl, array &$setOpts ) {
* // Determine new value from the DB
* $dbr = wfGetDB( DB_SLAVE );
- * $state = CatState::newFromResults( $dbr->select( ... ) );
+ * // Account for any snapshot/slave lag
+ * $setOpts += Database::getCacheSetOptions( $dbr );
*
- * // Set age of the transaction snapshot the data came from
- * $setOpts = array( 'since' => $dbr->trxTimestamp() );
- *
- * return $state;
+ * return CatState::newFromResults( $dbr->select( ... ) );
* },
* // Time-to-live (seconds)
* 900,
* // Key to store the cached value under
* wfMemcKey( 'cat-last-actions', 100 ),
* // Function that derives the new key value
- * function( $oldValue, &$ttl, array &$setOpts ) {
+ * function ( $oldValue, &$ttl, array &$setOpts ) {
* $dbr = wfGetDB( DB_SLAVE );
+ * // Account for any snapshot/slave lag
+ * $setOpts += Database::getCacheSetOptions( $dbr );
+ *
* // Start off with the last cached list
* $list = $oldValue ?: array();
* // Fetch the last 100 relevant rows in descending order;
* // only fetch rows newer than $list[0] to reduce scanning
* $rows = iterator_to_array( $dbr->select( ... ) );
* // Merge them and get the new "last 100" rows
- * $list = array_slice( array_merge( $new, $list ), 0, 100 );
- *
- * // Set age of the transaction snapshot the data came from
- * $setOpts = array( 'since' => $dbr->trxTimestamp() );
- *
- * return $list;
+ * return array_slice( array_merge( $new, $list ), 0, 100 );
* },
* // Time-to-live (seconds)
* 10,
* @see WANObjectCache::set()
*
* @param string $key Cache key
- * @param callable $callback Value generation function
* @param integer $ttl Seconds to live for key updates. Special values are:
- * - WANObjectCache::TTL_NONE : cache forever
- * - WANObjectCache::TTL_UNCACHEABLE : do not cache at all
- * @param array $checkKeys List of "check" keys
+ * - WANObjectCache::TTL_NONE : Cache forever
+ * - WANObjectCache::TTL_UNCACHEABLE: Do not cache at all
+ * @param callable $callback Value generation function
* @param array $opts Options map:
- * - lowTTL : consider pre-emptive updates when the current TTL (sec)
- * of the key is less than this. It becomes more likely
- * over time, becoming a certainty once the key is expired.
- * [Default: WANObjectCache::LOW_TTL seconds]
- * - lockTSE : if the key is tombstoned or expired (by $checkKeys) less
- * than this many seconds ago, then try to have a single
- * thread handle cache regeneration at any given time.
- * Other threads will try to use stale values if possible.
- * If, on miss, the time since expiration is low, the assumption
- * is that the key is hot and that a stampede is worth avoiding.
- * Setting this above WANObjectCache::HOLDOFF_TTL makes no difference.
- * The higher this is set, the higher the worst-case staleness can be.
- * Use WANObjectCache::TSE_NONE to disable this logic.
- * [Default: WANObjectCache::TSE_NONE]
+ * - checkKeys: List of "check" keys.
+ * - lowTTL: Consider pre-emptive updates when the current TTL (sec) of the key is less than
+ * this. It becomes more likely over time, becoming a certainty once the key is expired.
+ * Default: WANObjectCache::LOW_TTL seconds.
+ * - lockTSE: If the key is tombstoned or expired (by checkKeys) less than this many seconds
+ * ago, then try to have a single thread handle cache regeneration at any given time.
+ * Other threads will try to use stale values if possible. If, on miss, the time since
+ * expiration is low, the assumption is that the key is hot and that a stampede is worth
+ * avoiding. Setting this above WANObjectCache::HOLDOFF_TTL makes no difference. The
+ * higher this is set, the higher the worst-case staleness can be.
+ * Use WANObjectCache::TSE_NONE to disable this logic. Default: WANObjectCache::TSE_NONE.
* @return mixed Value to use for the key
*/
final public function getWithSetCallback(
- $key, $callback, $ttl, array $checkKeys = array(), array $opts = array()
+ $key, $ttl, $callback, array $opts = array(), $oldOpts = array()
) {
+ // Back-compat with 1.26: Swap $ttl and $callback
+ if ( is_int( $callback ) ) {
+ $temp = $ttl;
+ $ttl = $callback;
+ $callback = $temp;
+ }
+ // Back-compat with 1.26: $checkKeys as separate parameter
+ if ( $oldOpts || ( is_array( $opts ) && isset( $opts[0] ) ) ) {
+ $checkKeys = $opts;
+ $opts = $oldOpts;
+ } else {
+ $checkKeys = isset( $opts['checkKeys'] ) ? $opts['checkKeys'] : array();
+ }
+
$lowTTL = isset( $opts['lowTTL'] ) ? $opts['lowTTL'] : min( self::LOW_TTL, $ttl );
$lockTSE = isset( $opts['lockTSE'] ) ? $opts['lockTSE'] : self::TSE_NONE;