Skip to content

Commit bad814f

Browse files
committed
chore: fix when timeout errors throw
1 parent 4cafed6 commit bad814f

File tree

5 files changed

+145
-101
lines changed

5 files changed

+145
-101
lines changed

src/cmap/connection.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -737,6 +737,13 @@ export class Connection extends TypedEventEmitter<ConnectionEvents> {
737737
return;
738738
}
739739
}
740+
} catch (readError) {
741+
if (TimeoutError.is(readError)) {
742+
throw new MongoOperationTimeoutError(
743+
`Timed out during socket read (${readError.duration}ms)`
744+
);
745+
}
746+
throw readError;
740747
} finally {
741748
this.dataEvents = null;
742749
this.throwIfAborted();

src/cmap/wire_protocol/on_data.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { type EventEmitter } from 'events';
22

3-
import { MongoOperationTimeoutError } from '../../error';
4-
import { type TimeoutContext, TimeoutError } from '../../timeout';
3+
import { type TimeoutContext } from '../../timeout';
54
import { List, promiseWithResolvers } from '../../utils';
65

76
/**
@@ -91,8 +90,11 @@ export function onData(
9190
// Adding event handlers
9291
emitter.on('data', eventHandler);
9392
emitter.on('error', errorHandler);
93+
94+
const timeoutForSocketRead = timeoutContext?.timeoutForSocketRead;
95+
timeoutForSocketRead?.throwIfExpired();
9496
// eslint-disable-next-line github/no-then
95-
timeoutContext?.timeoutForSocketRead?.then(undefined, errorHandler);
97+
timeoutForSocketRead?.then(undefined, errorHandler);
9698

9799
return iterator;
98100

@@ -104,12 +106,9 @@ export function onData(
104106

105107
function errorHandler(err: Error) {
106108
const promise = unconsumedPromises.shift();
107-
const timeoutError = TimeoutError.is(err)
108-
? new MongoOperationTimeoutError(`Timed out during socket read (${err.duration}ms)`)
109-
: undefined;
110109

111-
if (promise != null) promise.reject(timeoutError ?? err);
112-
else error = timeoutError ?? err;
110+
if (promise != null) promise.reject(err);
111+
else error = err;
113112
void closeHandler();
114113
}
115114

src/sessions.ts

Lines changed: 120 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ import { ReadConcernLevel } from './read_concern';
3030
import { ReadPreference } from './read_preference';
3131
import { type AsyncDisposable, configureResourceManagement } from './resource_management';
3232
import { _advanceClusterTime, type ClusterTime, TopologyType } from './sdam/common';
33-
import { type TimeoutContext } from './timeout';
33+
import { TimeoutContext } from './timeout';
3434
import {
3535
isTransactionCommand,
3636
Transaction,
@@ -280,12 +280,13 @@ export class ClientSession
280280
async endSession(options?: EndSessionOptions): Promise<void> {
281281
try {
282282
if (this.inTransaction()) {
283-
if (typeof options?.timeoutMS === 'number') {
284-
await endTransaction(this, 'abortTransaction', { timeoutMS: options.timeoutMS });
285-
} else {
286-
await endTransaction(this, 'abortTransaction');
287-
}
283+
await this.abortTransaction({ ...options, throwTimeout: true });
288284
}
285+
} catch (error) {
286+
// spec indicates that we should ignore all errors for `endSessions`
287+
if (MongoOperationTimeoutError.is(error)) throw error;
288+
squashError(error);
289+
} finally {
289290
if (!this.hasEnded) {
290291
const serverSession = this[kServerSession];
291292
if (serverSession != null) {
@@ -301,11 +302,6 @@ export class ClientSession
301302
this.hasEnded = true;
302303
this.emit('ended', this);
303304
}
304-
} catch (error) {
305-
// spec indicates that we should ignore all errors for `endSessions`
306-
if (MongoOperationTimeoutError.is(error)) throw error;
307-
squashError(error);
308-
} finally {
309305
maybeClearPinnedConnection(this, { force: true, ...options });
310306
}
311307
}
@@ -460,7 +456,7 @@ export class ClientSession
460456
*
461457
* @param options - Optional options, can be used to override `defaultTimeoutMS`.
462458
*/
463-
async commitTransaction(): Promise<void> {
459+
async commitTransaction(options?: { timeoutMS?: number }): Promise<void> {
464460
if (this.transaction.state === TxnState.NO_TRANSACTION) {
465461
throw new MongoTransactionError('No transaction started');
466462
}
@@ -510,8 +506,19 @@ export class ClientSession
510506
bypassPinningCheck: true
511507
});
512508

509+
const timeoutMS =
510+
typeof options?.timeoutMS === 'number'
511+
? options.timeoutMS
512+
: typeof this.timeoutMS === 'number'
513+
? this.timeoutMS
514+
: null;
515+
516+
const timeoutContext = this.timeoutContext?.csotEnabled()
517+
? this.timeoutContext
518+
: TimeoutContext.create({ timeoutMS, ...this.clientOptions });
519+
513520
try {
514-
await executeOperation(this.client, operation);
521+
await executeOperation(this.client, operation, timeoutContext);
515522
return;
516523
} catch (firstCommitError) {
517524
if (firstCommitError instanceof MongoError && isRetryableWriteError(firstCommitError)) {
@@ -521,7 +528,7 @@ export class ClientSession
521528
this.unpin({ force: true });
522529

523530
try {
524-
await executeOperation(this.client, operation);
531+
await executeOperation(this.client, operation, timeoutContext);
525532
return;
526533
} catch (retryCommitError) {
527534
// If the retry failed, we process that error instead of the original
@@ -556,7 +563,10 @@ export class ClientSession
556563
*
557564
* @param options - Optional options, can be used to override `defaultTimeoutMS`.
558565
*/
559-
async abortTransaction(): Promise<void> {
566+
async abortTransaction(options?: { timeoutMS?: number }): Promise<void>;
567+
/** @internal */
568+
async abortTransaction(options?: { timeoutMS?: number; throwTimeout?: true }): Promise<void>;
569+
async abortTransaction(options?: { timeoutMS?: number; throwTimeout?: true }): Promise<void> {
560570
if (this.transaction.state === TxnState.NO_TRANSACTION) {
561571
throw new MongoTransactionError('No transaction started');
562572
}
@@ -601,18 +611,34 @@ export class ClientSession
601611
bypassPinningCheck: true
602612
});
603613

614+
const timeoutMS =
615+
typeof options?.timeoutMS === 'number'
616+
? options.timeoutMS
617+
: this.timeoutContext?.csotEnabled()
618+
? this.timeoutContext.timeoutMS // refresh timeoutMS for abort operation
619+
: typeof this.timeoutMS === 'number'
620+
? this.timeoutMS
621+
: null;
622+
623+
const timeoutContext = TimeoutContext.create({ timeoutMS, ...this.clientOptions });
624+
604625
try {
605-
await executeOperation(this.client, operation);
626+
await executeOperation(this.client, operation, timeoutContext);
606627
this.unpin();
607628
return;
608629
} catch (firstAbortError) {
609630
this.unpin();
610631

632+
if (options?.throwTimeout && MongoOperationTimeoutError.is(firstAbortError))
633+
throw firstAbortError;
634+
611635
if (firstAbortError instanceof MongoError && isRetryableWriteError(firstAbortError)) {
612636
try {
613-
await executeOperation(this.client, operation);
637+
await executeOperation(this.client, operation, timeoutContext);
614638
return;
615639
} catch (secondAbortError) {
640+
if (options?.throwTimeout && MongoOperationTimeoutError.is(secondAbortError))
641+
throw secondAbortError;
616642
// we do not retry the retry
617643
}
618644
}
@@ -670,93 +696,102 @@ export class ClientSession
670696
options?: TransactionOptions
671697
): Promise<T> {
672698
const MAX_TIMEOUT = 120000;
673-
const startTime = now();
674-
675-
let committed = false;
676-
let result: any;
677699

678-
while (!committed) {
679-
this.startTransaction(options); // may throw on error
700+
this.timeoutContext =
701+
options != null && 'timeoutMS' in options && typeof options.timeoutMS === 'number'
702+
? TimeoutContext.create({ timeoutMS: options.timeoutMS, ...this.clientOptions })
703+
: null;
680704

681-
try {
682-
const promise = fn(this);
683-
if (!isPromiseLike(promise)) {
684-
throw new MongoInvalidArgumentError(
685-
'Function provided to `withTransaction` must return a Promise'
686-
);
687-
}
705+
const startTime = this.timeoutContext?.csotEnabled() ? this.timeoutContext.start : now();
688706

689-
result = await promise;
707+
let committed = false;
708+
let result: any;
690709

691-
if (
692-
this.transaction.state === TxnState.NO_TRANSACTION ||
693-
this.transaction.state === TxnState.TRANSACTION_COMMITTED ||
694-
this.transaction.state === TxnState.TRANSACTION_ABORTED
695-
) {
696-
// Assume callback intentionally ended the transaction
697-
return result;
698-
}
699-
} catch (fnError) {
700-
if (!(fnError instanceof MongoError) || fnError instanceof MongoInvalidArgumentError) {
701-
await this.abortTransaction();
702-
throw fnError;
703-
}
710+
try {
711+
while (!committed) {
712+
this.startTransaction(options); // may throw on error
704713

705-
if (
706-
this.transaction.state === TxnState.STARTING_TRANSACTION ||
707-
this.transaction.state === TxnState.TRANSACTION_IN_PROGRESS
708-
) {
709-
await this.abortTransaction();
710-
}
714+
try {
715+
const promise = fn(this);
716+
if (!isPromiseLike(promise)) {
717+
throw new MongoInvalidArgumentError(
718+
'Function provided to `withTransaction` must return a Promise'
719+
);
720+
}
711721

712-
if (
713-
fnError.hasErrorLabel(MongoErrorLabel.TransientTransactionError) &&
714-
now() - startTime < MAX_TIMEOUT
715-
) {
716-
continue;
717-
}
722+
result = await promise;
718723

719-
throw fnError;
720-
}
724+
if (
725+
this.transaction.state === TxnState.NO_TRANSACTION ||
726+
this.transaction.state === TxnState.TRANSACTION_COMMITTED ||
727+
this.transaction.state === TxnState.TRANSACTION_ABORTED
728+
) {
729+
// Assume callback intentionally ended the transaction
730+
return result;
731+
}
732+
} catch (fnError) {
733+
if (!(fnError instanceof MongoError) || fnError instanceof MongoInvalidArgumentError) {
734+
await this.abortTransaction();
735+
throw fnError;
736+
}
721737

722-
while (!committed) {
723-
try {
724-
/*
725-
* We will rely on ClientSession.commitTransaction() to
726-
* apply a majority write concern if commitTransaction is
727-
* being retried (see: DRIVERS-601)
728-
*/
729-
await this.commitTransaction();
730-
committed = true;
731-
} catch (commitError) {
732-
/*
733-
* Note: a maxTimeMS error will have the MaxTimeMSExpired
734-
* code (50) and can be reported as a top-level error or
735-
* inside writeConcernError, ex.
736-
* { ok:0, code: 50, codeName: 'MaxTimeMSExpired' }
737-
* { ok:1, writeConcernError: { code: 50, codeName: 'MaxTimeMSExpired' } }
738-
*/
739738
if (
740-
!isMaxTimeMSExpiredError(commitError) &&
741-
commitError.hasErrorLabel(MongoErrorLabel.UnknownTransactionCommitResult) &&
742-
now() - startTime < MAX_TIMEOUT
739+
this.transaction.state === TxnState.STARTING_TRANSACTION ||
740+
this.transaction.state === TxnState.TRANSACTION_IN_PROGRESS
743741
) {
744-
continue;
742+
await this.abortTransaction();
745743
}
746744

747745
if (
748-
commitError.hasErrorLabel(MongoErrorLabel.TransientTransactionError) &&
749-
now() - startTime < MAX_TIMEOUT
746+
fnError.hasErrorLabel(MongoErrorLabel.TransientTransactionError) &&
747+
(this.timeoutContext != null || now() - startTime < MAX_TIMEOUT)
750748
) {
751-
break;
749+
continue;
752750
}
753751

754-
throw commitError;
752+
throw fnError;
753+
}
754+
755+
while (!committed) {
756+
try {
757+
/*
758+
* We will rely on ClientSession.commitTransaction() to
759+
* apply a majority write concern if commitTransaction is
760+
* being retried (see: DRIVERS-601)
761+
*/
762+
await this.commitTransaction();
763+
committed = true;
764+
} catch (commitError) {
765+
/*
766+
* Note: a maxTimeMS error will have the MaxTimeMSExpired
767+
* code (50) and can be reported as a top-level error or
768+
* inside writeConcernError, ex.
769+
* { ok:0, code: 50, codeName: 'MaxTimeMSExpired' }
770+
* { ok:1, writeConcernError: { code: 50, codeName: 'MaxTimeMSExpired' } }
771+
*/
772+
if (
773+
!isMaxTimeMSExpiredError(commitError) &&
774+
commitError.hasErrorLabel(MongoErrorLabel.UnknownTransactionCommitResult) &&
775+
(this.timeoutContext != null || now() - startTime < MAX_TIMEOUT)
776+
) {
777+
continue;
778+
}
779+
780+
if (
781+
commitError.hasErrorLabel(MongoErrorLabel.TransientTransactionError) &&
782+
(this.timeoutContext != null || now() - startTime < MAX_TIMEOUT)
783+
) {
784+
break;
785+
}
786+
787+
throw commitError;
788+
}
755789
}
756790
}
791+
return result;
792+
} finally {
793+
this.timeoutContext = null;
757794
}
758-
759-
return result;
760795
}
761796
}
762797

src/timeout.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ export class Timeout extends Promise<never> {
7474
this.duration = duration;
7575
this.start = Math.trunc(performance.now());
7676

77-
if (this.duration > 0) {
77+
if (rejection == null && this.duration > 0) {
7878
this.id = setTimeout(() => {
7979
this.ended = Math.trunc(performance.now());
8080
this.timedOut = true;
@@ -84,9 +84,11 @@ export class Timeout extends Promise<never> {
8484
// Ensure we do not keep the Node.js event loop running
8585
this.id.unref();
8686
}
87+
} else if (rejection != null) {
88+
this.ended = Math.trunc(performance.now());
89+
this.timedOut = true;
90+
reject(rejection);
8791
}
88-
89-
if (rejection != null) reject(rejection);
9092
}
9193

9294
/**
@@ -260,10 +262,8 @@ export class CSOTTimeoutContext extends TimeoutContext {
260262
// null or Timeout
261263
this._connectionCheckoutTimeout = this._serverSelectionTimeout;
262264
} else {
263-
return Timeout.reject(
264-
new MongoRuntimeError(
265-
'Unreachable. If you are seeing this error, please file a ticket on the NODE driver project on Jira'
266-
)
265+
throw new MongoRuntimeError(
266+
'Unreachable. If you are seeing this error, please file a ticket on the NODE driver project on Jira'
267267
);
268268
}
269269
}

src/transactions.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,10 @@ export interface TransactionOptions extends CommandOperationOptions {
6969
writeConcern?: WriteConcern;
7070
/** A default read preference for commands in this transaction */
7171
readPreference?: ReadPreferenceLike;
72-
/** Specifies the maximum amount of time to allow a commit action on a transaction to run in milliseconds */
72+
/**
73+
* Specifies the maximum amount of time to allow a commit action on a transaction to run in milliseconds
74+
* @deprecated This option is deprecated in favor of `timeoutMS` or `defaultTimeoutMS`.
75+
*/
7376
maxCommitTimeMS?: number;
7477
}
7578

0 commit comments

Comments
 (0)