Skip to content

Commit 0570864

Browse files
committed
PHPLIB-467: Connections survive primary stepdown
1 parent 03e8038 commit 0570864

File tree

1 file changed

+281
-0
lines changed

1 file changed

+281
-0
lines changed
Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
1+
<?php
2+
3+
namespace MongoDB\Tests\SpecTests;
4+
5+
use IteratorIterator;
6+
use MongoDB\Client;
7+
use MongoDB\Collection;
8+
use MongoDB\Driver\Command;
9+
use MongoDB\Driver\Exception\BulkWriteException;
10+
use MongoDB\Driver\Exception\Exception as DriverException;
11+
use MongoDB\Driver\ReadPreference;
12+
use MongoDB\Driver\Server;
13+
use MongoDB\Driver\WriteConcern;
14+
use MongoDB\Operation\BulkWrite;
15+
use MongoDB\Tests\CommandObserver;
16+
use Symfony\Bridge\PhpUnit\SetUpTearDownTrait;
17+
use UnexpectedValueException;
18+
use function current;
19+
20+
/**
21+
* @see https://github.com/mongodb/specifications/tree/master/source/connections-survive-step-down/tests
22+
*/
23+
class PrimaryStepDownSpecTest extends FunctionalTestCase
24+
{
25+
use SetUpTearDownTrait;
26+
27+
const INTERRUPTED_AT_SHUTDOWN = 11600;
28+
const NOT_MASTER = 10107;
29+
const SHUTDOWN_IN_PROGRESS = 91;
30+
31+
/** @var Client */
32+
private $client;
33+
34+
/** @var Collection */
35+
private $collection;
36+
37+
private function doSetUp()
38+
{
39+
parent::setUp();
40+
41+
$this->client = new Client(static::getUri(), ['retryWrites' => false, 'heartbeatFrequencyMS' => 500, 'serverSelectionTimeoutMS' => 20000, 'serverSelectionTryOnce' => false]);
42+
43+
$this->dropAndRecreateCollection();
44+
$this->collection = $this->client->selectCollection($this->getDatabaseName(), $this->getCollectionName());
45+
}
46+
47+
/**
48+
* @see https://github.com/mongodb/specifications/tree/master/source/connections-survive-step-down/tests#id10
49+
*/
50+
public function testNotMasterKeepsConnectionPool()
51+
{
52+
$runOn = [(object) ['minServerVersion' => '4.1.11', 'topology' => [self::TOPOLOGY_REPLICASET]]];
53+
$this->checkServerRequirements($runOn);
54+
55+
// Set a fail point
56+
$this->configureFailPoint([
57+
'configureFailPoint' => 'failCommand',
58+
'mode' => ['times' => 1],
59+
'data' => [
60+
'failCommands' => ['insert'],
61+
'errorCode' => self::NOT_MASTER,
62+
],
63+
]);
64+
65+
$totalConnectionsCreated = $this->getTotalConnectionsCreated();
66+
67+
// Execute an insert into the test collection of a {test: 1} document.
68+
try {
69+
$this->insertDocuments(1);
70+
} catch (BulkWriteException $e) {
71+
// Verify that the insert failed with an operation failure with 10107 code.
72+
$this->assertSame(self::NOT_MASTER, $e->getCode());
73+
}
74+
75+
// Execute an insert into the test collection of a {test: 1} document and verify that it succeeds.
76+
$result = $this->insertDocuments(1);
77+
$this->assertSame(1, $result->getInsertedCount());
78+
79+
// Verify that the connection pool has not been cleared
80+
$this->assertSame($totalConnectionsCreated, $this->getTotalConnectionsCreated());
81+
}
82+
83+
/**
84+
* @see https://github.com/mongodb/specifications/tree/master/source/connections-survive-step-down/tests#id11
85+
*/
86+
public function testNotMasterResetConnectionPool()
87+
{
88+
$runOn = [(object) ['minServerVersion' => '4.0.0', 'maxServerVersion' => '4.0.999', 'topology' => [self::TOPOLOGY_REPLICASET]]];
89+
$this->checkServerRequirements($runOn);
90+
91+
// Set a fail point
92+
$this->configureFailPoint([
93+
'configureFailPoint' => 'failCommand',
94+
'mode' => ['times' => 1],
95+
'data' => [
96+
'failCommands' => ['insert'],
97+
'errorCode' => self::NOT_MASTER,
98+
],
99+
]);
100+
101+
$totalConnectionsCreated = $this->getTotalConnectionsCreated();
102+
103+
// Execute an insert into the test collection of a {test: 1} document.
104+
try {
105+
$this->insertDocuments(1);
106+
} catch (BulkWriteException $e) {
107+
// Verify that the insert failed with an operation failure with 10107 code.
108+
$this->assertSame(self::NOT_MASTER, $e->getCode());
109+
}
110+
111+
// Verify that the connection pool has been cleared
112+
$this->assertSame($totalConnectionsCreated + 1, $this->getTotalConnectionsCreated());
113+
}
114+
115+
/**
116+
* @see https://github.com/mongodb/specifications/tree/master/source/connections-survive-step-down/tests#id12
117+
*/
118+
public function testShutdownResetConnectionPool()
119+
{
120+
$runOn = [(object) ['minServerVersion' => '4.0.0']];
121+
$this->checkServerRequirements($runOn);
122+
123+
// Set a fail point
124+
$this->configureFailPoint([
125+
'configureFailPoint' => 'failCommand',
126+
'mode' => ['times' => 1],
127+
'data' => [
128+
'failCommands' => ['insert'],
129+
'errorCode' => self::SHUTDOWN_IN_PROGRESS,
130+
],
131+
]);
132+
133+
$totalConnectionsCreated = $this->getTotalConnectionsCreated();
134+
135+
// Execute an insert into the test collection of a {test: 1} document.
136+
try {
137+
$this->insertDocuments(1);
138+
} catch (BulkWriteException $e) {
139+
// Verify that the insert failed with an operation failure with 91 code.
140+
$this->assertSame(self::SHUTDOWN_IN_PROGRESS, $e->getCode());
141+
}
142+
143+
// Verify that the connection pool has been cleared
144+
$this->assertSame($totalConnectionsCreated + 1, $this->getTotalConnectionsCreated());
145+
}
146+
147+
/**
148+
* @see https://github.com/mongodb/specifications/tree/master/source/connections-survive-step-down/tests#id13
149+
*/
150+
public function testInterruptedAtShutdownResetConnectionPool()
151+
{
152+
$runOn = [(object) ['minServerVersion' => '4.0.0']];
153+
$this->checkServerRequirements($runOn);
154+
155+
// Set a fail point
156+
$this->configureFailPoint([
157+
'configureFailPoint' => 'failCommand',
158+
'mode' => ['times' => 1],
159+
'data' => [
160+
'failCommands' => ['insert'],
161+
'errorCode' => self::INTERRUPTED_AT_SHUTDOWN,
162+
],
163+
]);
164+
165+
$totalConnectionsCreated = $this->getTotalConnectionsCreated();
166+
167+
// Execute an insert into the test collection of a {test: 1} document.
168+
try {
169+
$this->insertDocuments(1);
170+
} catch (BulkWriteException $e) {
171+
// Verify that the insert failed with an operation failure with 11600 code.
172+
$this->assertSame(self::INTERRUPTED_AT_SHUTDOWN, $e->getCode());
173+
}
174+
175+
// Verify that the connection pool has been cleared
176+
$this->assertSame($totalConnectionsCreated + 1, $this->getTotalConnectionsCreated());
177+
}
178+
179+
/**
180+
* @see https://github.com/mongodb/specifications/tree/master/source/connections-survive-step-down/tests#id9
181+
*/
182+
public function testGetMoreIteration()
183+
{
184+
$runOn = [(object) ['minServerVersion' => '4.1.11', 'topology' => [self::TOPOLOGY_REPLICASET]]];
185+
$this->checkServerRequirements($runOn);
186+
187+
// Insert 5 documents into a collection with a majority write concern.
188+
$this->insertDocuments(5);
189+
190+
// Start a find operation on the collection with a batch size of 2, and retrieve the first batch of results.
191+
$cursor = $this->collection->find([], ['batchSize' => 2]);
192+
193+
$iterator = new IteratorIterator($cursor);
194+
$iterator->rewind();
195+
$this->assertTrue($iterator->valid());
196+
197+
$iterator->next();
198+
$this->assertTrue($iterator->valid());
199+
200+
$totalConnectionsCreated = $this->getTotalConnectionsCreated();
201+
202+
// Send a {replSetStepDown: 5, force: true} command to the current primary and verify that the command succeeded
203+
$primary = $this->client->getManager()->selectServer(new ReadPreference(ReadPreference::RP_PRIMARY));
204+
$primary->executeCommand('admin', new Command(['replSetStepDown' => 5, 'force' => true]));
205+
206+
// Retrieve the next batch of results from the cursor obtained in the find operation, and verify that this operation succeeded.
207+
$events = [];
208+
$observer = new CommandObserver();
209+
$observer->observe(
210+
function () use ($iterator) {
211+
$iterator->next();
212+
},
213+
function ($event) use (&$events) {
214+
$events[] = $event;
215+
}
216+
);
217+
$this->assertTrue($iterator->valid());
218+
$this->assertCount(1, $events);
219+
$this->assertSame('getMore', $events[0]['started']->getCommandName());
220+
221+
// Verify that no new connections have been created
222+
$this->assertSame($totalConnectionsCreated, $this->getTotalConnectionsCreated($cursor->getServer()));
223+
224+
// Wait to allow primary election to complete and prevent subsequent test failures
225+
$this->waitForMasterReelection();
226+
}
227+
228+
private function insertDocuments($count)
229+
{
230+
$operations = [];
231+
232+
for ($i = 1; $i <= $count; $i++) {
233+
$operations[] = [
234+
BulkWrite::INSERT_ONE => [['test' => $i]],
235+
];
236+
}
237+
238+
return $this->collection->bulkWrite($operations, ['writeConcern' => new WriteConcern('majority')]);
239+
}
240+
241+
private function dropAndRecreateCollection()
242+
{
243+
$this->client->selectCollection($this->getDatabaseName(), $this->getCollectionName())->drop();
244+
$this->client->selectDatabase($this->getDatabaseName())->command(['create' => $this->getCollectionName()]);
245+
}
246+
247+
private function getTotalConnectionsCreated(Server $server = null)
248+
{
249+
$server = $server ?: $this->client->getManager()->selectServer(new ReadPreference('primary'));
250+
251+
$cursor = $server->executeCommand(
252+
$this->getDatabaseName(),
253+
new Command(['serverStatus' => 1]),
254+
new ReadPreference(ReadPreference::RP_PRIMARY)
255+
);
256+
257+
$cursor->setTypeMap(['root' => 'array', 'document' => 'array']);
258+
$document = current($cursor->toArray());
259+
260+
if (isset($document['connections'], $document['connections']['totalCreated'])) {
261+
return (int) $document['connections']['totalCreated'];
262+
}
263+
264+
throw new UnexpectedValueException('Could not determine number of total connections');
265+
}
266+
267+
private function waitForMasterReelection()
268+
{
269+
try {
270+
$this->insertDocuments(1);
271+
272+
return;
273+
} catch (DriverException $e) {
274+
$this->client->getManager()->selectServer(new ReadPreference('primary'));
275+
276+
return;
277+
}
278+
279+
$this->fail('Expected primary to be re-elected within 20 seconds.');
280+
}
281+
}

0 commit comments

Comments
 (0)