From 7acdf80647f52bc5d20acecdde2eed97a96781b9 Mon Sep 17 00:00:00 2001 From: Daria Pardue <81593090+dariakp@users.noreply.github.com> Date: Wed, 7 Jul 2021 15:33:15 -0400 Subject: [PATCH 01/16] chore: move non-unified sessions tests to legacy folder; fix readme --- test/functional/sessions.test.js | 3 ++- test/spec/read-write-concern/README.rst | 6 +++--- test/spec/sessions/{ => legacy}/dirty-session-errors.json | 0 test/spec/sessions/{ => legacy}/dirty-session-errors.yml | 0 4 files changed, 5 insertions(+), 4 deletions(-) rename test/spec/sessions/{ => legacy}/dirty-session-errors.json (100%) rename test/spec/sessions/{ => legacy}/dirty-session-errors.yml (100%) diff --git a/test/functional/sessions.test.js b/test/functional/sessions.test.js index f6226cd118..cc2f3158ee 100644 --- a/test/functional/sessions.test.js +++ b/test/functional/sessions.test.js @@ -176,7 +176,7 @@ describe('Sessions - functional', function () { } const testContext = new SessionSpecTestContext(); - const testSuites = loadSpecTests('sessions'); + const testSuites = loadSpecTests('sessions/legacy'); after(() => testContext.teardown()); before(function () { @@ -186,6 +186,7 @@ describe('Sessions - functional', function () { function testFilter(spec) { const SKIP_TESTS = [ // These two tests need to run against multiple mongoses + // TODO: maybe create ticket? 'Dirty explicit session is discarded', 'Dirty implicit session is discarded (write)' ]; diff --git a/test/spec/read-write-concern/README.rst b/test/spec/read-write-concern/README.rst index 5995590136..2f2b84dc9c 100644 --- a/test/spec/read-write-concern/README.rst +++ b/test/spec/read-write-concern/README.rst @@ -1,6 +1,6 @@ -======================= -Connection String Tests -======================= +============================ +Read and Write Concern Tests +============================ The YAML and JSON files in this directory tree are platform-independent tests that drivers can use to prove their conformance to the Read and Write Concern diff --git a/test/spec/sessions/dirty-session-errors.json b/test/spec/sessions/legacy/dirty-session-errors.json similarity index 100% rename from test/spec/sessions/dirty-session-errors.json rename to test/spec/sessions/legacy/dirty-session-errors.json diff --git a/test/spec/sessions/dirty-session-errors.yml b/test/spec/sessions/legacy/dirty-session-errors.yml similarity index 100% rename from test/spec/sessions/dirty-session-errors.yml rename to test/spec/sessions/legacy/dirty-session-errors.yml From 6e8a655602fe5c6fefbdae63ca7e661cdd7f15d0 Mon Sep 17 00:00:00 2001 From: Daria Pardue <81593090+dariakp@users.noreply.github.com> Date: Wed, 7 Jul 2021 15:51:40 -0400 Subject: [PATCH 02/16] test: Add the base snapshot reads tests --- test/functional/sessions.test.js | 30 +- ...t-sessions-not-supported-server-error.json | 111 +++ ...ot-sessions-not-supported-server-error.yml | 58 ++ .../sessions/unified/snapshot-sessions.json | 939 ++++++++++++++++++ .../sessions/unified/snapshot-sessions.yml | 452 +++++++++ 5 files changed, 1583 insertions(+), 7 deletions(-) create mode 100644 test/spec/sessions/unified/snapshot-sessions-not-supported-server-error.json create mode 100644 test/spec/sessions/unified/snapshot-sessions-not-supported-server-error.yml create mode 100644 test/spec/sessions/unified/snapshot-sessions.json create mode 100644 test/spec/sessions/unified/snapshot-sessions.yml diff --git a/test/functional/sessions.test.js b/test/functional/sessions.test.js index cc2f3158ee..eca6130fca 100644 --- a/test/functional/sessions.test.js +++ b/test/functional/sessions.test.js @@ -1,11 +1,11 @@ 'use strict'; +const path = require('path'); const expect = require('chai').expect; -const setupDatabase = require('./shared').setupDatabase; -const withMonitoredClient = require('./shared').withMonitoredClient; -const TestRunnerContext = require('./spec-runner').TestRunnerContext; -const generateTopologyTests = require('./spec-runner').generateTopologyTests; -const loadSpecTests = require('../spec').loadSpecTests; +const { setupDatabase, withMonitoredClient } = require('./shared'); +const { TestRunnerContext, generateTopologyTests } = require('./spec-runner'); +const { loadSpecTests } = require('../spec'); +const { runUnifiedTest } = require('./unified-spec-runner/runner'); const ignoredCommands = ['ismaster']; const test = { @@ -148,7 +148,7 @@ describe('Sessions - functional', function () { } }); - describe('spec tests', function () { + describe('legacy spec tests', function () { class SessionSpecTestContext extends TestRunnerContext { assertSessionNotDirty(options) { const session = options.session; @@ -176,7 +176,7 @@ describe('Sessions - functional', function () { } const testContext = new SessionSpecTestContext(); - const testSuites = loadSpecTests('sessions/legacy'); + const testSuites = loadSpecTests(path.join('sessions', 'legacy')); after(() => testContext.teardown()); before(function () { @@ -197,6 +197,22 @@ describe('Sessions - functional', function () { generateTopologyTests(testSuites, testContext, testFilter); }); + describe('unified spec tests', function () { + for (const sessionTests of loadSpecTests(path.join('sessions', 'unified'))) { + expect(sessionTests).to.be.an('object'); + context(String(sessionTests.description), function () { + for (const test of sessionTests.tests) { + it(String(test.description), { + metadata: { sessions: { skipLeakTests: true } }, + test: async function () { + await runUnifiedTest(this, sessionTests, test); + } + }); + } + }); + } + }); + context('unacknowledged writes', () => { it('should not include session for unacknowledged writes', { metadata: { requires: { topology: 'single', mongodb: '>=3.6.0' } }, diff --git a/test/spec/sessions/unified/snapshot-sessions-not-supported-server-error.json b/test/spec/sessions/unified/snapshot-sessions-not-supported-server-error.json new file mode 100644 index 0000000000..b6ce00216a --- /dev/null +++ b/test/spec/sessions/unified/snapshot-sessions-not-supported-server-error.json @@ -0,0 +1,111 @@ +{ + "description": "snapshot-sessions-not-supported-server-error", + "schemaVersion": "1.0", + "runOnRequirements": [ + { + "minServerVersion": "3.6", + "maxServerVersion": "4.4.99" + }, + { + "minServerVersion": "3.6", + "topologies": [ + "single" + ] + } + ], + "createEntities": [ + { + "client": { + "id": "client0", + "observeEvents": [ + "commandStartedEvent", + "commandFailedEvent" + ], + "ignoreCommandMonitoringEvents": [ + "findAndModify", + "insert", + "update" + ] + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "database0" + } + }, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "collection0" + } + }, + { + "session": { + "id": "session0", + "client": "client0", + "sessionOptions": { + "snapshot": true + } + } + } + ], + "initialData": [ + { + "collectionName": "collection0", + "databaseName": "database0", + "documents": [ + { + "_id": 1, + "x": 11 + } + ] + } + ], + "tests": [ + { + "description": "Server returns an error on find with snapshot", + "operations": [ + { + "name": "find", + "object": "collection0", + "arguments": { + "session": "session0", + "filter": {} + }, + "expectError": { + "isError": true, + "isClientError": false + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "find": "collection0", + "readConcern": { + "level": "snapshot", + "atClusterTime": { + "$$exists": false + } + } + } + } + }, + { + "commandFailedEvent": { + "commandName": "find" + } + } + ] + } + ] + } + ] +} diff --git a/test/spec/sessions/unified/snapshot-sessions-not-supported-server-error.yml b/test/spec/sessions/unified/snapshot-sessions-not-supported-server-error.yml new file mode 100644 index 0000000000..f12efeb9df --- /dev/null +++ b/test/spec/sessions/unified/snapshot-sessions-not-supported-server-error.yml @@ -0,0 +1,58 @@ +description: snapshot-sessions-not-supported-server-error + +schemaVersion: "1.0" + +runOnRequirements: + - minServerVersion: "3.6" + maxServerVersion: "4.4.99" + - minServerVersion: "3.6" + topologies: [ single ] + +createEntities: + - client: + id: &client0 client0 + observeEvents: [ commandStartedEvent, commandFailedEvent ] + ignoreCommandMonitoringEvents: [ findAndModify, insert, update ] + - database: + id: &database0 database0 + client: *client0 + databaseName: &database0Name database0 + - collection: + id: &collection0 collection0 + database: *database0 + collectionName: &collection0Name collection0 + - session: + id: session0 + client: client0 + sessionOptions: + snapshot: true + +initialData: + - collectionName: *collection0Name + databaseName: *database0Name + documents: + - { _id: 1, x: 11 } + +tests: +- description: Server returns an error on find with snapshot + operations: + - name: find + object: collection0 + arguments: + session: session0 + filter: {} + expectError: + isError: true + isClientError: false + expectEvents: + - client: client0 + events: + - commandStartedEvent: + command: + find: collection0 + readConcern: + level: snapshot + atClusterTime: + "$$exists": false + - commandFailedEvent: + commandName: find diff --git a/test/spec/sessions/unified/snapshot-sessions.json b/test/spec/sessions/unified/snapshot-sessions.json new file mode 100644 index 0000000000..4170a96699 --- /dev/null +++ b/test/spec/sessions/unified/snapshot-sessions.json @@ -0,0 +1,939 @@ +{ + "description": "snapshot-sessions", + "schemaVersion": "1.0", + "runOnRequirements": [ + { + "minServerVersion": "5.0", + "topologies": [ + "replicaset", + "sharded-replicaset" + ] + } + ], + "createEntities": [ + { + "client": { + "id": "client0", + "observeEvents": [ + "commandStartedEvent" + ], + "ignoreCommandMonitoringEvents": [ + "findAndModify", + "insert", + "update" + ] + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "database0" + } + }, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "collection0", + "collectionOptions": { + "writeConcern": { + "w": "majority" + } + } + } + }, + { + "session": { + "id": "session0", + "client": "client0", + "sessionOptions": { + "snapshot": true + } + } + }, + { + "session": { + "id": "session1", + "client": "client0", + "sessionOptions": { + "snapshot": true + } + } + } + ], + "initialData": [ + { + "collectionName": "collection0", + "databaseName": "database0", + "documents": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 11 + } + ] + } + ], + "tests": [ + { + "description": "Find operation with snapshot", + "operations": [ + { + "name": "find", + "object": "collection0", + "arguments": { + "session": "session0", + "filter": { + "_id": 1 + } + }, + "expectResult": [ + { + "_id": 1, + "x": 11 + } + ] + }, + { + "name": "findOneAndUpdate", + "object": "collection0", + "arguments": { + "filter": { + "_id": 1 + }, + "update": { + "$inc": { + "x": 1 + } + }, + "returnDocument": "After" + }, + "expectResult": { + "_id": 1, + "x": 12 + } + }, + { + "name": "find", + "object": "collection0", + "arguments": { + "session": "session1", + "filter": { + "_id": 1 + } + }, + "expectResult": [ + { + "_id": 1, + "x": 12 + } + ] + }, + { + "name": "findOneAndUpdate", + "object": "collection0", + "arguments": { + "filter": { + "_id": 1 + }, + "update": { + "$inc": { + "x": 1 + } + }, + "returnDocument": "After" + }, + "expectResult": { + "_id": 1, + "x": 13 + } + }, + { + "name": "find", + "object": "collection0", + "arguments": { + "filter": { + "_id": 1 + } + }, + "expectResult": [ + { + "_id": 1, + "x": 13 + } + ] + }, + { + "name": "find", + "object": "collection0", + "arguments": { + "session": "session0", + "filter": { + "_id": 1 + } + }, + "expectResult": [ + { + "_id": 1, + "x": 11 + } + ] + }, + { + "name": "find", + "object": "collection0", + "arguments": { + "session": "session1", + "filter": { + "_id": 1 + } + }, + "expectResult": [ + { + "_id": 1, + "x": 12 + } + ] + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "find": "collection0", + "readConcern": { + "level": "snapshot", + "atClusterTime": { + "$$exists": false + } + } + } + } + }, + { + "commandStartedEvent": { + "command": { + "find": "collection0", + "readConcern": { + "level": "snapshot", + "atClusterTime": { + "$$exists": false + } + } + } + } + }, + { + "commandStartedEvent": { + "command": { + "find": "collection0", + "readConcern": { + "$$exists": false + } + } + } + }, + { + "commandStartedEvent": { + "command": { + "find": "collection0", + "readConcern": { + "level": "snapshot", + "atClusterTime": { + "$$exists": true + } + } + } + } + }, + { + "commandStartedEvent": { + "command": { + "find": "collection0", + "readConcern": { + "level": "snapshot", + "atClusterTime": { + "$$exists": true + } + } + } + } + } + ] + } + ] + }, + { + "description": "Distinct operation with snapshot", + "operations": [ + { + "name": "distinct", + "object": "collection0", + "arguments": { + "fieldName": "x", + "filter": {}, + "session": "session0" + }, + "expectResult": [ + 11 + ] + }, + { + "name": "findOneAndUpdate", + "object": "collection0", + "arguments": { + "filter": { + "_id": 2 + }, + "update": { + "$inc": { + "x": 1 + } + }, + "returnDocument": "After" + }, + "expectResult": { + "_id": 2, + "x": 12 + } + }, + { + "name": "distinct", + "object": "collection0", + "arguments": { + "fieldName": "x", + "filter": {}, + "session": "session1" + }, + "expectResult": [ + 11, + 12 + ] + }, + { + "name": "findOneAndUpdate", + "object": "collection0", + "arguments": { + "filter": { + "_id": 2 + }, + "update": { + "$inc": { + "x": 1 + } + }, + "returnDocument": "After" + }, + "expectResult": { + "_id": 2, + "x": 13 + } + }, + { + "name": "distinct", + "object": "collection0", + "arguments": { + "fieldName": "x", + "filter": {} + }, + "expectResult": [ + 11, + 13 + ] + }, + { + "name": "distinct", + "object": "collection0", + "arguments": { + "fieldName": "x", + "filter": {}, + "session": "session0" + }, + "expectResult": [ + 11 + ] + }, + { + "name": "distinct", + "object": "collection0", + "arguments": { + "fieldName": "x", + "filter": {}, + "session": "session1" + }, + "expectResult": [ + 11, + 12 + ] + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "distinct": "collection0", + "readConcern": { + "level": "snapshot", + "atClusterTime": { + "$$exists": false + } + } + } + } + }, + { + "commandStartedEvent": { + "command": { + "distinct": "collection0", + "readConcern": { + "level": "snapshot", + "atClusterTime": { + "$$exists": false + } + } + } + } + }, + { + "commandStartedEvent": { + "command": { + "distinct": "collection0", + "readConcern": { + "$$exists": false + } + } + } + }, + { + "commandStartedEvent": { + "command": { + "distinct": "collection0", + "readConcern": { + "level": "snapshot", + "atClusterTime": { + "$$exists": true + } + } + } + } + }, + { + "commandStartedEvent": { + "command": { + "distinct": "collection0", + "readConcern": { + "level": "snapshot", + "atClusterTime": { + "$$exists": true + } + } + } + } + } + ] + } + ] + }, + { + "description": "Aggregate operation with snapshot", + "operations": [ + { + "name": "aggregate", + "object": "collection0", + "arguments": { + "pipeline": [ + { + "$match": { + "_id": 1 + } + } + ], + "session": "session0" + }, + "expectResult": [ + { + "_id": 1, + "x": 11 + } + ] + }, + { + "name": "findOneAndUpdate", + "object": "collection0", + "arguments": { + "filter": { + "_id": 1 + }, + "update": { + "$inc": { + "x": 1 + } + }, + "returnDocument": "After" + }, + "expectResult": { + "_id": 1, + "x": 12 + } + }, + { + "name": "aggregate", + "object": "collection0", + "arguments": { + "pipeline": [ + { + "$match": { + "_id": 1 + } + } + ], + "session": "session1" + }, + "expectResult": [ + { + "_id": 1, + "x": 12 + } + ] + }, + { + "name": "findOneAndUpdate", + "object": "collection0", + "arguments": { + "filter": { + "_id": 1 + }, + "update": { + "$inc": { + "x": 1 + } + }, + "returnDocument": "After" + }, + "expectResult": { + "_id": 1, + "x": 13 + } + }, + { + "name": "aggregate", + "object": "collection0", + "arguments": { + "pipeline": [ + { + "$match": { + "_id": 1 + } + } + ] + }, + "expectResult": [ + { + "_id": 1, + "x": 13 + } + ] + }, + { + "name": "aggregate", + "object": "collection0", + "arguments": { + "pipeline": [ + { + "$match": { + "_id": 1 + } + } + ], + "session": "session0" + }, + "expectResult": [ + { + "_id": 1, + "x": 11 + } + ] + }, + { + "name": "aggregate", + "object": "collection0", + "arguments": { + "pipeline": [ + { + "$match": { + "_id": 1 + } + } + ], + "session": "session1" + }, + "expectResult": [ + { + "_id": 1, + "x": 12 + } + ] + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "aggregate": "collection0", + "readConcern": { + "level": "snapshot", + "atClusterTime": { + "$$exists": false + } + } + } + } + }, + { + "commandStartedEvent": { + "command": { + "aggregate": "collection0", + "readConcern": { + "level": "snapshot", + "atClusterTime": { + "$$exists": false + } + } + } + } + }, + { + "commandStartedEvent": { + "command": { + "aggregate": "collection0", + "readConcern": { + "$$exists": false + } + } + } + }, + { + "commandStartedEvent": { + "command": { + "aggregate": "collection0", + "readConcern": { + "level": "snapshot", + "atClusterTime": { + "$$exists": true + } + } + } + } + }, + { + "commandStartedEvent": { + "command": { + "aggregate": "collection0", + "readConcern": { + "level": "snapshot", + "atClusterTime": { + "$$exists": true + } + } + } + } + } + ] + } + ] + }, + { + "description": "Mixed operation with snapshot", + "operations": [ + { + "name": "find", + "object": "collection0", + "arguments": { + "session": "session0", + "filter": { + "_id": 1 + } + }, + "expectResult": [ + { + "_id": 1, + "x": 11 + } + ] + }, + { + "name": "findOneAndUpdate", + "object": "collection0", + "arguments": { + "filter": { + "_id": 1 + }, + "update": { + "$inc": { + "x": 1 + } + }, + "returnDocument": "After" + }, + "expectResult": { + "_id": 1, + "x": 12 + } + }, + { + "name": "find", + "object": "collection0", + "arguments": { + "filter": { + "_id": 1 + } + }, + "expectResult": [ + { + "_id": 1, + "x": 12 + } + ] + }, + { + "name": "aggregate", + "object": "collection0", + "arguments": { + "pipeline": [ + { + "$match": { + "_id": 1 + } + } + ], + "session": "session0" + }, + "expectResult": [ + { + "_id": 1, + "x": 11 + } + ] + }, + { + "name": "distinct", + "object": "collection0", + "arguments": { + "fieldName": "x", + "filter": {}, + "session": "session0" + }, + "expectResult": [ + 11 + ] + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "find": "collection0", + "readConcern": { + "level": "snapshot", + "atClusterTime": { + "$$exists": false + } + } + } + } + }, + { + "commandStartedEvent": { + "command": { + "find": "collection0", + "readConcern": { + "$$exists": false + } + } + } + }, + { + "commandStartedEvent": { + "command": { + "aggregate": "collection0", + "readConcern": { + "level": "snapshot", + "atClusterTime": { + "$$exists": true + } + } + } + } + }, + { + "commandStartedEvent": { + "command": { + "distinct": "collection0", + "readConcern": { + "level": "snapshot", + "atClusterTime": { + "$$exists": true + } + } + } + } + } + ] + } + ] + }, + { + "description": "Write commands with snapshot session do not affect snapshot reads", + "operations": [ + { + "name": "find", + "object": "collection0", + "arguments": { + "filter": {}, + "session": "session0" + } + }, + { + "name": "insertOne", + "object": "collection0", + "arguments": { + "session": "session0", + "document": { + "_id": 22, + "x": 33 + } + } + }, + { + "name": "updateOne", + "object": "collection0", + "arguments": { + "filter": { + "_id": 1 + }, + "session": "session0", + "update": { + "$inc": { + "x": 1 + } + } + } + }, + { + "name": "find", + "object": "collection0", + "arguments": { + "filter": { + "_id": 1 + }, + "session": "session0" + }, + "expectResult": [ + { + "_id": 1, + "x": 11 + } + ] + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "find": "collection0", + "readConcern": { + "level": "snapshot", + "atClusterTime": { + "$$exists": false + } + } + } + } + }, + { + "commandStartedEvent": { + "command": { + "find": "collection0", + "readConcern": { + "level": "snapshot", + "atClusterTime": { + "$$exists": true + } + } + } + } + } + ] + } + ] + }, + { + "description": "First snapshot read does not send atClusterTime", + "operations": [ + { + "name": "find", + "object": "collection0", + "arguments": { + "filter": {}, + "session": "session0" + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "find": "collection0", + "readConcern": { + "level": "snapshot", + "atClusterTime": { + "$$exists": false + } + } + }, + "commandName": "find", + "databaseName": "database0" + } + } + ] + } + ] + }, + { + "description": "StartTransaction fails in snapshot session", + "operations": [ + { + "name": "startTransaction", + "object": "session0", + "expectError": { + "isError": true, + "isClientError": true, + "errorContains": "Transactions are not supported in snapshot sessions" + } + } + ] + } + ] +} diff --git a/test/spec/sessions/unified/snapshot-sessions.yml b/test/spec/sessions/unified/snapshot-sessions.yml new file mode 100644 index 0000000000..9beda1ac0e --- /dev/null +++ b/test/spec/sessions/unified/snapshot-sessions.yml @@ -0,0 +1,452 @@ +description: snapshot-sessions + +schemaVersion: "1.0" + +runOnRequirements: + - minServerVersion: "5.0" + topologies: [replicaset, sharded-replicaset] + +createEntities: + - client: + id: &client0 client0 + observeEvents: [ commandStartedEvent] + ignoreCommandMonitoringEvents: [ findAndModify, insert, update ] + - database: + id: &database0 database0 + client: *client0 + databaseName: &database0Name database0 + - collection: + id: &collection0 collection0 + database: *database0 + collectionName: &collection0Name collection0 + collectionOptions: + writeConcern: { w: majority } + - session: + id: session0 + client: client0 + sessionOptions: + snapshot: true + - session: + id: session1 + client: client0 + sessionOptions: + snapshot: true + +initialData: + - collectionName: *collection0Name + databaseName: *database0Name + documents: + - { _id: 1, x: 11 } + - { _id: 2, x: 11 } + +tests: +- description: Find operation with snapshot + operations: + - name: find + object: collection0 + arguments: + session: session0 + filter: { _id: 1 } + expectResult: + - {_id: 1, x: 11} + - name: findOneAndUpdate + object: collection0 + arguments: + filter: { _id: 1 } + update: { $inc: { x: 1 } } + returnDocument: After + expectResult: { _id: 1, x: 12 } + - name: find + object: collection0 + arguments: + session: session1 + filter: { _id: 1 } + expectResult: + - { _id: 1, x: 12 } + - name: findOneAndUpdate + object: collection0 + arguments: + filter: { _id: 1 } + update: { $inc: { x: 1 } } + returnDocument: After + expectResult: { _id: 1, x: 13 } + - name: find + object: collection0 + arguments: + filter: { _id: 1 } + expectResult: + - { _id: 1, x: 13 } + - name: find + object: collection0 + arguments: + session: session0 + filter: { _id: 1 } + expectResult: + - {_id: 1, x: 11} + - name: find + object: collection0 + arguments: + session: session1 + filter: { _id: 1 } + expectResult: + - {_id: 1, x: 12} + expectEvents: + - client: client0 + events: + - commandStartedEvent: + command: + find: collection0 + readConcern: + level: snapshot + atClusterTime: + "$$exists": false + - commandStartedEvent: + command: + find: collection0 + readConcern: + level: snapshot + atClusterTime: + "$$exists": false + - commandStartedEvent: + command: + find: collection0 + readConcern: + "$$exists": false + - commandStartedEvent: + command: + find: collection0 + readConcern: + level: snapshot + atClusterTime: + "$$exists": true + - commandStartedEvent: + command: + find: collection0 + readConcern: + level: snapshot + atClusterTime: + "$$exists": true + +- description: Distinct operation with snapshot + operations: + - name: distinct + object: collection0 + arguments: + fieldName: x + filter: {} + session: session0 + expectResult: + - 11 + - name: findOneAndUpdate + object: collection0 + arguments: + filter: { _id: 2 } + update: { $inc: { x: 1 } } + returnDocument: After + expectResult: { _id: 2, x: 12 } + - name: distinct + object: collection0 + arguments: + fieldName: x + filter: {} + session: session1 + expectResult: [11, 12] + - name: findOneAndUpdate + object: collection0 + arguments: + filter: { _id: 2 } + update: { $inc: { x: 1 } } + returnDocument: After + expectResult: { _id: 2, x: 13 } + - name: distinct + object: collection0 + arguments: + fieldName: x + filter: {} + expectResult: [ 11, 13 ] + - name: distinct + object: collection0 + arguments: + fieldName: x + filter: {} + session: session0 + expectResult: [ 11 ] + - name: distinct + object: collection0 + arguments: + fieldName: x + filter: {} + session: session1 + expectResult: [ 11, 12 ] + expectEvents: + - client: client0 + events: + - commandStartedEvent: + command: + distinct: collection0 + readConcern: + level: snapshot + atClusterTime: + "$$exists": false + - commandStartedEvent: + command: + distinct: collection0 + readConcern: + level: snapshot + atClusterTime: + "$$exists": false + - commandStartedEvent: + command: + distinct: collection0 + readConcern: + "$$exists": false + - commandStartedEvent: + command: + distinct: collection0 + readConcern: + level: snapshot + atClusterTime: + "$$exists": true + - commandStartedEvent: + command: + distinct: collection0 + readConcern: + level: snapshot + atClusterTime: + "$$exists": true + +- description: Aggregate operation with snapshot + operations: + - name: aggregate + object: collection0 + arguments: + pipeline: + - "$match": { _id: 1 } + session: session0 + expectResult: + - { _id: 1, x: 11 } + - name: findOneAndUpdate + object: collection0 + arguments: + filter: { _id: 1 } + update: { $inc: { x: 1 } } + returnDocument: After + expectResult: { _id: 1, x: 12 } + - name: aggregate + object: collection0 + arguments: + pipeline: + - "$match": + _id: 1 + session: session1 + expectResult: + - {_id: 1, x: 12} + - name: findOneAndUpdate + object: collection0 + arguments: + filter: { _id: 1 } + update: { $inc: { x: 1 } } + returnDocument: After + expectResult: { _id: 1, x: 13 } + - name: aggregate + object: collection0 + arguments: + pipeline: + - "$match": { _id: 1 } + expectResult: + - { _id: 1, x: 13 } + - name: aggregate + object: collection0 + arguments: + pipeline: + - "$match": + _id: 1 + session: session0 + expectResult: + - { _id: 1, x: 11 } + - name: aggregate + object: collection0 + arguments: + pipeline: + - "$match": { _id: 1 } + session: session1 + expectResult: + - { _id: 1, x: 12 } + expectEvents: + - client: client0 + events: + - commandStartedEvent: + command: + aggregate: collection0 + readConcern: + level: snapshot + atClusterTime: + "$$exists": false + - commandStartedEvent: + command: + aggregate: collection0 + readConcern: + level: snapshot + atClusterTime: + "$$exists": false + - commandStartedEvent: + command: + aggregate: collection0 + readConcern: + "$$exists": false + - commandStartedEvent: + command: + aggregate: collection0 + readConcern: + level: snapshot + atClusterTime: + "$$exists": true + - commandStartedEvent: + command: + aggregate: collection0 + readConcern: + level: snapshot + atClusterTime: + "$$exists": true + +- description: Mixed operation with snapshot + operations: + - name: find + object: collection0 + arguments: + session: session0 + filter: { _id: 1 } + expectResult: + - { _id: 1, x: 11 } + - name: findOneAndUpdate + object: collection0 + arguments: + filter: { _id: 1 } + update: { $inc: { x: 1 } } + returnDocument: After + expectResult: { _id: 1, x: 12 } + - name: find + object: collection0 + arguments: + filter: { _id: 1 } + expectResult: + - { _id: 1, x: 12 } + - name: aggregate + object: collection0 + arguments: + pipeline: + - "$match": + _id: 1 + session: session0 + expectResult: + - { _id: 1, x: 11 } + - name: distinct + object: collection0 + arguments: + fieldName: x + filter: {} + session: session0 + expectResult: [ 11 ] + expectEvents: + - client: client0 + events: + - commandStartedEvent: + command: + find: collection0 + readConcern: + level: snapshot + atClusterTime: + "$$exists": false + - commandStartedEvent: + command: + find: collection0 + readConcern: + "$$exists": false + - commandStartedEvent: + command: + aggregate: collection0 + readConcern: + level: snapshot + atClusterTime: + "$$exists": true + - commandStartedEvent: + command: + distinct: collection0 + readConcern: + level: snapshot + atClusterTime: + "$$exists": true + +- description: Write commands with snapshot session do not affect snapshot reads + operations: + - name: find + object: collection0 + arguments: + filter: {} + session: session0 + - name: insertOne + object: collection0 + arguments: + session: session0 + document: + _id: 22 + x: 33 + - name: updateOne + object: collection0 + arguments: + filter: { _id: 1 } + session: session0 + update: { $inc: { x: 1 } } + - name: find + object: collection0 + arguments: + filter: { _id: 1 } + session: session0 + expectResult: + - {_id: 1, x: 11} + expectEvents: + - client: client0 + events: + - commandStartedEvent: + command: + find: collection0 + readConcern: + level: snapshot + atClusterTime: + "$$exists": false + - commandStartedEvent: + command: + find: collection0 + readConcern: + level: snapshot + atClusterTime: + "$$exists": true + +- description: First snapshot read does not send atClusterTime + operations: + - name: find + object: collection0 + arguments: + filter: {} + session: session0 + expectEvents: + - client: client0 + events: + - commandStartedEvent: + command: + find: collection0 + readConcern: + level: snapshot + atClusterTime: + "$$exists": false + commandName: find + databaseName: database0 + +- description: StartTransaction fails in snapshot session + operations: + - name: startTransaction + object: session0 + expectError: + isError: true + isClientError: true + errorContains: Transactions are not supported in snapshot sessions From cd05607a9738679d73278baa1654846ecec7bf84 Mon Sep 17 00:00:00 2001 From: Daria Pardue <81593090+dariakp@users.noreply.github.com> Date: Wed, 7 Jul 2021 16:38:27 -0400 Subject: [PATCH 03/16] test: Bring in snapshot spec changes from DRIVERS-1820for server error tests --- .../unified-spec-runner/entities.ts | 4 + test/spec/sessions/README.rst | 26 ++++- ...t-sessions-not-supported-server-error.json | 96 +++++++++++++++++-- ...ot-sessions-not-supported-server-error.yml | 62 ++++++++++-- 4 files changed, 166 insertions(+), 22 deletions(-) diff --git a/test/functional/unified-spec-runner/entities.ts b/test/functional/unified-spec-runner/entities.ts index 1fcdcd5027..ecf3b9ad22 100644 --- a/test/functional/unified-spec-runner/entities.ts +++ b/test/functional/unified-spec-runner/entities.ts @@ -250,6 +250,10 @@ export class EntitiesMap extends Map { options.causalConsistency = entity.session.sessionOptions?.causalConsistency; } + if (entity.session.sessionOptions?.snapshot) { + options.snapshot = entity.session.sessionOptions?.snapshot; + } + if (entity.session.sessionOptions?.defaultTransactionOptions) { options.defaultTransactionOptions = Object.create(null); const defaultOptions = entity.session.sessionOptions.defaultTransactionOptions; diff --git a/test/spec/sessions/README.rst b/test/spec/sessions/README.rst index 3ed7eea96a..d88b5c7ba6 100644 --- a/test/spec/sessions/README.rst +++ b/test/spec/sessions/README.rst @@ -9,10 +9,11 @@ Driver Session Tests Introduction ============ -The YAML and JSON files in this directory are platform-independent tests that -drivers can use to prove their conformance to the Driver Sessions Spec. They are +The YAML and JSON files in the ``legacy`` and ``unified`` sub-directories are platform-independent tests +that drivers can use to prove their conformance to the Driver Sessions Spec. They are designed with the intention of sharing most test-runner code with the -Transactions spec tests. +`Transactions Spec tests <../../transactions/tests/README.rst#test-format>`_.. Tests in the +``unified`` directory are written using the `Unified Test Format <../../unified-test-format/unified-test-format.rst>`_. Several prose tests, which are not easily expressed in YAML, are also presented in the Driver Sessions Spec. Those tests will need to be manually implemented @@ -78,7 +79,26 @@ the given session is *not* marked dirty:: arguments: session: session0 +Snapshot session tests +====================== +Snapshot sessions tests require server of version 5.0 or higher and +replica set or a sharded cluster deployment. +Default snapshot history window on the server is 5 minutes. Running the test in debug mode, or in any other slow configuration +may lead to `SnapshotTooOld` errors. Drivers can work around this issue by increasing the server's `minSnapshotHistoryWindowInSeconds` parameter, for example: + +.. code:: python + + client.admin.command('setParameter', 1, minSnapshotHistoryWindowInSeconds=60) + +Prose tests +``````````` +- Setting both ``snapshot`` and ``causalConsistency`` is not allowed + + * ``client.startSession(snapshot = true, causalConsistency = true)`` + * Assert that an error was raised by driver + Changelog ========= :2019-05-15: Initial version. +:2021-06-15: Added snapshot-session tests. Introduced legacy and unified folders. diff --git a/test/spec/sessions/unified/snapshot-sessions-not-supported-server-error.json b/test/spec/sessions/unified/snapshot-sessions-not-supported-server-error.json index b6ce00216a..79213f314f 100644 --- a/test/spec/sessions/unified/snapshot-sessions-not-supported-server-error.json +++ b/test/spec/sessions/unified/snapshot-sessions-not-supported-server-error.json @@ -3,11 +3,7 @@ "schemaVersion": "1.0", "runOnRequirements": [ { - "minServerVersion": "3.6", - "maxServerVersion": "4.4.99" - }, - { - "minServerVersion": "3.6", + "minServerVersion": "5.0", "topologies": [ "single" ] @@ -20,11 +16,6 @@ "observeEvents": [ "commandStartedEvent", "commandFailedEvent" - ], - "ignoreCommandMonitoringEvents": [ - "findAndModify", - "insert", - "update" ] } }, @@ -106,6 +97,91 @@ ] } ] + }, + { + "description": "Server returns an error on aggregate with snapshot", + "operations": [ + { + "name": "aggregate", + "object": "collection0", + "arguments": { + "session": "session0", + "pipeline": [] + }, + "expectError": { + "isError": true, + "isClientError": false + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "aggregate": "collection0", + "readConcern": { + "level": "snapshot", + "atClusterTime": { + "$$exists": false + } + } + } + } + }, + { + "commandFailedEvent": { + "commandName": "aggregate" + } + } + ] + } + ] + }, + { + "description": "Server returns an error on distinct with snapshot", + "operations": [ + { + "name": "distinct", + "object": "collection0", + "arguments": { + "fieldName": "x", + "filter": {}, + "session": "session0" + }, + "expectError": { + "isError": true, + "isClientError": false + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "distinct": "collection0", + "readConcern": { + "level": "snapshot", + "atClusterTime": { + "$$exists": false + } + } + } + } + }, + { + "commandFailedEvent": { + "commandName": "distinct" + } + } + ] + } + ] } ] } diff --git a/test/spec/sessions/unified/snapshot-sessions-not-supported-server-error.yml b/test/spec/sessions/unified/snapshot-sessions-not-supported-server-error.yml index f12efeb9df..4953dbcbe5 100644 --- a/test/spec/sessions/unified/snapshot-sessions-not-supported-server-error.yml +++ b/test/spec/sessions/unified/snapshot-sessions-not-supported-server-error.yml @@ -3,24 +3,21 @@ description: snapshot-sessions-not-supported-server-error schemaVersion: "1.0" runOnRequirements: - - minServerVersion: "3.6" - maxServerVersion: "4.4.99" - - minServerVersion: "3.6" + - minServerVersion: "5.0" topologies: [ single ] createEntities: - client: id: &client0 client0 observeEvents: [ commandStartedEvent, commandFailedEvent ] - ignoreCommandMonitoringEvents: [ findAndModify, insert, update ] - database: - id: &database0 database0 + id: &database0Name database0 client: *client0 - databaseName: &database0Name database0 + databaseName: *database0Name - collection: - id: &collection0 collection0 - database: *database0 - collectionName: &collection0Name collection0 + id: &collection0Name collection0 + database: *database0Name + collectionName: *collection0Name - session: id: session0 client: client0 @@ -56,3 +53,50 @@ tests: "$$exists": false - commandFailedEvent: commandName: find + +- description: Server returns an error on aggregate with snapshot + operations: + - name: aggregate + object: collection0 + arguments: + session: session0 + pipeline: [] + expectError: + isError: true + isClientError: false + expectEvents: + - client: client0 + events: + - commandStartedEvent: + command: + aggregate: collection0 + readConcern: + level: snapshot + atClusterTime: + "$$exists": false + - commandFailedEvent: + commandName: aggregate + +- description: Server returns an error on distinct with snapshot + operations: + - name: distinct + object: collection0 + arguments: + fieldName: x + filter: {} + session: session0 + expectError: + isError: true + isClientError: false + expectEvents: + - client: client0 + events: + - commandStartedEvent: + command: + distinct: collection0 + readConcern: + level: snapshot + atClusterTime: + "$$exists": false + - commandFailedEvent: + commandName: distinct From 2f8c7fc42b7149dfd4f0517fa1f58cec045f0b7e Mon Sep 17 00:00:00 2001 From: Daria Pardue <81593090+dariakp@users.noreply.github.com> Date: Thu, 8 Jul 2021 16:03:39 -0400 Subject: [PATCH 04/16] first pass at snapshot sessions implementation with up to date spec tests for snapshot-sessions --- src/sessions.ts | 44 ++++++++++++++++--- .../unified-spec-runner/entities.ts | 2 +- .../sessions/unified/snapshot-sessions.json | 2 - .../sessions/unified/snapshot-sessions.yml | 2 - 4 files changed, 38 insertions(+), 12 deletions(-) diff --git a/src/sessions.ts b/src/sessions.ts index aca5b7dd26..1b8ff56c36 100644 --- a/src/sessions.ts +++ b/src/sessions.ts @@ -30,6 +30,7 @@ import type { AbstractCursor } from './cursor/abstract_cursor'; import type { CommandOptions } from './cmap/connection'; import type { WriteConcern } from './write_concern'; import { TypedEventEmitter } from './mongo_types'; +import { ReadConcernLevel } from './read_concern'; const minWireVersionForShardedTransactions = 8; @@ -51,6 +52,8 @@ function assertAlive(session: ClientSession, callback?: Callback): boolean { export interface ClientSessionOptions { /** Whether causal consistency should be enabled on this session */ causalConsistency?: boolean; + /** Whether all read operations should be read from the same snapshot for this session (NOTE: not compatible with `causalConsistency=true`) */ + snapshot?: boolean; /** The default TransactionOptions to use for transactions started on this session. */ defaultTransactionOptions?: TransactionOptions; @@ -88,6 +91,7 @@ class ClientSession extends TypedEventEmitter { clientOptions?: MongoOptions; supports: { causalConsistency: boolean }; clusterTime?: ClusterTime; + snapshotTime?: Timestamp; // TODO: should this be more "private"? operationTime?: Timestamp; explicit: boolean; /** @internal */ @@ -96,6 +100,8 @@ class ClientSession extends TypedEventEmitter { transaction: Transaction; /** @internal */ [kServerSession]?: ServerSession; + /** @internal */ + snapshot = false; // TODO: should this be more "private"? /** * Create a client session. @@ -123,6 +129,15 @@ class ClientSession extends TypedEventEmitter { options = options ?? {}; + if (options.snapshot === true) { + this.snapshot = true; + if (options.causalConsistency === true) { + // TODO: throw validation error; MongoDriverError or more specific + } + + // TODO(NODE-3394): also validate server version >= 5.0 + } + this.topology = topology; this.sessionPool = sessionPool; this.hasEnded = false; @@ -130,8 +145,7 @@ class ClientSession extends TypedEventEmitter { this[kServerSession] = undefined; this.supports = { - causalConsistency: - typeof options.causalConsistency === 'boolean' ? options.causalConsistency : true + causalConsistency: options.snapshot !== true && options.causalConsistency !== false }; this.clusterTime = options.initialClusterTime; @@ -257,6 +271,11 @@ class ClientSession extends TypedEventEmitter { * @param options - Options for the transaction */ startTransaction(options?: TransactionOptions): void { + if (this.snapshot) { + // TODO: should this be a different type? + throw new MongoDriverError('Transactions are not allowed with snapshot sessions'); + } + assertAlive(this); if (this.inTransaction()) { throw new MongoDriverError('Transaction already in progress'); @@ -801,28 +820,35 @@ function applySession( // first apply non-transaction-specific sessions data const inTransaction = session.inTransaction() || isTransactionCommand(command); const isRetryableWrite = options?.willRetryWrite || false; - const shouldApplyReadConcern = commandSupportsReadConcern(command, options); if (serverSession.txnNumber && (isRetryableWrite || inTransaction)) { command.txnNumber = Long.fromNumber(serverSession.txnNumber); } - // now attempt to apply transaction-specific sessions data if (!inTransaction) { if (session.transaction.state !== TxnState.NO_TRANSACTION) { session.transaction.transition(TxnState.NO_TRANSACTION); } - // TODO: the following should only be applied to read operation per spec. - // for causal consistency - if (session.supports.causalConsistency && session.operationTime && shouldApplyReadConcern) { + if ( + session.supports.causalConsistency && + session.operationTime && + commandSupportsReadConcern(command, options) + ) { command.readConcern = command.readConcern || {}; Object.assign(command.readConcern, { afterClusterTime: session.operationTime }); + } else if (session.snapshot) { + command.readConcern = command.readConcern || { level: ReadConcernLevel.snapshot }; // TODO: is there a better place to set this? + if (session.snapshotTime !== undefined) { + Object.assign(command.readConcern, { atClusterTime: session.snapshotTime }); + } } return; } + // now attempt to apply transaction-specific sessions data + // `autocommit` must always be false to differentiate from retryable writes command.autocommit = false; @@ -855,6 +881,10 @@ function updateSessionFromResponse(session: ClientSession, document: Document): if (document.recoveryToken && session && session.inTransaction()) { session.transaction._recoveryToken = document.recoveryToken; } + + if (document.cursor?.atClusterTime && session?.snapshot && session.snapshotTime === undefined) { + session.snapshotTime = document.cursor.atClusterTime; + } } export { diff --git a/test/functional/unified-spec-runner/entities.ts b/test/functional/unified-spec-runner/entities.ts index ecf3b9ad22..eccf54ce79 100644 --- a/test/functional/unified-spec-runner/entities.ts +++ b/test/functional/unified-spec-runner/entities.ts @@ -251,7 +251,7 @@ export class EntitiesMap extends Map { } if (entity.session.sessionOptions?.snapshot) { - options.snapshot = entity.session.sessionOptions?.snapshot; + options.snapshot = entity.session.sessionOptions.snapshot; } if (entity.session.sessionOptions?.defaultTransactionOptions) { diff --git a/test/spec/sessions/unified/snapshot-sessions.json b/test/spec/sessions/unified/snapshot-sessions.json index 4170a96699..ef3b9e50f2 100644 --- a/test/spec/sessions/unified/snapshot-sessions.json +++ b/test/spec/sessions/unified/snapshot-sessions.json @@ -813,7 +813,6 @@ "name": "insertOne", "object": "collection0", "arguments": { - "session": "session0", "document": { "_id": 22, "x": 33 @@ -827,7 +826,6 @@ "filter": { "_id": 1 }, - "session": "session0", "update": { "$inc": { "x": 1 diff --git a/test/spec/sessions/unified/snapshot-sessions.yml b/test/spec/sessions/unified/snapshot-sessions.yml index 9beda1ac0e..dc9485a7b1 100644 --- a/test/spec/sessions/unified/snapshot-sessions.yml +++ b/test/spec/sessions/unified/snapshot-sessions.yml @@ -387,7 +387,6 @@ tests: - name: insertOne object: collection0 arguments: - session: session0 document: _id: 22 x: 33 @@ -395,7 +394,6 @@ tests: object: collection0 arguments: filter: { _id: 1 } - session: session0 update: { $inc: { x: 1 } } - name: find object: collection0 From ebb9a921a5b45fb0440c8e0d84b26b7a62ef5dac Mon Sep 17 00:00:00 2001 From: Daria Pardue <81593090+dariakp@users.noreply.github.com> Date: Fri, 9 Jul 2021 14:45:11 -0400 Subject: [PATCH 05/16] clean up snapshot session implementation --- src/sdam/topology.ts | 6 +++++- src/sessions.ts | 33 ++++++++++++++++++++++++--------- src/utils.ts | 2 +- 3 files changed, 30 insertions(+), 11 deletions(-) diff --git a/src/sdam/topology.ts b/src/sdam/topology.ts index 835743d4c0..5f6171acf0 100644 --- a/src/sdam/topology.ts +++ b/src/sdam/topology.ts @@ -379,7 +379,7 @@ export class Topology extends TypedEventEmitter { return this.s.description; } - capabilities(): ServerCapabilities { + get capabilities(): ServerCapabilities { return new ServerCapabilities(this.lastIsMaster()); } @@ -1064,6 +1064,10 @@ export class ServerCapabilities { return this.maxWireVersion >= 3; } + get supportsSnapshotReads(): boolean { + return this.maxWireVersion >= 13; + } + get commandsTakeWriteConcern(): boolean { return this.maxWireVersion >= 5; } diff --git a/src/sessions.ts b/src/sessions.ts index 1b8ff56c36..4a2a979fe0 100644 --- a/src/sessions.ts +++ b/src/sessions.ts @@ -75,6 +75,10 @@ export type ClientSessionEvents = { /** @internal */ const kServerSession = Symbol('serverSession'); +/** @internal */ +const kSnapshotTime = Symbol('snapshotTime'); +/** @internal */ +const kSnapshotEnabled = Symbol('snapshotEnabled'); /** * A class representing a client session on the server @@ -91,7 +95,6 @@ class ClientSession extends TypedEventEmitter { clientOptions?: MongoOptions; supports: { causalConsistency: boolean }; clusterTime?: ClusterTime; - snapshotTime?: Timestamp; // TODO: should this be more "private"? operationTime?: Timestamp; explicit: boolean; /** @internal */ @@ -100,8 +103,9 @@ class ClientSession extends TypedEventEmitter { transaction: Transaction; /** @internal */ [kServerSession]?: ServerSession; + [kSnapshotTime]?: Timestamp; /** @internal */ - snapshot = false; // TODO: should this be more "private"? + [kSnapshotEnabled] = false; /** * Create a client session. @@ -130,9 +134,16 @@ class ClientSession extends TypedEventEmitter { options = options ?? {}; if (options.snapshot === true) { - this.snapshot = true; + if (!topology.capabilities.supportsSnapshotReads) { + throw new MongoDriverError('Snapshot reads require MongoDB 5.0 or later'); + } + + this[kSnapshotEnabled] = true; if (options.causalConsistency === true) { // TODO: throw validation error; MongoDriverError or more specific + throw new MongoDriverError( + 'Properties "causalConsistency" and "snapshot" are mutually exclusive' + ); } // TODO(NODE-3394): also validate server version >= 5.0 @@ -271,7 +282,7 @@ class ClientSession extends TypedEventEmitter { * @param options - Options for the transaction */ startTransaction(options?: TransactionOptions): void { - if (this.snapshot) { + if (this[kSnapshotEnabled]) { // TODO: should this be a different type? throw new MongoDriverError('Transactions are not allowed with snapshot sessions'); } @@ -837,10 +848,10 @@ function applySession( ) { command.readConcern = command.readConcern || {}; Object.assign(command.readConcern, { afterClusterTime: session.operationTime }); - } else if (session.snapshot) { + } else if (session[kSnapshotEnabled]) { command.readConcern = command.readConcern || { level: ReadConcernLevel.snapshot }; // TODO: is there a better place to set this? - if (session.snapshotTime !== undefined) { - Object.assign(command.readConcern, { atClusterTime: session.snapshotTime }); + if (session[kSnapshotTime] !== undefined) { + Object.assign(command.readConcern, { atClusterTime: session[kSnapshotTime] }); } } @@ -882,8 +893,12 @@ function updateSessionFromResponse(session: ClientSession, document: Document): session.transaction._recoveryToken = document.recoveryToken; } - if (document.cursor?.atClusterTime && session?.snapshot && session.snapshotTime === undefined) { - session.snapshotTime = document.cursor.atClusterTime; + if ( + document.cursor?.atClusterTime && + session?.[kSnapshotEnabled] && + session[kSnapshotTime] === undefined + ) { + session[kSnapshotTime] = document.cursor.atClusterTime; } } diff --git a/src/utils.ts b/src/utils.ts index 80bc3a09ce..562818e0db 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -394,7 +394,7 @@ export function decorateWithCollation( target: MongoClient | Db | Collection, options: AnyOptions ): void { - const capabilities = getTopology(target).capabilities(); + const capabilities = getTopology(target).capabilities; if (options.collation && typeof options.collation === 'object') { if (capabilities && capabilities.commandsTakeCollation) { command.collation = options.collation; From 3df029e2caf4673bf449906695e3ba29cfa108b6 Mon Sep 17 00:00:00 2001 From: Daria Pardue <81593090+dariakp@users.noreply.github.com> Date: Fri, 9 Jul 2021 14:49:05 -0400 Subject: [PATCH 06/16] Pull in latest snapshot-sessions spec tests --- .../sessions/unified/snapshot-sessions.json | 56 +++++++++++++++++++ .../sessions/unified/snapshot-sessions.yml | 32 +++++++++++ 2 files changed, 88 insertions(+) diff --git a/test/spec/sessions/unified/snapshot-sessions.json b/test/spec/sessions/unified/snapshot-sessions.json index ef3b9e50f2..75b577b039 100644 --- a/test/spec/sessions/unified/snapshot-sessions.json +++ b/test/spec/sessions/unified/snapshot-sessions.json @@ -655,6 +655,62 @@ } ] }, + { + "description": "countDocuments operation with snapshot", + "operations": [ + { + "name": "countDocuments", + "object": "collection0", + "arguments": { + "filter": {}, + "session": "session0" + }, + "expectResult": 2 + }, + { + "name": "countDocuments", + "object": "collection0", + "arguments": { + "filter": {}, + "session": "session0" + }, + "expectResult": 2 + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "aggregate": "collection0", + "readConcern": { + "level": "snapshot", + "atClusterTime": { + "$$exists": false + } + } + } + } + }, + { + "commandStartedEvent": { + "command": { + "aggregate": "collection0", + "readConcern": { + "level": "snapshot", + "atClusterTime": { + "$$exists": true + } + } + } + } + } + ] + } + ] + }, { "description": "Mixed operation with snapshot", "operations": [ diff --git a/test/spec/sessions/unified/snapshot-sessions.yml b/test/spec/sessions/unified/snapshot-sessions.yml index dc9485a7b1..2f5fc23125 100644 --- a/test/spec/sessions/unified/snapshot-sessions.yml +++ b/test/spec/sessions/unified/snapshot-sessions.yml @@ -309,6 +309,38 @@ tests: atClusterTime: "$$exists": true +- description: countDocuments operation with snapshot + operations: + - name: countDocuments + object: collection0 + arguments: + filter: {} + session: session0 + expectResult: 2 + - name: countDocuments + object: collection0 + arguments: + filter: {} + session: session0 + expectResult: 2 + expectEvents: + - client: client0 + events: + - commandStartedEvent: + command: + aggregate: collection0 + readConcern: + level: snapshot + atClusterTime: + "$$exists": false + - commandStartedEvent: + command: + aggregate: collection0 + readConcern: + level: snapshot + atClusterTime: + "$$exists": true + - description: Mixed operation with snapshot operations: - name: find From 7c43565b528fc34ca3799058fbc820ee0b498ef8 Mon Sep 17 00:00:00 2001 From: Daria Pardue <81593090+dariakp@users.noreply.github.com> Date: Fri, 9 Jul 2021 15:00:31 -0400 Subject: [PATCH 07/16] Temporarily skip test runner related test failures and pull in client error sessions spec tests --- test/functional/sessions.test.js | 11 +- ...t-sessions-not-supported-client-error.json | 113 ++++++++++++++++++ ...ot-sessions-not-supported-client-error.yml | 69 +++++++++++ 3 files changed, 192 insertions(+), 1 deletion(-) create mode 100644 test/spec/sessions/unified/snapshot-sessions-not-supported-client-error.json create mode 100644 test/spec/sessions/unified/snapshot-sessions-not-supported-client-error.yml diff --git a/test/functional/sessions.test.js b/test/functional/sessions.test.js index eca6130fca..523cd3604f 100644 --- a/test/functional/sessions.test.js +++ b/test/functional/sessions.test.js @@ -201,11 +201,20 @@ describe('Sessions - functional', function () { for (const sessionTests of loadSpecTests(path.join('sessions', 'unified'))) { expect(sessionTests).to.be.an('object'); context(String(sessionTests.description), function () { + // TODO: NODE-3393 fix test runner to apply session to all operations + const testsToSkip = + sessionTests.description === 'snapshot-sessions' + ? [ + 'countDocuments operation with snapshot', + 'Distinct operation with snapshot', + 'Mixed operation with snapshot' + ] + : ['Server returns an error on distinct with snapshot']; for (const test of sessionTests.tests) { it(String(test.description), { metadata: { sessions: { skipLeakTests: true } }, test: async function () { - await runUnifiedTest(this, sessionTests, test); + await runUnifiedTest(this, sessionTests, test, testsToSkip); } }); } diff --git a/test/spec/sessions/unified/snapshot-sessions-not-supported-client-error.json b/test/spec/sessions/unified/snapshot-sessions-not-supported-client-error.json new file mode 100644 index 0000000000..129aa8d74c --- /dev/null +++ b/test/spec/sessions/unified/snapshot-sessions-not-supported-client-error.json @@ -0,0 +1,113 @@ +{ + "description": "snapshot-sessions-not-supported-client-error", + "schemaVersion": "1.0", + "runOnRequirements": [ + { + "minServerVersion": "3.6", + "maxServerVersion": "4.4.99" + } + ], + "createEntities": [ + { + "client": { + "id": "client0", + "observeEvents": [ + "commandStartedEvent", + "commandFailedEvent" + ] + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "database0" + } + }, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "collection0" + } + }, + { + "session": { + "id": "session0", + "client": "client0", + "sessionOptions": { + "snapshot": true + } + } + } + ], + "initialData": [ + { + "collectionName": "collection0", + "databaseName": "database0", + "documents": [ + { + "_id": 1, + "x": 11 + } + ] + } + ], + "tests": [ + { + "description": "Client error on find with snapshot", + "operations": [ + { + "name": "find", + "object": "collection0", + "arguments": { + "session": "session0", + "filter": {} + }, + "expectError": { + "isClientError": true, + "errorContains": "Snapshot reads require MongoDB 5.0 or later" + } + } + ], + "expectEvents": [] + }, + { + "description": "Client error on aggregate with snapshot", + "operations": [ + { + "name": "aggregate", + "object": "collection0", + "arguments": { + "session": "session0", + "pipeline": [] + }, + "expectError": { + "isClientError": true, + "errorContains": "Snapshot reads require MongoDB 5.0 or later" + } + } + ], + "expectEvents": [] + }, + { + "description": "Client error on distinct with snapshot", + "operations": [ + { + "name": "distinct", + "object": "collection0", + "arguments": { + "fieldName": "x", + "filter": {}, + "session": "session0" + }, + "expectError": { + "isClientError": true, + "errorContains": "Snapshot reads require MongoDB 5.0 or later" + } + } + ], + "expectEvents": [] + } + ] +} diff --git a/test/spec/sessions/unified/snapshot-sessions-not-supported-client-error.yml b/test/spec/sessions/unified/snapshot-sessions-not-supported-client-error.yml new file mode 100644 index 0000000000..b57344ce94 --- /dev/null +++ b/test/spec/sessions/unified/snapshot-sessions-not-supported-client-error.yml @@ -0,0 +1,69 @@ +description: snapshot-sessions-not-supported-client-error + +schemaVersion: "1.0" + +runOnRequirements: + - minServerVersion: "3.6" + maxServerVersion: "4.4.99" + +createEntities: + - client: + id: &client0 client0 + observeEvents: [ commandStartedEvent, commandFailedEvent ] + - database: + id: &database0Name database0 + client: *client0 + databaseName: *database0Name + - collection: + id: &collection0Name collection0 + database: *database0Name + collectionName: *collection0Name + - session: + id: session0 + client: client0 + sessionOptions: + snapshot: true + +initialData: + - collectionName: *collection0Name + databaseName: *database0Name + documents: + - { _id: 1, x: 11 } + +tests: +- description: Client error on find with snapshot + operations: + - name: find + object: collection0 + arguments: + session: session0 + filter: {} + expectError: + isClientError: true + errorContains: Snapshot reads require MongoDB 5.0 or later + expectEvents: [] + +- description: Client error on aggregate with snapshot + operations: + - name: aggregate + object: collection0 + arguments: + session: session0 + pipeline: [] + expectError: + isClientError: true + errorContains: Snapshot reads require MongoDB 5.0 or later + expectEvents: [] + +- description: Client error on distinct with snapshot + operations: + - name: distinct + object: collection0 + arguments: + fieldName: x + filter: {} + session: session0 + expectError: + isClientError: true + errorContains: Snapshot reads require MongoDB 5.0 or later + expectEvents: [] From 53938aa66f64dba54474a80d06673dcd21e06b47 Mon Sep 17 00:00:00 2001 From: Daria Pardue <81593090+dariakp@users.noreply.github.com> Date: Fri, 9 Jul 2021 15:32:02 -0400 Subject: [PATCH 08/16] Pull in unsupported ops snapshot spec tests --- test/functional/sessions.test.js | 25 +- .../snapshot-sessions-unsupported-ops.json | 493 ++++++++++++++++++ .../snapshot-sessions-unsupported-ops.yml | 258 +++++++++ 3 files changed, 768 insertions(+), 8 deletions(-) create mode 100644 test/spec/sessions/unified/snapshot-sessions-unsupported-ops.json create mode 100644 test/spec/sessions/unified/snapshot-sessions-unsupported-ops.yml diff --git a/test/functional/sessions.test.js b/test/functional/sessions.test.js index 523cd3604f..8e5ffa2cc5 100644 --- a/test/functional/sessions.test.js +++ b/test/functional/sessions.test.js @@ -202,14 +202,23 @@ describe('Sessions - functional', function () { expect(sessionTests).to.be.an('object'); context(String(sessionTests.description), function () { // TODO: NODE-3393 fix test runner to apply session to all operations - const testsToSkip = - sessionTests.description === 'snapshot-sessions' - ? [ - 'countDocuments operation with snapshot', - 'Distinct operation with snapshot', - 'Mixed operation with snapshot' - ] - : ['Server returns an error on distinct with snapshot']; + const skipTestMap = { + 'snapshot-sessions': [ + 'countDocuments operation with snapshot', + 'Distinct operation with snapshot', + 'Mixed operation with snapshot' + ], + 'snapshot-sessions-not-supported-server-error': [ + 'Server returns an error on distinct with snapshot' + ], + 'snapshot-sessions-unsupported-ops': [ + 'Server returns an error on listCollections with snapshot', + 'Server returns an error on listDatabases with snapshot', + 'Server returns an error on listIndexes with snapshot', + 'Server returns an error on runCommand with snapshot' + ] + }; + const testsToSkip = skipTestMap[sessionTests.description] || []; for (const test of sessionTests.tests) { it(String(test.description), { metadata: { sessions: { skipLeakTests: true } }, diff --git a/test/spec/sessions/unified/snapshot-sessions-unsupported-ops.json b/test/spec/sessions/unified/snapshot-sessions-unsupported-ops.json new file mode 100644 index 0000000000..1021b7f264 --- /dev/null +++ b/test/spec/sessions/unified/snapshot-sessions-unsupported-ops.json @@ -0,0 +1,493 @@ +{ + "description": "snapshot-sessions-unsupported-ops", + "schemaVersion": "1.0", + "runOnRequirements": [ + { + "minServerVersion": "5.0", + "topologies": [ + "replicaset", + "sharded-replicaset" + ] + } + ], + "createEntities": [ + { + "client": { + "id": "client0", + "observeEvents": [ + "commandStartedEvent", + "commandFailedEvent" + ] + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "database0" + } + }, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "collection0" + } + }, + { + "session": { + "id": "session0", + "client": "client0", + "sessionOptions": { + "snapshot": true + } + } + } + ], + "initialData": [ + { + "collectionName": "collection0", + "databaseName": "database0", + "documents": [ + { + "_id": 1, + "x": 11 + } + ] + } + ], + "tests": [ + { + "description": "Server returns an error on insertOne with snapshot", + "runOnRequirements": [ + { + "topologies": [ + "replicaset" + ] + } + ], + "operations": [ + { + "name": "insertOne", + "object": "collection0", + "arguments": { + "session": "session0", + "document": { + "_id": 22, + "x": 22 + } + }, + "expectError": { + "isError": true, + "isClientError": false + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "insert": "collection0", + "readConcern": { + "level": "snapshot", + "atClusterTime": { + "$$exists": false + } + } + } + } + }, + { + "commandFailedEvent": { + "commandName": "insert" + } + } + ] + } + ] + }, + { + "description": "Server returns an error on insertMany with snapshot", + "runOnRequirements": [ + { + "topologies": [ + "replicaset" + ] + } + ], + "operations": [ + { + "name": "insertMany", + "object": "collection0", + "arguments": { + "session": "session0", + "documents": [ + { + "_id": 22, + "x": 22 + }, + { + "_id": 33, + "x": 33 + } + ] + }, + "expectError": { + "isError": true, + "isClientError": false + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "insert": "collection0", + "readConcern": { + "level": "snapshot", + "atClusterTime": { + "$$exists": false + } + } + } + } + }, + { + "commandFailedEvent": { + "commandName": "insert" + } + } + ] + } + ] + }, + { + "description": "Server returns an error on deleteOne with snapshot", + "runOnRequirements": [ + { + "topologies": [ + "replicaset" + ] + } + ], + "operations": [ + { + "name": "deleteOne", + "object": "collection0", + "arguments": { + "session": "session0", + "filter": {} + }, + "expectError": { + "isError": true, + "isClientError": false + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "delete": "collection0", + "readConcern": { + "level": "snapshot", + "atClusterTime": { + "$$exists": false + } + } + } + } + }, + { + "commandFailedEvent": { + "commandName": "delete" + } + } + ] + } + ] + }, + { + "description": "Server returns an error on updateOne with snapshot", + "runOnRequirements": [ + { + "topologies": [ + "replicaset" + ] + } + ], + "operations": [ + { + "name": "updateOne", + "object": "collection0", + "arguments": { + "session": "session0", + "filter": { + "_id": 1 + }, + "update": { + "$inc": { + "x": 1 + } + } + }, + "expectError": { + "isError": true, + "isClientError": false + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "update": "collection0", + "readConcern": { + "level": "snapshot", + "atClusterTime": { + "$$exists": false + } + } + } + } + }, + { + "commandFailedEvent": { + "commandName": "update" + } + } + ] + } + ] + }, + { + "description": "Server returns an error on findOneAndUpdate with snapshot", + "operations": [ + { + "name": "findOneAndUpdate", + "object": "collection0", + "arguments": { + "session": "session0", + "filter": { + "_id": 1 + }, + "update": { + "$inc": { + "x": 1 + } + } + }, + "expectError": { + "isError": true, + "isClientError": false + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "findAndModify": "collection0", + "readConcern": { + "level": "snapshot", + "atClusterTime": { + "$$exists": false + } + } + } + } + }, + { + "commandFailedEvent": { + "commandName": "findAndModify" + } + } + ] + } + ] + }, + { + "description": "Server returns an error on listDatabases with snapshot", + "operations": [ + { + "name": "listDatabases", + "object": "client0", + "arguments": { + "session": "session0" + }, + "expectError": { + "isError": true, + "isClientError": false + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "listDatabases": 1, + "readConcern": { + "level": "snapshot", + "atClusterTime": { + "$$exists": false + } + } + } + } + }, + { + "commandFailedEvent": { + "commandName": "listDatabases" + } + } + ] + } + ] + }, + { + "description": "Server returns an error on listCollections with snapshot", + "operations": [ + { + "name": "listCollections", + "object": "database0", + "arguments": { + "session": "session0" + }, + "expectError": { + "isError": true, + "isClientError": false + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "listCollections": 1, + "readConcern": { + "level": "snapshot", + "atClusterTime": { + "$$exists": false + } + } + } + } + }, + { + "commandFailedEvent": { + "commandName": "listCollections" + } + } + ] + } + ] + }, + { + "description": "Server returns an error on listIndexes with snapshot", + "operations": [ + { + "name": "listIndexes", + "object": "collection0", + "arguments": { + "session": "session0" + }, + "expectError": { + "isError": true, + "isClientError": false + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "listIndexes": "collection0", + "readConcern": { + "level": "snapshot", + "atClusterTime": { + "$$exists": false + } + } + } + } + }, + { + "commandFailedEvent": { + "commandName": "listIndexes" + } + } + ] + } + ] + }, + { + "description": "Server returns an error on runCommand with snapshot", + "operations": [ + { + "name": "runCommand", + "object": "database0", + "arguments": { + "session": "session0", + "commandName": "listCollections", + "command": { + "listCollections": 1 + } + }, + "expectError": { + "isError": true, + "isClientError": false + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "listCollections": 1, + "readConcern": { + "level": "snapshot", + "atClusterTime": { + "$$exists": false + } + } + } + } + }, + { + "commandFailedEvent": { + "commandName": "listCollections" + } + } + ] + } + ] + } + ] +} diff --git a/test/spec/sessions/unified/snapshot-sessions-unsupported-ops.yml b/test/spec/sessions/unified/snapshot-sessions-unsupported-ops.yml new file mode 100644 index 0000000000..1d5dce8933 --- /dev/null +++ b/test/spec/sessions/unified/snapshot-sessions-unsupported-ops.yml @@ -0,0 +1,258 @@ +description: snapshot-sessions-unsupported-ops + +schemaVersion: "1.0" + +runOnRequirements: + - minServerVersion: "5.0" + topologies: [replicaset, sharded-replicaset] + +createEntities: + - client: + id: &client0 client0 + observeEvents: [ commandStartedEvent, commandFailedEvent ] + - database: + id: &database0Name database0 + client: *client0 + databaseName: *database0Name + - collection: + id: &collection0Name collection0 + database: *database0Name + collectionName: *collection0Name + - session: + id: session0 + client: client0 + sessionOptions: + snapshot: true + +initialData: + - collectionName: *collection0Name + databaseName: *database0Name + documents: + - { _id: 1, x: 11 } + +tests: +- description: Server returns an error on insertOne with snapshot + # Skip on sharded clusters due to SERVER-58176. + runOnRequirements: + - topologies: [replicaset] + operations: + - name: insertOne + object: collection0 + arguments: + session: session0 + document: + _id: 22 + x: 22 + expectError: + isError: true + isClientError: false + expectEvents: + - client: client0 + events: + - commandStartedEvent: + command: + insert: collection0 + readConcern: + level: snapshot + atClusterTime: + "$$exists": false + - commandFailedEvent: + commandName: insert + +- description: Server returns an error on insertMany with snapshot + # Skip on sharded clusters due to SERVER-58176. + runOnRequirements: + - topologies: [replicaset] + operations: + - name: insertMany + object: collection0 + arguments: + session: session0 + documents: + - _id: 22 + x: 22 + - _id: 33 + x: 33 + expectError: + isError: true + isClientError: false + expectEvents: + - client: client0 + events: + - commandStartedEvent: + command: + insert: collection0 + readConcern: + level: snapshot + atClusterTime: + "$$exists": false + - commandFailedEvent: + commandName: insert + +- description: Server returns an error on deleteOne with snapshot + # Skip on sharded clusters due to SERVER-58176. + runOnRequirements: + - topologies: [replicaset] + operations: + - name: deleteOne + object: collection0 + arguments: + session: session0 + filter: {} + expectError: + isError: true + isClientError: false + expectEvents: + - client: client0 + events: + - commandStartedEvent: + command: + delete: collection0 + readConcern: + level: snapshot + atClusterTime: + "$$exists": false + - commandFailedEvent: + commandName: delete + +- description: Server returns an error on updateOne with snapshot + # Skip on sharded clusters due to SERVER-58176. + runOnRequirements: + - topologies: [replicaset] + operations: + - name: updateOne + object: collection0 + arguments: + session: session0 + filter: { _id: 1 } + update: { $inc: { x: 1 } } + expectError: + isError: true + isClientError: false + expectEvents: + - client: client0 + events: + - commandStartedEvent: + command: + update: collection0 + readConcern: + level: snapshot + atClusterTime: + "$$exists": false + - commandFailedEvent: + commandName: update + +- description: Server returns an error on findOneAndUpdate with snapshot + operations: + - name: findOneAndUpdate + object: collection0 + arguments: + session: session0 + filter: { _id: 1 } + update: { $inc: { x: 1 } } + expectError: + isError: true + isClientError: false + expectEvents: + - client: client0 + events: + - commandStartedEvent: + command: + findAndModify: collection0 + readConcern: + level: snapshot + atClusterTime: + "$$exists": false + - commandFailedEvent: + commandName: findAndModify + +- description: Server returns an error on listDatabases with snapshot + operations: + - name: listDatabases + object: client0 + arguments: + session: session0 + expectError: + isError: true + isClientError: false + expectEvents: + - client: client0 + events: + - commandStartedEvent: + command: + listDatabases: 1 + readConcern: + level: snapshot + atClusterTime: + "$$exists": false + - commandFailedEvent: + commandName: listDatabases + +- description: Server returns an error on listCollections with snapshot + operations: + - name: listCollections + object: database0 + arguments: + session: session0 + expectError: + isError: true + isClientError: false + expectEvents: + - client: client0 + events: + - commandStartedEvent: + command: + listCollections: 1 + readConcern: + level: snapshot + atClusterTime: + "$$exists": false + - commandFailedEvent: + commandName: listCollections + +- description: Server returns an error on listIndexes with snapshot + operations: + - name: listIndexes + object: collection0 + arguments: + session: session0 + expectError: + isError: true + isClientError: false + expectEvents: + - client: client0 + events: + - commandStartedEvent: + command: + listIndexes: collection0 + readConcern: + level: snapshot + atClusterTime: + "$$exists": false + - commandFailedEvent: + commandName: listIndexes + +- description: Server returns an error on runCommand with snapshot + operations: + - name: runCommand + object: database0 + arguments: + session: session0 + commandName: listCollections + command: + listCollections: 1 + expectError: + isError: true + isClientError: false + expectEvents: + - client: client0 + events: + - commandStartedEvent: + command: + listCollections: 1 + readConcern: + level: snapshot + atClusterTime: + "$$exists": false + - commandFailedEvent: + commandName: listCollections From 63062ca4bd6703fc590550af09a572c252d47bdb Mon Sep 17 00:00:00 2001 From: Daria Pardue <81593090+dariakp@users.noreply.github.com> Date: Fri, 9 Jul 2021 15:32:23 -0400 Subject: [PATCH 09/16] make typescript happy --- src/sessions.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/sessions.ts b/src/sessions.ts index 4a2a979fe0..7aa30081a7 100644 --- a/src/sessions.ts +++ b/src/sessions.ts @@ -103,6 +103,7 @@ class ClientSession extends TypedEventEmitter { transaction: Transaction; /** @internal */ [kServerSession]?: ServerSession; + /** @internal */ [kSnapshotTime]?: Timestamp; /** @internal */ [kSnapshotEnabled] = false; @@ -140,13 +141,10 @@ class ClientSession extends TypedEventEmitter { this[kSnapshotEnabled] = true; if (options.causalConsistency === true) { - // TODO: throw validation error; MongoDriverError or more specific throw new MongoDriverError( 'Properties "causalConsistency" and "snapshot" are mutually exclusive' ); } - - // TODO(NODE-3394): also validate server version >= 5.0 } this.topology = topology; @@ -283,7 +281,6 @@ class ClientSession extends TypedEventEmitter { */ startTransaction(options?: TransactionOptions): void { if (this[kSnapshotEnabled]) { - // TODO: should this be a different type? throw new MongoDriverError('Transactions are not allowed with snapshot sessions'); } @@ -849,7 +846,7 @@ function applySession( command.readConcern = command.readConcern || {}; Object.assign(command.readConcern, { afterClusterTime: session.operationTime }); } else if (session[kSnapshotEnabled]) { - command.readConcern = command.readConcern || { level: ReadConcernLevel.snapshot }; // TODO: is there a better place to set this? + command.readConcern = command.readConcern || { level: ReadConcernLevel.snapshot }; if (session[kSnapshotTime] !== undefined) { Object.assign(command.readConcern, { atClusterTime: session[kSnapshotTime] }); } From c531099aa875262130df62f2dacd8b43d83c4b5a Mon Sep 17 00:00:00 2001 From: Daria Pardue <81593090+dariakp@users.noreply.github.com> Date: Mon, 12 Jul 2021 10:27:08 -0400 Subject: [PATCH 10/16] Move server version check for snapshots to operation level --- src/operations/execute_operation.ts | 2 ++ src/sessions.ts | 8 ++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/operations/execute_operation.ts b/src/operations/execute_operation.ts index b9880c4688..c6baa30c07 100644 --- a/src/operations/execute_operation.ts +++ b/src/operations/execute_operation.ts @@ -86,6 +86,8 @@ export function executeOperation< session = topology.startSession({ owner, explicit: false }); } else if (session.hasEnded) { return cb(new MongoDriverError('Use of expired sessions is not permitted')); + } else if (session.snapshotEnabled && !topology.capabilities.supportsSnapshotReads) { + return cb(new MongoDriverError('Snapshot reads require MongoDB 5.0 or later')); } } else if (session) { // If the user passed an explicit session and we are still, after server selection, diff --git a/src/sessions.ts b/src/sessions.ts index 7aa30081a7..893f6543b1 100644 --- a/src/sessions.ts +++ b/src/sessions.ts @@ -108,6 +108,10 @@ class ClientSession extends TypedEventEmitter { /** @internal */ [kSnapshotEnabled] = false; + get snapshotEnabled(): boolean { + return this[kSnapshotEnabled]; + } + /** * Create a client session. * @internal @@ -135,10 +139,6 @@ class ClientSession extends TypedEventEmitter { options = options ?? {}; if (options.snapshot === true) { - if (!topology.capabilities.supportsSnapshotReads) { - throw new MongoDriverError('Snapshot reads require MongoDB 5.0 or later'); - } - this[kSnapshotEnabled] = true; if (options.causalConsistency === true) { throw new MongoDriverError( From 9b1d320482abdf4dd0e5b9127cb279e3a32794ae Mon Sep 17 00:00:00 2001 From: Daria Pardue <81593090+dariakp@users.noreply.github.com> Date: Mon, 12 Jul 2021 10:53:11 -0400 Subject: [PATCH 11/16] Skip distinct client error sessions test --- test/functional/sessions.test.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/functional/sessions.test.js b/test/functional/sessions.test.js index 8e5ffa2cc5..46faebfbd3 100644 --- a/test/functional/sessions.test.js +++ b/test/functional/sessions.test.js @@ -208,6 +208,9 @@ describe('Sessions - functional', function () { 'Distinct operation with snapshot', 'Mixed operation with snapshot' ], + 'snapshot-sessions-not-supported-client-error': [ + 'Client error on distinct with snapshot' + ], 'snapshot-sessions-not-supported-server-error': [ 'Server returns an error on distinct with snapshot' ], From 73378fa1c0bfb531879ec04c6b2b3202e3e7ea48 Mon Sep 17 00:00:00 2001 From: Daria Pardue <81593090+dariakp@users.noreply.github.com> Date: Mon, 12 Jul 2021 10:58:26 -0400 Subject: [PATCH 12/16] Clean up --- test/functional/sessions.test.js | 1 - 1 file changed, 1 deletion(-) diff --git a/test/functional/sessions.test.js b/test/functional/sessions.test.js index 46faebfbd3..93b99be5a0 100644 --- a/test/functional/sessions.test.js +++ b/test/functional/sessions.test.js @@ -186,7 +186,6 @@ describe('Sessions - functional', function () { function testFilter(spec) { const SKIP_TESTS = [ // These two tests need to run against multiple mongoses - // TODO: maybe create ticket? 'Dirty explicit session is discarded', 'Dirty implicit session is discarded (write)' ]; From e077392b4873f8d64add0da9c176e015932dd9d8 Mon Sep 17 00:00:00 2001 From: Daria Pardue <81593090+dariakp@users.noreply.github.com> Date: Mon, 12 Jul 2021 11:14:15 -0400 Subject: [PATCH 13/16] Update legacy exports --- src/sessions.ts | 22 ++++++---------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/src/sessions.ts b/src/sessions.ts index 893f6543b1..0b5ad7540c 100644 --- a/src/sessions.ts +++ b/src/sessions.ts @@ -86,7 +86,7 @@ const kSnapshotEnabled = Symbol('snapshotEnabled'); * NOTE: not meant to be instantiated directly. * @public */ -class ClientSession extends TypedEventEmitter { +export class ClientSession extends TypedEventEmitter { /** @internal */ topology: Topology; /** @internal */ @@ -650,7 +650,7 @@ export type ServerSessionId = { id: Binary }; * WARNING: not meant to be instantiated directly. For internal use only. * @public */ -class ServerSession { +export class ServerSession { id: ServerSessionId; lastUse: number; txnNumber: number; @@ -685,7 +685,7 @@ class ServerSession { * For internal use only * @internal */ -class ServerSessionPool { +export class ServerSessionPool { topology: Topology; sessions: ServerSession[]; @@ -773,7 +773,7 @@ class ServerSessionPool { // TODO: this should be codified in command construction // @see https://github.com/mongodb/specifications/blob/master/source/read-write-concern/read-write-concern.rst#read-concern -function commandSupportsReadConcern(command: Document, options?: Document): boolean { +export function commandSupportsReadConcern(command: Document, options?: Document): boolean { if (command.aggregate || command.count || command.distinct || command.find || command.geoNear) { return true; } @@ -797,7 +797,7 @@ function commandSupportsReadConcern(command: Document, options?: Document): bool * @param command - the command to decorate * @param options - Optional settings passed to calling operation */ -function applySession( +export function applySession( session: ClientSession, command: Document, options?: CommandOptions @@ -877,7 +877,7 @@ function applySession( } } -function updateSessionFromResponse(session: ClientSession, document: Document): void { +export function updateSessionFromResponse(session: ClientSession, document: Document): void { if (document.$clusterTime) { resolveClusterTime(session, document.$clusterTime); } @@ -898,13 +898,3 @@ function updateSessionFromResponse(session: ClientSession, document: Document): session[kSnapshotTime] = document.cursor.atClusterTime; } } - -export { - ClientSession, - ServerSession, - ServerSessionPool, - TxnState, - applySession, - updateSessionFromResponse, - commandSupportsReadConcern -}; From 87e981b43a4864c97fd71a5109703d03619b7945 Mon Sep 17 00:00:00 2001 From: Daria Pardue <81593090+dariakp@users.noreply.github.com> Date: Mon, 12 Jul 2021 13:59:54 -0400 Subject: [PATCH 14/16] Add unit test for causalConsistency and snapshot error --- test/unit/core/sessions.test.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/test/unit/core/sessions.test.js b/test/unit/core/sessions.test.js index 9283c671cf..fe4801b658 100644 --- a/test/unit/core/sessions.test.js +++ b/test/unit/core/sessions.test.js @@ -27,6 +27,17 @@ describe('Sessions - unit/core', function () { } }); + it('should throw an error if snapshot and causalConsistency options are both set to true', { + metadata: { requires: { topology: 'single' } }, + test: function () { + const client = new Topology('localhost:27017', {}); + const sessionPool = client.s.sessionPool; + expect( + () => new ClientSession(client, sessionPool, { causalConsistency: true, snapshot: true }) + ).not.to.throw('Properties "causalConsistency" and "snapshot" are mutually exclusive'); + } + }); + it('should default to `null` for `clusterTime`', { metadata: { requires: { topology: 'single' } }, test: function (done) { From 52d538deef8896b5860bbb77cf340235af1e5427 Mon Sep 17 00:00:00 2001 From: Daria Pardue <81593090+dariakp@users.noreply.github.com> Date: Mon, 12 Jul 2021 14:23:16 -0400 Subject: [PATCH 15/16] add unit tests for new errors --- test/unit/core/sessions.test.js | 48 +++++++++++++++++++++++---------- 1 file changed, 34 insertions(+), 14 deletions(-) diff --git a/test/unit/core/sessions.test.js b/test/unit/core/sessions.test.js index fe4801b658..2ecc8468fe 100644 --- a/test/unit/core/sessions.test.js +++ b/test/unit/core/sessions.test.js @@ -10,6 +10,17 @@ const { now } = require('../../../src/utils'); let test = {}; describe('Sessions - unit/core', function () { describe('ClientSession', function () { + let session; + let sessionPool; + + afterEach(done => { + if (sessionPool) { + sessionCleanupHandler(session, sessionPool, done)(); + } else { + done(); + } + }); + it('should throw errors with invalid parameters', { metadata: { requires: { topology: 'single' } }, test: function () { @@ -31,39 +42,48 @@ describe('Sessions - unit/core', function () { metadata: { requires: { topology: 'single' } }, test: function () { const client = new Topology('localhost:27017', {}); - const sessionPool = client.s.sessionPool; + sessionPool = client.s.sessionPool; expect( () => new ClientSession(client, sessionPool, { causalConsistency: true, snapshot: true }) - ).not.to.throw('Properties "causalConsistency" and "snapshot" are mutually exclusive'); + ).to.throw('Properties "causalConsistency" and "snapshot" are mutually exclusive'); } }); it('should default to `null` for `clusterTime`', { metadata: { requires: { topology: 'single' } }, - test: function (done) { + test: function () { const client = new Topology('localhost:27017', {}); - const sessionPool = client.s.sessionPool; - const session = new ClientSession(client, sessionPool); - done = sessionCleanupHandler(session, sessionPool, done); - + sessionPool = client.s.sessionPool; + session = new ClientSession(client, sessionPool); expect(session.clusterTime).to.not.exist; - done(); } }); it('should set the internal clusterTime to `initialClusterTime` if provided', { metadata: { requires: { topology: 'single' } }, - test: function (done) { + test: function () { const clusterTime = genClusterTime(Date.now()); const client = new Topology('localhost:27017'); - const sessionPool = client.s.sessionPool; - const session = new ClientSession(client, sessionPool, { initialClusterTime: clusterTime }); - done = sessionCleanupHandler(session, sessionPool, done); - + sessionPool = client.s.sessionPool; + session = new ClientSession(client, sessionPool, { initialClusterTime: clusterTime }); expect(session.clusterTime).to.eql(clusterTime); - done(); } }); + + describe('startTransaction()', () => { + it('should throw an error if the session is snapshot enabled', { + metadata: { requires: { topology: 'single' } }, + test: function () { + const client = new Topology('localhost:27017', {}); + sessionPool = client.s.sessionPool; + session = new ClientSession(client, sessionPool, { snapshot: true }); + expect(session.snapshotEnabled).to.equal(true); + expect(() => session.startTransaction()).to.throw( + 'Transactions are not allowed with snapshot sessions' + ); + } + }); + }); }); describe('ServerSessionPool', function () { From 92737a79101d804e4c47f69b50cafb04a87ba272 Mon Sep 17 00:00:00 2001 From: Daria Pardue <81593090+dariakp@users.noreply.github.com> Date: Mon, 12 Jul 2021 15:04:30 -0400 Subject: [PATCH 16/16] Move getter for consistency --- src/sessions.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/sessions.ts b/src/sessions.ts index 0b5ad7540c..2b5324061f 100644 --- a/src/sessions.ts +++ b/src/sessions.ts @@ -108,10 +108,6 @@ export class ClientSession extends TypedEventEmitter { /** @internal */ [kSnapshotEnabled] = false; - get snapshotEnabled(): boolean { - return this[kSnapshotEnabled]; - } - /** * Create a client session. * @internal @@ -180,6 +176,11 @@ export class ClientSession extends TypedEventEmitter { return this[kServerSession]!; } + /** Whether or not this session is configured for snapshot reads */ + get snapshotEnabled(): boolean { + return this[kSnapshotEnabled]; + } + /** * Ends this session on the server *