bacea1a44f
Remediates RESILIENCE-DISPATCH-GAP (surfaced by the 07/C-1 analyzer): the Phase 6.1 CapabilityInvoker resilience pipeline (retry / breaker / bulkhead / telemetry) was constructed only in tests — the production dispatch layer called driver-capability methods directly, bypassing it entirely. The filed plan (thread CapabilityInvoker directly into DriverInstanceActor) is INFEASIBLE: Runtime is deliberately Polly-free (references Core.Abstractions, not Core — the same boundary IDriverFactory documents). So this introduces a seam, mirroring IDriverFactory exactly: - Core.Abstractions: IDriverCapabilityInvoker (+ NullDriverCapabilityInvoker pass-through) and IDriverCapabilityInvokerFactory (+ null factory). - Core: CapabilityInvoker now implements the interface; new DriverCapabilityInvokerFactory builds a per-instance invoker over the process-singleton pipeline builder + status tracker + tier resolver. - Runtime: DriverInstanceActor takes an IDriverCapabilityInvoker (default = pass-through) and routes all 6 dispatch sites (write, alarm-ack, subscribe, unsubscribe, alarm-subscribe, discover) through it; per-host key resolved via IPerCallHostResolver for single-ref calls, driver-instance key for bulk calls. DriverHostActor builds + injects the real invoker per spawned driver. - Host: DriverFactoryBootstrap registers the tracker, pipeline builder, and concrete factory (needs DriverFactoryRegistry.GetTier). Runtime SCE resolves the factory from DI like IDriverFactory; pass-through on nodes without it bound. - Analyzer: IDriverCapabilityInvoker.ExecuteAsync/ExecuteWriteAsync are now recognized wrapper homes (interface-typed invoker calls must suppress OTOPCUA0001 too). All 6 RESILIENCE-DISPATCH-GAP pragmas removed — the analyzer (an error in Runtime) is now the standing regression guard. Also adds retry / breaker-open / breaker-close LOGGING to the pipeline builder — the operator-facing observability surface (the Admin /hosts reader, Phase 6.1 Stream E.2/E.3, was never built, so the pipeline otherwise runs silently). Scope notes: - Tier-DEFAULT policy only: DriverInstanceSpec (the deploy artifact) does not carry the per-instance ResilienceConfig JSON, so overrides aren't applied yet (tracked follow-up: plumb ResilienceConfig through the composer/artifact). - GenericDriverNodeManager's call is NOT wired: that class is test-only scaffolding (zero production references — the Server has its own address-space path); its pragma is re-annotated accordingly, not left as a "tracked gap". Verification (unit + analyzer; live behavioral gate still pending — see FOLLOWUP-10): - Negative control: unwrapping any site fails the Runtime build with OTOPCUA0001. - New Runtime guard (recording invoker): write routes via ExecuteWriteAsync with the IPerCallHostResolver host; subscribe via ExecuteAsync — proves runtime routing. - New analyzer test: the interface is a valid wrapper home. - New pipeline-builder test: retry events are logged. - Full solution builds clean (0 errors); Runtime.Tests 357, Core.Tests 238, Analyzers.Tests 32 all green; pass-through default keeps existing dispatch tests byte-for-byte unchanged.
100 lines
5.5 KiB
C#
100 lines
5.5 KiB
C#
namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
|
|
|
/// <summary>
|
|
/// Abstraction over the Phase 6.1 resilience pipeline that wraps a single driver instance's
|
|
/// capability calls (retry / circuit-breaker / bulkhead / tracker telemetry). The Runtime
|
|
/// dispatch layer (<c>DriverInstanceActor</c>) consumes this interface instead of the concrete
|
|
/// <c>CapabilityInvoker</c> so the actor assembly does NOT pull in
|
|
/// <c>ZB.MOM.WW.OtOpcUa.Core</c> (which drags in Polly + driver hosting) — the same boundary
|
|
/// rationale as <see cref="IDriverFactory"/>. The fused Host binds a real invoker (built by
|
|
/// <see cref="IDriverCapabilityInvokerFactory"/>) per spawned driver; when none is supplied the
|
|
/// dispatch layer uses <see cref="NullDriverCapabilityInvoker"/>, a genuine pass-through.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// The method shapes mirror <c>CapabilityInvoker</c> exactly so the concrete invoker satisfies
|
|
/// this interface without adapters. Callers pass the resolved <c>hostName</c> per call (a
|
|
/// multi-host driver resolves it from the tag reference via <see cref="IPerCallHostResolver"/>;
|
|
/// a single-host driver passes its <see cref="IDriver.DriverInstanceId"/>) so per-host breaker
|
|
/// isolation + bulkhead-depth accounting key on the right target.
|
|
/// </remarks>
|
|
public interface IDriverCapabilityInvoker
|
|
{
|
|
/// <summary>Execute a value-returning capability call through the per-capability pipeline.</summary>
|
|
/// <typeparam name="TResult">Return type of the underlying driver call.</typeparam>
|
|
/// <param name="capability">The driver capability being executed.</param>
|
|
/// <param name="hostName">The resolved host name for pipeline keying + status tracking.</param>
|
|
/// <param name="callSite">The async function to execute (the raw driver-capability call).</param>
|
|
/// <param name="cancellationToken">Cancellation token for the operation.</param>
|
|
/// <returns>The result of the underlying driver call.</returns>
|
|
ValueTask<TResult> ExecuteAsync<TResult>(
|
|
DriverCapability capability,
|
|
string hostName,
|
|
Func<CancellationToken, ValueTask<TResult>> callSite,
|
|
CancellationToken cancellationToken);
|
|
|
|
/// <summary>Execute a void-returning capability call through the per-capability pipeline.</summary>
|
|
/// <param name="capability">The driver capability being executed.</param>
|
|
/// <param name="hostName">The resolved host name for pipeline keying + status tracking.</param>
|
|
/// <param name="callSite">The async function to execute (the raw driver-capability call).</param>
|
|
/// <param name="cancellationToken">Cancellation token for the operation.</param>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
ValueTask ExecuteAsync(
|
|
DriverCapability capability,
|
|
string hostName,
|
|
Func<CancellationToken, ValueTask> callSite,
|
|
CancellationToken cancellationToken);
|
|
|
|
/// <summary>
|
|
/// Execute a <see cref="DriverCapability.Write"/> call honoring idempotence semantics — when
|
|
/// <paramref name="isIdempotent"/> is <c>false</c>, retries are disabled regardless of the
|
|
/// configured policy; when <c>true</c>, the call runs through the Write pipeline which may
|
|
/// retry if the tier configuration permits.
|
|
/// </summary>
|
|
/// <typeparam name="TResult">Return type of the underlying driver write call.</typeparam>
|
|
/// <param name="hostName">The resolved host name for pipeline keying + status tracking.</param>
|
|
/// <param name="isIdempotent">Whether the write operation is safe to retry.</param>
|
|
/// <param name="callSite">The async function to execute (the raw driver write call).</param>
|
|
/// <param name="cancellationToken">Cancellation token for the operation.</param>
|
|
/// <returns>The result of the underlying driver write call.</returns>
|
|
ValueTask<TResult> ExecuteWriteAsync<TResult>(
|
|
string hostName,
|
|
bool isIdempotent,
|
|
Func<CancellationToken, ValueTask<TResult>> callSite,
|
|
CancellationToken cancellationToken);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Pass-through <see cref="IDriverCapabilityInvoker"/> that invokes the call site directly with
|
|
/// NO pipeline — no timeout, retry, breaker, or telemetry. Bound when the dispatch layer is
|
|
/// constructed without a real invoker (unit tests, the F7 skeleton path, admin-only nodes that
|
|
/// never run drivers). Keeps behavior byte-for-byte identical to a raw driver call so existing
|
|
/// dispatch-layer tests are unaffected by the resilience wiring.
|
|
/// </summary>
|
|
public sealed class NullDriverCapabilityInvoker : IDriverCapabilityInvoker
|
|
{
|
|
/// <summary>The shared singleton instance.</summary>
|
|
public static readonly NullDriverCapabilityInvoker Instance = new();
|
|
private NullDriverCapabilityInvoker() { }
|
|
|
|
/// <inheritdoc />
|
|
public ValueTask<TResult> ExecuteAsync<TResult>(
|
|
DriverCapability capability,
|
|
string hostName,
|
|
Func<CancellationToken, ValueTask<TResult>> callSite,
|
|
CancellationToken cancellationToken) => callSite(cancellationToken);
|
|
|
|
/// <inheritdoc />
|
|
public ValueTask ExecuteAsync(
|
|
DriverCapability capability,
|
|
string hostName,
|
|
Func<CancellationToken, ValueTask> callSite,
|
|
CancellationToken cancellationToken) => callSite(cancellationToken);
|
|
|
|
/// <inheritdoc />
|
|
public ValueTask<TResult> ExecuteWriteAsync<TResult>(
|
|
string hostName,
|
|
bool isIdempotent,
|
|
Func<CancellationToken, ValueTask<TResult>> callSite,
|
|
CancellationToken cancellationToken) => callSite(cancellationToken);
|
|
}
|