Skip to content

Commit 6d9266e

Browse files
committed
chore: create and pass around timeout contexts
1 parent fd9ee7f commit 6d9266e

File tree

9 files changed

+287
-88
lines changed

9 files changed

+287
-88
lines changed

package-lock.json

Lines changed: 5 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@
9999
"mocha": "^10.4.0",
100100
"mocha-sinon": "^2.1.2",
101101
"mongodb-client-encryption": "^6.1.0-alpha.0",
102-
"mongodb-legacy": "^6.0.1",
102+
"mongodb-legacy": "^6.1.1",
103103
"nyc": "^15.1.0",
104104
"prettier": "^2.8.8",
105105
"semver": "^7.6.0",

src/error.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -765,9 +765,22 @@ export class MongoUnexpectedServerResponseError extends MongoRuntimeError {
765765
* @internal
766766
*/
767767
export class MongoOperationTimeoutError extends MongoRuntimeError {
768+
get [Symbol.toStringTag]() {
769+
return 'MongoOperationTimeoutError';
770+
}
771+
768772
override get name(): string {
769773
return 'MongoOperationTimeoutError';
770774
}
775+
776+
static is(error: unknown): error is MongoOperationTimeoutError {
777+
return (
778+
error != null &&
779+
typeof error === 'object' &&
780+
Symbol.toStringTag in error &&
781+
error[Symbol.toStringTag] === 'MongoOperationTimeoutError'
782+
);
783+
}
771784
}
772785

773786
/**

src/operations/execute_operation.ts

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -82,11 +82,6 @@ export async function executeOperation<
8282
} else if (session.client !== client) {
8383
throw new MongoInvalidArgumentError('ClientSession must be from the same MongoClient');
8484
}
85-
if (session.explicit && session?.timeoutMS != null && operation.options.timeoutMS != null) {
86-
throw new MongoInvalidArgumentError(
87-
'Do not specify timeoutMS on operation if already specified on an explicit session'
88-
);
89-
}
9085

9186
const readPreference = operation.readPreference ?? ReadPreference.primary;
9287
const inTransaction = !!session?.inTransaction();
@@ -107,11 +102,13 @@ export async function executeOperation<
107102
session.unpin();
108103
}
109104

110-
timeoutContext ??= TimeoutContext.create({
111-
serverSelectionTimeoutMS: client.s.options.serverSelectionTimeoutMS,
112-
waitQueueTimeoutMS: client.s.options.waitQueueTimeoutMS,
113-
timeoutMS: operation.options.timeoutMS
114-
});
105+
timeoutContext ??=
106+
session.timeoutContext ??
107+
TimeoutContext.create({
108+
serverSelectionTimeoutMS: client.s.options.serverSelectionTimeoutMS,
109+
waitQueueTimeoutMS: client.s.options.waitQueueTimeoutMS,
110+
timeoutMS: operation.options.timeoutMS ?? session.timeoutMS
111+
});
115112

116113
try {
117114
return await tryOperation(operation, {

src/sessions.ts

Lines changed: 59 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
MongoErrorLabel,
1717
MongoExpiredSessionError,
1818
MongoInvalidArgumentError,
19+
MongoOperationTimeoutError,
1920
MongoRuntimeError,
2021
MongoServerError,
2122
MongoTransactionError,
@@ -29,6 +30,7 @@ import { ReadConcernLevel } from './read_concern';
2930
import { ReadPreference } from './read_preference';
3031
import { type AsyncDisposable, configureResourceManagement } from './resource_management';
3132
import { _advanceClusterTime, type ClusterTime, TopologyType } from './sdam/common';
33+
import { TimeoutContext } from './timeout';
3234
import {
3335
isTransactionCommand,
3436
Transaction,
@@ -101,6 +103,9 @@ export interface EndSessionOptions {
101103
error?: AnyError;
102104
force?: boolean;
103105
forceClear?: boolean;
106+
107+
/** @internal */
108+
timeoutMS?: number;
104109
}
105110

106111
/**
@@ -118,7 +123,7 @@ export class ClientSession
118123
/** @internal */
119124
sessionPool: ServerSessionPool;
120125
hasEnded: boolean;
121-
clientOptions?: MongoOptions;
126+
clientOptions: MongoOptions;
122127
supports: { causalConsistency: boolean };
123128
clusterTime?: ClusterTime;
124129
operationTime?: Timestamp;
@@ -140,6 +145,9 @@ export class ClientSession
140145
/** @internal */
141146
timeoutMS?: number;
142147

148+
/** @internal */
149+
public timeoutContext: TimeoutContext | null = null;
150+
143151
/**
144152
* Create a client session.
145153
* @internal
@@ -152,7 +160,7 @@ export class ClientSession
152160
client: MongoClient,
153161
sessionPool: ServerSessionPool,
154162
options: ClientSessionOptions,
155-
clientOptions?: MongoOptions
163+
clientOptions: MongoOptions
156164
) {
157165
super();
158166

@@ -272,7 +280,11 @@ export class ClientSession
272280
async endSession(options?: EndSessionOptions): Promise<void> {
273281
try {
274282
if (this.inTransaction()) {
275-
await this.abortTransaction();
283+
if (typeof options?.timeoutMS === 'number') {
284+
await this.abortTransaction({ timeoutMS: options.timeoutMS });
285+
} else {
286+
await this.abortTransaction();
287+
}
276288
}
277289
if (!this.hasEnded) {
278290
const serverSession = this[kServerSession];
@@ -291,6 +303,7 @@ export class ClientSession
291303
}
292304
} catch (error) {
293305
// spec indicates that we should ignore all errors for `endSessions`
306+
if (MongoOperationTimeoutError.is(error)) throw error;
294307
squashError(error);
295308
} finally {
296309
maybeClearPinnedConnection(this, { force: true, ...options });
@@ -444,16 +457,20 @@ export class ClientSession
444457

445458
/**
446459
* Commits the currently active transaction in this session.
460+
*
461+
* @param options - Optional options, can be used to override `defaultTimeoutMS`.
447462
*/
448-
async commitTransaction(): Promise<void> {
449-
return await endTransaction(this, 'commitTransaction');
463+
async commitTransaction(options?: { timeoutMS: number }): Promise<void> {
464+
return await endTransaction(this, 'commitTransaction', options);
450465
}
451466

452467
/**
453468
* Aborts the currently active transaction in this session.
469+
*
470+
* @param options - Optional options, can be used to override `defaultTimeoutMS`.
454471
*/
455-
async abortTransaction(): Promise<void> {
456-
return await endTransaction(this, 'abortTransaction');
472+
async abortTransaction(options?: { timeoutMS: number }): Promise<void> {
473+
return await endTransaction(this, 'abortTransaction', options);
457474
}
458475

459476
/**
@@ -499,7 +516,15 @@ export class ClientSession
499516
fn: WithTransactionCallback<T>,
500517
options?: TransactionOptions
501518
): Promise<T> {
502-
const startTime = now();
519+
if (typeof options?.timeoutMS === 'number')
520+
this.timeoutContext = TimeoutContext.create({
521+
timeoutMS: options.timeoutMS,
522+
serverSelectionTimeoutMS: this.clientOptions.serverSelectionTimeoutMS,
523+
socketTimeoutMS: this.clientOptions.socketTimeoutMS
524+
});
525+
const { timeoutContext } = this;
526+
527+
const startTime = timeoutContext?.csotEnabled() ? timeoutContext.start : now();
503528
return await attemptTransaction(this, startTime, fn, options);
504529
}
505530
}
@@ -677,7 +702,8 @@ async function attemptTransaction<T>(
677702

678703
async function endTransaction(
679704
session: ClientSession,
680-
commandName: 'abortTransaction' | 'commitTransaction'
705+
commandName: 'abortTransaction' | 'commitTransaction',
706+
options: { timeoutMS?: number } = {}
681707
): Promise<void> {
682708
// handle any initial problematic cases
683709
const txnState = session.transaction.state;
@@ -749,6 +775,25 @@ async function endTransaction(
749775
command.recoveryToken = session.transaction.recoveryToken;
750776
}
751777

778+
const timeoutMS =
779+
'timeoutMS' in options && typeof options.timeoutMS === 'number'
780+
? options.timeoutMS
781+
: typeof session.timeoutMS === 'number'
782+
? session.timeoutMS
783+
: session.timeoutContext?.csotEnabled()
784+
? session.timeoutContext.timeoutMS
785+
: null;
786+
787+
const timeoutContext =
788+
// override for this operation
789+
TimeoutContext.create({
790+
serverSelectionTimeoutMS: session.clientOptions.serverSelectionTimeoutMS,
791+
socketTimeoutMS: session.clientOptions.socketTimeoutMS,
792+
...(timeoutMS != null
793+
? { timeoutMS }
794+
: { waitQueueTimeoutMS: session.clientOptions.waitQueueTimeoutMS })
795+
});
796+
752797
try {
753798
// send the command
754799
await executeOperation(
@@ -757,7 +802,8 @@ async function endTransaction(
757802
session,
758803
readPreference: ReadPreference.primary,
759804
bypassPinningCheck: true
760-
})
805+
}),
806+
timeoutContext
761807
);
762808
if (command.abortTransaction) {
763809
// always unpin on abort regardless of command outcome
@@ -794,7 +840,8 @@ async function endTransaction(
794840
session,
795841
readPreference: ReadPreference.primary,
796842
bypassPinningCheck: true
797-
})
843+
}),
844+
timeoutContext
798845
);
799846
if (commandName !== 'commitTransaction') {
800847
session.transaction.transition(TxnState.TRANSACTION_ABORTED);
@@ -824,6 +871,7 @@ function handleEndTransactionError(
824871
maybeClearPinnedConnection(session, { force: false });
825872
}
826873
// The spec indicates that if the operation times out or fails with a non-retryable error, we should ignore all errors on `abortTransaction`
874+
if (MongoOperationTimeoutError.is(error)) throw error; // But not if it is CSOT
827875
return;
828876
}
829877

src/timeout.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ export class CSOTTimeoutContext extends TimeoutContext {
184184
private _serverSelectionTimeout?: Timeout | null;
185185
private _connectionCheckoutTimeout?: Timeout | null;
186186
public minRoundTripTime = 0;
187-
private start: number;
187+
public start: number;
188188

189189
constructor(options: CSOTTimeoutContextOptions) {
190190
super();

src/utils.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -528,6 +528,12 @@ export function resolveOptions<T extends CommandOperationOptions>(
528528
result.readPreference = readPreference;
529529
}
530530

531+
if (session?.explicit && session.timeoutMS != null && options?.timeoutMS != null) {
532+
throw new MongoInvalidArgumentError(
533+
'Do not specify timeoutMS on operation if already specified on an explicit session'
534+
);
535+
}
536+
531537
const timeoutMS = options?.timeoutMS;
532538

533539
result.timeoutMS = timeoutMS ?? parent?.timeoutMS;

0 commit comments

Comments
 (0)