Skip to content

Commit 19f51b5

Browse files
committed
update to latest spec
1 parent ee325d0 commit 19f51b5

2 files changed

Lines changed: 206 additions & 112 deletions

File tree

src/sessions.ts

Lines changed: 43 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -742,20 +742,23 @@ export class ClientSession
742742
// 1.1 Record the current monotonic time, which will be used to enforce the 120-second / CSOT timeout before later retry attempts.
743743
// 1.2 Set `transactionAttempt` to `0`.
744744
// 1.3 Set `TIMEOUT_MS` to be `timeoutMS` if given, otherwise MAX_TIMEOUT (120-seconds).
745-
//
745+
746+
// Timeout Error propagation
747+
// When the previously encountered error needs to be propagated because there is no more time for another attempt,
748+
// and it is not already a timeout error, then:
749+
// - A timeout error MUST be propagated instead. It MUST expose the previously encountered error as specified in
750+
// the "Errors" section of the CSOT specification.
751+
// - If exposing the previously encountered error from a timeout error is impossible in a driver, then the driver
752+
// is exempt from the requirement and MUST propagate the previously encountered error as is. The timeout error
753+
// MUST copy all error labels from the previously encountered error.
754+
746755
// The spec describes timeout checks as "elapsed time < TIMEOUT_MS" (where elapsed = now - start).
747-
// We precompute `deadline = start + TIMEOUT_MS` so each check becomes simply `now < deadline`.
748-
//
749-
// Timeout Error propagation mechanism
750-
// When the TIMEOUT_MS (calculated in step 1.3) is reached we MUST report a timeout error wrapping the previously
751-
// encountered error. If timeoutMS is set, then timeout error is a special type which is defined in CSOT
752-
// specification, If timeoutMS is not set, then propagate it as timeout error if the language allows to expose the
753-
// previously encountered error as a cause of a timeout error (see makeTimeoutError below in pseudo-code). If
754-
// timeout error is thrown then it SHOULD copy all error label(s) from the previously encountered retriable error.
756+
// We precompute `deadline = now + remainingTimeMS` so each check becomes simply `now < deadline`.
755757
const csotEnabled = !!this.timeoutContext?.csotEnabled();
756-
const deadline = this.timeoutContext?.csotEnabled()
757-
? processTimeMS() + this.timeoutContext.remainingTimeMS
758-
: processTimeMS() + MAX_TIMEOUT;
758+
const remainingTimeMS = this.timeoutContext?.csotEnabled()
759+
? this.timeoutContext.remainingTimeMS
760+
: MAX_TIMEOUT;
761+
const deadline = processTimeMS() + remainingTimeMS;
759762

760763
let committed = false;
761764
let result: T;
@@ -764,23 +767,23 @@ export class ClientSession
764767

765768
try {
766769
retryTransaction: for (
767-
// 1.2 Set `transactionAttempt` to `0`.
768770
let transactionAttempt = 0, isRetry = false;
769771
!committed;
770772
++transactionAttempt, isRetry = transactionAttempt > 0
771773
) {
772774
// 2. If `transactionAttempt` > 0:
773775
if (isRetry) {
774-
// 2.1 If elapsed time + backoffMS > TIMEOUT_MS, then propagate the previously encountered
775-
// error (see propagation section above). If the elapsed time of withTransaction is less
776-
// than TIMEOUT_MS, calculate the backoffMS to be
777-
// jitter * min(BACKOFF_INITIAL * 1.5 ** (transactionAttempt - 1), BACKOFF_MAX).
778-
// sleep for backoffMS.
776+
// 2.1 Calculate backoffMS to be jitter * min(BACKOFF_INITIAL * 1.5 ** (transactionAttempt - 1), BACKOFF_MAX).
777+
// If elapsed time + backoffMS > TIMEOUT_MS, then propagate the previously encountered error to the caller of
778+
// withTransaction as per timeout error propagation and return immediately. Otherwise, sleep for backoffMS.
779+
// 2.1.1 jitter is a random float between [0, 1), optionally including 1, depending on what is most natural
780+
// for the given driver language.
781+
// 2.1.2 transactionAttempt is the variable defined in step 1.
782+
// 2.1.3 BACKOFF_INITIAL is 5ms
783+
// 2.1.4 BACKOFF_MAX is 500ms
779784
const BACKOFF_INITIAL_MS = 5;
780785
const BACKOFF_MAX_MS = 500;
781786
const BACKOFF_GROWTH = 1.5;
782-
// 2.1.1 Jitter is a random float between [0, 1), optionally including 1, depending on what is most natural
783-
// for the given driver language.
784787
const jitter = Math.random();
785788
const backoffMS =
786789
jitter *
@@ -805,8 +808,9 @@ export class ClientSession
805808
// 3. Invoke startTransaction on the session and increment transactionAttempt. If TransactionOptions were
806809
// specified in the call to withTransaction, those MUST be used for startTransaction. Note that
807810
// ClientSession.defaultTransactionOptions will be used in the absence of any explicit TransactionOptions.
808-
// 4. If startTransaction reported an error, propagate that error to the caller and return immediately.
809-
this.startTransaction(options); // may throw on error
811+
// 4. If startTransaction reported an error, propagate that error to the caller of withTransaction as is and
812+
// return immediately.
813+
this.startTransaction(options);
810814

811815
try {
812816
// 5. Invoke the callback. Drivers MUST ensure that the ClientSession can be accessed within the callback
@@ -825,16 +829,15 @@ export class ClientSession
825829

826830
// 8. If the ClientSession is in the "no transaction", "transaction aborted", or "transaction committed"
827831
// state, assume the callback intentionally aborted or committed the transaction and return immediately.
828-
// Drivers MAY allow the callback to return a value to be propagated as the return value of withTransaction.
829832
if (
830833
this.transaction.state === TxnState.NO_TRANSACTION ||
831834
this.transaction.state === TxnState.TRANSACTION_COMMITTED ||
832835
this.transaction.state === TxnState.TRANSACTION_ABORTED
833836
) {
834837
return result;
835838
}
836-
// 7. If the callback reported an error:
837839
} catch (fnError) {
840+
// 7. If the callback reported an error
838841
if (!(fnError instanceof MongoError) || fnError instanceof MongoInvalidArgumentError) {
839842
// This first preemptive abort regardless of TxnState isn't spec,
840843
// and it's unclear whether it's serving a practical purpose, but this logic is OLD
@@ -861,46 +864,45 @@ export class ClientSession
861864
continue retryTransaction;
862865
}
863866

864-
// 7.3 If the callback's error includes a "UnknownTransactionCommitResult" label, the callback
865-
// must have manually committed a transaction, propagate the error and return immediately.
866-
// (This check is redundant with step 8, so we don't write code for it.)
867-
// 7.4 Otherwise, propagate the callback's error (see Note 1) and return immediately.
867+
// 7.3 If the callback's error includes a "UnknownTransactionCommitResult" label, the callback must
868+
// have manually committed a transaction, propagate the callback's error to the caller of withTransaction
869+
// as is and return immediately.
870+
// 7.4 Otherwise, propagate the callback's error to the caller of withTransaction as is and return immediately.
868871
throw fnError;
869872
}
870873

871-
// 9. Invoke commitTransaction on the session.
872-
// We will rely on ClientSession.commitTransaction() to apply a majority write concern
873-
// if commitTransaction is being retried (see: DRIVERS-601).
874874
retryCommit: while (!committed) {
875875
try {
876+
// 9. Invoke commitTransaction on the session.
876877
await this.commitTransaction();
877878
committed = true;
878879
} catch (commitError) {
879880
// 10. If commitTransaction reported an error:
880881
lastError = commitError;
881882

882-
// 10.1 If the commitTransaction error includes a UnknownTransactionCommitResult label and the error is not MaxTimeMSExpired
883+
// 10.1 If the commitTransaction error includes a UnknownTransactionCommitResult label and the error is
884+
// not MaxTimeMSExpired
883885
if (
884-
!isMaxTimeMSExpiredError(commitError) &&
885-
commitError.hasErrorLabel(MongoErrorLabel.UnknownTransactionCommitResult)
886+
commitError.hasErrorLabel(MongoErrorLabel.UnknownTransactionCommitResult) &&
887+
!isMaxTimeMSExpiredError(commitError)
886888
) {
887-
// 10.1.1 If the elapsed time of withTransaction exceeded TIMEOUT_MS, propagate the commitTransaction error to the caller
888-
// of withTransaction and return immediately (see propagation section above)
889+
// 10.1.1 If the elapsed time of withTransaction exceeded TIMEOUT_MS, propagate the commitTransaction
890+
// error to the caller of withTransaction as per timeout error propagation and return immediately.
889891
if (processTimeMS() >= deadline) {
890892
throw makeTimeoutError(commitError, csotEnabled);
891893
}
892-
// 10.1.2 If the elapsed time of withTransaction is less than TIMEOUT_MS, jump back to step nine. We will trust
893-
// commitTransaction to apply a majority write concern on retry attempts (see: Majority write concern is used
894-
// when retrying commitTransaction).
894+
// 10.1.2 Otherwise, jump back to step nine. We will trust commitTransaction to apply a majority write
895+
// concern on retry attempts (see: Majority write concern is used when retrying commitTransaction).
895896
continue retryCommit;
896897
}
897898

898-
// 10.2 If the commitTransaction error includes a "TransientTransactionError" label, jump back to step two.
899+
// 10.2 If the commitTransaction error includes a TransientTransactionError label, jump back to step two.
899900
if (commitError.hasErrorLabel(MongoErrorLabel.TransientTransactionError)) {
900901
continue retryTransaction;
901902
}
902903

903-
// 10.3 Otherwise, propagate the commitTransaction error to the caller of withTransaction and return immediately.
904+
// 10.3 Otherwise, propagate the commitTransaction error to the caller of withTransaction as is and return
905+
// immediately.
904906
throw commitError;
905907
}
906908
}

0 commit comments

Comments
 (0)