PHPLIB-466: Allow transactions to run on sharded clusters in MongoDB 4.2

parent 9d0cbbb7
......@@ -151,7 +151,7 @@ abstract class FunctionalTestCase extends TestCase
* @param array|stdClass $command configureFailPoint command document
* @throws InvalidArgumentException if $command is not a configureFailPoint command
*/
protected function configureFailPoint($command)
public function configureFailPoint($command, Server $server = null)
{
if (! $this->isFailCommandSupported()) {
$this->markTestSkipped('failCommand is only supported on mongod >= 4.0.0 and mongos >= 4.1.5.');
......@@ -173,14 +173,16 @@ abstract class FunctionalTestCase extends TestCase
throw new InvalidArgumentException('$command is not a configureFailPoint command');
}
$failPointServer = $server ?: $this->getPrimaryServer();
$operation = new DatabaseCommand('admin', $command);
$cursor = $operation->execute($this->getPrimaryServer());
$cursor = $operation->execute($failPointServer);
$result = $cursor->toArray()[0];
$this->assertCommandSucceeded($result);
// Record the fail point so it can be disabled during tearDown()
$this->configuredFailPoints[] = $command->configureFailPoint;
$this->configuredFailPoints[] = [$command->configureFailPoint, $failPointServer];
}
/**
......@@ -408,9 +410,7 @@ abstract class FunctionalTestCase extends TestCase
return;
}
$server = $this->getPrimaryServer();
foreach ($this->configuredFailPoints as $failPoint) {
foreach ($this->configuredFailPoints as list($failPoint, $server)) {
$operation = new DatabaseCommand('admin', ['configureFailPoint' => $failPoint, 'mode' => 'off']);
$operation->execute($server);
}
......
......@@ -10,6 +10,7 @@ use MongoDB\Driver\Monitoring\CommandSubscriber;
use MongoDB\Driver\Monitoring\CommandSucceededEvent;
use MultipleIterator;
use function count;
use function in_array;
use function key;
use function MongoDB\Driver\Monitoring\addSubscriber;
use function MongoDB\Driver\Monitoring\removeSubscriber;
......@@ -37,6 +38,9 @@ class CommandExpectations implements CommandSubscriber
/** @var boolean */
private $ignoreExtraEvents = false;
/** @var string[] */
private $ignoredCommandNames = [];
private function __construct(array $events)
{
foreach ($events as $event) {
......@@ -110,6 +114,14 @@ class CommandExpectations implements CommandSubscriber
$o->ignoreCommandFailed = true;
$o->ignoreCommandSucceeded = true;
/* Ignore the buildInfo and getParameter commands as they are used to
* check for the availability of configureFailPoint and are not expected
* to be called by any spec tests.
* configureFailPoint needs to be ignored as the targetedFailPoint
* operation will be caught by command monitoring and is also not
* present in the expected commands in spec tests. */
$o->ignoredCommandNames = ['buildInfo', 'getParameter', 'configureFailPoint'];
return $o;
}
......@@ -120,7 +132,7 @@ class CommandExpectations implements CommandSubscriber
*/
public function commandFailed(CommandFailedEvent $event)
{
if ($this->ignoreCommandFailed || ($this->ignoreExtraEvents && count($this->actualEvents) === count($this->expectedEvents))) {
if ($this->ignoreCommandFailed || $this->isEventIgnored($event)) {
return;
}
......@@ -134,7 +146,7 @@ class CommandExpectations implements CommandSubscriber
*/
public function commandStarted(CommandStartedEvent $event)
{
if ($this->ignoreCommandStarted || ($this->ignoreExtraEvents && count($this->actualEvents) === count($this->expectedEvents))) {
if ($this->ignoreCommandStarted || $this->isEventIgnored($event)) {
return;
}
......@@ -148,7 +160,7 @@ class CommandExpectations implements CommandSubscriber
*/
public function commandSucceeded(CommandSucceededEvent $event)
{
if ($this->ignoreCommandSucceeded || ($this->ignoreExtraEvents && count($this->actualEvents) === count($this->expectedEvents))) {
if ($this->ignoreCommandSucceeded || $this->isEventIgnored($event)) {
return;
}
......@@ -212,4 +224,10 @@ class CommandExpectations implements CommandSubscriber
}
}
}
private function isEventIgnored($event)
{
return ($this->ignoreExtraEvents && count($this->actualEvents) === count($this->expectedEvents))
|| in_array($event->getCommandName(), $this->ignoredCommandNames);
}
}
......@@ -9,6 +9,7 @@ use MongoDB\Database;
use MongoDB\Driver\Cursor;
use MongoDB\Driver\Exception\BulkWriteException;
use MongoDB\Driver\Exception\Exception;
use MongoDB\Driver\Server;
use MongoDB\Driver\Session;
use MongoDB\Driver\WriteConcern;
use MongoDB\GridFS\Bucket;
......@@ -19,6 +20,7 @@ use function array_diff_key;
use function array_map;
use function fclose;
use function fopen;
use function in_array;
use function MongoDB\is_last_pipeline_operator_write;
use function MongoDB\with_transaction;
use function stream_get_contents;
......@@ -37,6 +39,7 @@ final class Operation
const OBJECT_SELECT_DATABASE = 'selectDatabase';
const OBJECT_SESSION0 = 'session0';
const OBJECT_SESSION1 = 'session1';
const OBJECT_TEST_RUNNER = 'testRunner';
/** @var ErrorExpectation|null */
public $errorExpectation;
......@@ -280,6 +283,8 @@ final class Operation
return $this->executeForSession($context->session0, $test, $context);
case self::OBJECT_SESSION1:
return $this->executeForSession($context->session1, $test, $context);
case self::OBJECT_TEST_RUNNER:
return $this->executeForTestRunner($test, $context);
default:
throw new LogicException('Unsupported object: ' . $this->object);
}
......@@ -545,6 +550,42 @@ final class Operation
}
}
private function executeForTestRunner(FunctionalTestCase $test, Context $context)
{
$args = $context->prepareOptions($this->arguments);
$context->replaceArgumentSessionPlaceholder($args);
switch ($this->name) {
case 'assertSessionPinned':
$test->assertInstanceOf(Session::class, $args['session']);
$test->assertInstanceOf(Server::class, $args['session']->getServer());
return null;
case 'assertSessionTransactionState':
$test->assertInstanceOf(Session::class, $args['session']);
/* PHPC currently does not expose the exact session state, but
* instead exposes a bool to let us know whether a transaction
* is currently in progress. This code may fail down the line
* and should be adjusted once PHPC-1438 is implemented. */
$expected = in_array($this->arguments['state'], ['in_progress', 'starting']);
$test->assertSame($expected, $args['session']->isInTransaction());
return null;
case 'assertSessionUnpinned':
$test->assertInstanceOf(Session::class, $args['session']);
$test->assertNull($args['session']->getServer());
return null;
case 'targetedFailPoint':
$test->assertInstanceOf(Session::class, $args['session']);
$test->configureFailPoint($this->arguments['failPoint'], $args['session']->getServer());
return null;
default:
throw new LogicException('Unsupported test runner operation: ' . $this->name);
}
}
/**
* @throws LogicException if the operation object is unsupported
*/
......@@ -561,6 +602,7 @@ final class Operation
return ResultExpectation::ASSERT_SAME;
case self::OBJECT_SESSION0:
case self::OBJECT_SESSION1:
case self::OBJECT_TEST_RUNNER:
return ResultExpectation::ASSERT_NOTHING;
default:
throw new LogicException('Unsupported object: ' . $this->object);
......
......@@ -35,6 +35,7 @@ class TransactionsSpecTest extends FunctionalTestCase
* @var array
*/
private static $incompleteTests = [
'transactions/pin-mongos: remain pinned after non-transient error on commit' => 'Blocked on SPEC-1320',
'transactions/read-pref: default readPreference' => 'PHPLIB does not properly inherit readPreference for transactions (PHPLIB-473)',
'transactions/read-pref: primary readPreference' => 'PHPLIB does not properly inherit readPreference for transactions (PHPLIB-473)',
'transactions/run-command: run command with secondary read preference in client option and primary read preference in transaction options' => 'PHPLIB does not properly inherit readPreference for transactions (PHPLIB-473)',
......@@ -48,6 +49,8 @@ class TransactionsSpecTest extends FunctionalTestCase
parent::setUp();
static::killAllSessions();
$this->skipIfTransactionsAreNotSupported();
}
private function doTearDown()
......@@ -128,8 +131,8 @@ class TransactionsSpecTest extends FunctionalTestCase
$this->markTestIncomplete(self::$incompleteTests[$this->dataDescription()]);
}
if ($this->isShardedCluster()) {
$this->markTestSkipped('PHP MongoDB driver 1.6.0alpha2 does not support running multi-document transactions on sharded clusters');
if (isset($test->skipReason)) {
$this->markTestSkipped($test->skipReason);
}
if (isset($test->useMultipleMongoses) && $test->useMultipleMongoses && $this->isShardedCluster()) {
......
{
"runOn": [
{
"minServerVersion": "4.0",
"topology": [
"replicaset"
]
},
{
"minServerVersion": "4.1.8",
"topology": [
"sharded"
]
}
],
"database_name": "transaction-tests",
"collection_name": "test",
"data": [],
"tests": [
{
"description": "Client side error in command starting transaction",
"operations": [
{
"name": "startTransaction",
"object": "session0"
},
{
"name": "insertOne",
"object": "collection",
"arguments": {
"session": "session0",
"document": {
"_id": {
".": "."
}
}
},
"error": true
},
{
"name": "assertSessionTransactionState",
"object": "testRunner",
"arguments": {
"session": "session0",
"state": "starting"
}
}
]
},
{
"description": "Client side error when transaction is in progress",
"operations": [
{
"name": "startTransaction",
"object": "session0"
},
{
"name": "insertOne",
"object": "collection",
"arguments": {
"session": "session0",
"document": {
"_id": 4
}
},
"result": {
"insertedId": 4
}
},
{
"name": "insertOne",
"object": "collection",
"arguments": {
"session": "session0",
"document": {
"_id": {
".": "."
}
}
},
"error": true
},
{
"name": "assertSessionTransactionState",
"object": "testRunner",
"arguments": {
"session": "session0",
"state": "in_progress"
}
}
]
}
]
}
......@@ -705,7 +705,7 @@
}
},
{
"description": "abortTransaction succeeds after InterruptedDueToStepDown",
"description": "abortTransaction succeeds after InterruptedDueToReplStateChange",
"failPoint": {
"configureFailPoint": "failCommand",
"mode": {
......@@ -1627,7 +1627,7 @@
}
},
{
"description": "abortTransaction succeeds after WriteConcernError InterruptedDueToStepDown",
"description": "abortTransaction succeeds after WriteConcernError InterruptedDueToReplStateChange",
"failPoint": {
"configureFailPoint": "failCommand",
"mode": {
......
......@@ -942,7 +942,7 @@
}
},
{
"description": "commitTransaction succeeds after InterruptedDueToStepDown",
"description": "commitTransaction succeeds after InterruptedDueToReplStateChange",
"failPoint": {
"configureFailPoint": "failCommand",
"mode": {
......@@ -1925,7 +1925,7 @@
}
},
{
"description": "commitTransaction succeeds after WriteConcernError InterruptedDueToStepDown",
"description": "commitTransaction succeeds after WriteConcernError InterruptedDueToReplStateChange",
"failPoint": {
"configureFailPoint": "failCommand",
"mode": {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment