From 023ef43157e4ea620349c5813d6a7c9b6da766eb Mon Sep 17 00:00:00 2001 From: Sergey Zelenov Date: Fri, 20 Feb 2026 14:06:05 +0100 Subject: [PATCH 1/5] feat(NODE-7452): restrict server deprioritization on replica sets to overload errors --- src/operations/execute_operation.ts | 8 +- .../retryable_reads.spec.prose.test.ts | 103 +++++++++++++++++- 2 files changed, 109 insertions(+), 2 deletions(-) diff --git a/src/operations/execute_operation.ts b/src/operations/execute_operation.ts index d4abb13de0..cbc1c45b42 100644 --- a/src/operations/execute_operation.ts +++ b/src/operations/execute_operation.ts @@ -19,6 +19,7 @@ import { } from '../error'; import type { MongoClient } from '../mongo_client'; import { ReadPreference } from '../read_preference'; +import { TopologyType } from '../sdam/common'; import { DeprioritizedServers, sameServerSelector, @@ -342,7 +343,12 @@ async function executeOperationWithRetries< session.unpin({ force: true, forceClear: true }); } - deprioritizedServers.add(server.description); + if ( + topology.description.type === TopologyType.Sharded || + operationError.hasErrorLabel(MongoErrorLabel.SystemOverloadedError) + ) { + deprioritizedServers.add(server.description); + } server = await topology.selectServer(selector, { session, diff --git a/test/integration/retryable-reads/retryable_reads.spec.prose.test.ts b/test/integration/retryable-reads/retryable_reads.spec.prose.test.ts index ca0576d218..74b9ca8c3f 100644 --- a/test/integration/retryable-reads/retryable_reads.spec.prose.test.ts +++ b/test/integration/retryable-reads/retryable_reads.spec.prose.test.ts @@ -1,7 +1,13 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ import { expect } from 'chai'; -import { type Collection, type MongoClient } from '../../mongodb'; +import { + type Collection, + type CommandFailedEvent, + type CommandSucceededEvent, + type MongoClient +} from '../../mongodb'; +import { filterForCommands } from '../shared'; describe('Retryable Reads Spec Prose', () => { let client: MongoClient, failPointName; @@ -136,4 +142,99 @@ describe('Retryable Reads Spec Prose', () => { } }); }); + + describe('Retrying Reads in a Replica Set', () => { + // These tests verify that server deprioritization on replica sets only occurs + // for SystemOverloadedError errors. + + const TEST_METADATA: MongoDBMetadataUI = { + requires: { mongodb: '>=4.2', topology: 'replicaset' } + }; + + describe('Retryable Reads Caused by Overload Errors Are Retried on a Different Server', () => { + let client: MongoClient; + const commandFailedEvents: CommandFailedEvent[] = []; + const commandSucceededEvents: CommandSucceededEvent[] = []; + + beforeEach(async function () { + client = this.configuration.newClient({ + retryReads: true, + readPreference: 'primaryPreferred', + monitorCommands: true + }); + + client.on('commandFailed', filterForCommands('find', commandFailedEvents)); + client.on('commandSucceeded', filterForCommands('find', commandSucceededEvents)); + + await client.db('admin').command({ + configureFailPoint: 'failCommand', + mode: { times: 1 }, + data: { + failCommands: ['find'], + errorCode: 6, + errorLabels: ['RetryableError', 'SystemOverloadedError'] + } + }); + + commandFailedEvents.length = 0; + commandSucceededEvents.length = 0; + }); + + afterEach(async function () { + await client?.db('admin').command({ configureFailPoint: 'failCommand', mode: 'off' }); + await client?.close(); + }); + + it('retries on a different server when SystemOverloadedError', TEST_METADATA, async () => { + await client.db('test').collection('test').find().toArray(); + + expect(commandFailedEvents).to.have.lengthOf(1); + expect(commandSucceededEvents).to.have.lengthOf(1); + expect(commandFailedEvents[0].address).to.not.equal(commandSucceededEvents[0].address); + }); + }); + + describe('Retryable Reads Caused by Non-Overload Errors Are Retried on the Same Server', () => { + let client: MongoClient; + const commandFailedEvents: CommandFailedEvent[] = []; + const commandSucceededEvents: CommandSucceededEvent[] = []; + + beforeEach(async function () { + client = this.configuration.newClient({ + retryReads: true, + readPreference: 'primaryPreferred', + monitorCommands: true + }); + + client.on('commandFailed', filterForCommands('find', commandFailedEvents)); + client.on('commandSucceeded', filterForCommands('find', commandSucceededEvents)); + + await client.db('admin').command({ + configureFailPoint: 'failCommand', + mode: { times: 1 }, + data: { + failCommands: ['find'], + errorCode: 6, + errorLabels: ['RetryableError'] + } + }); + + commandFailedEvents.length = 0; + commandSucceededEvents.length = 0; + }); + + afterEach(async function () { + await client?.db('admin').command({ configureFailPoint: 'failCommand', mode: 'off' }); + await client?.close(); + }); + + it('retries on the same server when no SystemOverloadedError', TEST_METADATA, async () => { + await client.db('test').collection('test').find().toArray(); + + expect(commandFailedEvents).to.have.lengthOf(1); + expect(commandSucceededEvents).to.have.lengthOf(1); + expect(commandFailedEvents[0].address).to.equal(commandSucceededEvents[0].address); + }); + }); + }); }); From 7fd21ca7084f1f9f230270e792baf94622c6bba7 Mon Sep 17 00:00:00 2001 From: Sergey Zelenov Date: Fri, 20 Feb 2026 14:49:40 +0100 Subject: [PATCH 2/5] re-run only one test with debug log --- .../retryable-reads/retryable_reads.spec.prose.test.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/test/integration/retryable-reads/retryable_reads.spec.prose.test.ts b/test/integration/retryable-reads/retryable_reads.spec.prose.test.ts index 74b9ca8c3f..d6a2c75d1f 100644 --- a/test/integration/retryable-reads/retryable_reads.spec.prose.test.ts +++ b/test/integration/retryable-reads/retryable_reads.spec.prose.test.ts @@ -151,7 +151,7 @@ describe('Retryable Reads Spec Prose', () => { requires: { mongodb: '>=4.2', topology: 'replicaset' } }; - describe('Retryable Reads Caused by Overload Errors Are Retried on a Different Server', () => { + describe.only('Retryable Reads Caused by Overload Errors Are Retried on a Different Server', () => { let client: MongoClient; const commandFailedEvents: CommandFailedEvent[] = []; const commandSucceededEvents: CommandSucceededEvent[] = []; @@ -166,6 +166,8 @@ describe('Retryable Reads Spec Prose', () => { client.on('commandFailed', filterForCommands('find', commandFailedEvents)); client.on('commandSucceeded', filterForCommands('find', commandSucceededEvents)); + await client.connect(); + await client.db('admin').command({ configureFailPoint: 'failCommand', mode: { times: 1 }, @@ -188,6 +190,8 @@ describe('Retryable Reads Spec Prose', () => { it('retries on a different server when SystemOverloadedError', TEST_METADATA, async () => { await client.db('test').collection('test').find().toArray(); + console.log('failed event:', JSON.stringify(commandFailedEvents[0])); + expect(commandFailedEvents).to.have.lengthOf(1); expect(commandSucceededEvents).to.have.lengthOf(1); expect(commandFailedEvents[0].address).to.not.equal(commandSucceededEvents[0].address); @@ -209,6 +213,8 @@ describe('Retryable Reads Spec Prose', () => { client.on('commandFailed', filterForCommands('find', commandFailedEvents)); client.on('commandSucceeded', filterForCommands('find', commandSucceededEvents)); + await client.connect(); + await client.db('admin').command({ configureFailPoint: 'failCommand', mode: { times: 1 }, From 5446d25cddcb7d2e9fe360f828e977544756efd4 Mon Sep 17 00:00:00 2001 From: Sergey Zelenov Date: Fri, 20 Feb 2026 14:58:16 +0100 Subject: [PATCH 3/5] do not serialize the entire error --- .../retryable-reads/retryable_reads.spec.prose.test.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/test/integration/retryable-reads/retryable_reads.spec.prose.test.ts b/test/integration/retryable-reads/retryable_reads.spec.prose.test.ts index d6a2c75d1f..773c59d11a 100644 --- a/test/integration/retryable-reads/retryable_reads.spec.prose.test.ts +++ b/test/integration/retryable-reads/retryable_reads.spec.prose.test.ts @@ -190,7 +190,14 @@ describe('Retryable Reads Spec Prose', () => { it('retries on a different server when SystemOverloadedError', TEST_METADATA, async () => { await client.db('test').collection('test').find().toArray(); - console.log('failed event:', JSON.stringify(commandFailedEvents[0])); + console.log('failed event:', { + address: commandFailedEvents[0].address, + failure: { + name: commandFailedEvents[0].failure.name, + code: (commandFailedEvents[0].failure as any).code, + labels: (commandFailedEvents[0].failure as any).errorLabels + } + }); expect(commandFailedEvents).to.have.lengthOf(1); expect(commandSucceededEvents).to.have.lengthOf(1); From d9c3933173752a2aed43f820f6d852025596dabe Mon Sep 17 00:00:00 2001 From: Sergey Zelenov Date: Fri, 20 Feb 2026 15:11:40 +0100 Subject: [PATCH 4/5] update mongodb version --- .../retryable_reads.spec.prose.test.ts | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/test/integration/retryable-reads/retryable_reads.spec.prose.test.ts b/test/integration/retryable-reads/retryable_reads.spec.prose.test.ts index 773c59d11a..5277a1b453 100644 --- a/test/integration/retryable-reads/retryable_reads.spec.prose.test.ts +++ b/test/integration/retryable-reads/retryable_reads.spec.prose.test.ts @@ -148,10 +148,10 @@ describe('Retryable Reads Spec Prose', () => { // for SystemOverloadedError errors. const TEST_METADATA: MongoDBMetadataUI = { - requires: { mongodb: '>=4.2', topology: 'replicaset' } + requires: { mongodb: '>=4.4', topology: 'replicaset' } }; - describe.only('Retryable Reads Caused by Overload Errors Are Retried on a Different Server', () => { + describe('Retryable Reads Caused by Overload Errors Are Retried on a Different Server', () => { let client: MongoClient; const commandFailedEvents: CommandFailedEvent[] = []; const commandSucceededEvents: CommandSucceededEvent[] = []; @@ -190,15 +190,6 @@ describe('Retryable Reads Spec Prose', () => { it('retries on a different server when SystemOverloadedError', TEST_METADATA, async () => { await client.db('test').collection('test').find().toArray(); - console.log('failed event:', { - address: commandFailedEvents[0].address, - failure: { - name: commandFailedEvents[0].failure.name, - code: (commandFailedEvents[0].failure as any).code, - labels: (commandFailedEvents[0].failure as any).errorLabels - } - }); - expect(commandFailedEvents).to.have.lengthOf(1); expect(commandSucceededEvents).to.have.lengthOf(1); expect(commandFailedEvents[0].address).to.not.equal(commandSucceededEvents[0].address); From af377b0355d3055b0ea3caa7e16825d3b7f967f6 Mon Sep 17 00:00:00 2001 From: Sergey Zelenov Date: Tue, 24 Feb 2026 13:32:44 +0100 Subject: [PATCH 5/5] add steps of prose tests --- .../retryable_reads.spec.prose.test.ts | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/test/integration/retryable-reads/retryable_reads.spec.prose.test.ts b/test/integration/retryable-reads/retryable_reads.spec.prose.test.ts index 5277a1b453..ac63ad2470 100644 --- a/test/integration/retryable-reads/retryable_reads.spec.prose.test.ts +++ b/test/integration/retryable-reads/retryable_reads.spec.prose.test.ts @@ -157,6 +157,8 @@ describe('Retryable Reads Spec Prose', () => { const commandSucceededEvents: CommandSucceededEvent[] = []; beforeEach(async function () { + // 1. Create a client `client` with `retryReads=true`, `readPreference=primaryPreferred`, and command event monitoring + // enabled. client = this.configuration.newClient({ retryReads: true, readPreference: 'primaryPreferred', @@ -168,6 +170,18 @@ describe('Retryable Reads Spec Prose', () => { await client.connect(); + /* + * 2. Configure the following fail point for `client`: + { + configureFailPoint: "failCommand", + mode: { times: 1 }, + data: { + failCommands: ["find"], + errorLabels: ["RetryableError", "SystemOverloadedError"] + errorCode: 6 + } + } + * */ await client.db('admin').command({ configureFailPoint: 'failCommand', mode: { times: 1 }, @@ -178,6 +192,7 @@ describe('Retryable Reads Spec Prose', () => { } }); + // 3. Reset the command event monitor to clear the failpoint command from its stored events. commandFailedEvents.length = 0; commandSucceededEvents.length = 0; }); @@ -188,10 +203,14 @@ describe('Retryable Reads Spec Prose', () => { }); it('retries on a different server when SystemOverloadedError', TEST_METADATA, async () => { + // 4. Execute a `find` command with `client`. await client.db('test').collection('test').find().toArray(); + // 5. Assert that one failed command event and one successful command event occurred. expect(commandFailedEvents).to.have.lengthOf(1); expect(commandSucceededEvents).to.have.lengthOf(1); + + // 6. Assert that both events occurred on different servers. expect(commandFailedEvents[0].address).to.not.equal(commandSucceededEvents[0].address); }); }); @@ -202,6 +221,8 @@ describe('Retryable Reads Spec Prose', () => { const commandSucceededEvents: CommandSucceededEvent[] = []; beforeEach(async function () { + // 1. Create a client `client` with `retryReads=true`, `readPreference=primaryPreferred`, and command event monitoring + // enabled. client = this.configuration.newClient({ retryReads: true, readPreference: 'primaryPreferred', @@ -213,6 +234,18 @@ describe('Retryable Reads Spec Prose', () => { await client.connect(); + /* + * 2. Configure the following fail point for `client`: + { + configureFailPoint: "failCommand", + mode: { times: 1 }, + data: { + failCommands: ["find"], + errorLabels: ["RetryableError"] + errorCode: 6 + } + } + * */ await client.db('admin').command({ configureFailPoint: 'failCommand', mode: { times: 1 }, @@ -223,6 +256,7 @@ describe('Retryable Reads Spec Prose', () => { } }); + // 3. Reset the command event monitor to clear the failpoint command from its stored events. commandFailedEvents.length = 0; commandSucceededEvents.length = 0; }); @@ -233,10 +267,14 @@ describe('Retryable Reads Spec Prose', () => { }); it('retries on the same server when no SystemOverloadedError', TEST_METADATA, async () => { + // 4. Execute a `find` command with `client`. await client.db('test').collection('test').find().toArray(); + // 5. Assert that one failed command event and one successful command event occurred. expect(commandFailedEvents).to.have.lengthOf(1); expect(commandSucceededEvents).to.have.lengthOf(1); + + // 6. Assert that both events occurred on the same server. expect(commandFailedEvents[0].address).to.equal(commandSucceededEvents[0].address); }); });