82512eb1db
Co-authored-by: gorbenko.pavel <gorbenko.pavel@tansu.kz> Reviewed-on: http://git.tnsu.kz/gorbenko.pavel/tnsu-approval-route/pulls/13
121 lines
5.1 KiB
C#
121 lines
5.1 KiB
C#
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Shouldly;
|
|
using Tnsu.ApprovalRoute.Domain.Entities;
|
|
using Tnsu.ApprovalRoute.Infrastructure.Persistence;
|
|
using Tnsu.ApprovalRoute.IntegrationTests.Infrastructure;
|
|
|
|
namespace Tnsu.ApprovalRoute.IntegrationTests.ApplicationServices;
|
|
|
|
/// <summary>
|
|
/// Блокировка провижининга — единственное, что не даёт двум фоновым задачам создать
|
|
/// по партиции каждая, поэтому проверяется именно на живом Postgres: смысл здесь
|
|
/// несёт атомарность условного UPDATE, а не код на C#.
|
|
/// </summary>
|
|
public sealed class ListPartitioningLockTests(PostgresDatabaseFixture fixture)
|
|
: IClassFixture<PostgresDatabaseFixture>
|
|
{
|
|
private static readonly TimeSpan StaleAfter = TimeSpan.FromMinutes(30);
|
|
|
|
private readonly ServiceProvider _provider = TestServiceProviderFactory.Build(fixture.ConnectionString);
|
|
|
|
[Fact(DisplayName = "Повторный захват занятой блокировки не удаётся")]
|
|
public async Task TryAcquire_WhenAlreadyHeld_ReturnsFalse()
|
|
{
|
|
// Arrange
|
|
var requestTypeId = await CreateRequestTypeAsync();
|
|
|
|
// Action
|
|
var first = await TryAcquireAsync(requestTypeId);
|
|
var second = await TryAcquireAsync(requestTypeId);
|
|
|
|
// Assert
|
|
first.ShouldBeTrue();
|
|
second.ShouldBeFalse();
|
|
}
|
|
|
|
[Fact(DisplayName = "При одновременном захвате блокировку получает ровно одна задача")]
|
|
public async Task TryAcquire_Concurrently_OnlyOneWins()
|
|
{
|
|
// Arrange
|
|
var requestTypeId = await CreateRequestTypeAsync();
|
|
|
|
// Action — каждый захват из своего scope, то есть своим соединением к БД:
|
|
// именно так это и происходит у параллельных задач Hangfire.
|
|
var results = await Task.WhenAll(
|
|
Enumerable.Range(0, 8).Select(_ => TryAcquireAsync(requestTypeId)));
|
|
|
|
// Assert
|
|
results.Count(x => x).ShouldBe(1);
|
|
}
|
|
|
|
[Fact(DisplayName = "После снятия блокировку можно захватить снова")]
|
|
public async Task TryAcquire_AfterRelease_Succeeds()
|
|
{
|
|
// Arrange
|
|
var requestTypeId = await CreateRequestTypeAsync();
|
|
(await TryAcquireAsync(requestTypeId)).ShouldBeTrue();
|
|
|
|
// Action
|
|
using (var scope = _provider.CreateScope())
|
|
{
|
|
await scope.ServiceProvider.GetRequiredService<ListPartitioningLock>()
|
|
.ReleaseAsync(requestTypeId, CancellationToken.None);
|
|
}
|
|
|
|
// Assert
|
|
(await TryAcquireAsync(requestTypeId)).ShouldBeTrue();
|
|
}
|
|
|
|
[Fact(DisplayName = "Просроченная блокировка перехватывается, свежая — нет")]
|
|
public async Task TryAcquire_StaleLock_IsTakenOver()
|
|
{
|
|
// Arrange
|
|
var requestTypeId = await CreateRequestTypeAsync();
|
|
(await TryAcquireAsync(requestTypeId)).ShouldBeTrue();
|
|
|
|
// Свежую блокировку с тем же порогом перехватить нельзя.
|
|
(await TryAcquireAsync(requestTypeId, TimeSpan.FromMinutes(10))).ShouldBeFalse();
|
|
|
|
// Action — отодвигаем метку в прошлое: так выглядит блокировка, которую
|
|
// оставил упавший, не снявший её процесс.
|
|
using (var scope = _provider.CreateScope())
|
|
{
|
|
var db = scope.ServiceProvider.GetRequiredService<ApprovalRouteDbContext>();
|
|
await db.Database.ExecuteSqlInterpolatedAsync(
|
|
$"UPDATE request_types SET list_partitioning_started_at = now() - interval '1 hour' WHERE id = {requestTypeId}");
|
|
}
|
|
|
|
// Assert
|
|
(await TryAcquireAsync(requestTypeId, TimeSpan.FromMinutes(10))).ShouldBeTrue();
|
|
}
|
|
|
|
[Fact(DisplayName = "Захват блокировки для несуществующего типа заявки не удаётся")]
|
|
public async Task TryAcquire_UnknownRequestType_ReturnsFalse() =>
|
|
(await TryAcquireAsync(999999)).ShouldBeFalse();
|
|
|
|
private async Task<bool> TryAcquireAsync(int requestTypeId, TimeSpan? staleAfter = null)
|
|
{
|
|
using var scope = _provider.CreateScope();
|
|
return await scope.ServiceProvider.GetRequiredService<ListPartitioningLock>()
|
|
.TryAcquireAsync(requestTypeId, staleAfter ?? StaleAfter, CancellationToken.None);
|
|
}
|
|
|
|
private async Task<int> CreateRequestTypeAsync()
|
|
{
|
|
using var scope = _provider.CreateScope();
|
|
var db = scope.ServiceProvider.GetRequiredService<ApprovalRouteDbContext>();
|
|
|
|
var entity = new RequestType
|
|
{
|
|
Name = $"Тип {Guid.NewGuid():N}",
|
|
PageUrl = "/test",
|
|
Section = "General"
|
|
};
|
|
db.RequestTypes.Add(entity);
|
|
await db.SaveChangesAsync();
|
|
|
|
return entity.Id;
|
|
}
|
|
}
|