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);
|
}
|
}
|
}
|