diff --git a/examples/Ydb.Sdk.AdoNet.QuickStart/Program.cs b/examples/Ydb.Sdk.AdoNet.QuickStart/Program.cs index bba59fd5..fb0a5d9d 100644 --- a/examples/Ydb.Sdk.AdoNet.QuickStart/Program.cs +++ b/examples/Ydb.Sdk.AdoNet.QuickStart/Program.cs @@ -364,7 +364,8 @@ ORDER BY -- Sorting of the results. while (await ydbDataReader.ReadAsync()) { - _logger.LogInformation("season_title: {}, series_title: {}, series_id: {}, season_id: {}", + _logger.LogInformation("season_title: {SeasonTitle}, series_title: {SeriesTitle}, " + + "series_id: {SeriesId}, season_id: {SeasonId}", ydbDataReader.GetString("season_title"), ydbDataReader.GetString("series_title"), ydbDataReader.GetUint64(2), ydbDataReader.GetUint64(3)); } diff --git a/src/EFCore.Ydb/src/Storage/Internal/YdbDatabaseCreator.cs b/src/EFCore.Ydb/src/Storage/Internal/YdbDatabaseCreator.cs index 28012d85..9d502404 100644 --- a/src/EFCore.Ydb/src/Storage/Internal/YdbDatabaseCreator.cs +++ b/src/EFCore.Ydb/src/Storage/Internal/YdbDatabaseCreator.cs @@ -78,7 +78,7 @@ public override async Task DeleteAsync(CancellationToken cancellationToken = def .GetSchemaAsync("Tables", [null, "TABLE"], cancellationToken); var dropTableOperations = (from DataRow row in dataTable.Rows - select new DropTableOperation { Name = row["table_name"].ToString() }).ToList(); + select new DropTableOperation { Name = row["table_name"].ToString()! }).ToList(); await Dependencies.MigrationCommandExecutor.ExecuteNonQueryAsync(Dependencies.MigrationsSqlGenerator .Generate(dropTableOperations), connection, cancellationToken); diff --git a/src/Ydb.Sdk/CHANGELOG.md b/src/Ydb.Sdk/CHANGELOG.md index 0cad865f..b44c4670 100644 --- a/src/Ydb.Sdk/CHANGELOG.md +++ b/src/Ydb.Sdk/CHANGELOG.md @@ -1,3 +1,4 @@ +- Feat: Implement `YdbRetryPolicy` with AWS-inspired Exponential Backoff and Jitter. - Dev: LogLevel `Warning` -> `Debug` on DeleteSession has been `RpcException`. ## v0.22.0 diff --git a/src/Ydb.Sdk/src/Ado/Internal/Random.cs b/src/Ydb.Sdk/src/Ado/Internal/Random.cs new file mode 100644 index 00000000..737457be --- /dev/null +++ b/src/Ydb.Sdk/src/Ado/Internal/Random.cs @@ -0,0 +1,13 @@ +namespace Ydb.Sdk.Ado.Internal; + +public interface IRandom +{ + public int Next(int maxValue); +} + +internal class ThreadLocalRandom : IRandom +{ + internal static readonly ThreadLocalRandom Instance = new(); + + public int Next(int maxValue) => Random.Shared.Next(maxValue); +} diff --git a/src/Ydb.Sdk/src/Ado/Internal/StatusCodeUtils.cs b/src/Ydb.Sdk/src/Ado/Internal/StatusCodeUtils.cs index 57073ccb..6598d9f9 100644 --- a/src/Ydb.Sdk/src/Ado/Internal/StatusCodeUtils.cs +++ b/src/Ydb.Sdk/src/Ado/Internal/StatusCodeUtils.cs @@ -16,11 +16,25 @@ public static class StatusCodeUtils internal static StatusCode Code(this StatusIds.Types.StatusCode statusCode) => Enum.IsDefined(typeof(StatusCode), (int)statusCode) ? (StatusCode)statusCode : StatusCode.Unavailable; - internal static bool IsNotSuccess(this StatusIds.Types.StatusCode code) => - code != StatusIds.Types.StatusCode.Success; + internal static bool IsNotSuccess(this StatusIds.Types.StatusCode statusCode) => + statusCode != StatusIds.Types.StatusCode.Success; internal static string ToMessage(this StatusCode statusCode, IReadOnlyList issueMessages) => issueMessages.Count == 0 ? $"Status: {statusCode}" : $"Status: {statusCode}, Issues:{Environment.NewLine}{issueMessages.IssuesToString()}"; + + internal static bool IsTransient(this StatusCode statusCode) => statusCode is + StatusCode.BadSession or + StatusCode.SessionBusy or + StatusCode.Aborted or + StatusCode.Unavailable or + StatusCode.Overloaded or + StatusCode.SessionExpired or + StatusCode.ClientTransportResourceExhausted; + + internal static bool IsTransientWhenIdempotent(this StatusCode statusCode) => statusCode.IsTransient() || + statusCode is StatusCode.Undetermined or + StatusCode.ClientTransportUnknown or + StatusCode.ClientTransportUnavailable; } diff --git a/src/Ydb.Sdk/src/Ado/RetryPolicy/IRetryPolicy.cs b/src/Ydb.Sdk/src/Ado/RetryPolicy/IRetryPolicy.cs new file mode 100644 index 00000000..84d977ef --- /dev/null +++ b/src/Ydb.Sdk/src/Ado/RetryPolicy/IRetryPolicy.cs @@ -0,0 +1,6 @@ +namespace Ydb.Sdk.Ado.RetryPolicy; + +public interface IRetryPolicy +{ + public TimeSpan? GetNextDelay(YdbException ydbException, int attempt); +} diff --git a/src/Ydb.Sdk/src/Ado/RetryPolicy/YdbRetryPolicy.cs b/src/Ydb.Sdk/src/Ado/RetryPolicy/YdbRetryPolicy.cs new file mode 100644 index 00000000..b9f96abd --- /dev/null +++ b/src/Ydb.Sdk/src/Ado/RetryPolicy/YdbRetryPolicy.cs @@ -0,0 +1,71 @@ +using Ydb.Sdk.Ado.Internal; + +namespace Ydb.Sdk.Ado.RetryPolicy; + +/// +/// See AWS paper +/// +public class YdbRetryPolicy : IRetryPolicy +{ + public static readonly YdbRetryPolicy Default = new(YdbRetryPolicyConfig.Default); + + private readonly int _maxAttempt; + private readonly int _fastBackoffBaseMs; + private readonly int _slowBackoffBaseMs; + private readonly int _fastCeiling; + private readonly int _slowCeiling; + private readonly int _fastCapBackoffMs; + private readonly int _slowCapBackoffMs; + private readonly bool _enableRetryIdempotence; + private readonly IRandom _random; + + public YdbRetryPolicy(YdbRetryPolicyConfig config) + { + _maxAttempt = config.MaxAttempt; + _fastBackoffBaseMs = config.FastBackoffBaseMs; + _slowBackoffBaseMs = config.SlowBackoffBaseMs; + _fastCeiling = (int)Math.Ceiling(Math.Log(config.FastCapBackoffMs + 1, 2)); + _slowCeiling = (int)Math.Ceiling(Math.Log(config.SlowCapBackoffMs + 1, 2)); + _fastCapBackoffMs = config.FastCapBackoffMs; + _slowCapBackoffMs = config.SlowCapBackoffMs; + _enableRetryIdempotence = config.EnableRetryIdempotence; + _random = ThreadLocalRandom.Instance; + } + + internal YdbRetryPolicy(YdbRetryPolicyConfig config, IRandom random) : this(config) + { + _random = random; + } + + public TimeSpan? GetNextDelay(YdbException ydbException, int attempt) + { + if (attempt >= _maxAttempt || (!_enableRetryIdempotence && !ydbException.IsTransient)) + return null; + + return ydbException.Code switch + { + StatusCode.BadSession or StatusCode.SessionBusy => TimeSpan.Zero, + StatusCode.Aborted or StatusCode.Undetermined => + FullJitter(_fastBackoffBaseMs, _fastCapBackoffMs, _fastCeiling, attempt, _random), + StatusCode.Unavailable or StatusCode.ClientTransportUnknown or StatusCode.ClientTransportUnavailable => + EqualJitter(_fastBackoffBaseMs, _fastCapBackoffMs, _fastCeiling, attempt, _random), + StatusCode.Overloaded or StatusCode.ClientTransportResourceExhausted => + EqualJitter(_slowBackoffBaseMs, _slowCapBackoffMs, _slowCeiling, attempt, _random), + _ => null + }; + } + + private static TimeSpan FullJitter(int backoffBaseMs, int capMs, int ceiling, int attempt, IRandom random) => + TimeSpan.FromMilliseconds(random.Next(CalculateBackoff(backoffBaseMs, capMs, ceiling, attempt) + 1)); + + private static TimeSpan EqualJitter(int backoffBaseMs, int capMs, int ceiling, int attempt, IRandom random) + { + var calculatedBackoff = CalculateBackoff(backoffBaseMs, capMs, ceiling, attempt); + var temp = calculatedBackoff / 2; + + return TimeSpan.FromMilliseconds(temp + calculatedBackoff % 2 + random.Next(temp + 1)); + } + + private static int CalculateBackoff(int backoffBaseMs, int capMs, int ceiling, int attempt) => + Math.Min(backoffBaseMs * (1 << Math.Min(ceiling, attempt)), capMs); +} diff --git a/src/Ydb.Sdk/src/Ado/RetryPolicy/YdbRetryPolicyConfig.cs b/src/Ydb.Sdk/src/Ado/RetryPolicy/YdbRetryPolicyConfig.cs new file mode 100644 index 00000000..2e6950bc --- /dev/null +++ b/src/Ydb.Sdk/src/Ado/RetryPolicy/YdbRetryPolicyConfig.cs @@ -0,0 +1,18 @@ +namespace Ydb.Sdk.Ado.RetryPolicy; + +public class YdbRetryPolicyConfig +{ + public static readonly YdbRetryPolicyConfig Default = new(); + + public int MaxAttempt { get; init; } = 10; + + public int FastBackoffBaseMs { get; init; } = 5; + + public int SlowBackoffBaseMs { get; init; } = 50; + + public int FastCapBackoffMs { get; init; } = 500; + + public int SlowCapBackoffMs { get; init; } = 5_000; + + public bool EnableRetryIdempotence { get; init; } = false; +} diff --git a/src/Ydb.Sdk/src/Ado/YdbConnection.cs b/src/Ydb.Sdk/src/Ado/YdbConnection.cs index fe69763f..1dd39ef1 100644 --- a/src/Ydb.Sdk/src/Ado/YdbConnection.cs +++ b/src/Ydb.Sdk/src/Ado/YdbConnection.cs @@ -53,23 +53,6 @@ public YdbConnection(YdbConnectionStringBuilder connectionStringBuilder) ConnectionStringBuilder = connectionStringBuilder; } - public IBulkUpsertImporter BeginBulkUpsertImport( - string name, - IReadOnlyList columns, - CancellationToken cancellationToken = default) - { - ThrowIfConnectionClosed(); - if (CurrentTransaction is { Completed: false }) - throw new InvalidOperationException("BulkUpsert cannot be used inside an active transaction."); - - var database = ConnectionStringBuilder.Database.TrimEnd('/'); - var tablePath = name.StartsWith(database) ? name : $"{database}/{name}"; - - var maxBytes = ConnectionStringBuilder.MaxSendMessageSize; - - return new BulkUpsertImporter(Session.Driver, tablePath, columns, maxBytes, cancellationToken); - } - protected override YdbTransaction BeginDbTransaction(IsolationLevel isolationLevel) { ThrowIfConnectionClosed(); @@ -279,4 +262,22 @@ public override async ValueTask DisposeAsync() /// to their pool. /// public static Task ClearAllPools() => PoolManager.ClearAllPools(); + + public IBulkUpsertImporter BeginBulkUpsertImport( + string name, + IReadOnlyList columns, + CancellationToken cancellationToken = default) + { + ThrowIfConnectionClosed(); + + if (CurrentTransaction is { Completed: false }) + throw new InvalidOperationException("BulkUpsert cannot be used inside an active transaction."); + + var database = ConnectionStringBuilder.Database.TrimEnd('/'); + var tablePath = name.StartsWith(database) ? name : $"{database}/{name}"; + + var maxBytes = ConnectionStringBuilder.MaxSendMessageSize; + + return new BulkUpsertImporter(Session.Driver, tablePath, columns, maxBytes, cancellationToken); + } } diff --git a/src/Ydb.Sdk/src/Ado/YdbException.cs b/src/Ydb.Sdk/src/Ado/YdbException.cs index a419ad34..aed5f07b 100644 --- a/src/Ydb.Sdk/src/Ado/YdbException.cs +++ b/src/Ydb.Sdk/src/Ado/YdbException.cs @@ -18,7 +18,6 @@ internal YdbException(RpcException e) : this(e.Status.Code(), "Transport RPC cal internal static YdbException FromServer(StatusIds.Types.StatusCode statusCode, IReadOnlyList issues) { var code = statusCode.Code(); - var message = code.ToMessage(issues); return new YdbException(code, message); @@ -28,10 +27,8 @@ internal YdbException(StatusCode statusCode, string message, Exception? innerExc : base(message, innerException) { Code = statusCode; - var policy = RetrySettings.DefaultInstance.GetRetryRule(statusCode).Policy; - - IsTransient = policy == RetryPolicy.Unconditional; - IsTransientWhenIdempotent = policy != RetryPolicy.None; + IsTransient = statusCode.IsTransient(); + IsTransientWhenIdempotent = statusCode.IsTransientWhenIdempotent(); // TODO: Add SQLSTATE message with order with https://en.wikipedia.org/wiki/SQLSTATE } diff --git a/src/Ydb.Sdk/src/Pool/EndpointPool.cs b/src/Ydb.Sdk/src/Pool/EndpointPool.cs index feff114e..6f4d8586 100644 --- a/src/Ydb.Sdk/src/Pool/EndpointPool.cs +++ b/src/Ydb.Sdk/src/Pool/EndpointPool.cs @@ -1,5 +1,6 @@ using System.Collections.Immutable; using Microsoft.Extensions.Logging; +using Ydb.Sdk.Ado.Internal; namespace Ydb.Sdk.Pool; @@ -152,23 +153,3 @@ private record PriorityEndpoint(string Endpoint) } public record EndpointSettings(long NodeId, string Endpoint, string LocationDc); - -public interface IRandom -{ - public int Next(int maxValue); -} - -internal class ThreadLocalRandom : IRandom -{ - internal static readonly ThreadLocalRandom Instance = new(); - - [ThreadStatic] private static Random? _random; - - private static Random ThreadStaticRandom => _random ??= new Random(); - - private ThreadLocalRandom() - { - } - - public int Next(int maxValue) => ThreadStaticRandom.Next(maxValue); -} diff --git a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Stress.Loader/StressLoadTank.cs b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Stress.Loader/StressLoadTank.cs index 8c28687e..1e6ca161 100644 --- a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Stress.Loader/StressLoadTank.cs +++ b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Stress.Loader/StressLoadTank.cs @@ -37,13 +37,12 @@ Starting YDB ADO.NET Stress Test Tank Peak RPS: {PeakRps} Medium RPS: {MediumRps} Min RPS: {MinRps} - Load Pattern: Peak({PeakDuration}s) -> Medium({MediumDuration}s) -> Min({MinDuration}s) -> Medium({MediumDuration}s) + Load Pattern: Peak({PeakDuration}s) -> Medium({MediumDuration}s) -> Min({MinDuration}s) Total Test Time: {TotalTime}s Test Query: {TestQuery} """, _config.PeakRps, _config.MediumRps, _config.MinRps, _config.PeakDurationSeconds, - _config.MediumDurationSeconds, _config.MinDurationSeconds, _config.MediumDurationSeconds, - _config.TotalTestTimeSeconds, _config.TestQuery + _config.MediumDurationSeconds, _config.MinDurationSeconds, _config.TotalTestTimeSeconds, _config.TestQuery ); var ctsRunJob = new CancellationTokenSource(); diff --git a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/Pool/EndpointPoolTests.cs b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/Pool/EndpointPoolTests.cs index 9840b761..fbd0adac 100644 --- a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/Pool/EndpointPoolTests.cs +++ b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/Pool/EndpointPoolTests.cs @@ -1,6 +1,7 @@ using System.Collections.Immutable; using Moq; using Xunit; +using Ydb.Sdk.Ado.Internal; using Ydb.Sdk.Ado.Tests.Utils; using Ydb.Sdk.Pool; @@ -112,7 +113,7 @@ public void PessimizeEndpoint_Reset_WhenPessimizedMajorityNodesThenResetAndAddNe listNewEndpointSettings.Add(new EndpointSettings(6, "n6.ydb.tech", "VLA")); listNewEndpointSettings.Add(new EndpointSettings(7, "n7.ydb.tech", "MAN")); - _endpointPool.Reset(listNewEndpointSettings.ToImmutableArray()); + _endpointPool.Reset([..listNewEndpointSettings]); for (var it = 0; it < listNewEndpointSettings.Count; it++) { @@ -141,7 +142,7 @@ public void PessimizeEndpoint_Reset_WhenResetNewNodes_ReturnRemovedNodes() listNewEndpointSettings.Add(new EndpointSettings(6, "n6.ydb.tech", "VLA")); listNewEndpointSettings.Add(new EndpointSettings(7, "n7.ydb.tech", "MAN")); - var removed = _endpointPool.Reset(listNewEndpointSettings.ToImmutableArray()); + var removed = _endpointPool.Reset([..listNewEndpointSettings]); Assert.Equal(2, removed.Length); Assert.Equal("n1.ydb.tech", removed[0]); diff --git a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/RetryPolicy/YdbRetryPolicyTests.cs b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/RetryPolicy/YdbRetryPolicyTests.cs new file mode 100644 index 00000000..3a38e696 --- /dev/null +++ b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/RetryPolicy/YdbRetryPolicyTests.cs @@ -0,0 +1,151 @@ +using Moq; +using Xunit; +using Ydb.Sdk.Ado.Internal; +using Ydb.Sdk.Ado.RetryPolicy; + +namespace Ydb.Sdk.Ado.Tests.RetryPolicy; + +public class YdbRetryPolicyTests +{ + [Theory] + [InlineData(StatusCode.BadSession)] + [InlineData(StatusCode.SessionBusy)] + public void GetNextDelay_WhenStatusIsBadSessionOrBusySession_ReturnTimeSpanZero(StatusCode statusCode) + { + var ydbRetryPolicy = new YdbRetryPolicy(new YdbRetryPolicyConfig { MaxAttempt = 2 }); + var ydbException = new YdbException(statusCode, "Mock message"); + + Assert.Equal(TimeSpan.Zero, ydbRetryPolicy.GetNextDelay(ydbException, 0)); + Assert.Equal(TimeSpan.Zero, ydbRetryPolicy.GetNextDelay(ydbException, 1)); + Assert.Null(ydbRetryPolicy.GetNextDelay(ydbException, 2)); + } + + [Theory] + [InlineData(StatusCode.ClientTransportUnavailable)] + [InlineData(StatusCode.Undetermined)] + public void GetNextDelay_WhenStatusIsIdempotenceAndDisableIdempotence_ReturnNull(StatusCode statusCode) + { + var ydbRetryPolicy = new YdbRetryPolicy(new YdbRetryPolicyConfig { MaxAttempt = 2 }); + var ydbException = new YdbException(statusCode, "Mock message"); + + Assert.Null(ydbRetryPolicy.GetNextDelay(ydbException, 0)); + Assert.Null(ydbRetryPolicy.GetNextDelay(ydbException, 1)); + } + + [Theory] + [InlineData(StatusCode.Aborted, false)] + [InlineData(StatusCode.Undetermined, true)] + public void GetNextDelay_WhenFullJitterWithFastBackoff_ReturnCalculatedBackoff(StatusCode statusCode, + bool enableRetryIdempotence) + { + var mockRandom = new Mock(MockBehavior.Strict); + var ydbRetryPolicy = new YdbRetryPolicy(new YdbRetryPolicyConfig + { + EnableRetryIdempotence = enableRetryIdempotence, + FastBackoffBaseMs = 5, + FastCapBackoffMs = 100 + }, mockRandom.Object); + var ydbException = new YdbException(statusCode, "Mock message"); + + mockRandom.Setup(random => random.Next(6)).Returns(2); + Assert.Equal(TimeSpan.FromMilliseconds(2), ydbRetryPolicy.GetNextDelay(ydbException, 0)); + + mockRandom.Setup(random => random.Next(11)).Returns(7); + Assert.Equal(TimeSpan.FromMilliseconds(7), ydbRetryPolicy.GetNextDelay(ydbException, 1)); + + mockRandom.Setup(random => random.Next(21)).Returns(14); + Assert.Equal(TimeSpan.FromMilliseconds(14), ydbRetryPolicy.GetNextDelay(ydbException, 2)); + + mockRandom.Setup(random => random.Next(41)).Returns(23); + Assert.Equal(TimeSpan.FromMilliseconds(23), ydbRetryPolicy.GetNextDelay(ydbException, 3)); + + mockRandom.Setup(random => random.Next(81)).Returns(53); + Assert.Equal(TimeSpan.FromMilliseconds(53), ydbRetryPolicy.GetNextDelay(ydbException, 4)); + + mockRandom.Setup(random => random.Next(101)).Returns(89); + Assert.Equal(TimeSpan.FromMilliseconds(89), ydbRetryPolicy.GetNextDelay(ydbException, 5)); + } + + [Theory] + [InlineData(StatusCode.Unavailable, false)] + [InlineData(StatusCode.ClientTransportUnknown, true)] + [InlineData(StatusCode.ClientTransportUnavailable, true)] + public void GetNextDelay_WhenEqualJitterWithFastBackoff_ReturnCalculatedBackoff(StatusCode statusCode, + bool enableRetryIdempotence) + { + var mockRandom = new Mock(MockBehavior.Strict); + var ydbRetryPolicy = new YdbRetryPolicy(new YdbRetryPolicyConfig + { + EnableRetryIdempotence = enableRetryIdempotence, + FastBackoffBaseMs = 5, + FastCapBackoffMs = 50 + }, mockRandom.Object); + var ydbException = new YdbException(statusCode, "Mock message"); + + mockRandom.Setup(random => random.Next(3)).Returns(1); + Assert.Equal(TimeSpan.FromMilliseconds(4), ydbRetryPolicy.GetNextDelay(ydbException, 0)); + + mockRandom.Setup(random => random.Next(6)).Returns(5); + Assert.Equal(TimeSpan.FromMilliseconds(10), ydbRetryPolicy.GetNextDelay(ydbException, 1)); + + mockRandom.Setup(random => random.Next(11)).Returns(8); + Assert.Equal(TimeSpan.FromMilliseconds(18), ydbRetryPolicy.GetNextDelay(ydbException, 2)); + + mockRandom.Setup(random => random.Next(21)).Returns(15); + Assert.Equal(TimeSpan.FromMilliseconds(35), ydbRetryPolicy.GetNextDelay(ydbException, 3)); + + mockRandom.Setup(random => random.Next(26)).Returns(11); + Assert.Equal(TimeSpan.FromMilliseconds(36), ydbRetryPolicy.GetNextDelay(ydbException, 4)); + } + + [Theory] + [InlineData(StatusCode.Overloaded, false)] + [InlineData(StatusCode.ClientTransportResourceExhausted, false)] + public void GetNextDelay_WhenEqualJitterWithSlowBackoff_ReturnCalculatedBackoff(StatusCode statusCode, + bool enableRetryIdempotence) + { + var mockRandom = new Mock(MockBehavior.Strict); + var ydbRetryPolicy = new YdbRetryPolicy(new YdbRetryPolicyConfig + { + EnableRetryIdempotence = enableRetryIdempotence, + SlowBackoffBaseMs = 100, + SlowCapBackoffMs = 1000 + }, mockRandom.Object); + var ydbException = new YdbException(statusCode, "Mock message"); + + mockRandom.Setup(random => random.Next(51)).Returns(27); + Assert.Equal(TimeSpan.FromMilliseconds(77), ydbRetryPolicy.GetNextDelay(ydbException, 0)); + + mockRandom.Setup(random => random.Next(101)).Returns(5); + Assert.Equal(TimeSpan.FromMilliseconds(105), ydbRetryPolicy.GetNextDelay(ydbException, 1)); + + mockRandom.Setup(random => random.Next(201)).Returns(123); + Assert.Equal(TimeSpan.FromMilliseconds(323), ydbRetryPolicy.GetNextDelay(ydbException, 2)); + + mockRandom.Setup(random => random.Next(401)).Returns(301); + Assert.Equal(TimeSpan.FromMilliseconds(701), ydbRetryPolicy.GetNextDelay(ydbException, 3)); + + mockRandom.Setup(random => random.Next(501)).Returns(257); + Assert.Equal(TimeSpan.FromMilliseconds(757), ydbRetryPolicy.GetNextDelay(ydbException, 4)); + } + + [Theory] + [InlineData(StatusCode.SchemeError)] + [InlineData(StatusCode.Unspecified)] + [InlineData(StatusCode.BadRequest)] + [InlineData(StatusCode.Unauthorized)] + [InlineData(StatusCode.InternalError)] + [InlineData(StatusCode.GenericError)] + [InlineData(StatusCode.Timeout)] + [InlineData(StatusCode.PreconditionFailed)] + [InlineData(StatusCode.AlreadyExists)] + [InlineData(StatusCode.NotFound)] + [InlineData(StatusCode.Cancelled)] + [InlineData(StatusCode.Unsupported)] + [InlineData(StatusCode.Success)] + [InlineData(StatusCode.ClientTransportTimeout)] + [InlineData(StatusCode.ClientTransportUnimplemented)] + public void GetNextDelay_WhenStatusCodeIsNotRetriable_ReturnNull(StatusCode statusCode) => + Assert.Null(new YdbRetryPolicy(new YdbRetryPolicyConfig { EnableRetryIdempotence = true }) + .GetNextDelay(new YdbException(statusCode, "Mock message"), 0)); +}