Phase 3C: Deployment pipeline & Store-and-Forward engine
Deployment Manager (WP-1–8, WP-16): - DeploymentService: full pipeline (flatten→validate→send→track→audit) - OperationLockManager: per-instance concurrency control - StateTransitionValidator: Enabled/Disabled/NotDeployed transition matrix - ArtifactDeploymentService: broadcast to all sites with per-site results - Deployment identity (GUID + revision hash), idempotency, staleness detection - Instance lifecycle commands (disable/enable/delete) with deduplication Store-and-Forward (WP-9–15): - StoreAndForwardStorage: SQLite persistence, 3 categories, no max buffer - StoreAndForwardService: fixed-interval retry, transient-only buffering, parking - ReplicationService: async best-effort to standby (fire-and-forget) - Parked message management (query/retry/discard from central) - Messages survive instance deletion, S&F drains on disable 620 tests pass (+79 new), zero warnings.
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
namespace ScadaLink.DeploymentManager.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// WP-3: Tests for per-instance operation lock.
|
||||
/// </summary>
|
||||
public class OperationLockManagerTests
|
||||
{
|
||||
private readonly OperationLockManager _lockManager = new();
|
||||
|
||||
[Fact]
|
||||
public async Task AcquireAsync_ReturnsDisposable()
|
||||
{
|
||||
using var lockHandle = await _lockManager.AcquireAsync("inst1", TimeSpan.FromSeconds(5));
|
||||
Assert.NotNull(lockHandle);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AcquireAsync_SameInstance_BlocksSecondCaller()
|
||||
{
|
||||
using var firstLock = await _lockManager.AcquireAsync("inst1", TimeSpan.FromSeconds(5));
|
||||
|
||||
// Second acquire should time out
|
||||
await Assert.ThrowsAsync<TimeoutException>(() =>
|
||||
_lockManager.AcquireAsync("inst1", TimeSpan.FromMilliseconds(50)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AcquireAsync_DifferentInstances_BothSucceed()
|
||||
{
|
||||
using var lock1 = await _lockManager.AcquireAsync("inst1", TimeSpan.FromSeconds(5));
|
||||
using var lock2 = await _lockManager.AcquireAsync("inst2", TimeSpan.FromSeconds(5));
|
||||
|
||||
Assert.NotNull(lock1);
|
||||
Assert.NotNull(lock2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AcquireAsync_AfterRelease_CanReacquire()
|
||||
{
|
||||
var firstLock = await _lockManager.AcquireAsync("inst1", TimeSpan.FromSeconds(5));
|
||||
firstLock.Dispose();
|
||||
|
||||
// Should succeed now
|
||||
using var secondLock = await _lockManager.AcquireAsync("inst1", TimeSpan.FromSeconds(5));
|
||||
Assert.NotNull(secondLock);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task IsLocked_ReturnsTrueWhileLocked()
|
||||
{
|
||||
Assert.False(_lockManager.IsLocked("inst1"));
|
||||
|
||||
using var lockHandle = await _lockManager.AcquireAsync("inst1", TimeSpan.FromSeconds(5));
|
||||
Assert.True(_lockManager.IsLocked("inst1"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task IsLocked_ReturnsFalseAfterRelease()
|
||||
{
|
||||
var lockHandle = await _lockManager.AcquireAsync("inst1", TimeSpan.FromSeconds(5));
|
||||
lockHandle.Dispose();
|
||||
|
||||
Assert.False(_lockManager.IsLocked("inst1"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AcquireAsync_DoubleDispose_DoesNotThrow()
|
||||
{
|
||||
var lockHandle = await _lockManager.AcquireAsync("inst1", TimeSpan.FromSeconds(5));
|
||||
lockHandle.Dispose();
|
||||
lockHandle.Dispose(); // Should not throw
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AcquireAsync_CancellationToken_Respected()
|
||||
{
|
||||
using var firstLock = await _lockManager.AcquireAsync("inst1", TimeSpan.FromSeconds(30));
|
||||
|
||||
using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(50));
|
||||
await Assert.ThrowsAnyAsync<OperationCanceledException>(() =>
|
||||
_lockManager.AcquireAsync("inst1", TimeSpan.FromSeconds(30), cts.Token));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AcquireAsync_ConcurrentDifferentInstances_AllSucceed()
|
||||
{
|
||||
var tasks = Enumerable.Range(0, 10).Select(async i =>
|
||||
{
|
||||
using var lockHandle = await _lockManager.AcquireAsync($"inst{i}", TimeSpan.FromSeconds(5));
|
||||
await Task.Delay(10);
|
||||
});
|
||||
|
||||
await Task.WhenAll(tasks);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user