Skip to content

Commit 7aa3008

Browse files
authored
fix(NODE-3393): snapshot time not applied if distinct executed first (#2908)
1 parent b67af3c commit 7aa3008

File tree

3 files changed

+50
-111
lines changed

3 files changed

+50
-111
lines changed

src/sessions.ts

+7-6
Original file line numberDiff line numberDiff line change
@@ -891,11 +891,12 @@ export function updateSessionFromResponse(session: ClientSession, document: Docu
891891
session.transaction._recoveryToken = document.recoveryToken;
892892
}
893893

894-
if (
895-
document.cursor?.atClusterTime &&
896-
session?.[kSnapshotEnabled] &&
897-
session[kSnapshotTime] === undefined
898-
) {
899-
session[kSnapshotTime] = document.cursor.atClusterTime;
894+
if (session?.[kSnapshotEnabled] && session[kSnapshotTime] === undefined) {
895+
// find and aggregate commands return atClusterTime on the cursor
896+
// distinct includes it in the response body
897+
const atClusterTime = document.cursor?.atClusterTime || document.atClusterTime;
898+
if (atClusterTime) {
899+
session[kSnapshotTime] = atClusterTime;
900+
}
900901
}
901902
}

test/functional/sessions.test.js

+1-25
Original file line numberDiff line numberDiff line change
@@ -200,35 +200,11 @@ describe('Sessions - functional', function () {
200200
for (const sessionTests of loadSpecTests(path.join('sessions', 'unified'))) {
201201
expect(sessionTests).to.be.an('object');
202202
context(String(sessionTests.description), function () {
203-
// TODO: NODE-3393 fix test runner to apply session to all operations
204-
const skipTestMap = {
205-
'snapshot-sessions': [
206-
'countDocuments operation with snapshot',
207-
'Distinct operation with snapshot',
208-
'Mixed operation with snapshot'
209-
],
210-
'snapshot-sessions-not-supported-client-error': [
211-
'Client error on distinct with snapshot'
212-
],
213-
'snapshot-sessions-not-supported-server-error': [
214-
'Server returns an error on distinct with snapshot'
215-
],
216-
'snapshot-sessions-unsupported-ops': [
217-
'Server returns an error on listCollections with snapshot',
218-
'Server returns an error on listDatabases with snapshot',
219-
'Server returns an error on listIndexes with snapshot',
220-
'Server returns an error on runCommand with snapshot',
221-
'Server returns an error on findOneAndUpdate with snapshot',
222-
'Server returns an error on deleteOne with snapshot',
223-
'Server returns an error on updateOne with snapshot'
224-
]
225-
};
226-
const testsToSkip = skipTestMap[sessionTests.description] || [];
227203
for (const test of sessionTests.tests) {
228204
it(String(test.description), {
229205
metadata: { sessions: { skipLeakTests: true } },
230206
test: async function () {
231-
await runUnifiedTest(this, sessionTests, test, testsToSkip);
207+
await runUnifiedTest(this, sessionTests, test);
232208
}
233209
});
234210
}

test/functional/unified-spec-runner/operations.ts

+42-80
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,13 @@ import { Collection, Db, GridFSFile, MongoClient, ObjectId, AbstractCursor } fro
44
import { ReadConcern } from '../../../src/read_concern';
55
import { ReadPreference } from '../../../src/read_preference';
66
import { WriteConcern } from '../../../src/write_concern';
7-
import { Document, InsertOneOptions } from '../../../src';
7+
import { Document } from '../../../src';
88
import { EventCollector } from '../../tools/utils';
99
import { EntitiesMap } from './entities';
1010
import { expectErrorCheck, resultCheck } from './match';
1111
import type { OperationDescription } from './schema';
1212
import { CommandStartedEvent } from '../../../src/cmap/command_monitoring_events';
1313
import { translateOptions } from './unified-utils';
14-
import { getSymbolFrom } from '../../tools/utils';
1514

1615
interface OperationFunctionParams {
1716
client: MongoClient;
@@ -22,18 +21,6 @@ interface OperationFunctionParams {
2221
type RunOperationFn = (p: OperationFunctionParams) => Promise<Document | boolean | number | void>;
2322
export const operations = new Map<string, RunOperationFn>();
2423

25-
function executeWithPotentialSession(
26-
entities: EntitiesMap,
27-
operation: OperationDescription,
28-
cursor: AbstractCursor
29-
) {
30-
const session = entities.getEntity('session', operation.arguments.session, false);
31-
if (session) {
32-
cursor.session = session;
33-
}
34-
return cursor.toArray();
35-
}
36-
3724
operations.set('abortTransaction', async ({ entities, operation }) => {
3825
const session = entities.getEntity('session', operation.object);
3926
return session.abortTransaction();
@@ -44,18 +31,9 @@ operations.set('aggregate', async ({ entities, operation }) => {
4431
if (!(dbOrCollection instanceof Db || dbOrCollection instanceof Collection)) {
4532
throw new Error(`Operation object '${operation.object}' must be a db or collection`);
4633
}
47-
const cursor = dbOrCollection.aggregate(operation.arguments.pipeline, {
48-
allowDiskUse: operation.arguments.allowDiskUse,
49-
batchSize: operation.arguments.batchSize,
50-
bypassDocumentValidation: operation.arguments.bypassDocumentValidation,
51-
maxTimeMS: operation.arguments.maxTimeMS,
52-
maxAwaitTimeMS: operation.arguments.maxAwaitTimeMS,
53-
collation: operation.arguments.collation,
54-
hint: operation.arguments.hint,
55-
let: operation.arguments.let,
56-
out: operation.arguments.out
57-
});
58-
return executeWithPotentialSession(entities, operation, cursor);
34+
const { pipeline, ...opts } = operation.arguments;
35+
const cursor = dbOrCollection.aggregate(pipeline, opts);
36+
return cursor.toArray();
5937
});
6038

6139
operations.set('assertCollectionExists', async ({ operation, client }) => {
@@ -139,27 +117,27 @@ operations.set('assertSameLsidOnLastTwoCommands', async ({ entities, operation }
139117
});
140118

141119
operations.set('assertSessionDirty', async ({ entities, operation }) => {
142-
const session = entities.getEntity('session', operation.arguments.session);
120+
const session = operation.arguments.session;
143121
expect(session.serverSession.isDirty).to.be.true;
144122
});
145123

146124
operations.set('assertSessionNotDirty', async ({ entities, operation }) => {
147-
const session = entities.getEntity('session', operation.arguments.session);
125+
const session = operation.arguments.session;
148126
expect(session.serverSession.isDirty).to.be.false;
149127
});
150128

151129
operations.set('assertSessionPinned', async ({ entities, operation }) => {
152-
const session = entities.getEntity('session', operation.arguments.session);
130+
const session = operation.arguments.session;
153131
expect(session.transaction.isPinned).to.be.true;
154132
});
155133

156134
operations.set('assertSessionUnpinned', async ({ entities, operation }) => {
157-
const session = entities.getEntity('session', operation.arguments.session);
135+
const session = operation.arguments.session;
158136
expect(session.transaction.isPinned).to.be.false;
159137
});
160138

161139
operations.set('assertSessionTransactionState', async ({ entities, operation }) => {
162-
const session = entities.getEntity('session', operation.arguments.session);
140+
const session = operation.arguments.session;
163141

164142
const transactionStateTranslation = {
165143
none: 'NO_TRANSACTION',
@@ -236,17 +214,14 @@ operations.set('createChangeStream', async ({ entities, operation }) => {
236214

237215
operations.set('createCollection', async ({ entities, operation }) => {
238216
const db = entities.getEntity('db', operation.object);
239-
const { session, collection, ...opts } = operation.arguments;
240-
await db.createCollection(collection, {
241-
session: entities.getEntity('session', session, false),
242-
...opts
243-
});
217+
const { collection, ...opts } = operation.arguments;
218+
await db.createCollection(collection, opts);
244219
});
245220

246221
operations.set('createFindCursor', async ({ entities, operation }) => {
247222
const collection = entities.getEntity('collection', operation.object);
248-
const { filter, sort, batchSize, limit, let: vars } = operation.arguments;
249-
const cursor = collection.find(filter, { sort, batchSize, limit, let: vars });
223+
const { filter, ...opts } = operation.arguments;
224+
const cursor = collection.find(filter, opts);
250225
// The spec dictates that we create the cursor and force the find command
251226
// to execute, but don't move the cursor forward. hasNext() accomplishes
252227
// this.
@@ -256,11 +231,8 @@ operations.set('createFindCursor', async ({ entities, operation }) => {
256231

257232
operations.set('createIndex', async ({ entities, operation }) => {
258233
const collection = entities.getEntity('collection', operation.object);
259-
const session = entities.getEntity('session', operation.arguments.session, false);
260-
await collection.createIndex(operation.arguments.keys, {
261-
session,
262-
name: operation.arguments.name
263-
});
234+
const { keys, ...opts } = operation.arguments;
235+
await collection.createIndex(keys, opts);
264236
});
265237

266238
operations.set('deleteOne', async ({ entities, operation }) => {
@@ -271,7 +243,8 @@ operations.set('deleteOne', async ({ entities, operation }) => {
271243

272244
operations.set('dropCollection', async ({ entities, operation }) => {
273245
const db = entities.getEntity('db', operation.object);
274-
return await db.dropCollection(operation.arguments.collection);
246+
const { collection, ...opts } = operation.arguments;
247+
return await db.dropCollection(collection, opts);
275248
});
276249

277250
operations.set('endSession', async ({ entities, operation }) => {
@@ -281,9 +254,8 @@ operations.set('endSession', async ({ entities, operation }) => {
281254

282255
operations.set('find', async ({ entities, operation }) => {
283256
const collection = entities.getEntity('collection', operation.object);
284-
const { filter, sort, batchSize, limit, let: vars } = operation.arguments;
285-
const cursor = collection.find(filter, { sort, batchSize, limit, let: vars });
286-
return executeWithPotentialSession(entities, operation, cursor);
257+
const { filter, ...opts } = operation.arguments;
258+
return collection.find(filter, opts).toArray();
287259
});
288260

289261
operations.set('findOneAndReplace', async ({ entities, operation }) => {
@@ -305,27 +277,14 @@ operations.set('failPoint', async ({ entities, operation }) => {
305277

306278
operations.set('insertOne', async ({ entities, operation }) => {
307279
const collection = entities.getEntity('collection', operation.object);
308-
309-
const session = entities.getEntity('session', operation.arguments.session, false);
310-
311-
const options = {
312-
session
313-
} as InsertOneOptions;
314-
315-
return collection.insertOne(operation.arguments.document, options);
280+
const { document, ...opts } = operation.arguments;
281+
return collection.insertOne(document, opts);
316282
});
317283

318284
operations.set('insertMany', async ({ entities, operation }) => {
319285
const collection = entities.getEntity('collection', operation.object);
320-
321-
const session = entities.getEntity('session', operation.arguments.session, false);
322-
323-
const options = {
324-
session,
325-
ordered: operation.arguments.ordered ?? true
326-
};
327-
328-
return collection.insertMany(operation.arguments.documents, options);
286+
const { documents, ...opts } = operation.arguments;
287+
return collection.insertMany(documents, opts);
329288
});
330289

331290
operations.set('iterateUntilDocumentOrError', async ({ entities, operation }) => {
@@ -350,7 +309,7 @@ operations.set('listCollections', async ({ entities, operation }) => {
350309

351310
operations.set('listDatabases', async ({ entities, operation }) => {
352311
const client = entities.getEntity('client', operation.object);
353-
return client.db().admin().listDatabases();
312+
return client.db().admin().listDatabases(operation.arguments);
354313
});
355314

356315
operations.set('listIndexes', async ({ entities, operation }) => {
@@ -360,12 +319,8 @@ operations.set('listIndexes', async ({ entities, operation }) => {
360319

361320
operations.set('replaceOne', async ({ entities, operation }) => {
362321
const collection = entities.getEntity('collection', operation.object);
363-
return collection.replaceOne(operation.arguments.filter, operation.arguments.replacement, {
364-
bypassDocumentValidation: operation.arguments.bypassDocumentValidation,
365-
collation: operation.arguments.collation,
366-
hint: operation.arguments.hint,
367-
upsert: operation.arguments.upsert
368-
});
322+
const { filter, replacement, ...opts } = operation.arguments;
323+
return collection.replaceOne(filter, replacement, opts);
369324
});
370325

371326
operations.set('startTransaction', async ({ entities, operation }) => {
@@ -374,7 +329,7 @@ operations.set('startTransaction', async ({ entities, operation }) => {
374329
});
375330

376331
operations.set('targetedFailPoint', async ({ entities, operation }) => {
377-
const session = entities.getEntity('session', operation.arguments.session);
332+
const session = operation.arguments.session;
378333
expect(session.transaction.isPinned, 'Session must be pinned for a targetedFailPoint').to.be.true;
379334
await entities.failPoints.enableFailPoint(
380335
session.transaction._pinnedServer.s.description.hostAddress,
@@ -433,20 +388,20 @@ operations.set('withTransaction', async ({ entities, operation, client }) => {
433388

434389
operations.set('countDocuments', async ({ entities, operation }) => {
435390
const collection = entities.getEntity('collection', operation.object);
436-
return collection.countDocuments(operation.arguments.filter as Document);
391+
const { filter, ...opts } = operation.arguments;
392+
return collection.countDocuments(filter, opts);
437393
});
438394

439395
operations.set('deleteMany', async ({ entities, operation }) => {
440396
const collection = entities.getEntity('collection', operation.object);
441-
return collection.deleteMany(operation.arguments.filter);
397+
const { filter, ...opts } = operation.arguments;
398+
return collection.deleteMany(filter, opts);
442399
});
443400

444401
operations.set('distinct', async ({ entities, operation }) => {
445402
const collection = entities.getEntity('collection', operation.object);
446-
return collection.distinct(
447-
operation.arguments.fieldName as string,
448-
operation.arguments.filter as Document
449-
);
403+
const { fieldName, filter, ...opts } = operation.arguments;
404+
return collection.distinct(fieldName, filter, opts);
450405
});
451406

452407
operations.set('estimatedDocumentCount', async ({ entities, operation }) => {
@@ -456,12 +411,14 @@ operations.set('estimatedDocumentCount', async ({ entities, operation }) => {
456411

457412
operations.set('findOneAndDelete', async ({ entities, operation }) => {
458413
const collection = entities.getEntity('collection', operation.object);
459-
return collection.findOneAndDelete(operation.arguments.filter);
414+
const { filter, ...opts } = operation.arguments;
415+
return collection.findOneAndDelete(filter, opts);
460416
});
461417

462418
operations.set('runCommand', async ({ entities, operation }: OperationFunctionParams) => {
463419
const db = entities.getEntity('db', operation.object);
464-
return db.command(operation.arguments.command);
420+
const { command, ...opts } = operation.arguments;
421+
return db.command(command, opts);
465422
});
466423

467424
operations.set('updateMany', async ({ entities, operation }) => {
@@ -484,6 +441,11 @@ export async function executeOperationAndCheck(
484441
const opFunc = operations.get(operation.name);
485442
expect(opFunc, `Unknown operation: ${operation.name}`).to.exist;
486443

444+
if (operation.arguments?.session) {
445+
const session = entities.getEntity('session', operation.arguments.session, false);
446+
operation.arguments.session = session;
447+
}
448+
487449
let result;
488450

489451
try {

0 commit comments

Comments
 (0)