Skip to content

Commit 1e0dfda

Browse files
nbbeekentadjik1
andauthored
test(NODE-7534): add prose tests for retry behavior with mixed overload/non-overload errors (#4921)
Co-authored-by: Sergey Zelenov <sergey.zelenov@mongodb.com>
1 parent e8c5e65 commit 1e0dfda

2 files changed

Lines changed: 366 additions & 3 deletions

File tree

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

Lines changed: 210 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
/* eslint-disable @typescript-eslint/no-non-null-assertion */
22
import { expect } from 'chai';
3+
import * as sinon from 'sinon';
4+
import * as timersPromises from 'timers/promises';
35

46
import {
57
type Collection,
68
type CommandFailedEvent,
79
type CommandSucceededEvent,
8-
type MongoClient
10+
type MongoClient,
11+
MongoErrorLabel,
12+
MongoServerError
913
} from '../../mongodb';
1014
import { filterForCommands } from '../shared';
1115

@@ -348,4 +352,209 @@ describe('Retryable Reads Spec Prose', () => {
348352
);
349353
});
350354
});
355+
356+
describe('4: Test that drivers set the maximum number of retries for all retryable read errors when an overload error is encountered', () => {
357+
// This test MUST be executed against a MongoDB 4.4+ server that supports `retryReads=true` and has enabled the
358+
// `configureFailPoint` command with the `errorLabels` option.
359+
360+
const TEST_METADATA: MongoDBMetadataUI = {
361+
requires: { mongodb: '>=4.4' }
362+
};
363+
const APP_NAME = 'retryable-reads-prose-4';
364+
// Separate admin client for configureFailPoint calls. Code 91 (ShutdownInProgress) is a
365+
// state-change error: the main client's server gets marked Unknown and its pool cleared on the
366+
// first failpoint hit, so the listener's configureFailPoint would fail with MongoPoolClearedError
367+
// if it went through `client`.
368+
let adminClient: MongoClient;
369+
370+
beforeEach(async function () {
371+
// 1. Create a client.
372+
client = this.configuration.newClient({
373+
monitorCommands: true,
374+
retryReads: true,
375+
appName: APP_NAME
376+
});
377+
await client.connect();
378+
379+
adminClient = this.configuration.newClient();
380+
await adminClient.connect();
381+
});
382+
383+
afterEach(async () => {
384+
await adminClient
385+
?.db('admin')
386+
.command({ configureFailPoint: 'failCommand', mode: 'off' })
387+
.catch(() => null);
388+
await adminClient?.close();
389+
});
390+
391+
it(
392+
'should retry MAX_RETRIES times for all retryable errors after encountering an overload error',
393+
TEST_METADATA,
394+
async () => {
395+
// 2. Configure a fail point with error code `91` (ShutdownInProgress) with the `RetryableError` and
396+
// `SystemOverloadedError` error labels.
397+
await adminClient.db('admin').command({
398+
configureFailPoint: 'failCommand',
399+
mode: { times: 1 },
400+
data: {
401+
failCommands: ['find'],
402+
errorLabels: ['RetryableError', 'SystemOverloadedError'],
403+
errorCode: 91,
404+
appName: APP_NAME
405+
}
406+
});
407+
408+
// 3. Via the command monitoring CommandFailedEvent, configure a fail point with error code `91`
409+
// (ShutdownInProgress) and the `RetryableError` label. Configure the second fail point command
410+
// only if the failed event is for the first error configured in step 2.
411+
let secondFailpointConfigured = false;
412+
client.on('commandFailed', (event: CommandFailedEvent) => {
413+
if (secondFailpointConfigured) return;
414+
if (event.commandName !== 'find') return;
415+
secondFailpointConfigured = true;
416+
adminClient
417+
.db('admin')
418+
.command({
419+
configureFailPoint: 'failCommand',
420+
mode: 'alwaysOn',
421+
data: {
422+
failCommands: ['find'],
423+
errorLabels: ['RetryableError'],
424+
errorCode: 91,
425+
appName: APP_NAME
426+
}
427+
})
428+
.catch(() => null);
429+
});
430+
431+
const findStartedEvents: Array<Record<string, any>> = [];
432+
client.on('commandStarted', ev => {
433+
if (ev.commandName === 'find') findStartedEvents.push(ev);
434+
});
435+
436+
// 4. Attempt a `findOne` operation on any record for any database and collection. Expect the `findOne` to fail with a
437+
// server error. Assert that `MAX_RETRIES + 1` attempts were made.
438+
const error = await client
439+
.db('test')
440+
.collection('test')
441+
.findOne({})
442+
.catch(e => e);
443+
444+
expect(error).to.be.instanceOf(MongoServerError);
445+
expect(error.code).to.equal(91);
446+
expect(error.hasErrorLabel(MongoErrorLabel.RetryableError)).to.be.true;
447+
// MAX_RETRIES + 1 (default maxAdaptiveRetries is 2).
448+
expect(findStartedEvents).to.have.lengthOf(3);
449+
450+
// 5. Disable the fail point — handled by the surrounding afterEach.
451+
}
452+
);
453+
});
454+
455+
describe('5: Test that drivers do not apply backoff to non-overload errors', () => {
456+
// This test MUST be executed against a MongoDB 4.4+ server that supports `retryReads=true` and has enabled the
457+
// `configureFailPoint` command with the `errorLabels` option.
458+
459+
const TEST_METADATA: MongoDBMetadataUI = {
460+
requires: { mongodb: '>=4.4' }
461+
};
462+
const APP_NAME = 'retryable-reads-prose-5';
463+
// Separate admin client for configureFailPoint calls. See Case 4 describe for rationale.
464+
let adminClient: MongoClient;
465+
466+
beforeEach(async function () {
467+
// 1. Create a client.
468+
client = this.configuration.newClient({
469+
monitorCommands: true,
470+
retryReads: true,
471+
appName: APP_NAME
472+
});
473+
await client.connect();
474+
475+
adminClient = this.configuration.newClient();
476+
await adminClient.connect();
477+
});
478+
479+
afterEach(async () => {
480+
sinon.restore();
481+
await adminClient
482+
?.db('admin')
483+
.command({ configureFailPoint: 'failCommand', mode: 'off' })
484+
.catch(() => null);
485+
await adminClient?.close();
486+
});
487+
488+
it(
489+
'should apply backoff only once for the initial overload error and not for subsequent non-overload retryable errors',
490+
TEST_METADATA,
491+
async function () {
492+
// Spy on `timers/promises.setTimeout` — the only sleep on the retry path
493+
// (src/operations/execute_operation.ts:337) — to count how many times backoff was applied.
494+
// We use a spy (not a stub) so the real sleep still happens, giving the commandFailed
495+
// listener below time to configure the second failpoint before the driver dispatches its
496+
// next retry.
497+
const setTimeoutSpy = sinon.spy(timersPromises, 'setTimeout');
498+
499+
// 2. Configure a fail point with error code `91` (ShutdownInProgress) with the `RetryableError` and
500+
// `SystemOverloadedError` error labels.
501+
await adminClient.db('admin').command({
502+
configureFailPoint: 'failCommand',
503+
mode: { times: 1 },
504+
data: {
505+
failCommands: ['find'],
506+
errorLabels: ['RetryableError', 'SystemOverloadedError'],
507+
errorCode: 91,
508+
appName: APP_NAME
509+
}
510+
});
511+
512+
// 3. Via the command monitoring CommandFailedEvent, configure a fail point with error code `91`
513+
// (ShutdownInProgress) and the `RetryableError` label. Configure the second fail point command
514+
// only if the failed event is for the first error configured in step 2.
515+
let secondFailpointConfigured = false;
516+
client.on('commandFailed', (event: CommandFailedEvent) => {
517+
if (secondFailpointConfigured) return;
518+
if (event.commandName !== 'find') return;
519+
secondFailpointConfigured = true;
520+
adminClient
521+
.db('admin')
522+
.command({
523+
configureFailPoint: 'failCommand',
524+
mode: 'alwaysOn',
525+
data: {
526+
failCommands: ['find'],
527+
errorLabels: ['RetryableError'],
528+
errorCode: 91,
529+
appName: APP_NAME
530+
}
531+
})
532+
.catch(() => null);
533+
});
534+
535+
const findStartedEvents: Array<Record<string, any>> = [];
536+
client.on('commandStarted', ev => {
537+
if (ev.commandName === 'find') findStartedEvents.push(ev);
538+
});
539+
540+
// 4. Attempt a `findOne` operation on any record for any database and collection. Expect the `findOne` to fail with a
541+
// server error. Assert that backoff was applied only once for the initial overload error and not for the subsequent
542+
// non-overload retryable errors.
543+
const error = await client
544+
.db('test')
545+
.collection('test')
546+
.findOne({})
547+
.catch(e => e);
548+
549+
expect(error).to.be.instanceOf(MongoServerError);
550+
expect(error.code).to.equal(91);
551+
// MAX_RETRIES + 1 (default maxAdaptiveRetries is 2) — the full retry sequence ran.
552+
expect(findStartedEvents).to.have.lengthOf(3);
553+
// Backoff was applied exactly once — for the initial overload error only.
554+
expect(setTimeoutSpy.callCount).to.equal(1);
555+
556+
// 5. Disable the fail point — handled by the surrounding afterEach.
557+
}
558+
);
559+
});
351560
});

0 commit comments

Comments
 (0)