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;
+ /** Default TTL for temporarily caching tombstoned keys */
+ const TEMP_TTL = 5;
/** Idiom for set()/getWithSetCallback() TTL */
const TTL_NONE = 0;
/**
* Purge a key from all clusters
*
- * This instantiates a hold-off period where the key cannot be
- * written to avoid race conditions where dependent keys get updated
- * with a stale value (e.g. from a DB slave).
- *
* This should only be called when the underlying data (being cached)
- * changes in a significant way. If called twice on the same key, then
- * the last TTL takes precedence.
+ * changes in a significant way. This deletes the key and starts a hold-off
+ * period where the key cannot be written to for a few seconds (HOLDOFF_TTL).
+ * This is done to avoid the following race condition:
+ * a) Some DB data changes and delete() is called on a corresponding key
+ * b) A request refills the key with a stale value from a lagged DB
+ * c) The stale value is stuck there until the key is expired/evicted
+ *
+ * This is implemented by storing a special "tombstone" value at the cache
+ * key that this class recognizes; get() calls will return false for the key
+ * and any set() calls will refuse to replace tombstone values at the key.
+ * For this to always avoid writing stale values, the following must hold:
+ * a) Replication lag is bounded to being less than HOLDOFF_TTL; or
+ * b) If lag is higher, the DB will have gone into read-only mode already
+ *
+ * If called twice on the same key, then the last hold-off TTL takes
+ * precedence. For idempotence, the $ttl should not vary for different
+ * delete() calls on the same key. Also note that lowering $ttl reduces
+ * the effective range of the 'lockTSE' parameter to getWithSetCallback().
*
* @param string $key Cache key
* @param integer $ttl How long to block writes to the key [seconds]
*/
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 cluster immediately
$ok = $this->cache->set( $key, self::PURGE_VAL_PREFIX . microtime( true ), $ttl );
// Publish the purge to all clusters
* 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.
- * - tempTTL : when 'lockTSE' is set, this determines the TTL of the temp
- * key used to cache values while a key is tombstoned.
- * This avoids excessive regeneration of hot keys on delete() but
- * may result in stale values.
+ * Setting this above WANObjectCache::HOLDOFF_TTL makes no difference.
+ * - tempTTL : TTL of the temp key used to cache values while a key is tombstoned.
+ * This avoids excessive regeneration of hot keys on delete() but may
+ * result in stale values.
* @return mixed Value to use for the key
*/
final public function getWithSetCallback(
$key, $callback, $ttl, array $checkKeys = array(), array $opts = array()
) {
- $lowTTL = isset( $opts['lowTTL'] ) ? $opts['lowTTL'] : min( 10, $ttl );
+ $lowTTL = isset( $opts['lowTTL'] ) ? $opts['lowTTL'] : min( self::LOW_TTL, $ttl );
$lockTSE = isset( $opts['lockTSE'] ) ? $opts['lockTSE'] : -1;
- $tempTTL = isset( $opts['tempTTL'] ) ? $opts['tempTTL'] : 5;
+ $tempTTL = isset( $opts['tempTTL'] ) ? $opts['tempTTL'] : self::TEMP_TTL;
// Get the current key value
$curTTL = null;
return $value;
}
+ // A deleted key with a negative TTL left must be tombstoned
$isTombstone = ( $curTTL !== null && $value === false );
// Assume a key is hot if requested soon after invalidation
$isHot = ( $curTTL !== null && $curTTL <= 0 && abs( $curTTL ) <= $lockTSE );
- $locked = false;
+ $lockAcquired = false;
if ( $isHot ) {
// Acquire a cluster-local non-blocking lock
if ( $this->cache->lock( $key, 0, self::LOCK_TTL ) ) {
// Lock acquired; this thread should update the key
- $locked = true;
+ $lockAcquired = true;
} elseif ( $value !== false ) {
// If it cannot be acquired; then the stale value can be used
return $value;
}
}
- if ( !$locked && ( $isTombstone || $isHot ) ) {
+ if ( !$lockAcquired && ( $isTombstone || $isHot ) ) {
// Use the stash value for tombstoned keys to reduce regeneration load.
// For hot keys, either another thread has the lock or the lock failed;
// use the stash value from the last thread that regenerated it.
$this->cache->set( self::STASH_KEY_PREFIX . $key, $value, $tempTTL );
}
- if ( $locked ) {
+ if ( $lockAcquired ) {
$this->cache->unlock( $key );
}
* moves from $lowTTL to 0 seconds. This handles widely varying
* levels of cache access traffic.
*
- * @param float|INF $curTTL Approximate TTL left on the key if present
+ * @param float $curTTL Approximate TTL left on the key if present
* @param float $lowTTL Consider a refresh when $curTTL is less than this
* @return bool
*/