mongodb 6.8.0-dev.20240813.sha.b70c8850 → 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.
- package/lib/beta.d.ts +7 -2
- package/lib/client-side-encryption/client_encryption.js.map +1 -1
- package/lib/cmap/commands.js +62 -5
- package/lib/cmap/commands.js.map +1 -1
- package/lib/sessions.js +207 -213
- package/lib/sessions.js.map +1 -1
- package/mongodb.d.ts +7 -2
- package/package.json +2 -2
- package/src/client-side-encryption/client_encryption.ts +17 -3
- package/src/cmap/commands.ts +65 -4
- package/src/sessions.ts +270 -275
package/src/cmap/commands.ts
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
494
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
514
|
-
|
|
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
|
-
|
|
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
|
|