52 lines
3.1 KiB
C#
52 lines
3.1 KiB
C#
using Microsoft.EntityFrameworkCore;
|
|
|
|
namespace Tnsu.ApprovalRoute.Infrastructure.Persistence;
|
|
|
|
/// <summary>
|
|
/// Блокировка «по типу заявок идёт создание следующей партиции списка» на строке
|
|
/// request_types.
|
|
///
|
|
/// Почему сырой SQL, а не обычный EF: через change tracker это было бы
|
|
/// read-modify-write (прочитали флаг → проверили → записали), и две задачи,
|
|
/// прочитавшие null одновременно, обе решили бы, что блокировка свободна.
|
|
/// Здесь проверка и захват — ОДИН условный UPDATE, атомарный на уровне строки:
|
|
/// Postgres берёт row lock на время оператора, поэтому ровно одна из
|
|
/// конкурирующих задач увидит affected rows = 1, остальные — 0.
|
|
///
|
|
/// Никакого pg_advisory_lock: он живёт в рамках сессии, а нам нужно, чтобы
|
|
/// блокировку было видно в самой строке (админке — что провижининг идёт) и чтобы
|
|
/// она переживала возврат соединения в пул между шагами задачи.
|
|
/// </summary>
|
|
public sealed class ListPartitioningLock(ApprovalRouteDbContext db)
|
|
{
|
|
/// <summary>
|
|
/// Попытаться захватить блокировку. true — захватили (и только тогда можно
|
|
/// провижинить), false — держит кто-то другой либо типа заявок нет.
|
|
///
|
|
/// Блокировка старше staleAfter считается брошенной (процесс упал, не сняв
|
|
/// флаг) и перехватывается.
|
|
/// </summary>
|
|
public async Task<bool> TryAcquireAsync(int requestTypeId, TimeSpan staleAfter, CancellationToken cancellationToken)
|
|
{
|
|
// Имена таблицы/колонки — snake_case из UseSnakeCaseNamingConvention
|
|
// (см. InfrastructureServiceCollectionExtensions).
|
|
var affected = await db.Database.ExecuteSqlInterpolatedAsync(
|
|
$"""
|
|
UPDATE request_types
|
|
SET list_partitioning_started_at = now()
|
|
WHERE id = {requestTypeId}
|
|
AND (list_partitioning_started_at IS NULL
|
|
OR list_partitioning_started_at < now() - CAST({staleAfter} AS interval))
|
|
""",
|
|
cancellationToken);
|
|
|
|
return affected == 1;
|
|
}
|
|
|
|
/// <summary>Снять блокировку. Идемпотентно: снятая блокировка снимается повторно без ошибки.</summary>
|
|
public Task ReleaseAsync(int requestTypeId, CancellationToken cancellationToken) =>
|
|
db.Database.ExecuteSqlInterpolatedAsync(
|
|
$"UPDATE request_types SET list_partitioning_started_at = NULL WHERE id = {requestTypeId}",
|
|
cancellationToken);
|
|
}
|