X-Git-Url: http://git.heureux-cyclage.org/?p=lhc%2Fweb%2Fwiklou.git;a=blobdiff_plain;f=tests%2Fphpunit%2Fincludes%2FStorage%2FNameTableStoreTest.php;h=ff106ba1e1e4305171f59d991643381159bdaeff;hp=7a4ea2dd6113ff71a0663273b905308c59b42d7f;hb=56f171b9586f56bd4f9eb4b0bd25859d56561c1e;hpb=1c7a8c1d25880c9fba972e4b019341fdbffff9f8 diff --git a/tests/phpunit/includes/Storage/NameTableStoreTest.php b/tests/phpunit/includes/Storage/NameTableStoreTest.php index 7a4ea2dd61..ff106ba1e1 100644 --- a/tests/phpunit/includes/Storage/NameTableStoreTest.php +++ b/tests/phpunit/includes/Storage/NameTableStoreTest.php @@ -5,10 +5,13 @@ namespace MediaWiki\Tests\Storage; use BagOStuff; use EmptyBagOStuff; use HashBagOStuff; +use MediaWiki\MediaWikiServices; use MediaWiki\Storage\NameTableAccessException; use MediaWiki\Storage\NameTableStore; use MediaWikiTestCase; +use PHPUnit\Framework\MockObject\MockObject; use Psr\Log\NullLogger; +use RuntimeException; use WANObjectCache; use Wikimedia\Rdbms\IDatabase; use Wikimedia\Rdbms\LoadBalancer; @@ -59,7 +62,13 @@ class NameTableStoreTest extends MediaWikiTestCase { return $mock; } - private function getCallCheckingDb( $insertCalls, $selectCalls ) { + /** + * @param null $insertCalls + * @param null $selectCalls + * + * @return MockObject|IDatabase + */ + private function getProxyDb( $insertCalls = null, $selectCalls = null ) { $proxiedMethods = [ 'select' => $selectCalls, 'insert' => $insertCalls, @@ -67,7 +76,12 @@ class NameTableStoreTest extends MediaWikiTestCase { 'insertId' => null, 'getSessionLagStatus' => null, 'writesPending' => null, - 'onTransactionPreCommitOrIdle' => null + 'onTransactionPreCommitOrIdle' => null, + 'onAtomicSectionCancel' => null, + 'doAtomicSection' => null, + 'begin' => null, + 'rollback' => null, + 'commit' => null, ]; $mock = $this->getMockBuilder( IDatabase::class ) ->disableOriginalConstructor() @@ -90,7 +104,7 @@ class NameTableStoreTest extends MediaWikiTestCase { $insertCallback = null ) { return new NameTableStore( - $this->getMockLoadBalancer( $this->getCallCheckingDb( $insertCalls, $selectCalls ) ), + $this->getMockLoadBalancer( $this->getProxyDb( $insertCalls, $selectCalls ) ), $this->getHashWANObjectCache( $cacheBag ), new NullLogger(), 'slot_roles', 'role_id', 'role_name', @@ -344,4 +358,150 @@ class NameTableStoreTest extends MediaWikiTestCase { $this->assertSame( 7251, $store->acquireId( 'A' ) ); } + public function testTransactionRollback() { + $lb = MediaWikiServices::getInstance()->getDBLoadBalancer(); + + // Two instances hitting the real database using separate caches. + $store1 = new NameTableStore( + $lb, + $this->getHashWANObjectCache( new HashBagOStuff() ), + new NullLogger(), + 'slot_roles', 'role_id', 'role_name' + ); + $store2 = new NameTableStore( + $lb, + $this->getHashWANObjectCache( new HashBagOStuff() ), + new NullLogger(), + 'slot_roles', 'role_id', 'role_name' + ); + + $this->db->begin( __METHOD__ ); + $fooId = $store1->acquireId( 'foo' ); + $this->db->rollback( __METHOD__ ); + + $this->assertSame( $fooId, $store2->getId( 'foo' ) ); + $this->assertSame( $fooId, $store1->getId( 'foo' ) ); + } + + public function testTransactionRollbackWithFailedRedo() { + $insertCalls = 0; + + $db = $this->getProxyDb( 2 ); + $db->method( 'insert' ) + ->willReturnCallback( function () use ( &$insertCalls, $db ) { + $insertCalls++; + switch ( $insertCalls ) { + case 1: + return true; + case 2: + throw new RuntimeException( 'Testing' ); + } + + return true; + } ); + + $lb = $this->getMockBuilder( LoadBalancer::class ) + ->disableOriginalConstructor() + ->getMock(); + $lb->method( 'getConnectionRef' ) + ->willReturn( $db ); + $lb->method( 'resolveDomainID' ) + ->willReturnArgument( 0 ); + + // Two instances hitting the real database using separate caches. + $store1 = new NameTableStore( + $lb, + $this->getHashWANObjectCache( new HashBagOStuff() ), + new NullLogger(), + 'slot_roles', 'role_id', 'role_name' + ); + + $this->db->begin( __METHOD__ ); + $store1->acquireId( 'foo' ); + $this->db->rollback( __METHOD__ ); + + $this->assertArrayNotHasKey( 'foo', $store1->getMap() ); + } + + public function testTransactionRollbackWithInterference() { + $lb = MediaWikiServices::getInstance()->getDBLoadBalancer(); + + // Two instances hitting the real database using separate caches. + $store1 = new NameTableStore( + $lb, + $this->getHashWANObjectCache( new HashBagOStuff() ), + new NullLogger(), + 'slot_roles', 'role_id', 'role_name' + ); + $store2 = new NameTableStore( + $lb, + $this->getHashWANObjectCache( new HashBagOStuff() ), + new NullLogger(), + 'slot_roles', 'role_id', 'role_name' + ); + + $this->db->begin( __METHOD__ ); + + $quuxId = null; + $this->db->onTransactionResolution( + function () use ( $store1, &$quuxId ) { + $quuxId = $store1->acquireId( 'quux' ); + } + ); + + $store1->acquireId( 'foo' ); + $this->db->rollback( __METHOD__ ); + + // $store2 should know about the insert by $store1 + $this->assertSame( $quuxId, $store2->getId( 'quux' ) ); + + // A "best effort" attempt was made to restore the entry for 'foo' + // after the transaction failed. This may succeed on some databases like MySQL, + // while it fails on others. Since we are giving no guarantee about this, + // the only thing we can test here is that acquireId( 'foo' ) returns an + // ID that is distinct from the ID of quux (but might be different from the + // value returned by the original call to acquireId( 'foo' ). + // Note that $store2 will not know about the ID for 'foo' acquired by $store1, + // because it's using a separate cache, and getId() does not fall back to + // checking the database. + $this->assertNotSame( $quuxId, $store1->acquireId( 'foo' ) ); + } + + public function testTransactionDoubleRollback() { + $fname = __METHOD__; + + $lb = MediaWikiServices::getInstance()->getDBLoadBalancer(); + $store = new NameTableStore( + $lb, + $this->getHashWANObjectCache( new HashBagOStuff() ), + new NullLogger(), + 'slot_roles', 'role_id', 'role_name' + ); + + // Nested atomic sections + $atomic1 = $this->db->startAtomic( $fname, $this->db::ATOMIC_CANCELABLE ); + $atomic2 = $this->db->startAtomic( $fname, $this->db::ATOMIC_CANCELABLE ); + + // Acquire ID + $id = $store->acquireId( 'foo' ); + + // Oops, rolled back + $this->db->cancelAtomic( $fname, $atomic2 ); + + // Should have been re-inserted + $store->reloadMap(); + $this->assertSame( $id, $store->getId( 'foo' ) ); + + // Oops, re-insert was rolled back too. + $this->db->cancelAtomic( $fname, $atomic1 ); + + // This time, no re-insertion happened. + try { + $id2 = $store->getId( 'foo' ); + $this->fail( "Expected NameTableAccessException, got $id2 (originally was $id)" ); + } catch ( NameTableAccessException $ex ) { + // expected + } + } + }