Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
15 changes: 15 additions & 0 deletions src/MongoDB.Driver/Core/Operations/RetryabilityHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,21 @@ public static void AddRetryableWriteErrorLabelIfRequired(MongoException exceptio
}
}

public static int GetRetryDelayMs(int attempt, double backoffBase = 2, int backoffInitial = 100, int backoffMax = 10_000)
{
Ensure.IsGreaterThanZero(attempt, nameof(attempt));
Ensure.IsGreaterThanZero(backoffBase, nameof(backoffBase));
Ensure.IsGreaterThanZero(backoffInitial, nameof(backoffInitial));
Ensure.IsGreaterThan(backoffMax, backoffInitial, nameof(backoffMax));

#if NET6_0_OR_GREATER
var j = Random.Shared.NextDouble();
#else
var j = (new Random()).NextDouble();
Copy link

Copilot AI Dec 19, 2025

Choose a reason for hiding this comment

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

Creating a new Random instance for each call can lead to predictable values when called in quick succession due to time-based seeding. Consider using a static ThreadLocal instance for .NET Framework targets to ensure better randomness in concurrent scenarios.

Copilot uses AI. Check for mistakes.
#endif
return (int)(j * Math.Min(backoffMax, backoffInitial * Math.Pow(backoffBase, attempt - 1)));
Copy link

Copilot AI Dec 19, 2025

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.

Suggested change
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);

Copilot uses AI. Check for mistakes.
}

public static bool IsCommandRetryable(BsonDocument command)
{
return
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
using MongoDB.Driver.Core.Misc;
using MongoDB.Driver.Core.TestHelpers;
using Xunit;
using MongoDB.Driver.Core.Connections;

namespace MongoDB.Driver.Core.Operations
{
Expand Down Expand Up @@ -102,6 +101,42 @@ public void AddRetryableWriteErrorLabelIfRequired_should_add_RetryableWriteError
hasRetryableWriteErrorLabel.Should().Be(shouldAddErrorLabel);
}

[Theory]
[InlineData(1, 2, 100, 10000, 0, 100)]
[InlineData(2, 2, 100, 10000, 0, 200)]
[InlineData(3, 2, 100, 10000, 0, 400)]
[InlineData(9999, 2, 100, 10000, 0, 10000)]

[InlineData(1, 1.5, 100, 10000, 0, 100)]
[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)
{
var result = RetryabilityHelper.GetRetryDelayMs(attempt, backoffBase, backoffInitial, backoffMax);

result.Should().BeInRange(expectedRangeMin, expectedRangeMax);
}

[Theory]
[InlineData(-1, 2, 100, 1000, "attempt")]
[InlineData(0, 2, 100, 1000, "attempt")]
[InlineData(1, -1, 100, 1000, "backoffBase")]
[InlineData(1, 0, 100, 1000, "backoffBase")]
[InlineData(1, 2, -1, 1000, "backoffInitial")]
[InlineData(1, 2, 0, 1000, "backoffInitial")]
[InlineData(1, 2, 100, -1, "backoffMax")]
[InlineData(1, 2, 100, 0, "backoffMax")]
[InlineData(1, 2, 100, 50, "backoffMax")]

public void GetRetryDelayMs_throws_on_wrong_parameters(int attempt, double backoffBase, int backoffInitial, int backoffMax, string expectedParameterName)
{
var exception = Record.Exception(() => RetryabilityHelper.GetRetryDelayMs(attempt, backoffBase, backoffInitial, backoffMax));

exception.Should().BeOfType<ArgumentOutOfRangeException>().Subject
.ParamName.Should().Be(expectedParameterName);
}

[Theory]
[InlineData("{ txnNumber : 1 }", true)]
[InlineData("{ commitTransaction : 1 }", true)]
Expand Down