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:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user