Skip to content

Commit e7f2c48

Browse files
committed
test(NODE-3903): update connections survive stepdown tests to check CMAP events
1 parent ce55ca9 commit e7f2c48

File tree

1 file changed

+181
-172
lines changed

1 file changed

+181
-172
lines changed
Lines changed: 181 additions & 172 deletions
Original file line numberDiff line numberDiff line change
@@ -1,191 +1,200 @@
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-
});
79-
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-
});
19+
afterEach(async () => {
20+
await client.db('admin').command({ configureFailPoint: 'failCommand', mode: 'off' });
21+
poolClearedEvents = [];
22+
await client.close();
8623
});
8724

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-
}
25+
beforeEach(async function () {
26+
// For each test, make sure the following steps have been completed before running the actual test:
27+
28+
// - Create a ``MongoClient`` with ``retryWrites=false``
29+
client = this.configuration.newClient({ retryWrites: false });
30+
// - Create a collection object from the ``MongoClient``, using ``step-down`` for the database and collection name.
31+
collection = client.db('step-down').collection('step-down');
32+
// - Drop the test collection, using ``writeConcern`` "majority".
33+
await collection.drop({ writeConcern: { w: 'majority' } }).catch(() => null);
34+
// - Execute the "create" command to recreate the collection, using writeConcern: "majority".
35+
collection = await client
36+
.db('step-down')
37+
.createCollection('step-down', { writeConcern: { w: 'majority' } });
38+
39+
poolClearedEvents = [];
40+
client.on('connectionPoolCleared', poolClearEvent => poolClearedEvents.push(poolClearEvent));
13041
});
13142

132-
function runStepownScenario(errorCode, predicate) {
133-
return connectionCount(checkClient).then(initialConnectionCount => {
134-
return client
43+
context('getMore Iteration', { requires: { mongodb: '>4.2', topology: ['replicaset'] } }, () => {
44+
// This test requires a replica set with server version 4.2 or higher.
45+
46+
let cursor: FindCursor;
47+
afterEach(() => cursor.close());
48+
49+
it('survives after primary step down', async () => {
50+
// - Insert 5 documents into a collection with a majority write concern.
51+
await collection.insertMany([{ a: 1 }, { a: 2 }, { a: 3 }, { a: 4 }, { a: 5 }], {
52+
writeConcern: { w: 'majority' }
53+
});
54+
// - Start a find operation on the collection with a batch size of 2, and retrieve the first batch of results.
55+
cursor = collection.find({}, { batchSize: 2 });
56+
expect(await cursor.next()).to.have.property('a', 1);
57+
expect(await cursor.next()).to.have.property('a', 2);
58+
// - Send a `{replSetFreeze: 0}` command to any secondary and verify that the command succeeded.
59+
// This command will unfreeze (because it is set to zero) the secondary and ensure that it will be eligible to be elected immediately.
60+
await client
13561
.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)));
62+
.command({ replSetFreeze: 0 }, { readPreference: ReadPreference.secondary });
63+
// - Send a ``{replSetStepDown: 30, force: true}`` command to the current primary and verify that the command succeeded.
64+
await client.db('admin').command({ replSetStepDown: 5, force: true });
65+
// - Retrieve the next batch of results from the cursor obtained in the find operation, and verify that this operation succeeded.
66+
expect(await cursor.next()).to.have.property('a', 3);
67+
// - If the driver implements the `CMAP`_ specification, verify that no new `PoolClearedEvent`_ has been
68+
// published. Otherwise verify that `connections.totalCreated`_ in `serverStatus`_ has not changed.
69+
expect(poolClearedEvents).to.be.empty;
70+
71+
// Referenced python's implementation. Changes from spec:
72+
// replSetStepDown: 5 instead of 30
73+
// Run these inserts to clear NotWritablePrimary issue
74+
75+
// Attempt insertion to mark server description as stale and prevent a
76+
// NotPrimaryError on the subsequent operation.
77+
const error = await collection.insertOne({ a: 6 }).catch(error => error);
78+
expect(error)
79+
.to.be.instanceOf(MongoServerError)
80+
.to.have.property('code', MONGODB_ERROR_CODES.NotWritablePrimary);
81+
82+
// Next insert should succeed on the new primary without clearing pool.
83+
await collection.insertOne({ a: 7 });
84+
85+
expect(poolClearedEvents).to.be.empty;
15386
});
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-
}
16387
});
16488

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

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

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

0 commit comments

Comments
 (0)