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

parent 9d0cbbb7
...@@ -151,7 +151,7 @@ abstract class FunctionalTestCase extends TestCase ...@@ -151,7 +151,7 @@ abstract class FunctionalTestCase extends TestCase
* @param array|stdClass $command configureFailPoint command document * @param array|stdClass $command configureFailPoint command document
* @throws InvalidArgumentException if $command is not a configureFailPoint command * @throws InvalidArgumentException if $command is not a configureFailPoint command
*/ */
protected function configureFailPoint($command) public function configureFailPoint($command, Server $server = null)
{ {
if (! $this->isFailCommandSupported()) { if (! $this->isFailCommandSupported()) {
$this->markTestSkipped('failCommand is only supported on mongod >= 4.0.0 and mongos >= 4.1.5.'); $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 ...@@ -173,14 +173,16 @@ abstract class FunctionalTestCase extends TestCase
throw new InvalidArgumentException('$command is not a configureFailPoint command'); throw new InvalidArgumentException('$command is not a configureFailPoint command');
} }
$failPointServer = $server ?: $this->getPrimaryServer();
$operation = new DatabaseCommand('admin', $command); $operation = new DatabaseCommand('admin', $command);
$cursor = $operation->execute($this->getPrimaryServer()); $cursor = $operation->execute($failPointServer);
$result = $cursor->toArray()[0]; $result = $cursor->toArray()[0];
$this->assertCommandSucceeded($result); $this->assertCommandSucceeded($result);
// Record the fail point so it can be disabled during tearDown() // 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 ...@@ -408,9 +410,7 @@ abstract class FunctionalTestCase extends TestCase
return; return;
} }
$server = $this->getPrimaryServer(); foreach ($this->configuredFailPoints as list($failPoint, $server)) {
foreach ($this->configuredFailPoints as $failPoint) {
$operation = new DatabaseCommand('admin', ['configureFailPoint' => $failPoint, 'mode' => 'off']); $operation = new DatabaseCommand('admin', ['configureFailPoint' => $failPoint, 'mode' => 'off']);
$operation->execute($server); $operation->execute($server);
} }
......
...@@ -10,6 +10,7 @@ use MongoDB\Driver\Monitoring\CommandSubscriber; ...@@ -10,6 +10,7 @@ use MongoDB\Driver\Monitoring\CommandSubscriber;
use MongoDB\Driver\Monitoring\CommandSucceededEvent; use MongoDB\Driver\Monitoring\CommandSucceededEvent;
use MultipleIterator; use MultipleIterator;
use function count; use function count;
use function in_array;
use function key; use function key;
use function MongoDB\Driver\Monitoring\addSubscriber; use function MongoDB\Driver\Monitoring\addSubscriber;
use function MongoDB\Driver\Monitoring\removeSubscriber; use function MongoDB\Driver\Monitoring\removeSubscriber;
...@@ -37,6 +38,9 @@ class CommandExpectations implements CommandSubscriber ...@@ -37,6 +38,9 @@ class CommandExpectations implements CommandSubscriber
/** @var boolean */ /** @var boolean */
private $ignoreExtraEvents = false; private $ignoreExtraEvents = false;
/** @var string[] */
private $ignoredCommandNames = [];
private function __construct(array $events) private function __construct(array $events)
{ {
foreach ($events as $event) { foreach ($events as $event) {
...@@ -110,6 +114,14 @@ class CommandExpectations implements CommandSubscriber ...@@ -110,6 +114,14 @@ class CommandExpectations implements CommandSubscriber
$o->ignoreCommandFailed = true; $o->ignoreCommandFailed = true;
$o->ignoreCommandSucceeded = 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; return $o;
} }
...@@ -120,7 +132,7 @@ class CommandExpectations implements CommandSubscriber ...@@ -120,7 +132,7 @@ class CommandExpectations implements CommandSubscriber
*/ */
public function commandFailed(CommandFailedEvent $event) public function commandFailed(CommandFailedEvent $event)
{ {
if ($this->ignoreCommandFailed || ($this->ignoreExtraEvents && count($this->actualEvents) === count($this->expectedEvents))) { if ($this->ignoreCommandFailed || $this->isEventIgnored($event)) {
return; return;
} }
...@@ -134,7 +146,7 @@ class CommandExpectations implements CommandSubscriber ...@@ -134,7 +146,7 @@ class CommandExpectations implements CommandSubscriber
*/ */
public function commandStarted(CommandStartedEvent $event) public function commandStarted(CommandStartedEvent $event)
{ {
if ($this->ignoreCommandStarted || ($this->ignoreExtraEvents && count($this->actualEvents) === count($this->expectedEvents))) { if ($this->ignoreCommandStarted || $this->isEventIgnored($event)) {
return; return;
} }
...@@ -148,7 +160,7 @@ class CommandExpectations implements CommandSubscriber ...@@ -148,7 +160,7 @@ class CommandExpectations implements CommandSubscriber
*/ */
public function commandSucceeded(CommandSucceededEvent $event) public function commandSucceeded(CommandSucceededEvent $event)
{ {
if ($this->ignoreCommandSucceeded || ($this->ignoreExtraEvents && count($this->actualEvents) === count($this->expectedEvents))) { if ($this->ignoreCommandSucceeded || $this->isEventIgnored($event)) {
return; return;
} }
...@@ -212,4 +224,10 @@ class CommandExpectations implements CommandSubscriber ...@@ -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; ...@@ -9,6 +9,7 @@ use MongoDB\Database;
use MongoDB\Driver\Cursor; use MongoDB\Driver\Cursor;
use MongoDB\Driver\Exception\BulkWriteException; use MongoDB\Driver\Exception\BulkWriteException;
use MongoDB\Driver\Exception\Exception; use MongoDB\Driver\Exception\Exception;
use MongoDB\Driver\Server;
use MongoDB\Driver\Session; use MongoDB\Driver\Session;
use MongoDB\Driver\WriteConcern; use MongoDB\Driver\WriteConcern;
use MongoDB\GridFS\Bucket; use MongoDB\GridFS\Bucket;
...@@ -19,6 +20,7 @@ use function array_diff_key; ...@@ -19,6 +20,7 @@ use function array_diff_key;
use function array_map; use function array_map;
use function fclose; use function fclose;
use function fopen; use function fopen;
use function in_array;
use function MongoDB\is_last_pipeline_operator_write; use function MongoDB\is_last_pipeline_operator_write;
use function MongoDB\with_transaction; use function MongoDB\with_transaction;
use function stream_get_contents; use function stream_get_contents;
...@@ -37,6 +39,7 @@ final class Operation ...@@ -37,6 +39,7 @@ final class Operation
const OBJECT_SELECT_DATABASE = 'selectDatabase'; const OBJECT_SELECT_DATABASE = 'selectDatabase';
const OBJECT_SESSION0 = 'session0'; const OBJECT_SESSION0 = 'session0';
const OBJECT_SESSION1 = 'session1'; const OBJECT_SESSION1 = 'session1';
const OBJECT_TEST_RUNNER = 'testRunner';
/** @var ErrorExpectation|null */ /** @var ErrorExpectation|null */
public $errorExpectation; public $errorExpectation;
...@@ -280,6 +283,8 @@ final class Operation ...@@ -280,6 +283,8 @@ final class Operation
return $this->executeForSession($context->session0, $test, $context); return $this->executeForSession($context->session0, $test, $context);
case self::OBJECT_SESSION1: case self::OBJECT_SESSION1:
return $this->executeForSession($context->session1, $test, $context); return $this->executeForSession($context->session1, $test, $context);
case self::OBJECT_TEST_RUNNER:
return $this->executeForTestRunner($test, $context);
default: default:
throw new LogicException('Unsupported object: ' . $this->object); throw new LogicException('Unsupported object: ' . $this->object);
} }
...@@ -545,6 +550,42 @@ final class Operation ...@@ -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 * @throws LogicException if the operation object is unsupported
*/ */
...@@ -561,6 +602,7 @@ final class Operation ...@@ -561,6 +602,7 @@ final class Operation
return ResultExpectation::ASSERT_SAME; return ResultExpectation::ASSERT_SAME;
case self::OBJECT_SESSION0: case self::OBJECT_SESSION0:
case self::OBJECT_SESSION1: case self::OBJECT_SESSION1:
case self::OBJECT_TEST_RUNNER:
return ResultExpectation::ASSERT_NOTHING; return ResultExpectation::ASSERT_NOTHING;
default: default:
throw new LogicException('Unsupported object: ' . $this->object); throw new LogicException('Unsupported object: ' . $this->object);
......
...@@ -35,6 +35,7 @@ class TransactionsSpecTest extends FunctionalTestCase ...@@ -35,6 +35,7 @@ class TransactionsSpecTest extends FunctionalTestCase
* @var array * @var array
*/ */
private static $incompleteTests = [ 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: 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/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)', '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 ...@@ -48,6 +49,8 @@ class TransactionsSpecTest extends FunctionalTestCase
parent::setUp(); parent::setUp();
static::killAllSessions(); static::killAllSessions();
$this->skipIfTransactionsAreNotSupported();
} }
private function doTearDown() private function doTearDown()
...@@ -128,8 +131,8 @@ class TransactionsSpecTest extends FunctionalTestCase ...@@ -128,8 +131,8 @@ class TransactionsSpecTest extends FunctionalTestCase
$this->markTestIncomplete(self::$incompleteTests[$this->dataDescription()]); $this->markTestIncomplete(self::$incompleteTests[$this->dataDescription()]);
} }
if ($this->isShardedCluster()) { if (isset($test->skipReason)) {
$this->markTestSkipped('PHP MongoDB driver 1.6.0alpha2 does not support running multi-document transactions on sharded clusters'); $this->markTestSkipped($test->skipReason);
} }
if (isset($test->useMultipleMongoses) && $test->useMultipleMongoses && $this->isShardedCluster()) { 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 @@ ...@@ -705,7 +705,7 @@
} }
}, },
{ {
"description": "abortTransaction succeeds after InterruptedDueToStepDown", "description": "abortTransaction succeeds after InterruptedDueToReplStateChange",
"failPoint": { "failPoint": {
"configureFailPoint": "failCommand", "configureFailPoint": "failCommand",
"mode": { "mode": {
...@@ -1627,7 +1627,7 @@ ...@@ -1627,7 +1627,7 @@
} }
}, },
{ {
"description": "abortTransaction succeeds after WriteConcernError InterruptedDueToStepDown", "description": "abortTransaction succeeds after WriteConcernError InterruptedDueToReplStateChange",
"failPoint": { "failPoint": {
"configureFailPoint": "failCommand", "configureFailPoint": "failCommand",
"mode": { "mode": {
......
...@@ -942,7 +942,7 @@ ...@@ -942,7 +942,7 @@
} }
}, },
{ {
"description": "commitTransaction succeeds after InterruptedDueToStepDown", "description": "commitTransaction succeeds after InterruptedDueToReplStateChange",
"failPoint": { "failPoint": {
"configureFailPoint": "failCommand", "configureFailPoint": "failCommand",
"mode": { "mode": {
...@@ -1925,7 +1925,7 @@ ...@@ -1925,7 +1925,7 @@
} }
}, },
{ {
"description": "commitTransaction succeeds after WriteConcernError InterruptedDueToStepDown", "description": "commitTransaction succeeds after WriteConcernError InterruptedDueToReplStateChange",
"failPoint": { "failPoint": {
"configureFailPoint": "failCommand", "configureFailPoint": "failCommand",
"mode": { "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