4.9 KiB
Follow-up #10 — Wire CapabilityInvoker into the production dispatch layer (RESILIENCE-DISPATCH-GAP)
Status: OPEN · Surfaced by: guard 07/C-1 (wiring the OTOPCUA0001 analyzer) on
fix/archreview-c1-wire-analyzerf0082af5· Task: TaskCreate #10 · Severity: likely High · Effort: M–L · Risk: Medium (behavior-changing hot path). SeeSTATUS.md.
The finding
Wiring the custom UnwrappedCapabilityCallAnalyzer (OTOPCUA0001) tree-wide surfaced that the entire
Phase 6.1 CapabilityInvoker resilience pipeline is constructed only in tests — it is instantiated
nowhere in production. The production dispatch layer calls driver-capability methods
(IReadable/IWritable/ISubscribable/ITagDiscovery/IAlarmSource) directly, bypassing the
retry / circuit-breaker / bulkhead / tracker-telemetry pipeline entirely.
CapabilityInvoker's own XML doc falsely claims: "The server's dispatch layer routes every capability
call … through this invoker." It does not. This is the exact "built-but-never-wired" class the whole
arch review targets — the analyzer did its job on the first wiring.
Evidence
grep -rn "new CapabilityInvoker"→ onlytests/Core/…(7 test files). Zero production constructions.DriverInstanceActorholds a rawIDriver _driverand calls it directly at 6 sites.GenericDriverNodeManager.BuildAddressSpaceAsynccallsdiscovery.DiscoverAsyncdirectly (1 site).
The 7 deferred sites (all marked #pragma warning disable OTOPCUA0001 // RESILIENCE-DISPATCH-GAP)
Grep the marker to find them: grep -rn "RESILIENCE-DISPATCH-GAP" src.
| File | ~line | Call |
|---|---|---|
src/Server/…/Runtime/Drivers/DriverInstanceActor.cs |
575 | writable.WriteAsync |
src/Server/…/Runtime/Drivers/DriverInstanceActor.cs |
623 | src.AcknowledgeAsync |
src/Server/…/Runtime/Drivers/DriverInstanceActor.cs |
660 | subscribable.SubscribeAsync |
src/Server/…/Runtime/Drivers/DriverInstanceActor.cs |
685 | subscribable.UnsubscribeAsync |
src/Server/…/Runtime/Drivers/DriverInstanceActor.cs |
762 | src.SubscribeAlarmsAsync |
src/Server/…/Runtime/Drivers/DriverInstanceActor.cs |
821 | discovery.DiscoverAsync |
src/Core/…/Core/OpcUa/GenericDriverNodeManager.cs |
71 | discovery.DiscoverAsync |
These are distinct from the 3 driver-internal self-call sites (AbCipAlarmProjection ×2, S7Driver ×1) which are
#pragma'd as intentional — a driver must not re-wrap its own internal poll/ack calls; the invoker wraps the driver from the dispatch layer OUTWARD. Do NOT touch those.
Why it wasn't fixed inside 07/C-1
07/C-1's scope was "wire the analyzer + triage." Actually routing all 7 sites through CapabilityInvoker
is a substantial, behavior-changing hot-path remediation (activates retry/breaker/bulkhead on every live
driver read/write/subscribe/discover), which must be its own verified change — exactly the discipline the
review espouses. So the sites were suppressed as a tracked gap (not "intentional"), keeping the tree
green and the analyzer live everywhere else, and this follow-up was filed.
Proposed remediation
- Construct a per-
DriverInstanceCapabilityInvokerand thread it intoDriverInstanceActor(via itsProps/ctor) andGenericDriverNodeManager. It needs:- the process-singleton
DriverResiliencePipelineBuilder, - a
Func<DriverResilienceOptions>options accessor (source the per-instance resilience options — confirm where they live / how Admin edits them), - the
driverTypestring (already on the actor), - the
DriverResilienceStatusTracker(so Admin/hostsshows in-flight/bulkhead depth), - the per-call host resolver (
IPerCallHostResolver) for multi-host drivers.
- the process-singleton
- Route each of the 7 sites through
ExecuteAsync/ExecuteWriteAsync(writes/acks use the write variant). Remove theRESILIENCE-DISPATCH-GAPpragma at each site as it is wired. - Fix
CapabilityInvoker's XML doc once the claim becomes true.
Verification bar (unit-green ≠ wired)
- Unit: an actor test asserting the capability call goes through a fake/recording invoker (not the raw driver). The analyzer itself becomes the standing guard — once the pragmas are gone, any regression that re-introduces a raw call fails the build.
- Live (decisive): on the docker-dev / driver rig, prove the pipeline is actually engaged — e.g. use
the modbus
exception_injector(see the write-outcome memory) or a flaky-read fixture to observe a retry / breaker-open transition and the/hostsin-flight counter moving. A behavior change on the live data path must not ship on unit tests alone.
Decision needed
- Confirm this is worth doing now (it changes runtime latency/behavior on every driver call) vs. deferring.
- If deferring, keep the
RESILIENCE-DISPATCH-GAPpragmas + this file as the standing record. - Own branch off
master(like the Criticals).