Skip to content

Commit e0dc5a0

Browse files
committed
feat(NODE-7452): restrict server deprioritization on replica sets to overload errors
1 parent e5a85d0 commit e0dc5a0

2 files changed

Lines changed: 110 additions & 2 deletions

File tree

src/operations/execute_operation.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
} from '../error';
1818
import type { MongoClient } from '../mongo_client';
1919
import { ReadPreference } from '../read_preference';
20+
import { TopologyType } from '../sdam/common';
2021
import {
2122
DeprioritizedServers,
2223
sameServerSelector,
@@ -304,7 +305,13 @@ async function tryOperation<T extends AbstractOperation, TResult = ResultTypeFro
304305
) {
305306
throw previousOperationError;
306307
}
307-
deprioritizedServers.add(server.description);
308+
if (
309+
topology.description.type === TopologyType.Sharded ||
310+
operationError.hasErrorLabel(MongoErrorLabel.SystemOverloadedError)
311+
) {
312+
deprioritizedServers.add(server.description);
313+
}
314+
308315
previousOperationError = operationError;
309316

310317
// Reset timeouts

test/integration/retryable-reads/retryable_reads.spec.prose.test.ts

Lines changed: 102 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
/* eslint-disable @typescript-eslint/no-non-null-assertion */
22
import { expect } from 'chai';
33

4-
import { type Collection, type MongoClient } from '../../mongodb';
4+
import {
5+
type Collection,
6+
type CommandFailedEvent,
7+
type CommandSucceededEvent,
8+
type MongoClient
9+
} from '../../mongodb';
10+
import { filterForCommands } from '../shared';
511

612
describe('Retryable Reads Spec Prose', () => {
713
let client: MongoClient, failPointName;
@@ -136,4 +142,99 @@ describe('Retryable Reads Spec Prose', () => {
136142
}
137143
});
138144
});
145+
146+
describe('Retrying Reads in a Replica Set', () => {
147+
// These tests verify that server deprioritization on replica sets only occurs
148+
// for SystemOverloadedError errors.
149+
150+
const TEST_METADATA: MongoDBMetadataUI = {
151+
requires: { mongodb: '>=4.2', topology: 'replicaset' }
152+
};
153+
154+
describe('Retryable Reads Caused by Overload Errors Are Retried on a Different Server', () => {
155+
let client: MongoClient;
156+
const commandFailedEvents: CommandFailedEvent[] = [];
157+
const commandSucceededEvents: CommandSucceededEvent[] = [];
158+
159+
beforeEach(async function () {
160+
client = this.configuration.newClient({
161+
retryReads: true,
162+
readPreference: 'primaryPreferred',
163+
monitorCommands: true
164+
});
165+
166+
client.on('commandFailed', filterForCommands('find', commandFailedEvents));
167+
client.on('commandSucceeded', filterForCommands('find', commandSucceededEvents));
168+
169+
await client.db('admin').command({
170+
configureFailPoint: 'failCommand',
171+
mode: { times: 1 },
172+
data: {
173+
failCommands: ['find'],
174+
errorCode: 6,
175+
errorLabels: ['RetryableError', 'SystemOverloadedError']
176+
}
177+
});
178+
179+
commandFailedEvents.length = 0;
180+
commandSucceededEvents.length = 0;
181+
});
182+
183+
afterEach(async function () {
184+
await client?.db('admin').command({ configureFailPoint: 'failCommand', mode: 'off' });
185+
await client?.close();
186+
});
187+
188+
it('retries on a different server when SystemOverloadedError', TEST_METADATA, async () => {
189+
await client.db('test').collection('test').find().toArray();
190+
191+
expect(commandFailedEvents).to.have.lengthOf(1);
192+
expect(commandSucceededEvents).to.have.lengthOf(1);
193+
expect(commandFailedEvents[0].address).to.not.equal(commandSucceededEvents[0].address);
194+
});
195+
});
196+
197+
describe('Retryable Reads Caused by Non-Overload Errors Are Retried on the Same Server', () => {
198+
let client: MongoClient;
199+
const commandFailedEvents: CommandFailedEvent[] = [];
200+
const commandSucceededEvents: CommandSucceededEvent[] = [];
201+
202+
beforeEach(async function () {
203+
client = this.configuration.newClient({
204+
retryReads: true,
205+
readPreference: 'primaryPreferred',
206+
monitorCommands: true
207+
});
208+
209+
client.on('commandFailed', filterForCommands('find', commandFailedEvents));
210+
client.on('commandSucceeded', filterForCommands('find', commandSucceededEvents));
211+
212+
await client.db('admin').command({
213+
configureFailPoint: 'failCommand',
214+
mode: { times: 1 },
215+
data: {
216+
failCommands: ['find'],
217+
errorCode: 6,
218+
errorLabels: ['RetryableError']
219+
}
220+
});
221+
222+
commandFailedEvents.length = 0;
223+
commandSucceededEvents.length = 0;
224+
});
225+
226+
afterEach(async function () {
227+
await client?.db('admin').command({ configureFailPoint: 'failCommand', mode: 'off' });
228+
await client?.close();
229+
});
230+
231+
it('retries on the same server when no SystemOverloadedError', TEST_METADATA, async () => {
232+
await client.db('test').collection('test').find().toArray();
233+
234+
expect(commandFailedEvents).to.have.lengthOf(1);
235+
expect(commandSucceededEvents).to.have.lengthOf(1);
236+
expect(commandFailedEvents[0].address).to.equal(commandSucceededEvents[0].address);
237+
});
238+
});
239+
});
139240
});

0 commit comments

Comments
 (0)