Skip to content

Commit a445660

Browse files
authored
PHPLIB-1459: Retryable read/write prose tests for mongos selection (#1333)
This also moves the existing, third prose test for retryable writes into its own file. * Implement comparator for Server objects
1 parent 1b91ead commit a445660

File tree

7 files changed

+416
-19
lines changed

7 files changed

+416
-19
lines changed

phpcs.xml.dist

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@
161161
<exclude-pattern>/examples</exclude-pattern>
162162
</rule>
163163
<rule ref="Squiz.Classes.ValidClassName.NotCamelCaps">
164-
<exclude-pattern>/tests/SpecTests/ClientSideEncryption/Prose*</exclude-pattern>
164+
<exclude-pattern>/tests/SpecTests/*/Prose*</exclude-pattern>
165165
</rule>
166166

167167
<!-- **************************************** -->

tests/Comparator/ServerComparator.php

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
namespace MongoDB\Tests\Comparator;
4+
5+
use MongoDB\Driver\Server;
6+
use SebastianBergmann\Comparator\Comparator;
7+
use SebastianBergmann\Comparator\ComparisonFailure;
8+
9+
use function sprintf;
10+
11+
class ServerComparator extends Comparator
12+
{
13+
public function accepts($expected, $actual)
14+
{
15+
return $expected instanceof Server && $actual instanceof Server;
16+
}
17+
18+
public function assertEquals($expected, $actual, $delta = 0.0, $canonicalize = false, $ignoreCase = false): void
19+
{
20+
if ($expected == $actual) {
21+
return;
22+
}
23+
24+
throw new ComparisonFailure(
25+
$expected,
26+
$actual,
27+
'',
28+
'',
29+
false,
30+
sprintf(
31+
'Failed asserting that Server("%s:%d") matches expected Server("%s:%d").',
32+
$actual->getHost(),
33+
$actual->getPort(),
34+
$expected->getHost(),
35+
$expected->getPort(),
36+
),
37+
);
38+
}
39+
}
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
<?php
2+
3+
namespace MongoDB\Tests\SpecTests\RetryableReads;
4+
5+
use MongoDB\Driver\Exception\CommandException;
6+
use MongoDB\Driver\Monitoring\CommandFailedEvent;
7+
use MongoDB\Driver\Monitoring\CommandStartedEvent;
8+
use MongoDB\Driver\Monitoring\CommandSubscriber;
9+
use MongoDB\Driver\Monitoring\CommandSucceededEvent;
10+
use MongoDB\Driver\Server;
11+
use MongoDB\Tests\SpecTests\FunctionalTestCase;
12+
13+
use function assert;
14+
use function count;
15+
16+
/**
17+
* Prose test 2: Retry on different or same mongos
18+
*
19+
* @see https://github.com/mongodb/specifications/blob/master/source/retryable-writes/tests/README.md
20+
*/
21+
class Prose2_RetryOnMongosTest extends FunctionalTestCase
22+
{
23+
public const HOST_UNREACHABLE = 6;
24+
25+
public function testRetryOnDifferentMongos(): void
26+
{
27+
if (! $this->isMongos()) {
28+
$this->markTestSkipped('Test requires connections to mongos');
29+
}
30+
31+
$this->skipIfServerVersion('<', '4.1.7', 'Test requires mongos support for configureFailPoint');
32+
33+
/* By default, the Manager under test is created with a single-mongos
34+
* URI. Explicitly create a Client with multiple mongoses and invoke
35+
* server selection to initialize SDAM. */
36+
$client = static::createTestClient(static::getUri(true), ['retryReads' => true]);
37+
$client->getManager()->selectServer();
38+
39+
/* Step 1: Select servers for each mongos in the cluster.
40+
*
41+
* TODO: Support topologies with 3+ servers by selecting only two and
42+
* recreating a client URI.
43+
*/
44+
$servers = $client->getManager()->getServers();
45+
assert(count($servers) === 2);
46+
$this->assertNotEquals($servers[0], $servers[1]);
47+
48+
// Step 2: Configure the following fail point on each mongos
49+
foreach ($servers as $server) {
50+
$this->configureFailPoint(
51+
[
52+
'configureFailPoint' => 'failCommand',
53+
'mode' => ['times' => 1],
54+
'data' => [
55+
'failCommands' => ['find'],
56+
'errorCode' => self::HOST_UNREACHABLE,
57+
],
58+
],
59+
$server,
60+
);
61+
}
62+
63+
/* Step 3: Use the previously created client with retryReads=true,
64+
* which is connected to a cluster with two mongoses */
65+
66+
// Step 4: Enable failed command event monitoring for client
67+
$subscriber = new class implements CommandSubscriber {
68+
public $commandFailedServers = [];
69+
70+
public function commandStarted(CommandStartedEvent $event): void
71+
{
72+
}
73+
74+
public function commandSucceeded(CommandSucceededEvent $event): void
75+
{
76+
}
77+
78+
public function commandFailed(CommandFailedEvent $event): void
79+
{
80+
$this->commandFailedServers[] = $event->getServer();
81+
}
82+
};
83+
84+
$client->addSubscriber($subscriber);
85+
86+
// Step 5: Execute a find command. Assert that the command failed.
87+
try {
88+
$client->selectCollection($this->getDatabaseName(), $this->getCollectionName())->find(['x' => 1]);
89+
$this->fail('BulkWriteException was not thrown');
90+
} catch (CommandException $e) {
91+
$this->assertSame(self::HOST_UNREACHABLE, $e->getCode());
92+
}
93+
94+
$client->removeSubscriber($subscriber);
95+
96+
/* Step 6: Assert that two failed command events occurred. Assert that
97+
* the failed command events occurred on different mongoses. */
98+
$this->assertCount(2, $subscriber->commandFailedServers);
99+
$this->assertNotEquals($subscriber->commandFailedServers[0], $subscriber->commandFailedServers[1]);
100+
101+
// Step 7: The fail points will be disabled during tearDown()
102+
}
103+
104+
public function testRetryOnSameMongos(): void
105+
{
106+
if (! $this->isMongos()) {
107+
$this->markTestSkipped('Test requires connections to mongos');
108+
}
109+
110+
$this->skipIfServerVersion('<', '4.1.7', 'Test requires mongos support for configureFailPoint');
111+
112+
// Step 1: Create a client that connects to a single mongos
113+
$client = static::createTestClient(null, ['directConnection' => false, 'retryReads' => true]);
114+
$server = $client->getManager()->selectServer();
115+
116+
// Step 2: Configure the following fail point on the mongos
117+
$this->configureFailPoint(
118+
[
119+
'configureFailPoint' => 'failCommand',
120+
'mode' => ['times' => 1],
121+
'data' => [
122+
'failCommands' => ['find'],
123+
'errorCode' => self::HOST_UNREACHABLE,
124+
],
125+
],
126+
$server,
127+
);
128+
129+
// Step 3 is omitted because we can re-use the same client
130+
131+
// Step 4: Enable succeeded and failed command event monitoring
132+
$subscriber = new class implements CommandSubscriber {
133+
public Server $commandSucceededServer;
134+
public Server $commandFailedServer;
135+
136+
public function commandStarted(CommandStartedEvent $event): void
137+
{
138+
}
139+
140+
public function commandSucceeded(CommandSucceededEvent $event): void
141+
{
142+
$this->commandSucceededServer = $event->getServer();
143+
}
144+
145+
public function commandFailed(CommandFailedEvent $event): void
146+
{
147+
$this->commandFailedServer = $event->getServer();
148+
}
149+
};
150+
151+
$client->addSubscriber($subscriber);
152+
153+
// Step 5: Execute a find command. Assert that the command succeeded.
154+
$cursor = $client->selectCollection($this->getDatabaseName(), $this->getCollectionName())->find(['x' => 1]);
155+
$this->assertSame([], $cursor->toArray());
156+
157+
$client->removeSubscriber($subscriber);
158+
159+
/* Step 6: Assert that exactly one failed command event and one
160+
* succeeded command event occurred. Assert that both events occurred on
161+
* the same mongos. */
162+
$this->assertNotNull($subscriber->commandSucceededServer);
163+
$this->assertNotNull($subscriber->commandFailedServer);
164+
$this->assertEquals($subscriber->commandSucceededServer, $subscriber->commandFailedServer);
165+
166+
// Step 7: The fail point will be disabled during tearDown()
167+
}
168+
}

tests/SpecTests/RetryableWritesSpecTest.php renamed to tests/SpecTests/RetryableWrites/Prose3_ReturnOriginalErrorTest.php

Lines changed: 13 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,25 @@
11
<?php
22

3-
namespace MongoDB\Tests\SpecTests;
3+
namespace MongoDB\Tests\SpecTests\RetryableWrites;
44

55
use MongoDB\Driver\Exception\BulkWriteException;
66
use MongoDB\Driver\Monitoring\CommandFailedEvent;
77
use MongoDB\Driver\Monitoring\CommandStartedEvent;
88
use MongoDB\Driver\Monitoring\CommandSubscriber;
99
use MongoDB\Driver\Monitoring\CommandSucceededEvent;
10+
use MongoDB\Tests\SpecTests\FunctionalTestCase;
1011

1112
/**
12-
* Retryable writes spec tests.
13+
* Prose test 3: Return Original Error
1314
*
14-
* @see https://github.com/mongodb/specifications/tree/master/source/retryable-writes
15+
* @see https://github.com/mongodb/specifications/blob/master/source/retryable-writes/tests/README.md
1516
* @group serverless
1617
*/
17-
class RetryableWritesSpecTest extends FunctionalTestCase
18+
class Prose3_ReturnOriginalErrorTest extends FunctionalTestCase
1819
{
19-
public const NOT_PRIMARY = 10107;
20+
public const NOT_WRITABLE_PRIMARY = 10107;
2021
public const SHUTDOWN_IN_PROGRESS = 91;
2122

22-
/**
23-
* Prose test 3: when encountering a NoWritesPerformed error after an error with a RetryableWriteError label
24-
*/
2523
public function testNoWritesPerformedErrorReturnsOriginalError(): void
2624
{
2725
if (! $this->isReplicaSet()) {
@@ -46,9 +44,9 @@ public function testNoWritesPerformedErrorReturnsOriginalError(): void
4644
]);
4745

4846
$subscriber = new class ($this) implements CommandSubscriber {
49-
private RetryableWritesSpecTest $testCase;
47+
private FunctionalTestCase $testCase;
5048

51-
public function __construct(RetryableWritesSpecTest $testCase)
49+
public function __construct(FunctionalTestCase $testCase)
5250
{
5351
$this->testCase = $testCase;
5452
}
@@ -65,7 +63,7 @@ public function commandSucceeded(CommandSucceededEvent $event): void
6563
'configureFailPoint' => 'failCommand',
6664
'mode' => ['times' => 1],
6765
'data' => [
68-
'errorCode' => RetryableWritesSpecTest::NOT_PRIMARY,
66+
'errorCode' => Prose3_ReturnOriginalErrorTest::NOT_WRITABLE_PRIMARY,
6967
'errorLabels' => ['RetryableWriteError', 'NoWritesPerformed'],
7068
'failCommands' => ['insert'],
7169
],
@@ -78,7 +76,7 @@ public function commandFailed(CommandFailedEvent $event): void
7876
}
7977
};
8078

81-
$client->getManager()->addSubscriber($subscriber);
79+
$client->addSubscriber($subscriber);
8280

8381
// Step 4: Run insertOne
8482
try {
@@ -91,11 +89,8 @@ public function commandFailed(CommandFailedEvent $event): void
9189
$this->assertSame(self::SHUTDOWN_IN_PROGRESS, $writeConcernError->getCode());
9290
}
9391

94-
// Step 5: Disable the fail point
95-
$client->getManager()->removeSubscriber($subscriber);
96-
$this->configureFailPoint([
97-
'configureFailPoint' => 'failCommand',
98-
'mode' => 'off',
99-
]);
92+
$client->removeSubscriber($subscriber);
93+
94+
// Step 5: The fail point will be disabled during tearDown()
10095
}
10196
}

0 commit comments

Comments
 (0)