/** Seconds to keep lock keys around */
const LOCK_TTL = 5;
+ /** Idiom for set()/getWithSetCallback() TTL */
+ const TTL_NONE = 0;
+ /** Idiom for getWithSetCallback() callbacks to avoid calling set() */
+ const TTL_UNCACHEABLE = -1;
+
/** Cache format version number */
const VERSION = 1;
/**
* Fetch the value of a timestamp "check" key
*
+ * Note that "check" keys won't collide with other regular keys
+ *
* @param string $key
* @return float|bool TS_UNIX timestamp of the key; false if not present
*/
* avoid race conditions where dependent keys get updated with a
* stale value (e.g. from a DB slave).
*
+ * Note that "check" keys won't collide with other regular keys
+ *
* @see WANObjectCache::get()
*
* @param string $key Cache key
/**
* Method to fetch/regenerate cache keys
*
- * On cache miss, the key will be set to the callback result.
+ * On cache miss, the key will be set to the callback result,
+ * unless the callback returns false. The arguments supplied are:
+ * (current value or false, &$ttl)
* The callback function returns the new value given the current
- * value (false if not present). If false is returned, then nothing
- * will be saved to cache.
+ * value (false if not present). Preemptive re-caching and $checkKeys
+ * can result in a non-false current value. The TTL of the new value
+ * can be set dynamically by altering $ttl in the callback (by reference).
*
- * Most callers should ignore the current value, but it can be used
+ * Usually, callbacks ignore the current value, but it can be used
* to maintain "most recent X" values that come from time or sequence
* based source data, provided that the "as of" id/time is tracked.
*
- * Usage of $checkKeys is the same as with get().
+ * Usage of $checkKeys is similar to get()/getMulti(). However,
+ * rather than the caller having to inspect a "current time left"
+ * variable (e.g. $curTTL, $curTTLs), a cache regeneration will be
+ * triggered using the callback.
*
* The simplest way to avoid stampedes for hot keys is to use
* the 'lockTSE' option in $opts. If cache purges are needed, also:
* the 'lowTTL' parameter.
*
* Example usage:
- * <code>
+ * @code
* $key = wfMemcKey( 'cat-recent-actions', $catId );
* // Function that derives the new key value given the old value
- * $callback = function( $cValue ) { ... };
+ * $callback = function( $cValue, &$ttl ) { ... };
* // Get the key value from cache or from source on cache miss;
* // try to only let one cluster thread manage doing cache updates
* $opts = array( 'lockTSE' => 5, 'lowTTL' => 10 );
* $value = $cache->getWithSetCallback( $key, $callback, 60, array(), $opts );
- * </code>
+ * @endcode
*
* Example usage:
- * <code>
+ * @code
* $key = wfMemcKey( 'cat-state', $catId );
* // The "check" keys that represent things the value depends on;
* // Calling touchCheckKey() on them invalidates "cat-state"
* // try to only let one cluster thread manage doing cache updates
* $opts = array( 'lockTSE' => 5, 'lowTTL' => 10 );
* $value = $cache->getWithSetCallback( $key, $callback, 60, $checkKeys, $opts );
- * </code>
+ * @endcode
*
* @see WANObjectCache::get()
*
* @param string $key Cache key
* @param callable $callback Value generation function
- * @param integer $ttl Seconds to live when the key is updated [0=forever]
+ * @param integer $ttl Seconds to live for key updates. Special values are:
+ * - WANObjectCache::TTL_NONE : cache forever
+ * - WANObjectCache::TTL_UNCACHEABLE : do not cache at all
* @param array $checkKeys List of "check" keys
* @param array $opts Options map:
* - lowTTL : consider pre-emptive updates when the current TTL (sec)
* of the key is less than this. It becomes more likely
* over time, becoming a certainty once the key is expired.
- * - lockTSE : if the key is tombstoned or expired less (by $checkKeys)
+ * - lockTSE : if the key is tombstoned or expired (by $checkKeys) less
* than this many seconds ago, then try to have a single
* thread handle cache regeneration at any given time.
* 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
return $value;
}
- if ( !is_callable( $callback ) ) {
- throw new InvalidArgumentException( "Invalid cache miss callback provided." );
- }
-
+ $isTombstone = ( $curTTL !== null && $value === false );
// Assume a key is hot if requested soon after invalidation
$isHot = ( $curTTL !== null && $curTTL <= 0 && abs( $curTTL ) <= $lockTSE );
- $isTombstone = ( $curTTL !== null && $value === false );
$locked = false;
- if ( $isHot || $isTombstone ) {
+ 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
} elseif ( $value !== false ) {
// If it cannot be acquired; then the stale value can be used
return $value;
- } else {
- // Either another thread has the lock or the lock failed.
- // Use the stash value, which is likely from the prior thread.
- $value = $this->cache->get( self::STASH_KEY_PREFIX . $key );
- // Regenerate on timeout or if the other thread failed
- if ( $value !== false ) {
- return $value;
- }
}
}
+ if ( !$locked && ( $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.
+ $value = $this->cache->get( self::STASH_KEY_PREFIX . $key );
+ if ( $value !== false ) {
+ return $value;
+ }
+ }
+
+ if ( !is_callable( $callback ) ) {
+ throw new InvalidArgumentException( "Invalid cache miss callback provided." );
+ }
+
// Generate the new value from the callback...
- $value = call_user_func( $callback, $cValue );
+ $value = call_user_func_array( $callback, array( $cValue, &$ttl ) );
// When delete() is called, writes are write-holed by the tombstone,
// so use a special stash key to pass the new value around threads.
- if ( $value !== false && ( $isHot || $isTombstone ) ) {
+ if ( $value !== false && ( $isHot || $isTombstone ) && $ttl >= 0 ) {
$this->cache->set( self::STASH_KEY_PREFIX . $key, $value, $tempTTL );
}
$this->cache->unlock( $key );
}
- if ( $value !== false ) {
+ if ( $value !== false && $ttl >= 0 ) {
// Update the cache; this will fail if the key is tombstoned
$this->set( $key, $value, $ttl );
}