// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using System.Collections.Generic; using System.Data; using System.Data.Odbc; using System.Data.OleDb; using System.Linq; using System.Threading; using System.Threading.Tasks; using EntityFrameworkCore.Jet.Data; using EntityFrameworkCore.Jet.FunctionalTests.TestUtilities; using EntityFrameworkCore.Jet.Infrastructure; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Infrastructure; using EntityFrameworkCore.Jet.Storage.Internal; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Storage; using Microsoft.EntityFrameworkCore.TestUtilities; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Xunit; using DbContext = Microsoft.EntityFrameworkCore.DbContext; // ReSharper disable MethodSupportsCancellation // ReSharper disable AccessToDisposedClosure // ReSharper disable InconsistentNaming namespace EntityFrameworkCore.Jet.FunctionalTests { public class ExecutionStrategyTest : IClassFixture { public ExecutionStrategyTest(ExecutionStrategyFixture fixture) { Fixture = fixture; Fixture.TestStore.CloseConnection(); Fixture.TestSqlLoggerFactory.Clear(); } protected ExecutionStrategyFixture Fixture { get; } [ConditionalTheory] [MemberData(nameof(DataGenerator.GetBoolCombinations), 1, MemberType = typeof(DataGenerator))] public void Handles_commit_failure(bool realFailure) { // Use all overloads of ExecuteInTransaction Test_commit_failure( realFailure, (e, db) => e.ExecuteInTransaction( () => { db.SaveChanges(false); }, () => db.Products.AsNoTracking().Any())); Test_commit_failure( realFailure, (e, db) => e.ExecuteInTransaction( () => db.SaveChanges(false), () => db.Products.AsNoTracking().Any())); Test_commit_failure( realFailure, (e, db) => e.ExecuteInTransaction( db, c => { c.SaveChanges(false); }, c => c.Products.AsNoTracking().Any())); Test_commit_failure( realFailure, (e, db) => e.ExecuteInTransaction( db, c => c.SaveChanges(false), c => c.Products.AsNoTracking().Any())); Test_commit_failure( realFailure, (e, db) => e.ExecuteInTransaction( () => { db.SaveChanges(false); }, () => db.Products.AsNoTracking().Any(), IsolationLevel.Serializable)); Test_commit_failure( realFailure, (e, db) => e.ExecuteInTransaction( () => db.SaveChanges(false), () => db.Products.AsNoTracking().Any(), IsolationLevel.Serializable)); Test_commit_failure( realFailure, (e, db) => e.ExecuteInTransaction( db, c => { c.SaveChanges(false); }, c => c.Products.AsNoTracking().Any(), IsolationLevel.Serializable)); Test_commit_failure( realFailure, (e, db) => e.ExecuteInTransaction( db, c => c.SaveChanges(false), c => c.Products.AsNoTracking().Any(), IsolationLevel.Serializable)); } private void Test_commit_failure(bool realFailure, Action execute) { CleanContext(); using (var context = CreateContext()) { var connection = (TestJetConnection)context.GetService(); connection.CommitFailures.Enqueue(new bool?[] { realFailure }); Fixture.TestSqlLoggerFactory.Clear(); context.Products.Add(new Product()); execute(new TestJetRetryingExecutionStrategy(context), context); context.ChangeTracker.AcceptAllChanges(); var retryMessage = (TestEnvironment.DataAccessProviderType == DataAccessProviderType.OleDb ? typeof(OleDbException) : typeof(OdbcException)).FullName + " : Bang!";; if (realFailure) { var logEntry = Fixture.TestSqlLoggerFactory.Log.Single(l => l.Id == CoreEventId.ExecutionStrategyRetrying); Assert.Contains(retryMessage, logEntry.Message); Assert.Equal(LogLevel.Information, logEntry.Level); } else { Assert.Empty(Fixture.TestSqlLoggerFactory.Log.Where(l => l.Id == CoreEventId.ExecutionStrategyRetrying)); } Assert.Equal(realFailure ? 3 : 2, connection.OpenCount); } using (var context = CreateContext()) { Assert.Equal(1, context.Products.Count()); } } [ConditionalTheory] [MemberData(nameof(DataGenerator.GetBoolCombinations), 1, MemberType = typeof(DataGenerator))] public async Task Handles_commit_failure_async(bool realFailure) { // Use all overloads of ExecuteInTransactionAsync await Test_commit_failure_async( realFailure, (e, db) => e.ExecuteInTransactionAsync( () => db.SaveChangesAsync(false), () => db.Products.AsNoTracking().AnyAsync())); await Test_commit_failure_async( realFailure, (e, db) => e.ExecuteInTransactionAsync( async ct => { await db.SaveChangesAsync(false); }, ct => db.Products.AsNoTracking().AnyAsync(), CancellationToken.None)); await Test_commit_failure_async( realFailure, (e, db) => e.ExecuteInTransactionAsync( ct => db.SaveChangesAsync(false, ct), ct => db.Products.AsNoTracking().AnyAsync(), CancellationToken.None)); await Test_commit_failure_async( realFailure, (e, db) => e.ExecuteInTransactionAsync( db, async (c, ct) => { await c.SaveChangesAsync(false, ct); }, (c, ct) => c.Products.AsNoTracking().AnyAsync(), CancellationToken.None)); await Test_commit_failure_async( realFailure, (e, db) => e.ExecuteInTransactionAsync( db, (c, ct) => c.SaveChangesAsync(false, ct), (c, ct) => c.Products.AsNoTracking().AnyAsync(), CancellationToken.None)); await Test_commit_failure_async( realFailure, (e, db) => e.ExecuteInTransactionAsync( () => db.SaveChangesAsync(false), () => db.Products.AsNoTracking().AnyAsync(), IsolationLevel.Serializable)); await Test_commit_failure_async( realFailure, (e, db) => e.ExecuteInTransactionAsync( async ct => { await db.SaveChangesAsync(false, ct); }, ct => db.Products.AsNoTracking().AnyAsync(ct), IsolationLevel.Serializable, CancellationToken.None)); await Test_commit_failure_async( realFailure, (e, db) => e.ExecuteInTransactionAsync( ct => db.SaveChangesAsync(false, ct), ct => db.Products.AsNoTracking().AnyAsync(ct), IsolationLevel.Serializable, CancellationToken.None)); await Test_commit_failure_async( realFailure, (e, db) => e.ExecuteInTransactionAsync( db, async (c, ct) => { await c.SaveChangesAsync(false, ct); }, (c, ct) => c.Products.AsNoTracking().AnyAsync(ct), IsolationLevel.Serializable, CancellationToken.None)); await Test_commit_failure_async( realFailure, (e, db) => e.ExecuteInTransactionAsync( db, (c, ct) => c.SaveChangesAsync(false, ct), (c, ct) => c.Products.AsNoTracking().AnyAsync(ct), IsolationLevel.Serializable, CancellationToken.None)); } private async Task Test_commit_failure_async( bool realFailure, Func execute) { CleanContext(); using (var context = CreateContext()) { var connection = (TestJetConnection)context.GetService(); connection.CommitFailures.Enqueue(new bool?[] { realFailure }); Fixture.TestSqlLoggerFactory.Clear(); context.Products.Add(new Product()); await execute(new TestJetRetryingExecutionStrategy(context), context); context.ChangeTracker.AcceptAllChanges(); var retryMessage = "System.Data.OleDb.OleDbException : Bang!"; if (realFailure) { var logEntry = Fixture.TestSqlLoggerFactory.Log.Single(l => l.Id == CoreEventId.ExecutionStrategyRetrying); Assert.Contains(retryMessage, logEntry.Message); Assert.Equal(LogLevel.Information, logEntry.Level); } else { Assert.Empty(Fixture.TestSqlLoggerFactory.Log.Where(l => l.Id == CoreEventId.ExecutionStrategyRetrying)); } Assert.Equal(realFailure ? 3 : 2, connection.OpenCount); } using (var context = CreateContext()) { Assert.Equal(1, await context.Products.CountAsync()); } } [ConditionalTheory] [MemberData(nameof(DataGenerator.GetBoolCombinations), 1, MemberType = typeof(DataGenerator))] public void Handles_commit_failure_multiple_SaveChanges(bool realFailure) { CleanContext(); using (var context1 = CreateContext()) { var connection = (TestJetConnection)context1.GetService(); using (var context2 = CreateContext()) { connection.CommitFailures.Enqueue(new bool?[] { realFailure }); context1.Products.Add(new Product()); context2.Products.Add(new Product()); new TestJetRetryingExecutionStrategy(context1).ExecuteInTransaction( context1, c1 => { context2.Database.UseTransaction(null); context2.Database.UseTransaction(context1.Database.CurrentTransaction.GetDbTransaction()); c1.SaveChanges(false); return context2.SaveChanges(false); }, c => c.Products.AsNoTracking().Any()); context1.ChangeTracker.AcceptAllChanges(); context2.ChangeTracker.AcceptAllChanges(); } using (var context = CreateContext()) { Assert.Equal(2, context.Products.Count()); } } } [ConditionalTheory] [MemberData(nameof(DataGenerator.GetBoolCombinations), 4, MemberType = typeof(DataGenerator))] public async Task Retries_SaveChanges_on_execution_failure( bool realFailure, bool externalStrategy, bool openConnection, bool async) { CleanContext(); using (var context = CreateContext()) { var connection = (TestJetConnection)context.GetService(); connection.ExecutionFailures.Enqueue(new bool?[] { null, realFailure }); Assert.Equal(ConnectionState.Closed, context.Database.GetDbConnection().State); if (openConnection) { if (async) { await context.Database.OpenConnectionAsync(); } else { context.Database.OpenConnection(); } Assert.Equal(ConnectionState.Open, context.Database.GetDbConnection().State); } context.Products.Add(new Product()); context.Products.Add(new Product()); if (async) { if (externalStrategy) { await new TestJetRetryingExecutionStrategy(context).ExecuteInTransactionAsync( context, (c, ct) => c.SaveChangesAsync(false, ct), (c, _) => { Assert.True(false); return Task.FromResult(false); }); context.ChangeTracker.AcceptAllChanges(); } else { await context.SaveChangesAsync(); } } else { if (externalStrategy) { new TestJetRetryingExecutionStrategy(context).ExecuteInTransaction( context, c => c.SaveChanges(false), c => { Assert.True(false); return false; }); context.ChangeTracker.AcceptAllChanges(); } else { context.SaveChanges(); } } Assert.Equal(2, connection.OpenCount); Assert.Equal(4, connection.ExecutionCount); Assert.Equal( openConnection ? ConnectionState.Open : ConnectionState.Closed, context.Database.GetDbConnection().State); if (openConnection) { if (async) { context.Database.CloseConnection(); } else { await context.Database.CloseConnectionAsync(); } } Assert.Equal(ConnectionState.Closed, context.Database.GetDbConnection().State); } using (var context = CreateContext()) { Assert.Equal(2, context.Products.Count()); } } [ConditionalTheory] [MemberData(nameof(DataGenerator.GetBoolCombinations), 2, MemberType = typeof(DataGenerator))] public async Task Retries_query_on_execution_failure(bool externalStrategy, bool async) { CleanContext(); using (var context = CreateContext()) { context.Products.Add(new Product()); context.Products.Add(new Product()); context.SaveChanges(); } using (var context = CreateContext()) { var connection = (TestJetConnection)context.GetService(); connection.ExecutionFailures.Enqueue(new bool?[] { true }); Assert.Equal(ConnectionState.Closed, context.Database.GetDbConnection().State); List list; if (async) { if (externalStrategy) { list = await new TestJetRetryingExecutionStrategy(context) .ExecuteAsync(context, (c, ct) => c.Products.ToListAsync(ct), null); } else { list = await context.Products.ToListAsync(); } } else { if (externalStrategy) { list = new TestJetRetryingExecutionStrategy(context) .Execute(context, c => c.Products.ToList(), null); } else { list = context.Products.ToList(); } } Assert.Equal(2, list.Count); Assert.Equal(1, connection.OpenCount); Assert.Equal(2, connection.ExecutionCount); Assert.Equal(ConnectionState.Closed, context.Database.GetDbConnection().State); } } [ConditionalTheory] [MemberData(nameof(DataGenerator.GetBoolCombinations), 2, MemberType = typeof(DataGenerator))] public async Task Retries_FromSqlRaw_on_execution_failure(bool externalStrategy, bool async) { CleanContext(); using (var context = CreateContext()) { context.Products.Add(new Product()); context.Products.Add(new Product()); context.SaveChanges(); } using (var context = CreateContext()) { var connection = (TestJetConnection)context.GetService(); connection.ExecutionFailures.Enqueue(new bool?[] { true }); Assert.Equal(ConnectionState.Closed, context.Database.GetDbConnection().State); List list; if (async) { if (externalStrategy) { list = await new TestJetRetryingExecutionStrategy(context) .ExecuteAsync( context, (c, ct) => c.Set().FromSqlRaw( @"SELECT `ID`, `name` FROM `Products`").ToListAsync(ct), null); } else { list = await context.Set().FromSqlRaw( @"SELECT `ID`, `name` FROM `Products`").ToListAsync(); } } else { if (externalStrategy) { list = new TestJetRetryingExecutionStrategy(context) .Execute( context, c => c.Set().FromSqlRaw( @"SELECT `ID`, `name` FROM `Products`").ToList(), null); } else { list = context.Set().FromSqlRaw( @"SELECT `ID`, `name` FROM `Products`").ToList(); } } Assert.Equal(2, list.Count); Assert.Equal(1, connection.OpenCount); Assert.Equal(2, connection.ExecutionCount); Assert.Equal(ConnectionState.Closed, context.Database.GetDbConnection().State); } } [ConditionalTheory] [MemberData(nameof(DataGenerator.GetBoolCombinations), 2, MemberType = typeof(DataGenerator))] public async Task Retries_OpenConnection_on_execution_failure(bool externalStrategy, bool async) { using (var context = CreateContext()) { var connection = (TestJetConnection)context.GetService(); connection.OpenFailures.Enqueue(new bool?[] { true }); Assert.Equal(ConnectionState.Closed, context.Database.GetDbConnection().State); if (async) { if (externalStrategy) { await new TestJetRetryingExecutionStrategy(context).ExecuteAsync( context, c => c.Database.OpenConnectionAsync()); } else { await context.Database.OpenConnectionAsync(); } } else { if (externalStrategy) { new TestJetRetryingExecutionStrategy(context).Execute( context, c => c.Database.OpenConnection()); } else { context.Database.OpenConnection(); } } Assert.Equal(2, connection.OpenCount); if (async) { context.Database.CloseConnection(); } else { await context.Database.CloseConnectionAsync(); } Assert.Equal(ConnectionState.Closed, context.Database.GetDbConnection().State); } } [ConditionalTheory] [InlineData(false)] [InlineData(true)] public async Task Retries_BeginTransaction_on_execution_failure(bool async) { using (var context = CreateContext()) { var connection = (TestJetConnection)context.GetService(); connection.OpenFailures.Enqueue(new bool?[] { true }); Assert.Equal(ConnectionState.Closed, context.Database.GetDbConnection().State); if (async) { var transaction = await new TestJetRetryingExecutionStrategy(context).ExecuteAsync( context, c => context.Database.BeginTransactionAsync()); transaction.Dispose(); } else { var transaction = new TestJetRetryingExecutionStrategy(context).Execute( context, c => context.Database.BeginTransaction()); transaction.Dispose(); } Assert.Equal(2, connection.OpenCount); Assert.Equal(ConnectionState.Closed, context.Database.GetDbConnection().State); } } [ConditionalFact] public void Verification_is_retried_using_same_retry_limit() { CleanContext(); using (var context = CreateContext()) { var connection = (TestJetConnection)context.GetService(); connection.ExecutionFailures.Enqueue(new bool?[] { true, null, true, true }); connection.CommitFailures.Enqueue(new bool?[] { true, true, true, true }); context.Products.Add(new Product()); Assert.Throws( () => new TestJetRetryingExecutionStrategy(context, TimeSpan.FromMilliseconds(100)) .ExecuteInTransaction( context, c => c.SaveChanges(false), c => false)); context.ChangeTracker.AcceptAllChanges(); Assert.Equal(7, connection.OpenCount); Assert.Equal(7, connection.ExecutionCount); } using (var context = CreateContext()) { Assert.Equal(0, context.Products.Count()); } } protected class ExecutionStrategyContext : DbContext { public ExecutionStrategyContext(DbContextOptions options) : base(options) { } public DbSet Products { get; set; } } protected class Product { public int Id { get; set; } public string Name { get; set; } } protected virtual ExecutionStrategyContext CreateContext() => (ExecutionStrategyContext)Fixture.CreateContext(); private void CleanContext() { using (var context = CreateContext()) { foreach (var product in context.Products.ToList()) { context.Remove(product); context.SaveChanges(); } } } public class ExecutionStrategyFixture : SharedStoreFixtureBase { protected override bool UsePooling => false; protected override string StoreName { get; } = nameof(ExecutionStrategyTest); public new RelationalTestStore TestStore => (RelationalTestStore)base.TestStore; public TestSqlLoggerFactory TestSqlLoggerFactory => (TestSqlLoggerFactory)ListLoggerFactory; protected override ITestStoreFactory TestStoreFactory => JetTestStoreFactory.Instance; protected override Type ContextType { get; } = typeof(ExecutionStrategyContext); protected override IServiceCollection AddServices(IServiceCollection serviceCollection) => base.AddServices(serviceCollection) .AddSingleton() .AddScoped() .AddSingleton(); public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) { var options = base.AddOptions(builder); new JetDbContextOptionsBuilder(options).MaxBatchSize(1); return options; } protected override bool ShouldLogCategory(string logCategory) => logCategory == DbLoggerCategory.Infrastructure.Name; } } }