Skip to content

Commit fd0eb2f

Browse files
committed
CSHARP-6089: Use attempt number as backoff exponent per updated spec
Update the exponential-backoff formula from 2^(attempt-1) to 2^attempt per the client-backpressure and transactions-convenient-api spec change (mongodb/specifications#1953), and update the dependent backoff tests. Also adds the baseBackoffMS overload integration test.
1 parent 0ee53b4 commit fd0eb2f

6 files changed

Lines changed: 85 additions & 20 deletions

File tree

src/MongoDB.Driver/Core/Operations/RetryabilityHelper.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ public static int GetRetryDelayMs(IRandom random, int attempt, double backoffBas
126126
Ensure.IsGreaterThanOrEqualTo(backoffMax, backoffInitial, nameof(backoffMax));
127127

128128
var j = random.NextDouble();
129-
return (int)(j * Math.Min(backoffMax, backoffInitial * Math.Pow(backoffBase, attempt - 1)));
129+
return (int)(j * Math.Min(backoffMax, backoffInitial * Math.Pow(backoffBase, attempt)));
130130
}
131131

132132
/// <summary>

tests/MongoDB.Driver.Tests/ClientSessionHandleTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -653,7 +653,7 @@ public async Task WithTransaction_retry_backoff_is_enforced([Values(true, false)
653653
var noBackoffTimeMs = await ExecuteWithTransactionAsync(0);
654654
var backoffTimeMs = await ExecuteWithTransactionAsync(1);
655655

656-
backoffTimeMs.Should().BeApproximately(noBackoffTimeMs + 1800, 150);
656+
backoffTimeMs.Should().BeApproximately(noBackoffTimeMs + 2280, 150);
657657

658658
async Task<double> ExecuteWithTransactionAsync(double randomValue)
659659
{

tests/MongoDB.Driver.Tests/Core/Operations/RetryabilityHelperTests.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -103,13 +103,13 @@ public void AddRetryableWriteErrorLabelIfRequired_should_add_RetryableWriteError
103103
}
104104

105105
[Theory]
106-
[InlineData(1, 2, 100, 10000, 0, 100)]
107-
[InlineData(2, 2, 100, 10000, 0, 200)]
108-
[InlineData(3, 2, 100, 10000, 0, 400)]
106+
[InlineData(1, 2, 100, 10000, 0, 200)]
107+
[InlineData(2, 2, 100, 10000, 0, 400)]
108+
[InlineData(3, 2, 100, 10000, 0, 800)]
109109
[InlineData(9999, 2, 100, 10000, 0, 10000)]
110-
[InlineData(1, 1.5, 100, 10000, 0, 100)]
111-
[InlineData(2, 1.5, 100, 10000, 0, 150)]
112-
[InlineData(3, 1.5, 100, 10000, 0, 225)]
110+
[InlineData(1, 1.5, 100, 10000, 0, 150)]
111+
[InlineData(2, 1.5, 100, 10000, 0, 225)]
112+
[InlineData(3, 1.5, 100, 10000, 0, 337)]
113113
[InlineData(9999, 1.5, 100, 10000, 0, 10000)]
114114
public void GetRetryDelayMs_should_return_expected_result(int attempt, double backoffBase, int backoffInitial, int backoffMax, int expectedRangeMin, int expectedRangeMax)
115115
{

tests/MongoDB.Driver.Tests/Core/Operations/RetryableReadOperationExecutorTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,8 +139,8 @@ public void ShouldRetry_with_system_overloaded_exception_should_not_retry_when_r
139139
}
140140

141141
[Theory]
142-
[InlineData(1, 50, 0, 50)]
143-
[InlineData(2, 50, 0, 100)]
142+
[InlineData(1, 50, 0, 100)]
143+
[InlineData(2, 50, 0, 200)]
144144
[InlineData(1, 10000, 0, 10000)]
145145
[InlineData(2, 20000, 0, 10000)]
146146
public void ShouldRetry_with_baseBackoffMs_should_use_it_as_backoff_base(

tests/MongoDB.Driver.Tests/Core/Operations/RetryableWriteOperationExecutorTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,8 +89,8 @@ public void IsOperationAcknowledged_should_return_expected_result(bool? isAcknow
8989
}
9090

9191
[Theory]
92-
[InlineData(1, 50, 0, 50)]
93-
[InlineData(2, 50, 0, 100)]
92+
[InlineData(1, 50, 0, 100)]
93+
[InlineData(2, 50, 0, 200)]
9494
[InlineData(1, 10000, 0, 10000)]
9595
[InlineData(2, 20000, 0, 10000)]
9696
public void ShouldRetry_with_baseBackoffMs_should_use_it_as_backoff_base(

tests/MongoDB.Driver.Tests/Specifications/client-backpressure/prose-tests/ClientBackpressureProseTests.cs

Lines changed: 73 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -169,11 +169,11 @@ private async Task AssertBackoffBehavior<TContext>(
169169

170170
withBackoffException.Should().NotBeNull().And.BeOfType<MongoCommandException>();
171171

172-
// With MAX_RETRIES=2 and jitter=1, total backoff = 100ms + 200ms = 300ms.
173-
// Per the spec: assertTrue(abs(with_backoff_time - (no_backoff_time + 0.3s)) < 0.3s)
172+
// With MAX_RETRIES=2 and jitter=1, total backoff = 200ms + 400ms = 600ms.
173+
// Per the spec: assertTrue(abs(with_backoff_time - (no_backoff_time + 0.6s)) < 0.6s)
174174
var difference = withBackoffTime - noBackoffTime;
175-
Math.Abs(difference - 300).Should().BeLessThan(300,
176-
$"backoff difference should be approximately 300ms, got {difference}ms (noBackoff: {noBackoffTime}ms, withBackoff: {withBackoffTime}ms)");
175+
Math.Abs(difference - 600).Should().BeLessThan(600,
176+
$"backoff difference should be approximately 600ms, got {difference}ms (noBackoff: {noBackoffTime}ms, withBackoff: {withBackoffTime}ms)");
177177
}
178178

179179
private async Task AssertBaseBackoffMsOverridesBackoff<TOperation, TContext>(
@@ -188,17 +188,17 @@ private async Task AssertBaseBackoffMsOverridesBackoff<TOperation, TContext>(
188188
var fullJitterRandom = new Mock<IRandom>();
189189
fullJitterRandom.Setup(r => r.NextDouble()).Returns(1.0);
190190

191-
// Exponential backoff run: overload error without baseBackoffMS -> 100ms + 200ms = 300ms with jitter = 1.
191+
// Exponential backoff run: overload error without baseBackoffMS -> 200ms + 400ms = 600ms with jitter = 1.
192192
var exponentialException = CoreExceptionHelper.CreateMongoCommandExceptionWithLabels(462, "SystemOverloadedError", "RetryableError");
193193
var exponentialMs = await MeasureBackoff(async, executeSync, executeAsync, operationContext, createOperation(exponentialException), createContext(fullJitterRandom.Object));
194194

195-
// baseBackoffMS override run: overload error with baseBackoffMS = 50 -> 50ms + 100ms = 150ms with jitter = 1.
195+
// baseBackoffMS override run: overload error with baseBackoffMS = 50 -> 100ms + 200ms = 300ms with jitter = 1.
196196
var overrideResult = BsonDocument.Parse("{ ok : 0, code : 462, baseBackoffMS : 50 }");
197197
var overrideException = CoreExceptionHelper.CreateMongoCommandExceptionWithLabels(overrideResult, "SystemOverloadedError", "RetryableError");
198198
var withBaseBackoffMs = await MeasureBackoff(async, executeSync, executeAsync, operationContext, createOperation(overrideException), createContext(fullJitterRandom.Object));
199199

200-
// Per the spec: assertTrue(abs(exponential_backoff_time - (with_retry_after_ms_time + 0.2s)) < 0.2s)
201-
Math.Abs(exponentialMs - (withBaseBackoffMs + 200)).Should().BeLessThan(200,
200+
// Per the spec: assertTrue(abs(exponential_backoff_time - (with_base_backoff_ms_time + 0.3s)) < 0.3s)
201+
Math.Abs(exponentialMs - (withBaseBackoffMs + 300)).Should().BeLessThan(300,
202202
$"baseBackoffMS should shorten the backoff base (exponential: {exponentialMs}ms, withBaseBackoffMS: {withBaseBackoffMs}ms)");
203203
}
204204

@@ -374,4 +374,69 @@ public void Overload_errors_are_retried_a_maximum_of_maxAdaptiveRetries_times_wh
374374
// maxAdaptiveRetries = 1, so total started commands = maxAdaptiveRetries + 1 = 2
375375
eventCapturer.Events.OfType<CommandStartedEvent>().Count().Should().Be(2);
376376
}
377+
378+
[Fact]
379+
// https://github.com/mongodb/specifications/pull/1953 (Test 5: overload errors with baseBackoffMS override base backoff)
380+
public void Overload_errors_with_baseBackoffMS_shorten_the_backoff_base()
381+
{
382+
RequireServer.Check()
383+
.ClusterTypes(ClusterType.ReplicaSet)
384+
.VersionGreaterThanOrEqualTo("9.0.0");
385+
386+
var failPointCommand = BsonDocument.Parse(
387+
@"{
388+
configureFailPoint: ""failCommand"",
389+
mode: ""alwaysOn"",
390+
data:
391+
{
392+
failCommands: [""insert""],
393+
errorCode: 462,
394+
errorLabels: [""SystemOverloadedError"", ""RetryableError""]
395+
}
396+
}");
397+
398+
using var client = DriverTestConfiguration.CreateMongoClient(s => s.RetryWrites = true);
399+
var database = client.GetDatabase(DriverTestConfiguration.DatabaseNamespace.DatabaseName);
400+
var collection = database.GetCollection<BsonDocument>(DriverTestConfiguration.CollectionNamespace.CollectionName);
401+
var adminDatabase = client.GetDatabase(DatabaseNamespace.Admin.DatabaseName);
402+
403+
long MeasureFailedInsertMs()
404+
{
405+
using var failPoint = FailPoint.Configure(failPointCommand);
406+
var stopwatch = Stopwatch.StartNew();
407+
var exception = Record.Exception(() => collection.InsertOne(new BsonDocument("a", 1)));
408+
stopwatch.Stop();
409+
410+
var mongoException = exception.Should().BeAssignableTo<MongoException>().Subject;
411+
mongoException.HasErrorLabel("SystemOverloadedError").Should().BeTrue();
412+
return stopwatch.ElapsedMilliseconds;
413+
}
414+
415+
// Baseline: no override. With MaxAdaptiveRetries = 2, the worst case (jitter = 1 on both retries)
416+
// is 200ms + 400ms = 600ms of backoff.
417+
var baselineMs = MeasureFailedInsertMs();
418+
419+
// A value at/above MaxBackoff (10000ms) clamps to 10000ms on every attempt, so both retries draw
420+
// their backoff from [0, 10000) instead of [0, 100)/[0, 200) -- an order of magnitude bigger
421+
// regardless of jitter.
422+
const int overrideBaseBackoffMs = 20000;
423+
adminDatabase.RunCommand<BsonDocument>(
424+
$@"{{ setParameter : 1, externalClientBaseBackoffMS : {overrideBaseBackoffMs} }}");
425+
try
426+
{
427+
var overrideMs = MeasureFailedInsertMs();
428+
429+
// Baseline maxes out at 600ms; the override's two draws from [0, 10000) average 10000ms
430+
// combined. A 1s gap requires both override jitters to independently land below ~0.13, which
431+
// happens well under 1% of the time -- comfortably low flake risk while still being a real,
432+
// unmocked timing check.
433+
(overrideMs - baselineMs).Should().BeGreaterThan(1000,
434+
$"a large baseBackoffMS override should produce a much longer backoff (baseline: {baselineMs}ms, override: {overrideMs}ms)");
435+
}
436+
finally
437+
{
438+
adminDatabase.RunCommand<BsonDocument>(
439+
@"{ setParameter : 1, externalClientBaseBackoffMS : 0 }");
440+
}
441+
}
377442
}

0 commit comments

Comments
 (0)