Skip to content

Commit bdc0d67

Browse files
authored
fix(NODE-3144): pool clear event ordering and retryability tests (#3407)
1 parent b8b765b commit bdc0d67

File tree

4 files changed

+318
-53
lines changed

4 files changed

+318
-53
lines changed

src/cmap/connection_pool.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -423,11 +423,10 @@ export class ConnectionPool extends TypedEventEmitter<ConnectionPoolEvents> {
423423
this[kPoolState] = PoolState.paused;
424424

425425
this.clearMinPoolSizeTimer();
426-
this.processWaitQueue();
427-
428426
if (!alreadyPaused) {
429427
this.emit(ConnectionPool.CONNECTION_POOL_CLEARED, new ConnectionPoolClearedEvent(this));
430428
}
429+
this.processWaitQueue();
431430
}
432431

433432
/** Close the pool */

src/cmap/errors.ts

-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ export class PoolClosedError extends MongoDriverError {
2424
* @category Error
2525
*/
2626
export class PoolClearedError extends MongoNetworkError {
27-
// TODO(NODE-3144): needs to extend RetryableError or be marked retryable in some other way per spec
2827
/** The address of the connection pool */
2928
address: string;
3029

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
/* eslint-disable @typescript-eslint/no-non-null-assertion */
2+
import { expect } from 'chai';
3+
4+
import { Collection, MongoClient } from '../../../src';
5+
6+
describe('Retryable Reads Spec Prose', () => {
7+
let client: MongoClient, failPointName;
8+
9+
afterEach(async () => {
10+
try {
11+
if (failPointName) {
12+
await client.db('admin').command({ configureFailPoint: failPointName, mode: 'off' });
13+
}
14+
} finally {
15+
failPointName = undefined;
16+
await client?.close();
17+
}
18+
});
19+
20+
describe('PoolClearedError Retryability Test', () => {
21+
// This test will be used to ensure drivers properly retry after encountering PoolClearedErrors.
22+
// It MUST be implemented by any driver that implements the CMAP specification.
23+
// This test requires MongoDB 4.2.9+ for blockConnection support in the failpoint.
24+
25+
let cmapEvents: Array<{ name: string; event: Record<string, any> }>;
26+
let commandStartedEvents: Array<Record<string, any>>;
27+
let testCollection: Collection;
28+
beforeEach(async function () {
29+
// 1. Create a client with maxPoolSize=1 and retryReads=true.
30+
client = this.configuration.newClient(
31+
this.configuration.url({
32+
useMultipleMongoses: false // If testing against a sharded deployment, be sure to connect to only a single mongos.
33+
}),
34+
{ maxPoolSize: 1, retryReads: true, monitorCommands: true }
35+
);
36+
37+
console.log(client.options);
38+
39+
testCollection = client.db('retryable-reads-prose').collection('pool-clear-retry');
40+
await testCollection.drop().catch(() => null);
41+
await testCollection.insertMany([{ test: 1 }, { test: 2 }]);
42+
43+
// 2. Enable the following failpoint:
44+
// NOTE: "9. Disable the failpoint" is done in afterEach
45+
failPointName = 'failCommand';
46+
const failPoint = await client.db('admin').command({
47+
configureFailPoint: failPointName,
48+
mode: { times: 1 },
49+
data: {
50+
failCommands: ['find'],
51+
errorCode: 91,
52+
blockConnection: true,
53+
blockTimeMS: 1000
54+
}
55+
});
56+
57+
expect(failPoint).to.have.property('ok', 1);
58+
59+
cmapEvents = [];
60+
commandStartedEvents = [];
61+
for (const observedEvent of [
62+
'connectionCheckOutStarted',
63+
'connectionCheckedOut',
64+
'connectionCheckOutFailed',
65+
'connectionPoolCleared'
66+
]) {
67+
client.on(observedEvent, ev => {
68+
cmapEvents.push({ name: observedEvent, event: ev });
69+
});
70+
}
71+
72+
client.on('commandStarted', ev => {
73+
commandStartedEvents.push(ev);
74+
});
75+
});
76+
77+
it('should emit events in the expected sequence', {
78+
metadata: { requires: { mongodb: '>=4.2.9', topology: '!load-balanced' } },
79+
test: async function () {
80+
// 3. Start two threads and attempt to perform a findOne simultaneously on both.
81+
const results = await Promise.all([
82+
testCollection.findOne({ test: 1 }),
83+
testCollection.findOne({ test: 2 })
84+
]);
85+
86+
client.removeAllListeners();
87+
// 4. Verify that both findOne attempts succeed.
88+
expect(results[0]).to.have.property('test', 1);
89+
expect(results[1]).to.have.property('test', 2);
90+
91+
// NOTE: For the subsequent checks, we rely on the exact sequence of ALL events
92+
// for ease of readability; however, only the relative order matters for
93+
// the purposes of this test, so if this ever becomes an issue, the test
94+
// can be refactored to assert on relative index values instead
95+
96+
// 5. Via CMAP monitoring, assert that the first check out succeeds.
97+
expect(cmapEvents.shift()).to.have.property(
98+
'name',
99+
'connectionCheckOutStarted',
100+
'expected 1) checkout 1 to start'
101+
);
102+
expect(cmapEvents.shift()).to.have.property(
103+
'name',
104+
'connectionCheckOutStarted',
105+
'expected 2) checkout 2 to start'
106+
);
107+
expect(cmapEvents.shift()).to.have.property(
108+
'name',
109+
'connectionCheckedOut',
110+
'expected 3) first checkout to succeed'
111+
);
112+
113+
// 6. Via CMAP monitoring, assert that a PoolClearedEvent is then emitted.
114+
expect(cmapEvents.shift()).to.have.property(
115+
'name',
116+
'connectionPoolCleared',
117+
'expected 4) pool to clear'
118+
);
119+
120+
// 7. Via CMAP monitoring, assert that the second check out then fails due to a connection error.
121+
const nextEvent = cmapEvents.shift();
122+
expect(nextEvent).to.have.property(
123+
'name',
124+
'connectionCheckOutFailed',
125+
'expected 5) checkout 2 to fail'
126+
);
127+
expect(nextEvent!.event).to.have.property('reason', 'connectionError');
128+
129+
// 8. Via Command Monitoring, assert that exactly three find CommandStartedEvents were observed in total.
130+
const observedFindCommandStartedEvents = commandStartedEvents.filter(
131+
({ commandName }) => commandName === 'find'
132+
);
133+
expect(observedFindCommandStartedEvents).to.have.lengthOf(
134+
3,
135+
'expected 3 find command started events'
136+
);
137+
}
138+
});
139+
});
140+
});

0 commit comments

Comments
 (0)