56 lines
2 KiB
C#
56 lines
2 KiB
C#
using w4c_workflows.Services;
|
|
using Xunit;
|
|
|
|
namespace w4c_workflows.Tests;
|
|
|
|
/// <summary>
|
|
/// P1-9: two overlapping syncs for the same <c>(tenant, repo)</c> must not run
|
|
/// concurrently, or they insert the same deterministic workflow/task ids and
|
|
/// collide on the primary key. The gate serializes that pair only — syncs for a
|
|
/// different repo/tenant stay parallel.
|
|
/// </summary>
|
|
public class SyncGateTests
|
|
{
|
|
[Fact]
|
|
public async Task Same_tenant_and_repo_waits_for_the_holder()
|
|
{
|
|
var gate = new SyncGate();
|
|
var held = await gate.AcquireAsync("tenant-a", "repo", default);
|
|
var waiter = gate.AcquireAsync("tenant-a", "repo", default);
|
|
|
|
Assert.False(waiter.IsCompleted);
|
|
|
|
held.Dispose();
|
|
using var acquired = await waiter.WaitAsync(TimeSpan.FromSeconds(2));
|
|
Assert.True(waiter.IsCompletedSuccessfully);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Different_keys_do_not_block_each_other()
|
|
{
|
|
var gate = new SyncGate();
|
|
using var first = await gate.AcquireAsync("tenant-a", "repo", default);
|
|
|
|
// Another repo for the same tenant (and another tenant for the same repo)
|
|
// must not wait behind the held gate.
|
|
using var otherRepo = await gate.AcquireAsync("tenant-a", "other-repo", default).WaitAsync(TimeSpan.FromSeconds(2));
|
|
using var otherTenant = await gate.AcquireAsync("tenant-b", "repo", default).WaitAsync(TimeSpan.FromSeconds(2));
|
|
|
|
Assert.NotNull(otherRepo);
|
|
Assert.NotNull(otherTenant);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Dispose_is_idempotent_and_releases_once()
|
|
{
|
|
var gate = new SyncGate();
|
|
var held = await gate.AcquireAsync("tenant-a", "repo", default);
|
|
held.Dispose();
|
|
held.Dispose();
|
|
|
|
// A second acquisition after the (double) release must still succeed.
|
|
using var next = await gate.AcquireAsync("tenant-a", "repo", default).WaitAsync(TimeSpan.FromSeconds(2));
|
|
Assert.NotNull(next);
|
|
}
|
|
}
|