using Polly;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Core.Observability;
namespace ZB.MOM.WW.OtOpcUa.Core.Resilience;
///
/// Executes driver-capability calls through a shared Polly pipeline. One invoker per
/// (DriverInstance, IDriver) pair; the underlying
/// is process-singleton so all invokers share its cache. Implements
/// so the Polly-free Runtime dispatch layer can consume it
/// through the abstraction (bound per-driver by ).
///
///
/// The Runtime dispatch layer (DriverInstanceActor) routes its capability calls
/// (IWritable.WriteAsync, ITagDiscovery.DiscoverAsync,
/// ISubscribable.SubscribeAsync/UnsubscribeAsync,
/// IAlarmSource.SubscribeAlarmsAsync/AcknowledgeAsync) through this invoker via the
/// seam. The UnwrappedCapabilityCallAnalyzer
/// (OTOPCUA0001) enforces that every guarded call is wrapped in an
/// / lambda (the interface's methods are registered wrapper
/// homes too, so an interface-typed invoker call suppresses the diagnostic).
///
public sealed class CapabilityInvoker : IDriverCapabilityInvoker
{
private readonly DriverResiliencePipelineBuilder _builder;
private readonly string _driverInstanceId;
private readonly string _driverType;
private readonly Func _optionsAccessor;
private readonly DriverResilienceStatusTracker? _statusTracker;
private readonly long _optionsGeneration;
private DriverResilienceOptions? _noRetryWriteOptions;
///
/// Construct an invoker for one driver instance.
///
/// Shared, process-singleton pipeline builder.
/// The DriverInstance.Id column value.
///
/// Snapshot accessor for the current resilience options. Invoked per call so Admin-edit +
/// pipeline-invalidate can take effect without restarting the invoker.
///
/// Driver type name for structured-log enrichment (e.g. "Modbus").
/// Optional resilience-status tracker. When wired, every capability call records start/complete so the AdminUI resilience panel can surface as the in-flight-call-depth proxy, and each successful call resets so the counter reflects only an active retry storm.
///
/// Options epoch for this invoker (01/S-7). Threaded into every pipeline-cache lookup so a lingering
/// old child's late re-cache can never poison a newer invoker's pipeline. Defaults to 0 so existing
/// callers/tests are unaffected.
///
public CapabilityInvoker(
DriverResiliencePipelineBuilder builder,
string driverInstanceId,
Func optionsAccessor,
string driverType = "Unknown",
DriverResilienceStatusTracker? statusTracker = null,
long optionsGeneration = 0)
{
ArgumentNullException.ThrowIfNull(builder);
ArgumentNullException.ThrowIfNull(optionsAccessor);
_builder = builder;
_driverInstanceId = driverInstanceId;
_driverType = driverType;
_optionsAccessor = optionsAccessor;
_statusTracker = statusTracker;
_optionsGeneration = optionsGeneration;
}
///
public async ValueTask ExecuteAsync(
DriverCapability capability,
string hostName,
Func> callSite,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(callSite);
var pipeline = ResolvePipeline(capability, hostName);
_statusTracker?.RecordCallStart(_driverInstanceId, hostName);
try
{
using (LogContextEnricher.Push(_driverInstanceId, _driverType, capability, LogContextEnricher.NewCorrelationId()))
{
var result = await pipeline.ExecuteAsync(callSite, cancellationToken).ConfigureAwait(false);
// Success resets ConsecutiveFailures so the counter is a true "consecutive" gauge
// (nonzero only during an active retry storm), not stuck at the last blip's count.
_statusTracker?.RecordSuccess(_driverInstanceId, hostName, DateTime.UtcNow);
return result;
}
}
finally
{
_statusTracker?.RecordCallComplete(_driverInstanceId, hostName);
}
}
///
public async ValueTask ExecuteAsync(
DriverCapability capability,
string hostName,
Func callSite,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(callSite);
var pipeline = ResolvePipeline(capability, hostName);
_statusTracker?.RecordCallStart(_driverInstanceId, hostName);
try
{
using (LogContextEnricher.Push(_driverInstanceId, _driverType, capability, LogContextEnricher.NewCorrelationId()))
{
await pipeline.ExecuteAsync(callSite, cancellationToken).ConfigureAwait(false);
_statusTracker?.RecordSuccess(_driverInstanceId, hostName, DateTime.UtcNow);
}
}
finally
{
_statusTracker?.RecordCallComplete(_driverInstanceId, hostName);
}
}
///
public async ValueTask ExecuteWriteAsync(
string hostName,
bool isIdempotent,
Func> callSite,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(callSite);
if (!isIdempotent)
{
// Build the no-retry options snapshot ONCE per invoker lifetime (01/P-4). The options are
// immutable for the invoker's lifetime — a ResilienceConfig change respawns the driver child,
// which constructs a fresh invoker (#13 respawn-on-change) — so lazily caching the snapshot is
// safe and removes the per-write allocation the previous once-per-call snapshot incurred. The
// null-check-assign tolerates a benign first-call race (both threads build an equivalent record).
var noRetryOptions = _noRetryWriteOptions ??= BuildNoRetryWriteOptions();
var pipeline = _builder.GetOrCreate(_driverInstanceId, $"{hostName}::non-idempotent", DriverCapability.Write, noRetryOptions, _optionsGeneration);
// Record in-flight accounting on the CALLER's host (the "::non-idempotent" suffix is a
// pipeline-cache key, not an operator-facing host) so Admin /hosts sees write depth too.
_statusTracker?.RecordCallStart(_driverInstanceId, hostName);
try
{
using (LogContextEnricher.Push(_driverInstanceId, _driverType, DriverCapability.Write, LogContextEnricher.NewCorrelationId()))
{
var result = await pipeline.ExecuteAsync(callSite, cancellationToken).ConfigureAwait(false);
_statusTracker?.RecordSuccess(_driverInstanceId, hostName, DateTime.UtcNow);
return result;
}
}
finally
{
_statusTracker?.RecordCallComplete(_driverInstanceId, hostName);
}
}
return await ExecuteAsync(DriverCapability.Write, hostName, callSite, cancellationToken).ConfigureAwait(false);
}
private DriverResilienceOptions BuildNoRetryWriteOptions()
{
var snapshot = _optionsAccessor();
return snapshot with
{
CapabilityPolicies = new Dictionary
{
[DriverCapability.Write] = snapshot.Resolve(DriverCapability.Write) with { RetryCount = 0 },
},
};
}
private ResiliencePipeline ResolvePipeline(DriverCapability capability, string hostName) =>
_builder.GetOrCreate(_driverInstanceId, hostName, capability, _optionsAccessor(), _optionsGeneration);
}