* This class is intended for caching data from primary stores.
* If the get() method does not return a value, then the caller
* should query the new value and backfill the cache using set().
+ * The preferred way to do this logic is through getWithSetCallback().
* When querying the store on cache miss, the closest DB replica
* should be used. Try to avoid heavyweight DB master or quorum reads.
* When the source data changes, a purge method should be called.
*
* The simplest purge method is delete().
*
- * Instances of this class must be configured to point to a valid
- * PubSub endpoint, and there must be listeners on the cache servers
- * that subscribe to the endpoint and update the caches.
+ * There are two supported ways to handle broadcasted operations:
+ * - a) Configure the 'purge' EventRelayer to point to a valid PubSub endpoint
+ * that has subscribed listeners on the cache servers applying the cache updates.
+ * - b) Ignore the 'purge' EventRelayer configuration (default is NullEventRelayer)
+ * and set up mcrouter as the underlying cache backend, using one of the memcached
+ * BagOStuff classes as 'cache'. Use OperationSelectorRoute in the mcrouter settings
+ * to configure 'set' and 'delete' operations to go to all DCs via AllAsyncRoute and
+ * configure other operations to go to the local DC via PoolRoute (for reference,
+ * see https://github.com/facebook/mcrouter/wiki/List-of-Route-Handles).
*
- * Broadcasted operations like delete() and touchCheckKey() are done
- * synchronously in the local datacenter, but are relayed asynchronously.
- * This means that callers in other datacenters will see older values
- * for however many milliseconds the datacenters are apart. As with
- * any cache, this should not be relied on for cases where reads are
- * used to determine writes to source (e.g. non-cache) data stores.
+ * Broadcasted operations like delete() and touchCheckKey() are done asynchronously
+ * in all datacenters this way, though the local one should likely be near immediate.
+ *
+ * This means that callers in all datacenters may see older values for however many
+ * milliseconds that the purge took to reach that datacenter. As with any cache, this
+ * should not be relied on for cases where reads are used to determine writes to source
+ * (e.g. non-cache) data stores, except when reading immutable data.
*
* All values are wrapped in metadata arrays. Keys use a "WANCache:" prefix
* to avoid collisions with keys that are not wrapped as metadata arrays. The
* - a) "WANCache:v" : used for regular value keys
* - b) "WANCache:i" : used for temporarily storing values of tombstoned keys
* - c) "WANCache:t" : used for storing timestamp "check" keys
+ * - d) "WANCache:m" : used for temporary mutex keys to avoid cache stampedes
*
* @ingroup Cache
* @since 1.26
const VALUE_KEY_PREFIX = 'WANCache:v:';
const INTERIM_KEY_PREFIX = 'WANCache:i:';
const TIME_KEY_PREFIX = 'WANCache:t:';
+ const MUTEX_KEY_PREFIX = 'WANCache:m:';
const PURGE_VAL_PREFIX = 'PURGED:';
*
* Example usage:
* @code
- * $dbr = wfGetDB( DB_SLAVE );
+ * $dbr = wfGetDB( DB_REPLICA );
* $setOpts = Database::getCacheSetOptions( $dbr );
* // Fetch the row from the DB
* $row = $dbr->selectRow( ... );
* @param integer $ttl Seconds to live. Special values are:
* - WANObjectCache::TTL_INDEFINITE: Cache 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
+ * - 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
*
* When using potentially long-running ACID transactions, a good pattern is
* to use a pre-commit hook to issue the delete. This means that immediately
- * after commit, callers will see the tombstone in cache in the local datacenter
- * and in the others upon relay. It also avoids the following race condition:
+ * after commit, callers will see the tombstone in cache upon purge relay.
+ * It also avoids the following race condition:
* - a) T1 begins, changes a row, and calls delete()
* - b) The HOLDOFF_TTL passes, expiring the delete() tombstone
* - c) T2 starts, reads the row and calls set() due to a cache miss
*
* Example usage:
* @code
- * $dbw->begin( __METHOD__ ); // start of request
+ * $dbw->startAtomic( __METHOD__ ); // start of request
* ... <execute some stuff> ...
* // Update the row in the DB
* $dbw->update( ... );
* $cache->delete( $key );
* } );
* ... <execute some stuff> ...
- * $dbw->commit( __METHOD__ ); // end of request
+ * $dbw->endAtomic( __METHOD__ ); // end of request
* @endcode
*
* The $ttl parameter can be used when purging values that have not actually changed
$key = self::VALUE_KEY_PREFIX . $key;
if ( $ttl <= 0 ) {
- // Update the local datacenter immediately
- $ok = $this->cache->delete( $key );
// Publish the purge to all datacenters
- $ok = $this->relayDelete( $key ) && $ok;
+ $ok = $this->relayDelete( $key );
} else {
- // Update the local datacenter immediately
- $ok = $this->cache->set( $key,
- $this->makePurgeValue( microtime( true ), self::HOLDOFF_NONE ),
- $ttl
- );
// Publish the purge to all datacenters
- $ok = $this->relayPurge( $key, $ttl, self::HOLDOFF_NONE ) && $ok;
+ $ok = $this->relayPurge( $key, $ttl, self::HOLDOFF_NONE );
}
return $ok;
* keys, the relevant "check" keys must be supplied for this to work.
*
* The "check" key essentially represents a last-modified field.
- * When touched, keys using it via get(), getMulti(), or getWithSetCallback()
- * will be invalidated. It is treated as being HOLDOFF_TTL seconds in the future
+ * When touched, the field will be updated on all cache servers.
+ * Keys using it via get(), getMulti(), or getWithSetCallback() will
+ * be invalidated. It is treated as being HOLDOFF_TTL seconds in the future
* by those methods to avoid race conditions where dependent keys get updated
- * with stale values (e.g. from a DB slave).
+ * with stale values (e.g. from a DB replica DB).
*
* This is typically useful for keys with hardcoded names or in some cases
* dynamically generated names where a low number of combinations exist.
* When a few important keys get a large number of hits, a high cache
* time is usually desired as well as "lockTSE" logic. The resetCheckKey()
* method is less appropriate in such cases since the "time since expiry"
- * cannot be inferred.
+ * cannot be inferred, causing any get() after the reset to treat the key
+ * as being "hot", resulting in more stale value usage.
*
* Note that "check" keys won't collide with other regular keys.
*
* @return bool True if the item was purged or not found, false on failure
*/
final public function touchCheckKey( $key, $holdoff = self::HOLDOFF_TTL ) {
- $key = self::TIME_KEY_PREFIX . $key;
- // Update the local datacenter immediately
- $ok = $this->cache->set( $key,
- $this->makePurgeValue( microtime( true ), $holdoff ),
- self::CHECK_KEY_TTL
- );
// Publish the purge to all datacenters
- return $this->relayPurge( $key, self::CHECK_KEY_TTL, $holdoff ) && $ok;
+ return $this->relayPurge( self::TIME_KEY_PREFIX . $key, self::CHECK_KEY_TTL, $holdoff );
}
/**
*
* This is similar to touchCheckKey() in that keys using it via get(), getMulti(),
* or getWithSetCallback() will be invalidated. The differences are:
- * - a) The timestamp will be deleted from all caches and lazily
+ * - a) The "check" key will be deleted from all caches and lazily
* re-initialized when accessed (rather than set everywhere)
* - 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()
+ * - c) Since "check" keys are initialized only on the server the key hashes
+ * to, any temporary ejection of that server will cause the value to be
+ * seen as purged as a new server will initialize the "check" key.
*
* 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
* @return bool True if the item was purged or not found, false on failure
*/
final public function resetCheckKey( $key ) {
- $key = self::TIME_KEY_PREFIX . $key;
- // Update the local datacenter immediately
- $ok = $this->cache->delete( $key );
// Publish the purge to all datacenters
- return $this->relayDelete( $key ) && $ok;
+ return $this->relayDelete( self::TIME_KEY_PREFIX . $key );
}
/**
* $cache::TTL_MINUTE,
* // Function that derives the new key value
* function ( $oldValue, &$ttl, array &$setOpts ) {
- * $dbr = wfGetDB( DB_SLAVE );
- * // Account for any snapshot/slave lag
+ * $dbr = wfGetDB( DB_REPLICA );
+ * // Account for any snapshot/replica DB lag
* $setOpts += Database::getCacheSetOptions( $dbr );
*
* return $dbr->selectRow( ... );
* $cache::TTL_DAY,
* // Function that derives the new key value
* function ( $oldValue, &$ttl, array &$setOpts ) {
- * $dbr = wfGetDB( DB_SLAVE );
- * // Account for any snapshot/slave lag
+ * $dbr = wfGetDB( DB_REPLICA );
+ * // Account for any snapshot/replica DB lag
* $setOpts += Database::getCacheSetOptions( $dbr );
*
* return CatConfig::newFromRow( $dbr->selectRow( ... ) );
* // Calling touchCheckKey() on this key invalidates the cache
* 'checkKeys' => [ $cache->makeKey( 'site-cat-config' ) ],
* // Try to only let one datacenter thread manage cache updates at a time
- * 'lockTSE' => 30
+ * 'lockTSE' => 30,
+ * // Avoid querying cache servers multiple times in a web request
+ * 'pcTTL' => $cache::TTL_PROC_LONG
* ]
* );
* @endcode
* // Function that derives the new key value
* function ( $oldValue, &$ttl, array &$setOpts ) {
* // Determine new value from the DB
- * $dbr = wfGetDB( DB_SLAVE );
- * // Account for any snapshot/slave lag
+ * $dbr = wfGetDB( DB_REPLICA );
+ * // Account for any snapshot/replica DB lag
* $setOpts += Database::getCacheSetOptions( $dbr );
*
* return CatState::newFromResults( $dbr->select( ... ) );
* 10,
* // Function that derives the new key value
* function ( $oldValue, &$ttl, array &$setOpts ) {
- * $dbr = wfGetDB( DB_SLAVE );
- * // Account for any snapshot/slave lag
+ * $dbr = wfGetDB( DB_REPLICA );
+ * // Account for any snapshot/replica DB lag
* $setOpts += Database::getCacheSetOptions( $dbr );
*
* // Start off with the last cached list
* // Merge them and get the new "last 100" rows
* return array_slice( array_merge( $new, $list ), 0, 100 );
* },
- * // Try to only let one datacenter thread manage cache updates at a time
- * [ 'lockTSE' => 30 ]
+ * [
+ * // Try to only let one datacenter thread manage cache updates at a time
+ * 'lockTSE' => 30,
+ * // Use a magic value when no cache value is ready rather than stampeding
+ * 'busyValue' => 'computing'
+ * ]
* );
* @endcode
*
* - pcTTL: Process cache the value in this PHP instance for this many seconds. This avoids
* network I/O when a key is read several times. This will not cache when the callback
* returns false, however. Note that any purges will not be seen while process cached;
- * since the callback should use slave DBs and they may be lagged or have snapshot
+ * since the callback should use replica DBs and they may be lagged or have snapshot
* isolation anyway, this should not typically matter.
* Default: WANObjectCache::TTL_UNCACHEABLE.
* - version: Integer version number. This allows for callers to make breaking changes to
$lockAcquired = false;
if ( $useMutex ) {
// Acquire a datacenter-local non-blocking lock
- if ( $this->cache->lock( $key, 0, self::LOCK_TTL ) ) {
+ if ( $this->cache->add( self::MUTEX_KEY_PREFIX . $key, 1, self::LOCK_TTL ) ) {
// Lock acquired; this thread should update the key
$lockAcquired = true;
} elseif ( $value !== false && $this->isValid( $value, $versioned, $asOf, $minTime ) ) {
if ( ( $isTombstone && $lockTSE > 0 ) && $value !== false && $ttl >= 0 ) {
$tempTTL = max( 1, (int)$lockTSE ); // set() expects seconds
$wrapped = $this->wrap( $value, $tempTTL, $asOf );
- $this->cache->set( self::INTERIM_KEY_PREFIX . $key, $wrapped, $tempTTL );
- }
-
- if ( $lockAcquired ) {
- $this->cache->unlock( $key );
+ // Avoid using set() to avoid pointless mcrouter broadcasting
+ $this->cache->merge(
+ self::INTERIM_KEY_PREFIX . $key,
+ function () use ( $wrapped ) {
+ return $wrapped;
+ },
+ $tempTTL,
+ 1
+ );
}
if ( $value !== false && $ttl >= 0 ) {
$this->set( $key, $value, $ttl, $setOpts );
}
+ if ( $lockAcquired ) {
+ // Avoid using delete() to avoid pointless mcrouter broadcasting
+ $this->cache->changeTTL( self::MUTEX_KEY_PREFIX . $key, 1 );
+ }
+
return $value;
}
return $this->cache->getQoS( $flag );
}
+ /**
+ * Get a TTL that is higher for objects that have not changed recently
+ *
+ * This is useful for keys that get explicit purges and DB or purge relay
+ * lag is a potential concern (especially how it interacts with CDN cache)
+ *
+ * Example usage:
+ * @code
+ * // Last-modified time of page
+ * $mtime = wfTimestamp( TS_UNIX, $page->getTimestamp() );
+ * // Get adjusted TTL. If $mtime is 3600 seconds ago and $minTTL/$factor left at
+ * // defaults, then $ttl is 3600 * .2 = 720. If $minTTL was greater than 720, then
+ * // $ttl would be $minTTL. If $maxTTL was smaller than 720, $ttl would be $maxTTL.
+ * $ttl = $cache->adaptiveTTL( $mtime, $cache::TTL_DAY );
+ * @endcode
+ *
+ * @param integer|float $mtime UNIX timestamp
+ * @param integer $maxTTL Maximum TTL (seconds)
+ * @param integer $minTTL Minimum TTL (seconds); Default: 30
+ * @param float $factor Value in the range (0,1); Default: .2
+ * @return integer Adaptive TTL
+ * @since 1.28
+ */
+ public function adaptiveTTL( $mtime, $maxTTL, $minTTL = 30, $factor = .2 ) {
+ if ( is_float( $mtime ) ) {
+ $mtime = (int)$mtime; // ignore fractional seconds
+ }
+
+ if ( !is_int( $mtime ) || $mtime <= 0 ) {
+ return $minTTL; // no last-modified time provided
+ }
+
+ $age = time() - $mtime;
+
+ return (int)min( $maxTTL, max( $minTTL, $factor * $age ) );
+ }
+
/**
* Do the actual async bus purge of a key
*
* @return bool Success
*/
protected function relayPurge( $key, $ttl, $holdoff ) {
- $event = $this->cache->modifySimpleRelayEvent( [
- 'cmd' => 'set',
- 'key' => $key,
- 'val' => 'PURGED:$UNIXTIME$:' . (int)$holdoff,
- 'ttl' => max( $ttl, 1 ),
- 'sbt' => true, // substitute $UNIXTIME$ with actual microtime
- ] );
-
- $ok = $this->purgeRelayer->notify( $this->purgeChannel, $event );
- if ( !$ok ) {
- $this->lastRelayError = self::ERR_RELAY;
+ if ( $this->purgeRelayer instanceof EventRelayerNull ) {
+ // This handles the mcrouter and the single-DC case
+ $ok = $this->cache->set( $key,
+ $this->makePurgeValue( microtime( true ), self::HOLDOFF_NONE ),
+ $ttl
+ );
+ } else {
+ $event = $this->cache->modifySimpleRelayEvent( [
+ 'cmd' => 'set',
+ 'key' => $key,
+ 'val' => 'PURGED:$UNIXTIME$:' . (int)$holdoff,
+ 'ttl' => max( $ttl, 1 ),
+ 'sbt' => true, // substitute $UNIXTIME$ with actual microtime
+ ] );
+
+ $ok = $this->purgeRelayer->notify( $this->purgeChannel, $event );
+ if ( !$ok ) {
+ $this->lastRelayError = self::ERR_RELAY;
+ }
}
return $ok;
* @return bool Success
*/
protected function relayDelete( $key ) {
- $event = $this->cache->modifySimpleRelayEvent( [
- 'cmd' => 'delete',
- 'key' => $key,
- ] );
-
- $ok = $this->purgeRelayer->notify( $this->purgeChannel, $event );
- if ( !$ok ) {
- $this->lastRelayError = self::ERR_RELAY;
+ if ( $this->purgeRelayer instanceof EventRelayerNull ) {
+ // This handles the mcrouter and the single-DC case
+ $ok = $this->cache->delete( $key );
+ } else {
+ $event = $this->cache->modifySimpleRelayEvent( [
+ 'cmd' => 'delete',
+ 'key' => $key,
+ ] );
+
+ $ok = $this->purgeRelayer->notify( $this->purgeChannel, $event );
+ if ( !$ok ) {
+ $this->lastRelayError = self::ERR_RELAY;
+ }
}
return $ok;