Skip to content

Commit 03eef99

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

File tree

1 file changed

+278
-0
lines changed

1 file changed

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

0 commit comments

Comments
 (0)