From e4ddaffc7ebd38df7ea758a54b3b8ca3e3eb01c1 Mon Sep 17 00:00:00 2001 From: Daria Pardue <81593090+dariakp@users.noreply.github.com> Date: Mon, 12 Jul 2021 16:56:11 -0400 Subject: [PATCH] feat(NODE-3392): enable snapshot reads on secondaries (#2897) --- src/operations/execute_operation.ts | 2 + src/sdam/topology.ts | 6 +- src/sessions.ts | 79 +- src/utils.ts | 2 +- test/functional/sessions.test.js | 51 +- .../unified-spec-runner/entities.ts | 4 + test/spec/read-write-concern/README.rst | 6 +- test/spec/sessions/README.rst | 26 +- .../{ => legacy}/dirty-session-errors.json | 0 .../{ => legacy}/dirty-session-errors.yml | 0 ...t-sessions-not-supported-client-error.json | 113 ++ ...ot-sessions-not-supported-client-error.yml | 69 ++ ...t-sessions-not-supported-server-error.json | 187 ++++ ...ot-sessions-not-supported-server-error.yml | 102 ++ .../snapshot-sessions-unsupported-ops.json | 493 +++++++++ .../snapshot-sessions-unsupported-ops.yml | 258 +++++ .../sessions/unified/snapshot-sessions.json | 993 ++++++++++++++++++ .../sessions/unified/snapshot-sessions.yml | 482 +++++++++ test/unit/core/sessions.test.js | 55 +- 19 files changed, 2878 insertions(+), 50 deletions(-) rename test/spec/sessions/{ => legacy}/dirty-session-errors.json (100%) rename test/spec/sessions/{ => legacy}/dirty-session-errors.yml (100%) 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 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-unsupported-ops.json create mode 100644 test/spec/sessions/unified/snapshot-sessions-unsupported-ops.yml create mode 100644 test/spec/sessions/unified/snapshot-sessions.json create mode 100644 test/spec/sessions/unified/snapshot-sessions.yml diff --git a/src/operations/execute_operation.ts b/src/operations/execute_operation.ts index b9880c4688b..c6baa30c072 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/sdam/topology.ts b/src/sdam/topology.ts index 835743d4c04..5f6171acf03 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 aca5b7dd261..2b5324061f3 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; @@ -72,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 @@ -79,7 +86,7 @@ const kServerSession = Symbol('serverSession'); * NOTE: not meant to be instantiated directly. * @public */ -class ClientSession extends TypedEventEmitter { +export class ClientSession extends TypedEventEmitter { /** @internal */ topology: Topology; /** @internal */ @@ -96,6 +103,10 @@ class ClientSession extends TypedEventEmitter { transaction: Transaction; /** @internal */ [kServerSession]?: ServerSession; + /** @internal */ + [kSnapshotTime]?: Timestamp; + /** @internal */ + [kSnapshotEnabled] = false; /** * Create a client session. @@ -123,6 +134,15 @@ class ClientSession extends TypedEventEmitter { options = options ?? {}; + if (options.snapshot === true) { + this[kSnapshotEnabled] = true; + if (options.causalConsistency === true) { + throw new MongoDriverError( + 'Properties "causalConsistency" and "snapshot" are mutually exclusive' + ); + } + } + this.topology = topology; this.sessionPool = sessionPool; this.hasEnded = false; @@ -130,8 +150,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; @@ -157,6 +176,11 @@ 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 * @@ -257,6 +281,10 @@ class ClientSession extends TypedEventEmitter { * @param options - Options for the transaction */ startTransaction(options?: TransactionOptions): void { + if (this[kSnapshotEnabled]) { + throw new MongoDriverError('Transactions are not allowed with snapshot sessions'); + } + assertAlive(this); if (this.inTransaction()) { throw new MongoDriverError('Transaction already in progress'); @@ -623,7 +651,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; @@ -658,7 +686,7 @@ class ServerSession { * For internal use only * @internal */ -class ServerSessionPool { +export class ServerSessionPool { topology: Topology; sessions: ServerSession[]; @@ -746,7 +774,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; } @@ -770,7 +798,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 @@ -801,28 +829,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[kSnapshotEnabled]) { + command.readConcern = command.readConcern || { level: ReadConcernLevel.snapshot }; + if (session[kSnapshotTime] !== undefined) { + Object.assign(command.readConcern, { atClusterTime: session[kSnapshotTime] }); + } } return; } + // now attempt to apply transaction-specific sessions data + // `autocommit` must always be false to differentiate from retryable writes command.autocommit = false; @@ -843,7 +878,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); } @@ -855,14 +890,12 @@ function updateSessionFromResponse(session: ClientSession, document: Document): if (document.recoveryToken && session && session.inTransaction()) { session.transaction._recoveryToken = document.recoveryToken; } -} -export { - ClientSession, - ServerSession, - ServerSessionPool, - TxnState, - applySession, - updateSessionFromResponse, - commandSupportsReadConcern -}; + 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 80bc3a09cef..562818e0db5 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; diff --git a/test/functional/sessions.test.js b/test/functional/sessions.test.js index f6226cd1185..93b99be5a04 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'); + const testSuites = loadSpecTests(path.join('sessions', 'legacy')); after(() => testContext.teardown()); before(function () { @@ -196,6 +196,43 @@ 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 () { + // TODO: NODE-3393 fix test runner to apply session to all operations + const skipTestMap = { + 'snapshot-sessions': [ + 'countDocuments operation with snapshot', + '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' + ], + '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 } }, + test: async function () { + await runUnifiedTest(this, sessionTests, test, testsToSkip); + } + }); + } + }); + } + }); + context('unacknowledged writes', () => { it('should not include session for unacknowledged writes', { metadata: { requires: { topology: 'single', mongodb: '>=3.6.0' } }, diff --git a/test/functional/unified-spec-runner/entities.ts b/test/functional/unified-spec-runner/entities.ts index 1fcdcd50273..eccf54ce79a 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/read-write-concern/README.rst b/test/spec/read-write-concern/README.rst index 5995590136a..2f2b84dc9cc 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/README.rst b/test/spec/sessions/README.rst index 3ed7eea96a4..d88b5c7ba69 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/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 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 00000000000..129aa8d74c8 --- /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 00000000000..b57344ce946 --- /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: [] 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 00000000000..79213f314f7 --- /dev/null +++ b/test/spec/sessions/unified/snapshot-sessions-not-supported-server-error.json @@ -0,0 +1,187 @@ +{ + "description": "snapshot-sessions-not-supported-server-error", + "schemaVersion": "1.0", + "runOnRequirements": [ + { + "minServerVersion": "5.0", + "topologies": [ + "single" + ] + } + ], + "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 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" + } + } + ] + } + ] + }, + { + "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 new file mode 100644 index 00000000000..4953dbcbe59 --- /dev/null +++ b/test/spec/sessions/unified/snapshot-sessions-not-supported-server-error.yml @@ -0,0 +1,102 @@ +description: snapshot-sessions-not-supported-server-error + +schemaVersion: "1.0" + +runOnRequirements: + - minServerVersion: "5.0" + topologies: [ single ] + +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 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 + +- 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-unsupported-ops.json b/test/spec/sessions/unified/snapshot-sessions-unsupported-ops.json new file mode 100644 index 00000000000..1021b7f2642 --- /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 00000000000..1d5dce89335 --- /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 diff --git a/test/spec/sessions/unified/snapshot-sessions.json b/test/spec/sessions/unified/snapshot-sessions.json new file mode 100644 index 00000000000..75b577b039f --- /dev/null +++ b/test/spec/sessions/unified/snapshot-sessions.json @@ -0,0 +1,993 @@ +{ + "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": "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", + "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": { + "document": { + "_id": 22, + "x": 33 + } + } + }, + { + "name": "updateOne", + "object": "collection0", + "arguments": { + "filter": { + "_id": 1 + }, + "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 00000000000..2f5fc23125e --- /dev/null +++ b/test/spec/sessions/unified/snapshot-sessions.yml @@ -0,0 +1,482 @@ +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: 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 + 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: + document: + _id: 22 + x: 33 + - name: updateOne + object: collection0 + arguments: + filter: { _id: 1 } + 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/unit/core/sessions.test.js b/test/unit/core/sessions.test.js index 9283c671cf6..2ecc8468fe6 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 () { @@ -27,32 +38,52 @@ describe('Sessions - unit/core', function () { } }); - it('should default to `null` for `clusterTime`', { + it('should throw an error if snapshot and causalConsistency options are both set to true', { 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; + expect( + () => new ClientSession(client, sessionPool, { causalConsistency: true, snapshot: true }) + ).to.throw('Properties "causalConsistency" and "snapshot" are mutually exclusive'); + } + }); + it('should default to `null` for `clusterTime`', { + metadata: { requires: { topology: 'single' } }, + test: function () { + const client = new Topology('localhost:27017', {}); + 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 () {