Skip to content

Commit e8af442

Browse files
add retry context
1 parent 27fced8 commit e8af442

3 files changed

Lines changed: 48 additions & 24 deletions

File tree

src/operations/execute_operation.ts

Lines changed: 6 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ import {
3737
supportsRetryableWrites
3838
} from '../utils';
3939
import { AggregateOperation } from './aggregate';
40-
import { AbstractOperation, Aspect } from './operation';
40+
import { AbstractOperation, Aspect, RetryContext } from './operation';
4141

4242
const MMAPv1_RETRY_WRITES_ERROR_CODE = MONGODB_ERROR_CODES.IllegalOperation;
4343
const MMAPv1_RETRY_WRITES_ERROR_MESSAGE =
@@ -254,18 +254,9 @@ async function executeOperationWithRetries<
254254
2 // backoff rate
255255
);
256256

257-
let maxAttempts =
258-
(operation.maxAttempts ?? willRetry) ? (timeoutContext.csotEnabled() ? Infinity : 2) : 1;
259-
260-
for (
261-
let attempt = 0;
262-
attempt < maxAttempts;
263-
attempt++,
264-
maxAttempts =
265-
willRetry && previousOperationError?.hasErrorLabel(MongoErrorLabel.SystemOverloadedError)
266-
? 6
267-
: maxAttempts
268-
) {
257+
const retryContext = new RetryContext(willRetry, timeoutContext.csotEnabled() ? Infinity : 2);
258+
259+
for (; retryContext.shouldRetry(); retryContext.recordFailure(previousOperationError)) {
269260
if (previousOperationError) {
270261
if (hasWriteAspect && previousOperationError.code === MMAPv1_RETRY_WRITES_ERROR_CODE) {
271262
throw new MongoServerError({
@@ -294,7 +285,6 @@ async function executeOperationWithRetries<
294285

295286
// if the delay would exhaust the CSOT timeout, short-circuit.
296287
if (timeoutContext.csotEnabled() && delayMS > timeoutContext.remainingTimeMS) {
297-
// TODO: is this the right error to throw?
298288
throw new MongoOperationTimeoutError(
299289
`MongoDB SystemOverload exponential backoff would exceed timeoutMS deadline: remaining CSOT deadline=${timeoutContext.remainingTimeMS}, backoff delayMS=${delayMS}`,
300290
{
@@ -337,17 +327,15 @@ async function executeOperationWithRetries<
337327
operation.server = server;
338328

339329
try {
340-
const isRetry = attempt > 0;
341-
342330
// If attempt > 0 and we are command batching we need to reset the batch.
343-
if (isRetry && operation.hasAspect(Aspect.COMMAND_BATCHING)) {
331+
if (retryContext.isRetry && operation.hasAspect(Aspect.COMMAND_BATCHING)) {
344332
operation.resetBatch();
345333
}
346334

347335
try {
348336
const result = await server.command(operation, timeoutContext);
349337
topology.tokenBucket.deposit(
350-
isRetry
338+
retryContext.isRetry
351339
? // on successful retry, deposit the retry cost + the refresh rate.
352340
TOKEN_REFRESH_RATE + RETRY_COST
353341
: // otherwise, just deposit the refresh rate.

src/operations/operation.ts

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { type Connection, type MongoError } from '..';
1+
import { type Connection, type MongoError, MongoErrorLabel } from '..';
22
import { type BSONSerializeOptions, type Document, resolveBSONOptions } from '../bson';
33
import { type MongoDBResponse } from '../cmap/wire_protocol/responses';
44
import { type Abortable } from '../mongo_types';
@@ -45,6 +45,42 @@ export interface OperationOptions extends BSONSerializeOptions {
4545
timeoutMS?: number;
4646
}
4747

48+
/**
49+
* @internal
50+
*/
51+
export class RetryContext {
52+
private maxAttempts: number;
53+
private willRetry: boolean;
54+
private attempts: number = 0;
55+
56+
constructor(willRetry: boolean, maxAttempts: number) {
57+
this.maxAttempts = maxAttempts;
58+
this.willRetry = willRetry;
59+
}
60+
61+
get isRetry() {
62+
return this.attempts > 0;
63+
}
64+
65+
shouldRetry() {
66+
return this.attempts < this.maxAttempts;
67+
}
68+
69+
recordFailure(error: MongoError) {
70+
this.attempts++;
71+
72+
const isRetryableOverloadError =
73+
error.hasErrorLabel(MongoErrorLabel.RetryableError) &&
74+
error.hasErrorLabel(MongoErrorLabel.SystemOverloadedError);
75+
76+
if (!(this.willRetry || isRetryableOverloadError)) return;
77+
78+
this.maxAttempts = error.hasErrorLabel(MongoErrorLabel.SystemOverloadedError)
79+
? 6
80+
: this.maxAttempts;
81+
}
82+
}
83+
4884
/**
4985
* This class acts as a parent class for any operation and is responsible for setting this.options,
5086
* as well as setting and getting a session.
@@ -66,7 +102,7 @@ export abstract class AbstractOperation<TResult = any> {
66102
/** Specifies the time an operation will run until it throws a timeout error. */
67103
timeoutMS?: number;
68104

69-
maxAttempts?: number;
105+
retryContext?: RetryContext;
70106

71107
private _session: ClientSession | undefined;
72108

src/sessions.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import {
2525
import type { MongoClient, MongoOptions } from './mongo_client';
2626
import { TypedEventEmitter } from './mongo_types';
2727
import { executeOperation } from './operations/execute_operation';
28-
import { RetryAttemptContext } from './operations/operation';
28+
import { RetryContext } from './operations/operation';
2929
import { RunCommandOperation } from './operations/run_command';
3030
import { ReadConcernLevel } from './read_concern';
3131
import { ReadPreference } from './read_preference';
@@ -494,14 +494,14 @@ export class ClientSession
494494
command.recoveryToken = this.transaction.recoveryToken;
495495
}
496496

497-
const retryContext = new RetryAttemptContext(5);
497+
const retryContext = new RetryContext(false, 5);
498498

499499
const operation = new RunCommandOperation(new MongoDBNamespace('admin'), command, {
500500
session: this,
501501
readPreference: ReadPreference.primary,
502502
bypassPinningCheck: true
503503
});
504-
operation.attempts = retryContext;
504+
operation.retryContext = retryContext;
505505

506506
const timeoutContext =
507507
this.timeoutContext ??
@@ -531,7 +531,7 @@ export class ClientSession
531531
readPreference: ReadPreference.primary,
532532
bypassPinningCheck: true
533533
});
534-
op.attempts = retryContext;
534+
op.retryContext = operation.retryContext;
535535
await executeOperation(this.client, op, timeoutContext);
536536
return;
537537
} catch (retryCommitError) {

0 commit comments

Comments
 (0)