Skip to content

Commit f1f816f

Browse files
nbbeekendurran
andauthored
test(NODE-3903): update connections survive stepdown tests to check CMAP events (#4071)
Co-authored-by: Durran Jordan <[email protected]>
1 parent 6248174 commit f1f816f

File tree

1 file changed

+184
-171
lines changed

1 file changed

+184
-171
lines changed
Lines changed: 184 additions & 171 deletions
Original file line numberDiff line numberDiff line change
@@ -1,191 +1,204 @@
11
import { expect } from 'chai';
22

3-
import type { Collection, Db, MongoClient } from '../../mongodb';
4-
import { skipBrokenAuthTestBeforeEachHook } from '../../tools/runner/hooks/configuration';
5-
6-
function ignoreNsNotFound(err) {
7-
if (!err.message.match(/ns not found/)) {
8-
throw err;
9-
}
10-
}
11-
12-
function connectionCount(client) {
13-
return client
14-
.db()
15-
.admin()
16-
.serverStatus()
17-
.then(result => result.connections.totalCreated);
18-
}
19-
20-
function expectPoolWasCleared(initialCount) {
21-
return count => expect(count).to.greaterThan(initialCount);
22-
}
23-
24-
function expectPoolWasNotCleared(initialCount) {
25-
return count => expect(count).to.equal(initialCount);
26-
}
27-
28-
// TODO: NODE-3819: Unskip flaky MacOS tests.
29-
// TODO: NODE-3903: check events as specified in the corresponding prose test description
30-
const maybeDescribe = process.platform === 'darwin' ? describe.skip : describe;
31-
maybeDescribe('Connections survive primary step down - prose', function () {
3+
import {
4+
type Collection,
5+
type ConnectionPoolClearedEvent,
6+
type FindCursor,
7+
type MongoClient,
8+
MONGODB_ERROR_CODES,
9+
MongoServerError,
10+
ReadPreference
11+
} from '../../mongodb';
12+
import { type FailPoint } from '../../tools/utils';
13+
14+
describe('Connections Survive Primary Step Down - prose', function () {
3215
let client: MongoClient;
33-
let checkClient: MongoClient;
34-
let db: Db;
3516
let collection: Collection;
17+
let poolClearedEvents: ConnectionPoolClearedEvent[];
3618

37-
beforeEach(
38-
skipBrokenAuthTestBeforeEachHook({
39-
skippedTests: [
40-
'getMore iteration',
41-
'Not Primary - Keep Connection Pool',
42-
'Not Primary - Reset Connection Pool',
43-
'Shutdown in progress - Reset Connection Pool',
44-
'Interrupted at shutdown - Reset Connection Pool'
45-
]
46-
})
47-
);
48-
49-
beforeEach(function () {
50-
const clientOptions = {
51-
maxPoolSize: 1,
52-
retryWrites: false,
53-
heartbeatFrequencyMS: 100
54-
};
55-
56-
client = this.configuration.newClient(clientOptions);
57-
return client
58-
.db()
59-
.command({ ping: 1 })
60-
.then(() => {
61-
const primary = Array.from(client.topology.description.servers.values()).filter(
62-
sd => sd.type === 'RSPrimary'
63-
)[0];
64-
65-
checkClient = this.configuration.newClient(
66-
`mongodb://${primary.address}/?directConnection=true`,
67-
clientOptions
68-
);
69-
return checkClient.connect();
70-
})
71-
.then(() => {
72-
db = client.db('step-down');
73-
collection = db.collection('step-down');
74-
})
75-
.then(() => collection.drop({ writeConcern: { w: 'majority' } }))
76-
.catch(ignoreNsNotFound)
77-
.then(() => db.createCollection('step-down', { writeConcern: { w: 'majority' } }));
78-
});
19+
afterEach(() => client.close());
7920

80-
let deferred = [];
81-
afterEach(function () {
82-
return Promise.all(deferred.map(d => d())).then(() => {
83-
deferred = [];
84-
return Promise.all([client, checkClient].filter(x => !!x).map(client => client.close()));
85-
});
21+
afterEach(async function () {
22+
const utilClient = this.configuration.newClient();
23+
await utilClient.db('admin').command({ configureFailPoint: 'failCommand', mode: 'off' });
24+
await utilClient.close();
25+
poolClearedEvents = [];
8626
});
8727

88-
it('getMore iteration', {
89-
metadata: {
90-
requires: { mongodb: '>=4.2.0', topology: 'replicaset' }
91-
},
92-
93-
test: function () {
94-
return collection
95-
.insertMany([{ a: 1 }, { a: 2 }, { a: 3 }, { a: 4 }, { a: 5 }], {
96-
writeConcern: { w: 'majority' }
97-
})
98-
.then(result => expect(result.insertedCount).to.equal(5))
99-
.then(() => {
100-
const cursor = collection.find({}, { batchSize: 2 });
101-
deferred.push(() => cursor.close());
102-
103-
return cursor
104-
.next()
105-
.then(item => expect(item.a).to.equal(1))
106-
.then(() => cursor.next())
107-
.then(item => expect(item.a).to.equal(2))
108-
.then(() => {
109-
return connectionCount(checkClient).then(initialConnectionCount => {
110-
return client
111-
.db('admin')
112-
.command({ replSetFreeze: 0 }, { readPreference: 'secondary' })
113-
.then(result => expect(result).property('info').to.equal('unfreezing'))
114-
.then(() =>
115-
client
116-
.db('admin')
117-
.command({ replSetStepDown: 30, force: true }, { readPreference: 'primary' })
118-
)
119-
.then(() => cursor.next())
120-
.then(item => expect(item.a).to.equal(3))
121-
.then(() =>
122-
connectionCount(checkClient).then(
123-
expectPoolWasNotCleared(initialConnectionCount)
124-
)
125-
);
126-
});
127-
});
128-
});
129-
}
28+
beforeEach(async function () {
29+
// For each test, make sure the following steps have been completed before running the actual test:
30+
31+
// - Create a ``MongoClient`` with ``retryWrites=false``
32+
client = this.configuration.newClient({ retryWrites: false, heartbeatFrequencyMS: 500 });
33+
// - Create a collection object from the ``MongoClient``, using ``step-down`` for the database and collection name.
34+
collection = client.db('step-down').collection('step-down');
35+
// - Drop the test collection, using ``writeConcern`` "majority".
36+
await collection.drop({ writeConcern: { w: 'majority' } }).catch(() => null);
37+
// - Execute the "create" command to recreate the collection, using writeConcern: "majority".
38+
collection = await client
39+
.db('step-down')
40+
.createCollection('step-down', { writeConcern: { w: 'majority' } });
41+
42+
poolClearedEvents = [];
43+
client.on('connectionPoolCleared', poolClearEvent => poolClearedEvents.push(poolClearEvent));
13044
});
13145

132-
function runStepownScenario(errorCode, predicate) {
133-
return connectionCount(checkClient).then(initialConnectionCount => {
134-
return client
46+
context('getMore Iteration', { requires: { mongodb: '>4.2', topology: ['replicaset'] } }, () => {
47+
// This test requires a replica set with server version 4.2 or higher.
48+
49+
let cursor: FindCursor;
50+
afterEach(() => cursor.close());
51+
52+
it('survives after primary step down', async () => {
53+
// - Insert 5 documents into a collection with a majority write concern.
54+
await collection.insertMany([{ a: 1 }, { a: 2 }, { a: 3 }, { a: 4 }, { a: 5 }], {
55+
writeConcern: { w: 'majority' }
56+
});
57+
// - Start a find operation on the collection with a batch size of 2, and retrieve the first batch of results.
58+
cursor = collection.find({}, { batchSize: 2 });
59+
expect(await cursor.next()).to.have.property('a', 1);
60+
expect(await cursor.next()).to.have.property('a', 2);
61+
// - Send a `{replSetFreeze: 0}` command to any secondary and verify that the command succeeded.
62+
// This command will unfreeze (because it is set to zero) the secondary and ensure that it will be eligible to be elected immediately.
63+
await client
13564
.db('admin')
136-
.command({
137-
configureFailPoint: 'failCommand',
138-
mode: { times: 1 },
139-
data: { failCommands: ['insert'], errorCode }
140-
})
141-
.then(() => {
142-
deferred.push(() =>
143-
client.db('admin').command({ configureFailPoint: 'failCommand', mode: 'off' })
144-
);
145-
146-
return collection.insertOne({ test: 1 }).then(
147-
() => Promise.reject(new Error('expected an error')),
148-
err => expect(err.code).to.equal(errorCode)
149-
);
150-
})
151-
.then(() => collection.insertOne({ test: 1 }))
152-
.then(() => connectionCount(checkClient).then(predicate(initialConnectionCount)));
65+
.command({ replSetFreeze: 0 }, { readPreference: ReadPreference.secondary });
66+
// - Send a ``{replSetStepDown: 30, force: true}`` command to the current primary and verify that the command succeeded.
67+
await client.db('admin').command({ replSetStepDown: 5, force: true });
68+
// - Retrieve the next batch of results from the cursor obtained in the find operation, and verify that this operation succeeded.
69+
expect(await cursor.next()).to.have.property('a', 3);
70+
// - If the driver implements the `CMAP`_ specification, verify that no new `PoolClearedEvent`_ has been
71+
// published. Otherwise verify that `connections.totalCreated`_ in `serverStatus`_ has not changed.
72+
expect(poolClearedEvents).to.be.empty;
73+
74+
// Referenced python's implementation. Changes from spec:
75+
// replSetStepDown: 5 instead of 30
76+
// Run these inserts to clear NotWritablePrimary issue
77+
// Create client with heartbeatFrequencyMS=500 instead of default of 10_000
78+
79+
// Attempt insertion to mark server description as stale and prevent a
80+
// NotPrimaryError on the subsequent operation.
81+
const error = await collection.insertOne({ a: 6 }).catch(error => error);
82+
expect(error)
83+
.to.be.instanceOf(MongoServerError)
84+
.to.have.property('code', MONGODB_ERROR_CODES.NotWritablePrimary);
85+
86+
// Next insert should succeed on the new primary without clearing pool.
87+
await collection.insertOne({ a: 7 });
88+
89+
expect(poolClearedEvents).to.be.empty;
15390
});
154-
}
155-
156-
it('Not Primary - Keep Connection Pool', {
157-
metadata: {
158-
requires: { mongodb: '>=4.2.0', topology: 'replicaset' }
159-
},
160-
test: function () {
161-
return runStepownScenario(10107, expectPoolWasNotCleared);
162-
}
16391
});
16492

165-
it('Not Primary - Reset Connection Pool', {
166-
metadata: {
167-
requires: { mongodb: '4.0.x', topology: 'replicaset' }
168-
},
169-
test: function () {
170-
return runStepownScenario(10107, expectPoolWasCleared);
93+
context(
94+
'Not Primary - Keep Connection Pool',
95+
{ requires: { mongodb: '>4.2', topology: ['replicaset'] } },
96+
() => {
97+
// This test requires a replica set with server version 4.2 or higher.
98+
99+
// - Set the following fail point: ``{configureFailPoint: "failCommand", mode: {times: 1}, data: {failCommands: ["insert"], errorCode: 10107}}``
100+
const failPoint: FailPoint = {
101+
configureFailPoint: 'failCommand',
102+
mode: { times: 1 },
103+
data: { failCommands: ['insert'], errorCode: 10107 }
104+
};
105+
106+
it('survives after primary step down', async () => {
107+
await client.db('admin').command(failPoint);
108+
// - Execute an insert into the test collection of a ``{test: 1}`` document.
109+
const error = await collection.insertOne({ test: 1 }).catch(error => error);
110+
// - Verify that the insert failed with an operation failure with 10107 code.
111+
expect(error).to.be.instanceOf(MongoServerError).and.has.property('code', 10107);
112+
// - Execute an insert into the test collection of a ``{test: 1}`` document and verify that it succeeds.
113+
await collection.insertOne({ test: 1 });
114+
// - If the driver implements the `CMAP`_ specification, verify that no new `PoolClearedEvent`_ has been
115+
// published. Otherwise verify that `connections.totalCreated`_ in `serverStatus`_ has not changed.
116+
expect(poolClearedEvents).to.be.empty;
117+
});
171118
}
172-
});
119+
);
173120

174-
it('Shutdown in progress - Reset Connection Pool', {
175-
metadata: {
176-
requires: { mongodb: '>=4.0.0', topology: 'replicaset' }
177-
},
178-
test: function () {
179-
return runStepownScenario(91, expectPoolWasCleared);
121+
context(
122+
'Not Primary - Reset Connection Pool',
123+
{ requires: { mongodb: '>=4.0.0 <4.2.0', topology: ['replicaset'] } },
124+
() => {
125+
// This test requires a replica set with server version 4.0.
126+
127+
// - Set the following fail point: ``{configureFailPoint: "failCommand", mode: {times: 1}, data: {failCommands: ["insert"], errorCode: 10107}}``
128+
const failPoint: FailPoint = {
129+
configureFailPoint: 'failCommand',
130+
mode: { times: 1 },
131+
data: { failCommands: ['insert'], errorCode: 10107 }
132+
};
133+
134+
it('survives after primary step down', async () => {
135+
await client.db('admin').command(failPoint);
136+
// - Execute an insert into the test collection of a ``{test: 1}`` document.
137+
const error = await collection.insertOne({ test: 1 }).catch(error => error);
138+
// - Verify that the insert failed with an operation failure with 10107 code.
139+
expect(error).to.be.instanceOf(MongoServerError).and.has.property('code', 10107);
140+
// - If the driver implements the `CMAP`_ specification, verify that a `PoolClearedEvent`_ has been published
141+
expect(poolClearedEvents).to.have.lengthOf(1);
142+
// - Execute an insert into the test collection of a ``{test: 1}`` document and verify that it succeeds.
143+
await collection.insertOne({ test: 1 });
144+
// - If the driver does NOT implement the `CMAP`_ specification, use the `serverStatus`_ command to verify `connections.totalCreated`_ has increased by 1.
145+
});
180146
}
181-
});
147+
);
182148

183-
it('Interrupted at shutdown - Reset Connection Pool', {
184-
metadata: {
185-
requires: { mongodb: '>=4.0.0', topology: 'replicaset' }
186-
},
187-
test: function () {
188-
return runStepownScenario(11600, expectPoolWasCleared);
149+
context(
150+
'Shutdown in progress - Reset Connection Pool',
151+
{ requires: { mongodb: '>=4.0', topology: ['replicaset'] } },
152+
() => {
153+
// This test should be run on all server versions >= 4.0.
154+
155+
// - Set the following fail point: ``{configureFailPoint: "failCommand", mode: {times: 1}, data: {failCommands: ["insert"], errorCode: 91}}``
156+
const failPoint: FailPoint = {
157+
configureFailPoint: 'failCommand',
158+
mode: { times: 1 },
159+
data: { failCommands: ['insert'], errorCode: 91 }
160+
};
161+
162+
it('survives after primary step down', async () => {
163+
await client.db('admin').command(failPoint);
164+
// - Execute an insert into the test collection of a ``{test: 1}`` document.
165+
const error = await collection.insertOne({ test: 1 }).catch(error => error);
166+
// - Verify that the insert failed with an operation failure with 91 code.
167+
expect(error).to.be.instanceOf(MongoServerError).and.has.property('code', 91);
168+
// - If the driver implements the `CMAP`_ specification, verify that a `PoolClearedEvent`_ has been published
169+
expect(poolClearedEvents).to.have.lengthOf(1);
170+
// - Execute an insert into the test collection of a ``{test: 1}`` document and verify that it succeeds.
171+
await collection.insertOne({ test: 1 });
172+
// - If the driver does NOT implement the `CMAP`_ specification, use the `serverStatus`_ command to verify `connections.totalCreated`_ has increased by 1.
173+
});
189174
}
190-
});
175+
);
176+
177+
context(
178+
'Interrupted at shutdown - Reset Connection Pool',
179+
{ requires: { mongodb: '>=4.0', topology: ['replicaset'] } },
180+
() => {
181+
// This test should be run on all server versions >= 4.0.
182+
183+
// - Set the following fail point: ``{configureFailPoint: "failCommand", mode: {times: 1}, data: {failCommands: ["insert"], errorCode: 11600}}``
184+
const failPoint: FailPoint = {
185+
configureFailPoint: 'failCommand',
186+
mode: { times: 1 },
187+
data: { failCommands: ['insert'], errorCode: 11600 }
188+
};
189+
190+
it('survives after primary step down', async () => {
191+
await client.db('admin').command(failPoint);
192+
// - Execute an insert into the test collection of a ``{test: 1}`` document.
193+
const error = await collection.insertOne({ test: 1 }).catch(error => error);
194+
// - Verify that the insert failed with an operation failure with 11600 code.
195+
expect(error).to.be.instanceOf(MongoServerError).and.has.property('code', 11600);
196+
// - If the driver implements the `CMAP`_ specification, verify that a `PoolClearedEvent`_ has been published
197+
expect(poolClearedEvents).to.have.lengthOf(1);
198+
// - Execute an insert into the test collection of a ``{test: 1}`` document and verify that it succeeds.
199+
await collection.insertOne({ test: 1 });
200+
// - If the driver does NOT implement the `CMAP`_ specification, use the `serverStatus`_ command to verify `connections.totalCreated`_ has increased by 1.
201+
});
202+
}
203+
);
191204
});

0 commit comments

Comments
 (0)