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:
Joseph Doherty
2026-05-22 08:24:17 -04:00
parent 6cec98caef
commit ce86deca62
3 changed files with 89 additions and 16 deletions

View File

@@ -48,7 +48,9 @@ public sealed class AlarmSurfaceInvoker
/// <summary>
/// Subscribe to alarm events for a set of source node ids, fanning out by resolved host
/// so per-host breakers / bulkheads apply. Returns one handle per host — callers that
/// don't care about per-host separation may concatenate them.
/// don't care about per-host separation may concatenate them. Each returned handle wraps
/// the driver's opaque handle together with its resolved host so <see cref="UnsubscribeAsync"/>
/// routes through the same host's pipeline that the subscription was created on.
/// </summary>
public async Task<IReadOnlyList<IAlarmSubscriptionHandle>> SubscribeAsync(
IReadOnlyList<string> sourceNodeIds,
@@ -61,24 +63,34 @@ public sealed class AlarmSurfaceInvoker
var handles = new List<IAlarmSubscriptionHandle>(byHost.Count);
foreach (var (host, ids) in byHost)
{
var handle = await _invoker.ExecuteAsync(
var inner = await _invoker.ExecuteAsync(
DriverCapability.AlarmSubscribe,
host,
async ct => await _alarmSource.SubscribeAlarmsAsync(ids, ct).ConfigureAwait(false),
cancellationToken).ConfigureAwait(false);
handles.Add(handle);
handles.Add(new HostBoundHandle(inner, host));
}
return handles;
}
/// <summary>Cancel an alarm subscription. Routes through the AlarmSubscribe pipeline for parity.</summary>
/// <summary>
/// Cancel an alarm subscription. Routes through the same host's resilience pipeline
/// that the subscription was created on (carried in the <see cref="HostBoundHandle"/>
/// wrapper returned by <see cref="SubscribeAsync"/>). Falls back to the default host for
/// handles not created by this invoker so the method remains safe to call on any
/// <see cref="IAlarmSubscriptionHandle"/> implementation.
/// </summary>
public ValueTask UnsubscribeAsync(IAlarmSubscriptionHandle handle, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(handle);
var (innerHandle, host) = handle is HostBoundHandle bound
? (bound.Inner, bound.Host)
: (handle, _defaultHost);
return _invoker.ExecuteAsync(
DriverCapability.AlarmSubscribe,
_defaultHost,
async ct => await _alarmSource.UnsubscribeAlarmsAsync(handle, ct).ConfigureAwait(false),
host,
async ct => await _alarmSource.UnsubscribeAlarmsAsync(innerHandle, ct).ConfigureAwait(false),
cancellationToken);
}
@@ -126,4 +138,16 @@ public sealed class AlarmSurfaceInvoker
}
return result;
}
/// <summary>
/// Wraps an <see cref="IAlarmSubscriptionHandle"/> returned by the driver with the
/// resolved host name used when the subscription was created. <see cref="UnsubscribeAsync"/>
/// unwraps this to route the unsubscribe through the same host's resilience pipeline.
/// </summary>
private sealed class HostBoundHandle(IAlarmSubscriptionHandle inner, string host) : IAlarmSubscriptionHandle
{
public IAlarmSubscriptionHandle Inner { get; } = inner;
public string Host { get; } = host;
public string DiagnosticId => Inner.DiagnosticId;
}
}