Resistencia de conexión
La resistencia de la conexión reintenta automáticamente los comandos de base de datos con errores. La característica se puede usar con cualquier base de datos proporcionando una "estrategia de ejecución", que encapsula la lógica necesaria para detectar errores y volver a intentar la ejecución de comandos. Los proveedores de EF Core pueden proporcionar estrategias de ejecución adaptadas a sus condiciones de error de base de datos específicas y directivas de reintento óptimas.
Por ejemplo, el proveedor de SQL Server incluye una estrategia de ejecución que se adapta específicamente a SQL Server (incluido SQL Azure). Es consciente de los tipos de excepción que se pueden reintentar y tiene valores predeterminados razonables para los reintentos máximos, la demora entre reintentos, etc.
Se especifica una estrategia de ejecución al configurar las opciones del contexto. Normalmente, esto se encuentra en el método OnConfiguring
del contexto derivado:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder
.UseSqlServer(
@"Server=(localdb)\mssqllocaldb;Database=EFMiscellanous.ConnectionResiliency;Trusted_Connection=True;ConnectRetryCount=0",
options => options.EnableRetryOnFailure());
}
o en Startup.cs
para una aplicación ASP.NET Core:
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<PicnicContext>(
options => options.UseSqlServer(
"<connection string>",
providerOptions => providerOptions.EnableRetryOnFailure()));
}
Nota:
La habilitación del reintento en caso de error hace que EF almacene internamente en búfer el conjunto de resultados, lo que puede aumentar significativamente los requisitos de memoria para las consultas que devuelven grandes conjuntos de resultados. Para más información, consulte Almacenamiento en búfer y streaming.
Estrategia de ejecución personalizada
Existe un mecanismo para registrar una estrategia de ejecución personalizada propia en caso de que desee cambiar alguno de los valores predeterminados.
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder
.UseMyProvider(
"<connection string>",
options => options.ExecutionStrategy(...));
}
Estrategias y transacciones de ejecución
Una estrategia de ejecución que reintente automáticamente en caso de error debe ser capaz de reproducir cada operación de un bloque de reintento que genere un error. Cuando se habilitan los reintentos, cada operación que se realiza mediante EF Core se convierte en su propia operación que se puede reintentar. Esto es, cada consulta y cada llamada a SaveChanges()
se reintentará como una unidad si se produce un error transitorio.
Sin embargo, si el código inicia una transacción con BeginTransaction()
, va a definir su propio grupo de operaciones que se deben tratar como una unidad, y habría que reproducir todo dentro de la transacción si se produce un error. Recibirá una excepción similar a la siguiente si intenta hacerlo al usar una estrategia de ejecución:
InvalidOperationException: la estrategia de ejecución configurada "SqlServerRetryingExecutionStrategy" no es compatible con las transacciones que el usuario inicie. Use la estrategia de ejecución que devuelve "DbContext.Database.CreateExecutionStrategy()" para ejecutar todas las operaciones en la transacción como una unidad que se puede reintentar.
La solución consiste en invocar manualmente la estrategia de ejecución con un delegado que representa a todos los elementos que se deben ejecutar. Si se produce un error transitorio, la estrategia de ejecución vuelve a invocar al delegado.
using var db = new BloggingContext();
var strategy = db.Database.CreateExecutionStrategy();
strategy.Execute(
() =>
{
using var context = new BloggingContext();
using var transaction = context.Database.BeginTransaction();
context.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/dotnet" });
context.SaveChanges();
context.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/visualstudio" });
context.SaveChanges();
transaction.Commit();
});
Este enfoque también se puede usar con transacciones ambiente.
using var context1 = new BloggingContext();
context1.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/visualstudio" });
var strategy = context1.Database.CreateExecutionStrategy();
strategy.Execute(
() =>
{
using var context2 = new BloggingContext();
using var transaction = new TransactionScope();
context2.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/dotnet" });
context2.SaveChanges();
context1.SaveChanges();
transaction.Complete();
});
Error de confirmación de transacción y problema de idempotencia
En general, cuando se produce un error de conexión, la transacción actual se revierte. Sin embargo, si la conexión se interrumpe mientras la transacción se está confirmando, el estado resultante de la transacción es desconocido.
De forma predeterminada, la estrategia de ejecución reintentará la operación como si la transacción se revirtiera, pero si no es así, se producirá una excepción si el nuevo estado de la base de datos no es compatible o se podrían provocar daños en los datos si la operación no se basa en un estado determinado, por ejemplo, al insertar una nueva fila con valores de clave generados automáticamente.
Hay varias formas de abordar esta cuestión.
Opción 1: No hacer (casi) nada
La probabilidad de un error de conexión durante la confirmación de la transacción es baja, por lo que puede ser aceptable que solo se produzca un error en la aplicación si realmente se da esta condición.
Sin embargo, debe evitar el uso de claves generadas por el almacén para asegurarse de que se produce una excepción en lugar de agregar una fila duplicada. Considere la posibilidad de usar un valor GUID generado por el cliente o un generador de valores del lado del cliente.
Opción 2: Volver a generar el estado de la aplicación
- Descarte el valor
DbContext
actual. - Cree una instancia de
DbContext
y restaure el estado de la aplicación desde la base de datos. - Informe al usuario de que es posible que la última operación no se haya completado correctamente.
Opción 3: Agregar comprobación de estado
Para la mayoría de las operaciones que cambian el estado de la base de datos, es posible agregar código que compruebe si se realizó correctamente. EF proporciona un método de extensión para facilitar esta tarea: IExecutionStrategy.ExecuteInTransaction
.
Este método comienza y confirma una transacción y también acepta una función en el parámetro verifySucceeded
que se invoca cuando se produce un error transitorio durante la confirmación de la transacción.
using var db = new BloggingContext();
var strategy = db.Database.CreateExecutionStrategy();
var blogToAdd = new Blog { Url = "http://blogs.msdn.com/dotnet" };
db.Blogs.Add(blogToAdd);
strategy.ExecuteInTransaction(
db,
operation: context => { context.SaveChanges(acceptAllChangesOnSuccess: false); },
verifySucceeded: context => context.Blogs.AsNoTracking().Any(b => b.BlogId == blogToAdd.BlogId));
db.ChangeTracker.AcceptAllChanges();
Nota:
Aquí, SaveChanges
se invoca con acceptAllChangesOnSuccess
establecido en false
para evitar cambiar el estado de la entidad Blog
a Unchanged
si SaveChanges
se completa correctamente. Esto permite reintentar la misma operación si se produce un error en la confirmación y se revierte la transacción.
Opción 4: Seguir manualmente la transacción
Si necesita usar claves generadas por el almacén o una forma genérica de controlar los errores de confirmación que no dependen de la operación realizada, se podría asignar a cada transacción un identificador que se comprueba cuando se produce un error en la confirmación.
- Agregue una tabla a la base de datos utilizada para realizar el seguimiento del estado de las transacciones.
- Inserte una fila en la tabla al principio de cada transacción.
- Si se produce un error en la conexión durante la confirmación, compruebe la presencia de la fila correspondiente en la base de datos.
- Si la confirmación se realiza correctamente, elimine la fila correspondiente para evitar el crecimiento de la tabla.
using var db = new BloggingContext();
var strategy = db.Database.CreateExecutionStrategy();
db.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/dotnet" });
var transaction = new TransactionRow { Id = Guid.NewGuid() };
db.Transactions.Add(transaction);
strategy.ExecuteInTransaction(
db,
operation: context => { context.SaveChanges(acceptAllChangesOnSuccess: false); },
verifySucceeded: context => context.Transactions.AsNoTracking().Any(t => t.Id == transaction.Id));
db.ChangeTracker.AcceptAllChanges();
db.Transactions.Remove(transaction);
db.SaveChanges();
Nota:
Asegúrese de que el contexto utilizado para la comprobación tiene definida una estrategia de ejecución, ya que es probable que la conexión vuelva a generar un error durante la comprobación si ya se produjo un error durante la confirmación de la transacción.