const TSE_NONE = -1;
/** Max TTL to store keys when a data sourced is lagged */
const TTL_LAGGED = 30;
+ /** Idiom for delete() for "no hold-off" */
+ const HOLDOFF_NONE = 0;
/** Tiny negative float to use when CTL comes up >= 0 due to clock skew */
const TINY_NEGATIVE = -0.000001;
const FLD_TTL = 2;
const FLD_TIME = 3;
const FLD_FLAGS = 4;
+ const FLD_HOLDOFF = 5;
/** @var integer Treat this value as expired-on-arrival */
const FLG_STALE = 1;
*
* @param array $keys List of cache keys
* @param array $curTTLs Map of (key => approximate TTL left) for existing keys [returned]
- * @param array $checkKeys List of "check" keys to apply to all of $keys
+ * @param array $checkKeys List of check keys to apply to all $keys. May also apply "check"
+ * keys to specific cache keys only by using cache keys as keys in the $checkKeys array.
* @return array Map of (key => value) for keys that exist
*/
final public function getMulti(
$vPrefixLen = strlen( self::VALUE_KEY_PREFIX );
$valueKeys = self::prefixCacheKeys( $keys, self::VALUE_KEY_PREFIX );
- $checkKeys = self::prefixCacheKeys( $checkKeys, self::TIME_KEY_PREFIX );
+
+ $checkKeysForAll = array();
+ $checkKeysByKey = array();
+ $checkKeysFlat = array();
+ foreach ( $checkKeys as $i => $keys ) {
+ $prefixed = self::prefixCacheKeys( (array)$keys, self::TIME_KEY_PREFIX );
+ $checkKeysFlat = array_merge( $checkKeysFlat, $prefixed );
+ // Is this check keys for a specific cache key, or for all keys being fetched?
+ if ( is_int( $i ) ) {
+ $checkKeysForAll = array_merge( $checkKeysForAll, $prefixed );
+ } else {
+ $checkKeysByKey[$i] = isset( $checkKeysByKey[$i] )
+ ? array_merge( $checkKeysByKey[$i], $prefixed )
+ : $prefixed;
+ }
+ }
// Fetch all of the raw values
- $wrappedValues = $this->cache->getMulti( array_merge( $valueKeys, $checkKeys ) );
+ $wrappedValues = $this->cache->getMulti( array_merge( $valueKeys, $checkKeysFlat ) );
+ // Time used to compare/init "check" keys (derived after getMulti() to be pessimistic)
$now = microtime( true );
- // Get/initialize the timestamp of all the "check" keys
- $checkKeyTimes = array();
- foreach ( $checkKeys as $checkKey ) {
- $timestamp = isset( $wrappedValues[$checkKey] )
- ? self::parsePurgeValue( $wrappedValues[$checkKey] )
- : false;
- if ( !is_float( $timestamp ) ) {
- // Key is not set or invalid; regenerate
- $this->cache->add( $checkKey,
- self::PURGE_VAL_PREFIX . $now, self::CHECK_KEY_TTL );
- $timestamp = $now;
- }
-
- $checkKeyTimes[] = $timestamp;
+ // Collect timestamps from all "check" keys
+ $purgeValuesForAll = $this->processCheckKeys( $checkKeysForAll, $wrappedValues, $now );
+ $purgeValuesByKey = array();
+ foreach ( $checkKeysByKey as $cacheKey => $checks ) {
+ $purgeValuesByKey[$cacheKey] =
+ $this->processCheckKeys( $checks, $wrappedValues, $now );
}
// Get the main cache value for each key and validate them
list( $value, $curTTL ) = $this->unwrap( $wrappedValues[$vKey], $now );
if ( $value !== false ) {
$result[$key] = $value;
- foreach ( $checkKeyTimes as $checkKeyTime ) {
- // Force dependant keys to be invalid for a while after purging
- // to reduce race conditions involving stale data getting cached
- $safeTimestamp = $checkKeyTime + self::HOLDOFF_TTL;
+
+ // Force dependant keys to be invalid for a while after purging
+ // to reduce race conditions involving stale data getting cached
+ $purgeValues = $purgeValuesForAll;
+ if ( isset( $purgeValuesByKey[$key] ) ) {
+ $purgeValues = array_merge( $purgeValues, $purgeValuesByKey[$key] );
+ }
+ foreach ( $purgeValues as $purge ) {
+ $safeTimestamp = $purge[self::FLD_TIME] + $purge[self::FLD_HOLDOFF];
if ( $safeTimestamp >= $wrappedValues[$vKey][self::FLD_TIME] ) {
- $curTTL = min( $curTTL, $checkKeyTime - $now );
+ // How long ago this value was expired by *this* check key
+ $ago = min( $purge[self::FLD_TIME] - $now, self::TINY_NEGATIVE );
+ // How long ago this value was expired by *any* known check key
+ $curTTL = min( $curTTL, $ago );
}
}
}
-
$curTTLs[$key] = $curTTL;
}
return $result;
}
+ /**
+ * @since 1.27
+ * @param array $timeKeys List of prefixed time check keys
+ * @param array $wrappedValues
+ * @param float $now
+ * @return array List of purge value arrays
+ */
+ private function processCheckKeys( array $timeKeys, array $wrappedValues, $now ) {
+ $purgeValues = array();
+ foreach ( $timeKeys as $timeKey ) {
+ $purge = isset( $wrappedValues[$timeKey] )
+ ? self::parsePurgeValue( $wrappedValues[$timeKey] )
+ : false;
+ if ( $purge === false ) {
+ // Key is not set or invalid; regenerate
+ $this->cache->add( $timeKey,
+ $this->makePurgeValue( $now, self::HOLDOFF_TTL ),
+ self::CHECK_KEY_TTL
+ );
+ $purge = array( self::FLD_TIME => $now, self::FLD_HOLDOFF => self::HOLDOFF_TTL );
+ }
+ $purgeValues[] = $purge;
+ }
+ return $purgeValues;
+ }
+
/**
* Set the value of a key in cache
*
$wrapExtra = array(); // additional wrapped value fields
// Check if there's a risk of writing stale data after the purge tombstone expired
- if ( ( $lag + $age ) > self::MAX_READ_LAG ) {
+ if ( $lag === false || ( $lag + $age ) > self::MAX_READ_LAG ) {
// Case A: read lag with "lockTSE"; save but record value as stale
if ( $lockTSE >= 0 ) {
$ttl = max( 1, (int)$lockTSE ); // set() expects seconds
return true; // no-op the write for being unsafe
// Case C: high replication lag; lower TTL instead of ignoring all set()s
- } elseif ( $lag > self::MAX_READ_LAG ) {
+ } elseif ( $lag === false || $lag > self::MAX_READ_LAG ) {
$ttl = $ttl ? min( $ttl, self::TTL_LAGGED ) : self::TTL_LAGGED;
$this->logger->warning( "Lowered set() TTL for $key due to replication lag." );
// Case D: medium length request with medium replication lag; ignore this set()
*
* The $ttl parameter can be used when purging values that have not actually changed
* recently. For example, a cleanup script to purge cache entries does not really need
- * a hold-off period, so it can use the value 1. Likewise for user-requested purge.
+ * a hold-off period, so it can use HOLDOFF_NONE. Likewise for user-requested purge.
* Note that $ttl limits the effective range of 'lockTSE' for getWithSetCallback().
*
* If called twice on the same key, then the last hold-off TTL takes precedence. For
*/
final public function delete( $key, $ttl = self::HOLDOFF_TTL ) {
$key = self::VALUE_KEY_PREFIX . $key;
- // Avoid indefinite key salting for sanity
- $ttl = max( $ttl, 1 );
- // Update the local datacenter immediately
- $ok = $this->cache->set( $key, self::PURGE_VAL_PREFIX . microtime( true ), $ttl );
- // Publish the purge to all datacenters
- return $this->relayPurge( $key, $ttl ) && $ok;
+
+ if ( $ttl <= 0 ) {
+ // Update the local datacenter immediately
+ $ok = $this->cache->delete( $key );
+ // Publish the purge to all datacenters
+ $ok = $this->relayDelete( $key ) && $ok;
+ } 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;
+ }
+
+ return $ok;
}
/**
* Note that "check" keys won't collide with other regular keys.
*
* @param string $key
- * @return float UNIX timestamp of the key
+ * @return float UNIX timestamp of the check key
*/
final public function getCheckKeyTime( $key ) {
$key = self::TIME_KEY_PREFIX . $key;
- $time = self::parsePurgeValue( $this->cache->get( $key ) );
- if ( $time === false ) {
+ $purge = self::parsePurgeValue( $this->cache->get( $key ) );
+ if ( $purge !== false ) {
+ $time = $purge[self::FLD_TIME];
+ } else {
// Casting assures identical floats for the next getCheckKeyTime() calls
- $time = (string)microtime( true );
- $this->cache->add( $key, self::PURGE_VAL_PREFIX . $time, self::CHECK_KEY_TTL );
- $time = (float)$time;
+ $now = (string)microtime( true );
+ $this->cache->add( $key,
+ $this->makePurgeValue( $now, self::HOLDOFF_TTL ),
+ self::CHECK_KEY_TTL
+ );
+ $time = (float)$now;
}
return $time;
* @see WANObjectCache::resetCheckKey()
*
* @param string $key Cache key
+ * @param int $holdoff HOLDOFF_TTL or HOLDOFF_NONE constant
* @return bool True if the item was purged or not found, false on failure
*/
- final public function touchCheckKey( $key ) {
+ 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,
- self::PURGE_VAL_PREFIX . microtime( true ), self::CHECK_KEY_TTL );
+ $this->makePurgeValue( microtime( true ), $holdoff ),
+ self::CHECK_KEY_TTL
+ );
// Publish the purge to all datacenters
- return $this->relayPurge( $key, self::CHECK_KEY_TTL ) && $ok;
+ return $this->relayPurge( $key, self::CHECK_KEY_TTL, $holdoff ) && $ok;
}
/**
/**
* Do the actual async bus purge of a key
*
- * This must set the key to "PURGED:<UNIX timestamp>"
+ * This must set the key to "PURGED:<UNIX timestamp>:<holdoff>"
*
* @param string $key Cache key
* @param integer $ttl How long to keep the tombstone [seconds]
+ * @param integer $holdoff HOLDOFF_* constant controlling how long to ignore sets for this key
* @return bool Success
*/
- protected function relayPurge( $key, $ttl ) {
+ protected function relayPurge( $key, $ttl, $holdoff ) {
$event = $this->cache->modifySimpleRelayEvent( array(
'cmd' => 'set',
'key' => $key,
- 'val' => 'PURGED:$UNIXTIME$',
+ 'val' => 'PURGED:$UNIXTIME$:' . (int)$holdoff,
'ttl' => max( $ttl, 1 ),
'sbt' => true, // substitute $UNIXTIME$ with actual microtime
) );
*/
protected function unwrap( $wrapped, $now ) {
// Check if the value is a tombstone
- $purgeTimestamp = self::parsePurgeValue( $wrapped );
- if ( is_float( $purgeTimestamp ) ) {
+ $purge = self::parsePurgeValue( $wrapped );
+ if ( $purge !== false ) {
// Purged values should always have a negative current $ttl
- $curTTL = min( $purgeTimestamp - $now, self::TINY_NEGATIVE );
+ $curTTL = min( $purge[self::FLD_TIME] - $now, self::TINY_NEGATIVE );
return array( false, $curTTL );
}
}
/**
- * @param string $value String like "PURGED:<timestamp>"
- * @return float|bool UNIX timestamp or false on failure
+ * @param string $value Wrapped value like "PURGED:<timestamp>:<holdoff>"
+ * @return array|bool Array containing a UNIX timestamp (float) and holdoff period (integer),
+ * or false if value isn't a valid purge value
*/
protected static function parsePurgeValue( $value ) {
- $m = array();
- if ( is_string( $value ) &&
- preg_match( '/^' . self::PURGE_VAL_PREFIX . '([^:]+)$/', $value, $m )
+ if ( !is_string( $value ) ) {
+ return false;
+ }
+ $segments = explode( ':', $value, 3 );
+ if ( !isset( $segments[0] ) || !isset( $segments[1] )
+ || "{$segments[0]}:" !== self::PURGE_VAL_PREFIX
) {
- return (float)$m[1];
- } else {
return false;
}
+ if ( !isset( $segments[2] ) ) {
+ // Back-compat with old purge values without holdoff
+ $segments[2] = self::HOLDOFF_TTL;
+ }
+ return array(
+ self::FLD_TIME => (float)$segments[1],
+ self::FLD_HOLDOFF => (int)$segments[2],
+ );
+ }
+
+ /**
+ * @param float $timestamp
+ * @param int $holdoff In seconds
+ * @return string Wrapped purge value
+ */
+ protected function makePurgeValue( $timestamp, $holdoff ) {
+ return self::PURGE_VAL_PREFIX . (float)$timestamp . ':' . (int)$holdoff;
}
}