Skip to content

Conversation

@prestonvasquez
Copy link
Member

@prestonvasquez prestonvasquez commented Dec 18, 2025

GODRIVER-3663

Summary

The snapshot session specification states that drivers should store the atClusterTime value in a private field, but there are many instances where an application may want to set/access the value.


Add T.Setup() and T.Teardown() to mtest to allow using mtest on one-off tests without subtests.


Add assert.Empty and require.Empty to our vendored testify code.


⚠️This PR changes the function signature of mongo#Session.ClientSession ⚠️

This change is required to prevent users from mutating the client session, which can lead to dangerous behavior. This function is deprecated with the warning "It may be changed or removed in any release." Regardless, we should consider the existing definition as a bug.

Background & Motivation

Users of a driver that would like to take advantage of snapshot reads.

@mongodb-drivers-pr-bot
Copy link
Contributor

mongodb-drivers-pr-bot bot commented Dec 18, 2025

🧪 Performance Results

Commit SHA: 956bb6f

The following benchmark tests for version 6945cc77c7a4cf0007e1148f had statistically significant changes (i.e., |z-score| > 1.96):

Benchmark Measurement % Change Patch Value Stable Region H-Score Z-Score
BenchmarkBSONFullDocumentDecoding ops_per_second_min 18.3665 2322.8372 Avg: 1962.4104
Med: 1974.2348
Stdev: 163.9313
0.7509 2.1986
BenchmarkSingleFindOneByID total_bytes_allocated 11.2891 114182960.0000 Avg: 102600303.0283
Med: 103269840.0000
Stdev: 5002929.8830
0.7638 2.3152
BenchmarkSingleFindOneByID total_mem_allocs 10.3586 1830359.0000 Avg: 1658555.9838
Med: 1669130.0000
Stdev: 80881.6123
0.7441 2.1241
BenchmarkMultiInsertSmallDocument ns_per_op -6.0182 5992.0000 Avg: 6375.7027
Med: 6356.0000
Stdev: 168.7712
0.7559 -2.2735
BenchmarkBSONFullDocumentEncoding ns_per_op -5.2214 21987.0000 Avg: 23198.2778
Med: 23245.5000
Stdev: 405.8251
0.8195 -2.9847
BenchmarkBSONFlatDocumentDecoding total_mem_allocs 4.8688 11116866.0000 Avg: 10600741.1944
Med: 10634846.5000
Stdev: 219129.6184
0.7645 2.3553
BenchmarkBSONFlatDocumentDecoding total_bytes_allocated 4.8596 437166288.0000 Avg: 416906139.1111
Med: 418242736.0000
Stdev: 8601735.8789
0.7645 2.3554
BenchmarkBSONFullDocumentEncoding ops_per_second_med 4.8520 48744.8209 Avg: 46489.1536
Med: 46559.2704
Stdev: 754.1023
0.8195 2.9912
BenchmarkBSONFullDocumentDecoding total_bytes_allocated 4.8402 427981592.0000 Avg: 408222700.6667
Med: 408157768.0000
Stdev: 8301398.3527
0.7670 2.3802
BenchmarkBSONFullDocumentDecoding total_mem_allocs 4.8101 10341093.0000 Avg: 9866507.6944
Med: 9865830.0000
Stdev: 199712.8087
0.7667 2.3763
BenchmarkBSONFlatDocumentDecoding ns_per_op -4.7813 49512.0000 Avg: 51998.1944
Med: 51960.0000
Stdev: 1048.3345
0.7642 -2.3716
BenchmarkBSONFullDocumentDecoding ns_per_op -4.4886 70908.0000 Avg: 74240.3889
Med: 74086.0000
Stdev: 1597.8806
0.7412 -2.0855
BenchmarkBSONFlatDocumentDecoding ops_per_second_med 4.3852 21642.6794 Avg: 20733.4765
Med: 20734.8430
Stdev: 372.7739
0.7713 2.4390
BenchmarkBSONFullDocumentEncoding total_mem_allocs 4.0217 1610476.0000 Avg: 1548212.1389
Med: 1543308.5000
Stdev: 24399.7868
0.7863 2.5518
BenchmarkBSONFullDocumentEncoding total_bytes_allocated 3.9979 278542680.0000 Avg: 267834976.6667
Med: 267025292.0000
Stdev: 4193825.8804
0.7861 2.5532
BenchmarkBSONFullDocumentDecoding ops_per_second_med 3.9857 15118.3007 Avg: 14538.8331
Med: 14552.1758
Stdev: 261.8271
0.7609 2.2132
BenchmarkMultiFindMany ops_per_second_max 3.8715 4385964.9123 Avg: 4222492.2161
Med: 4237288.1356
Stdev: 51511.4882
0.8353 3.1735
BenchmarkBSONFlatDocumentDecoding ops_per_second_max 3.7007 22434.6031 Avg: 21633.9913
Med: 21628.4035
Stdev: 332.8740
0.7681 2.4051
BenchmarkMultiFindMany ops_per_second_med 3.5791 3861003.8610 Avg: 3727589.2679
Med: 3731343.2836
Stdev: 43634.5121
0.8201 3.0575
BenchmarkBSONFullDocumentDecoding ops_per_second_max 3.3595 15659.7451 Avg: 15150.7564
Med: 15157.1417
Stdev: 170.0687
0.8335 2.9928
BenchmarkBSONDeepDocumentDecoding ns_per_op -3.2716 61247.0000 Avg: 63318.5000
Med: 63227.5000
Stdev: 821.4374
0.7874 -2.5218
BenchmarkBSONFullDocumentEncoding ops_per_second_max 3.2381 49785.9205 Avg: 48224.3729
Med: 48151.0015
Stdev: 678.1645
0.7747 2.3026
BenchmarkBSONDeepDocumentDecoding total_mem_allocs 2.8700 13532863.0000 Avg: 13155302.8056
Med: 13182572.0000
Stdev: 157371.9468
0.7787 2.3992
BenchmarkBSONDeepDocumentDecoding total_bytes_allocated 2.8646 293994608.0000 Avg: 285807409.5556
Med: 286397724.0000
Stdev: 3409986.4909
0.7789 2.4009

For a comprehensive view of all microbenchmark results for this PR's commit, please check out the Evergreen perf task for this patch.

@mongodb-drivers-pr-bot
Copy link
Contributor

API Change Report

./v2/mongo

incompatible changes

##(*Session).ClientSession: changed from func() *./v2/x/mongo/driver/session.Client to func() ./v2/x/mongo/driver/session.Client

./v2/mongo/options

compatible changes

(*SessionOptionsBuilder).SetSnapshotTime: added
SessionOptions.SnapshotTime: added

./v2/x/mongo/driver/session

incompatible changes

##Client.SnapshotTime: changed from *./v2/bson.Timestamp to ./v2/bson.Timestamp

compatible changes

Client.SnapshotTimeSet: added
ClientOptions.SnapshotTime: added

@prestonvasquez prestonvasquez changed the title GODRIVER-366 Expose atClusterTime parameter in snapshot sessions GODRIVER-3663 Expose atClusterTime parameter in snapshot sessions Dec 19, 2025
}()

// Expect this call to panic.
sess.ClientSession().SetServer()
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't possible now.

@prestonvasquez prestonvasquez marked this pull request as ready for review December 19, 2025 02:52
@prestonvasquez prestonvasquez requested a review from a team as a code owner December 19, 2025 02:52
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR exposes the atClusterTime parameter for snapshot sessions by adding a SnapshotTime option that allows users to set or access snapshot read timestamps. The change addresses GODRIVER-3663 and includes test infrastructure improvements.

Key changes:

  • Added SnapshotTime field to session options and client session struct, making it immutable once set
  • Changed Session.ClientSession() to return a copy instead of a pointer to prevent mutation
  • Enhanced test utilities with Setup()/Teardown() methods and added Empty assertion functions

Reviewed changes

Copilot reviewed 19 out of 19 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
x/mongo/driver/session/options.go Added SnapshotTime field to ClientOptions
x/mongo/driver/session/client_session.go Moved SnapshotTime from pointer to value type with immutability flag
x/mongo/driver/operation.go Updated snapshot time check to use SnapshotTimeSet flag
mongo/session.go Changed ClientSession() to return copy instead of pointer
mongo/options/sessionoptions.go Added SetSnapshotTime() builder method
mongo/client.go Wired up SnapshotTime option in StartSession and cleaned up formatting
internal/spectest/skip.go Removed skipped tests for GODRIVER-3663
internal/require/require.go Added Empty() assertion function
internal/integration/unified_spec_test.go Updated to use session copy and improved session validation
internal/integration/unified/testrunner_operation.go Updated function signature for session copy
internal/integration/unified/session_options.go Added snapshotTimeID field for entity lookup
internal/integration/unified/session_operation_execution.go Implemented getSnapshotTime operation
internal/integration/unified/operation.go Added getSnapshotTime case handler
internal/integration/unified/matches.go Changed variable declarations to use short form
internal/integration/unified/entity.go Enhanced addBSONEntity to handle any type and added snapshot time resolution
internal/integration/sessions_test.go Added prose tests for snapshot time behavior
internal/integration/mtest/mongotest.go Refactored Setup/Teardown into standalone methods
internal/integration/mongointernal_test.go Removed test that relied on pointer mutation
internal/assert/assertions.go Added Empty() assertion and removed extra blank lines

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 19 out of 19 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 19 out of 19 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 19 out of 19 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 19 out of 19 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 19 out of 19 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 19 out of 19 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@prestonvasquez prestonvasquez force-pushed the enhancement/godriver-3663-expose-atClusterTime branch from a1ffbe6 to 3bbb4d6 Compare December 19, 2025 03:27
Introduce Setup/Teardown helpers to centralize mtest client/collection initialization and cleanup.

RunOpts now calls Setup/Teardown automatically, reducing duplicated setup logic and making manual New() usage clearer.
Add Empty assertions to the internal assert/require packages.

This allows tests to assert "unset" for value types without relying on pointer-only checks and keeps assertions consistent with existing zero-value semantics.
Change Session.ClientSession to return a copy of the underlying driver session to prevent mutation of internal session state.

Update internal test helpers for value semantics, add explicit session/pinning guards for testRunner ops, and remove a mongointernal panic test that no longer applies.
Expose snapshotTime on SessionOptions and plumb it through StartSession into the driver session.

Driver sessions track snapshotTime as an immutable value (SnapshotTimeSet), validate snapshotTime cannot be provided when snapshot=false, and include atClusterTime in readConcern when set.
Extend the unified spec runner to round-trip snapshotTime values through entities.

Parse sessionOptions.snapshotTime as an entity reference during entity creation and add a getSnapshotTime operation to store a session's snapshotTime as a BSON entity.
Remove GODRIVER-3663 skip list entries for snapshotTime snapshot session tests now that the driver and unified runner support them.
Add prose tests covering snapshotTime session option behavior.

Validate StartSession rejects snapshotTime when snapshot=false, and verify Session.ClientSession() returns a defensive copy by demonstrating snapshotTime cannot be mutated through the returned value.

Prose 22 is skipped because the Go driver does not expose a snapshotTime getter that errors for non-snapshot sessions.
@prestonvasquez prestonvasquez force-pushed the enhancement/godriver-3663-expose-atClusterTime branch from 3bbb4d6 to a4b00c5 Compare December 19, 2025 03:39
t.T.Run(name, func(wrapped *testing.T) {
sub := newT(wrapped, t.baseOpts, opts)
sub.Setup()
defer sub.Teardown()
Copy link
Collaborator

@matthewdale matthewdale Dec 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of deferring this call, use T.Cleanup so we never have to explicitly call it. Ideally call it in Setup. Also, you can unexport Teardown since it's only called internally.

E.g.

func (t *T) Setup() {
	// ...
	t.T.Cleanup(t.teardown())
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Teardown and Setup were added in this PR so that I could use mtest with one-off non-subtest tests:

func TestSessionsProse_21_SettingSnapshotTimeWithoutSnapshot(t *testing.T) {
	mt := mtest.New(t, mtOpts)

	mt.Setup()
	defer mt.Teardown()
}

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are there any cases where you call mt.Setup() and not call mt.Teardown() after the test? If not, setting mt.Teardown() as a test cleanup task means you don't have to remember to call it, preventing a possible programming error.

Comment on lines +593 to +595
mt := mtest.New(t, mtOpts)

mt.Setup()
Copy link
Collaborator

@matthewdale matthewdale Dec 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional: Consider combining these two calls into a new function to make it easier to use, like mtest.Setup. We can reconcile the two entry points in GODRIVER-3656.

E.g.

func TestBlah(...) {
	mt := mtest.Setup(t, mtOpts)
	// ...
}
package mtest

func Setup(...) *T {
	mt := mtest.New(...)
	mt.setup()
	// ...
	return mt
}

Copy link
Member Author

@prestonvasquez prestonvasquez Dec 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggest deferring to GODRIVER-3721 / GODRIVER-3656, which should end up removing Setup entirely.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good. Consider this resolved.

Comment on lines +83 to +88
func (s *Session) ClientSession() session.Client {
if s.clientSession == nil {
return session.Client{}
}

return *s.clientSession // Return a copy to prevent mutation.
Copy link
Collaborator

@matthewdale matthewdale Dec 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change doesn't make a deep copy, so many fields can still be mutated. I'm also concerned that this may break some internal use case that requires modifying session.Client values.

Do you have a concrete example of what we're trying to avoid with this change? Are there examples of external users doing that?

Edit: I see the description links to this comment as an example of dangerous behavior, but that seems like behavior we're facilitating by adding SetSnapshotTime. What does it have to do with returning a copy from Session.ClientSession?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change doesn't make a deep copy

The intent is to prevent mutation of the live session’s top-level state through this deprecated accessor. A deep copy seems unnecessary at the moment.

I'm also concerned that this may break some internal use case that requires modifying session.Client values.

IIRC the only reason we export this accessor is for testing. Letting callers mutate the underlying configuration bypasses invariants and can create bugs that are very difficult to reason about.

We should create a follow-up ticket to use a build tag to hide ClientSession from non-testing environments.

Edit: I see the description links to this comment as an example of dangerous behavior, but that seems like behavior we're facilitating by adding SetSnapshotTime. What does it have to do with returning a copy from Session.ClientSession?

Snapshot sessions cannot be used in transactions, so this conflict scenario doesn't apply to snapshotTime on snapshot sessions. However, if Session.ClientSession returns a pointer then users could set snapshotTime with snapshot=false which could lead to the conflict scenario.

Copy link
Collaborator

@matthewdale matthewdale Dec 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense. In that case, I believe we can remove the ClientSession method completely and use reflection to access it for testing, similar to how we extract a Topology from a Client.

E.g.

func getClientSessionFromSession(sess *mongo.Session) *session.Client {
	elem := reflect.ValueOf(sess).Elem()
	field := elem.FieldByName("clientSession")
	field = reflect.NewAt(field.Type(), unsafe.Pointer(field.UnsafeAddr())).Elem()
	return field.Interface().(*session.Client)
}

I also did a 3rd party code search for uses of ClientSession and found no uses.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement review-priority-normal Medium Priority PR for Review: within 1 business day

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants