mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 09:33:13 +00:00
The per-work-order gate moves into a shared data-layer helper. A CRM mutation that cancels an existing work order now runs under it, so a create in flight either commits first and is cancelled, or sees the cancelled work order.
79 lines
3.1 KiB
C#
79 lines
3.1 KiB
C#
using System.Collections.Concurrent;
|
|
using Data.SeaHavenIndustries;
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
namespace SeaHaven.DataServices.Helpers
|
|
{
|
|
/// <summary>
|
|
/// The per-work-order gate that serializes work-order-scoped invariants (uplift create,
|
|
/// decide, revoke, and cancelling a work order's pending uplifts): an in-process gate per
|
|
/// work order, a transaction, and an update lock on the work order row on SQL Server.
|
|
/// Every caller shares the same gates, so a cancel and a create on one work order never
|
|
/// interleave.
|
|
/// </summary>
|
|
public static class WorkOrderMutationLock
|
|
{
|
|
private static readonly ConcurrentDictionary<int, SemaphoreSlim> Gates = new();
|
|
|
|
/// <summary>
|
|
/// Runs <paramref name="work"/> under the work order's gate and row lock. The
|
|
/// transaction commits when the work returns and <paramref name="commitWhen"/> (when
|
|
/// given) accepts its result; otherwise, or when the work throws, it rolls back.
|
|
/// </summary>
|
|
public static async Task<T> RunAsync<T>(
|
|
ApplicationDbContext context,
|
|
int workOrderId,
|
|
Func<CancellationToken, Task<T>> work,
|
|
CancellationToken cancellationToken,
|
|
Func<T, bool>? commitWhen = null)
|
|
{
|
|
var gate = Gates.GetOrAdd(workOrderId, _ => new SemaphoreSlim(1, 1));
|
|
await gate.WaitAsync(cancellationToken);
|
|
try
|
|
{
|
|
await using var transaction = context.Database.IsRelational()
|
|
? await context.Database.BeginTransactionAsync(cancellationToken)
|
|
: null;
|
|
try
|
|
{
|
|
await LockRowAsync(context, workOrderId, cancellationToken);
|
|
var result = await work(cancellationToken);
|
|
if (transaction is not null)
|
|
{
|
|
if (commitWhen is null || commitWhen(result))
|
|
await transaction.CommitAsync(cancellationToken);
|
|
else
|
|
await transaction.RollbackAsync(cancellationToken);
|
|
}
|
|
return result;
|
|
}
|
|
catch
|
|
{
|
|
if (transaction is not null)
|
|
await transaction.RollbackAsync(cancellationToken);
|
|
throw;
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
gate.Release();
|
|
}
|
|
}
|
|
|
|
private static async Task LockRowAsync(
|
|
ApplicationDbContext context,
|
|
int workOrderId,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
if (context.Database.ProviderName?.Contains("SqlServer", StringComparison.OrdinalIgnoreCase) != true)
|
|
return;
|
|
|
|
await context.workOrders
|
|
.FromSqlRaw(
|
|
"SELECT * FROM [workOrders] WITH (UPDLOCK, ROWLOCK, HOLDLOCK) WHERE [Id] = {0}",
|
|
workOrderId)
|
|
.Select(workOrder => workOrder.Id)
|
|
.FirstOrDefaultAsync(cancellationToken);
|
|
}
|
|
}
|
|
}
|