using System; using System.IO; using System.Threading; using System.Threading.Tasks; using Aging.Infrastructure.Core; using Aging.ResourceContracts; using Xunit; namespace Aging.MvpTests { public class DbLeaseStoreTests : IDisposable { private readonly string _dbPath; private readonly DbLeaseStore _store; public DbLeaseStoreTests() { _dbPath = Path.Combine(Path.GetTempPath(), "arb_test_" + Guid.NewGuid().ToString("N") + ".db"); _store = new DbLeaseStore(_dbPath, "local-main"); } public void Dispose() { try { File.Delete(_dbPath); File.Delete(_dbPath + "-wal"); File.Delete(_dbPath + "-shm"); } catch { } } private AcquireRequest Req(string key, OwnerTypeV3 type = OwnerTypeV3.Maintenance, int ttl = 30) => new AcquireRequest { OwnerId = "test-owner", OwnerType = type, ProcessInstanceId = Guid.NewGuid().ToString("N"), ResourceKeys = new[] { key }, TtlSeconds = ttl }; [Fact] public void Acquire_FirstOwner_Succeeds() { var r = _store.Acquire(Req("can/can02")); Assert.Equal(AcquireResultCode.Acquired, r.Code); Assert.Equal(1, r.Grants[0].Generation); } [Fact] public void Acquire_WhileActiveByOther_ReturnsConflictWithHolder() { _store.Acquire(Req("can/can02")); var second = new AcquireRequest { OwnerId = "other-owner", OwnerType = OwnerTypeV3.Production, ProcessInstanceId = "x", ResourceKeys = new[] { "can/can02" } }; var r = _store.Acquire(second); Assert.Equal(AcquireResultCode.ConflictHeld, r.Code); Assert.Equal("test-owner", r.Holder.OwnerId); } [Fact] public void TwoIndependentStores_SameFile_SimulatesTwoProcesses() { var storeB = new DbLeaseStore(_dbPath, "local-main"); var a = _store.Acquire(Req("psu/psu01/ch/5")); var b = storeB.Acquire(new AcquireRequest { OwnerId = "W02", OwnerType = OwnerTypeV3.Production, ProcessInstanceId = "p2", ResourceKeys = new[] { "psu/psu01/ch/5" } }); Assert.Equal(AcquireResultCode.Acquired, a.Code); Assert.Equal(AcquireResultCode.ConflictHeld, b.Code); } [Fact] public void Concurrent_50Threads_OnlyOneWins() { int winners = 0; Parallel.For(0, 50, _ => { var s = new DbLeaseStore(_dbPath, "local-main"); var r = s.Acquire(new AcquireRequest { OwnerId = Guid.NewGuid().ToString("N"), OwnerType = OwnerTypeV3.Production, ProcessInstanceId = "p", ResourceKeys = new[] { "can/bus" }, WaitTimeoutMs = 3000 }); if (r.Code == AcquireResultCode.Acquired) Interlocked.Increment(ref winners); }); Assert.Equal(1, winners); } [Fact] public void ExpiredLease_EntersRecoveryRequired_NotAutoReclaim() { _store.Acquire(Req("can/can02", ttl: -1)); _store.SweepExpired(); var r = _store.Acquire(Req("can/can02", OwnerTypeV3.Production)); Assert.Equal(AcquireResultCode.RecoveryBlocked, r.Code); Assert.True(_store.ApproveRecovery("can/can02", "{\"output\":\"off\"}", "admin")); var again = _store.Acquire(Req("can/can02", OwnerTypeV3.Production)); Assert.Equal(AcquireResultCode.Acquired, again.Code); Assert.Equal(2, again.Grants[0].Generation); } [Fact] public void Renew_WithStaleGeneration_Fails() { var g = _store.Acquire(Req("can/can02")).Grants[0]; var stale = new LeaseGrant(g.AuthorityId, g.ResourceKey, g.LeaseId, g.OwnerId, g.OwnerType, g.Epoch, 999, g.ExpiresUtcMs); Assert.False(_store.Renew(stale, 30)); Assert.True(_store.Renew(g, 30)); } [Fact] public void BumpEpoch_InvalidatesAllActiveGrants() { var g = _store.Acquire(Req("can/can02")).Grants[0]; _store.BumpEpochForTest("arb.db 重启模拟"); Assert.False(_store.Renew(g, 30)); } [Fact] public void GroupAcquire_PartialConflict_RollsBackAll() { _store.Acquire(Req("psu/b")); var r = _store.Acquire(new AcquireRequest { OwnerId = "W09", OwnerType = OwnerTypeV3.Production, ProcessInstanceId = "p", ResourceKeys = new[] { "psu/a", "psu/b", "psu/c" } }); Assert.Equal(AcquireResultCode.ConflictHeld, r.Code); Assert.Null(_store.ProbeConflict("psu/a")); } [Fact] public void ExpiredLease_ReconcileAfterBoot_FreesResource_ForNewAcquire() { // 模拟旧 owner 进程未释放就异常退出:短 TTL 租约泄漏后过期 var leaked = _store.Acquire(Req("can/can99", OwnerTypeV3.Maintenance, ttl: 1)); Assert.Equal(AcquireResultCode.Acquired, leaked.Code); Thread.Sleep(1600); // 未对账前:过期租约进入 RecoveryBlocked(永久阻塞现状) var blocked = _store.Acquire(new AcquireRequest { OwnerId = "new-owner", OwnerType = OwnerTypeV3.Maintenance, ProcessInstanceId = "y", ResourceKeys = new[] { "can/can99" } }); Assert.Equal(AcquireResultCode.RecoveryBlocked, blocked.Code); // 启动对账自愈(INV-03 自动化形式,证据落 recovery_checkpoint) Assert.True(_store.ReconcileAfterBoot("HOST/999") >= 1); Assert.Equal(LeasePhase.Free, _store.PhaseOf("can/can99")); // 对账后可重新获取,且 Generation 只前进不回退 var reacquired = _store.Acquire(new AcquireRequest { OwnerId = "new-owner", OwnerType = OwnerTypeV3.Maintenance, ProcessInstanceId = "y", ResourceKeys = new[] { "can/can99" } }); Assert.Equal(AcquireResultCode.Acquired, reacquired.Code); Assert.True(reacquired.Grants[0].Generation > leaked.Grants[0].Generation); } } }