+ 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
+ }
+ }
+