You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
EntityFrameworkCore.Jet/test/EFCore.Jet.FunctionalTests/ExecutionStrategyTest.cs

688 lines
26 KiB
C#

// 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
#nullable disable
namespace EntityFrameworkCore.Jet.FunctionalTests
{
public class ExecutionStrategyTest : IClassFixture<ExecutionStrategyTest.ExecutionStrategyFixture>
{
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<TestJetRetryingExecutionStrategy, ExecutionStrategyContext> execute)
{
CleanContext();
using (var context = CreateContext())
{
var connection = (TestJetConnection)context.GetService<IJetRelationalConnection>();
connection.CommitFailures.Enqueue([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 + " (0xFFFFFFFE): 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<TestJetRetryingExecutionStrategy, ExecutionStrategyContext, Task> execute)
{
CleanContext();
using (var context = CreateContext())
{
var connection = (TestJetConnection)context.GetService<IJetRelationalConnection>();
connection.CommitFailures.Enqueue([realFailure]);
Fixture.TestSqlLoggerFactory.Clear();
context.Products.Add(new Product());
await execute(new TestJetRetryingExecutionStrategy(context), context);
context.ChangeTracker.AcceptAllChanges();
var retryMessage = (TestEnvironment.DataAccessProviderType == DataAccessProviderType.OleDb
? typeof(OleDbException)
: typeof(OdbcException)).FullName + " (0xFFFFFFFE): 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<IJetRelationalConnection>();
using (var context2 = CreateContext())
{
connection.CommitFailures.Enqueue([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<IJetRelationalConnection>();
connection.ExecutionFailures.Enqueue([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<IJetRelationalConnection>();
connection.ExecutionFailures.Enqueue([true]);
Assert.Equal(ConnectionState.Closed, context.Database.GetDbConnection().State);
List<Product> 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];
}
}
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<IJetRelationalConnection>();
connection.ExecutionFailures.Enqueue([true]);
Assert.Equal(ConnectionState.Closed, context.Database.GetDbConnection().State);
List<Product> list;
if (async)
{
if (externalStrategy)
{
list = await new TestJetRetryingExecutionStrategy(context)
.ExecuteAsync(
context, (c, ct) => c.Set<Product>().FromSqlRaw(
"""
SELECT `ID`, `name`
FROM `Products`
""").ToListAsync(ct), null);
}
else
{
list = await context.Set<Product>().FromSqlRaw(
"""
SELECT `ID`, `name`
FROM `Products`
""").ToListAsync();
}
}
else
{
if (externalStrategy)
{
list = new TestJetRetryingExecutionStrategy(context)
.Execute(
context, c => c.Set<Product>().FromSqlRaw(
"""
SELECT `ID`, `name`
FROM `Products`
""").ToList(), null);
}
else
{
list = [.. context.Set<Product>().FromSqlRaw(
"""
SELECT `ID`, `name`
FROM `Products`
""")];
}
}
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<IJetRelationalConnection>();
connection.OpenFailures.Enqueue([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<IJetRelationalConnection>();
connection.OpenFailures.Enqueue([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<IJetRelationalConnection>();
connection.ExecutionFailures.Enqueue([true, null, true, true]);
connection.CommitFailures.Enqueue([true, true, true, true]);
context.Products.Add(new Product());
Assert.Throws<RetryLimitExceededException>(
() =>
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(DbContextOptions options) : DbContext(options)
{
public DbSet<Product> 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<DbContext>
{
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<IRelationalTransactionFactory, TestRelationalTransactionFactory>()
.AddScoped<IJetRelationalConnection, TestJetConnection>()
.AddSingleton<IRelationalCommandBuilderFactory, TestRelationalCommandBuilderFactory>();
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;
}
}
}