Release uplift gate when transaction setup fails

This commit is contained in:
Alexandre Brandizzi 2026-09-22 16:54:47 -03:00
parent d282799127
commit adb192cad7
2 changed files with 94 additions and 14 deletions

View file

@ -535,22 +535,25 @@ namespace SeaHaven.DataServices.Implementation
{
var gate = WorkOrderGates.GetOrAdd(workOrderId, _ => new SemaphoreSlim(1, 1));
await gate.WaitAsync(cancellationToken);
await using var transaction = _context.Database.IsRelational()
? await _context.Database.BeginTransactionAsync(cancellationToken)
: null;
try
{
await LockWorkOrderRowAsync(workOrderId, cancellationToken);
var result = await work(cancellationToken);
if (transaction is not null)
await transaction.CommitAsync(cancellationToken);
return result;
}
catch
{
if (transaction is not null)
await transaction.RollbackAsync(cancellationToken);
throw;
await using var transaction = _context.Database.IsRelational()
? await _context.Database.BeginTransactionAsync(cancellationToken)
: null;
try
{
await LockWorkOrderRowAsync(workOrderId, cancellationToken);
var result = await work(cancellationToken);
if (transaction is not null)
await transaction.CommitAsync(cancellationToken);
return result;
}
catch
{
if (transaction is not null)
await transaction.RollbackAsync(cancellationToken);
throw;
}
}
finally
{

View file

@ -0,0 +1,77 @@
using System.Data.Common;
using Data.SeaHavenIndustries;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;
using SeaHaven.DataServices.Implementation;
namespace SeaHavenIndustries.Tests;
public sealed class UpliftDataServiceTransactionTests
{
[Fact]
public async Task ExecuteWorkOrderMutationAsync_ReleasesGate_WhenTransactionInitializationFails()
{
await using var connection = new SqliteConnection("DataSource=:memory:");
await connection.OpenAsync();
var failingOptions = new DbContextOptionsBuilder<ApplicationDbContext>()
.UseSqlite(connection)
.AddInterceptors(new ThrowOnceTransactionInterceptor())
.Options;
await using (var failedContext = new ApplicationDbContext(failingOptions))
{
var failedService = new UpliftDataService(failedContext);
await Assert.ThrowsAsync<InvalidOperationException>(() =>
failedService.ExecuteWorkOrderMutationAsync(
387,
_ => Task.FromResult(true),
CancellationToken.None));
}
var retryOptions = new DbContextOptionsBuilder<ApplicationDbContext>()
.UseSqlite(connection)
.Options;
await using var retryContext = new ApplicationDbContext(retryOptions);
var retryService = new UpliftDataService(retryContext);
var retry = retryService.ExecuteWorkOrderMutationAsync(
387,
_ => Task.FromResult("entered"),
CancellationToken.None);
Assert.Equal("entered", await retry.WaitAsync(TimeSpan.FromSeconds(1)));
}
private sealed class ThrowOnceTransactionInterceptor : DbTransactionInterceptor
{
private int _remaining = 1;
public override InterceptionResult<DbTransaction> TransactionStarting(
DbConnection connection,
TransactionStartingEventData eventData,
InterceptionResult<DbTransaction> result)
{
ThrowOnce();
return base.TransactionStarting(connection, eventData, result);
}
public override ValueTask<InterceptionResult<DbTransaction>> TransactionStartingAsync(
DbConnection connection,
TransactionStartingEventData eventData,
InterceptionResult<DbTransaction> result,
CancellationToken cancellationToken = default)
{
ThrowOnce();
return base.TransactionStartingAsync(connection, eventData, result, cancellationToken);
}
private void ThrowOnce()
{
if (Interlocked.Exchange(ref _remaining, 0) == 1)
throw new InvalidOperationException("Forced transaction initialization failure");
}
}
}