Skip to content

Commit 8c01d69

Browse files
nbbeekenW-A-James
authored andcommitted
feat(NODE-6313): add CSOT support to sessions and transactions (#4199)
1 parent 72ccc6a commit 8c01d69

File tree

2 files changed

+153
-1
lines changed

2 files changed

+153
-1
lines changed

src/cmap/wire_protocol/on_data.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,11 @@ export function onData(
9090
// Adding event handlers
9191
emitter.on('data', eventHandler);
9292
emitter.on('error', errorHandler);
93+
94+
const timeoutForSocketRead = timeoutContext?.timeoutForSocketRead;
95+
timeoutForSocketRead?.throwIfExpired();
9396
// eslint-disable-next-line github/no-then
94-
timeoutContext?.timeoutForSocketRead?.then(undefined, errorHandler);
97+
timeoutForSocketRead?.then(undefined, errorHandler);
9598

9699
const timeoutForSocketRead = timeoutContext?.timeoutForSocketRead;
97100
timeoutForSocketRead?.throwIfExpired();

test/integration/client-side-operations-timeout/node_csot.test.ts

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -894,4 +894,153 @@ describe('CSOT driver tests', metadata, () => {
894894
});
895895
});
896896
});
897+
898+
describe('when using an explicit session', () => {
899+
const metadata: MongoDBMetadataUI = {
900+
requires: { topology: ['replicaset'], mongodb: '>=4.4' }
901+
};
902+
903+
describe('created for a withTransaction callback', () => {
904+
describe('passing a timeoutMS and a session with a timeoutContext', () => {
905+
let client: MongoClient;
906+
907+
beforeEach(async function () {
908+
client = this.configuration.newClient({ timeoutMS: 123 });
909+
});
910+
911+
afterEach(async function () {
912+
await client.close();
913+
});
914+
915+
it('throws a validation error from the operation', metadata, async () => {
916+
// Drivers MUST raise a validation error if an explicit session with a timeout is used and
917+
// the timeoutMS option is set at the operation level for operations executed as part of a withTransaction callback.
918+
919+
const coll = client.db('db').collection('coll');
920+
921+
const session = client.startSession();
922+
923+
let insertError: Error | null = null;
924+
const withTransactionError = await session
925+
.withTransaction(async session => {
926+
insertError = await coll
927+
.insertOne({ x: 1 }, { session, timeoutMS: 1234 })
928+
.catch(error => error);
929+
throw insertError;
930+
})
931+
.catch(error => error);
932+
933+
expect(insertError).to.be.instanceOf(MongoInvalidArgumentError);
934+
expect(withTransactionError).to.be.instanceOf(MongoInvalidArgumentError);
935+
});
936+
});
937+
});
938+
939+
describe('created manually', () => {
940+
describe('passing a timeoutMS and a session with an inherited timeoutMS', () => {
941+
let client: MongoClient;
942+
943+
beforeEach(async function () {
944+
client = this.configuration.newClient({ timeoutMS: 123 });
945+
});
946+
947+
afterEach(async function () {
948+
await client.close();
949+
});
950+
951+
it('does not throw a validation error', metadata, async () => {
952+
const coll = client.db('db').collection('coll');
953+
const session = client.startSession();
954+
session.startTransaction();
955+
await coll.insertOne({ x: 1 }, { session, timeoutMS: 1234 });
956+
await session.abortTransaction(); // this uses the inherited timeoutMS, not the insert
957+
});
958+
});
959+
});
960+
});
961+
962+
describe('Convenient Transactions', () => {
963+
/** Tests in this section MUST only run against replica sets and sharded clusters with server versions 4.4 or higher. */
964+
const metadata: MongoDBMetadataUI = {
965+
requires: { topology: ['replicaset', 'sharded'], mongodb: '>=5.0' }
966+
};
967+
968+
describe('when an operation fails inside withTransaction callback', () => {
969+
const failpoint: FailPoint = {
970+
configureFailPoint: 'failCommand',
971+
mode: { times: 2 },
972+
data: {
973+
failCommands: ['insert', 'abortTransaction'],
974+
blockConnection: true,
975+
blockTimeMS: 600
976+
}
977+
};
978+
979+
beforeEach(async function () {
980+
if (!semver.satisfies(this.configuration.version, '>=4.4')) {
981+
this.skipReason = 'Requires server version 4.4+';
982+
this.skip();
983+
}
984+
const internalClient = this.configuration.newClient();
985+
await internalClient
986+
.db('db')
987+
.collection('coll')
988+
.drop()
989+
.catch(() => null);
990+
await internalClient.db('admin').command(failpoint);
991+
await internalClient.close();
992+
});
993+
994+
let client: MongoClient;
995+
996+
afterEach(async function () {
997+
if (semver.satisfies(this.configuration.version, '>=4.4')) {
998+
const internalClient = this.configuration.newClient();
999+
await internalClient
1000+
.db('admin')
1001+
.command({ configureFailPoint: 'failCommand', mode: 'off' });
1002+
await internalClient.close();
1003+
}
1004+
await client?.close();
1005+
});
1006+
1007+
it(
1008+
'timeoutMS is refreshed for abortTransaction and the timeout error is thrown from the operation',
1009+
metadata,
1010+
async function () {
1011+
const commandsFailed = [];
1012+
const commandsStarted = [];
1013+
1014+
client = this.configuration
1015+
.newClient({ timeoutMS: 500, monitorCommands: true })
1016+
.on('commandStarted', e => commandsStarted.push(e.commandName))
1017+
.on('commandFailed', e => commandsFailed.push(e.commandName));
1018+
1019+
const coll = client.db('db').collection('coll');
1020+
1021+
const session = client.startSession();
1022+
1023+
let insertError: Error | null = null;
1024+
const withTransactionError = await session
1025+
.withTransaction(async session => {
1026+
insertError = await coll.insertOne({ x: 1 }, { session }).catch(error => error);
1027+
throw insertError;
1028+
})
1029+
.catch(error => error);
1030+
1031+
try {
1032+
expect(insertError).to.be.instanceOf(MongoOperationTimeoutError);
1033+
expect(withTransactionError).to.be.instanceOf(MongoOperationTimeoutError);
1034+
expect(commandsStarted, 'commands started').to.deep.equal([
1035+
'insert',
1036+
'abortTransaction'
1037+
]);
1038+
expect(commandsFailed, 'commands failed').to.deep.equal(['insert', 'abortTransaction']);
1039+
} finally {
1040+
await session.endSession();
1041+
}
1042+
}
1043+
);
1044+
});
1045+
});
8971046
});

0 commit comments

Comments
 (0)