Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GODRIVER-3217 Use the manually-specified maxTimeMS on Find and Aggregate if it would be ommitted by CSOT #1644

Merged
merged 2 commits into from
Jun 12, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions mongo/integration/csot_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,46 @@ func TestCSOT(t *testing.T) {
bsoncore.ErrElementNotFound,
"expected maxTimeMS BSON value to be missing, but is present")
})

// TODO(GODRIVER-2944): Remove this test once the "timeoutMode" option is
// supported.
mt.RunOpts("Find always sends manually-specified maxTimeMS", csotOpts, func(mt *mtest.T) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

This test is being run with a client-level timeout of 10 seconds, set by previous subtests. The csotOpts should be reset before executing to test that the deprecated operation-level maxTimeMS value is being propagated to command message when omitting CSOT.

Suggested change
mt.RunOpts("Find always sends manually-specified maxTimeMS", csotOpts, func(mt *mtest.T) {
csotOpts = mtest.NewOptions().ClientOptions(options.Client())
mt.RunOpts("Find always sends manually-specified maxTimeMS", csotOpts, func(mt *mtest.T) {

Suggest adding the following function to test that a 5s timeout is being used:

assertMaxTimeMS := func(mt *mtest.T, command bson.Raw, expected int64) {
	mt.Helper()

	maxTimeVal := command.Lookup("maxTimeMS")

	require.Greater(mt,
		len(maxTimeVal.Value),
		0,
		"expected maxTimeMS BSON value to be non-empty")
	require.Equal(mt,
		maxTimeVal.Type,
		bson.TypeInt64,
		"expected maxTimeMS BSON value to be type Int64")
	assert.Greater(mt,
		maxTimeVal.Int64(),
		int64(0),
		"expected maxTimeMS value to be greater than 0")
	assert.Equal(mt, expected, maxTimeVal.Int64())
}

Also suggest creating an additional test ensuring behavior defined in the Validation and Overrides section of the CSOT specifications:

When executing an operation, drivers MUST ignore any deprecated timeout options if timeoutMS is set on the operation or is inherited from the collection/database/client levels.

csotOpts = mtest.NewOptions().ClientOptions(options.Client().SetTimeout(15 * time.Second))
mt.RunOpts("Find sends client-level timeout over deprecated maxTimeMS", csotOpts, func(mt *mtest.T) {
	ctx := context.Background()

	// Manually set a maxTimeMS on the Find and assert that it's sent with
	// the command, even when CSOT is enabled.
	cursor, err := mt.Coll.Find(
		ctx,
		bson.D{},
		options.Find().SetMaxTime(5*time.Second))
	require.NoError(mt, err, "Find error")
	err = cursor.Close(context.Background())
	require.NoError(mt, err, "Cursor.Close error")

	evt := getStartedEvent(mt, "find")
	fmt.Println(evt)
	//assertMaxTimeMSIsSet(mt, evt.Command)
	assertMaxTimeMS(mt, evt.Command, 15000)
})

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think the test matrix should look something like this:

  1. no client-level, operation-level, deprecated operation-level -> use DOL
  2. ~CL, ~OL, DOL -> DOL
  3. CL, OL, DOL -> DOL
  4. CL, ~OL, DOL -> CL

Copy link
Collaborator Author

@matthewdale matthewdale Jun 3, 2024

Choose a reason for hiding this comment

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

The current test case is basically

CL, OL, DOL -> DOL

so setting timeoutMS is intentional. However, the test doesn't cover the rest of the cases, which could be confusing (especially because the test description is actually incorrect, since a manually-set maxTimeMS is ignored when a client-level timeout is set but no operation-level timeout is set).

I'll update the test to cover the additional cases.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I added test cases for the permutations you described.

ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()

// Manually set a maxTimeMS on the Find and assert that it's sent with
// the command, even when CSOT is enabled.
cursor, err := mt.Coll.Find(
ctx,
bson.D{},
options.Find().SetMaxTime(5*time.Second))
require.NoError(mt, err, "Find error")
err = cursor.Close(context.Background())
require.NoError(mt, err, "Cursor.Close error")

evt := getStartedEvent(mt, "find")
assertMaxTimeMSIsSet(mt, evt.Command)
})

// TODO(GODRIVER-2944): Remove this test once the "timeoutMode" option is
// supported.
mt.RunOpts("Aggregate always sends manually-specified maxTimeMS", csotOpts, func(mt *mtest.T) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Same considerations for "Find always sends manually-specified maxTimeMS" apply to the aggregate counterpart.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I added test cases for the permutations described in the other comment.

ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()

// Manually set a maxTimeMS on the Aggregate and assert that it's sent with
// the command, even when CSOT is enabled.
cursor, err := mt.Coll.Aggregate(
ctx,
bson.D{},
options.Aggregate().SetMaxTime(5*time.Second))
require.NoError(mt, err, "Aggregate error")
err = cursor.Close(context.Background())
require.NoError(mt, err, "Cursor.Close error")

evt := getStartedEvent(mt, "aggregate")
assertMaxTimeMSIsSet(mt, evt.Command)
})
}

func TestCSOT_errors(t *testing.T) {
Expand Down
16 changes: 11 additions & 5 deletions x/mongo/driver/operation.go
Original file line number Diff line number Diff line change
Expand Up @@ -1574,11 +1574,17 @@ func (op Operation) addClusterTime(dst []byte, desc description.SelectedServer)
// operation's MaxTimeMS if set. If no MaxTimeMS is set on the operation, and context is
// not a Timeout context, calculateMaxTimeMS returns 0.
func (op Operation) calculateMaxTimeMS(ctx context.Context, mon RTTMonitor) (uint64, error) {
if csot.IsTimeoutContext(ctx) {
if op.OmitCSOTMaxTimeMS {
return 0, nil
}

// If CSOT is enabled and we're not omitting the CSOT-calculated maxTimeMS
// value, then calculate maxTimeMS.
//
// This allows commands that do not currently send CSOT-calculated maxTimeMS
// (e.g. Find and Aggregate) to still use a manually-provided maxTimeMS
// value.
//
// TODO(GODRIVER-2944): Remove or refactor this logic when we add the
// "timeoutMode" option, which will allow users to opt-in to the
// CSOT-calculated maxTimeMS values if that's the behavior they want.
if csot.IsTimeoutContext(ctx) && !op.OmitCSOTMaxTimeMS {
if deadline, ok := ctx.Deadline(); ok {
remainingTimeout := time.Until(deadline)
rtt90 := mon.P90()
Expand Down
Loading