docs(archreview): file follow-up records for open decisions #10 (RESILIENCE-DISPATCH-GAP) + #11 (Crit-1 SBR premise)
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
# 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-analyzer` `f0082af5` · **Task:** TaskCreate #10 · **Severity:** likely High ·
|
||||
> **Effort:** M–L · **Risk:** Medium (behavior-changing hot path). See [`STATUS.md`](STATUS.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"` → only `tests/Core/…` (7 test files). Zero production constructions.
|
||||
- `DriverInstanceActor` holds a raw `IDriver _driver` and calls it directly at 6 sites.
|
||||
- `GenericDriverNodeManager.BuildAddressSpaceAsync` calls `discovery.DiscoverAsync` directly (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
|
||||
|
||||
1. **Construct a per-`DriverInstance` `CapabilityInvoker`** and thread it into `DriverInstanceActor` (via
|
||||
its `Props`/ctor) and `GenericDriverNodeManager`. 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 `driverType` string (already on the actor),
|
||||
- the `DriverResilienceStatusTracker` (so Admin `/hosts` shows in-flight/bulkhead depth),
|
||||
- the per-call host resolver (`IPerCallHostResolver`) for multi-host drivers.
|
||||
2. **Route each of the 7 sites** through `ExecuteAsync` / `ExecuteWriteAsync` (writes/acks use the write
|
||||
variant). Remove the `RESILIENCE-DISPATCH-GAP` pragma at each site as it is wired.
|
||||
3. **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 `/hosts` in-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-GAP` pragmas + this file as the standing record.
|
||||
- Own branch off `master` (like the Criticals).
|
||||
Reference in New Issue
Block a user