* There are three 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).
- * - c) Ignore the 'purge' EventRelayer configuration (default is NullEventRelayer)
- * and set up dynomite as cache middleware between the web servers and either
- * memcached or redis. This will also broadcast all key setting operations, not just purges,
- * which can be useful for cache warming. Writes are eventually consistent via the
- * Dynamo replication model (see https://github.com/Netflix/dynomite).
+ * - b) Ommit the 'purge' EventRelayer parameter and set up mcrouter as the underlying cache
+ * backend, using a memcached BagOStuff class for the 'cache' parameter. The 'region'
+ * and 'cluster' parameters must be provided and 'mcrouterAware' must be set to 'true'.
+ * Configure mcrouter as follows:
+ * - 1) Use Route Prefixing based on region (datacenter) and cache cluster.
+ * See https://github.com/facebook/mcrouter/wiki/Routing-Prefix and
+ * https://github.com/facebook/mcrouter/wiki/Multi-cluster-broadcast-setup
+ * - 2) To increase the consistency of delete() and touchCheckKey() during cache
+ * server membership changes, you can use the OperationSelectorRoute to
+ * configure 'set' and 'delete' operations to go to all servers in the cache
+ * cluster, instead of just one server determined by hashing.
+ * See https://github.com/facebook/mcrouter/wiki/List-of-Route-Handles
+ * - c) Ommit the 'purge' EventRelayer parameter and set up dynomite as cache middleware
+ * between the web servers and either memcached or redis. This will also broadcast all
+ * key setting operations, not just purges, which can be useful for cache warming.
+ * Writes are eventually consistent via the Dynamo replication model.
+ * See https://github.com/Netflix/dynomite
*
* Broadcasted operations like delete() and touchCheckKey() are done asynchronously
* in all datacenters this way, though the local one should likely be near immediate.
protected $purgeChannel;
/** @var EventRelayer Bus that handles purge broadcasts */
protected $purgeRelayer;
+ /** @bar bool Whether to use mcrouter key prefixing for routing */
+ protected $mcrouterAware;
+ /** @var string Physical region for mcrouter use */
+ protected $region;
+ /** @var string Cache cluster name for mcrouter use */
+ protected $cluster;
/** @var LoggerInterface */
protected $logger;
/** @var StatsdDataFactoryInterface */
* callback supplied by the getWithSetCallback() caller. The result will be saved
* as normal. The handler is expected to call the WAN cache callback at an opportune
* time (e.g. HTTP post-send), though generally within a few 100ms. [optional]
+ * - region: the current physical region. This is required when using mcrouter as the
+ * backing store proxy. [optional]
+ * - cluster: name of the cache cluster used by this WAN cache. The name must be the
+ * same in all datacenters; the ("region","cluster") tuple is what distinguishes
+ * the counterpart cache clusters among all the datacenter. The contents of
+ * https://github.com/facebook/mcrouter/wiki/Config-Files give background on this.
+ * This is required when using mcrouter as the backing store proxy. [optional]
+ * - mcrouterAware: set as true if mcrouter is the backing store proxy and mcrouter
+ * is configured to interpret /<region>/<cluster>/ key prefixes as routes. This
+ * requires that "region" and "cluster" are both set above. [optional]
*/
public function __construct( array $params ) {
$this->cache = $params['cache'];
$this->purgeRelayer = isset( $params['relayers']['purge'] )
? $params['relayers']['purge']
: new EventRelayerNull( [] );
+ $this->region = isset( $params['region'] ) ? $params['region'] : 'main';
+ $this->cluster = isset( $params['cluster'] ) ? $params['cluster'] : 'wan-main';
+ $this->mcrouterAware = !empty( $params['mcrouterAware'] );
+
$this->setLogger( isset( $params['logger'] ) ? $params['logger'] : new NullLogger() );
$this->stats = isset( $params['stats'] ) ? $params['stats'] : new NullStatsdDataFactory();
$this->asyncHandler = isset( $params['asyncHandler'] ) ? $params['asyncHandler'] : null;
}
+ /**
+ * @param LoggerInterface $logger
+ */
public function setLogger( LoggerInterface $logger ) {
$this->logger = $logger;
}
*
* Setting 'lag' and 'since' help avoids keys getting stuck in stale states.
*
+ * Be aware that this does not update the process cache for getWithSetCallback()
+ * callers. Keys accessed via that method are not generally meant to also be set
+ * using this primitive method.
+ *
+ * Do not use this method on versioned keys accessed via getWithSetCallback().
+ *
* Example usage:
* @code
* $dbr = wfGetDB( DB_REPLICA );
*
* Note that set() can also be lag-aware and lower the TTL if it's high.
*
+ * Be aware that this does not clear the process cache. Even if it did, callbacks
+ * used by getWithSetCallback() might still return stale data in the case of either
+ * uncommitted or not-yet-replicated changes (callback generally use replica DBs).
+ *
* 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 upon purge relay.
* );
* @endcode
*
+ * Example usage (key holding an LRU subkey:value map; this can avoid flooding cache with
+ * keys for an unlimited set of (constraint,situation) pairs, thereby avoiding elevated
+ * cache evictions and wasted memory):
+ * @code
+ * $catSituationTolerabilityCache = $this->cache->getWithSetCallback(
+ * // Group by constraint ID/hash, cat family ID/hash, or something else useful
+ * $this->cache->makeKey( 'cat-situation-tolerablity-checks', $groupKey ),
+ * WANObjectCache::TTL_DAY, // rarely used groups should fade away
+ * // The $scenarioKey format is $constraintId:<ID/hash of $situation>
+ * function ( $cacheMap ) use ( $scenarioKey, $constraintId, $situation ) {
+ * $lruCache = MapCacheLRU::newFromArray( $cacheMap ?: [], self::CACHE_SIZE );
+ * $result = $lruCache->get( $scenarioKey ); // triggers LRU bump if present
+ * if ( $result === null || $this->isScenarioResultExpired( $result ) ) {
+ * $result = $this->checkScenarioTolerability( $constraintId, $situation );
+ * $lruCache->set( $scenarioKey, $result, 3 / 8 );
+ * }
+ * // Save the new LRU cache map and reset the map's TTL
+ * return $lruCache->toArray();
+ * },
+ * [
+ * // Once map is > 1 sec old, consider refreshing
+ * 'ageNew' => 1,
+ * // Update within 5 seconds after "ageNew" given a 1hz cache check rate
+ * 'hotTTR' => 5,
+ * // Avoid querying cache servers multiple times in a request; this also means
+ * // that a request can only alter the value of any given constraint key once
+ * 'pcTTL' => WANObjectCache::TTL_PROC_LONG
+ * ]
+ * );
+ * $tolerability = isset( $catSituationTolerabilityCache[$scenarioKey] )
+ * ? $catSituationTolerabilityCache[$scenarioKey]
+ * : $this->checkScenarioTolerability( $constraintId, $situation );
+ * @endcode
+ *
* @see WANObjectCache::get()
* @see WANObjectCache::set()
*
* @param string $key
* @param bool $versioned
* @param float $minTime
- * @param mixed $asOf
+ * @param mixed &$asOf
* @return mixed
*/
protected function getInterimValue( $key, $versioned, $minTime, &$asOf ) {
* This works the same as getWithSetCallback() except:
* - a) The $keys argument expects the result of WANObjectCache::makeMultiKeys()
* - b) The $callback argument expects a callback returning a map of (ID => new value)
- * for all entity IDs in $regenById and it takes the following arguments:
- * - $ids: a list of entity IDs to regenerate
+ * for all entity IDs in $ids and it takes the following arguments:
+ * - $ids: a list of entity IDs that require cache regeneration
* - &$ttls: a reference to the (entity ID => new TTL) map
* - &$setOpts: a reference to options for set() which can be altered
* - c) The return value is a map of (cache key => value) in the order of $keyedIds
* $ttl = ( $newList === $oldValue )
* // No change: cache for 150% of the age of $oldValue
* ? $cache->adaptiveTTL( $oldAsOf, $maxTTL, $minTTL, 1.5 )
- * // Changed: cache for %50 of the age of $oldValue
+ * // Changed: cache for 50% of the age of $oldValue
* : $cache->adaptiveTTL( $oldAsOf, $maxTTL, $minTTL, .5 );
* }
*
* @return bool Success
*/
protected function relayPurge( $key, $ttl, $holdoff ) {
- if ( $this->purgeRelayer instanceof EventRelayerNull ) {
+ if ( $this->mcrouterAware ) {
+ // See https://github.com/facebook/mcrouter/wiki/Multi-cluster-broadcast-setup
+ // Wildcards select all matching routes, e.g. the WAN cluster on all DCs
+ $ok = $this->cache->set(
+ "/*/{$this->cluster}/{$key}",
+ $this->makePurgeValue( $this->getCurrentTime(), self::HOLDOFF_NONE ),
+ $ttl
+ );
+ } elseif ( $this->purgeRelayer instanceof EventRelayerNull ) {
// This handles the mcrouter and the single-DC case
- $ok = $this->cache->set( $key,
+ $ok = $this->cache->set(
+ $key,
$this->makePurgeValue( $this->getCurrentTime(), self::HOLDOFF_NONE ),
$ttl
);
* @return bool Success
*/
protected function relayDelete( $key ) {
- if ( $this->purgeRelayer instanceof EventRelayerNull ) {
- // This handles the mcrouter and the single-DC case
+ if ( $this->mcrouterAware ) {
+ // See https://github.com/facebook/mcrouter/wiki/Multi-cluster-broadcast-setup
+ // Wildcards select all matching routes, e.g. the WAN cluster on all DCs
+ $ok = $this->cache->delete( "/*/{$this->cluster}/{$key}" );
+ } elseif ( $this->purgeRelayer instanceof EventRelayerNull ) {
+ // Some other proxy handles broadcasting or there is only one datacenter
$ok = $this->cache->delete( $key );
} else {
$event = $this->cache->modifySimpleRelayEvent( [