Skip to content

Commit f6a15f0

Browse files
committed
PHPLIB-466: Allow transactions to run on sharded clusters in MongoDB 4.2
1 parent 780fe86 commit f6a15f0

File tree

8 files changed

+160
-18
lines changed

8 files changed

+160
-18
lines changed

tests/FunctionalTestCase.php

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ protected function assertSameObjectId($expectedObjectId, $actualObjectId)
151151
* @param array|stdClass $command configureFailPoint command document
152152
* @throws InvalidArgumentException if $command is not a configureFailPoint command
153153
*/
154-
protected function configureFailPoint($command)
154+
public function configureFailPoint($command, Server $server = null)
155155
{
156156
if (! $this->isFailCommandSupported()) {
157157
$this->markTestSkipped('failCommand is only supported on mongod >= 4.0.0 and mongos >= 4.1.5.');
@@ -173,14 +173,16 @@ protected function configureFailPoint($command)
173173
throw new InvalidArgumentException('$command is not a configureFailPoint command');
174174
}
175175

176+
$failPointServer = $server ?: $this->getPrimaryServer();
177+
176178
$operation = new DatabaseCommand('admin', $command);
177-
$cursor = $operation->execute($this->getPrimaryServer());
179+
$cursor = $operation->execute($failPointServer);
178180
$result = $cursor->toArray()[0];
179181

180182
$this->assertCommandSucceeded($result);
181183

182184
// Record the fail point so it can be disabled during tearDown()
183-
$this->configuredFailPoints[] = $command->configureFailPoint;
185+
$this->configuredFailPoints[] = [$command->configureFailPoint, $failPointServer];
184186
}
185187

186188
/**
@@ -408,9 +410,7 @@ private function disableFailPoints()
408410
return;
409411
}
410412

411-
$server = $this->getPrimaryServer();
412-
413-
foreach ($this->configuredFailPoints as $failPoint) {
413+
foreach ($this->configuredFailPoints as [$failPoint, $server]) {
414414
$operation = new DatabaseCommand('admin', ['configureFailPoint' => $failPoint, 'mode' => 'off']);
415415
$operation->execute($server);
416416
}

tests/SpecTests/CommandExpectations.php

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use MongoDB\Driver\Monitoring\CommandSucceededEvent;
1111
use MultipleIterator;
1212
use function count;
13+
use function in_array;
1314
use function key;
1415
use function MongoDB\Driver\Monitoring\addSubscriber;
1516
use function MongoDB\Driver\Monitoring\removeSubscriber;
@@ -37,6 +38,9 @@ class CommandExpectations implements CommandSubscriber
3738
/** @var boolean */
3839
private $ignoreExtraEvents = false;
3940

41+
/** @var string[] */
42+
private $ignoredCommandNames = [];
43+
4044
private function __construct(array $events)
4145
{
4246
foreach ($events as $event) {
@@ -109,6 +113,7 @@ public static function fromTransactions(array $expectedEvents)
109113

110114
$o->ignoreCommandFailed = true;
111115
$o->ignoreCommandSucceeded = true;
116+
$o->ignoredCommandNames = ['buildInfo', 'getParameter', 'configureFailPoint'];
112117

113118
return $o;
114119
}
@@ -120,7 +125,7 @@ public static function fromTransactions(array $expectedEvents)
120125
*/
121126
public function commandFailed(CommandFailedEvent $event)
122127
{
123-
if ($this->ignoreCommandFailed || ($this->ignoreExtraEvents && count($this->actualEvents) === count($this->expectedEvents))) {
128+
if ($this->ignoreCommandFailed || $this->isEventIgnored($event)) {
124129
return;
125130
}
126131

@@ -134,7 +139,7 @@ public function commandFailed(CommandFailedEvent $event)
134139
*/
135140
public function commandStarted(CommandStartedEvent $event)
136141
{
137-
if ($this->ignoreCommandStarted || ($this->ignoreExtraEvents && count($this->actualEvents) === count($this->expectedEvents))) {
142+
if ($this->ignoreCommandStarted || $this->isEventIgnored($event)) {
138143
return;
139144
}
140145

@@ -148,7 +153,7 @@ public function commandStarted(CommandStartedEvent $event)
148153
*/
149154
public function commandSucceeded(CommandSucceededEvent $event)
150155
{
151-
if ($this->ignoreCommandSucceeded || ($this->ignoreExtraEvents && count($this->actualEvents) === count($this->expectedEvents))) {
156+
if ($this->ignoreCommandSucceeded || $this->isEventIgnored($event)) {
152157
return;
153158
}
154159

@@ -212,4 +217,10 @@ public function assert(FunctionalTestCase $test, Context $context)
212217
}
213218
}
214219
}
220+
221+
private function isEventIgnored($event)
222+
{
223+
return ($this->ignoreExtraEvents && count($this->actualEvents) === count($this->expectedEvents))
224+
|| in_array($event->getCommandName(), $this->ignoredCommandNames);
225+
}
215226
}

tests/SpecTests/FunctionalTestCase.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use LogicException;
88
use MongoDB\Collection;
99
use MongoDB\Driver\Server;
10+
use MongoDB\Driver\WriteConcern;
1011
use MongoDB\Tests\FunctionalTestCase as BaseFunctionalTestCase;
1112
use MultipleIterator;
1213
use PHPUnit\Framework\SkippedTest;

tests/SpecTests/Operation.php

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
use function array_map;
2020
use function fclose;
2121
use function fopen;
22+
use function in_array;
2223
use function MongoDB\is_last_pipeline_operator_write;
2324
use function stream_get_contents;
2425
use function strtolower;
@@ -36,6 +37,7 @@ final class Operation
3637
const OBJECT_SELECT_DATABASE = 'selectDatabase';
3738
const OBJECT_SESSION0 = 'session0';
3839
const OBJECT_SESSION1 = 'session1';
40+
const OBJECT_TEST_RUNNER = 'testRunner';
3941

4042
/** @var ErrorExpectation|null */
4143
public $errorExpectation;
@@ -186,7 +188,7 @@ public function assert(FunctionalTestCase $test, Context $context)
186188
$exception = null;
187189

188190
try {
189-
$result = $this->execute($context);
191+
$result = $this->execute($test, $context);
190192

191193
/* Eagerly iterate the results of a cursor. This both allows an
192194
* exception to be thrown sooner and ensures that any expected
@@ -220,7 +222,7 @@ public function assert(FunctionalTestCase $test, Context $context)
220222
* @return mixed
221223
* @throws LogicException if the operation is unsupported
222224
*/
223-
private function execute(Context $context)
225+
private function execute(FunctionalTestCase $test, Context $context)
224226
{
225227
switch ($this->object) {
226228
case self::OBJECT_CLIENT:
@@ -251,6 +253,8 @@ private function execute(Context $context)
251253
return $this->executeForSession($context->session0, $context);
252254
case self::OBJECT_SESSION1:
253255
return $this->executeForSession($context->session1, $context);
256+
case self::OBJECT_TEST_RUNNER:
257+
return $this->executeForTestRunner($test, $context);
254258
default:
255259
throw new LogicException('Unsupported object: ' . $this->object);
256260
}
@@ -500,6 +504,35 @@ private function executeForSession(Session $session, Context $context)
500504
}
501505
}
502506

507+
private function executeForTestRunner(FunctionalTestCase $test, Context $context)
508+
{
509+
switch ($this->name) {
510+
case 'assertSessionPinned':
511+
$session = $context->{$this->arguments['session']};
512+
$test->assertNotNull($session->getServer());
513+
514+
return null;
515+
case 'assertSessionTransactionState':
516+
$session = $context->{$this->arguments['session']};
517+
$expected = in_array($this->arguments['state'], ['in_progress', 'starting']);
518+
$test->assertSame($expected, $session->isInTransaction());
519+
520+
return null;
521+
case 'assertSessionUnpinned':
522+
$session = $context->{$this->arguments['session']};
523+
$test->assertNull($session->getServer());
524+
525+
return null;
526+
case 'targetedFailPoint':
527+
$session = $context->{$this->arguments['session']};
528+
$test->configureFailPoint($this->arguments['failPoint'], $session->getServer());
529+
530+
return null;
531+
default:
532+
throw new LogicException('Unsupported test runner operation: ' . $this->name);
533+
}
534+
}
535+
503536
/**
504537
* @throws LogicException if the operation object is unsupported
505538
*/
@@ -516,6 +549,7 @@ private function getResultAssertionType()
516549
return ResultExpectation::ASSERT_SAME;
517550
case self::OBJECT_SESSION0:
518551
case self::OBJECT_SESSION1:
552+
case self::OBJECT_TEST_RUNNER:
519553
return ResultExpectation::ASSERT_NOTHING;
520554
default:
521555
throw new LogicException('Unsupported object: ' . $this->object);

tests/SpecTests/TransactionsSpecTest.php

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use MongoDB\Driver\Manager;
1010
use MongoDB\Driver\ReadPreference;
1111
use MongoDB\Driver\Server;
12+
use MongoDB\Driver\WriteConcern;
1213
use stdClass;
1314
use Symfony\Bridge\PhpUnit\SetUpTearDownTrait;
1415
use function basename;
@@ -36,6 +37,7 @@ class TransactionsSpecTest extends FunctionalTestCase
3637
private static $incompleteTests = [
3738
'error-labels: add unknown commit label to MaxTimeMSExpired' => 'PHPC-1382',
3839
'error-labels: add unknown commit label to writeConcernError MaxTimeMSExpired' => 'PHPC-1382',
40+
'pin-mongos: remain pinned after non-transient error on commit' => 'Blocked on SPEC-1320',
3941
'read-pref: default readPreference' => 'PHPLIB does not properly inherit readPreference for transactions',
4042
'read-pref: primary readPreference' => 'PHPLIB does not properly inherit readPreference for transactions',
4143
'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',
@@ -132,8 +134,8 @@ public function testTransactions(stdClass $test, array $runOn = null, array $dat
132134
$this->markTestIncomplete(self::$incompleteTests[$this->dataDescription()]);
133135
}
134136

135-
if ($this->isShardedCluster()) {
136-
$this->markTestSkipped('PHP MongoDB driver 1.6.0alpha2 does not support running multi-document transactions on sharded clusters');
137+
if (isset($test->skipReason)) {
138+
$this->markTestSkipped($test->skipReason);
137139
}
138140

139141
if (isset($test->useMultipleMongoses) && $test->useMultipleMongoses && $this->isShardedCluster()) {
@@ -214,7 +216,7 @@ protected function createTestCollection()
214216
$context = $this->getContext();
215217

216218
$database = $context->getDatabase();
217-
$database->createCollection($context->collectionName, $context->defaultWriteOptions);
219+
$database->createCollection($context->collectionName, ['writeConcern' => new WriteConcern('majority')] + $context->defaultWriteOptions);
218220
}
219221

220222
/**
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
{
2+
"runOn": [
3+
{
4+
"minServerVersion": "4.0",
5+
"topology": [
6+
"replicaset"
7+
]
8+
},
9+
{
10+
"minServerVersion": "4.1.8",
11+
"topology": [
12+
"sharded"
13+
]
14+
}
15+
],
16+
"database_name": "transaction-tests",
17+
"collection_name": "test",
18+
"data": [],
19+
"tests": [
20+
{
21+
"description": "Client side error in command starting transaction",
22+
"operations": [
23+
{
24+
"name": "startTransaction",
25+
"object": "session0"
26+
},
27+
{
28+
"name": "insertOne",
29+
"object": "collection",
30+
"arguments": {
31+
"session": "session0",
32+
"document": {
33+
"_id": {
34+
".": "."
35+
}
36+
}
37+
},
38+
"error": true
39+
},
40+
{
41+
"name": "assertSessionTransactionState",
42+
"object": "testRunner",
43+
"arguments": {
44+
"session": "session0",
45+
"state": "starting"
46+
}
47+
}
48+
]
49+
},
50+
{
51+
"description": "Client side error when transaction is in progress",
52+
"operations": [
53+
{
54+
"name": "startTransaction",
55+
"object": "session0"
56+
},
57+
{
58+
"name": "insertOne",
59+
"object": "collection",
60+
"arguments": {
61+
"session": "session0",
62+
"document": {
63+
"_id": 4
64+
}
65+
},
66+
"result": {
67+
"insertedId": 4
68+
}
69+
},
70+
{
71+
"name": "insertOne",
72+
"object": "collection",
73+
"arguments": {
74+
"session": "session0",
75+
"document": {
76+
"_id": {
77+
".": "."
78+
}
79+
}
80+
},
81+
"error": true
82+
},
83+
{
84+
"name": "assertSessionTransactionState",
85+
"object": "testRunner",
86+
"arguments": {
87+
"session": "session0",
88+
"state": "in_progress"
89+
}
90+
}
91+
]
92+
}
93+
]
94+
}

tests/SpecTests/transactions/retryable-abort.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -705,7 +705,7 @@
705705
}
706706
},
707707
{
708-
"description": "abortTransaction succeeds after InterruptedDueToStepDown",
708+
"description": "abortTransaction succeeds after InterruptedDueToReplStateChange",
709709
"failPoint": {
710710
"configureFailPoint": "failCommand",
711711
"mode": {
@@ -1627,7 +1627,7 @@
16271627
}
16281628
},
16291629
{
1630-
"description": "abortTransaction succeeds after WriteConcernError InterruptedDueToStepDown",
1630+
"description": "abortTransaction succeeds after WriteConcernError InterruptedDueToReplStateChange",
16311631
"failPoint": {
16321632
"configureFailPoint": "failCommand",
16331633
"mode": {

tests/SpecTests/transactions/retryable-commit.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -942,7 +942,7 @@
942942
}
943943
},
944944
{
945-
"description": "commitTransaction succeeds after InterruptedDueToStepDown",
945+
"description": "commitTransaction succeeds after InterruptedDueToReplStateChange",
946946
"failPoint": {
947947
"configureFailPoint": "failCommand",
948948
"mode": {
@@ -1925,7 +1925,7 @@
19251925
}
19261926
},
19271927
{
1928-
"description": "commitTransaction succeeds after WriteConcernError InterruptedDueToStepDown",
1928+
"description": "commitTransaction succeeds after WriteConcernError InterruptedDueToReplStateChange",
19291929
"failPoint": {
19301930
"configureFailPoint": "failCommand",
19311931
"mode": {

0 commit comments

Comments
 (0)