feat(processing): persist safe atomic operation history
This commit is contained in:
@@ -79,10 +79,11 @@ public sealed class StartupAndLockTests(ITestOutputHelper output)
|
||||
db.Add(job);
|
||||
await db.SaveChangesAsync();
|
||||
var migrations = (await db.Database.GetAppliedMigrationsAsync()).ToArray();
|
||||
Assert.Equal("20261026090000_AddCvImportReviewEvidence", migrations[^1]);
|
||||
Assert.Equal("20261028090000_AddUserOperationEvents", migrations[^1]);
|
||||
// Only this test's newly-created schema is downgraded; no existing database is selected.
|
||||
await db.GetService<IMigrator>().MigrateAsync(migrations[^3]);
|
||||
Assert.Equal(migrations[^2..], await db.Database.GetPendingMigrationsAsync());
|
||||
const string beforeRd21 = "20261021090000_AddDashboardLayoutPreference";
|
||||
await db.GetService<IMigrator>().MigrateAsync(beforeRd21);
|
||||
Assert.Equal(migrations.SkipWhile(migration => migration != beforeRd21).Skip(1), await db.Database.GetPendingMigrationsAsync());
|
||||
db.ChangeTracker.Clear();
|
||||
await database.App.InitializeJobTrackerAsync();
|
||||
await using var verify = database.Context();
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
using JobTrackerApi.Models;
|
||||
using JobTrackerApi.Services;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Xunit;
|
||||
|
||||
namespace JobTrackerApi.MariaDb.Tests;
|
||||
|
||||
public sealed class UserOperationHistoryPersistenceTests
|
||||
{
|
||||
private static CreateUserOperation Request(string key) => new("cv.process", key, "authorized", "local_only");
|
||||
|
||||
[Fact]
|
||||
public async Task Concurrent_claim_and_repeated_stage_have_one_durable_event_each()
|
||||
{
|
||||
await using var database = await MariaDbDatabase.CreateAsync();
|
||||
await using (var owner = database.Context())
|
||||
await new UserOperationStore(owner, TimeProvider.System).CreateAsync(Request("history-concurrency"), default);
|
||||
await using var first = database.Context(null!);
|
||||
await using var second = database.Context(null!);
|
||||
var claims = await Task.WhenAll(new UserOperationStore(first, TimeProvider.System).ClaimNextAsync(TimeSpan.FromSeconds(30), default),
|
||||
new UserOperationStore(second, TimeProvider.System).ClaimNextAsync(TimeSpan.FromSeconds(30), default));
|
||||
var lease = Assert.Single(claims.OfType<UserOperationLease>());
|
||||
await using var ownerOne = database.Context();
|
||||
await using var ownerTwo = database.Context();
|
||||
var stages = await Task.WhenAll(new UserOperationStore(ownerOne, TimeProvider.System).ReportStageAsync(lease.OperationId, lease.LeaseToken, "cv_extracting", default),
|
||||
new UserOperationStore(ownerTwo, TimeProvider.System).ReportStageAsync(lease.OperationId, lease.LeaseToken, "cv_extracting", default));
|
||||
Assert.All(stages, count => Assert.Equal(1, count));
|
||||
await using var readback = database.Context();
|
||||
var events = await readback.UserOperationEvents.OrderBy(item => item.Id).ToListAsync();
|
||||
Assert.Equal(new[] { "Created", "Claimed", "StageChanged" }, events.Select(item => item.Kind));
|
||||
Assert.All(events, item => Assert.Equal("rd21-owner", item.OwnerUserId));
|
||||
await using var other = database.Context("other");
|
||||
Assert.Empty(await other.UserOperationEvents.ToListAsync());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Event_insert_failure_rolls_back_completion_and_notifications_in_MariaDB()
|
||||
{
|
||||
await using var database = await MariaDbDatabase.CreateAsync();
|
||||
await using var owner = database.Context();
|
||||
var store = new UserOperationStore(owner, TimeProvider.System);
|
||||
var id = (await store.CreateAsync(Request("history-rollback"), default)).Operation.Id;
|
||||
await using var worker = database.Context(null!);
|
||||
var lease = Assert.IsType<UserOperationLease>(await new UserOperationStore(worker, TimeProvider.System).ClaimNextAsync(TimeSpan.FromSeconds(30), default));
|
||||
await owner.Database.ExecuteSqlRawAsync("CREATE TRIGGER fail_history BEFORE INSERT ON UserOperationEvents FOR EACH ROW SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'synthetic event insert failure';");
|
||||
await Assert.ThrowsAnyAsync<Exception>(() => store.CompleteAsync(id, lease.LeaseToken, null, default));
|
||||
await using var readback = database.Context();
|
||||
Assert.Equal(OperationStatuses.Running, (await readback.UserOperations.SingleAsync()).Status);
|
||||
Assert.Equal(2, await readback.UserOperationEvents.CountAsync());
|
||||
Assert.Empty(await readback.UserNotifications.ToListAsync());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user