Skip to content

Commit d0b9dbc

Browse files
committed
feat: add StatusDelayProfile with status-based backoff logic
1 parent ef0d4f5 commit d0b9dbc

File tree

6 files changed

+53
-55
lines changed

6 files changed

+53
-55
lines changed

src/Ydb.Sdk/src/Ado/Retry/DefaultRetryPolicy.cs

Lines changed: 8 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,18 @@
1-
using System.Data;
2-
using System.Data.Common;
3-
41
namespace Ydb.Sdk.Ado.Retry;
52

63
public sealed class DefaultRetryPolicy : IRetryPolicy
74
{
85
private readonly RetryConfig _cfg;
96

10-
public DefaultRetryPolicy(RetryConfig? config = null) => _cfg = config ?? new RetryConfig();
7+
public DefaultRetryPolicy(RetryConfig? config = null)
8+
=> _cfg = config ?? new RetryConfig();
119

1210
public int MaxAttempts => _cfg.MaxAttempts;
1311

14-
public bool IsStreaming(DbCommand command, CommandBehavior behavior) => _cfg.IsStreaming(command, behavior);
15-
1612
public bool CanRetry(Exception ex, bool isIdempotent)
1713
{
18-
if (TryUnwrapYdbException(ex, out var ydb))
19-
{
14+
if (ex is YdbException ydb)
2015
return isIdempotent ? ydb.IsTransientWhenIdempotent : ydb.IsTransient;
21-
}
22-
23-
if (ex is TimeoutException) return true;
24-
if (ex is OperationCanceledException oce && !oce.CancellationToken.IsCancellationRequested) return true;
2516

2617
return false;
2718
}
@@ -30,28 +21,19 @@ public bool CanRetry(Exception ex, bool isIdempotent)
3021
{
3122
if (attempt <= 0) attempt = 1;
3223

33-
if (TryUnwrapYdbException(ex, out var ydb))
24+
if (ex is YdbException ydb)
3425
{
3526
if (_cfg.PerStatusDelay.TryGetValue(ydb.Code, out var calc))
3627
return Cap(calc(attempt));
28+
29+
var profileDelay = _cfg.StatusDelayProfile.GetDelay(ydb.Code, attempt);
30+
if (profileDelay is not null)
31+
return Cap(profileDelay);
3732
}
3833

3934
return Cap(_cfg.DefaultDelay(ex, attempt));
4035
}
4136

42-
private static bool TryUnwrapYdbException(Exception ex, out YdbException ydb)
43-
{
44-
for (var e = ex; e is not null; e = e.InnerException!)
45-
{
46-
if (e is YdbException yy)
47-
{
48-
ydb = yy; return true;
49-
}
50-
}
51-
ydb = null!;
52-
return false;
53-
}
54-
5537
private TimeSpan? Cap(TimeSpan? delay)
5638
{
5739
if (delay is null) return null;
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
namespace Ydb.Sdk.Ado.Retry.Delay;
2+
3+
public sealed class DefaultStatusDelayProfile : IStatusDelayProfile
4+
{
5+
public TimeSpan? GetDelay(StatusCode code, int attempt)
6+
{
7+
TimeSpan Calc(TimeSpan baseDelay)
8+
{
9+
var baseMs = baseDelay.TotalMilliseconds * Math.Pow(2.0, attempt - 1);
10+
var jitter = 1.0 + (Random.Shared.NextDouble() * 0.5);
11+
var ms = Math.Min(baseMs * jitter, TimeSpan.FromSeconds(10).TotalMilliseconds);
12+
return TimeSpan.FromMilliseconds(ms);
13+
}
14+
15+
switch (code)
16+
{
17+
case StatusCode.BadSession:
18+
return TimeSpan.Zero;
19+
case StatusCode.Aborted:
20+
case StatusCode.Unavailable:
21+
case StatusCode.SessionBusy:
22+
case StatusCode.ClientInternalError:
23+
case StatusCode.ClientTransportUnavailable:
24+
case StatusCode.ClientTransportTimeout:
25+
return Calc(TimeSpan.FromMilliseconds(100));
26+
case StatusCode.Overloaded:
27+
case StatusCode.ClientResourceExhausted:
28+
case StatusCode.ClientTransportResourceExhausted:
29+
return Calc(TimeSpan.FromMilliseconds(500));
30+
default:
31+
return null;
32+
}
33+
}
34+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
namespace Ydb.Sdk.Ado.Retry.Delay;
2+
3+
public interface IStatusDelayProfile
4+
{
5+
TimeSpan? GetDelay(StatusCode code, int attempt);
6+
}
Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
using System.Data.Common;
2-
31
namespace Ydb.Sdk.Ado.Retry;
42

53
public interface IRetryPolicy
@@ -9,6 +7,4 @@ public interface IRetryPolicy
97
bool CanRetry(Exception ex, bool isIdempotent);
108

119
TimeSpan? GetDelay(Exception ex, int attempt);
12-
13-
bool IsStreaming(DbCommand command, System.Data.CommandBehavior behavior);
1410
}

src/Ydb.Sdk/src/Ado/Retry/RetryConfig.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
using Ydb.Sdk.Ado.Retry.Delay;
2+
13
namespace Ydb.Sdk.Ado.Retry;
24

35
public sealed class RetryConfig
@@ -14,12 +16,10 @@ public sealed class RetryConfig
1416

1517
public Dictionary<StatusCode, Func<int, TimeSpan?>> PerStatusDelay { get; } = new();
1618

17-
public Func<System.Data.Common.DbCommand, System.Data.CommandBehavior, bool> IsStreaming { get; set; }
18-
= static (_, behavior) => (behavior & System.Data.CommandBehavior.SequentialAccess) != 0;
19-
20-
public Func<Exception, int, TimeSpan?> DefaultDelay { get; set; }
19+
public IStatusDelayProfile StatusDelayProfile { get; set; } = new DefaultStatusDelayProfile();
2120

22-
= static (ex, attempt) =>
21+
public Func<Exception, int, TimeSpan?> DefaultDelay { get; set; } =
22+
static (_, attempt) =>
2323
{
2424
var baseMs = 100.0 * Math.Pow(2.0, Math.Max(0, attempt - 1));
2525
var jitter = 1.0 + (Random.Shared.NextDouble() * 0.5);

src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/DefaultRetryPolicyTests.cs

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -98,26 +98,6 @@ public void CanRetry_WhenUserCancelled_ReturnsFalse()
9898
var ex = new OperationCanceledException(cts.Token);
9999
Assert.False(policy.CanRetry(ex, isIdempotent: true));
100100
}
101-
102-
[Fact]
103-
public void IsStreaming_WhenSequentialAccess_ReturnsTrue()
104-
{
105-
var config = new RetryConfig();
106-
var policy = new DefaultRetryPolicy(config);
107-
var cmd = new DummyCommand();
108-
109-
Assert.True(policy.IsStreaming(cmd, CommandBehavior.SequentialAccess));
110-
}
111-
112-
[Fact]
113-
public void IsStreaming_WhenCustomConfigDelegateUsed_ReturnsTrue()
114-
{
115-
var config = new RetryConfig { IsStreaming = (c, b) => true };
116-
var policy = new DefaultRetryPolicy(config);
117-
var cmd = new DummyCommand();
118-
119-
Assert.True(policy.IsStreaming(cmd, CommandBehavior.Default));
120-
}
121101

122102
[Fact]
123103
public void GetDelay_WhenDelayExceedsMaxDelay_IsCappedToMaxDelay()

0 commit comments

Comments
 (0)