/** @var BagOStuff */
private $cache;
+ const TEST_KEY = 'test';
+
protected function setUp() {
parent::setUp();
$this->cache = new HashBagOStuff;
}
- $this->cache->delete( wfMemcKey( 'test' ) );
+ $this->cache->delete( $this->cache->makeKey( self::TEST_KEY ) );
+ $this->cache->delete( $this->cache->makeKey( self::TEST_KEY ) . ':lock' );
}
/**
/**
* @covers BagOStuff::merge
* @covers BagOStuff::mergeViaLock
+ * @covers BagOStuff::mergeViaCas
*/
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 );
+ $key = $this->cache->makeKey( self::TEST_KEY );
+ $locks = false;
+ $checkLockingCallback = function ( BagOStuff $cache, $key, $oldVal ) use ( &$locks ) {
+ $locks = $cache->get( "$key:lock" );
+
+ return false;
+ };
+
+ $this->cache->merge( $key, $checkLockingCallback, 5 );
+ $this->assertFalse( $this->cache->get( $key ) );
- if ( $existingValue === false ) {
- return 'merged';
+ $calls = 0;
+ $casRace = false; // emulate a race
+ $callback = function ( BagOStuff $cache, $key, $oldVal ) use ( &$calls, &$casRace ) {
+ ++$calls;
+ if ( $casRace ) {
+ // Uses CAS instead?
+ $cache->set( $key, 'conflict', 5 );
}
- return $existingValue . 'merged';
+ return ( $oldVal === false ) ? 'merged' : $oldVal . 'merged';
};
// merge on non-existing value
- $merged = $this->cache->merge( $key, $callback, 0 );
+ $merged = $this->cache->merge( $key, $callback, 5 );
$this->assertTrue( $merged );
- $this->assertEquals( $this->cache->get( $key ), 'merged' );
+ $this->assertEquals( 'merged', $this->cache->get( $key ) );
// merge on existing value
- $merged = $this->cache->merge( $key, $callback, 0 );
+ $merged = $this->cache->merge( $key, $callback, 5 );
$this->assertTrue( $merged );
- $this->assertEquals( $this->cache->get( $key ), 'mergedmerged' );
+ $this->assertEquals( 'mergedmerged', $this->cache->get( $key ) );
+
+ $calls = 0;
+ if ( $locks ) {
+ // merge were something else already was merging (e.g. had the lock)
+ $this->cache->lock( $key );
+ $this->assertFalse(
+ $this->cache->merge( $key, $callback, 5, 1 ),
+ 'Non-blocking merge (locking)'
+ );
+ $this->cache->unlock( $key );
+ $this->assertEquals( 0, $calls );
+ } else {
+ $casRace = true;
+ $this->assertFalse(
+ $this->cache->merge( $key, $callback, 5, 1 ),
+ 'Non-blocking merge (CAS)'
+ );
+ $this->assertEquals( 1, $calls );
+ }
+ }
+
+ /**
+ * @covers BagOStuff::merge
+ * @covers BagOStuff::mergeViaLock
+ * @dataProvider provideTestMerge_fork
+ */
+ public function testMerge_fork( $exists, $winsLocking, $resLocking, $resCAS ) {
+ $key = $this->cache->makeKey( self::TEST_KEY );
+ $pCallback = function ( BagOStuff $cache, $key, $oldVal ) {
+ return ( $oldVal === false ) ? 'init-parent' : $oldVal . '-merged-parent';
+ };
+ $cCallback = function ( BagOStuff $cache, $key, $oldVal ) {
+ return ( $oldVal === false ) ? 'init-child' : $oldVal . '-merged-child';
+ };
+
+ if ( $exists ) {
+ $this->cache->set( $key, 'x', 5 );
+ }
/*
* Test concurrent merges by forking this process, if:
$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 );
+ $pid = null;
+ $locked = false;
+ // Function to start merge(), run another merge() midway through, then finish
+ $func = function ( BagOStuff $cache, $key, $cur )
+ use ( $pCallback, $cCallback, &$pid, &$locked )
+ {
+ $pid = pcntl_fork();
+ if ( $pid == -1 ) {
+ return false;
+ } elseif ( $pid ) {
+ $locked = $cache->get( "$key:lock" ); // parent has lock?
+ pcntl_wait( $status );
+
+ return $pCallback( $cache, $key, $cur );
+ } else {
+ $this->cache->merge( $key, $cCallback, 0, 1 );
+ // Bail out of the outer merge() in the child process since it does not
+ // need to attempt to write anything. Success is checked by the parent.
+ parent::tearDown(); // avoid phpunit notices
+ exit;
+ }
+ };
+
+ // attempt a merge - this should fail
+ $merged = $this->cache->merge( $key, $func, 0, 1 );
- // 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 );
+ if ( $pid == -1 ) {
+ return; // can't fork, ignore this test...
+ }
- // make sure the child's merge is completed and verify
- usleep( 3000 );
- $this->assertEquals( $this->cache->get( $key ), 'mergedmergedmerged' );
+ if ( $locked ) {
+ // merge succeed since child was locked out
+ $this->assertEquals( $winsLocking, $merged );
+ $this->assertEquals( $this->cache->get( $key ), $resLocking );
} 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;
+ // merge has failed because child process was merging (and we only attempted once)
+ $this->assertEquals( !$winsLocking, $merged );
+ $this->assertEquals( $this->cache->get( $key ), $resCAS );
}
+ } else {
+ $this->markTestSkipped( 'No pcntl methods available' );
}
}
+ function provideTestMerge_fork() {
+ return [
+ // (already exists, parent wins if locking, result if locking, result if CAS)
+ [ false, true, 'init-parent', 'init-child' ],
+ [ true, true, 'x-merged-parent', 'x-merged-child' ]
+ ];
+ }
+
/**
* @covers BagOStuff::changeTTL
*/
public function testChangeTTL() {
- $key = wfMemcKey( 'test' );
+ $key = $this->cache->makeKey( self::TEST_KEY );
$value = 'meow';
- $this->cache->add( $key, $value );
+ $this->cache->add( $key, $value, 5 );
$this->assertTrue( $this->cache->changeTTL( $key, 5 ) );
$this->assertEquals( $this->cache->get( $key ), $value );
$this->cache->delete( $key );
* @covers BagOStuff::add
*/
public function testAdd() {
- $key = wfMemcKey( 'test' );
- $this->assertTrue( $this->cache->add( $key, 'test' ) );
+ $key = $this->cache->makeKey( self::TEST_KEY );
+ $this->assertTrue( $this->cache->add( $key, 'test', 5 ) );
}
/**
public function testGet() {
$value = [ 'this' => 'is', 'a' => 'test' ];
- $key = wfMemcKey( 'test' );
- $this->cache->add( $key, $value );
+ $key = $this->cache->makeKey( self::TEST_KEY );
+ $this->cache->add( $key, $value, 5 );
$this->assertEquals( $this->cache->get( $key ), $value );
}
/**
+ * @covers BagOStuff::get
+ * @covers BagOStuff::set
* @covers BagOStuff::getWithSetCallback
*/
public function testGetWithSetCallback() {
- $key = wfMemcKey( 'test' );
+ $key = $this->cache->makeKey( self::TEST_KEY );
$value = $this->cache->getWithSetCallback(
$key,
30,
* @covers BagOStuff::incr
*/
public function testIncr() {
- $key = wfMemcKey( 'test' );
- $this->cache->add( $key, 0 );
+ $key = $this->cache->makeKey( self::TEST_KEY );
+ $this->cache->add( $key, 0, 5 );
$this->cache->incr( $key );
$expectedValue = 1;
$actualValue = $this->cache->get( $key );
* @covers BagOStuff::incrWithInit
*/
public function testIncrWithInit() {
- $key = wfMemcKey( 'test' );
+ $key = $this->cache->makeKey( self::TEST_KEY );
$val = $this->cache->incrWithInit( $key, 0, 1, 3 );
$this->assertEquals( 3, $val, "Correct init value" );
$value3 = [ 'testing a key that may be encoded when sent to cache backend' ];
$value4 = [ 'another test where chars in key will be encoded' ];
- $key1 = wfMemcKey( 'test1' );
- $key2 = wfMemcKey( 'test2' );
+ $key1 = $this->cache->makeKey( 'test-1' );
+ $key2 = $this->cache->makeKey( 'test-2' );
// internally, MemcachedBagOStuffs will encode to will-%25-encode
- $key3 = wfMemcKey( 'will-%-encode' );
- $key4 = wfMemcKey(
+ $key3 = $this->cache->makeKey( 'will-%-encode' );
+ $key4 = $this->cache->makeKey(
'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 );
+ // cleanup
+ $this->cache->delete( $key1 );
+ $this->cache->delete( $key2 );
+ $this->cache->delete( $key3 );
+ $this->cache->delete( $key4 );
+
+ $this->cache->add( $key1, $value1, 5 );
+ $this->cache->add( $key2, $value2, 5 );
+ $this->cache->add( $key3, $value3, 5 );
+ $this->cache->add( $key4, $value4, 5 );
$this->assertEquals(
[ $key1 => $value1, $key2 => $value2, $key3 => $value3, $key4 => $value4 ],
$this->cache->delete( $key4 );
}
+ /**
+ * @covers BagOStuff::setMulti
+ * @covers BagOStuff::deleteMulti
+ */
+ public function testSetDeleteMulti() {
+ $map = [
+ $this->cache->makeKey( 'test-1' ) => 'Siberian',
+ $this->cache->makeKey( 'test-2' ) => [ 'Huskies' ],
+ $this->cache->makeKey( 'test-3' ) => [ 'are' => 'the' ],
+ $this->cache->makeKey( 'test-4' ) => (object)[ 'greatest' => 'animal' ],
+ $this->cache->makeKey( 'test-5' ) => 4,
+ $this->cache->makeKey( 'test-6' ) => 'ever'
+ ];
+
+ $this->cache->setMulti( $map, 5 );
+ $this->assertEquals(
+ $map,
+ $this->cache->getMulti( array_keys( $map ) )
+ );
+
+ $this->assertTrue( $this->cache->deleteMulti( array_keys( $map ), 5 ) );
+
+ $this->assertEquals(
+ [],
+ $this->cache->getMulti( array_keys( $map ) )
+ );
+ }
+
/**
* @covers BagOStuff::getScopedLock
*/
public function testGetScopedLock() {
- $key = wfMemcKey( 'test' );
+ $key = $this->cache->makeKey( self::TEST_KEY );
$value1 = $this->cache->getScopedLock( $key, 0 );
$value2 = $this->cache->getScopedLock( $key, 0 );
DeferredUpdates::doUpdates();
}
+
+ /**
+ * @covers BagOStuff::lock()
+ * @covers BagOStuff::unlock()
+ */
+ public function testLocking() {
+ $key = 'test';
+ $this->assertTrue( $this->cache->lock( $key ) );
+ $this->assertFalse( $this->cache->lock( $key ) );
+ $this->assertTrue( $this->cache->unlock( $key ) );
+
+ $key2 = 'test2';
+ $this->assertTrue( $this->cache->lock( $key2, 5, 5, 'rclass' ) );
+ $this->assertTrue( $this->cache->lock( $key2, 5, 5, 'rclass' ) );
+ $this->assertTrue( $this->cache->unlock( $key2 ) );
+ $this->assertTrue( $this->cache->unlock( $key2 ) );
+ }
}