Skip to content

Commit 1f4b5c1

Browse files
committed
PHPLIB-467: Connections survive primary stepdown
1 parent 67ffd33 commit 1f4b5c1

File tree

1 file changed

+274
-0
lines changed

1 file changed

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

0 commit comments

Comments
 (0)