*
* The simplest purge method is delete().
*
- * There are two supported ways to handle broadcasted operations:
+ * 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.
+ * 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).
+ * 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).
*
* Broadcasted operations like delete() and touchCheckKey() are done asynchronously
* in all datacenters this way, though the local one should likely be near immediate.
private $callbackDepth = 0;
/** @var mixed[] Temporary warm-up cache */
private $warmupCache = [];
+ /** @var integer Key fetched */
+ private $warmupKeyMisses = 0;
/** Max time expected to pass between delete() and DB commit finishing */
const MAX_COMMIT_DELAY = 3;
if ( $this->warmupCache ) {
$wrappedValues = array_intersect_key( $this->warmupCache, array_flip( $keysGet ) );
$keysGet = array_diff( $keysGet, array_keys( $wrappedValues ) ); // keys left to fetch
+ $this->warmupKeyMisses += count( $keysGet );
} else {
$wrappedValues = [];
}
- $wrappedValues += $this->cache->getMulti( $keysGet );
+ if ( $keysGet ) {
+ $wrappedValues += $this->cache->getMulti( $keysGet );
+ }
// Time used to compare/init "check" keys (derived after getMulti() to be pessimistic)
$now = microtime( true );
* // Time-to-live (in seconds)
* $cache::TTL_DAY,
* // Function that derives the new key value
- * return function ( $id, $oldValue, &$ttl, array &$setOpts ) {
+ * function ( $id, $oldValue, &$ttl, array &$setOpts ) {
* $dbr = wfGetDB( DB_REPLICA );
* // Account for any snapshot/replica DB lag
* $setOpts += Database::getCacheSetOptions( $dbr );
final public function getMultiWithSetCallback(
ArrayIterator $keyedIds, $ttl, callable $callback, array $opts = []
) {
- $keysWarmUp = iterator_to_array( $keyedIds, true );
$checkKeys = isset( $opts['checkKeys'] ) ? $opts['checkKeys'] : [];
+
+ $keysWarmUp = [];
+ // Get all the value keys to fetch...
+ foreach ( $keyedIds as $key => $id ) {
+ $keysWarmUp[] = self::VALUE_KEY_PREFIX . $key;
+ }
+ // Get all the check keys to fetch...
foreach ( $checkKeys as $i => $checkKeyOrKeys ) {
if ( is_int( $i ) ) {
- $keysWarmUp[] = $checkKeyOrKeys;
+ // Single check key that applies to all value keys
+ $keysWarmUp[] = self::TIME_KEY_PREFIX . $checkKeyOrKeys;
} else {
- $keysWarmUp = array_merge( $keysWarmUp, $checkKeyOrKeys );
+ // List of check keys that apply to value key $i
+ $keysWarmUp = array_merge(
+ $keysWarmUp,
+ self::prefixCacheKeys( $checkKeyOrKeys, self::TIME_KEY_PREFIX )
+ );
}
}
$this->warmupCache = $this->cache->getMulti( $keysWarmUp );
$this->warmupCache += array_fill_keys( $keysWarmUp, false );
+ $this->warmupKeyMisses = 0;
// Wrap $callback to match the getWithSetCallback() format while passing $id to $callback
$id = null;
- $func = function ( $oldValue, &$ttl, array $setOpts, $oldAsOf ) use ( $callback, &$id ) {
+ $func = function ( $oldValue, &$ttl, array &$setOpts, $oldAsOf ) use ( $callback, &$id ) {
return $callback( $id, $oldValue, $ttl, $setOpts, $oldAsOf );
};
return $values;
}
+ /**
+ * Locally set a key to expire soon if it is stale based on $purgeTimestamp
+ *
+ * This sets stale keys' time-to-live at HOLDOFF_TTL seconds, which both avoids
+ * broadcasting in mcrouter setups and also avoids races with new tombstones.
+ *
+ * @param string $key Cache key
+ * @param int $purgeTimestamp UNIX timestamp of purge
+ * @param bool &$isStale Whether the key is stale
+ * @return bool Success
+ * @since 1.28
+ */
+ public function reap( $key, $purgeTimestamp, &$isStale = false ) {
+ $minAsOf = $purgeTimestamp + self::HOLDOFF_TTL;
+ $wrapped = $this->cache->get( self::VALUE_KEY_PREFIX . $key );
+ if ( is_array( $wrapped ) && $wrapped[self::FLD_TIME] < $minAsOf ) {
+ $isStale = true;
+ $this->logger->warning( "Reaping stale value key '$key'." );
+ $ttlReap = self::HOLDOFF_TTL; // avoids races with tombstone creation
+ $ok = $this->cache->changeTTL( self::VALUE_KEY_PREFIX . $key, $ttlReap );
+ if ( !$ok ) {
+ $this->logger->error( "Could not complete reap of key '$key'." );
+ }
+
+ return $ok;
+ }
+
+ $isStale = false;
+
+ return true;
+ }
+
+ /**
+ * Locally set a "check" key to expire soon if it is stale based on $purgeTimestamp
+ *
+ * @param string $key Cache key
+ * @param int $purgeTimestamp UNIX timestamp of purge
+ * @param bool &$isStale Whether the key is stale
+ * @return bool Success
+ * @since 1.28
+ */
+ public function reapCheckKey( $key, $purgeTimestamp, &$isStale = false ) {
+ $purge = $this->parsePurgeValue( $this->cache->get( self::TIME_KEY_PREFIX . $key ) );
+ if ( $purge && $purge[self::FLD_TIME] < $purgeTimestamp ) {
+ $isStale = true;
+ $this->logger->warning( "Reaping stale check key '$key'." );
+ $ok = $this->cache->changeTTL( self::TIME_KEY_PREFIX . $key, 1 );
+ if ( !$ok ) {
+ $this->logger->error( "Could not complete reap of check key '$key'." );
+ }
+
+ return $ok;
+ }
+
+ $isStale = false;
+
+ return false;
+ }
+
/**
* @see BagOStuff::makeKey()
* @param string ... Key component
return (int)min( $maxTTL, max( $minTTL, $factor * $age ) );
}
+ /**
+ * @return integer Number of warmup key cache misses last round
+ * @since 1.30
+ */
+ public function getWarmupKeyMisses() {
+ return $this->warmupKeyMisses;
+ }
+
/**
* Do the actual async bus purge of a key
*