-
Notifications
You must be signed in to change notification settings - Fork 1.3k
CSHARP-5712: withTransaction API retries too frequently #1841
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
base: main
Are you sure you want to change the base?
Conversation
There was a problem hiding this 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 implements the RetryabilityHelper.GetRetryDelayMs method to control retry delays with exponential backoff, addressing the issue of transactions retrying too frequently. The implementation adds jitter to prevent thundering herd problems and includes comprehensive parameter validation.
Key Changes
- Adds
GetRetryDelayMsmethod with exponential backoff calculation and random jitter - Removes unused
MongoDB.Driver.Core.Connectionsimport from test file - Adds comprehensive test coverage for valid inputs, edge cases, and parameter validation
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| src/MongoDB.Driver/Core/Operations/RetryabilityHelper.cs | Implements the core retry delay calculation with exponential backoff and jitter |
| tests/MongoDB.Driver.Tests/Core/Operations/RetryabilityHelperTests.cs | Adds unit tests for the new method and removes unused import |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| Ensure.IsGreaterThan(backoffMax, backoffInitial, nameof(backoffMax)); | ||
|
|
||
| var j = __backoffRandom.NextDouble(); | ||
| return (int)(j * Math.Min(backoffMax, backoffInitial * Math.Pow(backoffBase, attempt - 1))); |
Copilot
AI
Dec 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The calculation backoffInitial * Math.Pow(backoffBase, attempt - 1) may overflow for large attempt values, which would then be capped by Math.Min. Consider adding overflow protection or documenting the maximum safe attempt value to prevent unexpected behavior.
| return (int)(j * Math.Min(backoffMax, backoffInitial * Math.Pow(backoffBase, attempt - 1))); | |
| // compute the largest exponent such that backoffInitial * backoffBase^exponent <= backoffMax | |
| var maxExponent = Math.Log(backoffMax / (double)backoffInitial, backoffBase); | |
| var effectiveExponent = attempt - 1; | |
| double delayWithoutJitter; | |
| if (effectiveExponent >= maxExponent) | |
| { | |
| delayWithoutJitter = backoffMax; | |
| } | |
| else | |
| { | |
| delayWithoutJitter = backoffInitial * Math.Pow(backoffBase, effectiveExponent); | |
| } | |
| return (int)(j * delayWithoutJitter); |
There was a problem hiding this 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 2 out of 2 changed files in this pull request and generated 1 comment.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this 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 4 out of 4 changed files in this pull request and generated 1 comment.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
papafe
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good!
| } | ||
|
|
||
| var subject = CreateSubject(coreSession: mockCoreSession.Object, clock: mockClock.Object); | ||
| SetupTransactionState(subject, true); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this related to the ticket?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, changes were rolled back. Thank you for noticing this.
BorisDog
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Partial review
| { | ||
| return callbackOutcome.Result; | ||
| var delay = GetRetryDelay(randomNumberGenerator, attempt); | ||
| if (HasTimedOut(operationContext, delay)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IsTimedOut?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
| var backoffTime = await ExecuteWithTransactionAsync(1); | ||
|
|
||
| var difference = backoffTime - noBackoffTime - TimeSpan.FromSeconds(2.2); | ||
| difference.Should().BeLessThan(TimeSpan.FromMilliseconds(1)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are we using a 1 millisecond window here instead of a 1 second because we're "faking" the prose test?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep. We do not interact with the server, so timing should be more strict. 1ms could be small though, we might will need to adjust it in future.
|
|
||
| return sb.ToString(); | ||
| #else | ||
| return string.Create(length, legalCharacters, (buffer, charset) => |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice!
| return callback(clientSession, cancellationToken); | ||
| } | ||
| catch (Exception ex) | ||
| catch (Exception) when (IsTransactionInStartingOrInProgressState(clientSession)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will
catch when (IsTransactionInStartingOrInProgressState(clientSession))
work?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The entire method was removed.
| var noBackoffTime = await ExecuteWithTransactionAsync(0); | ||
| var backoffTime = await ExecuteWithTransactionAsync(1); | ||
|
|
||
| var difference = backoffTime - noBackoffTime - TimeSpan.FromSeconds(2.2); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will
backoffTimeMS.Should().BeApproximately(noBackoffTimeMS + 2200, 1);
be more readable?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
| [InlineData(1, 2, 100, -1, "backoffMax")] | ||
| [InlineData(1, 2, 100, 0, "backoffMax")] | ||
| [InlineData(1, 2, 100, 50, "backoffMax")] | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Empty line
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed.
| [InlineData(2, 1.5, 100, 10000, 0, 150)] | ||
| [InlineData(3, 1.5, 100, 10000, 0, 225)] | ||
| [InlineData(9999, 1.5, 100, 10000, 0, 10000)] | ||
| public void GetRetryDelayMs_should_return_expected_results(int attempt, double backoffBase, int backoffInitial, int backoffMax, int expectedRangeMin, int expectedRangeMax) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
minor: expected_result
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
BorisDog
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Minor suggestion
| { | ||
| return callbackOutcome.Result; // assume callback intentionally ended the transaction | ||
| } | ||
| if (IsTransactionInStartingOrInProgressState(clientSession)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similar to ShouldRetryTransaction consider ShouldAbortTransaction(out AbortTransactionOptions)
No description provided.