mongodb 6.8.0-dev.20240808.sha.5565d500 → 6.8.0-dev.20240821.sha.55bdeaa9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -30,6 +30,8 @@ const QUERY_FAILURE = 2;
30
30
  const SHARD_CONFIG_STALE = 4;
31
31
  const AWAIT_CAPABLE = 8;
32
32
 
33
+ const encodeUTF8Into = BSON.BSON.onDemand.ByteUtils.encodeUTF8Into;
34
+
33
35
  /** @internal */
34
36
  export type WriteProtocolMessageType = OpQueryRequest | OpMsgRequest;
35
37
 
@@ -411,6 +413,15 @@ export interface OpMsgOptions {
411
413
  readPreference: ReadPreference;
412
414
  }
413
415
 
416
+ /** @internal */
417
+ export class DocumentSequence {
418
+ documents: Document[];
419
+
420
+ constructor(documents: Document[]) {
421
+ this.documents = documents;
422
+ }
423
+ }
424
+
414
425
  /** @internal */
415
426
  export class OpMsgRequest {
416
427
  requestId: number;
@@ -480,7 +491,7 @@ export class OpMsgRequest {
480
491
 
481
492
  let totalLength = header.length;
482
493
  const command = this.command;
483
- totalLength += this.makeDocumentSegment(buffers, command);
494
+ totalLength += this.makeSections(buffers, command);
484
495
 
485
496
  header.writeInt32LE(totalLength, 0); // messageLength
486
497
  header.writeInt32LE(this.requestId, 4); // requestID
@@ -490,15 +501,65 @@ export class OpMsgRequest {
490
501
  return buffers;
491
502
  }
492
503
 
493
- makeDocumentSegment(buffers: Uint8Array[], document: Document): number {
494
- const payloadTypeBuffer = Buffer.alloc(1);
504
+ /**
505
+ * Add the sections to the OP_MSG request's buffers and returns the length.
506
+ */
507
+ makeSections(buffers: Uint8Array[], document: Document): number {
508
+ const sequencesBuffer = this.extractDocumentSequences(document);
509
+ const payloadTypeBuffer = Buffer.allocUnsafe(1);
495
510
  payloadTypeBuffer[0] = 0;
496
511
 
497
512
  const documentBuffer = this.serializeBson(document);
513
+ // First section, type 0
498
514
  buffers.push(payloadTypeBuffer);
499
515
  buffers.push(documentBuffer);
516
+ // Subsequent sections, type 1
517
+ buffers.push(sequencesBuffer);
500
518
 
501
- return payloadTypeBuffer.length + documentBuffer.length;
519
+ return payloadTypeBuffer.length + documentBuffer.length + sequencesBuffer.length;
520
+ }
521
+
522
+ /**
523
+ * Extracts the document sequences from the command document and returns
524
+ * a buffer to be added as multiple sections after the initial type 0
525
+ * section in the message.
526
+ */
527
+ extractDocumentSequences(document: Document): Uint8Array {
528
+ // Pull out any field in the command document that's value is a document sequence.
529
+ const chunks = [];
530
+ for (const [key, value] of Object.entries(document)) {
531
+ if (value instanceof DocumentSequence) {
532
+ // Document sequences starts with type 1 at the first byte.
533
+ const buffer = Buffer.allocUnsafe(1 + 4 + key.length);
534
+ buffer[0] = 1;
535
+ // Third part is the field name at offset 5.
536
+ encodeUTF8Into(buffer, key, 5);
537
+ chunks.push(buffer);
538
+ // Fourth part are the documents' bytes.
539
+ let docsLength = 0;
540
+ for (const doc of value.documents) {
541
+ const docBson = this.serializeBson(doc);
542
+ docsLength += docBson.length;
543
+ chunks.push(docBson);
544
+ }
545
+ // Second part of the sequence is the length at offset 1;
546
+ buffer.writeInt32LE(key.length + docsLength, 1);
547
+ // Why are we removing the field from the command? This is because it needs to be
548
+ // removed in the OP_MSG request first section, and DocumentSequence is not a
549
+ // BSON type and is specific to the MongoDB wire protocol so there's nothing
550
+ // our BSON serializer can do about this. Since DocumentSequence is not exposed
551
+ // in the public API and only used internally, we are never mutating an original
552
+ // command provided by the user, just our own, and it's cheaper to delete from
553
+ // our own command than copying it.
554
+ delete document[key];
555
+ }
556
+ }
557
+ if (chunks.length > 0) {
558
+ return Buffer.concat(chunks);
559
+ }
560
+ // If we have no document sequences we return an empty buffer for nothing to add
561
+ // to the payload.
562
+ return Buffer.alloc(0);
502
563
  }
503
564
 
504
565
  serializeBson(document: Document): Uint8Array {
package/src/sessions.ts CHANGED
@@ -46,7 +46,7 @@ import {
46
46
  squashError,
47
47
  uuidV4
48
48
  } from './utils';
49
- import { WriteConcern } from './write_concern';
49
+ import { WriteConcern, type WriteConcernOptions, type WriteConcernSettings } from './write_concern';
50
50
 
51
51
  const minWireVersionForShardedTransactions = 8;
52
52
 
@@ -443,14 +443,167 @@ export class ClientSession
443
443
  * Commits the currently active transaction in this session.
444
444
  */
445
445
  async commitTransaction(): Promise<void> {
446
- return await endTransaction(this, 'commitTransaction');
446
+ if (this.transaction.state === TxnState.NO_TRANSACTION) {
447
+ throw new MongoTransactionError('No transaction started');
448
+ }
449
+
450
+ if (
451
+ this.transaction.state === TxnState.STARTING_TRANSACTION ||
452
+ this.transaction.state === TxnState.TRANSACTION_COMMITTED_EMPTY
453
+ ) {
454
+ // the transaction was never started, we can safely exit here
455
+ this.transaction.transition(TxnState.TRANSACTION_COMMITTED_EMPTY);
456
+ return;
457
+ }
458
+
459
+ if (this.transaction.state === TxnState.TRANSACTION_ABORTED) {
460
+ throw new MongoTransactionError(
461
+ 'Cannot call commitTransaction after calling abortTransaction'
462
+ );
463
+ }
464
+
465
+ const command: {
466
+ commitTransaction: 1;
467
+ writeConcern?: WriteConcernSettings;
468
+ recoveryToken?: Document;
469
+ maxTimeMS?: number;
470
+ } = { commitTransaction: 1 };
471
+
472
+ const wc = this.transaction.options.writeConcern ?? this.clientOptions?.writeConcern;
473
+ if (wc != null) {
474
+ WriteConcern.apply(command, { wtimeoutMS: 10000, w: 'majority', ...wc });
475
+ }
476
+
477
+ if (this.transaction.state === TxnState.TRANSACTION_COMMITTED) {
478
+ WriteConcern.apply(command, { wtimeoutMS: 10000, ...wc, w: 'majority' });
479
+ }
480
+
481
+ if (typeof this.transaction.options.maxTimeMS === 'number') {
482
+ command.maxTimeMS = this.transaction.options.maxTimeMS;
483
+ }
484
+
485
+ if (this.transaction.recoveryToken) {
486
+ command.recoveryToken = this.transaction.recoveryToken;
487
+ }
488
+
489
+ const operation = new RunAdminCommandOperation(command, {
490
+ session: this,
491
+ readPreference: ReadPreference.primary,
492
+ bypassPinningCheck: true
493
+ });
494
+
495
+ try {
496
+ await executeOperation(this.client, operation);
497
+ return;
498
+ } catch (firstCommitError) {
499
+ if (firstCommitError instanceof MongoError && isRetryableWriteError(firstCommitError)) {
500
+ // SPEC-1185: apply majority write concern when retrying commitTransaction
501
+ WriteConcern.apply(command, { wtimeoutMS: 10000, ...wc, w: 'majority' });
502
+ // per txns spec, must unpin session in this case
503
+ this.unpin({ force: true });
504
+
505
+ try {
506
+ await executeOperation(this.client, operation);
507
+ return;
508
+ } catch (retryCommitError) {
509
+ // If the retry failed, we process that error instead of the original
510
+ if (shouldAddUnknownTransactionCommitResultLabel(retryCommitError)) {
511
+ retryCommitError.addErrorLabel(MongoErrorLabel.UnknownTransactionCommitResult);
512
+ }
513
+
514
+ if (shouldUnpinAfterCommitError(retryCommitError)) {
515
+ this.unpin({ error: retryCommitError });
516
+ }
517
+
518
+ throw retryCommitError;
519
+ }
520
+ }
521
+
522
+ if (shouldAddUnknownTransactionCommitResultLabel(firstCommitError)) {
523
+ firstCommitError.addErrorLabel(MongoErrorLabel.UnknownTransactionCommitResult);
524
+ }
525
+
526
+ if (shouldUnpinAfterCommitError(firstCommitError)) {
527
+ this.unpin({ error: firstCommitError });
528
+ }
529
+
530
+ throw firstCommitError;
531
+ } finally {
532
+ this.transaction.transition(TxnState.TRANSACTION_COMMITTED);
533
+ }
447
534
  }
448
535
 
449
536
  /**
450
537
  * Aborts the currently active transaction in this session.
451
538
  */
452
539
  async abortTransaction(): Promise<void> {
453
- return await endTransaction(this, 'abortTransaction');
540
+ if (this.transaction.state === TxnState.NO_TRANSACTION) {
541
+ throw new MongoTransactionError('No transaction started');
542
+ }
543
+
544
+ if (this.transaction.state === TxnState.STARTING_TRANSACTION) {
545
+ // the transaction was never started, we can safely exit here
546
+ this.transaction.transition(TxnState.TRANSACTION_ABORTED);
547
+ return;
548
+ }
549
+
550
+ if (this.transaction.state === TxnState.TRANSACTION_ABORTED) {
551
+ throw new MongoTransactionError('Cannot call abortTransaction twice');
552
+ }
553
+
554
+ if (
555
+ this.transaction.state === TxnState.TRANSACTION_COMMITTED ||
556
+ this.transaction.state === TxnState.TRANSACTION_COMMITTED_EMPTY
557
+ ) {
558
+ throw new MongoTransactionError(
559
+ 'Cannot call abortTransaction after calling commitTransaction'
560
+ );
561
+ }
562
+
563
+ const command: {
564
+ abortTransaction: 1;
565
+ writeConcern?: WriteConcernOptions;
566
+ recoveryToken?: Document;
567
+ } = { abortTransaction: 1 };
568
+
569
+ const wc = this.transaction.options.writeConcern ?? this.clientOptions?.writeConcern;
570
+ if (wc != null) {
571
+ WriteConcern.apply(command, { wtimeoutMS: 10000, w: 'majority', ...wc });
572
+ }
573
+
574
+ if (this.transaction.recoveryToken) {
575
+ command.recoveryToken = this.transaction.recoveryToken;
576
+ }
577
+
578
+ const operation = new RunAdminCommandOperation(command, {
579
+ session: this,
580
+ readPreference: ReadPreference.primary,
581
+ bypassPinningCheck: true
582
+ });
583
+
584
+ try {
585
+ await executeOperation(this.client, operation);
586
+ this.unpin();
587
+ return;
588
+ } catch (firstAbortError) {
589
+ this.unpin();
590
+
591
+ if (firstAbortError instanceof MongoError && isRetryableWriteError(firstAbortError)) {
592
+ try {
593
+ await executeOperation(this.client, operation);
594
+ return;
595
+ } catch (secondAbortError) {
596
+ // we do not retry the retry
597
+ }
598
+ }
599
+
600
+ // The spec indicates that if the operation times out or fails with a non-retryable error, we should ignore all errors on `abortTransaction`
601
+ } finally {
602
+ this.transaction.transition(TxnState.TRANSACTION_ABORTED);
603
+ if (this.loadBalanced) {
604
+ maybeClearPinnedConnection(this, { force: false });
605
+ }
606
+ }
454
607
  }
455
608
 
456
609
  /**
@@ -496,25 +649,132 @@ export class ClientSession
496
649
  fn: WithTransactionCallback<T>,
497
650
  options?: TransactionOptions
498
651
  ): Promise<T> {
652
+ const MAX_TIMEOUT = 120000;
499
653
  const startTime = now();
500
- return await attemptTransaction(this, startTime, fn, options);
654
+
655
+ let committed = false;
656
+ let result: any;
657
+
658
+ while (!committed) {
659
+ this.startTransaction(options); // may throw on error
660
+
661
+ try {
662
+ const promise = fn(this);
663
+ if (!isPromiseLike(promise)) {
664
+ throw new MongoInvalidArgumentError(
665
+ 'Function provided to `withTransaction` must return a Promise'
666
+ );
667
+ }
668
+
669
+ result = await promise;
670
+
671
+ if (
672
+ this.transaction.state === TxnState.NO_TRANSACTION ||
673
+ this.transaction.state === TxnState.TRANSACTION_COMMITTED ||
674
+ this.transaction.state === TxnState.TRANSACTION_ABORTED
675
+ ) {
676
+ // Assume callback intentionally ended the transaction
677
+ return result;
678
+ }
679
+ } catch (fnError) {
680
+ if (!(fnError instanceof MongoError) || fnError instanceof MongoInvalidArgumentError) {
681
+ await this.abortTransaction();
682
+ throw fnError;
683
+ }
684
+
685
+ if (
686
+ this.transaction.state === TxnState.STARTING_TRANSACTION ||
687
+ this.transaction.state === TxnState.TRANSACTION_IN_PROGRESS
688
+ ) {
689
+ await this.abortTransaction();
690
+ }
691
+
692
+ if (
693
+ fnError.hasErrorLabel(MongoErrorLabel.TransientTransactionError) &&
694
+ now() - startTime < MAX_TIMEOUT
695
+ ) {
696
+ continue;
697
+ }
698
+
699
+ throw fnError;
700
+ }
701
+
702
+ while (!committed) {
703
+ try {
704
+ /*
705
+ * We will rely on ClientSession.commitTransaction() to
706
+ * apply a majority write concern if commitTransaction is
707
+ * being retried (see: DRIVERS-601)
708
+ */
709
+ await this.commitTransaction();
710
+ committed = true;
711
+ } catch (commitError) {
712
+ /*
713
+ * Note: a maxTimeMS error will have the MaxTimeMSExpired
714
+ * code (50) and can be reported as a top-level error or
715
+ * inside writeConcernError, ex.
716
+ * { ok:0, code: 50, codeName: 'MaxTimeMSExpired' }
717
+ * { ok:1, writeConcernError: { code: 50, codeName: 'MaxTimeMSExpired' } }
718
+ */
719
+ if (
720
+ !isMaxTimeMSExpiredError(commitError) &&
721
+ commitError.hasErrorLabel(MongoErrorLabel.UnknownTransactionCommitResult) &&
722
+ now() - startTime < MAX_TIMEOUT
723
+ ) {
724
+ continue;
725
+ }
726
+
727
+ if (
728
+ commitError.hasErrorLabel(MongoErrorLabel.TransientTransactionError) &&
729
+ now() - startTime < MAX_TIMEOUT
730
+ ) {
731
+ break;
732
+ }
733
+
734
+ throw commitError;
735
+ }
736
+ }
737
+ }
738
+
739
+ return result;
501
740
  }
502
741
  }
503
742
 
504
743
  configureResourceManagement(ClientSession.prototype);
505
744
 
506
- const MAX_WITH_TRANSACTION_TIMEOUT = 120000;
507
745
  const NON_DETERMINISTIC_WRITE_CONCERN_ERRORS = new Set([
508
746
  'CannotSatisfyWriteConcern',
509
747
  'UnknownReplWriteConcern',
510
748
  'UnsatisfiableWriteConcern'
511
749
  ]);
512
750
 
513
- function hasNotTimedOut(startTime: number, max: number) {
514
- return calculateDurationInMs(startTime) < max;
751
+ function shouldUnpinAfterCommitError(commitError: Error) {
752
+ if (commitError instanceof MongoError) {
753
+ if (
754
+ isRetryableWriteError(commitError) ||
755
+ commitError instanceof MongoWriteConcernError ||
756
+ isMaxTimeMSExpiredError(commitError)
757
+ ) {
758
+ if (isUnknownTransactionCommitResult(commitError)) {
759
+ // per txns spec, must unpin session in this case
760
+ return true;
761
+ }
762
+ } else if (commitError.hasErrorLabel(MongoErrorLabel.TransientTransactionError)) {
763
+ return true;
764
+ }
765
+ }
766
+ return false;
767
+ }
768
+
769
+ function shouldAddUnknownTransactionCommitResultLabel(commitError: MongoError) {
770
+ let ok = isRetryableWriteError(commitError);
771
+ ok ||= commitError instanceof MongoWriteConcernError;
772
+ ok ||= isMaxTimeMSExpiredError(commitError);
773
+ ok &&= isUnknownTransactionCommitResult(commitError);
774
+ return ok;
515
775
  }
516
776
 
517
- function isUnknownTransactionCommitResult(err: MongoError) {
777
+ function isUnknownTransactionCommitResult(err: MongoError): err is MongoError {
518
778
  const isNonDeterministicWriteConcernError =
519
779
  err instanceof MongoServerError &&
520
780
  err.codeName &&
@@ -569,282 +829,17 @@ export function maybeClearPinnedConnection(
569
829
  }
570
830
  }
571
831
 
572
- function isMaxTimeMSExpiredError(err: MongoError) {
832
+ function isMaxTimeMSExpiredError(err: MongoError): boolean {
573
833
  if (err == null || !(err instanceof MongoServerError)) {
574
834
  return false;
575
835
  }
576
836
 
577
837
  return (
578
838
  err.code === MONGODB_ERROR_CODES.MaxTimeMSExpired ||
579
- (err.writeConcernError && err.writeConcernError.code === MONGODB_ERROR_CODES.MaxTimeMSExpired)
839
+ err.writeConcernError?.code === MONGODB_ERROR_CODES.MaxTimeMSExpired
580
840
  );
581
841
  }
582
842
 
583
- async function attemptTransactionCommit<T>(
584
- session: ClientSession,
585
- startTime: number,
586
- fn: WithTransactionCallback<T>,
587
- result: T,
588
- options: TransactionOptions
589
- ): Promise<T> {
590
- try {
591
- await session.commitTransaction();
592
- return result;
593
- } catch (commitErr) {
594
- if (
595
- commitErr instanceof MongoError &&
596
- hasNotTimedOut(startTime, MAX_WITH_TRANSACTION_TIMEOUT) &&
597
- !isMaxTimeMSExpiredError(commitErr)
598
- ) {
599
- if (commitErr.hasErrorLabel(MongoErrorLabel.UnknownTransactionCommitResult)) {
600
- return await attemptTransactionCommit(session, startTime, fn, result, options);
601
- }
602
-
603
- if (commitErr.hasErrorLabel(MongoErrorLabel.TransientTransactionError)) {
604
- return await attemptTransaction(session, startTime, fn, options);
605
- }
606
- }
607
-
608
- throw commitErr;
609
- }
610
- }
611
-
612
- const USER_EXPLICIT_TXN_END_STATES = new Set<TxnState>([
613
- TxnState.NO_TRANSACTION,
614
- TxnState.TRANSACTION_COMMITTED,
615
- TxnState.TRANSACTION_ABORTED
616
- ]);
617
-
618
- function userExplicitlyEndedTransaction(session: ClientSession) {
619
- return USER_EXPLICIT_TXN_END_STATES.has(session.transaction.state);
620
- }
621
-
622
- async function attemptTransaction<T>(
623
- session: ClientSession,
624
- startTime: number,
625
- fn: WithTransactionCallback<T>,
626
- options: TransactionOptions = {}
627
- ): Promise<T> {
628
- session.startTransaction(options);
629
-
630
- let promise;
631
- try {
632
- promise = fn(session);
633
- } catch (err) {
634
- promise = Promise.reject(err);
635
- }
636
-
637
- if (!isPromiseLike(promise)) {
638
- try {
639
- await session.abortTransaction();
640
- } catch (error) {
641
- squashError(error);
642
- }
643
- throw new MongoInvalidArgumentError(
644
- 'Function provided to `withTransaction` must return a Promise'
645
- );
646
- }
647
-
648
- try {
649
- const result = await promise;
650
- if (userExplicitlyEndedTransaction(session)) {
651
- return result;
652
- }
653
- return await attemptTransactionCommit(session, startTime, fn, result, options);
654
- } catch (err) {
655
- if (session.inTransaction()) {
656
- await session.abortTransaction();
657
- }
658
-
659
- if (
660
- err instanceof MongoError &&
661
- err.hasErrorLabel(MongoErrorLabel.TransientTransactionError) &&
662
- hasNotTimedOut(startTime, MAX_WITH_TRANSACTION_TIMEOUT)
663
- ) {
664
- return await attemptTransaction(session, startTime, fn, options);
665
- }
666
-
667
- if (isMaxTimeMSExpiredError(err)) {
668
- err.addErrorLabel(MongoErrorLabel.UnknownTransactionCommitResult);
669
- }
670
-
671
- throw err;
672
- }
673
- }
674
-
675
- async function endTransaction(
676
- session: ClientSession,
677
- commandName: 'abortTransaction' | 'commitTransaction'
678
- ): Promise<void> {
679
- // handle any initial problematic cases
680
- const txnState = session.transaction.state;
681
-
682
- if (txnState === TxnState.NO_TRANSACTION) {
683
- throw new MongoTransactionError('No transaction started');
684
- }
685
-
686
- if (commandName === 'commitTransaction') {
687
- if (
688
- txnState === TxnState.STARTING_TRANSACTION ||
689
- txnState === TxnState.TRANSACTION_COMMITTED_EMPTY
690
- ) {
691
- // the transaction was never started, we can safely exit here
692
- session.transaction.transition(TxnState.TRANSACTION_COMMITTED_EMPTY);
693
- return;
694
- }
695
-
696
- if (txnState === TxnState.TRANSACTION_ABORTED) {
697
- throw new MongoTransactionError(
698
- 'Cannot call commitTransaction after calling abortTransaction'
699
- );
700
- }
701
- } else {
702
- if (txnState === TxnState.STARTING_TRANSACTION) {
703
- // the transaction was never started, we can safely exit here
704
- session.transaction.transition(TxnState.TRANSACTION_ABORTED);
705
- return;
706
- }
707
-
708
- if (txnState === TxnState.TRANSACTION_ABORTED) {
709
- throw new MongoTransactionError('Cannot call abortTransaction twice');
710
- }
711
-
712
- if (
713
- txnState === TxnState.TRANSACTION_COMMITTED ||
714
- txnState === TxnState.TRANSACTION_COMMITTED_EMPTY
715
- ) {
716
- throw new MongoTransactionError(
717
- 'Cannot call abortTransaction after calling commitTransaction'
718
- );
719
- }
720
- }
721
-
722
- // construct and send the command
723
- const command: Document = { [commandName]: 1 };
724
-
725
- // apply a writeConcern if specified
726
- let writeConcern;
727
- if (session.transaction.options.writeConcern) {
728
- writeConcern = Object.assign({}, session.transaction.options.writeConcern);
729
- } else if (session.clientOptions && session.clientOptions.writeConcern) {
730
- writeConcern = { w: session.clientOptions.writeConcern.w };
731
- }
732
-
733
- if (txnState === TxnState.TRANSACTION_COMMITTED) {
734
- writeConcern = Object.assign({ wtimeoutMS: 10000 }, writeConcern, { w: 'majority' });
735
- }
736
-
737
- if (writeConcern) {
738
- WriteConcern.apply(command, writeConcern);
739
- }
740
-
741
- if (commandName === 'commitTransaction' && session.transaction.options.maxTimeMS) {
742
- Object.assign(command, { maxTimeMS: session.transaction.options.maxTimeMS });
743
- }
744
-
745
- if (session.transaction.recoveryToken) {
746
- command.recoveryToken = session.transaction.recoveryToken;
747
- }
748
-
749
- try {
750
- // send the command
751
- await executeOperation(
752
- session.client,
753
- new RunAdminCommandOperation(command, {
754
- session,
755
- readPreference: ReadPreference.primary,
756
- bypassPinningCheck: true
757
- })
758
- );
759
- if (command.abortTransaction) {
760
- // always unpin on abort regardless of command outcome
761
- session.unpin();
762
- }
763
- if (commandName !== 'commitTransaction') {
764
- session.transaction.transition(TxnState.TRANSACTION_ABORTED);
765
- if (session.loadBalanced) {
766
- maybeClearPinnedConnection(session, { force: false });
767
- }
768
- } else {
769
- session.transaction.transition(TxnState.TRANSACTION_COMMITTED);
770
- }
771
- } catch (firstAttemptErr) {
772
- if (command.abortTransaction) {
773
- // always unpin on abort regardless of command outcome
774
- session.unpin();
775
- }
776
- if (firstAttemptErr instanceof MongoError && isRetryableWriteError(firstAttemptErr)) {
777
- // SPEC-1185: apply majority write concern when retrying commitTransaction
778
- if (command.commitTransaction) {
779
- // per txns spec, must unpin session in this case
780
- session.unpin({ force: true });
781
-
782
- command.writeConcern = Object.assign({ wtimeout: 10000 }, command.writeConcern, {
783
- w: 'majority'
784
- });
785
- }
786
-
787
- try {
788
- await executeOperation(
789
- session.client,
790
- new RunAdminCommandOperation(command, {
791
- session,
792
- readPreference: ReadPreference.primary,
793
- bypassPinningCheck: true
794
- })
795
- );
796
- if (commandName !== 'commitTransaction') {
797
- session.transaction.transition(TxnState.TRANSACTION_ABORTED);
798
- if (session.loadBalanced) {
799
- maybeClearPinnedConnection(session, { force: false });
800
- }
801
- } else {
802
- session.transaction.transition(TxnState.TRANSACTION_COMMITTED);
803
- }
804
- } catch (secondAttemptErr) {
805
- handleEndTransactionError(session, commandName, secondAttemptErr);
806
- }
807
- } else {
808
- handleEndTransactionError(session, commandName, firstAttemptErr);
809
- }
810
- }
811
- }
812
-
813
- function handleEndTransactionError(
814
- session: ClientSession,
815
- commandName: 'abortTransaction' | 'commitTransaction',
816
- error: Error
817
- ) {
818
- if (commandName !== 'commitTransaction') {
819
- session.transaction.transition(TxnState.TRANSACTION_ABORTED);
820
- if (session.loadBalanced) {
821
- maybeClearPinnedConnection(session, { force: false });
822
- }
823
- // The spec indicates that if the operation times out or fails with a non-retryable error, we should ignore all errors on `abortTransaction`
824
- return;
825
- }
826
-
827
- session.transaction.transition(TxnState.TRANSACTION_COMMITTED);
828
- if (error instanceof MongoError) {
829
- if (
830
- isRetryableWriteError(error) ||
831
- error instanceof MongoWriteConcernError ||
832
- isMaxTimeMSExpiredError(error)
833
- ) {
834
- if (isUnknownTransactionCommitResult(error)) {
835
- error.addErrorLabel(MongoErrorLabel.UnknownTransactionCommitResult);
836
-
837
- // per txns spec, must unpin session in this case
838
- session.unpin({ error });
839
- }
840
- } else if (error.hasErrorLabel(MongoErrorLabel.TransientTransactionError)) {
841
- session.unpin({ error });
842
- }
843
- }
844
-
845
- throw error;
846
- }
847
-
848
843
  /** @public */
849
844
  export type ServerSessionId = { id: Binary };
850
845