diff --git a/specifications/sessions/tests/snapshot-sessions.json b/specifications/sessions/tests/snapshot-sessions.json index 260f8b6f489..8f806ea7595 100644 --- a/specifications/sessions/tests/snapshot-sessions.json +++ b/specifications/sessions/tests/snapshot-sessions.json @@ -988,6 +988,810 @@ } } ] + }, + { + "description": "Find operation with snapshot and snapshot time", + "operations": [ + { + "name": "find", + "object": "collection0", + "arguments": { + "session": "session0", + "filter": {} + }, + "expectResult": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 11 + } + ] + }, + { + "name": "getSnapshotTime", + "object": "session0", + "saveResultAsEntity": "savedSnapshotTime" + }, + { + "name": "insertOne", + "object": "collection0", + "arguments": { + "document": { + "_id": 3, + "x": 33 + } + } + }, + { + "name": "createEntities", + "object": "testRunner", + "arguments": { + "entities": [ + { + "session": { + "id": "session2", + "client": "client0", + "sessionOptions": { + "snapshot": true, + "snapshotTime": "savedSnapshotTime" + } + } + } + ] + } + }, + { + "name": "find", + "object": "collection0", + "arguments": { + "session": "session2", + "filter": {} + }, + "expectResult": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 11 + } + ] + }, + { + "name": "find", + "object": "collection0", + "arguments": { + "session": "session2", + "filter": {} + }, + "expectResult": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 11 + } + ] + }, + { + "name": "find", + "object": "collection0", + "arguments": { + "filter": {} + }, + "expectResult": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 11 + }, + { + "_id": 3, + "x": 33 + } + ] + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "find": "collection0", + "readConcern": { + "level": "snapshot", + "atClusterTime": { + "$$exists": false + } + } + }, + "databaseName": "database0" + } + }, + { + "commandStartedEvent": { + "command": { + "find": "collection0", + "readConcern": { + "level": "snapshot", + "atClusterTime": { + "$$matchesEntity": "savedSnapshotTime" + } + } + }, + "databaseName": "database0" + } + }, + { + "commandStartedEvent": { + "command": { + "find": "collection0", + "readConcern": { + "level": "snapshot", + "atClusterTime": { + "$$matchesEntity": "savedSnapshotTime" + } + } + }, + "databaseName": "database0" + } + }, + { + "commandStartedEvent": { + "command": { + "find": "collection0", + "readConcern": { + "$$exists": false + } + }, + "databaseName": "database0" + } + } + ] + } + ] + }, + { + "description": "Distinct operation with snapshot and snapshot time", + "operations": [ + { + "name": "distinct", + "object": "collection0", + "arguments": { + "session": "session0", + "filter": {}, + "fieldName": "x" + }, + "expectResult": [ + 11 + ] + }, + { + "name": "getSnapshotTime", + "object": "session0", + "saveResultAsEntity": "savedSnapshotTime" + }, + { + "name": "insertOne", + "object": "collection0", + "arguments": { + "document": { + "_id": 3, + "x": 33 + } + } + }, + { + "name": "createEntities", + "object": "testRunner", + "arguments": { + "entities": [ + { + "session": { + "id": "session2", + "client": "client0", + "sessionOptions": { + "snapshot": true, + "snapshotTime": "savedSnapshotTime" + } + } + } + ] + } + }, + { + "name": "distinct", + "object": "collection0", + "arguments": { + "session": "session2", + "filter": {}, + "fieldName": "x" + }, + "expectResult": [ + 11 + ] + }, + { + "name": "distinct", + "object": "collection0", + "arguments": { + "session": "session2", + "filter": {}, + "fieldName": "x" + }, + "expectResult": [ + 11 + ] + }, + { + "name": "distinct", + "object": "collection0", + "arguments": { + "filter": {}, + "fieldName": "x" + }, + "expectResult": [ + 11, + 33 + ] + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "distinct": "collection0", + "readConcern": { + "level": "snapshot", + "atClusterTime": { + "$$exists": false + } + } + }, + "databaseName": "database0" + } + }, + { + "commandStartedEvent": { + "command": { + "distinct": "collection0", + "readConcern": { + "level": "snapshot", + "atClusterTime": { + "$$matchesEntity": "savedSnapshotTime" + } + } + }, + "databaseName": "database0" + } + }, + { + "commandStartedEvent": { + "command": { + "distinct": "collection0", + "readConcern": { + "level": "snapshot", + "atClusterTime": { + "$$matchesEntity": "savedSnapshotTime" + } + } + }, + "databaseName": "database0" + } + }, + { + "commandStartedEvent": { + "command": { + "distinct": "collection0", + "readConcern": { + "$$exists": false + } + }, + "databaseName": "database0" + } + } + ] + } + ] + }, + { + "description": "Aggregate operation with snapshot and snapshot time", + "operations": [ + { + "name": "aggregate", + "object": "collection0", + "arguments": { + "session": "session0", + "pipeline": [ + { + "$match": { + "_id": 1 + } + } + ] + }, + "expectResult": [ + { + "_id": 1, + "x": 11 + } + ] + }, + { + "name": "getSnapshotTime", + "object": "session0", + "saveResultAsEntity": "savedSnapshotTime" + }, + { + "name": "findOneAndUpdate", + "object": "collection0", + "arguments": { + "filter": { + "_id": 1 + }, + "update": { + "$inc": { + "x": 1 + } + }, + "returnDocument": "After" + }, + "expectResult": { + "_id": 1, + "x": 12 + } + }, + { + "name": "createEntities", + "object": "testRunner", + "arguments": { + "entities": [ + { + "session": { + "id": "session2", + "client": "client0", + "sessionOptions": { + "snapshot": true, + "snapshotTime": "savedSnapshotTime" + } + } + } + ] + } + }, + { + "name": "aggregate", + "object": "collection0", + "arguments": { + "session": "session2", + "pipeline": [ + { + "$match": { + "_id": 1 + } + } + ] + }, + "expectResult": [ + { + "_id": 1, + "x": 11 + } + ] + }, + { + "name": "aggregate", + "object": "collection0", + "arguments": { + "session": "session2", + "pipeline": [ + { + "$match": { + "_id": 1 + } + } + ] + }, + "expectResult": [ + { + "_id": 1, + "x": 11 + } + ] + }, + { + "name": "aggregate", + "object": "collection0", + "arguments": { + "pipeline": [ + { + "$match": { + "_id": 1 + } + } + ] + }, + "expectResult": [ + { + "_id": 1, + "x": 12 + } + ] + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "aggregate": "collection0", + "readConcern": { + "level": "snapshot", + "atClusterTime": { + "$$exists": false + } + } + }, + "databaseName": "database0" + } + }, + { + "commandStartedEvent": { + "command": { + "aggregate": "collection0", + "readConcern": { + "level": "snapshot", + "atClusterTime": { + "$$matchesEntity": "savedSnapshotTime" + } + } + }, + "databaseName": "database0" + } + }, + { + "commandStartedEvent": { + "command": { + "aggregate": "collection0", + "readConcern": { + "level": "snapshot", + "atClusterTime": { + "$$matchesEntity": "savedSnapshotTime" + } + } + }, + "databaseName": "database0" + } + }, + { + "commandStartedEvent": { + "command": { + "aggregate": "collection0", + "readConcern": { + "$$exists": false + } + }, + "databaseName": "database0" + } + } + ] + } + ] + }, + { + "description": "countDocuments operation with snapshot and snapshot time", + "operations": [ + { + "name": "countDocuments", + "object": "collection0", + "arguments": { + "session": "session0", + "filter": {} + }, + "expectResult": 2 + }, + { + "name": "getSnapshotTime", + "object": "session0", + "saveResultAsEntity": "savedSnapshotTime" + }, + { + "name": "insertOne", + "object": "collection0", + "arguments": { + "document": { + "_id": 3, + "x": 33 + } + } + }, + { + "name": "createEntities", + "object": "testRunner", + "arguments": { + "entities": [ + { + "session": { + "id": "session2", + "client": "client0", + "sessionOptions": { + "snapshot": true, + "snapshotTime": "savedSnapshotTime" + } + } + } + ] + } + }, + { + "name": "countDocuments", + "object": "collection0", + "arguments": { + "session": "session2", + "filter": {} + }, + "expectResult": 2 + }, + { + "name": "countDocuments", + "object": "collection0", + "arguments": { + "session": "session2", + "filter": {} + }, + "expectResult": 2 + }, + { + "name": "countDocuments", + "object": "collection0", + "arguments": { + "filter": {} + }, + "expectResult": 3 + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "aggregate": "collection0", + "readConcern": { + "level": "snapshot", + "atClusterTime": { + "$$exists": false + } + } + }, + "databaseName": "database0" + } + }, + { + "commandStartedEvent": { + "command": { + "aggregate": "collection0", + "readConcern": { + "level": "snapshot", + "atClusterTime": { + "$$matchesEntity": "savedSnapshotTime" + } + } + }, + "databaseName": "database0" + } + }, + { + "commandStartedEvent": { + "command": { + "aggregate": "collection0", + "readConcern": { + "level": "snapshot", + "atClusterTime": { + "$$matchesEntity": "savedSnapshotTime" + } + } + }, + "databaseName": "database0" + } + }, + { + "commandStartedEvent": { + "command": { + "aggregate": "collection0", + "readConcern": { + "$$exists": false + } + }, + "databaseName": "database0" + } + } + ] + } + ] + }, + { + "description": "Mixed operation with snapshot and snapshotTime", + "operations": [ + { + "name": "find", + "object": "collection0", + "arguments": { + "session": "session0", + "filter": { + "_id": 1 + } + }, + "expectResult": [ + { + "_id": 1, + "x": 11 + } + ] + }, + { + "name": "getSnapshotTime", + "object": "session0", + "saveResultAsEntity": "savedSnapshotTime" + }, + { + "name": "findOneAndUpdate", + "object": "collection0", + "arguments": { + "filter": { + "_id": 1 + }, + "update": { + "$inc": { + "x": 1 + } + }, + "returnDocument": "After" + }, + "expectResult": { + "_id": 1, + "x": 12 + } + }, + { + "name": "createEntities", + "object": "testRunner", + "arguments": { + "entities": [ + { + "session": { + "id": "session2", + "client": "client0", + "sessionOptions": { + "snapshot": true, + "snapshotTime": "savedSnapshotTime" + } + } + } + ] + } + }, + { + "name": "find", + "object": "collection0", + "arguments": { + "filter": { + "_id": 1 + } + }, + "expectResult": [ + { + "_id": 1, + "x": 12 + } + ] + }, + { + "name": "aggregate", + "object": "collection0", + "arguments": { + "pipeline": [ + { + "$match": { + "_id": 1 + } + } + ], + "session": "session2" + }, + "expectResult": [ + { + "_id": 1, + "x": 11 + } + ] + }, + { + "name": "distinct", + "object": "collection0", + "arguments": { + "fieldName": "x", + "filter": {}, + "session": "session2" + }, + "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": { + "$$matchesEntity": "savedSnapshotTime" + } + } + } + } + }, + { + "commandStartedEvent": { + "command": { + "distinct": "collection0", + "readConcern": { + "level": "snapshot", + "atClusterTime": { + "$$matchesEntity": "savedSnapshotTime" + } + } + } + } + } + ] + } + ] } ] } diff --git a/specifications/sessions/tests/snapshot-sessions.yml b/specifications/sessions/tests/snapshot-sessions.yml index bcf0f7eec6b..48cf415b4aa 100644 --- a/specifications/sessions/tests/snapshot-sessions.yml +++ b/specifications/sessions/tests/snapshot-sessions.yml @@ -309,6 +309,7 @@ tests: atClusterTime: "$$exists": true +## This test seems to be wrong - description: countDocuments operation with snapshot operations: - name: countDocuments @@ -378,7 +379,7 @@ tests: fieldName: x filter: {} session: session0 - expectResult: [ 11 ] + expectResult: [ 11 ] expectEvents: - client: client0 events: @@ -480,3 +481,410 @@ tests: isError: true isClientError: true errorContains: Transactions are not supported in snapshot sessions + +- description: Find operation with snapshot and snapshot time + operations: + - name: find + object: collection0 + arguments: + session: session0 + filter: {} + expectResult: + - { _id: 1, x: 11 } + - { _id: 2, x: 11 } + - name: getSnapshotTime + object: session0 + saveResultAsEntity: &savedSnapshotTime savedSnapshotTime + - name: insertOne + object: collection0 + arguments: + document: { _id: 3, x: 33 } + - name: createEntities + object: testRunner + arguments: + entities: + - session: + id: session2 + client: client0 + sessionOptions: + snapshot: true + snapshotTime: *savedSnapshotTime + - name: find + object: collection0 + arguments: + session: session2 + filter: {} + expectResult: + - { _id: 1, x: 11 } + - { _id: 2, x: 11 } + ## Calling find again to verify that atClusterTime/snapshotTime has not been modified after the first query + ## as it would happen if snapshotTime had not been specified + - name: find + object: collection0 + arguments: + session: session2 + filter: {} + expectResult: + - { _id: 1, x: 11 } + - { _id: 2, x: 11 } + - name: find + object: collection0 + arguments: + filter: {} + expectResult: + - { _id: 1, x: 11 } + - { _id: 2, x: 11 } + - { _id: 3, x: 33 } + expectEvents: + - client: client0 + events: + - commandStartedEvent: + command: + find: collection0 + readConcern: + level: snapshot + atClusterTime: + "$$exists": false + databaseName: database0 + - commandStartedEvent: + command: + find: collection0 + readConcern: + level: snapshot + atClusterTime: { $$matchesEntity: *savedSnapshotTime } + databaseName: database0 + - commandStartedEvent: + command: + find: collection0 + readConcern: + level: snapshot + atClusterTime: { $$matchesEntity: *savedSnapshotTime } + databaseName: database0 + - commandStartedEvent: + command: + find: collection0 + readConcern: + "$$exists": false + databaseName: database0 + +- description: Distinct operation with snapshot and snapshot time + operations: + - name: distinct + object: collection0 + arguments: + session: session0 + filter: {} + fieldName: x + expectResult: [11] + - name: getSnapshotTime + object: session0 + saveResultAsEntity: &savedSnapshotTime savedSnapshotTime + - name: insertOne + object: collection0 + arguments: + document: { _id: 3, x: 33 } + - name: createEntities + object: testRunner + arguments: + entities: + - session: + id: session2 + client: client0 + sessionOptions: + snapshot: true + snapshotTime: *savedSnapshotTime + - name: distinct + object: collection0 + arguments: + session: session2 + filter: {} + fieldName: x + expectResult: [11] + ## Calling find again to verify that atClusterTime/snapshotTime has not been modified after the first query + ## as it would happen if snapshotTime had not been specified + - name: distinct + object: collection0 + arguments: + session: session2 + filter: {} + fieldName: x + expectResult: [11] + - name: distinct + object: collection0 + arguments: + filter: {} + fieldName: x + expectResult: [11, 33] + expectEvents: + - client: client0 + events: + - commandStartedEvent: + command: + distinct: collection0 + readConcern: + level: snapshot + atClusterTime: + "$$exists": false + databaseName: database0 + - commandStartedEvent: + command: + distinct: collection0 + readConcern: + level: snapshot + atClusterTime: { $$matchesEntity: *savedSnapshotTime } + databaseName: database0 + - commandStartedEvent: + command: + distinct: collection0 + readConcern: + level: snapshot + atClusterTime: { $$matchesEntity: *savedSnapshotTime } + databaseName: database0 + - commandStartedEvent: + command: + distinct: collection0 + readConcern: + "$$exists": false + databaseName: database0 + +- description: Aggregate operation with snapshot and snapshot time + operations: + - name: aggregate + object: collection0 + arguments: + session: session0 + pipeline: + - "$match": { _id: 1 } + expectResult: + - { _id: 1, x: 11 } + - name: getSnapshotTime + object: session0 + saveResultAsEntity: &savedSnapshotTime savedSnapshotTime + - name: findOneAndUpdate + object: collection0 + arguments: + filter: { _id: 1 } + update: { $inc: { x: 1 } } + returnDocument: After + expectResult: { _id: 1, x: 12 } + - name: createEntities + object: testRunner + arguments: + entities: + - session: + id: session2 + client: client0 + sessionOptions: + snapshot: true + snapshotTime: *savedSnapshotTime + - name: aggregate + object: collection0 + arguments: + session: session2 + pipeline: + - "$match": { _id: 1 } + expectResult: + - { _id: 1, x: 11 } + ## Calling find again to verify that atClusterTime/snapshotTime has not been modified after the first query + ## as it would happen if snapshotTime had not been specified + - name: aggregate + object: collection0 + arguments: + session: session2 + pipeline: + - "$match": { _id: 1 } + expectResult: + - { _id: 1, x: 11 } + - name: aggregate + object: collection0 + arguments: + pipeline: + - "$match": { _id: 1 } + expectResult: + - { _id: 1, x: 12 } + expectEvents: + - client: client0 + events: + - commandStartedEvent: + command: + aggregate: collection0 + readConcern: + level: snapshot + atClusterTime: + "$$exists": false + databaseName: database0 + - commandStartedEvent: + command: + aggregate: collection0 + readConcern: + level: snapshot + atClusterTime: { $$matchesEntity: *savedSnapshotTime } + databaseName: database0 + - commandStartedEvent: + command: + aggregate: collection0 + readConcern: + level: snapshot + atClusterTime: { $$matchesEntity: *savedSnapshotTime } + databaseName: database0 + - commandStartedEvent: + command: + aggregate: collection0 + readConcern: + "$$exists": false + databaseName: database0 + +- description: countDocuments operation with snapshot and snapshot time + operations: + - name: countDocuments + object: collection0 + arguments: + session: session0 + filter: {} + expectResult: 2 + - name: getSnapshotTime + object: session0 + saveResultAsEntity: &savedSnapshotTime savedSnapshotTime + - name: insertOne + object: collection0 + arguments: + document: { _id: 3, x: 33 } + - name: createEntities + object: testRunner + arguments: + entities: + - session: + id: session2 + client: client0 + sessionOptions: + snapshot: true + snapshotTime: *savedSnapshotTime + - name: countDocuments + object: collection0 + arguments: + session: session2 + filter: {} + expectResult: 2 + ## Calling find again to verify that atClusterTime/snapshotTime has not been modified after the first query + ## as it would happen if snapshotTime had not been specified + - name: countDocuments + object: collection0 + arguments: + session: session2 + filter: {} + expectResult: 2 + - name: countDocuments + object: collection0 + arguments: + filter: {} + expectResult: 3 + expectEvents: + - client: client0 + events: + - commandStartedEvent: + command: + aggregate: collection0 + readConcern: + level: snapshot + atClusterTime: + "$$exists": false + databaseName: database0 + - commandStartedEvent: + command: + aggregate: collection0 + readConcern: + level: snapshot + atClusterTime: { $$matchesEntity: *savedSnapshotTime } + databaseName: database0 + - commandStartedEvent: + command: + aggregate: collection0 + readConcern: + level: snapshot + atClusterTime: { $$matchesEntity: *savedSnapshotTime } + databaseName: database0 + - commandStartedEvent: + command: + aggregate: collection0 + readConcern: + "$$exists": false + databaseName: database0 + +- description: Mixed operation with snapshot and snapshotTime + operations: + - name: find + object: collection0 + arguments: + session: session0 + filter: { _id: 1 } + expectResult: + - { _id: 1, x: 11 } + - name: getSnapshotTime + object: session0 + saveResultAsEntity: &savedSnapshotTime savedSnapshotTime + - name: findOneAndUpdate + object: collection0 + arguments: + filter: { _id: 1 } + update: { $inc: { x: 1 } } + returnDocument: After + expectResult: { _id: 1, x: 12 } + - name: createEntities + object: testRunner + arguments: + entities: + - session: + id: session2 + client: client0 + sessionOptions: + snapshot: true + snapshotTime: *savedSnapshotTime + - name: find + object: collection0 + arguments: + filter: { _id: 1 } + expectResult: + - { _id: 1, x: 12 } + - name: aggregate + object: collection0 + arguments: + pipeline: + - "$match": + _id: 1 + session: session2 + expectResult: + - { _id: 1, x: 11 } + - name: distinct + object: collection0 + arguments: + fieldName: x + filter: {} + session: session2 + 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: { $$matchesEntity: *savedSnapshotTime } + - commandStartedEvent: + command: + distinct: collection0 + readConcern: + level: snapshot + atClusterTime: { $$matchesEntity: *savedSnapshotTime } \ No newline at end of file diff --git a/src/MongoDB.Driver/ClientSessionHandle.cs b/src/MongoDB.Driver/ClientSessionHandle.cs index 144e6a94991..084c4db740b 100644 --- a/src/MongoDB.Driver/ClientSessionHandle.cs +++ b/src/MongoDB.Driver/ClientSessionHandle.cs @@ -89,6 +89,8 @@ public IServerSession ServerSession } } + public BsonTimestamp SnapshotTime => _coreSession.SnapshotTime; + /// public ICoreSessionHandle WrappedCoreSession => _coreSession; diff --git a/src/MongoDB.Driver/ClientSessionOptions.cs b/src/MongoDB.Driver/ClientSessionOptions.cs index f4b7f80ccac..3ff667ada60 100644 --- a/src/MongoDB.Driver/ClientSessionOptions.cs +++ b/src/MongoDB.Driver/ClientSessionOptions.cs @@ -14,6 +14,7 @@ */ using System; +using MongoDB.Bson; using MongoDB.Driver.Core.Bindings; namespace MongoDB.Driver @@ -46,6 +47,12 @@ public class ClientSessionOptions /// public bool Snapshot { get; set;} + /// + /// Gets or sets the snapshot time. If set, Snapshot must be true. + /// The snapshot time + /// + public BsonTimestamp SnapshotTime { get; set; } + // internal methods internal CoreSessionOptions ToCore(bool isImplicit = false) { @@ -55,7 +62,8 @@ internal CoreSessionOptions ToCore(bool isImplicit = false) isCausallyConsistent: isCausallyConsistent, isImplicit: isImplicit, isSnapshot: Snapshot, - defaultTransactionOptions: DefaultTransactionOptions); + defaultTransactionOptions: DefaultTransactionOptions, + snapshotTime: SnapshotTime); } } } diff --git a/src/MongoDB.Driver/Core/Bindings/CoreSession.cs b/src/MongoDB.Driver/Core/Bindings/CoreSession.cs index 954c639e244..d3366cdc188 100644 --- a/src/MongoDB.Driver/Core/Bindings/CoreSession.cs +++ b/src/MongoDB.Driver/Core/Bindings/CoreSession.cs @@ -71,6 +71,7 @@ private CoreSession( { _cluster = Ensure.IsNotNull(cluster, nameof(cluster)); _options = Ensure.IsNotNull(options, nameof(options)); + _snapshotTime = options.SnapshotTime; } // public properties diff --git a/src/MongoDB.Driver/Core/Bindings/CoreSessionOptions.cs b/src/MongoDB.Driver/Core/Bindings/CoreSessionOptions.cs index d5697312fcf..d0bc03f2fe6 100644 --- a/src/MongoDB.Driver/Core/Bindings/CoreSessionOptions.cs +++ b/src/MongoDB.Driver/Core/Bindings/CoreSessionOptions.cs @@ -13,6 +13,8 @@ * limitations under the License. */ +using MongoDB.Bson; + namespace MongoDB.Driver.Core.Bindings { /// @@ -25,6 +27,7 @@ public class CoreSessionOptions private readonly bool _isCausallyConsistent; private readonly bool _isImplicit; private readonly bool _isSnapshot; + private readonly BsonTimestamp _snapshotTime; // constructors /// @@ -34,16 +37,35 @@ public class CoreSessionOptions /// if set to true this session is an implicit session. /// if set to true this session is a snapshot session. /// The default transaction options. + /// The snapshot time. If this is set, isSnapshot must be true. public CoreSessionOptions( bool isCausallyConsistent = false, bool isImplicit = false, TransactionOptions defaultTransactionOptions = null, - bool isSnapshot = false) + bool isSnapshot = false, + BsonTimestamp snapshotTime = null) { _isCausallyConsistent = isCausallyConsistent; _isImplicit = isImplicit; _isSnapshot = isSnapshot; _defaultTransactionOptions = defaultTransactionOptions; + _snapshotTime = snapshotTime; + } + + /// + /// Initializes a new instance of the class. + /// + /// if set to true this session is causally consistent] + /// if set to true this session is an implicit session. + /// if set to true this session is a snapshot session. + /// The default transaction options. + public CoreSessionOptions( + bool isCausallyConsistent, + bool isImplicit, + TransactionOptions defaultTransactionOptions, + bool isSnapshot) + : this(isCausallyConsistent, isImplicit, defaultTransactionOptions, isSnapshot, null) + { } // public properties @@ -78,5 +100,10 @@ public CoreSessionOptions( /// true if this session is a snapshot session; otherwise, false. /// public bool IsSnapshot => _isSnapshot; + + /// + /// //TODO + /// + public BsonTimestamp SnapshotTime => _snapshotTime; } } diff --git a/src/MongoDB.Driver/IClientSession.cs b/src/MongoDB.Driver/IClientSession.cs index a0809fcae3c..7e17f1331ac 100644 --- a/src/MongoDB.Driver/IClientSession.cs +++ b/src/MongoDB.Driver/IClientSession.cs @@ -21,6 +21,23 @@ namespace MongoDB.Driver { + /// + /// //TODO + /// + public static class ClientSessionExtensions + { + //TODO This will need to be moved somewhere else + /// + /// //TODO + /// + /// + /// + public static BsonTimestamp GetSnapshotTime(this IClientSessionHandle session) + { + return ((ClientSessionHandle)session).SnapshotTime; + } + } + /// /// The interface for a client session. /// diff --git a/src/MongoDB.Driver/MongoClient.cs b/src/MongoDB.Driver/MongoClient.cs index 4b70bfdfd16..833991d511c 100644 --- a/src/MongoDB.Driver/MongoClient.cs +++ b/src/MongoDB.Driver/MongoClient.cs @@ -622,9 +622,17 @@ private RenderArgs GetRenderArgs() private IClientSessionHandle StartSession(ClientSessionOptions options) { - if (options != null && options.Snapshot && options.CausalConsistency == true) + if (options != null) { - throw new NotSupportedException("Combining both causal consistency and snapshot options is not supported."); + if (options.SnapshotTime != null && !options.Snapshot) + { + throw new NotSupportedException("Specifying a snapshot time requires snapshot to be true."); + } + + if (options.Snapshot && options.CausalConsistency == true) + { + throw new NotSupportedException("Combining both causal consistency and snapshot options is not supported."); + } } options ??= new ClientSessionOptions(); diff --git a/tests/MongoDB.Driver.Tests/AtClusterTimeTests.cs b/tests/MongoDB.Driver.Tests/AtClusterTimeTests.cs new file mode 100644 index 00000000000..a97e96343bf --- /dev/null +++ b/tests/MongoDB.Driver.Tests/AtClusterTimeTests.cs @@ -0,0 +1,208 @@ +/* Copyright 2010-present 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. + */ + +using System.Collections.Generic; +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; +using MongoDB.Driver.Core.Clusters; +using MongoDB.Driver.Core.Misc; +using MongoDB.Driver.TestHelpers; +using Xunit; + +namespace MongoDB.Driver.Tests; +//TODO This file will need to be deleted, but it's useful for testing at the moment +public class AtClusterTimeTests : IntegrationTest +{ + public AtClusterTimeTests(ClassFixture fixture) + : base(fixture, server => server.Supports(Feature.SnapshotReads).ClusterType(ClusterType.ReplicaSet)) + { + } + + [Fact] + public void MainTest() + { + var client = Fixture.Client; + var collection = Fixture.Collection; + + BsonTimestamp clusterTime1; + + var sessionOptions1 = new ClientSessionOptions + { + Snapshot = true + }; + + using (var session1 = client.StartSession(sessionOptions1)) + { + var results = GetTestObjects(collection, session1); + AssertOneObj(results); + + clusterTime1 = session1.GetSnapshotTime(); + Assert.NotEqual(null, clusterTime1); + } + + var obj2 = new TestObject { Name = "obj2" }; + collection.InsertOne(obj2); + + var sessionOptions2 = new ClientSessionOptions + { + Snapshot = true, + SnapshotTime = clusterTime1 + }; + + //Snapshot read session at clusterTime1 should not see obj2 + using (var session2 = client.StartSession(sessionOptions2)) + { + var results = GetTestObjects(collection, session2); + AssertOneObj(results); + + var clusterTime2 = session2.GetSnapshotTime(); + Assert.Equal(clusterTime2, clusterTime1); + } + + var sessionOptions3 = new ClientSessionOptions + { + Snapshot = true, + }; + + //Snapshot read session without cluster time should see obj2 + using (var session3 = client.StartSession(sessionOptions3)) + { + var results = GetTestObjects(collection, session3); + AssertTwoObjs(results); + + var clusterTime3 = session3.GetSnapshotTime(); + Assert.NotEqual(clusterTime3, clusterTime1); + } + } + + [Fact] + public void IncreasedTimestamp() + { + var client = Fixture.Client; + var collection = Fixture.Collection; + + BsonTimestamp clusterTime1; + + var sessionOptions1 = new ClientSessionOptions + { + Snapshot = true + }; + + using (var session1 = client.StartSession(sessionOptions1)) + { + var results = GetTestObjects(collection, session1); + AssertOneObj(results); + + clusterTime1 = session1.GetSnapshotTime(); + Assert.NotEqual(null, clusterTime1); + } + + var obj2 = new TestObject { Name = "obj2" }; + collection.InsertOne(obj2); + + var modifiedClusterTime = new BsonTimestamp(clusterTime1.Value + 1); + var sessionOptions2 = new ClientSessionOptions + { + Snapshot = true, + SnapshotTime = modifiedClusterTime + }; + + //Snapshot read session at clusterTime1+1 should see obj2 + using (var session2 = client.StartSession(sessionOptions2)) + { + var results = GetTestObjects(collection, session2); + AssertTwoObjs(results); + + var clusterTime2 = session2.GetSnapshotTime(); + Assert.Equal(modifiedClusterTime, clusterTime2); + } + } + + [Fact] + public void DecreasedTimestamp() + { + var client = Fixture.Client; + var collection = Fixture.Collection; + + BsonTimestamp clusterTime1; + + var sessionOptions1 = new ClientSessionOptions + { + Snapshot = true + }; + + using (var session1 = client.StartSession(sessionOptions1)) + { + var results = GetTestObjects(collection, session1); + AssertOneObj(results); + + clusterTime1 = session1.GetSnapshotTime(); + Assert.NotEqual(null, clusterTime1); + } + + var obj2 = new TestObject { Name = "obj2" }; + collection.InsertOne(obj2); + + var modifiedClusterTime = new BsonTimestamp(clusterTime1.Value - 1); + var sessionOptions2 = new ClientSessionOptions + { + Snapshot = true, + SnapshotTime = modifiedClusterTime + }; + + //Snapshot read session at clusterTime1-1 should not see obj2 + using (var session2 = client.StartSession(sessionOptions2)) + { + var results = GetTestObjects(collection, session2); + Assert.Equal(0, results.Count); + + var clusterTime2 = session2.GetSnapshotTime(); + Assert.Equal(modifiedClusterTime, clusterTime2); + } + } + + List GetTestObjects(IMongoCollection collection, IClientSessionHandle session) + { + var filterDefinition = Builders.Filter.Empty; + var sortDefinition = Builders.Sort.Ascending(o => o.Name); + return collection.Find(session, filterDefinition).Sort(sortDefinition).ToList(); + } + + void AssertOneObj(List objs) + { + Assert.Equal(1, objs.Count); + Assert.Equal("obj1", objs[0].Name); + } + + void AssertTwoObjs(List objs) + { + Assert.Equal(2, objs.Count); + Assert.Equal("obj1", objs[0].Name); + Assert.Equal("obj2", objs[1].Name); + } + + public class ClassFixture : MongoCollectionFixture + { + public override bool InitializeDataBeforeEachTestCase => true; + protected override IEnumerable InitialData => [new() { Name = "obj1" }] ; + } + + public class TestObject + { + [BsonId] + public ObjectId Id { get; set; } + public string Name { get; set; } + } +} \ No newline at end of file diff --git a/tests/MongoDB.Driver.Tests/Specifications/sessions/SessionsProseTests.cs b/tests/MongoDB.Driver.Tests/Specifications/sessions/SessionsProseTests.cs index 649e2ae2df9..40244a88523 100644 --- a/tests/MongoDB.Driver.Tests/Specifications/sessions/SessionsProseTests.cs +++ b/tests/MongoDB.Driver.Tests/Specifications/sessions/SessionsProseTests.cs @@ -357,6 +357,24 @@ public void Ensure_cluster_times_are_not_gossiped_on_SDAM_commands() commandStartedEvents[0].Command["$clusterTime"].Should().Be(clusterTime); } + [Fact] + public void If_SnapshotTime_is_set_Snapshot_must_be_true() + { + RequireServer.Check(); + + var sessionOptions = new ClientSessionOptions + { + Snapshot = false, + SnapshotTime = new BsonTimestamp(1, 1) + }; + + var mongoClient = DriverTestConfiguration.Client; + + var exception = Record.Exception(() => mongoClient.StartSession(sessionOptions)); + exception.Should().BeOfType(); + } + + private sealed class MongocryptdContext : IDisposable { public IMongoClient MongoClient { get; } diff --git a/tests/MongoDB.Driver.Tests/UnifiedTestOperations/UnifiedEntityMap.cs b/tests/MongoDB.Driver.Tests/UnifiedTestOperations/UnifiedEntityMap.cs index e5ea553ff15..cea7cc56391 100644 --- a/tests/MongoDB.Driver.Tests/UnifiedTestOperations/UnifiedEntityMap.cs +++ b/tests/MongoDB.Driver.Tests/UnifiedTestOperations/UnifiedEntityMap.cs @@ -1056,6 +1056,9 @@ private IClientSessionHandle CreateSession(BsonDocument entity, Dictionary GetSnapshotTime(); + + //TODO Do we necessarily need an async version of this...? + public Task ExecuteAsync(CancellationToken cancellationToken) => Task.FromResult(GetSnapshotTime()); + + private OperationResult GetSnapshotTime() + { + try + { + return OperationResult.FromResult(_session.GetSnapshotTime()); + } + catch (Exception exception) + { + return OperationResult.FromException(exception); + } + } +} + +public class UnifiedGetSnapshotOperationBuilder +{ + private readonly UnifiedEntityMap _entityMap; + + public UnifiedGetSnapshotOperationBuilder(UnifiedEntityMap entityMap) + { + _entityMap = entityMap; + } + + public UnifiedGetSnapshotOperation Build(string targetSessionId, BsonDocument arguments) + { + if (arguments != null) + { + throw new FormatException("GetSnapshotTime is not expected to contain arguments."); + } + + var session = _entityMap.Sessions[targetSessionId]; + return new UnifiedGetSnapshotOperation(session); + } +} \ No newline at end of file diff --git a/tests/MongoDB.Driver.Tests/UnifiedTestOperations/UnifiedTestOperationFactory.cs b/tests/MongoDB.Driver.Tests/UnifiedTestOperations/UnifiedTestOperationFactory.cs index 32e6ec08f3d..099ae07359f 100644 --- a/tests/MongoDB.Driver.Tests/UnifiedTestOperations/UnifiedTestOperationFactory.cs +++ b/tests/MongoDB.Driver.Tests/UnifiedTestOperations/UnifiedTestOperationFactory.cs @@ -133,6 +133,7 @@ public IUnifiedTestOperation CreateOperation(string operationName, string target "abortTransaction" => new UnifiedAbortTransactionOperationBuilder(_entityMap).Build(targetEntityId, operationArguments), "commitTransaction" => new UnifiedCommitTransactionOperationBuilder(_entityMap).Build(targetEntityId, operationArguments), "endSession" => new UnifiedEndSessionOperationBuilder(_entityMap).Build(targetEntityId, operationArguments), + "getSnapshotTime" => new UnifiedGetSnapshotOperationBuilder(_entityMap).Build(targetEntityId, operationArguments), "startTransaction" => new UnifiedStartTransactionOperationBuilder(_entityMap).Build(targetEntityId, operationArguments), "withTransaction" => new UnifiedWithTransactionOperationBuilder(_entityMap).Build(targetEntityId, operationArguments), _ => throw new FormatException($"Invalid method name: '{operationName}'."),