From: Aaron Schulz Date: Sat, 24 Oct 2015 07:48:40 +0000 (-0700) Subject: Move MultiWriteBagOStuff to /libs X-Git-Tag: 1.31.0-rc.0~9267^2 X-Git-Url: http://git.cyclocoop.org/%24image?a=commitdiff_plain;h=cce813a9227e67ccba714c3968395eb1ad7ae491;p=lhc%2Fweb%2Fwiklou.git Move MultiWriteBagOStuff to /libs Also moved related tests files to /libs. Change-Id: I806eeaa30205733d497adde933baf0c4157f7aae --- diff --git a/autoload.php b/autoload.php index 8720f333af..b32824d5c7 100644 --- a/autoload.php +++ b/autoload.php @@ -816,7 +816,7 @@ $wgAutoloadLocalClasses = array( 'MssqlUpdater' => __DIR__ . '/includes/installer/MssqlUpdater.php', 'MultiConfig' => __DIR__ . '/includes/config/MultiConfig.php', 'MultiHttpClient' => __DIR__ . '/includes/libs/MultiHttpClient.php', - 'MultiWriteBagOStuff' => __DIR__ . '/includes/objectcache/MultiWriteBagOStuff.php', + 'MultiWriteBagOStuff' => __DIR__ . '/includes/libs/objectcache/MultiWriteBagOStuff.php', 'MutableConfig' => __DIR__ . '/includes/config/MutableConfig.php', 'MutableContext' => __DIR__ . '/includes/context/MutableContext.php', 'MwSql' => __DIR__ . '/maintenance/sql.php', diff --git a/includes/libs/objectcache/MultiWriteBagOStuff.php b/includes/libs/objectcache/MultiWriteBagOStuff.php new file mode 100644 index 0000000000..73bdabd1c2 --- /dev/null +++ b/includes/libs/objectcache/MultiWriteBagOStuff.php @@ -0,0 +1,235 @@ +caches = array(); + foreach ( $params['caches'] as $cacheInfo ) { + if ( $cacheInfo instanceof BagOStuff ) { + $this->caches[] = $cacheInfo; + } else { + if ( !isset( $cacheInfo['args'] ) ) { + // B/C for when $cacheInfo was for ObjectCache::newFromParams(). + // Callers intenting this to be for ObjectFactory::getObjectFromSpec + // should have set "args" per the docs above. Doings so avoids extra + // (likely harmless) params (factory/class/calls) ending up in "args". + $cacheInfo['args'] = array( $cacheInfo ); + } + $this->caches[] = ObjectFactory::getObjectFromSpec( $cacheInfo ); + } + } + + $this->asyncHandler = isset( $params['asyncHandler'] ) + ? $params['asyncHandler'] + : null; + $this->asyncWrites = ( + isset( $params['replication'] ) && + $params['replication'] === 'async' && + is_callable( $this->asyncHandler ) + ); + } + + public function setDebug( $debug ) { + foreach ( $this->caches as $cache ) { + $cache->setDebug( $debug ); + } + } + + protected function doGet( $key, $flags = 0 ) { + if ( ( $flags & self::READ_LATEST ) == self::READ_LATEST ) { + // If the latest write was a delete(), we do NOT want to fallback + // to the other tiers and possibly see the old value. Also, this + // is used by mergeViaLock(), which only needs to hit the primary. + return $this->caches[0]->get( $key, $flags ); + } + + $misses = 0; // number backends checked + $value = false; + foreach ( $this->caches as $cache ) { + $value = $cache->get( $key, $flags ); + if ( $value !== false ) { + break; + } + ++$misses; + } + + if ( $value !== false + && $misses > 0 + && ( $flags & self::READ_VERIFIED ) == self::READ_VERIFIED + ) { + $this->doWrite( $misses, $this->asyncWrites, 'set', $key, $value, self::UPGRADE_TTL ); + } + + return $value; + } + + public function set( $key, $value, $exptime = 0, $flags = 0 ) { + $asyncWrites = ( ( $flags & self::WRITE_SYNC ) == self::WRITE_SYNC ) + ? false + : $this->asyncWrites; + + return $this->doWrite( self::ALL, $asyncWrites, 'set', $key, $value, $exptime ); + } + + public function delete( $key ) { + return $this->doWrite( self::ALL, $this->asyncWrites, 'delete', $key ); + } + + public function add( $key, $value, $exptime = 0 ) { + return $this->doWrite( self::ALL, $this->asyncWrites, 'add', $key, $value, $exptime ); + } + + public function incr( $key, $value = 1 ) { + return $this->doWrite( self::ALL, $this->asyncWrites, 'incr', $key, $value ); + } + + public function decr( $key, $value = 1 ) { + return $this->doWrite( self::ALL, $this->asyncWrites, 'decr', $key, $value ); + } + + public function lock( $key, $timeout = 6, $expiry = 6, $rclass = '' ) { + // Only need to lock the first cache; also avoids deadlocks + return $this->caches[0]->lock( $key, $timeout, $expiry, $rclass ); + } + + public function unlock( $key ) { + // Only the first cache is locked + return $this->caches[0]->unlock( $key ); + } + + public function getLastError() { + return $this->caches[0]->getLastError(); + } + + public function clearLastError() { + $this->caches[0]->clearLastError(); + } + + /** + * Apply a write method to the first $count backing caches + * + * @param integer $count + * @param bool $asyncWrites + * @param string $method + * @param mixed ... + * @return bool + */ + protected function doWrite( $count, $asyncWrites, $method /*, ... */ ) { + $ret = true; + $args = array_slice( func_get_args(), 3 ); + + foreach ( $this->caches as $i => $cache ) { + if ( $i >= $count ) { + break; // ignore the lower tiers + } + + if ( $i == 0 || !$asyncWrites ) { + // First store or in sync mode: write now and get result + if ( !call_user_func_array( array( $cache, $method ), $args ) ) { + $ret = false; + } + } else { + // Secondary write in async mode: do not block this HTTP request + $logger = $this->logger; + call_user_func( + $this->asyncHandler, + function () use ( $cache, $method, $args, $logger ) { + if ( !call_user_func_array( array( $cache, $method ), $args ) ) { + $logger->warning( "Async $method op failed" ); + } + } + ); + } + } + + return $ret; + } + + /** + * Delete objects expiring before a certain date. + * + * Succeed if any of the child caches succeed. + * @param string $date + * @param bool|callable $progressCallback + * @return bool + */ + public function deleteObjectsExpiringBefore( $date, $progressCallback = false ) { + $ret = false; + foreach ( $this->caches as $cache ) { + if ( $cache->deleteObjectsExpiringBefore( $date, $progressCallback ) ) { + $ret = true; + } + } + + return $ret; + } +} diff --git a/includes/objectcache/MultiWriteBagOStuff.php b/includes/objectcache/MultiWriteBagOStuff.php deleted file mode 100644 index 73bdabd1c2..0000000000 --- a/includes/objectcache/MultiWriteBagOStuff.php +++ /dev/null @@ -1,235 +0,0 @@ -caches = array(); - foreach ( $params['caches'] as $cacheInfo ) { - if ( $cacheInfo instanceof BagOStuff ) { - $this->caches[] = $cacheInfo; - } else { - if ( !isset( $cacheInfo['args'] ) ) { - // B/C for when $cacheInfo was for ObjectCache::newFromParams(). - // Callers intenting this to be for ObjectFactory::getObjectFromSpec - // should have set "args" per the docs above. Doings so avoids extra - // (likely harmless) params (factory/class/calls) ending up in "args". - $cacheInfo['args'] = array( $cacheInfo ); - } - $this->caches[] = ObjectFactory::getObjectFromSpec( $cacheInfo ); - } - } - - $this->asyncHandler = isset( $params['asyncHandler'] ) - ? $params['asyncHandler'] - : null; - $this->asyncWrites = ( - isset( $params['replication'] ) && - $params['replication'] === 'async' && - is_callable( $this->asyncHandler ) - ); - } - - public function setDebug( $debug ) { - foreach ( $this->caches as $cache ) { - $cache->setDebug( $debug ); - } - } - - protected function doGet( $key, $flags = 0 ) { - if ( ( $flags & self::READ_LATEST ) == self::READ_LATEST ) { - // If the latest write was a delete(), we do NOT want to fallback - // to the other tiers and possibly see the old value. Also, this - // is used by mergeViaLock(), which only needs to hit the primary. - return $this->caches[0]->get( $key, $flags ); - } - - $misses = 0; // number backends checked - $value = false; - foreach ( $this->caches as $cache ) { - $value = $cache->get( $key, $flags ); - if ( $value !== false ) { - break; - } - ++$misses; - } - - if ( $value !== false - && $misses > 0 - && ( $flags & self::READ_VERIFIED ) == self::READ_VERIFIED - ) { - $this->doWrite( $misses, $this->asyncWrites, 'set', $key, $value, self::UPGRADE_TTL ); - } - - return $value; - } - - public function set( $key, $value, $exptime = 0, $flags = 0 ) { - $asyncWrites = ( ( $flags & self::WRITE_SYNC ) == self::WRITE_SYNC ) - ? false - : $this->asyncWrites; - - return $this->doWrite( self::ALL, $asyncWrites, 'set', $key, $value, $exptime ); - } - - public function delete( $key ) { - return $this->doWrite( self::ALL, $this->asyncWrites, 'delete', $key ); - } - - public function add( $key, $value, $exptime = 0 ) { - return $this->doWrite( self::ALL, $this->asyncWrites, 'add', $key, $value, $exptime ); - } - - public function incr( $key, $value = 1 ) { - return $this->doWrite( self::ALL, $this->asyncWrites, 'incr', $key, $value ); - } - - public function decr( $key, $value = 1 ) { - return $this->doWrite( self::ALL, $this->asyncWrites, 'decr', $key, $value ); - } - - public function lock( $key, $timeout = 6, $expiry = 6, $rclass = '' ) { - // Only need to lock the first cache; also avoids deadlocks - return $this->caches[0]->lock( $key, $timeout, $expiry, $rclass ); - } - - public function unlock( $key ) { - // Only the first cache is locked - return $this->caches[0]->unlock( $key ); - } - - public function getLastError() { - return $this->caches[0]->getLastError(); - } - - public function clearLastError() { - $this->caches[0]->clearLastError(); - } - - /** - * Apply a write method to the first $count backing caches - * - * @param integer $count - * @param bool $asyncWrites - * @param string $method - * @param mixed ... - * @return bool - */ - protected function doWrite( $count, $asyncWrites, $method /*, ... */ ) { - $ret = true; - $args = array_slice( func_get_args(), 3 ); - - foreach ( $this->caches as $i => $cache ) { - if ( $i >= $count ) { - break; // ignore the lower tiers - } - - if ( $i == 0 || !$asyncWrites ) { - // First store or in sync mode: write now and get result - if ( !call_user_func_array( array( $cache, $method ), $args ) ) { - $ret = false; - } - } else { - // Secondary write in async mode: do not block this HTTP request - $logger = $this->logger; - call_user_func( - $this->asyncHandler, - function () use ( $cache, $method, $args, $logger ) { - if ( !call_user_func_array( array( $cache, $method ), $args ) ) { - $logger->warning( "Async $method op failed" ); - } - } - ); - } - } - - return $ret; - } - - /** - * Delete objects expiring before a certain date. - * - * Succeed if any of the child caches succeed. - * @param string $date - * @param bool|callable $progressCallback - * @return bool - */ - public function deleteObjectsExpiringBefore( $date, $progressCallback = false ) { - $ret = false; - foreach ( $this->caches as $cache ) { - if ( $cache->deleteObjectsExpiringBefore( $date, $progressCallback ) ) { - $ret = true; - } - } - - return $ret; - } -} diff --git a/tests/phpunit/includes/libs/objectcache/BagOStuffTest.php b/tests/phpunit/includes/libs/objectcache/BagOStuffTest.php new file mode 100644 index 0000000000..94b74cb651 --- /dev/null +++ b/tests/phpunit/includes/libs/objectcache/BagOStuffTest.php @@ -0,0 +1,243 @@ + + * @group BagOStuff + */ +class BagOStuffTest extends MediaWikiTestCase { + /** @var BagOStuff */ + private $cache; + + protected function setUp() { + parent::setUp(); + + // type defined through parameter + if ( $this->getCliArg( 'use-bagostuff' ) ) { + $name = $this->getCliArg( 'use-bagostuff' ); + + $this->cache = ObjectCache::newFromId( $name ); + } else { + // no type defined - use simple hash + $this->cache = new HashBagOStuff; + } + + $this->cache->delete( wfMemcKey( 'test' ) ); + } + + /** + * @covers BagOStuff::makeGlobalKey + * @covers BagOStuff::makeKeyInternal + */ + public function testMakeKey() { + $cache = ObjectCache::newFromId( 'hash' ); + + $localKey = $cache->makeKey( 'first', 'second', 'third' ); + $globalKey = $cache->makeGlobalKey( 'first', 'second', 'third' ); + + $this->assertStringMatchesFormat( + '%Sfirst%Ssecond%Sthird%S', + $localKey, + 'Local key interpolates parameters' + ); + + $this->assertStringMatchesFormat( + 'global%Sfirst%Ssecond%Sthird%S', + $globalKey, + 'Global key interpolates parameters and contains global prefix' + ); + + $this->assertNotEquals( + $localKey, + $globalKey, + 'Local key and global key with same parameters should not be equal' + ); + + $this->assertNotEquals( + $cache->makeKeyInternal( 'prefix', array( 'a', 'bc:', 'de' ) ), + $cache->makeKeyInternal( 'prefix', array( 'a', 'bc', ':de' ) ) + ); + } + + /** + * @covers BagOStuff::merge + * @covers BagOStuff::mergeViaLock + */ + public function testMerge() { + $key = wfMemcKey( 'test' ); + + $usleep = 0; + + /** + * Callback method: append "merged" to whatever is in cache. + * + * @param BagOStuff $cache + * @param string $key + * @param int $existingValue + * @use int $usleep + * @return int + */ + $callback = function ( BagOStuff $cache, $key, $existingValue ) use ( &$usleep ) { + // let's pretend this is an expensive callback to test concurrent merge attempts + usleep( $usleep ); + + if ( $existingValue === false ) { + return 'merged'; + } + + return $existingValue . 'merged'; + }; + + // merge on non-existing value + $merged = $this->cache->merge( $key, $callback, 0 ); + $this->assertTrue( $merged ); + $this->assertEquals( $this->cache->get( $key ), 'merged' ); + + // merge on existing value + $merged = $this->cache->merge( $key, $callback, 0 ); + $this->assertTrue( $merged ); + $this->assertEquals( $this->cache->get( $key ), 'mergedmerged' ); + + /* + * Test concurrent merges by forking this process, if: + * - not manually called with --use-bagostuff + * - pcntl_fork is supported by the system + * - cache type will correctly support calls over forks + */ + $fork = (bool)$this->getCliArg( 'use-bagostuff' ); + $fork &= function_exists( 'pcntl_fork' ); + $fork &= !$this->cache instanceof HashBagOStuff; + $fork &= !$this->cache instanceof EmptyBagOStuff; + $fork &= !$this->cache instanceof MultiWriteBagOStuff; + if ( $fork ) { + // callback should take awhile now so that we can test concurrent merge attempts + $pid = pcntl_fork(); + if ( $pid == -1 ) { + // can't fork, ignore this test... + } elseif ( $pid ) { + // wait a little, making sure that the child process is calling merge + usleep( 3000 ); + + // attempt a merge - this should fail + $merged = $this->cache->merge( $key, $callback, 0, 1 ); + + // merge has failed because child process was merging (and we only attempted once) + $this->assertFalse( $merged ); + + // make sure the child's merge is completed and verify + usleep( 3000 ); + $this->assertEquals( $this->cache->get( $key ), 'mergedmergedmerged' ); + } else { + $this->cache->merge( $key, $callback, 0, 1 ); + + // Note: I'm not even going to check if the merge worked, I'll + // compare values in the parent process to test if this merge worked. + // I'm just going to exit this child process, since I don't want the + // child to output any test results (would be rather confusing to + // have test output twice) + exit; + } + } + } + + /** + * @covers BagOStuff::add + */ + public function testAdd() { + $key = wfMemcKey( 'test' ); + $this->assertTrue( $this->cache->add( $key, 'test' ) ); + } + + public function testGet() { + $value = array( 'this' => 'is', 'a' => 'test' ); + + $key = wfMemcKey( 'test' ); + $this->cache->add( $key, $value ); + $this->assertEquals( $this->cache->get( $key ), $value ); + } + + /** + * @covers BagOStuff::getWithSetCallback + */ + public function testGetWithSetCallback() { + $key = wfMemcKey( 'test' ); + $value = $this->cache->getWithSetCallback( + $key, + 30, + function () { + return 'hello kitty'; + } + ); + + $this->assertEquals( 'hello kitty', $value ); + $this->assertEquals( $value, $this->cache->get( $key ) ); + } + + /** + * @covers BagOStuff::incr + */ + public function testIncr() { + $key = wfMemcKey( 'test' ); + $this->cache->add( $key, 0 ); + $this->cache->incr( $key ); + $expectedValue = 1; + $actualValue = $this->cache->get( $key ); + $this->assertEquals( $expectedValue, $actualValue, 'Value should be 1 after incrementing' ); + } + + /** + * @covers BagOStuff::getMulti + */ + public function testGetMulti() { + $value1 = array( 'this' => 'is', 'a' => 'test' ); + $value2 = array( 'this' => 'is', 'another' => 'test' ); + $value3 = array( 'testing a key that may be encoded when sent to cache backend' ); + $value4 = array( 'another test where chars in key will be encoded' ); + + $key1 = wfMemcKey( 'test1' ); + $key2 = wfMemcKey( 'test2' ); + // internally, MemcachedBagOStuffs will encode to will-%25-encode + $key3 = wfMemcKey( 'will-%-encode' ); + $key4 = wfMemcKey( + 'flowdb:flow_ref:wiki:by-source:v3:Parser\'s_"broken"_+_(page)_&_grill:testwiki:1:4.7' + ); + + $this->cache->add( $key1, $value1 ); + $this->cache->add( $key2, $value2 ); + $this->cache->add( $key3, $value3 ); + $this->cache->add( $key4, $value4 ); + + $this->assertEquals( + array( $key1 => $value1, $key2 => $value2, $key3 => $value3, $key4 => $value4 ), + $this->cache->getMulti( array( $key1, $key2, $key3, $key4 ) ) + ); + + // cleanup + $this->cache->delete( $key1 ); + $this->cache->delete( $key2 ); + $this->cache->delete( $key3 ); + $this->cache->delete( $key4 ); + } + + /** + * @covers BagOStuff::getScopedLock + */ + public function testGetScopedLock() { + $key = wfMemcKey( 'test' ); + $value1 = $this->cache->getScopedLock( $key, 0 ); + $value2 = $this->cache->getScopedLock( $key, 0 ); + + $this->assertType( 'ScopedCallback', $value1, 'First call returned lock' ); + $this->assertNull( $value2, 'Duplicate call returned no lock' ); + + unset( $value1 ); + + $value3 = $this->cache->getScopedLock( $key, 0 ); + $this->assertType( 'ScopedCallback', $value3, 'Lock returned callback after release' ); + unset( $value3 ); + + $value1 = $this->cache->getScopedLock( $key, 0, 5, 'reentry' ); + $value2 = $this->cache->getScopedLock( $key, 0, 5, 'reentry' ); + + $this->assertType( 'ScopedCallback', $value1, 'First reentrant call returned lock' ); + $this->assertType( 'ScopedCallback', $value1, 'Second reentrant call returned lock' ); + } +} diff --git a/tests/phpunit/includes/libs/objectcache/MultiWriteBagOStuffTest.php b/tests/phpunit/includes/libs/objectcache/MultiWriteBagOStuffTest.php new file mode 100644 index 0000000000..1d8f43a0e0 --- /dev/null +++ b/tests/phpunit/includes/libs/objectcache/MultiWriteBagOStuffTest.php @@ -0,0 +1,91 @@ +cache1 = new HashBagOStuff(); + $this->cache2 = new HashBagOStuff(); + $this->cache = new MultiWriteBagOStuff( array( + 'caches' => array( $this->cache1, $this->cache2 ), + 'replication' => 'async', + 'asyncHandler' => 'DeferredUpdates::addCallableUpdate' + ) ); + } + + public function testSetImmediate() { + $key = wfRandomString(); + $value = wfRandomString(); + $this->cache->set( $key, $value ); + + // Set in tier 1 + $this->assertEquals( $value, $this->cache1->get( $key ), 'Written to tier 1' ); + // Set in tier 2 + $this->assertEquals( $value, $this->cache2->get( $key ), 'Written to tier 2' ); + } + + public function testSyncMerge() { + $key = wfRandomString(); + $value = wfRandomString(); + $func = function () use ( $value ) { + return $value; + }; + + // XXX: DeferredUpdates bound to transactions in CLI mode + $dbw = wfGetDB( DB_MASTER ); + $dbw->begin(); + $this->cache->merge( $key, $func ); + + // Set in tier 1 + $this->assertEquals( $value, $this->cache1->get( $key ), 'Written to tier 1' ); + // Not yet set in tier 2 + $this->assertEquals( false, $this->cache2->get( $key ), 'Not written to tier 2' ); + + $dbw->commit(); + + // Set in tier 2 + $this->assertEquals( $value, $this->cache2->get( $key ), 'Written to tier 2' ); + + $key = wfRandomString(); + + $dbw->begin(); + $this->cache->merge( $key, $func, 0, 1, BagOStuff::WRITE_SYNC ); + + // Set in tier 1 + $this->assertEquals( $value, $this->cache1->get( $key ), 'Written to tier 1' ); + // Also set in tier 2 + $this->assertEquals( $value, $this->cache2->get( $key ), 'Written to tier 2' ); + + $dbw->commit(); + } + + public function testSetDelayed() { + $key = wfRandomString(); + $value = wfRandomString(); + + // XXX: DeferredUpdates bound to transactions in CLI mode + $dbw = wfGetDB( DB_MASTER ); + $dbw->begin(); + $this->cache->set( $key, $value ); + + // Set in tier 1 + $this->assertEquals( $value, $this->cache1->get( $key ), 'Written to tier 1' ); + // Not yet set in tier 2 + $this->assertEquals( false, $this->cache2->get( $key ), 'Not written to tier 2' ); + + $dbw->commit(); + + // Set in tier 2 + $this->assertEquals( $value, $this->cache2->get( $key ), 'Written to tier 2' ); + } +} diff --git a/tests/phpunit/includes/libs/objectcache/ReplicatedBagOStuffTest.php b/tests/phpunit/includes/libs/objectcache/ReplicatedBagOStuffTest.php new file mode 100644 index 0000000000..a419f5b67a --- /dev/null +++ b/tests/phpunit/includes/libs/objectcache/ReplicatedBagOStuffTest.php @@ -0,0 +1,62 @@ +writeCache = new HashBagOStuff(); + $this->readCache = new HashBagOStuff(); + $this->cache = new ReplicatedBagOStuff( array( + 'writeFactory' => $this->writeCache, + 'readFactory' => $this->readCache, + ) ); + } + + /** + * @covers ReplicatedBagOStuff::set + */ + public function testSet() { + $key = wfRandomString(); + $value = wfRandomString(); + $this->cache->set( $key, $value ); + + // Write to master. + $this->assertEquals( $this->writeCache->get( $key ), $value ); + // Don't write to slave. Replication is deferred to backend. + $this->assertEquals( $this->readCache->get( $key ), false ); + } + + /** + * @covers ReplicatedBagOStuff::get + */ + public function testGet() { + $key = wfRandomString(); + + $write = wfRandomString(); + $this->writeCache->set( $key, $write ); + $read = wfRandomString(); + $this->readCache->set( $key, $read ); + + // Read from slave. + $this->assertEquals( $this->cache->get( $key ), $read ); + } + + /** + * @covers ReplicatedBagOStuff::get + */ + public function testGetAbsent() { + $key = wfRandomString(); + $value = wfRandomString(); + $this->writeCache->set( $key, $value ); + + // Don't read from master. No failover if value is absent. + $this->assertEquals( $this->cache->get( $key ), false ); + } +} diff --git a/tests/phpunit/includes/libs/objectcache/WANObjectCacheTest.php b/tests/phpunit/includes/libs/objectcache/WANObjectCacheTest.php new file mode 100644 index 0000000000..c3702c5945 --- /dev/null +++ b/tests/phpunit/includes/libs/objectcache/WANObjectCacheTest.php @@ -0,0 +1,316 @@ +getCliArg( 'use-wanobjectcache' ) ) { + $name = $this->getCliArg( 'use-wanobjectcache' ); + + $this->cache = ObjectCache::getWANInstance( $name ); + } else { + $this->cache = new WANObjectCache( array( + 'cache' => new HashBagOStuff(), + 'pool' => 'testcache-hash', + 'relayer' => new EventRelayerNull( array() ) + ) ); + } + + $wanCache = TestingAccessWrapper::newFromObject( $this->cache ); + $this->internalCache = $wanCache->cache; + } + + /** + * @dataProvider provider_testSetAndGet + * @covers WANObjectCache::set() + * @covers WANObjectCache::get() + * @param mixed $value + * @param integer $ttl + */ + public function testSetAndGet( $value, $ttl ) { + $key = wfRandomString(); + $this->cache->set( $key, $value, $ttl ); + + $curTTL = null; + $this->assertEquals( $value, $this->cache->get( $key, $curTTL ) ); + if ( is_infinite( $ttl ) || $ttl == 0 ) { + $this->assertTrue( is_infinite( $curTTL ), "Current TTL is infinite" ); + } else { + $this->assertGreaterThan( 0, $curTTL, "Current TTL > 0" ); + $this->assertLessThanOrEqual( $ttl, $curTTL, "Current TTL < nominal TTL" ); + } + } + + public static function provider_testSetAndGet() { + return array( + array( 14141, 3 ), + array( 3535.666, 3 ), + array( array(), 3 ), + array( null, 3 ), + array( '0', 3 ), + array( (object)array( 'meow' ), 3 ), + array( INF, 3 ), + array( '', 3 ), + array( 'pizzacat', INF ), + ); + } + + public function testGetNotExists() { + $key = wfRandomString(); + $curTTL = null; + $value = $this->cache->get( $key, $curTTL ); + + $this->assertFalse( $value, "Non-existing key has false value" ); + $this->assertNull( $curTTL, "Non-existing key has null current TTL" ); + } + + public function testSetOver() { + $key = wfRandomString(); + for ( $i = 0; $i < 3; ++$i ) { + $value = wfRandomString(); + $this->cache->set( $key, $value, 3 ); + + $this->assertEquals( $this->cache->get( $key ), $value ); + } + } + + public function testStaleSet() { + $key = wfRandomString(); + $value = wfRandomString(); + $this->cache->set( $key, $value, 3, array( 'since' => microtime( true ) - 30 ) ); + + $this->assertFalse( $this->cache->get( $key ), "Stale set() value ignored" ); + } + + /** + * @covers WANObjectCache::getWithSetCallback() + */ + public function testGetWithSetCallback() { + $cache = $this->cache; + + $key = wfRandomString(); + $value = wfRandomString(); + $cKey1 = wfRandomString(); + $cKey2 = wfRandomString(); + + $wasSet = 0; + $func = function( $old, &$ttl ) use ( &$wasSet, $value ) { + ++$wasSet; + $ttl = 20; // override with another value + return $value; + }; + + $wasSet = 0; + $v = $cache->getWithSetCallback( $key, 30, $func, array( 'lockTSE' => 5 ) ); + $this->assertEquals( $value, $v, "Value returned" ); + $this->assertEquals( 1, $wasSet, "Value regenerated" ); + + $curTTL = null; + $cache->get( $key, $curTTL ); + $this->assertLessThanOrEqual( 20, $curTTL, 'Current TTL between 19-20 (overriden)' ); + $this->assertGreaterThanOrEqual( 19, $curTTL, 'Current TTL between 19-20 (overriden)' ); + + $wasSet = 0; + $v = $cache->getWithSetCallback( $key, 30, $func, array( + 'lowTTL' => 0, + 'lockTSE' => 5, + ) ); + $this->assertEquals( $value, $v, "Value returned" ); + $this->assertEquals( 0, $wasSet, "Value not regenerated" ); + + $priorTime = microtime( true ); + usleep( 1 ); + $wasSet = 0; + $v = $cache->getWithSetCallback( $key, 30, $func, + array( 'checkKeys' => array( $cKey1, $cKey2 ) ) ); + $this->assertEquals( $value, $v, "Value returned" ); + $this->assertEquals( 1, $wasSet, "Value regenerated due to check keys" ); + $t1 = $cache->getCheckKeyTime( $cKey1 ); + $this->assertGreaterThanOrEqual( $priorTime, $t1, 'Check keys generated on miss' ); + $t2 = $cache->getCheckKeyTime( $cKey2 ); + $this->assertGreaterThanOrEqual( $priorTime, $t2, 'Check keys generated on miss' ); + + $priorTime = microtime( true ); + $wasSet = 0; + $v = $cache->getWithSetCallback( $key, 30, $func, + array( 'checkKeys' => array( $cKey1, $cKey2 ) ) ); + $this->assertEquals( $value, $v, "Value returned" ); + $this->assertEquals( 1, $wasSet, "Value regenerated due to still-recent check keys" ); + $t1 = $cache->getCheckKeyTime( $cKey1 ); + $this->assertLessThanOrEqual( $priorTime, $t1, 'Check keys did not change again' ); + $t2 = $cache->getCheckKeyTime( $cKey2 ); + $this->assertLessThanOrEqual( $priorTime, $t2, 'Check keys did not change again' ); + + $curTTL = null; + $v = $cache->get( $key, $curTTL, array( $cKey1, $cKey2 ) ); + $this->assertEquals( $value, $v, "Value returned" ); + $this->assertLessThanOrEqual( 0, $curTTL, "Value has current TTL < 0 due to check keys" ); + + $wasSet = 0; + $key = wfRandomString(); + $v = $cache->getWithSetCallback( $key, 30, $func, array( 'pcTTL' => 5 ) ); + $this->assertEquals( $value, $v, "Value returned" ); + $cache->delete( $key ); + $v = $cache->getWithSetCallback( $key, 30, $func, array( 'pcTTL' => 5 ) ); + $this->assertEquals( $value, $v, "Value still returned after deleted" ); + $this->assertEquals( 1, $wasSet, "Value process cached while deleted" ); + } + + /** + * @covers WANObjectCache::getWithSetCallback() + */ + public function testLockTSE() { + $cache = $this->cache; + $key = wfRandomString(); + $value = wfRandomString(); + + $calls = 0; + $func = function() use ( &$calls, $value ) { + ++$calls; + return $value; + }; + + $cache->delete( $key ); + $ret = $cache->getWithSetCallback( $key, 30, $func, array( 'lockTSE' => 5 ) ); + $this->assertEquals( $value, $ret ); + $this->assertEquals( 1, $calls, 'Value was populated' ); + + // Acquire a lock to verify that getWithSetCallback uses lockTSE properly + $this->internalCache->lock( $key, 0 ); + $ret = $cache->getWithSetCallback( $key, 30, $func, array( 'lockTSE' => 5 ) ); + $this->assertEquals( $value, $ret ); + $this->assertEquals( 1, $calls, 'Callback was not used' ); + } + + /** + * @covers WANObjectCache::getMulti() + */ + public function testGetMulti() { + $cache = $this->cache; + + $value1 = array( 'this' => 'is', 'a' => 'test' ); + $value2 = array( 'this' => 'is', 'another' => 'test' ); + + $key1 = wfRandomString(); + $key2 = wfRandomString(); + $key3 = wfRandomString(); + + $cache->set( $key1, $value1, 5 ); + $cache->set( $key2, $value2, 10 ); + + $curTTLs = array(); + $this->assertEquals( + array( $key1 => $value1, $key2 => $value2 ), + $cache->getMulti( array( $key1, $key2, $key3 ), $curTTLs ) + ); + + $this->assertEquals( 2, count( $curTTLs ), "Two current TTLs in array" ); + $this->assertGreaterThan( 0, $curTTLs[$key1], "Key 1 has current TTL > 0" ); + $this->assertGreaterThan( 0, $curTTLs[$key2], "Key 2 has current TTL > 0" ); + + $cKey1 = wfRandomString(); + $cKey2 = wfRandomString(); + $curTTLs = array(); + $this->assertEquals( + array( $key1 => $value1, $key2 => $value2 ), + $cache->getMulti( array( $key1, $key2, $key3 ), $curTTLs ), + 'Result array populated' + ); + + $priorTime = microtime( true ); + usleep( 1 ); + $curTTLs = array(); + $this->assertEquals( + array( $key1 => $value1, $key2 => $value2 ), + $cache->getMulti( array( $key1, $key2, $key3 ), $curTTLs, array( $cKey1, $cKey2 ) ), + "Result array populated even with new check keys" + ); + $t1 = $cache->getCheckKeyTime( $cKey1 ); + $this->assertGreaterThanOrEqual( $priorTime, $t1, 'Check key 1 generated on miss' ); + $t2 = $cache->getCheckKeyTime( $cKey2 ); + $this->assertGreaterThanOrEqual( $priorTime, $t2, 'Check key 2 generated on miss' ); + $this->assertEquals( 2, count( $curTTLs ), "Current TTLs array set" ); + $this->assertLessThanOrEqual( 0, $curTTLs[$key1], 'Key 1 has current TTL <= 0' ); + $this->assertLessThanOrEqual( 0, $curTTLs[$key2], 'Key 2 has current TTL <= 0' ); + + usleep( 1 ); + $curTTLs = array(); + $this->assertEquals( + array( $key1 => $value1, $key2 => $value2 ), + $cache->getMulti( array( $key1, $key2, $key3 ), $curTTLs, array( $cKey1, $cKey2 ) ), + "Result array still populated even with new check keys" + ); + $this->assertEquals( 2, count( $curTTLs ), "Current TTLs still array set" ); + $this->assertLessThan( 0, $curTTLs[$key1], 'Key 1 has negative current TTL' ); + $this->assertLessThan( 0, $curTTLs[$key2], 'Key 2 has negative current TTL' ); + } + + /** + * @covers WANObjectCache::delete() + */ + public function testDelete() { + $key = wfRandomString(); + $value = wfRandomString(); + $this->cache->set( $key, $value ); + + $curTTL = null; + $v = $this->cache->get( $key, $curTTL ); + $this->assertEquals( $value, $v, "Key was created with value" ); + $this->assertGreaterThan( 0, $curTTL, "Existing key has current TTL > 0" ); + + $this->cache->delete( $key ); + + $curTTL = null; + $v = $this->cache->get( $key, $curTTL ); + $this->assertFalse( $v, "Deleted key has false value" ); + $this->assertLessThan( 0, $curTTL, "Deleted key has current TTL < 0" ); + + $this->cache->set( $key, $value . 'more' ); + $this->assertFalse( $v, "Deleted key is tombstoned and has false value" ); + $this->assertLessThan( 0, $curTTL, "Deleted key is tombstoned and has current TTL < 0" ); + } + + /** + * @covers WANObjectCache::touchCheckKey() + * @covers WANObjectCache::resetCheckKey() + * @covers WANObjectCache::getCheckKeyTime() + */ + public function testTouchKeys() { + $key = wfRandomString(); + + $priorTime = microtime( true ); + usleep( 100 ); + $t0 = $this->cache->getCheckKeyTime( $key ); + $this->assertGreaterThanOrEqual( $priorTime, $t0, 'Check key auto-created' ); + + $priorTime = microtime( true ); + usleep( 100 ); + $this->cache->touchCheckKey( $key ); + $t1 = $this->cache->getCheckKeyTime( $key ); + $this->assertGreaterThanOrEqual( $priorTime, $t1, 'Check key created' ); + + $t2 = $this->cache->getCheckKeyTime( $key ); + $this->assertEquals( $t1, $t2, 'Check key time did not change' ); + + usleep( 100 ); + $this->cache->touchCheckKey( $key ); + $t3 = $this->cache->getCheckKeyTime( $key ); + $this->assertGreaterThan( $t2, $t3, 'Check key time increased' ); + + $t4 = $this->cache->getCheckKeyTime( $key ); + $this->assertEquals( $t3, $t4, 'Check key time did not change' ); + + usleep( 100 ); + $this->cache->resetCheckKey( $key ); + $t5 = $this->cache->getCheckKeyTime( $key ); + $this->assertGreaterThan( $t4, $t5, 'Check key time increased' ); + + $t6 = $this->cache->getCheckKeyTime( $key ); + $this->assertEquals( $t5, $t6, 'Check key time did not change' ); + } +} diff --git a/tests/phpunit/includes/objectcache/BagOStuffTest.php b/tests/phpunit/includes/objectcache/BagOStuffTest.php deleted file mode 100644 index 94b74cb651..0000000000 --- a/tests/phpunit/includes/objectcache/BagOStuffTest.php +++ /dev/null @@ -1,243 +0,0 @@ - - * @group BagOStuff - */ -class BagOStuffTest extends MediaWikiTestCase { - /** @var BagOStuff */ - private $cache; - - protected function setUp() { - parent::setUp(); - - // type defined through parameter - if ( $this->getCliArg( 'use-bagostuff' ) ) { - $name = $this->getCliArg( 'use-bagostuff' ); - - $this->cache = ObjectCache::newFromId( $name ); - } else { - // no type defined - use simple hash - $this->cache = new HashBagOStuff; - } - - $this->cache->delete( wfMemcKey( 'test' ) ); - } - - /** - * @covers BagOStuff::makeGlobalKey - * @covers BagOStuff::makeKeyInternal - */ - public function testMakeKey() { - $cache = ObjectCache::newFromId( 'hash' ); - - $localKey = $cache->makeKey( 'first', 'second', 'third' ); - $globalKey = $cache->makeGlobalKey( 'first', 'second', 'third' ); - - $this->assertStringMatchesFormat( - '%Sfirst%Ssecond%Sthird%S', - $localKey, - 'Local key interpolates parameters' - ); - - $this->assertStringMatchesFormat( - 'global%Sfirst%Ssecond%Sthird%S', - $globalKey, - 'Global key interpolates parameters and contains global prefix' - ); - - $this->assertNotEquals( - $localKey, - $globalKey, - 'Local key and global key with same parameters should not be equal' - ); - - $this->assertNotEquals( - $cache->makeKeyInternal( 'prefix', array( 'a', 'bc:', 'de' ) ), - $cache->makeKeyInternal( 'prefix', array( 'a', 'bc', ':de' ) ) - ); - } - - /** - * @covers BagOStuff::merge - * @covers BagOStuff::mergeViaLock - */ - public function testMerge() { - $key = wfMemcKey( 'test' ); - - $usleep = 0; - - /** - * Callback method: append "merged" to whatever is in cache. - * - * @param BagOStuff $cache - * @param string $key - * @param int $existingValue - * @use int $usleep - * @return int - */ - $callback = function ( BagOStuff $cache, $key, $existingValue ) use ( &$usleep ) { - // let's pretend this is an expensive callback to test concurrent merge attempts - usleep( $usleep ); - - if ( $existingValue === false ) { - return 'merged'; - } - - return $existingValue . 'merged'; - }; - - // merge on non-existing value - $merged = $this->cache->merge( $key, $callback, 0 ); - $this->assertTrue( $merged ); - $this->assertEquals( $this->cache->get( $key ), 'merged' ); - - // merge on existing value - $merged = $this->cache->merge( $key, $callback, 0 ); - $this->assertTrue( $merged ); - $this->assertEquals( $this->cache->get( $key ), 'mergedmerged' ); - - /* - * Test concurrent merges by forking this process, if: - * - not manually called with --use-bagostuff - * - pcntl_fork is supported by the system - * - cache type will correctly support calls over forks - */ - $fork = (bool)$this->getCliArg( 'use-bagostuff' ); - $fork &= function_exists( 'pcntl_fork' ); - $fork &= !$this->cache instanceof HashBagOStuff; - $fork &= !$this->cache instanceof EmptyBagOStuff; - $fork &= !$this->cache instanceof MultiWriteBagOStuff; - if ( $fork ) { - // callback should take awhile now so that we can test concurrent merge attempts - $pid = pcntl_fork(); - if ( $pid == -1 ) { - // can't fork, ignore this test... - } elseif ( $pid ) { - // wait a little, making sure that the child process is calling merge - usleep( 3000 ); - - // attempt a merge - this should fail - $merged = $this->cache->merge( $key, $callback, 0, 1 ); - - // merge has failed because child process was merging (and we only attempted once) - $this->assertFalse( $merged ); - - // make sure the child's merge is completed and verify - usleep( 3000 ); - $this->assertEquals( $this->cache->get( $key ), 'mergedmergedmerged' ); - } else { - $this->cache->merge( $key, $callback, 0, 1 ); - - // Note: I'm not even going to check if the merge worked, I'll - // compare values in the parent process to test if this merge worked. - // I'm just going to exit this child process, since I don't want the - // child to output any test results (would be rather confusing to - // have test output twice) - exit; - } - } - } - - /** - * @covers BagOStuff::add - */ - public function testAdd() { - $key = wfMemcKey( 'test' ); - $this->assertTrue( $this->cache->add( $key, 'test' ) ); - } - - public function testGet() { - $value = array( 'this' => 'is', 'a' => 'test' ); - - $key = wfMemcKey( 'test' ); - $this->cache->add( $key, $value ); - $this->assertEquals( $this->cache->get( $key ), $value ); - } - - /** - * @covers BagOStuff::getWithSetCallback - */ - public function testGetWithSetCallback() { - $key = wfMemcKey( 'test' ); - $value = $this->cache->getWithSetCallback( - $key, - 30, - function () { - return 'hello kitty'; - } - ); - - $this->assertEquals( 'hello kitty', $value ); - $this->assertEquals( $value, $this->cache->get( $key ) ); - } - - /** - * @covers BagOStuff::incr - */ - public function testIncr() { - $key = wfMemcKey( 'test' ); - $this->cache->add( $key, 0 ); - $this->cache->incr( $key ); - $expectedValue = 1; - $actualValue = $this->cache->get( $key ); - $this->assertEquals( $expectedValue, $actualValue, 'Value should be 1 after incrementing' ); - } - - /** - * @covers BagOStuff::getMulti - */ - public function testGetMulti() { - $value1 = array( 'this' => 'is', 'a' => 'test' ); - $value2 = array( 'this' => 'is', 'another' => 'test' ); - $value3 = array( 'testing a key that may be encoded when sent to cache backend' ); - $value4 = array( 'another test where chars in key will be encoded' ); - - $key1 = wfMemcKey( 'test1' ); - $key2 = wfMemcKey( 'test2' ); - // internally, MemcachedBagOStuffs will encode to will-%25-encode - $key3 = wfMemcKey( 'will-%-encode' ); - $key4 = wfMemcKey( - 'flowdb:flow_ref:wiki:by-source:v3:Parser\'s_"broken"_+_(page)_&_grill:testwiki:1:4.7' - ); - - $this->cache->add( $key1, $value1 ); - $this->cache->add( $key2, $value2 ); - $this->cache->add( $key3, $value3 ); - $this->cache->add( $key4, $value4 ); - - $this->assertEquals( - array( $key1 => $value1, $key2 => $value2, $key3 => $value3, $key4 => $value4 ), - $this->cache->getMulti( array( $key1, $key2, $key3, $key4 ) ) - ); - - // cleanup - $this->cache->delete( $key1 ); - $this->cache->delete( $key2 ); - $this->cache->delete( $key3 ); - $this->cache->delete( $key4 ); - } - - /** - * @covers BagOStuff::getScopedLock - */ - public function testGetScopedLock() { - $key = wfMemcKey( 'test' ); - $value1 = $this->cache->getScopedLock( $key, 0 ); - $value2 = $this->cache->getScopedLock( $key, 0 ); - - $this->assertType( 'ScopedCallback', $value1, 'First call returned lock' ); - $this->assertNull( $value2, 'Duplicate call returned no lock' ); - - unset( $value1 ); - - $value3 = $this->cache->getScopedLock( $key, 0 ); - $this->assertType( 'ScopedCallback', $value3, 'Lock returned callback after release' ); - unset( $value3 ); - - $value1 = $this->cache->getScopedLock( $key, 0, 5, 'reentry' ); - $value2 = $this->cache->getScopedLock( $key, 0, 5, 'reentry' ); - - $this->assertType( 'ScopedCallback', $value1, 'First reentrant call returned lock' ); - $this->assertType( 'ScopedCallback', $value1, 'Second reentrant call returned lock' ); - } -} diff --git a/tests/phpunit/includes/objectcache/MultiWriteBagOStuffTest.php b/tests/phpunit/includes/objectcache/MultiWriteBagOStuffTest.php deleted file mode 100644 index 1d8f43a0e0..0000000000 --- a/tests/phpunit/includes/objectcache/MultiWriteBagOStuffTest.php +++ /dev/null @@ -1,91 +0,0 @@ -cache1 = new HashBagOStuff(); - $this->cache2 = new HashBagOStuff(); - $this->cache = new MultiWriteBagOStuff( array( - 'caches' => array( $this->cache1, $this->cache2 ), - 'replication' => 'async', - 'asyncHandler' => 'DeferredUpdates::addCallableUpdate' - ) ); - } - - public function testSetImmediate() { - $key = wfRandomString(); - $value = wfRandomString(); - $this->cache->set( $key, $value ); - - // Set in tier 1 - $this->assertEquals( $value, $this->cache1->get( $key ), 'Written to tier 1' ); - // Set in tier 2 - $this->assertEquals( $value, $this->cache2->get( $key ), 'Written to tier 2' ); - } - - public function testSyncMerge() { - $key = wfRandomString(); - $value = wfRandomString(); - $func = function () use ( $value ) { - return $value; - }; - - // XXX: DeferredUpdates bound to transactions in CLI mode - $dbw = wfGetDB( DB_MASTER ); - $dbw->begin(); - $this->cache->merge( $key, $func ); - - // Set in tier 1 - $this->assertEquals( $value, $this->cache1->get( $key ), 'Written to tier 1' ); - // Not yet set in tier 2 - $this->assertEquals( false, $this->cache2->get( $key ), 'Not written to tier 2' ); - - $dbw->commit(); - - // Set in tier 2 - $this->assertEquals( $value, $this->cache2->get( $key ), 'Written to tier 2' ); - - $key = wfRandomString(); - - $dbw->begin(); - $this->cache->merge( $key, $func, 0, 1, BagOStuff::WRITE_SYNC ); - - // Set in tier 1 - $this->assertEquals( $value, $this->cache1->get( $key ), 'Written to tier 1' ); - // Also set in tier 2 - $this->assertEquals( $value, $this->cache2->get( $key ), 'Written to tier 2' ); - - $dbw->commit(); - } - - public function testSetDelayed() { - $key = wfRandomString(); - $value = wfRandomString(); - - // XXX: DeferredUpdates bound to transactions in CLI mode - $dbw = wfGetDB( DB_MASTER ); - $dbw->begin(); - $this->cache->set( $key, $value ); - - // Set in tier 1 - $this->assertEquals( $value, $this->cache1->get( $key ), 'Written to tier 1' ); - // Not yet set in tier 2 - $this->assertEquals( false, $this->cache2->get( $key ), 'Not written to tier 2' ); - - $dbw->commit(); - - // Set in tier 2 - $this->assertEquals( $value, $this->cache2->get( $key ), 'Written to tier 2' ); - } -} diff --git a/tests/phpunit/includes/objectcache/ReplicatedBagOStuffTest.php b/tests/phpunit/includes/objectcache/ReplicatedBagOStuffTest.php deleted file mode 100644 index a419f5b67a..0000000000 --- a/tests/phpunit/includes/objectcache/ReplicatedBagOStuffTest.php +++ /dev/null @@ -1,62 +0,0 @@ -writeCache = new HashBagOStuff(); - $this->readCache = new HashBagOStuff(); - $this->cache = new ReplicatedBagOStuff( array( - 'writeFactory' => $this->writeCache, - 'readFactory' => $this->readCache, - ) ); - } - - /** - * @covers ReplicatedBagOStuff::set - */ - public function testSet() { - $key = wfRandomString(); - $value = wfRandomString(); - $this->cache->set( $key, $value ); - - // Write to master. - $this->assertEquals( $this->writeCache->get( $key ), $value ); - // Don't write to slave. Replication is deferred to backend. - $this->assertEquals( $this->readCache->get( $key ), false ); - } - - /** - * @covers ReplicatedBagOStuff::get - */ - public function testGet() { - $key = wfRandomString(); - - $write = wfRandomString(); - $this->writeCache->set( $key, $write ); - $read = wfRandomString(); - $this->readCache->set( $key, $read ); - - // Read from slave. - $this->assertEquals( $this->cache->get( $key ), $read ); - } - - /** - * @covers ReplicatedBagOStuff::get - */ - public function testGetAbsent() { - $key = wfRandomString(); - $value = wfRandomString(); - $this->writeCache->set( $key, $value ); - - // Don't read from master. No failover if value is absent. - $this->assertEquals( $this->cache->get( $key ), false ); - } -} diff --git a/tests/phpunit/includes/objectcache/WANObjectCacheTest.php b/tests/phpunit/includes/objectcache/WANObjectCacheTest.php deleted file mode 100644 index c3702c5945..0000000000 --- a/tests/phpunit/includes/objectcache/WANObjectCacheTest.php +++ /dev/null @@ -1,316 +0,0 @@ -getCliArg( 'use-wanobjectcache' ) ) { - $name = $this->getCliArg( 'use-wanobjectcache' ); - - $this->cache = ObjectCache::getWANInstance( $name ); - } else { - $this->cache = new WANObjectCache( array( - 'cache' => new HashBagOStuff(), - 'pool' => 'testcache-hash', - 'relayer' => new EventRelayerNull( array() ) - ) ); - } - - $wanCache = TestingAccessWrapper::newFromObject( $this->cache ); - $this->internalCache = $wanCache->cache; - } - - /** - * @dataProvider provider_testSetAndGet - * @covers WANObjectCache::set() - * @covers WANObjectCache::get() - * @param mixed $value - * @param integer $ttl - */ - public function testSetAndGet( $value, $ttl ) { - $key = wfRandomString(); - $this->cache->set( $key, $value, $ttl ); - - $curTTL = null; - $this->assertEquals( $value, $this->cache->get( $key, $curTTL ) ); - if ( is_infinite( $ttl ) || $ttl == 0 ) { - $this->assertTrue( is_infinite( $curTTL ), "Current TTL is infinite" ); - } else { - $this->assertGreaterThan( 0, $curTTL, "Current TTL > 0" ); - $this->assertLessThanOrEqual( $ttl, $curTTL, "Current TTL < nominal TTL" ); - } - } - - public static function provider_testSetAndGet() { - return array( - array( 14141, 3 ), - array( 3535.666, 3 ), - array( array(), 3 ), - array( null, 3 ), - array( '0', 3 ), - array( (object)array( 'meow' ), 3 ), - array( INF, 3 ), - array( '', 3 ), - array( 'pizzacat', INF ), - ); - } - - public function testGetNotExists() { - $key = wfRandomString(); - $curTTL = null; - $value = $this->cache->get( $key, $curTTL ); - - $this->assertFalse( $value, "Non-existing key has false value" ); - $this->assertNull( $curTTL, "Non-existing key has null current TTL" ); - } - - public function testSetOver() { - $key = wfRandomString(); - for ( $i = 0; $i < 3; ++$i ) { - $value = wfRandomString(); - $this->cache->set( $key, $value, 3 ); - - $this->assertEquals( $this->cache->get( $key ), $value ); - } - } - - public function testStaleSet() { - $key = wfRandomString(); - $value = wfRandomString(); - $this->cache->set( $key, $value, 3, array( 'since' => microtime( true ) - 30 ) ); - - $this->assertFalse( $this->cache->get( $key ), "Stale set() value ignored" ); - } - - /** - * @covers WANObjectCache::getWithSetCallback() - */ - public function testGetWithSetCallback() { - $cache = $this->cache; - - $key = wfRandomString(); - $value = wfRandomString(); - $cKey1 = wfRandomString(); - $cKey2 = wfRandomString(); - - $wasSet = 0; - $func = function( $old, &$ttl ) use ( &$wasSet, $value ) { - ++$wasSet; - $ttl = 20; // override with another value - return $value; - }; - - $wasSet = 0; - $v = $cache->getWithSetCallback( $key, 30, $func, array( 'lockTSE' => 5 ) ); - $this->assertEquals( $value, $v, "Value returned" ); - $this->assertEquals( 1, $wasSet, "Value regenerated" ); - - $curTTL = null; - $cache->get( $key, $curTTL ); - $this->assertLessThanOrEqual( 20, $curTTL, 'Current TTL between 19-20 (overriden)' ); - $this->assertGreaterThanOrEqual( 19, $curTTL, 'Current TTL between 19-20 (overriden)' ); - - $wasSet = 0; - $v = $cache->getWithSetCallback( $key, 30, $func, array( - 'lowTTL' => 0, - 'lockTSE' => 5, - ) ); - $this->assertEquals( $value, $v, "Value returned" ); - $this->assertEquals( 0, $wasSet, "Value not regenerated" ); - - $priorTime = microtime( true ); - usleep( 1 ); - $wasSet = 0; - $v = $cache->getWithSetCallback( $key, 30, $func, - array( 'checkKeys' => array( $cKey1, $cKey2 ) ) ); - $this->assertEquals( $value, $v, "Value returned" ); - $this->assertEquals( 1, $wasSet, "Value regenerated due to check keys" ); - $t1 = $cache->getCheckKeyTime( $cKey1 ); - $this->assertGreaterThanOrEqual( $priorTime, $t1, 'Check keys generated on miss' ); - $t2 = $cache->getCheckKeyTime( $cKey2 ); - $this->assertGreaterThanOrEqual( $priorTime, $t2, 'Check keys generated on miss' ); - - $priorTime = microtime( true ); - $wasSet = 0; - $v = $cache->getWithSetCallback( $key, 30, $func, - array( 'checkKeys' => array( $cKey1, $cKey2 ) ) ); - $this->assertEquals( $value, $v, "Value returned" ); - $this->assertEquals( 1, $wasSet, "Value regenerated due to still-recent check keys" ); - $t1 = $cache->getCheckKeyTime( $cKey1 ); - $this->assertLessThanOrEqual( $priorTime, $t1, 'Check keys did not change again' ); - $t2 = $cache->getCheckKeyTime( $cKey2 ); - $this->assertLessThanOrEqual( $priorTime, $t2, 'Check keys did not change again' ); - - $curTTL = null; - $v = $cache->get( $key, $curTTL, array( $cKey1, $cKey2 ) ); - $this->assertEquals( $value, $v, "Value returned" ); - $this->assertLessThanOrEqual( 0, $curTTL, "Value has current TTL < 0 due to check keys" ); - - $wasSet = 0; - $key = wfRandomString(); - $v = $cache->getWithSetCallback( $key, 30, $func, array( 'pcTTL' => 5 ) ); - $this->assertEquals( $value, $v, "Value returned" ); - $cache->delete( $key ); - $v = $cache->getWithSetCallback( $key, 30, $func, array( 'pcTTL' => 5 ) ); - $this->assertEquals( $value, $v, "Value still returned after deleted" ); - $this->assertEquals( 1, $wasSet, "Value process cached while deleted" ); - } - - /** - * @covers WANObjectCache::getWithSetCallback() - */ - public function testLockTSE() { - $cache = $this->cache; - $key = wfRandomString(); - $value = wfRandomString(); - - $calls = 0; - $func = function() use ( &$calls, $value ) { - ++$calls; - return $value; - }; - - $cache->delete( $key ); - $ret = $cache->getWithSetCallback( $key, 30, $func, array( 'lockTSE' => 5 ) ); - $this->assertEquals( $value, $ret ); - $this->assertEquals( 1, $calls, 'Value was populated' ); - - // Acquire a lock to verify that getWithSetCallback uses lockTSE properly - $this->internalCache->lock( $key, 0 ); - $ret = $cache->getWithSetCallback( $key, 30, $func, array( 'lockTSE' => 5 ) ); - $this->assertEquals( $value, $ret ); - $this->assertEquals( 1, $calls, 'Callback was not used' ); - } - - /** - * @covers WANObjectCache::getMulti() - */ - public function testGetMulti() { - $cache = $this->cache; - - $value1 = array( 'this' => 'is', 'a' => 'test' ); - $value2 = array( 'this' => 'is', 'another' => 'test' ); - - $key1 = wfRandomString(); - $key2 = wfRandomString(); - $key3 = wfRandomString(); - - $cache->set( $key1, $value1, 5 ); - $cache->set( $key2, $value2, 10 ); - - $curTTLs = array(); - $this->assertEquals( - array( $key1 => $value1, $key2 => $value2 ), - $cache->getMulti( array( $key1, $key2, $key3 ), $curTTLs ) - ); - - $this->assertEquals( 2, count( $curTTLs ), "Two current TTLs in array" ); - $this->assertGreaterThan( 0, $curTTLs[$key1], "Key 1 has current TTL > 0" ); - $this->assertGreaterThan( 0, $curTTLs[$key2], "Key 2 has current TTL > 0" ); - - $cKey1 = wfRandomString(); - $cKey2 = wfRandomString(); - $curTTLs = array(); - $this->assertEquals( - array( $key1 => $value1, $key2 => $value2 ), - $cache->getMulti( array( $key1, $key2, $key3 ), $curTTLs ), - 'Result array populated' - ); - - $priorTime = microtime( true ); - usleep( 1 ); - $curTTLs = array(); - $this->assertEquals( - array( $key1 => $value1, $key2 => $value2 ), - $cache->getMulti( array( $key1, $key2, $key3 ), $curTTLs, array( $cKey1, $cKey2 ) ), - "Result array populated even with new check keys" - ); - $t1 = $cache->getCheckKeyTime( $cKey1 ); - $this->assertGreaterThanOrEqual( $priorTime, $t1, 'Check key 1 generated on miss' ); - $t2 = $cache->getCheckKeyTime( $cKey2 ); - $this->assertGreaterThanOrEqual( $priorTime, $t2, 'Check key 2 generated on miss' ); - $this->assertEquals( 2, count( $curTTLs ), "Current TTLs array set" ); - $this->assertLessThanOrEqual( 0, $curTTLs[$key1], 'Key 1 has current TTL <= 0' ); - $this->assertLessThanOrEqual( 0, $curTTLs[$key2], 'Key 2 has current TTL <= 0' ); - - usleep( 1 ); - $curTTLs = array(); - $this->assertEquals( - array( $key1 => $value1, $key2 => $value2 ), - $cache->getMulti( array( $key1, $key2, $key3 ), $curTTLs, array( $cKey1, $cKey2 ) ), - "Result array still populated even with new check keys" - ); - $this->assertEquals( 2, count( $curTTLs ), "Current TTLs still array set" ); - $this->assertLessThan( 0, $curTTLs[$key1], 'Key 1 has negative current TTL' ); - $this->assertLessThan( 0, $curTTLs[$key2], 'Key 2 has negative current TTL' ); - } - - /** - * @covers WANObjectCache::delete() - */ - public function testDelete() { - $key = wfRandomString(); - $value = wfRandomString(); - $this->cache->set( $key, $value ); - - $curTTL = null; - $v = $this->cache->get( $key, $curTTL ); - $this->assertEquals( $value, $v, "Key was created with value" ); - $this->assertGreaterThan( 0, $curTTL, "Existing key has current TTL > 0" ); - - $this->cache->delete( $key ); - - $curTTL = null; - $v = $this->cache->get( $key, $curTTL ); - $this->assertFalse( $v, "Deleted key has false value" ); - $this->assertLessThan( 0, $curTTL, "Deleted key has current TTL < 0" ); - - $this->cache->set( $key, $value . 'more' ); - $this->assertFalse( $v, "Deleted key is tombstoned and has false value" ); - $this->assertLessThan( 0, $curTTL, "Deleted key is tombstoned and has current TTL < 0" ); - } - - /** - * @covers WANObjectCache::touchCheckKey() - * @covers WANObjectCache::resetCheckKey() - * @covers WANObjectCache::getCheckKeyTime() - */ - public function testTouchKeys() { - $key = wfRandomString(); - - $priorTime = microtime( true ); - usleep( 100 ); - $t0 = $this->cache->getCheckKeyTime( $key ); - $this->assertGreaterThanOrEqual( $priorTime, $t0, 'Check key auto-created' ); - - $priorTime = microtime( true ); - usleep( 100 ); - $this->cache->touchCheckKey( $key ); - $t1 = $this->cache->getCheckKeyTime( $key ); - $this->assertGreaterThanOrEqual( $priorTime, $t1, 'Check key created' ); - - $t2 = $this->cache->getCheckKeyTime( $key ); - $this->assertEquals( $t1, $t2, 'Check key time did not change' ); - - usleep( 100 ); - $this->cache->touchCheckKey( $key ); - $t3 = $this->cache->getCheckKeyTime( $key ); - $this->assertGreaterThan( $t2, $t3, 'Check key time increased' ); - - $t4 = $this->cache->getCheckKeyTime( $key ); - $this->assertEquals( $t3, $t4, 'Check key time did not change' ); - - usleep( 100 ); - $this->cache->resetCheckKey( $key ); - $t5 = $this->cache->getCheckKeyTime( $key ); - $this->assertGreaterThan( $t4, $t5, 'Check key time increased' ); - - $t6 = $this->cache->getCheckKeyTime( $key ); - $this->assertEquals( $t5, $t6, 'Check key time did not change' ); - } -}