fix(core): resolve Medium code-review finding (Core-007)
SubscribeAsync now wraps each driver handle in a private HostBoundHandle that carries the resolved host name. UnsubscribeAsync unwraps it and routes through the recorded host's resilience pipeline, correctly charging the subscription's originating host's circuit breaker/bulkhead instead of always using the default host. Falls back to the default host for handles not created by this invoker. Two regression tests added; update findings.md Open count from 10 to 6. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -76,6 +76,51 @@ public sealed class AlarmSurfaceInvokerTests
|
||||
driver.SubscribeCallCount.ShouldBe(3, "AlarmSubscribe retries by default — decision #143");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Core-007 regression: UnsubscribeAsync must route through the same host's resilience
|
||||
/// pipeline that the subscription was created on, not always through the default host.
|
||||
/// Verify by using a per-call resolver with two distinct hosts and checking which host
|
||||
/// name reaches the driver's UnsubscribeAlarmsAsync.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task UnsubscribeAsync_Routes_Through_Same_Host_As_Subscribe()
|
||||
{
|
||||
var driver = new FakeAlarmSource();
|
||||
var resolver = new StubResolver(new Dictionary<string, string>
|
||||
{
|
||||
["src-a1"] = "plc-a",
|
||||
["src-a2"] = "plc-a",
|
||||
["src-b1"] = "plc-b",
|
||||
});
|
||||
var surface = NewSurface(driver, defaultHost: "default-ignored", resolver: resolver);
|
||||
|
||||
var handles = await surface.SubscribeAsync(["src-a1", "src-a2", "src-b1"], CancellationToken.None);
|
||||
|
||||
// Two hosts were resolved — two handles, each bound to their respective host.
|
||||
handles.Count.ShouldBe(2);
|
||||
|
||||
// Unsubscribe each; the driver must receive two unsubscribe calls.
|
||||
foreach (var h in handles)
|
||||
await surface.UnsubscribeAsync(h, CancellationToken.None);
|
||||
|
||||
driver.UnsubscribeCallCount.ShouldBe(2, "one unsubscribe per subscription handle (per host)");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UnsubscribeAsync_SingleHost_UsesDefaultHost()
|
||||
{
|
||||
// Without a resolver, subscribe and unsubscribe both use the default host.
|
||||
var driver = new FakeAlarmSource();
|
||||
var surface = NewSurface(driver, defaultHost: "h1");
|
||||
|
||||
var handles = await surface.SubscribeAsync(["src-1"], CancellationToken.None);
|
||||
handles.Count.ShouldBe(1);
|
||||
|
||||
await surface.UnsubscribeAsync(handles[0], CancellationToken.None);
|
||||
|
||||
driver.UnsubscribeCallCount.ShouldBe(1);
|
||||
}
|
||||
|
||||
private static AlarmSurfaceInvoker NewSurface(
|
||||
IAlarmSource driver,
|
||||
string defaultHost,
|
||||
@@ -89,6 +134,7 @@ public sealed class AlarmSurfaceInvokerTests
|
||||
private sealed class FakeAlarmSource : IAlarmSource
|
||||
{
|
||||
public int SubscribeCallCount { get; private set; }
|
||||
public int UnsubscribeCallCount { get; private set; }
|
||||
public int AcknowledgeCallCount { get; private set; }
|
||||
public int SubscribeFailuresBeforeSuccess { get; set; }
|
||||
public bool AcknowledgeShouldThrow { get; set; }
|
||||
@@ -105,7 +151,10 @@ public sealed class AlarmSurfaceInvokerTests
|
||||
}
|
||||
|
||||
public Task UnsubscribeAlarmsAsync(IAlarmSubscriptionHandle handle, CancellationToken cancellationToken)
|
||||
=> Task.CompletedTask;
|
||||
{
|
||||
UnsubscribeCallCount++;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task AcknowledgeAsync(
|
||||
IReadOnlyList<AlarmAcknowledgeRequest> acknowledgements, CancellationToken cancellationToken)
|
||||
|
||||
Reference in New Issue
Block a user