Also moved related tests files to /libs.
Change-Id: I806eeaa30205733d497adde933baf0c4157f7aae
'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',
--- /dev/null
+<?php
+/**
+ * Wrapper for object caching in different caches.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Cache
+ */
+
+/**
+ * A cache class that replicates all writes to multiple child caches. Reads
+ * are implemented by reading from the caches in the order they are given in
+ * the configuration until a cache gives a positive result.
+ *
+ * @ingroup Cache
+ */
+class MultiWriteBagOStuff extends BagOStuff {
+ /** @var BagOStuff[] */
+ protected $caches;
+ /** @var bool Use async secondary writes */
+ protected $asyncWrites = false;
+ /** @var callback|null */
+ protected $asyncHandler;
+
+ /** Idiom for "write to all backends" */
+ const ALL = INF;
+
+ const UPGRADE_TTL = 3600; // TTL when a key is copied to a higher cache tier
+
+ /**
+ * $params include:
+ * - caches: A numbered array of either ObjectFactory::getObjectFromSpec
+ * arrays yeilding BagOStuff objects or direct BagOStuff objects.
+ * If using the former, the 'args' field *must* be set.
+ * The first cache is the primary one, being the first to
+ * be read in the fallback chain. Writes happen to all stores
+ * in the order they are defined. However, lock()/unlock() calls
+ * only use the primary store.
+ * - replication: Either 'sync' or 'async'. This controls whether writes
+ * to secondary stores are deferred when possible. Async writes
+ * require setting 'asyncCallback'. HHVM register_postsend_function() function.
+ * Async writes can increase the chance of some race conditions
+ * or cause keys to expire seconds later than expected. It is
+ * safe to use for modules when cached values: are immutable,
+ * invalidation uses logical TTLs, invalidation uses etag/timestamp
+ * validation against the DB, or merge() is used to handle races.
+ * - asyncHandler: callable that takes a callback and runs it after the
+ * current web request ends. In CLI mode, it should run it immediately.
+ * @param array $params
+ * @throws InvalidArgumentException
+ */
+ public function __construct( $params ) {
+ parent::__construct( $params );
+
+ if ( empty( $params['caches'] ) || !is_array( $params['caches'] ) ) {
+ throw new InvalidArgumentException(
+ __METHOD__ . ': "caches" parameter must be an array of caches'
+ );
+ }
+
+ $this->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;
+ }
+}
+++ /dev/null
-<?php
-/**
- * Wrapper for object caching in different caches.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- * @ingroup Cache
- */
-
-/**
- * A cache class that replicates all writes to multiple child caches. Reads
- * are implemented by reading from the caches in the order they are given in
- * the configuration until a cache gives a positive result.
- *
- * @ingroup Cache
- */
-class MultiWriteBagOStuff extends BagOStuff {
- /** @var BagOStuff[] */
- protected $caches;
- /** @var bool Use async secondary writes */
- protected $asyncWrites = false;
- /** @var callback|null */
- protected $asyncHandler;
-
- /** Idiom for "write to all backends" */
- const ALL = INF;
-
- const UPGRADE_TTL = 3600; // TTL when a key is copied to a higher cache tier
-
- /**
- * $params include:
- * - caches: A numbered array of either ObjectFactory::getObjectFromSpec
- * arrays yeilding BagOStuff objects or direct BagOStuff objects.
- * If using the former, the 'args' field *must* be set.
- * The first cache is the primary one, being the first to
- * be read in the fallback chain. Writes happen to all stores
- * in the order they are defined. However, lock()/unlock() calls
- * only use the primary store.
- * - replication: Either 'sync' or 'async'. This controls whether writes
- * to secondary stores are deferred when possible. Async writes
- * require setting 'asyncCallback'. HHVM register_postsend_function() function.
- * Async writes can increase the chance of some race conditions
- * or cause keys to expire seconds later than expected. It is
- * safe to use for modules when cached values: are immutable,
- * invalidation uses logical TTLs, invalidation uses etag/timestamp
- * validation against the DB, or merge() is used to handle races.
- * - asyncHandler: callable that takes a callback and runs it after the
- * current web request ends. In CLI mode, it should run it immediately.
- * @param array $params
- * @throws InvalidArgumentException
- */
- public function __construct( $params ) {
- parent::__construct( $params );
-
- if ( empty( $params['caches'] ) || !is_array( $params['caches'] ) ) {
- throw new InvalidArgumentException(
- __METHOD__ . ': "caches" parameter must be an array of caches'
- );
- }
-
- $this->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;
- }
-}
--- /dev/null
+<?php
+/**
+ * @author Matthias Mullie <mmullie@wikimedia.org>
+ * @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' );
+ }
+}
--- /dev/null
+<?php
+
+/**
+ * @group Database
+ */
+class MultiWriteBagOStuffTest extends MediaWikiTestCase {
+ /** @var HashBagOStuff */
+ private $cache1;
+ /** @var HashBagOStuff */
+ private $cache2;
+ /** @var MultiWriteBagOStuff */
+ private $cache;
+
+ protected function setUp() {
+ parent::setUp();
+
+ $this->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' );
+ }
+}
--- /dev/null
+<?php
+
+class ReplicatedBagOStuffTest extends MediaWikiTestCase {
+ /** @var HashBagOStuff */
+ private $writeCache;
+ /** @var HashBagOStuff */
+ private $readCache;
+ /** @var ReplicatedBagOStuff */
+ private $cache;
+
+ protected function setUp() {
+ parent::setUp();
+
+ $this->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 );
+ }
+}
--- /dev/null
+<?php
+
+class WANObjectCacheTest extends MediaWikiTestCase {
+ /** @var WANObjectCache */
+ private $cache;
+ /**@var BagOStuff */
+ private $internalCache;
+
+ protected function setUp() {
+ parent::setUp();
+
+ if ( $this->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' );
+ }
+}
+++ /dev/null
-<?php
-/**
- * @author Matthias Mullie <mmullie@wikimedia.org>
- * @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' );
- }
-}
+++ /dev/null
-<?php
-
-/**
- * @group Database
- */
-class MultiWriteBagOStuffTest extends MediaWikiTestCase {
- /** @var HashBagOStuff */
- private $cache1;
- /** @var HashBagOStuff */
- private $cache2;
- /** @var MultiWriteBagOStuff */
- private $cache;
-
- protected function setUp() {
- parent::setUp();
-
- $this->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' );
- }
-}
+++ /dev/null
-<?php
-
-class ReplicatedBagOStuffTest extends MediaWikiTestCase {
- /** @var HashBagOStuff */
- private $writeCache;
- /** @var HashBagOStuff */
- private $readCache;
- /** @var ReplicatedBagOStuff */
- private $cache;
-
- protected function setUp() {
- parent::setUp();
-
- $this->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 );
- }
-}
+++ /dev/null
-<?php
-
-class WANObjectCacheTest extends MediaWikiTestCase {
- /** @var WANObjectCache */
- private $cache;
- /**@var BagOStuff */
- private $internalCache;
-
- protected function setUp() {
- parent::setUp();
-
- if ( $this->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' );
- }
-}