Skip to content

Commit b15712e

Browse files
RUBY-3712 Expose atClusterTime in snapshot sessions (#3035)
1 parent e2efaeb commit b15712e

6 files changed

Lines changed: 499 additions & 9 deletions

File tree

lib/mongo/operation/shared/executable.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ def do_execute(connection, context, options = {})
5959
end
6060
end
6161

62-
if session.snapshot? && !session.snapshot_timestamp
62+
if session.snapshot?
6363
session.snapshot_timestamp = result.snapshot_timestamp
6464
end
6565
end

lib/mongo/session.rb

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ class Session
7474
# and *:nearest*.
7575
# @option options [ true | false ] :snapshot Set up the session for
7676
# snapshot reads.
77+
# @option options [ BSON::Timestamp ] :snapshot_time The desired snapshot
78+
# time for snapshot reads. Only valid when :snapshot is true.
7779
#
7880
# @since 2.5.0
7981
# @api private
@@ -82,6 +84,14 @@ def initialize(server_session, client, options = {})
8284
raise ArgumentError, ':causal_consistency and :snapshot options cannot be both set on a session'
8385
end
8486

87+
if options[:snapshot_time] && !options[:snapshot]
88+
raise ArgumentError, ':snapshot_time can only be set when :snapshot is true'
89+
end
90+
91+
if options[:snapshot_time] && !options[:snapshot_time].is_a?(BSON::Timestamp)
92+
raise ArgumentError, ':snapshot_time must be a BSON::Timestamp'
93+
end
94+
8595
if options[:implicit]
8696
unless server_session.nil?
8797
raise ArgumentError, 'Implicit session cannot reference server session during construction'
@@ -104,6 +114,7 @@ def initialize(server_session, client, options = {})
104114
@with_transaction_deadline = nil
105115
@with_transaction_timeout_ms = nil
106116
@inside_with_transaction = false
117+
@snapshot_timestamp = options[:snapshot_time]
107118
end
108119

109120
# @return [ Hash ] The options for this session.
@@ -1273,8 +1284,23 @@ def txn_num
12731284
@server_session.txn_num
12741285
end
12751286

1287+
# @return [ BSON::Timestamp | nil ] The snapshot time for this session.
1288+
# nil if the session is not a snapshot session, or if it is a snapshot
1289+
# session for which no :snapshot_time option was provided and no read
1290+
# has yet captured atClusterTime from the server.
1291+
attr_reader :snapshot_timestamp
1292+
1293+
# Sets the snapshot time for the session. Once set, subsequent
1294+
# assignments are ignored: snapshotTime is established at most once per
1295+
# session, either from the :snapshot_time option at construction or from
1296+
# the atClusterTime returned by the first find/aggregate/distinct
1297+
# response. This keeps the property effectively read-only for callers,
1298+
# per the snapshot-sessions spec rationale.
1299+
#
12761300
# @api private
1277-
attr_accessor :snapshot_timestamp
1301+
def snapshot_timestamp=(value)
1302+
@snapshot_timestamp ||= value
1303+
end
12781304

12791305
# @return [ Integer | nil ] The deadline for the current transaction, if any.
12801306
# @api private

spec/mongo/session_spec.rb

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,53 @@
3232
it 'sets the cluster' do
3333
expect(session.cluster).to be(authorized_client.cluster)
3434
end
35+
36+
context 'when :snapshot_time is set without :snapshot' do
37+
let(:options) do
38+
{ snapshot_time: BSON::Timestamp.new(0, 1) }
39+
end
40+
41+
it 'raises ArgumentError' do
42+
expect { session }.to raise_error(
43+
ArgumentError, /:snapshot_time can only be set when :snapshot is true/
44+
)
45+
end
46+
end
47+
48+
context 'when :snapshot_time is not a BSON::Timestamp' do
49+
let(:options) do
50+
{ snapshot: true, snapshot_time: 12_345 }
51+
end
52+
53+
it 'raises ArgumentError' do
54+
expect { session }.to raise_error(
55+
ArgumentError, /:snapshot_time must be a BSON::Timestamp/
56+
)
57+
end
58+
end
59+
60+
context 'when :snapshot_time is set with :snapshot' do
61+
let(:timestamp) { BSON::Timestamp.new(42, 7) }
62+
63+
let(:options) do
64+
{ snapshot: true, snapshot_time: timestamp }
65+
end
66+
67+
it 'exposes the timestamp via snapshot_timestamp' do
68+
expect(session.snapshot_timestamp).to eq(timestamp)
69+
end
70+
end
71+
end
72+
73+
describe '#snapshot_timestamp=' do
74+
let(:initial_timestamp) { BSON::Timestamp.new(1, 1) }
75+
let(:later_timestamp) { BSON::Timestamp.new(2, 2) }
76+
let(:options) { { snapshot: true, snapshot_time: initial_timestamp } }
77+
78+
it 'is a no-op once a snapshot timestamp is set' do
79+
session.snapshot_timestamp = later_timestamp
80+
expect(session.snapshot_timestamp).to eq(initial_timestamp)
81+
end
3582
end
3683

3784
describe '#inspect' do

spec/runners/unified/support_operations.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,11 @@ def end_session(op)
6868
session.end_session
6969
end
7070

71+
def get_snapshot_time(op)
72+
session = entities.get(:session, op.use!('object'))
73+
session.snapshot_timestamp
74+
end
75+
7176
def assert_session_dirty(op)
7277
consume_test_runner(op)
7378
use_arguments(op) do |args|

spec/runners/unified/test.rb

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -285,12 +285,7 @@ def generate_entities(es)
285285
database.fs
286286
when 'session'
287287
client = entities.get(:client, spec.use!('client'))
288-
289-
opts = if smc_opts = spec.use('sessionOptions')
290-
::Utils.underscore_hash(smc_opts)
291-
else
292-
{}
293-
end
288+
opts = build_session_options(spec)
294289

295290
client.start_session(**opts).tap do |session|
296291
session.advance_cluster_time(@cluster_time)
@@ -380,6 +375,20 @@ class << thread
380375
@entities_created = true
381376
end
382377

378+
# Builds the keyword options for Client#start_session from a session
379+
# entity spec. When sessionOptions contains snapshotTime, the value is the
380+
# name of a previously saved entity holding the actual BSON::Timestamp.
381+
def build_session_options(spec)
382+
smc_opts = spec.use('sessionOptions')
383+
return {} unless smc_opts
384+
385+
opts = ::Utils.underscore_hash(smc_opts)
386+
if opts[:snapshot_time].is_a?(String)
387+
opts[:snapshot_time] = entities.get(:result, opts[:snapshot_time])
388+
end
389+
opts
390+
end
391+
383392
def set_initial_data
384393
@spec['initialData']&.each do |entity_spec|
385394
spec = UsingHash[entity_spec]

0 commit comments

Comments
 (0)