From: Aaron Schulz Date: Wed, 5 Dec 2018 19:46:57 +0000 (-0500) Subject: objectcache: add expiration check callback to WANObjectCache::getWithSetCallback X-Git-Tag: 1.34.0-rc.0~3197^2 X-Git-Url: http://git.cyclocoop.org/%7B%24admin_url%7Dmembres/cotisations/gestion/rappel_supprimer.php?a=commitdiff_plain;h=2190b78987ad609a14176bc75c32f33582d3cf7b;p=lhc%2Fweb%2Fwiklou.git objectcache: add expiration check callback to WANObjectCache::getWithSetCallback 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 --- diff --git a/includes/libs/objectcache/WANObjectCache.php b/includes/libs/objectcache/WANObjectCache.php index ed5c7f57c7..4bbebd6cc5 100644 --- a/includes/libs/objectcache/WANObjectCache.php +++ b/includes/libs/objectcache/WANObjectCache.php @@ -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 diff --git a/tests/phpunit/includes/libs/objectcache/WANObjectCacheTest.php b/tests/phpunit/includes/libs/objectcache/WANObjectCacheTest.php index 22aa6671bb..3e5211548e 100644 --- a/tests/phpunit/includes/libs/objectcache/WANObjectCacheTest.php +++ b/tests/phpunit/includes/libs/objectcache/WANObjectCacheTest.php @@ -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 ],