Skip to content
804 changes: 804 additions & 0 deletions specifications/sessions/tests/snapshot-sessions.json

Large diffs are not rendered by default.

410 changes: 409 additions & 1 deletion specifications/sessions/tests/snapshot-sessions.yml

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions src/MongoDB.Driver/ClientSessionHandle.cs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ public IServerSession ServerSession
}
}

public BsonTimestamp SnapshotTime => _coreSession.SnapshotTime;

/// <inheritdoc />
public ICoreSessionHandle WrappedCoreSession => _coreSession;

Expand Down
10 changes: 9 additions & 1 deletion src/MongoDB.Driver/ClientSessionOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
*/

using System;
using MongoDB.Bson;
using MongoDB.Driver.Core.Bindings;

namespace MongoDB.Driver
Expand Down Expand Up @@ -46,6 +47,12 @@ public class ClientSessionOptions
/// </value>
public bool Snapshot { get; set;}

/// <summary>
/// Gets or sets the snapshot time. If set, Snapshot must be true.
/// <value> The snapshot time </value>
/// </summary>
public BsonTimestamp SnapshotTime { get; set; }

// internal methods
internal CoreSessionOptions ToCore(bool isImplicit = false)
{
Expand All @@ -55,7 +62,8 @@ internal CoreSessionOptions ToCore(bool isImplicit = false)
isCausallyConsistent: isCausallyConsistent,
isImplicit: isImplicit,
isSnapshot: Snapshot,
defaultTransactionOptions: DefaultTransactionOptions);
defaultTransactionOptions: DefaultTransactionOptions,
snapshotTime: SnapshotTime);
}
}
}
1 change: 1 addition & 0 deletions src/MongoDB.Driver/Core/Bindings/CoreSession.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ private CoreSession(
{
_cluster = Ensure.IsNotNull(cluster, nameof(cluster));
_options = Ensure.IsNotNull(options, nameof(options));
_snapshotTime = options.SnapshotTime;
}

// public properties
Expand Down
29 changes: 28 additions & 1 deletion src/MongoDB.Driver/Core/Bindings/CoreSessionOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
* limitations under the License.
*/

using MongoDB.Bson;

namespace MongoDB.Driver.Core.Bindings
{
/// <summary>
Expand All @@ -25,6 +27,7 @@ public class CoreSessionOptions
private readonly bool _isCausallyConsistent;
private readonly bool _isImplicit;
private readonly bool _isSnapshot;
private readonly BsonTimestamp _snapshotTime;

// constructors
/// <summary>
Expand All @@ -34,16 +37,35 @@ public class CoreSessionOptions
/// <param name="isImplicit">if set to <c>true</c> this session is an implicit session.</param>
/// <param name="isSnapshot">if set to <c>true</c> this session is a snapshot session.</param>
/// <param name="defaultTransactionOptions">The default transaction options.</param>
/// <param name="snapshotTime">The snapshot time. If this is set, isSnapshot must be true.</param>
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;
}

/// <summary>
/// Initializes a new instance of the <see cref="CoreSessionOptions" /> class.
/// </summary>
/// <param name="isCausallyConsistent">if set to <c>true</c> this session is causally consistent]</param>
/// <param name="isImplicit">if set to <c>true</c> this session is an implicit session.</param>
/// <param name="isSnapshot">if set to <c>true</c> this session is a snapshot session.</param>
/// <param name="defaultTransactionOptions">The default transaction options.</param>
public CoreSessionOptions(
bool isCausallyConsistent,
bool isImplicit,
TransactionOptions defaultTransactionOptions,
bool isSnapshot)
: this(isCausallyConsistent, isImplicit, defaultTransactionOptions, isSnapshot, null)
{
}

// public properties
Expand Down Expand Up @@ -78,5 +100,10 @@ public CoreSessionOptions(
/// <c>true</c> if this session is a snapshot session; otherwise, <c>false</c>.
/// </value>
public bool IsSnapshot => _isSnapshot;

/// <summary>
/// //TODO
/// </summary>
public BsonTimestamp SnapshotTime => _snapshotTime;
}
}
17 changes: 17 additions & 0 deletions src/MongoDB.Driver/IClientSession.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,23 @@

namespace MongoDB.Driver
{
/// <summary>
/// //TODO
/// </summary>
public static class ClientSessionExtensions
{
//TODO This will need to be moved somewhere else
/// <summary>
/// //TODO
/// </summary>
/// <param name="session"></param>
/// <returns></returns>
public static BsonTimestamp GetSnapshotTime(this IClientSessionHandle session)
{
return ((ClientSessionHandle)session).SnapshotTime;
}
}

/// <summary>
/// The interface for a client session.
/// </summary>
Expand Down
12 changes: 10 additions & 2 deletions src/MongoDB.Driver/MongoClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -622,9 +622,17 @@ private RenderArgs<BsonDocument> 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();
Expand Down
208 changes: 208 additions & 0 deletions tests/MongoDB.Driver.Tests/AtClusterTimeTests.cs
Original file line number Diff line number Diff line change
@@ -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<AtClusterTimeTests.ClassFixture>
{
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<TestObject> GetTestObjects(IMongoCollection<TestObject> collection, IClientSessionHandle session)
{
var filterDefinition = Builders<TestObject>.Filter.Empty;
var sortDefinition = Builders<TestObject>.Sort.Ascending(o => o.Name);
return collection.Find(session, filterDefinition).Sort(sortDefinition).ToList();
}

void AssertOneObj(List<TestObject> objs)
{
Assert.Equal(1, objs.Count);
Assert.Equal("obj1", objs[0].Name);
}

void AssertTwoObjs(List<TestObject> objs)
{
Assert.Equal(2, objs.Count);
Assert.Equal("obj1", objs[0].Name);
Assert.Equal("obj2", objs[1].Name);
}

public class ClassFixture : MongoCollectionFixture<TestObject>
{
public override bool InitializeDataBeforeEachTestCase => true;
protected override IEnumerable<TestObject> InitialData => [new() { Name = "obj1" }] ;
}

public class TestObject
{
[BsonId]
public ObjectId Id { get; set; }
public string Name { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<NotSupportedException>();
}


private sealed class MongocryptdContext : IDisposable
{
public IMongoClient MongoClient { get; }
Expand Down
Loading
Loading