objectcache: add expiration check callback to WANObjectCache::getWithSetCallback
authorAaron Schulz <aschulz@wikimedia.org>
Wed, 5 Dec 2018 19:46:57 +0000 (14:46 -0500)
committerAaron Schulz <aschulz@wikimedia.org>
Fri, 21 Dec 2018 19:25:26 +0000 (19:25 +0000)
This is useful when the timestamps to be checked depend on the value or are stored
in the database rather than as check keys.

Change-Id: I81ab08a943ee7d2f96a132d371965501941ed37f

includes/libs/objectcache/WANObjectCache.php
tests/phpunit/includes/libs/objectcache/WANObjectCacheTest.php

index ed5c7f5..4bbebd6 100644 (file)
@@ -1082,9 +1082,19 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
         *      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 = [] ) {
@@ -1183,6 +1193,7 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
                $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 );
@@ -1192,6 +1203,9 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
                $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
@@ -1310,6 +1324,32 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
                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
index 22aa667..3e52115 100644 (file)
@@ -197,8 +197,7 @@ class WANObjectCacheTest extends PHPUnit\Framework\TestCase {
                $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;
@@ -351,7 +350,7 @@ class WANObjectCacheTest extends PHPUnit\Framework\TestCase {
                $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,
@@ -365,6 +364,65 @@ class WANObjectCacheTest extends PHPUnit\Framework\TestCase {
                $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 ],