00957bf384
Adds a lease-loss/reacquire integration rehearsal and fixes Dapper due-schedule materialization with an explicit row DTO. Evidence: PostgreSQL test 1/1 passed; TRX SHA256 49627FF0180034D2A7A1E4393448C73D337D918E7CE47EA9FC2BDB144FBBA833.
79 lines
3.8 KiB
C#
79 lines
3.8 KiB
C#
using Dapper;
|
|
using KArtSell.BuildingBlocks.Data;
|
|
using KArtSell.Modules.ModelOperations.Infrastructure;
|
|
using Npgsql;
|
|
using Xunit;
|
|
|
|
namespace KArtSell.Integration.Tests.Scheduling;
|
|
|
|
[Collection("Database")]
|
|
public sealed class ModelScheduleCasTests : IAsyncLifetime
|
|
{
|
|
private readonly NpgsqlDataSource _dataSource = new NpgsqlDataSourceBuilder(TestDatabaseConnection.GetConnectionString()).Build();
|
|
|
|
public async Task InitializeAsync()
|
|
{
|
|
await using var connection = await _dataSource.OpenConnectionAsync();
|
|
await connection.ExecuteAsync("select 1");
|
|
}
|
|
|
|
public Task DisposeAsync() => _dataSource.DisposeAsync().AsTask();
|
|
|
|
[Fact]
|
|
public async Task Stale_dispatch_revision_cannot_advance_a_reacquired_schedule()
|
|
{
|
|
var scheduleId = Guid.NewGuid();
|
|
var scheduledFor = new DateTimeOffset(2026, 8, 9, 0, 0, 0, TimeSpan.Zero);
|
|
const string firstOwner = "cas-test:first";
|
|
const string secondOwner = "cas-test:second";
|
|
var repository = new DapperModelScheduleRepository(new NpgsqlConnectionFactory(_dataSource));
|
|
|
|
try
|
|
{
|
|
await using (var connection = await _dataSource.OpenConnectionAsync())
|
|
{
|
|
await connection.ExecuteAsync("""
|
|
insert into evaluation.model_operation_schedule
|
|
(schedule_id, operation_code, operation_name, scope_key, cadence, automation_mode,
|
|
queue_name, schedule_version, enabled, next_due_at, max_lag, primary_owner, secondary_owner)
|
|
values
|
|
(@ScheduleId, 'J31', 'CasTest', @ScopeKey, 'DAILY', 'EVALUATION_ONLY',
|
|
'q-control', 1, true, @ScheduledFor, interval '1 hour', 'BE', 'QA');
|
|
""", new { ScheduleId = scheduleId, ScopeKey = $"CAS-{scheduleId:N}", ScheduledFor = scheduledFor });
|
|
}
|
|
|
|
var first = Assert.Single(await repository.AcquireDueAsync(scheduledFor.AddMinutes(1), firstOwner, TimeSpan.FromMinutes(5), 1, CancellationToken.None));
|
|
|
|
await using (var connection = await _dataSource.OpenConnectionAsync())
|
|
{
|
|
await connection.ExecuteAsync("""
|
|
update evaluation.model_operation_schedule
|
|
set lease_until = @ExpiredAt
|
|
where schedule_id = @ScheduleId;
|
|
""", new { ScheduleId = scheduleId, ExpiredAt = scheduledFor });
|
|
}
|
|
|
|
var second = Assert.Single(await repository.AcquireDueAsync(scheduledFor.AddMinutes(2), secondOwner, TimeSpan.FromMinutes(5), 1, CancellationToken.None));
|
|
Assert.True(second.DispatchRevision > first.DispatchRevision);
|
|
|
|
await Assert.ThrowsAsync<InvalidOperationException>(() => repository.MarkDispatchedAsync(
|
|
scheduleId, firstOwner, first.DispatchRevision, "stale-job", scheduledFor.AddMinutes(3), scheduledFor.AddDays(1), CancellationToken.None));
|
|
|
|
await using var verify = await _dataSource.OpenConnectionAsync();
|
|
var row = await verify.QuerySingleAsync<(DateTimeOffset NextDueAt, string LeaseOwner, int DispatchRevision)>("""
|
|
select next_due_at as NextDueAt, lease_owner as LeaseOwner, dispatch_revision as DispatchRevision
|
|
from evaluation.model_operation_schedule
|
|
where schedule_id = @ScheduleId;
|
|
""", new { ScheduleId = scheduleId });
|
|
Assert.Equal(scheduledFor, row.NextDueAt);
|
|
Assert.Equal(secondOwner, row.LeaseOwner);
|
|
Assert.Equal(second.DispatchRevision, row.DispatchRevision);
|
|
}
|
|
finally
|
|
{
|
|
await using var cleanup = await _dataSource.OpenConnectionAsync();
|
|
await cleanup.ExecuteAsync("delete from evaluation.model_operation_schedule where schedule_id = @ScheduleId", new { ScheduleId = scheduleId });
|
|
}
|
|
}
|
|
}
|