* expired for this specified time. This is useful if adaptiveTTL() is used on the old
* value's as-of time when it is verified as still being correct.
* Default: WANObjectCache::STALE_TTL_NONE
+ * - touchedCallback: A callback that takes the current value and returns a timestamp that
+ * indicates the last time a dynamic dependency changed. Null can be returned if there
+ * are no relevant dependency changes to check. This can be used to check against things
+ * like last-modified times of files or DB timestamp fields. This should generally not be
+ * used for small and easily queried values in a DB if the callback itself ends up doing
+ * a similarly expensive DB query to check a timestamp. Usages of this option makes the
+ * most sense for values that are moderately to highly expensive to regenerate and easy
+ * to query for dependency timestamps. The use of "pcTTL" reduces timestamp queries.
+ * Default: null.
* @return mixed Value found or written to the key
* @note Options added in 1.28: version, busyValue, hotTTR, ageNew, pcGroup, minAsOf
* @note Options added in 1.31: staleTTL, graceTTL
+ * @note Options added in 1.33: touchedCallback
* @note Callable type hints are not used to avoid class-autoloading
*/
final public function getWithSetCallback( $key, $ttl, $callback, array $opts = [] ) {
$ageNew = $opts['ageNew'] ?? self::AGE_NEW;
$minTime = $opts['minAsOf'] ?? self::MIN_TIMESTAMP_NONE;
$versioned = isset( $opts['version'] );
+ $touchedCallback = $opts['touchedCallback'] ?? null;
// Get a collection name to describe this class of key
$kClass = $this->determineKeyClass( $key );
$cValue = $this->get( $key, $curTTL, $checkKeys, $asOf ); // current value
$value = $cValue; // return value
+ // Apply additional dynamic expiration logic if supplied
+ $curTTL = $this->applyTouchedCallback( $value, $asOf, $curTTL, $touchedCallback );
+
$preCallbackTime = $this->getCurrentTime();
// Determine if a cached value regeneration is needed or desired
if ( $value !== false
return $value;
}
+ /**
+ * @param mixed $value
+ * @param float $asOf
+ * @param float $curTTL
+ * @param callable|null $callback
+ * @return float
+ */
+ protected function applyTouchedCallback( $value, $asOf, $curTTL, $callback ) {
+ if ( $callback === null ) {
+ return $curTTL;
+ }
+
+ if ( !is_callable( $callback ) ) {
+ throw new InvalidArgumentException( "Invalid expiration callback provided." );
+ }
+
+ if ( $value !== false ) {
+ $touched = $callback( $value );
+ if ( $touched !== null && $touched >= $asOf ) {
+ $curTTL = min( $curTTL, self::TINY_NEGATIVE, $asOf - $touched );
+ }
+ }
+
+ return $curTTL;
+ }
+
/**
* @param string $key
* @param bool $versioned
$priorAsOf = null;
$wasSet = 0;
$func = function ( $old, &$ttl, &$opts, $asOf )
- use ( &$wasSet, &$priorValue, &$priorAsOf, $value )
- {
+ use ( &$wasSet, &$priorValue, &$priorAsOf, $value ) {
++$wasSet;
$priorValue = $old;
$priorAsOf = $asOf;
$this->assertEquals( 'xxx1', $v, "Value still returned after expired (in grace)" );
$this->assertEquals( 1, $wasSet, "Value still returned after expired (in grace)" );
- // Change of refresh increase to unity as staleness approaches graceTTL
+ // Chance of refresh increase to unity as staleness approaches graceTTL
$mockWallClock += $cache::TTL_WEEK; // 8 days of being stale
$v = $cache->getWithSetCallback(
$key,
$this->assertNotEquals( null, $oldAsOfReceived, "Callback got post-grace stale value" );
}
+ /**
+ * @dataProvider getWithSetCallback_provider
+ * @covers WANObjectCache::getWithSetCallback()
+ * @covers WANObjectCache::doGetWithSetCallback()
+ * @param array $extOpts
+ * @param bool $versioned
+ */
+ function testGetWithSetcallback_touched( array $extOpts, $versioned ) {
+ $cache = $this->cache;
+
+ $mockWallClock = microtime( true );
+ $cache->setMockTime( $mockWallClock );
+
+ $checkFunc = function ( $oldVal, &$ttl, array $setOpts, $oldAsOf )
+ use ( &$wasSet ) {
+ ++$wasSet;
+
+ return 'xxx' . $wasSet;
+ };
+
+ $key = wfRandomString();
+ $wasSet = 0;
+ $touched = null;
+ $touchedCallback = function () use ( &$touched ) {
+ return $touched;
+ };
+ $v = $cache->getWithSetCallback(
+ $key,
+ $cache::TTL_INDEFINITE,
+ $checkFunc,
+ [ 'touchedCallback' => $touchedCallback ] + $extOpts
+ );
+ $mockWallClock += 60;
+ $v = $cache->getWithSetCallback(
+ $key,
+ $cache::TTL_INDEFINITE,
+ $checkFunc,
+ [ 'touchedCallback' => $touchedCallback ] + $extOpts
+ );
+ $this->assertEquals( 'xxx1', $v, "Value was computed once" );
+ $this->assertEquals( 1, $wasSet, "Value was computed once" );
+
+ $touched = $mockWallClock - 10;
+ $v = $cache->getWithSetCallback(
+ $key,
+ $cache::TTL_INDEFINITE,
+ $checkFunc,
+ [ 'touchedCallback' => $touchedCallback ] + $extOpts
+ );
+ $v = $cache->getWithSetCallback(
+ $key,
+ $cache::TTL_INDEFINITE,
+ $checkFunc,
+ [ 'touchedCallback' => $touchedCallback ] + $extOpts
+ );
+ $this->assertEquals( 'xxx2', $v, "Value was recomputed once" );
+ $this->assertEquals( 2, $wasSet, "Value was recomputed once" );
+ }
+
public static function getWithSetCallback_provider() {
return [
[ [], false ],