джоба есть
This commit is contained in:
+120
@@ -0,0 +1,120 @@
|
||||
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 RequestTypeProvisioningLockTests(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<RequestTypeProvisioningLock>()
|
||||
.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_provisioning_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<RequestTypeProvisioningLock>()
|
||||
.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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user