From 0e2da13b39379ed8df680dcbc7d915f6de5d4053 Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Thu, 16 Apr 2026 10:15:05 -0400 Subject: [PATCH 1/5] NODE-7534: Add prose tests for retry behavior with mixed overload/non-overload errors - Add 4 new prose tests per spec commit 7039e69 - Test 4 (reads/writes): Verify MAX_RETRIES applies to all errors after overload - Test 5 (reads/writes): Verify backoff only applies to overload errors - Uses mocking pattern consistent with existing prose tests --- .../retryable_reads.spec.prose.test.ts | 154 +++++++++++++++++- .../retryable_writes.spec.prose.test.ts | 135 ++++++++++++++- 2 files changed, 287 insertions(+), 2 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 ac63ad24704..782f41349ec 100644 --- a/test/integration/retryable-reads/retryable_reads.spec.prose.test.ts +++ b/test/integration/retryable-reads/retryable_reads.spec.prose.test.ts @@ -1,12 +1,18 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ import { expect } from 'chai'; +import * as sinon from 'sinon'; import { type Collection, type CommandFailedEvent, type CommandSucceededEvent, - type MongoClient + MAX_RETRIES, + type MongoClient, + MongoErrorLabel, + MongoServerError, + Server } from '../../mongodb'; +import { measureDuration } from '../../tools/utils'; import { filterForCommands } from '../shared'; describe('Retryable Reads Spec Prose', () => { @@ -279,4 +285,150 @@ describe('Retryable Reads Spec Prose', () => { }); }); }); + + describe('4: Test that drivers set the maximum number of retries for all retryable read errors when an overload error is encountered', () => { + // This test MUST be executed against a MongoDB 4.4+ server that supports `retryReads=true` and has enabled the + // `configureFailPoint` command with the `errorLabels` option. + + const TEST_METADATA: MongoDBMetadataUI = { + requires: { mongodb: '>=4.4' } + }; + + let client: MongoClient; + + beforeEach(async function () { + // 1. Create a client. + client = this.configuration.newClient({ + monitorCommands: true + }); + await client.connect(); + }); + + afterEach(async function () { + sinon.restore(); + await client?.close(); + }); + + it( + 'should retry MAX_RETRIES times for all retryable errors after encountering an overload error', + TEST_METADATA, + async () => { + // 2. Configure a fail point with error code `91` (ShutdownInProgress) with the `RetryableError` and + // `SystemOverloadedError` error labels: + + // 3. Via the command monitoring CommandFailedEvent, configure a fail point with error code `91` (ShutdownInProgress) and + // the `RetryableError` label: + + // We use mocking to simulate the failpoint sequence: + // - First call: error WITH SystemOverloadedError + // - Subsequent calls: error WITHOUT SystemOverloadedError (but still retryable) + const serverCommandStub = sinon + .stub(Server.prototype, 'command') + .callsFake(async function () { + const errorLabels = + serverCommandStub.callCount === 1 + ? [MongoErrorLabel.RetryableError, MongoErrorLabel.SystemOverloadedError] + : [MongoErrorLabel.RetryableError]; + + throw new MongoServerError({ + message: 'Server Error', + errorLabels, + code: 91, + ok: 0 + }); + }); + + // 4. Attempt a `findOne` operation on any record for any database and collection. Expect the `findOne` to fail with a + // server error. Assert that `MAX_RETRIES + 1` attempts were made. + const error = await client + .db('test') + .collection('test') + .findOne({}) + .catch(e => e); + + expect(error).to.exist; + expect(serverCommandStub.callCount).to.equal(MAX_RETRIES + 1); + } + ); + }); + + describe('5: Test that drivers do not apply backoff to non-overload errors', () => { + // This test MUST be executed against a MongoDB 4.4+ server that supports `retryReads=true` and has enabled the + // `configureFailPoint` command with the `errorLabels` option. + + const TEST_METADATA: MongoDBMetadataUI = { + requires: { mongodb: '>=4.4' } + }; + + let client: MongoClient; + + beforeEach(async function () { + // 1. Create a client. + client = this.configuration.newClient({ + monitorCommands: true + }); + await client.connect(); + }); + + afterEach(async function () { + sinon.restore(); + await client?.close(); + }); + + it( + 'should apply backoff only once for the initial overload error and not for subsequent non-overload retryable errors', + TEST_METADATA, + async function () { + // Configure the random number generator used for jitter to always return a number as close as possible to `1`. + const randomStub = sinon.stub(Math, 'random'); + randomStub.returns(0.99); + + // 2. Configure a fail point with error code `91` (ShutdownInProgress) with the `RetryableError` and + // `SystemOverloadedError` error labels: + + // 3. Via the command monitoring CommandFailedEvent, configure a fail point with error code `91` (ShutdownInProgress) and + // the `RetryableError` label: + + // We use mocking to simulate the failpoint sequence: + // - First call: error WITH SystemOverloadedError + // - Subsequent calls: error WITHOUT SystemOverloadedError (but still retryable) + const serverCommandStub = sinon + .stub(Server.prototype, 'command') + .callsFake(async function () { + const errorLabels = + serverCommandStub.callCount === 1 + ? [MongoErrorLabel.RetryableError, MongoErrorLabel.SystemOverloadedError] + : [MongoErrorLabel.RetryableError]; + + throw new MongoServerError({ + message: 'Server Error', + errorLabels, + code: 91, + ok: 0 + }); + }); + + // 4. Attempt a `findOne` operation on any record for any database and collection. Expect the `findOne` to fail with a + // server error. Assert that backoff was applied only once for the initial overload error and not for the subsequent + // non-overload retryable errors. + const { duration } = await measureDuration(async () => { + const error = await client + .db('test') + .collection('test') + .findOne({}) + .catch(e => e); + expect(error).to.exist; + }); + + // The expected backoff for the first (overload) error is: Math.random() * Math.min(10000, 100 * 2^0) + // With Math.random() = 0.99, this gives us: 0.99 * 100 = 99ms + // Subsequent errors are non-overload, so they should have NO backoff applied. + // We add a margin for test execution overhead. + const expectedMinBackoff = 99; // First backoff + const expectedMaxBackoff = expectedMinBackoff + 1000; // Allow 1 second margin for test overhead + + expect(duration).to.be.within(expectedMinBackoff, expectedMaxBackoff); + } + ); + }); }); diff --git a/test/integration/retryable-writes/retryable_writes.spec.prose.test.ts b/test/integration/retryable-writes/retryable_writes.spec.prose.test.ts index 5f60712a1d9..d8fd5b8c7aa 100644 --- a/test/integration/retryable-writes/retryable_writes.spec.prose.test.ts +++ b/test/integration/retryable-writes/retryable_writes.spec.prose.test.ts @@ -6,6 +6,7 @@ import * as sinon from 'sinon'; import { type Collection, + MAX_RETRIES, type MongoClient, MongoError, MongoErrorLabel, @@ -13,7 +14,7 @@ import { MongoWriteConcernError, Server } from '../../mongodb'; -import { sleep } from '../../tools/utils'; +import { measureDuration, sleep } from '../../tools/utils'; describe('Retryable Writes Spec Prose', () => { describe('1. Test that retryable writes raise an exception when using the MMAPv1 storage engine.', () => { @@ -551,5 +552,137 @@ describe('Retryable Writes Spec Prose', () => { expect(insertResult.errorLabels).to.not.include(MongoErrorLabel.NoWritesPerformed); } ); + + it( + 'Case 4: Test that drivers set the maximum number of retries for all retryable write errors when an overload error is encountered', + { requires: { topology: 'replicaset', mongodb: '>=6.0' } }, + async () => { + // 2. Configure a fail point with error code `91` (ShutdownInProgress) with the `RetryableError` and + // `SystemOverloadedError` error labels: + // ```javascript + // { + // configureFailPoint: "failCommand", + // mode: {times: 1}, + // data: { + // failCommands: ["insert"], + // errorLabels: ["RetryableError", "SystemOverloadedError"], + // errorCode: 91 + // } + // } + // ``` + + // 3. Via the command monitoring CommandFailedEvent, configure a fail point with error code `91` (ShutdownInProgress) and + // the `RetryableWriteError` and `RetryableError` labels: + // ```javascript + // { + // configureFailPoint: "failCommand", + // mode: "alwaysOn", + // data: { + // failCommands: ["insert"], + // errorLabels: ["RetryableError", "RetryableWriteError"], + // errorCode: 91 + // } + // } + // ``` + // Configure the second fail point command only if the failed event is for the first error configured in step 2. + const serverCommandStub = sinon + .stub(Server.prototype, 'command') + .callsFake(async function () { + // First call: error WITH SystemOverloadedError + // Subsequent calls: error WITHOUT SystemOverloadedError (but still retryable) + const errorLabels = + serverCommandStub.callCount === 1 + ? [MongoErrorLabel.RetryableError, MongoErrorLabel.SystemOverloadedError] + : [MongoErrorLabel.RetryableError, MongoErrorLabel.RetryableWriteError]; + + throw new MongoServerError({ + message: 'Server Error', + errorLabels, + code: 91, + ok: 0 + }); + }); + + // 4. Attempt an `insertOne` operation on any record for any database and collection. Expect the `insertOne` to fail with a + // server error. Assert that `MAX_RETRIES + 1` attempts were made. + const insertResult = await collection.insertOne({ _id: 1 }).catch(error => error); + + expect(insertResult).to.be.instanceOf(MongoServerError); + expect(serverCommandStub.callCount).to.equal(MAX_RETRIES + 1); + } + ); + + it( + 'Case 5: Test that drivers do not apply backoff to non-overload errors', + { requires: { topology: 'replicaset', mongodb: '>=6.0' } }, + async function () { + // Configure the random number generator used for jitter to always return a number as close as possible to `1`. + const stub = sinon.stub(Math, 'random'); + stub.returns(0.99); + + // 2. Configure a fail point with error code `91` (ShutdownInProgress) with the `RetryableError` and + // `SystemOverloadedError` error labels: + // ```javascript + // { + // configureFailPoint: "failCommand", + // mode: {times: 1}, + // data: { + // failCommands: ["insert"], + // errorLabels: ["RetryableError", "SystemOverloadedError"], + // errorCode: 91 + // } + // } + // ``` + + // 3. Via the command monitoring CommandFailedEvent, configure a fail point with error code `91` (ShutdownInProgress) and + // the `RetryableWriteError` and `RetryableError` labels: + // ```javascript + // { + // configureFailPoint: "failCommand", + // mode: "alwaysOn", + // data: { + // failCommands: ["insert"], + // errorLabels: ["RetryableError", "RetryableWriteError"], + // errorCode: 91 + // } + // } + // ``` + // Configure the second fail point command only if the failed event is for the first error configured in step 2. + const serverCommandStub = sinon + .stub(Server.prototype, 'command') + .callsFake(async function () { + // First call: error WITH SystemOverloadedError + // Subsequent calls: error WITHOUT SystemOverloadedError (but still retryable) + const errorLabels = + serverCommandStub.callCount === 1 + ? [MongoErrorLabel.RetryableError, MongoErrorLabel.SystemOverloadedError] + : [MongoErrorLabel.RetryableError, MongoErrorLabel.RetryableWriteError]; + + throw new MongoServerError({ + message: 'Server Error', + errorLabels, + code: 91, + ok: 0 + }); + }); + + // 4. Attempt an `insertOne` operation on any record for any database and collection. Expect the `insertOne` to fail with a + // server error. Assert that backoff was applied only once for the initial overload error and not for the subsequent + // non-overload retryable errors. + const { duration } = await measureDuration(async () => { + const insertResult = await collection.insertOne({ _id: 1 }).catch(error => error); + expect(insertResult).to.be.instanceOf(MongoServerError); + }); + + // The expected backoff for the first (overload) error is: Math.random() * Math.min(10000, 100 * 2^0) + // With Math.random() = 0.99, this gives us: 0.99 * 100 = 99ms + // Subsequent errors are non-overload, so they should have NO backoff applied. + // We add a margin for test execution overhead. + const expectedMinBackoff = 99; // First backoff + const expectedMaxBackoff = expectedMinBackoff + 1000; // Allow 1 second margin for test overhead + + expect(duration).to.be.within(expectedMinBackoff, expectedMaxBackoff); + } + ); }); }); From 475f9a6b30cafe39eb8b293bde5625d691156ea8 Mon Sep 17 00:00:00 2001 From: Sergey Zelenov Date: Tue, 21 Apr 2026 13:10:40 +0200 Subject: [PATCH 2/5] update tests --- .../retryable_reads.spec.prose.test.ts | 22 ++++++++++++++----- .../retryable_writes.spec.prose.test.ts | 12 ++++++++-- 2 files changed, 26 insertions(+), 8 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 689430ee811..465be9fd815 100644 --- a/test/integration/retryable-reads/retryable_reads.spec.prose.test.ts +++ b/test/integration/retryable-reads/retryable_reads.spec.prose.test.ts @@ -6,7 +6,6 @@ import { type Collection, type CommandFailedEvent, type CommandSucceededEvent, - MAX_RETRIES, type MongoClient, MongoErrorLabel, MongoServerError, @@ -368,7 +367,8 @@ describe('Retryable Reads Spec Prose', () => { beforeEach(async function () { // 1. Create a client. client = this.configuration.newClient({ - monitorCommands: true + monitorCommands: true, + retryReads: true }); await client.connect(); }); @@ -415,8 +415,11 @@ describe('Retryable Reads Spec Prose', () => { .findOne({}) .catch(e => e); - expect(error).to.exist; - expect(serverCommandStub.callCount).to.equal(MAX_RETRIES + 1); + expect(error).to.be.instanceOf(MongoServerError); + expect(error.code).to.equal(91); + expect(error.hasErrorLabel(MongoErrorLabel.RetryableError)).to.be.true; + // MAX_RETRIES + 1 (default maxAdaptiveRetries is 2). + expect(serverCommandStub.callCount).to.equal(3); } ); }); @@ -434,7 +437,8 @@ describe('Retryable Reads Spec Prose', () => { beforeEach(async function () { // 1. Create a client. client = this.configuration.newClient({ - monitorCommands: true + monitorCommands: true, + retryReads: true }); await client.connect(); }); @@ -486,9 +490,15 @@ describe('Retryable Reads Spec Prose', () => { .collection('test') .findOne({}) .catch(e => e); - expect(error).to.exist; + expect(error).to.be.instanceOf(MongoServerError); + expect(error.code).to.equal(91); }); + // Ensure the full retry sequence executed (i.e. the test really exercised the + // post-overload non-overload retries, not just bailed after the first attempt). + // MAX_RETRIES + 1 (default maxAdaptiveRetries is 2). + expect(serverCommandStub.callCount).to.equal(3); + // The expected backoff for the first (overload) error is: Math.random() * Math.min(10000, 100 * 2^0) // With Math.random() = 0.99, this gives us: 0.99 * 100 = 99ms // Subsequent errors are non-overload, so they should have NO backoff applied. diff --git a/test/integration/retryable-writes/retryable_writes.spec.prose.test.ts b/test/integration/retryable-writes/retryable_writes.spec.prose.test.ts index e52d7c80ac6..1087072203e 100644 --- a/test/integration/retryable-writes/retryable_writes.spec.prose.test.ts +++ b/test/integration/retryable-writes/retryable_writes.spec.prose.test.ts @@ -6,7 +6,6 @@ import * as sinon from 'sinon'; import { type Collection, - MAX_RETRIES, type MongoClient, MongoError, MongoErrorLabel, @@ -609,7 +608,10 @@ describe('Retryable Writes Spec Prose', () => { const insertResult = await collection.insertOne({ _id: 1 }).catch(error => error); expect(insertResult).to.be.instanceOf(MongoServerError); - expect(serverCommandStub.callCount).to.equal(MAX_RETRIES + 1); + expect(insertResult.code).to.equal(91); + expect(insertResult.hasErrorLabel(MongoErrorLabel.RetryableError)).to.be.true; + // MAX_RETRIES + 1 (default maxAdaptiveRetries is 2). + expect(serverCommandStub.callCount).to.equal(3); } ); @@ -673,8 +675,14 @@ describe('Retryable Writes Spec Prose', () => { const { duration } = await measureDuration(async () => { const insertResult = await collection.insertOne({ _id: 1 }).catch(error => error); expect(insertResult).to.be.instanceOf(MongoServerError); + expect(insertResult.code).to.equal(91); }); + // Ensure the full retry sequence executed (i.e. the test really exercised the + // post-overload non-overload retries, not just bailed after the first attempt). + // MAX_RETRIES + 1 (default maxAdaptiveRetries is 2). + expect(serverCommandStub.callCount).to.equal(3); + // The expected backoff for the first (overload) error is: Math.random() * Math.min(10000, 100 * 2^0) // With Math.random() = 0.99, this gives us: 0.99 * 100 = 99ms // Subsequent errors are non-overload, so they should have NO backoff applied. From 8f07da6e04328b339d5781dbb40ff2c354b27413 Mon Sep 17 00:00:00 2001 From: Sergey Zelenov Date: Tue, 21 Apr 2026 14:18:59 +0200 Subject: [PATCH 3/5] round lower bound of the first retry --- .../retryable-reads/retryable_reads.spec.prose.test.ts | 8 ++++---- .../retryable-writes/retryable_writes.spec.prose.test.ts | 8 ++++---- 2 files changed, 8 insertions(+), 8 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 465be9fd815..4e4c7d7f8dc 100644 --- a/test/integration/retryable-reads/retryable_reads.spec.prose.test.ts +++ b/test/integration/retryable-reads/retryable_reads.spec.prose.test.ts @@ -500,10 +500,10 @@ describe('Retryable Reads Spec Prose', () => { expect(serverCommandStub.callCount).to.equal(3); // The expected backoff for the first (overload) error is: Math.random() * Math.min(10000, 100 * 2^0) - // With Math.random() = 0.99, this gives us: 0.99 * 100 = 99ms - // Subsequent errors are non-overload, so they should have NO backoff applied. - // We add a margin for test execution overhead. - const expectedMinBackoff = 99; // First backoff + // With Math.random() = 0.99, this gives ~99ms. `measureDuration` uses `Math.floor(performance.now())` + // on both ends and setTimeout can fire a couple ms early, so we allow slack on the lower bound. + // If the driver incorrectly applied backoff to all retries, total would be 0.99*(100+200) = ~297ms. + const expectedMinBackoff = 90; // First backoff const expectedMaxBackoff = expectedMinBackoff + 1000; // Allow 1 second margin for test overhead expect(duration).to.be.within(expectedMinBackoff, expectedMaxBackoff); diff --git a/test/integration/retryable-writes/retryable_writes.spec.prose.test.ts b/test/integration/retryable-writes/retryable_writes.spec.prose.test.ts index 1087072203e..d7d806d334e 100644 --- a/test/integration/retryable-writes/retryable_writes.spec.prose.test.ts +++ b/test/integration/retryable-writes/retryable_writes.spec.prose.test.ts @@ -684,10 +684,10 @@ describe('Retryable Writes Spec Prose', () => { expect(serverCommandStub.callCount).to.equal(3); // The expected backoff for the first (overload) error is: Math.random() * Math.min(10000, 100 * 2^0) - // With Math.random() = 0.99, this gives us: 0.99 * 100 = 99ms - // Subsequent errors are non-overload, so they should have NO backoff applied. - // We add a margin for test execution overhead. - const expectedMinBackoff = 99; // First backoff + // With Math.random() = 0.99, this gives ~99ms. `measureDuration` uses `Math.floor(performance.now())` + // on both ends and setTimeout can fire a couple ms early, so we allow slack on the lower bound. + // If the driver incorrectly applied backoff to all retries, total would be 0.99*(100+200) = ~297ms. + const expectedMinBackoff = 90; // First backoff const expectedMaxBackoff = expectedMinBackoff + 1000; // Allow 1 second margin for test overhead expect(duration).to.be.within(expectedMinBackoff, expectedMaxBackoff); From dc8c06a8a9a3046c90ddea44482b0c1ccaedb207 Mon Sep 17 00:00:00 2001 From: Sergey Zelenov Date: Thu, 23 Apr 2026 14:03:03 +0200 Subject: [PATCH 4/5] test(NODE-7534): use real DB failPoints for retries --- .../retryable_reads.spec.prose.test.ts | 192 +++++++++------- .../retryable_writes.spec.prose.test.ts | 214 +++++++++--------- 2 files changed, 215 insertions(+), 191 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 4e4c7d7f8dc..f6447db6926 100644 --- a/test/integration/retryable-reads/retryable_reads.spec.prose.test.ts +++ b/test/integration/retryable-reads/retryable_reads.spec.prose.test.ts @@ -1,6 +1,7 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ import { expect } from 'chai'; import * as sinon from 'sinon'; +import * as timersPromises from 'timers/promises'; import { type Collection, @@ -8,10 +9,8 @@ import { type CommandSucceededEvent, type MongoClient, MongoErrorLabel, - MongoServerError, - Server + MongoServerError } from '../../mongodb'; -import { measureDuration } from '../../tools/utils'; import { filterForCommands } from '../shared'; describe('Retryable Reads Spec Prose', () => { @@ -361,21 +360,23 @@ describe('Retryable Reads Spec Prose', () => { const TEST_METADATA: MongoDBMetadataUI = { requires: { mongodb: '>=4.4' } }; - - let client: MongoClient; + const APP_NAME = 'retryable-reads-prose-4'; beforeEach(async function () { // 1. Create a client. client = this.configuration.newClient({ monitorCommands: true, - retryReads: true + retryReads: true, + appName: APP_NAME }); await client.connect(); }); - afterEach(async function () { - sinon.restore(); - await client?.close(); + afterEach(async () => { + await client + ?.db('admin') + .command({ configureFailPoint: 'failCommand', mode: 'off' }) + .catch(() => null); }); it( @@ -383,29 +384,42 @@ describe('Retryable Reads Spec Prose', () => { TEST_METADATA, async () => { // 2. Configure a fail point with error code `91` (ShutdownInProgress) with the `RetryableError` and - // `SystemOverloadedError` error labels: - - // 3. Via the command monitoring CommandFailedEvent, configure a fail point with error code `91` (ShutdownInProgress) and - // the `RetryableError` label: - - // We use mocking to simulate the failpoint sequence: - // - First call: error WITH SystemOverloadedError - // - Subsequent calls: error WITHOUT SystemOverloadedError (but still retryable) - const serverCommandStub = sinon - .stub(Server.prototype, 'command') - .callsFake(async function () { - const errorLabels = - serverCommandStub.callCount === 1 - ? [MongoErrorLabel.RetryableError, MongoErrorLabel.SystemOverloadedError] - : [MongoErrorLabel.RetryableError]; - - throw new MongoServerError({ - message: 'Server Error', - errorLabels, - code: 91, - ok: 0 - }); + // `SystemOverloadedError` error labels. + await client.db('admin').command({ + configureFailPoint: 'failCommand', + mode: { times: 1 }, + data: { + failCommands: ['find'], + errorLabels: ['RetryableError', 'SystemOverloadedError'], + errorCode: 91, + appName: APP_NAME + } + }); + + // 3. Via the command monitoring CommandFailedEvent, configure a fail point with error code `91` + // (ShutdownInProgress) and the `RetryableError` label. Configure the second fail point command + // only if the failed event is for the first error configured in step 2. + let secondFailpointConfigured = false; + client.on('commandFailed', async (event: CommandFailedEvent) => { + if (secondFailpointConfigured) return; + if (event.commandName !== 'find') return; + secondFailpointConfigured = true; + await client.db('admin').command({ + configureFailPoint: 'failCommand', + mode: 'alwaysOn', + data: { + failCommands: ['find'], + errorLabels: ['RetryableError'], + errorCode: 91, + appName: APP_NAME + } }); + }); + + const findStartedEvents: Array> = []; + client.on('commandStarted', ev => { + if (ev.commandName === 'find') findStartedEvents.push(ev); + }); // 4. Attempt a `findOne` operation on any record for any database and collection. Expect the `findOne` to fail with a // server error. Assert that `MAX_RETRIES + 1` attempts were made. @@ -419,7 +433,9 @@ describe('Retryable Reads Spec Prose', () => { expect(error.code).to.equal(91); expect(error.hasErrorLabel(MongoErrorLabel.RetryableError)).to.be.true; // MAX_RETRIES + 1 (default maxAdaptiveRetries is 2). - expect(serverCommandStub.callCount).to.equal(3); + expect(findStartedEvents).to.have.lengthOf(3); + + // 5. Disable the fail point — handled by the surrounding afterEach. } ); }); @@ -431,82 +447,92 @@ describe('Retryable Reads Spec Prose', () => { const TEST_METADATA: MongoDBMetadataUI = { requires: { mongodb: '>=4.4' } }; - - let client: MongoClient; + const APP_NAME = 'retryable-reads-prose-5'; beforeEach(async function () { // 1. Create a client. client = this.configuration.newClient({ monitorCommands: true, - retryReads: true + retryReads: true, + appName: APP_NAME }); await client.connect(); }); - afterEach(async function () { + afterEach(async () => { sinon.restore(); - await client?.close(); + await client + ?.db('admin') + .command({ configureFailPoint: 'failCommand', mode: 'off' }) + .catch(() => null); }); it( 'should apply backoff only once for the initial overload error and not for subsequent non-overload retryable errors', TEST_METADATA, async function () { - // Configure the random number generator used for jitter to always return a number as close as possible to `1`. - const randomStub = sinon.stub(Math, 'random'); - randomStub.returns(0.99); + // Spy on `timers/promises.setTimeout` — the only sleep on the retry path + // (src/operations/execute_operation.ts:337) — to count how many times backoff was applied. + // We use a spy (not a stub) so the real sleep still happens, giving the commandFailed + // listener below time to configure the second failpoint before the driver dispatches its + // next retry. + const setTimeoutSpy = sinon.spy(timersPromises, 'setTimeout'); // 2. Configure a fail point with error code `91` (ShutdownInProgress) with the `RetryableError` and - // `SystemOverloadedError` error labels: - - // 3. Via the command monitoring CommandFailedEvent, configure a fail point with error code `91` (ShutdownInProgress) and - // the `RetryableError` label: - - // We use mocking to simulate the failpoint sequence: - // - First call: error WITH SystemOverloadedError - // - Subsequent calls: error WITHOUT SystemOverloadedError (but still retryable) - const serverCommandStub = sinon - .stub(Server.prototype, 'command') - .callsFake(async function () { - const errorLabels = - serverCommandStub.callCount === 1 - ? [MongoErrorLabel.RetryableError, MongoErrorLabel.SystemOverloadedError] - : [MongoErrorLabel.RetryableError]; - - throw new MongoServerError({ - message: 'Server Error', - errorLabels, - code: 91, - ok: 0 - }); + // `SystemOverloadedError` error labels. + await client.db('admin').command({ + configureFailPoint: 'failCommand', + mode: { times: 1 }, + data: { + failCommands: ['find'], + errorLabels: ['RetryableError', 'SystemOverloadedError'], + errorCode: 91, + appName: APP_NAME + } + }); + + // 3. Via the command monitoring CommandFailedEvent, configure a fail point with error code `91` + // (ShutdownInProgress) and the `RetryableError` label. Configure the second fail point command + // only if the failed event is for the first error configured in step 2. + let secondFailpointConfigured = false; + client.on('commandFailed', async (event: CommandFailedEvent) => { + if (secondFailpointConfigured) return; + if (event.commandName !== 'find') return; + secondFailpointConfigured = true; + await client.db('admin').command({ + configureFailPoint: 'failCommand', + mode: 'alwaysOn', + data: { + failCommands: ['find'], + errorLabels: ['RetryableError'], + errorCode: 91, + appName: APP_NAME + } }); + }); + + const findStartedEvents: Array> = []; + client.on('commandStarted', ev => { + if (ev.commandName === 'find') findStartedEvents.push(ev); + }); // 4. Attempt a `findOne` operation on any record for any database and collection. Expect the `findOne` to fail with a // server error. Assert that backoff was applied only once for the initial overload error and not for the subsequent // non-overload retryable errors. - const { duration } = await measureDuration(async () => { - const error = await client - .db('test') - .collection('test') - .findOne({}) - .catch(e => e); - expect(error).to.be.instanceOf(MongoServerError); - expect(error.code).to.equal(91); - }); - - // Ensure the full retry sequence executed (i.e. the test really exercised the - // post-overload non-overload retries, not just bailed after the first attempt). - // MAX_RETRIES + 1 (default maxAdaptiveRetries is 2). - expect(serverCommandStub.callCount).to.equal(3); + const error = await client + .db('test') + .collection('test') + .findOne({}) + .catch(e => e); - // The expected backoff for the first (overload) error is: Math.random() * Math.min(10000, 100 * 2^0) - // With Math.random() = 0.99, this gives ~99ms. `measureDuration` uses `Math.floor(performance.now())` - // on both ends and setTimeout can fire a couple ms early, so we allow slack on the lower bound. - // If the driver incorrectly applied backoff to all retries, total would be 0.99*(100+200) = ~297ms. - const expectedMinBackoff = 90; // First backoff - const expectedMaxBackoff = expectedMinBackoff + 1000; // Allow 1 second margin for test overhead + expect(error).to.be.instanceOf(MongoServerError); + expect(error.code).to.equal(91); + // MAX_RETRIES + 1 (default maxAdaptiveRetries is 2) — the full retry sequence ran. + expect(findStartedEvents).to.have.lengthOf(3); + // Backoff was applied exactly once — for the initial overload error only. + expect(setTimeoutSpy.callCount).to.equal(1); - expect(duration).to.be.within(expectedMinBackoff, expectedMaxBackoff); + // 5. Disable the fail point — handled by the surrounding afterEach. } ); }); diff --git a/test/integration/retryable-writes/retryable_writes.spec.prose.test.ts b/test/integration/retryable-writes/retryable_writes.spec.prose.test.ts index d7d806d334e..dbd1bce9df3 100644 --- a/test/integration/retryable-writes/retryable_writes.spec.prose.test.ts +++ b/test/integration/retryable-writes/retryable_writes.spec.prose.test.ts @@ -3,9 +3,11 @@ import { once } from 'node:events'; import { expect } from 'chai'; import * as sinon from 'sinon'; +import * as timersPromises from 'timers/promises'; import { type Collection, + type CommandFailedEvent, type MongoClient, MongoError, MongoErrorLabel, @@ -13,7 +15,7 @@ import { MongoWriteConcernError, Server } from '../../mongodb'; -import { measureDuration, sleep } from '../../tools/utils'; +import { sleep } from '../../tools/utils'; describe('Retryable Writes Spec Prose', () => { describe('1. Test that retryable writes raise an exception when using the MMAPv1 storage engine.', () => { @@ -359,10 +361,18 @@ describe('Retryable Writes Spec Prose', () => { let client: MongoClient; let collection: Collection<{ _id: 1 }>; + // Scope Cases 4-5 failpoints to our client via appName so a stale failpoint from these tests + // can't affect unrelated clients (and vice versa). Cases 1-3 ignore this — they mock the + // server response via sinon and never configure a real failpoint. + const FAILPOINT_APP_NAME = 'retryable-writes-prose-section-6'; beforeEach(async function () { // 1. Create a client with `retryWrites=true`. - client = this.configuration.newClient({ monitorCommands: true, retryWrites: true }); + client = this.configuration.newClient({ + monitorCommands: true, + retryWrites: true, + appName: FAILPOINT_APP_NAME + }); await client .db() .collection('retryReturnsOriginal') @@ -372,8 +382,12 @@ describe('Retryable Writes Spec Prose', () => { }); afterEach(async function () { - // 5. Disable the fail point (we don't use a failPoint, so we use sinon.restore instead) + // 5. Disable the fail point. Cases 1-3 rely on sinon stubs; Cases 4-5 configure real failpoints. sinon.restore(); + await client + .db('admin') + .command({ configureFailPoint: 'failCommand', mode: 'off' }) + .catch(() => null); await client.close(); }); @@ -557,51 +571,45 @@ describe('Retryable Writes Spec Prose', () => { 'Case 4: Test that drivers set the maximum number of retries for all retryable write errors when an overload error is encountered', { requires: { topology: 'replicaset', mongodb: '>=6.0' } }, async () => { - // 2. Configure a fail point with error code `91` (ShutdownInProgress) with the `RetryableError` and - // `SystemOverloadedError` error labels: - // ```javascript - // { - // configureFailPoint: "failCommand", - // mode: {times: 1}, - // data: { - // failCommands: ["insert"], - // errorLabels: ["RetryableError", "SystemOverloadedError"], - // errorCode: 91 - // } - // } - // ``` + // 1. Create a client with `retryWrites=true` — handled by the surrounding beforeEach. - // 3. Via the command monitoring CommandFailedEvent, configure a fail point with error code `91` (ShutdownInProgress) and - // the `RetryableWriteError` and `RetryableError` labels: - // ```javascript - // { - // configureFailPoint: "failCommand", - // mode: "alwaysOn", - // data: { - // failCommands: ["insert"], - // errorLabels: ["RetryableError", "RetryableWriteError"], - // errorCode: 91 - // } - // } - // ``` - // Configure the second fail point command only if the failed event is for the first error configured in step 2. - const serverCommandStub = sinon - .stub(Server.prototype, 'command') - .callsFake(async function () { - // First call: error WITH SystemOverloadedError - // Subsequent calls: error WITHOUT SystemOverloadedError (but still retryable) - const errorLabels = - serverCommandStub.callCount === 1 - ? [MongoErrorLabel.RetryableError, MongoErrorLabel.SystemOverloadedError] - : [MongoErrorLabel.RetryableError, MongoErrorLabel.RetryableWriteError]; + // 2. Configure a fail point with error code `91` (ShutdownInProgress) with the `RetryableError` and + // `SystemOverloadedError` error labels. + await client.db('admin').command({ + configureFailPoint: 'failCommand', + mode: { times: 1 }, + data: { + failCommands: ['insert'], + errorLabels: ['RetryableError', 'SystemOverloadedError'], + errorCode: 91, + appName: FAILPOINT_APP_NAME + } + }); - throw new MongoServerError({ - message: 'Server Error', - errorLabels, - code: 91, - ok: 0 - }); + // 3. Via the command monitoring CommandFailedEvent, configure a fail point with error code `91` + // (ShutdownInProgress) and the `RetryableWriteError` and `RetryableError` labels. Configure the + // second fail point command only if the failed event is for the first error configured in step 2. + let secondFailpointConfigured = false; + client.on('commandFailed', async (event: CommandFailedEvent) => { + if (secondFailpointConfigured) return; + if (event.commandName !== 'insert') return; + secondFailpointConfigured = true; + await client.db('admin').command({ + configureFailPoint: 'failCommand', + mode: 'alwaysOn', + data: { + failCommands: ['insert'], + errorLabels: ['RetryableError', 'RetryableWriteError'], + errorCode: 91, + appName: FAILPOINT_APP_NAME + } }); + }); + + const insertStartedEvents: Array> = []; + client.on('commandStarted', ev => { + if (ev.commandName === 'insert') insertStartedEvents.push(ev); + }); // 4. Attempt an `insertOne` operation on any record for any database and collection. Expect the `insertOne` to fail with a // server error. Assert that `MAX_RETRIES + 1` attempts were made. @@ -611,7 +619,9 @@ describe('Retryable Writes Spec Prose', () => { expect(insertResult.code).to.equal(91); expect(insertResult.hasErrorLabel(MongoErrorLabel.RetryableError)).to.be.true; // MAX_RETRIES + 1 (default maxAdaptiveRetries is 2). - expect(serverCommandStub.callCount).to.equal(3); + expect(insertStartedEvents).to.have.lengthOf(3); + + // 5. Disable the fail point — handled by the surrounding afterEach. } ); @@ -619,78 +629,66 @@ describe('Retryable Writes Spec Prose', () => { 'Case 5: Test that drivers do not apply backoff to non-overload errors', { requires: { topology: 'replicaset', mongodb: '>=6.0' } }, async function () { - // Configure the random number generator used for jitter to always return a number as close as possible to `1`. - const stub = sinon.stub(Math, 'random'); - stub.returns(0.99); + // 1. Create a client with `retryWrites=true` — handled by the surrounding beforeEach. - // 2. Configure a fail point with error code `91` (ShutdownInProgress) with the `RetryableError` and - // `SystemOverloadedError` error labels: - // ```javascript - // { - // configureFailPoint: "failCommand", - // mode: {times: 1}, - // data: { - // failCommands: ["insert"], - // errorLabels: ["RetryableError", "SystemOverloadedError"], - // errorCode: 91 - // } - // } - // ``` + // Spy on `timers/promises.setTimeout` — the only sleep on the retry path + // (src/operations/execute_operation.ts:337) — to count how many times backoff was applied. + // We use a spy (not a stub) so the real sleep still happens, giving the commandFailed + // listener below time to configure the second failpoint before the driver dispatches its + // next retry. + const setTimeoutSpy = sinon.spy(timersPromises, 'setTimeout'); - // 3. Via the command monitoring CommandFailedEvent, configure a fail point with error code `91` (ShutdownInProgress) and - // the `RetryableWriteError` and `RetryableError` labels: - // ```javascript - // { - // configureFailPoint: "failCommand", - // mode: "alwaysOn", - // data: { - // failCommands: ["insert"], - // errorLabels: ["RetryableError", "RetryableWriteError"], - // errorCode: 91 - // } - // } - // ``` - // Configure the second fail point command only if the failed event is for the first error configured in step 2. - const serverCommandStub = sinon - .stub(Server.prototype, 'command') - .callsFake(async function () { - // First call: error WITH SystemOverloadedError - // Subsequent calls: error WITHOUT SystemOverloadedError (but still retryable) - const errorLabels = - serverCommandStub.callCount === 1 - ? [MongoErrorLabel.RetryableError, MongoErrorLabel.SystemOverloadedError] - : [MongoErrorLabel.RetryableError, MongoErrorLabel.RetryableWriteError]; + // 2. Configure a fail point with error code `91` (ShutdownInProgress) with the `RetryableError` and + // `SystemOverloadedError` error labels. + await client.db('admin').command({ + configureFailPoint: 'failCommand', + mode: { times: 1 }, + data: { + failCommands: ['insert'], + errorLabels: ['RetryableError', 'SystemOverloadedError'], + errorCode: 91, + appName: FAILPOINT_APP_NAME + } + }); - throw new MongoServerError({ - message: 'Server Error', - errorLabels, - code: 91, - ok: 0 - }); + // 3. Via the command monitoring CommandFailedEvent, configure a fail point with error code `91` + // (ShutdownInProgress) and the `RetryableWriteError` and `RetryableError` labels. Configure the + // second fail point command only if the failed event is for the first error configured in step 2. + let secondFailpointConfigured = false; + client.on('commandFailed', async (event: CommandFailedEvent) => { + if (secondFailpointConfigured) return; + if (event.commandName !== 'insert') return; + secondFailpointConfigured = true; + await client.db('admin').command({ + configureFailPoint: 'failCommand', + mode: 'alwaysOn', + data: { + failCommands: ['insert'], + errorLabels: ['RetryableError', 'RetryableWriteError'], + errorCode: 91, + appName: FAILPOINT_APP_NAME + } }); + }); + + const insertStartedEvents: Array> = []; + client.on('commandStarted', ev => { + if (ev.commandName === 'insert') insertStartedEvents.push(ev); + }); // 4. Attempt an `insertOne` operation on any record for any database and collection. Expect the `insertOne` to fail with a // server error. Assert that backoff was applied only once for the initial overload error and not for the subsequent // non-overload retryable errors. - const { duration } = await measureDuration(async () => { - const insertResult = await collection.insertOne({ _id: 1 }).catch(error => error); - expect(insertResult).to.be.instanceOf(MongoServerError); - expect(insertResult.code).to.equal(91); - }); - - // Ensure the full retry sequence executed (i.e. the test really exercised the - // post-overload non-overload retries, not just bailed after the first attempt). - // MAX_RETRIES + 1 (default maxAdaptiveRetries is 2). - expect(serverCommandStub.callCount).to.equal(3); + const insertResult = await collection.insertOne({ _id: 1 }).catch(error => error); - // The expected backoff for the first (overload) error is: Math.random() * Math.min(10000, 100 * 2^0) - // With Math.random() = 0.99, this gives ~99ms. `measureDuration` uses `Math.floor(performance.now())` - // on both ends and setTimeout can fire a couple ms early, so we allow slack on the lower bound. - // If the driver incorrectly applied backoff to all retries, total would be 0.99*(100+200) = ~297ms. - const expectedMinBackoff = 90; // First backoff - const expectedMaxBackoff = expectedMinBackoff + 1000; // Allow 1 second margin for test overhead + expect(insertResult).to.be.instanceOf(MongoServerError); + expect(insertResult.code).to.equal(91); + // MAX_RETRIES + 1 (default maxAdaptiveRetries is 2) — the full retry sequence ran. + expect(insertStartedEvents).to.have.lengthOf(3); + // Backoff was applied exactly once — for the initial overload error only. + expect(setTimeoutSpy.callCount).to.equal(1); - expect(duration).to.be.within(expectedMinBackoff, expectedMaxBackoff); + // 5. Disable the fail point — handled by the surrounding afterEach. } ); }); From e2256100c8b1667d64e66a6490bd6ed85d96ef73 Mon Sep 17 00:00:00 2001 From: Sergey Zelenov Date: Thu, 23 Apr 2026 15:55:24 +0200 Subject: [PATCH 5/5] test(NODE-7534): use separate admin client for failPoints --- .../retryable_reads.spec.prose.test.ts | 73 ++++++++++++------- .../retryable_writes.spec.prose.test.ts | 67 ++++++++++------- 2 files changed, 88 insertions(+), 52 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 f6447db6926..ee95fad6cbf 100644 --- a/test/integration/retryable-reads/retryable_reads.spec.prose.test.ts +++ b/test/integration/retryable-reads/retryable_reads.spec.prose.test.ts @@ -361,6 +361,11 @@ describe('Retryable Reads Spec Prose', () => { requires: { mongodb: '>=4.4' } }; const APP_NAME = 'retryable-reads-prose-4'; + // Separate admin client for configureFailPoint calls. Code 91 (ShutdownInProgress) is a + // state-change error: the main client's server gets marked Unknown and its pool cleared on the + // first failpoint hit, so the listener's configureFailPoint would fail with MongoPoolClearedError + // if it went through `client`. + let adminClient: MongoClient; beforeEach(async function () { // 1. Create a client. @@ -370,13 +375,17 @@ describe('Retryable Reads Spec Prose', () => { appName: APP_NAME }); await client.connect(); + + adminClient = this.configuration.newClient(); + await adminClient.connect(); }); afterEach(async () => { - await client + await adminClient ?.db('admin') .command({ configureFailPoint: 'failCommand', mode: 'off' }) .catch(() => null); + await adminClient?.close(); }); it( @@ -385,7 +394,7 @@ describe('Retryable Reads Spec Prose', () => { async () => { // 2. Configure a fail point with error code `91` (ShutdownInProgress) with the `RetryableError` and // `SystemOverloadedError` error labels. - await client.db('admin').command({ + await adminClient.db('admin').command({ configureFailPoint: 'failCommand', mode: { times: 1 }, data: { @@ -400,20 +409,23 @@ describe('Retryable Reads Spec Prose', () => { // (ShutdownInProgress) and the `RetryableError` label. Configure the second fail point command // only if the failed event is for the first error configured in step 2. let secondFailpointConfigured = false; - client.on('commandFailed', async (event: CommandFailedEvent) => { + client.on('commandFailed', (event: CommandFailedEvent) => { if (secondFailpointConfigured) return; if (event.commandName !== 'find') return; secondFailpointConfigured = true; - await client.db('admin').command({ - configureFailPoint: 'failCommand', - mode: 'alwaysOn', - data: { - failCommands: ['find'], - errorLabels: ['RetryableError'], - errorCode: 91, - appName: APP_NAME - } - }); + adminClient + .db('admin') + .command({ + configureFailPoint: 'failCommand', + mode: 'alwaysOn', + data: { + failCommands: ['find'], + errorLabels: ['RetryableError'], + errorCode: 91, + appName: APP_NAME + } + }) + .catch(() => null); }); const findStartedEvents: Array> = []; @@ -448,6 +460,8 @@ describe('Retryable Reads Spec Prose', () => { requires: { mongodb: '>=4.4' } }; const APP_NAME = 'retryable-reads-prose-5'; + // Separate admin client for configureFailPoint calls. See Case 4 describe for rationale. + let adminClient: MongoClient; beforeEach(async function () { // 1. Create a client. @@ -457,14 +471,18 @@ describe('Retryable Reads Spec Prose', () => { appName: APP_NAME }); await client.connect(); + + adminClient = this.configuration.newClient(); + await adminClient.connect(); }); afterEach(async () => { sinon.restore(); - await client + await adminClient ?.db('admin') .command({ configureFailPoint: 'failCommand', mode: 'off' }) .catch(() => null); + await adminClient?.close(); }); it( @@ -480,7 +498,7 @@ describe('Retryable Reads Spec Prose', () => { // 2. Configure a fail point with error code `91` (ShutdownInProgress) with the `RetryableError` and // `SystemOverloadedError` error labels. - await client.db('admin').command({ + await adminClient.db('admin').command({ configureFailPoint: 'failCommand', mode: { times: 1 }, data: { @@ -495,20 +513,23 @@ describe('Retryable Reads Spec Prose', () => { // (ShutdownInProgress) and the `RetryableError` label. Configure the second fail point command // only if the failed event is for the first error configured in step 2. let secondFailpointConfigured = false; - client.on('commandFailed', async (event: CommandFailedEvent) => { + client.on('commandFailed', (event: CommandFailedEvent) => { if (secondFailpointConfigured) return; if (event.commandName !== 'find') return; secondFailpointConfigured = true; - await client.db('admin').command({ - configureFailPoint: 'failCommand', - mode: 'alwaysOn', - data: { - failCommands: ['find'], - errorLabels: ['RetryableError'], - errorCode: 91, - appName: APP_NAME - } - }); + adminClient + .db('admin') + .command({ + configureFailPoint: 'failCommand', + mode: 'alwaysOn', + data: { + failCommands: ['find'], + errorLabels: ['RetryableError'], + errorCode: 91, + appName: APP_NAME + } + }) + .catch(() => null); }); const findStartedEvents: Array> = []; diff --git a/test/integration/retryable-writes/retryable_writes.spec.prose.test.ts b/test/integration/retryable-writes/retryable_writes.spec.prose.test.ts index dbd1bce9df3..430f263011e 100644 --- a/test/integration/retryable-writes/retryable_writes.spec.prose.test.ts +++ b/test/integration/retryable-writes/retryable_writes.spec.prose.test.ts @@ -365,6 +365,11 @@ describe('Retryable Writes Spec Prose', () => { // can't affect unrelated clients (and vice versa). Cases 1-3 ignore this — they mock the // server response via sinon and never configure a real failpoint. const FAILPOINT_APP_NAME = 'retryable-writes-prose-section-6'; + // Separate admin client for Cases 4-5 configureFailPoint calls. Code 91 (ShutdownInProgress) is + // a state-change error: `client`'s server gets marked Unknown and its pool cleared on the first + // failpoint hit, so the listener's configureFailPoint would fail with MongoPoolClearedError if + // it went through `client`. + let adminClient: MongoClient; beforeEach(async function () { // 1. Create a client with `retryWrites=true`. @@ -379,15 +384,19 @@ describe('Retryable Writes Spec Prose', () => { .drop() .catch(() => null); collection = client.db().collection('retryReturnsOriginal'); + + adminClient = this.configuration.newClient(); + await adminClient.connect(); }); afterEach(async function () { // 5. Disable the fail point. Cases 1-3 rely on sinon stubs; Cases 4-5 configure real failpoints. sinon.restore(); - await client - .db('admin') + await adminClient + ?.db('admin') .command({ configureFailPoint: 'failCommand', mode: 'off' }) .catch(() => null); + await adminClient?.close(); await client.close(); }); @@ -575,7 +584,7 @@ describe('Retryable Writes Spec Prose', () => { // 2. Configure a fail point with error code `91` (ShutdownInProgress) with the `RetryableError` and // `SystemOverloadedError` error labels. - await client.db('admin').command({ + await adminClient.db('admin').command({ configureFailPoint: 'failCommand', mode: { times: 1 }, data: { @@ -590,20 +599,23 @@ describe('Retryable Writes Spec Prose', () => { // (ShutdownInProgress) and the `RetryableWriteError` and `RetryableError` labels. Configure the // second fail point command only if the failed event is for the first error configured in step 2. let secondFailpointConfigured = false; - client.on('commandFailed', async (event: CommandFailedEvent) => { + client.on('commandFailed', (event: CommandFailedEvent) => { if (secondFailpointConfigured) return; if (event.commandName !== 'insert') return; secondFailpointConfigured = true; - await client.db('admin').command({ - configureFailPoint: 'failCommand', - mode: 'alwaysOn', - data: { - failCommands: ['insert'], - errorLabels: ['RetryableError', 'RetryableWriteError'], - errorCode: 91, - appName: FAILPOINT_APP_NAME - } - }); + adminClient + .db('admin') + .command({ + configureFailPoint: 'failCommand', + mode: 'alwaysOn', + data: { + failCommands: ['insert'], + errorLabels: ['RetryableError', 'RetryableWriteError'], + errorCode: 91, + appName: FAILPOINT_APP_NAME + } + }) + .catch(() => null); }); const insertStartedEvents: Array> = []; @@ -640,7 +652,7 @@ describe('Retryable Writes Spec Prose', () => { // 2. Configure a fail point with error code `91` (ShutdownInProgress) with the `RetryableError` and // `SystemOverloadedError` error labels. - await client.db('admin').command({ + await adminClient.db('admin').command({ configureFailPoint: 'failCommand', mode: { times: 1 }, data: { @@ -655,20 +667,23 @@ describe('Retryable Writes Spec Prose', () => { // (ShutdownInProgress) and the `RetryableWriteError` and `RetryableError` labels. Configure the // second fail point command only if the failed event is for the first error configured in step 2. let secondFailpointConfigured = false; - client.on('commandFailed', async (event: CommandFailedEvent) => { + client.on('commandFailed', (event: CommandFailedEvent) => { if (secondFailpointConfigured) return; if (event.commandName !== 'insert') return; secondFailpointConfigured = true; - await client.db('admin').command({ - configureFailPoint: 'failCommand', - mode: 'alwaysOn', - data: { - failCommands: ['insert'], - errorLabels: ['RetryableError', 'RetryableWriteError'], - errorCode: 91, - appName: FAILPOINT_APP_NAME - } - }); + adminClient + .db('admin') + .command({ + configureFailPoint: 'failCommand', + mode: 'alwaysOn', + data: { + failCommands: ['insert'], + errorLabels: ['RetryableError', 'RetryableWriteError'], + errorCode: 91, + appName: FAILPOINT_APP_NAME + } + }) + .catch(() => null); }); const insertStartedEvents: Array> = [];