diff --git a/docs/reference/create-client.txt b/docs/reference/create-client.txt index 56a69df192..b5b15ebca1 100644 --- a/docs/reference/create-client.txt +++ b/docs/reference/create-client.txt @@ -619,8 +619,9 @@ Ruby Options - ``{ :mode => :primary }`` * - ``:read_concern`` - - Specifies the read concern options. The only valid key is ``level``, for which the valid - values are ``:local``, ``:majority``, and ``:snapshot``. + - Specifies the read concern options. The only valid key is ``level``, + for which the valid values are ``:local``, ``:majority``, and + ``:snapshot``. - ``Hash`` - none diff --git a/lib/mongo/client.rb b/lib/mongo/client.rb index 34fe7da4ae..0464fbbfca 100644 --- a/lib/mongo/client.rb +++ b/lib/mongo/client.rb @@ -887,6 +887,7 @@ def reconnect # # See https://docs.mongodb.com/manual/reference/command/listDatabases/ # for more information and usage. + # @option opts [ Session ] :session The session to use. # # @return [ Array ] The names of the databases. # @@ -910,6 +911,7 @@ def database_names(filter = {}, opts = {}) # # See https://docs.mongodb.com/manual/reference/command/listDatabases/ # for more information and usage. + # @option opts [ Session ] :session The session to use. # # @return [ Array ] The info for each database. # @@ -930,6 +932,8 @@ def list_databases(filter = {}, name_only = false, opts = {}) # @param [ Hash ] filter The filter criteria for getting a list of databases. # @param [ Hash ] opts The command options. # + # @option opts [ Session ] :session The session to use. + # # @return [ Array ] The list of database objects. # # @since 2.5.0 diff --git a/lib/mongo/database.rb b/lib/mongo/database.rb index a907758bf6..1db2a69908 100644 --- a/lib/mongo/database.rb +++ b/lib/mongo/database.rb @@ -253,12 +253,12 @@ def read_command(operation, opts = {}) client.send(:with_session, opts) do |session| read_with_retry(session, preference) do |server| - Operation::Command.new({ - :selector => operation.dup, - :db_name => name, - :read => preference, - :session => session - }).execute(server, context: Operation::Context.new(client: client, session: session)) + Operation::Command.new( + selector: operation.dup, + db_name: name, + read: preference, + session: session, + ).execute(server, context: Operation::Context.new(client: client, session: session)) end end end diff --git a/lib/mongo/database/view.rb b/lib/mongo/database/view.rb index 32492439e1..03076dec4c 100644 --- a/lib/mongo/database/view.rb +++ b/lib/mongo/database/view.rb @@ -57,6 +57,7 @@ class View # # See https://docs.mongodb.com/manual/reference/command/listCollections/ # for more information and usage. + # @option options [ Session ] :session The session to use. # # @return [ Array ] The names of all non-system collections. # @@ -100,12 +101,13 @@ def collection_names(options = {}) # # See https://docs.mongodb.com/manual/reference/command/listCollections/ # for more information and usage. + # @option options [ Session ] :session The session to use. # # @return [ Array ] Info for each collection in the database. # # @since 2.0.5 def list_collections(options = {}) - session = client.send(:get_session) + session = client.send(:get_session, options) collections_info(session, ServerSelector.primary, options) end diff --git a/lib/mongo/error.rb b/lib/mongo/error.rb index 62826ecf62..1a63311185 100644 --- a/lib/mongo/error.rb +++ b/lib/mongo/error.rb @@ -223,6 +223,8 @@ def add_label(label) require 'mongo/error/no_srv_records' require 'mongo/error/session_ended' require 'mongo/error/sessions_not_supported' +require 'mongo/error/snapshot_session_invalid_server_version' +require 'mongo/error/snapshot_session_transaction_prohibited' require 'mongo/error/operation_failure' require 'mongo/error/pool_closed_error' require 'mongo/error/raise_original_error' diff --git a/lib/mongo/error/snapshot_session_invalid_server_version.rb b/lib/mongo/error/snapshot_session_invalid_server_version.rb new file mode 100644 index 0000000000..e8a14c2d62 --- /dev/null +++ b/lib/mongo/error/snapshot_session_invalid_server_version.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true +# encoding: utf-8 + +# Copyright (C) 2021 MongoDB Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +module Mongo + class Error + + # Exception raised if an operation using a snapshot session is + # directed to a pre-5.0 server. + class SnapshotSessionInvalidServerVersion < Error + + # Instantiate the new exception. + def initialize + super("Snapshot reads require MongoDB 5.0 or later") + end + end + end +end diff --git a/lib/mongo/error/snapshot_session_transaction_prohibited.rb b/lib/mongo/error/snapshot_session_transaction_prohibited.rb new file mode 100644 index 0000000000..93fd5e6a1f --- /dev/null +++ b/lib/mongo/error/snapshot_session_transaction_prohibited.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true +# encoding: utf-8 + +# Copyright (C) 2021 MongoDB Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +module Mongo + class Error + + # Exception raised if a transaction is attempted on a snapshot session. + class SnapshotSessionTransactionProhibited < Error + + # Instantiate the new exception. + def initialize + super("Transactions are not supported in snapshot sessions") + end + end + end +end diff --git a/lib/mongo/operation/result.rb b/lib/mongo/operation/result.rb index d31ceac3a5..953bd39e6a 100644 --- a/lib/mongo/operation/result.rb +++ b/lib/mongo/operation/result.rb @@ -429,6 +429,12 @@ def write_concern_error? !!(first_document && first_document['writeConcernError']) end + def snapshot_timestamp + if doc = reply.documents.first + doc['cursor']&.[]('atClusterTime') || doc['atClusterTime'] + end + end + private def aggregate_returned_count diff --git a/lib/mongo/operation/shared/executable.rb b/lib/mongo/operation/shared/executable.rb index abe8dbfc4f..7bc04a3100 100644 --- a/lib/mongo/operation/shared/executable.rb +++ b/lib/mongo/operation/shared/executable.rb @@ -43,6 +43,10 @@ def do_execute(connection, context, options = {}) session.pin_to_service(connection.service_id) end end + + if session.snapshot? && !session.snapshot_timestamp + session.snapshot_timestamp = result.snapshot_timestamp + end end process_result(result, connection) end diff --git a/lib/mongo/operation/shared/sessions_supported.rb b/lib/mongo/operation/shared/sessions_supported.rb index d2328e642d..1a00c5cfa1 100644 --- a/lib/mongo/operation/shared/sessions_supported.rb +++ b/lib/mongo/operation/shared/sessions_supported.rb @@ -224,6 +224,17 @@ def apply_session_options(sel, connection) then sel[:recoveryToken] = session.recovery_token end + + if session.snapshot? + unless connection.description.server_version_gte?('5.0') + raise Error::SnapshotSessionInvalidServerVersion + end + + sel[:readConcern] = {level: 'snapshot'} + if session.snapshot_timestamp + sel[:readConcern][:atClusterTime] = session.snapshot_timestamp + end + end end def build_message(connection, context) diff --git a/lib/mongo/session.rb b/lib/mongo/session.rb index 99a805497f..b695c5c87c 100644 --- a/lib/mongo/session.rb +++ b/lib/mongo/session.rb @@ -56,10 +56,16 @@ class Session # - *:mode* -- the read preference as a string or symbol; valid values are # *:primary*, *:primary_preferred*, *:secondary*, *:secondary_preferred* # and *:nearest*. + # @option options [ true | false ] :snapshot Set up the session for + # snapshot reads. # # @since 2.5.0 # @api private def initialize(server_session, client, options = {}) + if options[:causal_consistency] && options[:snapshot] + raise ArgumentError, ':causal_consistency and :snapshot options cannot be both set on a session' + end + @server_session = server_session options = options.dup @@ -83,6 +89,12 @@ def cluster @client.cluster end + # @return [ true | false ] Whether the session is configured for snapshot + # reads. + def snapshot? + !!options[:snapshot] + end + # @return [ BSON::Timestamp ] The latest seen operation time for this session. # # @since 2.5.0 @@ -506,6 +518,10 @@ def start_transaction(options = nil) =end end + if snapshot? + raise Mongo::Error::SnapshotSessionTransactionProhibited + end + check_if_ended! if within_states?(STARTING_TRANSACTION_STATE, TRANSACTION_IN_PROGRESS_STATE) @@ -1024,6 +1040,9 @@ def txn_num @server_session.txn_num end + # @api private + attr_accessor :snapshot_timestamp + private # Get the read concern the session will use when starting a transaction. diff --git a/spec/runners/unified/crud_operations.rb b/spec/runners/unified/crud_operations.rb index 1c734b7960..535492cfb6 100644 --- a/spec/runners/unified/crud_operations.rb +++ b/spec/runners/unified/crud_operations.rb @@ -29,7 +29,11 @@ def find(op) def count_documents(op) collection = entities.get(:collection, op.use!('object')) use_arguments(op) do |args| - collection.find(args.use!('filter')).count_documents + opts = {} + if session = args.use('session') + opts[:session] = entities.get(:session, session) + end + collection.find(args.use!('filter')).count_documents(**opts) end end @@ -47,7 +51,11 @@ def estimated_document_count(op) def distinct(op) collection = entities.get(:collection, op.use!('object')) use_arguments(op) do |args| - req = collection.find(args.use!('filter')).distinct(args.use!('fieldName')) + opts = {} + if session = args.use('session') + opts[:session] = entities.get(:session, session) + end + req = collection.find(args.use!('filter'), **opts).distinct(args.use!('fieldName'), **opts) result = req.to_a end end @@ -61,6 +69,9 @@ def find_one_and_update(op) if return_document = args.use('returnDocument') opts[:return_document] = return_document.downcase.to_sym end + if session = args.use('session') + opts[:session] = entities.get(:session, session) + end collection.find_one_and_update(filter, update, **opts) end end @@ -70,7 +81,11 @@ def find_one_and_replace(op) use_arguments(op) do |args| filter = args.use!('filter') update = args.use!('replacement') - collection.find_one_and_replace(filter, update) + opts = {} + if session = args.use('session') + opts[:session] = entities.get(:session, session) + end + collection.find_one_and_replace(filter, update, **opts) end end @@ -78,7 +93,11 @@ def find_one_and_delete(op) collection = entities.get(:collection, op.use!('object')) use_arguments(op) do |args| filter = args.use!('filter') - collection.find_one_and_delete(filter) + opts = {} + if session = args.use('session') + opts[:session] = entities.get(:session, session) + end + collection.find_one_and_delete(filter, **opts) end end @@ -96,18 +115,25 @@ def insert_one(op) def insert_many(op) collection = entities.get(:collection, op.use!('object')) use_arguments(op) do |args| - options = {} + opts = {} unless (ordered = args.use('ordered')).nil? - options[:ordered] = ordered + opts[:ordered] = ordered + end + if session = args.use('session') + opts[:session] = entities.get(:session, session) end - collection.insert_many(args.use!('documents'), **options) + collection.insert_many(args.use!('documents'), **opts) end end def update_one(op) collection = entities.get(:collection, op.use!('object')) use_arguments(op) do |args| - collection.update_one(args.use!('filter'), args.use!('update')) + opts = {} + if session = args.use('session') + opts[:session] = entities.get(:session, session) + end + collection.update_one(args.use!('filter'), args.use!('update'), **opts) end end @@ -132,7 +158,11 @@ def replace_one(op) def delete_one(op) collection = entities.get(:collection, op.use!('object')) use_arguments(op) do |args| - collection.delete_one(args.use!('filter')) + opts = {} + if session = args.use('session') + opts[:session] = entities.get(:session, session) + end + collection.delete_one(args.use!('filter'), **opts) end end @@ -157,6 +187,20 @@ def bulk_write(op) end end + def aggregate(op) + obj = entities.get_any(op.use!('object')) + args = op.use!('arguments') + pipeline = args.use!('pipeline') + opts = {} + if session = args.use('session') + opts[:session] = entities.get(:session, session) + end + unless args.empty? + raise NotImplementedError, "Unhandled spec keys: #{args} in #{test_spec}" + end + obj.aggregate(pipeline, **opts).to_a + end + private def convert_bulk_write_spec(spec) @@ -192,15 +236,5 @@ def convert_bulk_write_spec(spec) end {Utils.underscore(op) =>out} end - - def aggregate(op) - obj = entities.get_any(op.use!('object')) - args = op.use!('arguments') - pipeline = args.use!('pipeline') - unless args.empty? - raise NotImplementedError, "Unhandled spec keys: #{test_spec}" - end - obj.aggregate(pipeline).to_a - end end end diff --git a/spec/runners/unified/ddl_operations.rb b/spec/runners/unified/ddl_operations.rb index dae9ad9966..59f66e308c 100644 --- a/spec/runners/unified/ddl_operations.rb +++ b/spec/runners/unified/ddl_operations.rb @@ -7,7 +7,13 @@ module DdlOperations def list_databases(op) client = entities.get(:client, op.use!('object')) - client.list_databases + use_arguments(op) do |args| + opts = {} + if session = args.use('session') + opts[:session] = entities.get(:session, session) + end + client.list_databases({}, false, **opts) + end end def create_collection(op) @@ -28,6 +34,17 @@ def create_collection(op) end end + def list_collections(op) + database = entities.get(:database, op.use!('object')) + use_arguments(op) do |args| + opts = {} + if session = args.use('session') + opts[:session] = entities.get(:session, session) + end + database.list_collections(**opts) + end + end + def drop_collection(op) database = entities.get(:database, op.use!('object')) use_arguments(op) do |args| @@ -58,6 +75,17 @@ def assert_collection_not_exists(op) assert_collection_exists(op, false) end + def list_indexes(op) + collection = entities.get(:collection, op.use!('object')) + use_arguments(op) do |args| + opts = {} + if session = args.use('session') + opts[:session] = entities.get(:session, session) + end + collection.indexes(**opts).to_a + end + end + def create_index(op) collection = entities.get(:collection, op.use!('object')) use_arguments(op) do |args| diff --git a/spec/runners/unified/entity_map.rb b/spec/runners/unified/entity_map.rb index c0725a1b43..503aa2e644 100644 --- a/spec/runners/unified/entity_map.rb +++ b/spec/runners/unified/entity_map.rb @@ -3,6 +3,8 @@ module Unified class EntityMap + extend Forwardable + def initialize @map = {} end @@ -35,8 +37,6 @@ def get_any(id) raise Error::EntityMissing, "There is no #{id} known" end - def [](type) - @map[type] - end + def_delegators :@map, :[], :fetch end end diff --git a/spec/runners/unified/support_operations.rb b/spec/runners/unified/support_operations.rb index f713a103a6..f13cf4e2c1 100644 --- a/spec/runners/unified/support_operations.rb +++ b/spec/runners/unified/support_operations.rb @@ -13,7 +13,12 @@ def run_command(op) cmd = args.use!('command') - database.command(cmd) + opts = {} + if session = args.use('session') + opts[:session] = entities.get(:session, session) + end + + database.command(cmd, **opts) end end diff --git a/spec/runners/unified/test.rb b/spec/runners/unified/test.rb index 69b7f58563..dc5dbe9b7b 100644 --- a/spec/runners/unified/test.rb +++ b/spec/runners/unified/test.rb @@ -195,7 +195,8 @@ def create_entities def set_initial_data @spec['initialData']&.each do |entity_spec| spec = UsingHash[entity_spec] - collection = root_authorized_client.use(spec.use!('databaseName'))[spec.use!('collectionName')] + collection = root_authorized_client.with(write_concern: {w: :majority}). + use(spec.use!('databaseName'))[spec.use!('collectionName')] collection.drop docs = spec.use!('documents') if docs.any? @@ -268,7 +269,7 @@ def execute_operation(op) end if expected_error = op.use('expectError') begin - send(method_name, op) + public_send(method_name, op) rescue Mongo::Error, BSON::String::IllegalKey => e if expected_error.use('isClientError') # isClientError doesn't actually mean a client error. diff --git a/spec/spec_tests/data/sessions_unified/snapshot-sessions-not-supported-client-error.yml b/spec/spec_tests/data/sessions_unified/snapshot-sessions-not-supported-client-error.yml new file mode 100644 index 0000000000..b57344ce94 --- /dev/null +++ b/spec/spec_tests/data/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/spec/spec_tests/data/sessions_unified/snapshot-sessions-not-supported-server-error.yml b/spec/spec_tests/data/sessions_unified/snapshot-sessions-not-supported-server-error.yml new file mode 100644 index 0000000000..4953dbcbe5 --- /dev/null +++ b/spec/spec_tests/data/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/spec/spec_tests/data/sessions_unified/snapshot-sessions-unsupported-ops.yml b/spec/spec_tests/data/sessions_unified/snapshot-sessions-unsupported-ops.yml new file mode 100644 index 0000000000..1d5dce8933 --- /dev/null +++ b/spec/spec_tests/data/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/spec/spec_tests/data/sessions_unified/snapshot-sessions.yml b/spec/spec_tests/data/sessions_unified/snapshot-sessions.yml new file mode 100644 index 0000000000..2f5fc23125 --- /dev/null +++ b/spec/spec_tests/data/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/spec/spec_tests/sessions_unified_spec.rb b/spec/spec_tests/sessions_unified_spec.rb new file mode 100644 index 0000000000..8217299205 --- /dev/null +++ b/spec/spec_tests/sessions_unified_spec.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true +# encoding: utf-8 + +require 'spec_helper' + +require 'runners/unified' + +base = "#{CURRENT_PATH}/spec_tests/data/sessions_unified" +SESSIONS_UNIFIED_TESTS = Dir.glob("#{base}/**/*.yml").sort + +describe 'Sessions unified spec tests' do + define_unified_spec_tests(base, SESSIONS_UNIFIED_TESTS) +end