diff --git a/examples/EntityFrameworkCore.Ydb.Yandex.Cloud/Program.cs b/examples/EntityFrameworkCore.Ydb.Yandex.Cloud/Program.cs index abce795c..5e2a7322 100644 --- a/examples/EntityFrameworkCore.Ydb.Yandex.Cloud/Program.cs +++ b/examples/EntityFrameworkCore.Ydb.Yandex.Cloud/Program.cs @@ -12,8 +12,8 @@ await Parser.Default.ParseArguments(args).WithParsedAsync(async cmd var options = new DbContextOptionsBuilder() .UseYdb(cmd.ConnectionString, builder => builder - .WithCredentialsProvider(saProvider) - .WithServerCertificates(YcCerts.GetYcServerCertificates()) + .UseCredentialsProvider(saProvider) + .UseServerCertificates(YcCerts.GetYcServerCertificates()) ) .Options; diff --git a/slo/src/EF/SloTableContext.cs b/slo/src/EF/SloTableContext.cs index 651ef93c..fc220ac0 100644 --- a/slo/src/EF/SloTableContext.cs +++ b/slo/src/EF/SloTableContext.cs @@ -12,7 +12,8 @@ public class SloTableContext : SloTableContext "EF"; protected override PooledDbContextFactory CreateClient(Config config) => - new(new DbContextOptionsBuilder().UseYdb(config.ConnectionString).Options); + new(new DbContextOptionsBuilder().UseYdb(config.ConnectionString, + builder => builder.EnableRetryIdempotence()).Options); protected override async Task Create( PooledDbContextFactory client, diff --git a/src/EFCore.Ydb/CHANGELOG.md b/src/EFCore.Ydb/CHANGELOG.md index fab05259..3b4ec968 100644 --- a/src/EFCore.Ydb/CHANGELOG.md +++ b/src/EFCore.Ydb/CHANGELOG.md @@ -1,3 +1,7 @@ +- Added support for the YDB retry policy (ADO.NET) and new configuration methods in `YdbDbContextOptionsBuilder`: + - `EnableRetryIdempotence()`: enables retries for errors classified as idempotent. You must ensure the operation itself is idempotent. + - `UseRetryPolicy(YdbRetryPolicyConfig retryPolicyConfig)`: configures custom backoff parameters and the maximum number of retry attempts. + ## v0.1.0 - Fixed bug: incompatible coalesce types ([#531](https://github.com/ydb-platform/ydb-dotnet-sdk/issues/531)). diff --git a/src/EFCore.Ydb/src/Infrastructure/Internal/YdbOptionsExtension.cs b/src/EFCore.Ydb/src/Infrastructure/Internal/YdbOptionsExtension.cs index 34c258db..12047dae 100644 --- a/src/EFCore.Ydb/src/Infrastructure/Internal/YdbOptionsExtension.cs +++ b/src/EFCore.Ydb/src/Infrastructure/Internal/YdbOptionsExtension.cs @@ -13,8 +13,6 @@ public class YdbOptionsExtension : RelationalOptionsExtension public X509Certificate2Collection? ServerCertificates { get; private set; } - public bool DisableRetryExecutionStrategy { get; private set; } - private DbContextOptionsExtensionInfo? _info; public YdbOptionsExtension() @@ -25,13 +23,11 @@ private YdbOptionsExtension(YdbOptionsExtension copyFrom) : base(copyFrom) { CredentialsProvider = copyFrom.CredentialsProvider; ServerCertificates = copyFrom.ServerCertificates; - DisableRetryExecutionStrategy = copyFrom.DisableRetryExecutionStrategy; } protected override RelationalOptionsExtension Clone() => new YdbOptionsExtension(this); - public override void ApplyServices(IServiceCollection services) => - services.AddEntityFrameworkYdb(!DisableRetryExecutionStrategy); + public override void ApplyServices(IServiceCollection services) => services.AddEntityFrameworkYdb(); public override DbContextOptionsExtensionInfo Info => _info ??= new ExtensionInfo(this); @@ -53,15 +49,6 @@ public YdbOptionsExtension WithServerCertificates(X509Certificate2Collection? se return clone; } - public YdbOptionsExtension DisableRetryOnFailure() - { - var clone = (YdbOptionsExtension)Clone(); - - clone.DisableRetryExecutionStrategy = true; - - return clone; - } - private sealed class ExtensionInfo(YdbOptionsExtension extension) : RelationalExtensionInfo(extension) { public override bool IsDatabaseProvider => true; diff --git a/src/EFCore.Ydb/src/Infrastructure/YdbDbContextOptionsBuilder.cs b/src/EFCore.Ydb/src/Infrastructure/YdbDbContextOptionsBuilder.cs index 8a35327c..b94bac18 100644 --- a/src/EFCore.Ydb/src/Infrastructure/YdbDbContextOptionsBuilder.cs +++ b/src/EFCore.Ydb/src/Infrastructure/YdbDbContextOptionsBuilder.cs @@ -1,20 +1,29 @@ using System.Security.Cryptography.X509Certificates; using EntityFrameworkCore.Ydb.Infrastructure.Internal; +using EntityFrameworkCore.Ydb.Storage.Internal; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage; +using Ydb.Sdk.Ado.RetryPolicy; using Ydb.Sdk.Auth; namespace EntityFrameworkCore.Ydb.Infrastructure; -public class YdbDbContextOptionsBuilder(DbContextOptionsBuilder optionsBuilder) +public sealed class YdbDbContextOptionsBuilder(DbContextOptionsBuilder optionsBuilder) : RelationalDbContextOptionsBuilder(optionsBuilder) { - public YdbDbContextOptionsBuilder WithCredentialsProvider(ICredentialsProvider? credentialsProvider) => + public YdbDbContextOptionsBuilder UseCredentialsProvider(ICredentialsProvider? credentialsProvider) => WithOption(optionsBuilder => optionsBuilder.WithCredentialsProvider(credentialsProvider)); - public YdbDbContextOptionsBuilder WithServerCertificates(X509Certificate2Collection? serverCertificates) => + public YdbDbContextOptionsBuilder UseServerCertificates(X509Certificate2Collection? serverCertificates) => WithOption(optionsBuilder => optionsBuilder.WithServerCertificates(serverCertificates)); + public YdbDbContextOptionsBuilder EnableRetryIdempotence() + => UseRetryPolicy(new YdbRetryPolicyConfig { EnableRetryIdempotence = true }); + + public YdbDbContextOptionsBuilder UseRetryPolicy(YdbRetryPolicyConfig retryPolicyConfig) + => ExecutionStrategy(d => new YdbExecutionStrategy(d, retryPolicyConfig)); + public YdbDbContextOptionsBuilder DisableRetryOnFailure() => - WithOption(optionsBuilder => optionsBuilder.DisableRetryOnFailure()); + ExecutionStrategy(d => new NonRetryingExecutionStrategy(d)); } diff --git a/src/EFCore.Ydb/src/Storage/Internal/YdbExecutionStrategy.cs b/src/EFCore.Ydb/src/Storage/Internal/YdbExecutionStrategy.cs index 46cea9ed..d88a4634 100644 --- a/src/EFCore.Ydb/src/Storage/Internal/YdbExecutionStrategy.cs +++ b/src/EFCore.Ydb/src/Storage/Internal/YdbExecutionStrategy.cs @@ -1,12 +1,38 @@ using System; using Microsoft.EntityFrameworkCore.Storage; +using Ydb.Sdk; using Ydb.Sdk.Ado; +using Ydb.Sdk.Ado.RetryPolicy; namespace EntityFrameworkCore.Ydb.Storage.Internal; -public class YdbExecutionStrategy(ExecutionStrategyDependencies dependencies) - : ExecutionStrategy(dependencies, maxRetryCount: 10, maxRetryDelay: TimeSpan.FromSeconds(10)) // TODO User settings +/// +/// Retry strategy for YDB.
+/// +///
IMPORTANT: +///
- The maximum number of attempts and backoff logic are encapsulated in . +/// The base ExecutionStrategy parameters (maxRetryCount, maxRetryDelay) are not used. +///
- This strategy must be invoked in the correct EF Core context/connection (YDB), +/// so that exception types and ShouldRetryOn semantics match the provider. +///
- The base is a good place to emit metrics/logs (attempt number, delay, exception type, etc.). +///
+public class YdbExecutionStrategy(ExecutionStrategyDependencies dependencies, YdbRetryPolicyConfig retryPolicyConfig) +// We pass "placeholders" to the base class: +// - MaxAttempts and TimeSpan.Zero are not used in the real retry logic. +// - Actual limits/delays are driven by YdbRetryPolicy. + : ExecutionStrategy(dependencies, retryPolicyConfig.MaxAttempts, TimeSpan.Zero /* unused! */) { - protected override bool ShouldRetryOn(Exception exception) - => exception is YdbException { IsTransient: true }; + private readonly YdbRetryPolicy _retryPolicy = new(retryPolicyConfig); + + public override bool RetriesOnFailure => true; + + protected override bool ShouldRetryOn(Exception exception) => + exception is YdbException ydbException && + (ydbException.IsTransient || (retryPolicyConfig.EnableRetryIdempotence && ydbException.Code is + StatusCode.ClientTransportUnknown or + StatusCode.ClientTransportUnavailable or + StatusCode.Undetermined)); + + protected override TimeSpan? GetNextDelay(Exception lastException) => + _retryPolicy.GetNextDelay((YdbException)lastException, ExceptionsEncountered.Count - 1); } diff --git a/src/EFCore.Ydb/src/Storage/Internal/YdbExecutionStrategyFactory.cs b/src/EFCore.Ydb/src/Storage/Internal/YdbExecutionStrategyFactory.cs index d9c3a1f2..40acc7a6 100644 --- a/src/EFCore.Ydb/src/Storage/Internal/YdbExecutionStrategyFactory.cs +++ b/src/EFCore.Ydb/src/Storage/Internal/YdbExecutionStrategyFactory.cs @@ -1,10 +1,11 @@ using Microsoft.EntityFrameworkCore.Storage; +using Ydb.Sdk.Ado.RetryPolicy; namespace EntityFrameworkCore.Ydb.Storage.Internal; public class YdbExecutionStrategyFactory(ExecutionStrategyDependencies dependencies) : RelationalExecutionStrategyFactory(dependencies) { - protected override IExecutionStrategy CreateDefaultStrategy(ExecutionStrategyDependencies dependencies) - => new YdbExecutionStrategy(dependencies); + protected override IExecutionStrategy CreateDefaultStrategy(ExecutionStrategyDependencies dependencies) => + new YdbExecutionStrategy(dependencies, YdbRetryPolicyConfig.Default); } diff --git a/src/EFCore.Ydb/src/Storage/Internal/YdbRelationalConnection.cs b/src/EFCore.Ydb/src/Storage/Internal/YdbRelationalConnection.cs index 23990cfe..91f3423c 100644 --- a/src/EFCore.Ydb/src/Storage/Internal/YdbRelationalConnection.cs +++ b/src/EFCore.Ydb/src/Storage/Internal/YdbRelationalConnection.cs @@ -13,7 +13,6 @@ public class YdbRelationalConnection : RelationalConnection, IYdbRelationalConne { private readonly ICredentialsProvider? _credentialsProvider; private readonly X509Certificate2Collection? _serverCertificates; - private readonly bool _disableRetryExecuteStrategy; public YdbRelationalConnection(RelationalConnectionDependencies dependencies) : base(dependencies) { @@ -22,7 +21,6 @@ public YdbRelationalConnection(RelationalConnectionDependencies dependencies) : _credentialsProvider = ydbOptionsExtension.CredentialsProvider; _serverCertificates = ydbOptionsExtension.ServerCertificates; - _disableRetryExecuteStrategy = ydbOptionsExtension.DisableRetryExecutionStrategy; } protected override DbConnection CreateDbConnection() @@ -40,12 +38,8 @@ public IYdbRelationalConnection Clone() { var options = new DbContextOptionsBuilder().UseYdb(GetValidatedConnectionString(), builder => { - builder.WithCredentialsProvider(_credentialsProvider); - builder.WithServerCertificates(_serverCertificates); - if (_disableRetryExecuteStrategy) - { - builder.DisableRetryOnFailure(); - } + builder.UseCredentialsProvider(_credentialsProvider); + builder.UseServerCertificates(_serverCertificates); }).Options; return new YdbRelationalConnection(Dependencies with { ContextOptions = options }); diff --git a/src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/QueryExpressionInterceptionYdbTestBase.cs b/src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/QueryExpressionInterceptionYdbTestBase.cs index f9869944..36e571ac 100644 --- a/src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/QueryExpressionInterceptionYdbTestBase.cs +++ b/src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/QueryExpressionInterceptionYdbTestBase.cs @@ -7,6 +7,7 @@ using Microsoft.EntityFrameworkCore.TestUtilities; using Microsoft.Extensions.DependencyInjection; using Xunit; +using Ydb.Sdk.Ado.RetryPolicy; namespace EntityFrameworkCore.Ydb.FunctionalTests; @@ -28,7 +29,7 @@ public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder build { new YdbDbContextOptionsBuilder(base.AddOptions(builder)) #pragma warning disable EF1001 - .ExecutionStrategy(d => new YdbExecutionStrategy(d)); + .ExecutionStrategy(d => new YdbExecutionStrategy(d, YdbRetryPolicyConfig.Default)); #pragma warning restore EF1001 return builder; } diff --git a/src/Ydb.Sdk/src/Ado/RetryPolicy/YdbRetryPolicy.cs b/src/Ydb.Sdk/src/Ado/RetryPolicy/YdbRetryPolicy.cs index 728ce873..e35fe92e 100644 --- a/src/Ydb.Sdk/src/Ado/RetryPolicy/YdbRetryPolicy.cs +++ b/src/Ydb.Sdk/src/Ado/RetryPolicy/YdbRetryPolicy.cs @@ -85,7 +85,7 @@ internal YdbRetryPolicy(YdbRetryPolicyConfig config, IRandom random) : this(conf return ydbException.Code switch { - StatusCode.BadSession or StatusCode.SessionBusy => TimeSpan.Zero, + StatusCode.BadSession or StatusCode.SessionBusy or StatusCode.SessionExpired => TimeSpan.Zero, StatusCode.Aborted or StatusCode.Undetermined => FullJitter(_fastBackoffBaseMs, _fastCapBackoffMs, _fastCeiling, attempt, _random), StatusCode.Unavailable or StatusCode.ClientTransportUnknown or StatusCode.ClientTransportUnavailable => 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 index fd411445..06ac64d5 100644 --- a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/RetryPolicy/YdbRetryPolicyTests.cs +++ b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/RetryPolicy/YdbRetryPolicyTests.cs @@ -10,6 +10,7 @@ public class YdbRetryPolicyTests [Theory] [InlineData(StatusCode.BadSession)] [InlineData(StatusCode.SessionBusy)] + [InlineData(StatusCode.SessionExpired)] public void GetNextDelay_WhenStatusIsBadSessionOrBusySession_ReturnTimeSpanZero(StatusCode statusCode) { var ydbRetryPolicy = new YdbRetryPolicy(new YdbRetryPolicyConfig { MaxAttempts = 2 });