wiki.techinc.nl/tests/phpunit/includes/deferred/DeferredUpdatesTest.php
Aaron Schulz 4b7ddd3c67 deferred: make DeferredUpdates::doUpdates() recursion more uniform
Follow-up ba6490aa1e.

That patch worked around $executeContext iteration errors due to the
lack of recursion support in doUpdates()/handleUpdateQueue(). Errors
were triggered by MediaWiki::schedulePostSendJobs() enqueueing deferred
updates that invoke JobRunner::run(), which, in turn, invoke doUpdates()
for each job via JobRunner:: doExecuteJob(). The doUpdates() method was
changed to operate on the sub-queue for the in-progress DeferrableUpdate.

Further improve the recursion logic:
* Use classes for the scope stack and scope queues.
* Put *all* updates added during a DeferrableUpdate::doUpdate() call into
  the subqueue, regardless of "defer until" stage or whether it implements
  MergeableUpdate. Now, doUpdate() can run *everything* it enqueued instead
  of a non-obvious subset. Note that the TransactionRoundDefiningUpdate
  that invokes JobRunner was already a POSTSEND update before ba6490aa1eb;
  the only effect of this change is that MergeableUpdate instances from jobs
  will once again run in doExecuteJob().
* Make recursive DeferredUpdates::doUpdate() calls error out immediately
  unless the DeferrableUpdate responsible is a TransactionRoundAwareUpdate
  with the TRX_ROUND_ABSENT flag. This covers the schedulePostSendJobs()
  scenario and only prohibits insane call patterns. Failing early avoids
  the risk of handleUpdateQueue() dropping all the updates due to the same
  DBTransactionError error in DeferredUpdates::attemptUpdate().
* Avoid recursion loop in tryOpportunisticExecute() when JobQueueDB is in
  use and a large number of tasks are pending. This happened due to methods
  like onTransactionPreCommitOrIdle() being used within JobQueueDB.

Mark DeferredUpdates::doUpdates()/tryOpportunisticExecute() as @internal
and create a Maintenance::shutdown() method to avoid a direct call in the
doMaintenance.php file.

Bug: T249069
Bug: T268840
Change-Id: Ib369f0e74243a48ababdb9cd83b155c9a0f5e741
2021-01-14 15:37:58 -08:00

484 lines
14 KiB
PHP

<?php
use MediaWiki\MediaWikiServices;
class DeferredUpdatesTest extends MediaWikiIntegrationTestCase {
/**
* @covers DeferredUpdates::addUpdate
* @covers DeferredUpdates::doUpdates
* @covers DeferredUpdates::attemptUpdate
* @covers DeferredUpdatesScopeStack
* @covers DeferredUpdatesScope
*/
public function testAddAndRun() {
$update = $this->getMockBuilder( DeferrableUpdate::class )
->setMethods( [ 'doUpdate' ] )->getMock();
$update->expects( $this->once() )->method( 'doUpdate' );
DeferredUpdates::addUpdate( $update );
DeferredUpdates::doUpdates();
}
/**
* @covers DeferredUpdates::addUpdate
* @covers DeferredUpdatesScopeStack
* @covers DeferredUpdatesScope
*/
public function testAddMergeable() {
$this->setMwGlobals( 'wgCommandLineMode', false );
$update1 = $this->getMockBuilder( MergeableUpdate::class )
->setMethods( [ 'merge', 'doUpdate' ] )->getMock();
$update1->expects( $this->once() )->method( 'merge' );
$update1->expects( $this->never() )->method( 'doUpdate' );
$update2 = $this->getMockBuilder( MergeableUpdate::class )
->setMethods( [ 'merge', 'doUpdate' ] )->getMock();
$update2->expects( $this->never() )->method( 'merge' );
$update2->expects( $this->never() )->method( 'doUpdate' );
DeferredUpdates::addUpdate( $update1 );
DeferredUpdates::addUpdate( $update2 );
}
/**
* @covers DeferredUpdates::addCallableUpdate
* @covers MWCallableUpdate::getOrigin
*/
public function testAddCallableUpdate() {
$this->setMwGlobals( 'wgCommandLineMode', true );
$ran = 0;
DeferredUpdates::addCallableUpdate( function () use ( &$ran ) {
$ran++;
} );
DeferredUpdates::doUpdates();
$this->assertSame( 1, $ran, 'Update ran' );
}
/**
* @covers DeferredUpdates::getPendingUpdates
* @covers DeferredUpdates::clearPendingUpdates
*/
public function testGetPendingUpdates() {
// Prevent updates from running
$this->setMwGlobals( 'wgCommandLineMode', false );
$pre = DeferredUpdates::PRESEND;
$post = DeferredUpdates::POSTSEND;
$all = DeferredUpdates::ALL;
$update = $this->createMock( DeferrableUpdate::class );
$update->expects( $this->never() )
->method( 'doUpdate' );
DeferredUpdates::addUpdate( $update, $pre );
$this->assertCount( 1, DeferredUpdates::getPendingUpdates( $pre ) );
$this->assertSame( [], DeferredUpdates::getPendingUpdates( $post ) );
$this->assertCount( 1, DeferredUpdates::getPendingUpdates( $all ) );
$this->assertCount( 1, DeferredUpdates::getPendingUpdates() );
DeferredUpdates::clearPendingUpdates();
$this->assertSame( [], DeferredUpdates::getPendingUpdates() );
DeferredUpdates::addUpdate( $update, $post );
$this->assertSame( [], DeferredUpdates::getPendingUpdates( $pre ) );
$this->assertCount( 1, DeferredUpdates::getPendingUpdates( $post ) );
$this->assertCount( 1, DeferredUpdates::getPendingUpdates( $all ) );
$this->assertCount( 1, DeferredUpdates::getPendingUpdates() );
DeferredUpdates::clearPendingUpdates();
$this->assertSame( [], DeferredUpdates::getPendingUpdates() );
}
/**
* @covers DeferredUpdates::doUpdates
* @covers DeferredUpdates::addUpdate
* @covers DeferredUpdatesScopeStack
* @covers DeferredUpdatesScope
*/
public function testDoUpdatesWeb() {
$this->setMwGlobals( 'wgCommandLineMode', false );
$updates = [
'1' => "deferred update 1;\n",
'2' => "deferred update 2;\n",
'2-1' => "deferred update 1 within deferred update 2;\n",
'2-2' => "deferred update 2 within deferred update 2;\n",
'3' => "deferred update 3;\n",
'3-1' => "deferred update 1 within deferred update 3;\n",
'3-2' => "deferred update 2 within deferred update 3;\n",
'3-1-1' => "deferred update 1 within deferred update 1 within deferred update 3;\n",
'3-2-1' => "deferred update 1 within deferred update 2 with deferred update 3;\n",
];
DeferredUpdates::addCallableUpdate(
function () use ( $updates ) {
echo $updates['1'];
}
);
DeferredUpdates::addCallableUpdate(
function () use ( $updates ) {
echo $updates['2'];
DeferredUpdates::addCallableUpdate(
function () use ( $updates ) {
echo $updates['2-1'];
}
);
DeferredUpdates::addCallableUpdate(
function () use ( $updates ) {
echo $updates['2-2'];
}
);
}
);
DeferredUpdates::addCallableUpdate(
function () use ( $updates ) {
echo $updates['3'];
DeferredUpdates::addCallableUpdate(
function () use ( $updates ) {
echo $updates['3-1'];
DeferredUpdates::addCallableUpdate(
function () use ( $updates ) {
echo $updates['3-1-1'];
}
);
}
);
DeferredUpdates::addCallableUpdate(
function () use ( $updates ) {
echo $updates['3-2'];
DeferredUpdates::addCallableUpdate(
function () use ( $updates ) {
echo $updates['3-2-1'];
}
);
}
);
}
);
$this->assertEquals( 3, DeferredUpdates::pendingUpdatesCount() );
$this->expectOutputString( implode( '', $updates ) );
DeferredUpdates::doUpdates();
$x = null;
$y = null;
DeferredUpdates::addCallableUpdate(
function () use ( &$x ) {
$x = 'Sherity';
},
DeferredUpdates::PRESEND
);
DeferredUpdates::addCallableUpdate(
function () use ( &$y ) {
$y = 'Marychu';
},
DeferredUpdates::POSTSEND
);
$this->assertNull( $x, "Update not run yet" );
$this->assertNull( $y, "Update not run yet" );
DeferredUpdates::doUpdates( 'run', DeferredUpdates::PRESEND );
$this->assertEquals( "Sherity", $x, "PRESEND update ran" );
$this->assertNull( $y, "POSTSEND update not run yet" );
DeferredUpdates::doUpdates( 'run', DeferredUpdates::POSTSEND );
$this->assertEquals( "Marychu", $y, "POSTSEND update ran" );
}
/**
* @covers DeferredUpdates::doUpdates
* @covers DeferredUpdates::addUpdate
* @covers DeferredUpdatesScopeStack
* @covers DeferredUpdatesScope
*/
public function testDoUpdatesCLI() {
$this->setMwGlobals( 'wgCommandLineMode', true );
$updates = [
'1' => "deferred update 1;\n",
'2' => "deferred update 2;\n",
'2-1' => "deferred update 1 within deferred update 2;\n",
'2-2' => "deferred update 2 within deferred update 2;\n",
'3' => "deferred update 3;\n",
'3-1' => "deferred update 1 within deferred update 3;\n",
'3-2' => "deferred update 2 within deferred update 3;\n",
'3-1-1' => "deferred update 1 within deferred update 1 within deferred update 3;\n",
'3-2-1' => "deferred update 1 within deferred update 2 with deferred update 3;\n",
];
// clear anything
$lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
$lbFactory->commitMasterChanges( __METHOD__ );
DeferredUpdates::addCallableUpdate(
function () use ( $updates ) {
echo $updates['1'];
}
);
DeferredUpdates::addCallableUpdate(
function () use ( $updates ) {
echo $updates['2'];
DeferredUpdates::addCallableUpdate(
function () use ( $updates ) {
echo $updates['2-1'];
}
);
DeferredUpdates::addCallableUpdate(
function () use ( $updates ) {
echo $updates['2-2'];
}
);
}
);
DeferredUpdates::addCallableUpdate(
function () use ( $updates ) {
echo $updates['3'];
DeferredUpdates::addCallableUpdate(
function () use ( $updates ) {
echo $updates['3-1'];
DeferredUpdates::addCallableUpdate(
function () use ( $updates ) {
echo $updates['3-1-1'];
}
);
}
);
DeferredUpdates::addCallableUpdate(
function () use ( $updates ) {
echo $updates['3-2'];
DeferredUpdates::addCallableUpdate(
function () use ( $updates ) {
echo $updates['3-2-1'];
}
);
}
);
}
);
$this->expectOutputString( implode( '', $updates ) );
DeferredUpdates::doUpdates();
}
/**
* @covers DeferredUpdates::doUpdates
* @covers DeferredUpdates::addUpdate
* @covers DeferredUpdatesScopeStack
* @covers DeferredUpdatesScope
*/
public function testPresendAddOnPostsendRun() {
$this->setMwGlobals( 'wgCommandLineMode', true );
$x = false;
$y = false;
// clear anything
$lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
$lbFactory->commitMasterChanges( __METHOD__ );
DeferredUpdates::addCallableUpdate(
function () use ( &$x, &$y ) {
$x = true;
DeferredUpdates::addCallableUpdate(
function () use ( &$y ) {
$y = true;
},
DeferredUpdates::PRESEND
);
},
DeferredUpdates::POSTSEND
);
DeferredUpdates::doUpdates();
$this->assertTrue( $x, "Outer POSTSEND update ran" );
$this->assertTrue( $y, "Nested PRESEND update ran" );
}
/**
* @covers DeferredUpdates::attemptUpdate
*/
public function testRunUpdateTransactionScope() {
$this->setMwGlobals( 'wgCommandLineMode', false );
$lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
$this->assertFalse( $lbFactory->hasTransactionRound(), 'Initial state' );
$ran = 0;
DeferredUpdates::addCallableUpdate( function () use ( &$ran, $lbFactory ) {
$ran++;
$this->assertTrue( $lbFactory->hasTransactionRound(), 'Has transaction' );
} );
DeferredUpdates::doUpdates();
$this->assertSame( 1, $ran, 'Update ran' );
$this->assertFalse( $lbFactory->hasTransactionRound(), 'Final state' );
}
/**
* @covers DeferredUpdates::attemptUpdate
* @covers TransactionRoundDefiningUpdate::getOrigin
*/
public function testRunOuterScopeUpdate() {
$this->setMwGlobals( 'wgCommandLineMode', false );
$lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
$this->assertFalse( $lbFactory->hasTransactionRound(), 'Initial state' );
$ran = 0;
DeferredUpdates::addUpdate( new TransactionRoundDefiningUpdate(
function () use ( &$ran, $lbFactory ) {
$ran++;
$this->assertFalse( $lbFactory->hasTransactionRound(), 'No transaction' );
} )
);
DeferredUpdates::doUpdates();
$this->assertSame( 1, $ran, 'Update ran' );
}
/**
* @covers DeferredUpdates::tryOpportunisticExecute
* @covers DeferredUpdates::doUpdates
* @covers DeferredUpdatesScopeStack
* @covers DeferredUpdatesScope
*/
public function testTryOpportunisticExecute() {
$calls = [];
$callback1 = function () use ( &$calls ) {
$calls[] = 1;
};
$callback2 = function () use ( &$calls ) {
$calls[] = 2;
};
$lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
$lbFactory->beginMasterChanges( __METHOD__ );
DeferredUpdates::addCallableUpdate( $callback1 );
$this->assertEquals( [], $calls );
DeferredUpdates::tryOpportunisticExecute( 'run' );
$this->assertEquals( [], $calls );
$dbw = wfGetDB( DB_MASTER );
$dbw->onTransactionCommitOrIdle( function () use ( &$calls, $callback2 ) {
DeferredUpdates::addCallableUpdate( $callback2 );
$this->assertEquals( [], $calls );
$calls[] = 'oti';
} );
$this->assertSame( 1, $dbw->trxLevel() );
$this->assertEquals( [], $calls );
$lbFactory->commitMasterChanges( __METHOD__ );
$this->assertEquals( [ 'oti' ], $calls );
DeferredUpdates::tryOpportunisticExecute( 'run' );
$this->assertEquals( [ 'oti', 1, 2 ], $calls );
}
/**
* @covers DeferredUpdates::attemptUpdate
*/
public function testCallbackUpdateRounds() {
$lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
$fname = __METHOD__;
$called = false;
DeferredUpdates::attemptUpdate(
new MWCallableUpdate(
function () use ( $lbFactory, $fname, &$called ) {
$lbFactory->flushReplicaSnapshots( $fname );
$lbFactory->commitMasterChanges( $fname );
$called = true;
},
$fname
),
$lbFactory
);
$this->assertTrue( $called, "Callback ran" );
}
/**
* @covers DeferredUpdates::doUpdates
* @covers DeferredUpdatesScopeStack
* @covers DeferredUpdatesScope
*/
public function testNestedExecution() {
// No immediate execution
$this->setMwGlobals( 'wgCommandLineMode', false );
$res = null;
$resSub = null;
$resSubSub = null;
$resA = null;
DeferredUpdates::clearPendingUpdates();
$this->assertSame( 0, DeferredUpdates::pendingUpdatesCount() );
$this->assertSame( 0, DeferredUpdates::getRecursiveExecutionStackDepth() );
// T249069: TransactionRoundDefiningUpdate => JobRunner => DeferredUpdates::doUpdates()
DeferredUpdates::addUpdate( new TransactionRoundDefiningUpdate(
function () use ( &$res, &$resSub, &$resSubSub, &$resA ) {
$res = 1;
$this->assertSame( 0, DeferredUpdates::pendingUpdatesCount() );
$this->assertSame( 1, DeferredUpdates::getRecursiveExecutionStackDepth() );
// Add update to subqueue of in-progress top-queue job
DeferredUpdates::addCallableUpdate( function () use ( &$resSub, &$resSubSub ) {
$resSub = 'a';
$this->assertSame( 0, DeferredUpdates::pendingUpdatesCount() );
$this->assertSame( 2, DeferredUpdates::getRecursiveExecutionStackDepth() );
// Add update to subqueue of in-progress top-queue job (not recursive)
DeferredUpdates::addCallableUpdate( function () use ( &$resSubSub ) {
$resSubSub = 'b';
} );
$this->assertSame( 1, DeferredUpdates::pendingUpdatesCount() );
} );
$this->assertSame( 1, DeferredUpdates::pendingUpdatesCount() );
$this->assertSame( 1, DeferredUpdates::getRecursiveExecutionStackDepth() );
if ( $resSub === null && $resA === null && $resSubSub === null ) {
$res = 418;
}
DeferredUpdates::doUpdates();
}
) );
$this->assertSame( 1, DeferredUpdates::pendingUpdatesCount() );
$this->assertSame( 0, DeferredUpdates::getRecursiveExecutionStackDepth() );
DeferredUpdates::addCallableUpdate( function () use ( &$resA ) {
$resA = 93;
} );
$this->assertSame( 2, DeferredUpdates::pendingUpdatesCount() );
$this->assertSame( 0, DeferredUpdates::getRecursiveExecutionStackDepth() );
$this->assertSame( null, $resA );
$this->assertSame( null, $res );
$this->assertSame( null, $resSub );
$this->assertSame( null, $resSubSub );
DeferredUpdates::doUpdates();
$this->assertSame( 0, DeferredUpdates::pendingUpdatesCount() );
$this->assertSame( 0, DeferredUpdates::getRecursiveExecutionStackDepth() );
$this->assertSame( 418, $res );
$this->assertSame( 'a', $resSub );
$this->assertSame( 'b', $resSubSub );
$this->assertSame( 93, $resA );
}
}