Files
lmxopcua/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IDriverCapabilityInvokerFactory.cs
T
Joseph Doherty 75403caa1a feat(archreview #13): apply per-instance ResilienceConfig from the deploy artifact
The DriverInstance.ResilienceConfig column was authored in AdminUI, persisted to the
entity, and serialized into the deployment artifact by ConfigComposer — but the runtime
read path dropped it: DriverInstanceSpec didn't carry it and the invoker factory always
passed null, so every driver got tier defaults regardless of its configured overrides
(a silent dead-config gap — #10's residual sub-finding).

Read-path plumbing (write side was already complete):
- DriverInstanceSpec gains ResilienceConfig; DeploymentArtifact.TryReadSpec reads the column.
- IDriverCapabilityInvokerFactory.Create takes resilienceConfigJson; DriverHostActor.SpawnChild
  threads spec.ResilienceConfig; the concrete factory parses it (ParseOrDefaults, layering on the
  tier), logs any parse diagnostic (never throws), and builds the invoker with the merged options.
- Invalidate-on-change: the pipeline cache keys on (instance, host, capability) and ignores options
  on a hit, so Create() now Invalidate()s the instance's cached pipelines first (no-op on first
  spawn) — a respawn with changed options rebuilds them.
- DriverSpawnPlanner treats a ResilienceConfig change as a stop+respawn (the invoker/options are
  bound to the child at spawn); a pure DriverConfig change stays an in-place delta (no reconnect).
- Host DI passes a logger to the factory for the parse diagnostic.

Verification (deterministic): factory Create applies a retryCount:0 override to actual execution
(control test proves tier default retries), invalidates the instance's cache on re-create (scoped —
sibling survives), malformed config logs+falls-back; planner respawns on ResilienceConfig change
(incl null→json) and stays delta on a pure config change; artifact parse carries/omits the column.
Core.Tests 243 (+5), Runtime.Tests 363 (+5), Host builds clean.
2026-07-08 22:48:17 -04:00

48 lines
2.8 KiB
C#

namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions;
/// <summary>
/// Builds a per-driver-instance <see cref="IDriverCapabilityInvoker"/>. The Runtime
/// <c>DriverHostActor</c> consumes this factory (resolved from DI, like <see cref="IDriverFactory"/>)
/// to attach a resilience invoker to each spawned <c>DriverInstanceActor</c> WITHOUT the actor
/// assembly referencing <c>ZB.MOM.WW.OtOpcUa.Core</c> (which drags in Polly). The fused Host
/// binds a concrete factory closing over the process-singleton pipeline builder + status tracker +
/// driver-tier resolver; when unbound, <see cref="NullDriverCapabilityInvokerFactory"/> yields the
/// pass-through invoker so dispatch behaves exactly as before.
/// </summary>
public interface IDriverCapabilityInvokerFactory
{
/// <summary>
/// Create the resilience invoker for one driver instance. The returned invoker is keyed on
/// <paramref name="driverInstanceId"/> and applies the tier policy resolved from
/// <paramref name="driverType"/>, layering any per-instance
/// <paramref name="resilienceConfigJson"/> overrides on top.
/// </summary>
/// <param name="driverInstanceId">The driver instance identifier (pipeline + tracker key).</param>
/// <param name="driverType">The driver type name, used to resolve the resilience tier defaults.</param>
/// <param name="resilienceConfigJson">
/// Optional per-instance <c>DriverInstance.ResilienceConfig</c> JSON (from the deployment artifact)
/// layered on top of the tier defaults. <c>null</c>/empty ⇒ pure tier defaults. A concrete factory
/// invalidates any cached pipelines for <paramref name="driverInstanceId"/> so a changed config
/// applied across a driver respawn takes effect (the pipeline cache is keyed on instance/host/
/// capability, not options).
/// </param>
/// <returns>A per-instance <see cref="IDriverCapabilityInvoker"/>.</returns>
IDriverCapabilityInvoker Create(string driverInstanceId, string driverType, string? resilienceConfigJson = null);
}
/// <summary>
/// Returns <see cref="NullDriverCapabilityInvoker.Instance"/> for every driver instance. Bound
/// when the fused Host hasn't registered a real resilience factory (Mac dev path, smoke tests,
/// admin-only nodes). Dispatch then routes calls straight through with no pipeline.
/// </summary>
public sealed class NullDriverCapabilityInvokerFactory : IDriverCapabilityInvokerFactory
{
/// <summary>The shared singleton instance.</summary>
public static readonly NullDriverCapabilityInvokerFactory Instance = new();
private NullDriverCapabilityInvokerFactory() { }
/// <inheritdoc />
public IDriverCapabilityInvoker Create(string driverInstanceId, string driverType, string? resilienceConfigJson = null) =>
NullDriverCapabilityInvoker.Instance;
}