Skip to content

PHPLIB-1459: Retryable read/write prose tests for mongos selection #1333

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jun 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion phpcs.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@
<exclude-pattern>/examples</exclude-pattern>
</rule>
<rule ref="Squiz.Classes.ValidClassName.NotCamelCaps">
<exclude-pattern>/tests/SpecTests/ClientSideEncryption/Prose*</exclude-pattern>
<exclude-pattern>/tests/SpecTests/*/Prose*</exclude-pattern>
</rule>

<!-- **************************************** -->
Expand Down
39 changes: 39 additions & 0 deletions tests/Comparator/ServerComparator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

namespace MongoDB\Tests\Comparator;

use MongoDB\Driver\Server;
use SebastianBergmann\Comparator\Comparator;
use SebastianBergmann\Comparator\ComparisonFailure;

use function sprintf;

class ServerComparator extends Comparator
{
public function accepts($expected, $actual)
{
return $expected instanceof Server && $actual instanceof Server;
}

public function assertEquals($expected, $actual, $delta = 0.0, $canonicalize = false, $ignoreCase = false): void
{
if ($expected == $actual) {
return;
}

throw new ComparisonFailure(
$expected,
$actual,
'',
'',
false,
sprintf(
'Failed asserting that Server("%s:%d") matches expected Server("%s:%d").',
$actual->getHost(),
$actual->getPort(),
$expected->getHost(),
$expected->getPort(),
),
);
}
}
168 changes: 168 additions & 0 deletions tests/SpecTests/RetryableReads/Prose2_RetryOnMongosTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
<?php

namespace MongoDB\Tests\SpecTests\RetryableReads;

use MongoDB\Driver\Exception\CommandException;
use MongoDB\Driver\Monitoring\CommandFailedEvent;
use MongoDB\Driver\Monitoring\CommandStartedEvent;
use MongoDB\Driver\Monitoring\CommandSubscriber;
use MongoDB\Driver\Monitoring\CommandSucceededEvent;
use MongoDB\Driver\Server;
use MongoDB\Tests\SpecTests\FunctionalTestCase;

use function assert;
use function count;

/**
* Prose test 2: Retry on different or same mongos
*
* @see https://github.com/mongodb/specifications/blob/master/source/retryable-writes/tests/README.md
*/
class Prose2_RetryOnMongosTest extends FunctionalTestCase
{
public const HOST_UNREACHABLE = 6;

public function testRetryOnDifferentMongos(): void
{
if (! $this->isMongos()) {
$this->markTestSkipped('Test requires connections to mongos');
}

$this->skipIfServerVersion('<', '4.1.7', 'Test requires mongos support for configureFailPoint');

/* By default, the Manager under test is created with a single-mongos
* URI. Explicitly create a Client with multiple mongoses and invoke
* server selection to initialize SDAM. */
$client = static::createTestClient(static::getUri(true), ['retryReads' => true]);
$client->getManager()->selectServer();

/* Step 1: Select servers for each mongos in the cluster.
*
* TODO: Support topologies with 3+ servers by selecting only two and
* recreating a client URI.
*/
$servers = $client->getManager()->getServers();
assert(count($servers) === 2);
$this->assertNotEquals($servers[0], $servers[1]);

// Step 2: Configure the following fail point on each mongos
foreach ($servers as $server) {
$this->configureFailPoint(
[
'configureFailPoint' => 'failCommand',
'mode' => ['times' => 1],
'data' => [
'failCommands' => ['find'],
'errorCode' => self::HOST_UNREACHABLE,
],
],
$server,
);
}

/* Step 3: Use the previously created client with retryReads=true,
* which is connected to a cluster with two mongoses */

// Step 4: Enable failed command event monitoring for client
$subscriber = new class implements CommandSubscriber {
public $commandFailedServers = [];

public function commandStarted(CommandStartedEvent $event): void
{
}

public function commandSucceeded(CommandSucceededEvent $event): void
{
}

public function commandFailed(CommandFailedEvent $event): void
{
$this->commandFailedServers[] = $event->getServer();
}
};

$client->addSubscriber($subscriber);

// Step 5: Execute a find command. Assert that the command failed.
try {
$client->selectCollection($this->getDatabaseName(), $this->getCollectionName())->find(['x' => 1]);
$this->fail('BulkWriteException was not thrown');
} catch (CommandException $e) {
$this->assertSame(self::HOST_UNREACHABLE, $e->getCode());
}

$client->removeSubscriber($subscriber);

/* Step 6: Assert that two failed command events occurred. Assert that
* the failed command events occurred on different mongoses. */
$this->assertCount(2, $subscriber->commandFailedServers);
$this->assertNotEquals($subscriber->commandFailedServers[0], $subscriber->commandFailedServers[1]);

// Step 7: The fail points will be disabled during tearDown()
}

public function testRetryOnSameMongos(): void
{
if (! $this->isMongos()) {
$this->markTestSkipped('Test requires connections to mongos');
}

$this->skipIfServerVersion('<', '4.1.7', 'Test requires mongos support for configureFailPoint');

// Step 1: Create a client that connects to a single mongos
$client = static::createTestClient(null, ['directConnection' => false, 'retryReads' => true]);
$server = $client->getManager()->selectServer();

// Step 2: Configure the following fail point on the mongos
$this->configureFailPoint(
[
'configureFailPoint' => 'failCommand',
'mode' => ['times' => 1],
'data' => [
'failCommands' => ['find'],
'errorCode' => self::HOST_UNREACHABLE,
],
],
$server,
);

// Step 3 is omitted because we can re-use the same client

// Step 4: Enable succeeded and failed command event monitoring
$subscriber = new class implements CommandSubscriber {
public Server $commandSucceededServer;
public Server $commandFailedServer;

public function commandStarted(CommandStartedEvent $event): void
{
}

public function commandSucceeded(CommandSucceededEvent $event): void
{
$this->commandSucceededServer = $event->getServer();
}

public function commandFailed(CommandFailedEvent $event): void
{
$this->commandFailedServer = $event->getServer();
}
};

$client->addSubscriber($subscriber);

// Step 5: Execute a find command. Assert that the command succeeded.
$cursor = $client->selectCollection($this->getDatabaseName(), $this->getCollectionName())->find(['x' => 1]);
$this->assertSame([], $cursor->toArray());

$client->removeSubscriber($subscriber);

/* Step 6: Assert that exactly one failed command event and one
* succeeded command event occurred. Assert that both events occurred on
* the same mongos. */
$this->assertNotNull($subscriber->commandSucceededServer);
$this->assertNotNull($subscriber->commandFailedServer);
$this->assertEquals($subscriber->commandSucceededServer, $subscriber->commandFailedServer);

// Step 7: The fail point will be disabled during tearDown()
}
}
Original file line number Diff line number Diff line change
@@ -1,27 +1,25 @@
<?php

namespace MongoDB\Tests\SpecTests;
namespace MongoDB\Tests\SpecTests\RetryableWrites;

use MongoDB\Driver\Exception\BulkWriteException;
use MongoDB\Driver\Monitoring\CommandFailedEvent;
use MongoDB\Driver\Monitoring\CommandStartedEvent;
use MongoDB\Driver\Monitoring\CommandSubscriber;
use MongoDB\Driver\Monitoring\CommandSucceededEvent;
use MongoDB\Tests\SpecTests\FunctionalTestCase;

/**
* Retryable writes spec tests.
* Prose test 3: Return Original Error
*
* @see https://github.com/mongodb/specifications/tree/master/source/retryable-writes
* @see https://github.com/mongodb/specifications/blob/master/source/retryable-writes/tests/README.md
* @group serverless
*/
class RetryableWritesSpecTest extends FunctionalTestCase
class Prose3_ReturnOriginalErrorTest extends FunctionalTestCase
{
public const NOT_PRIMARY = 10107;
public const NOT_WRITABLE_PRIMARY = 10107;
public const SHUTDOWN_IN_PROGRESS = 91;

/**
* Prose test 3: when encountering a NoWritesPerformed error after an error with a RetryableWriteError label
*/
public function testNoWritesPerformedErrorReturnsOriginalError(): void
{
if (! $this->isReplicaSet()) {
Expand All @@ -46,9 +44,9 @@ public function testNoWritesPerformedErrorReturnsOriginalError(): void
]);

$subscriber = new class ($this) implements CommandSubscriber {
private RetryableWritesSpecTest $testCase;
private FunctionalTestCase $testCase;

public function __construct(RetryableWritesSpecTest $testCase)
public function __construct(FunctionalTestCase $testCase)
{
$this->testCase = $testCase;
}
Expand All @@ -65,7 +63,7 @@ public function commandSucceeded(CommandSucceededEvent $event): void
'configureFailPoint' => 'failCommand',
'mode' => ['times' => 1],
'data' => [
'errorCode' => RetryableWritesSpecTest::NOT_PRIMARY,
'errorCode' => Prose3_ReturnOriginalErrorTest::NOT_WRITABLE_PRIMARY,
'errorLabels' => ['RetryableWriteError', 'NoWritesPerformed'],
'failCommands' => ['insert'],
],
Expand All @@ -78,7 +76,7 @@ public function commandFailed(CommandFailedEvent $event): void
}
};

$client->getManager()->addSubscriber($subscriber);
$client->addSubscriber($subscriber);

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

// Step 5: Disable the fail point
$client->getManager()->removeSubscriber($subscriber);
$this->configureFailPoint([
'configureFailPoint' => 'failCommand',
'mode' => 'off',
]);
$client->removeSubscriber($subscriber);

// Step 5: The fail point will be disabled during tearDown()
}
}
Loading
Loading