From 07eec11d31d3127c0addc5d52fbc0ab1857d4652 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Mon, 13 Jul 2026 10:15:32 -0400 Subject: [PATCH] docs+test(r2-02): bulkhead doc sweep + parsed-knob-must-be-wired guard (01/U-6, OVERALL theme 1) --- ...lience-config-hardening-plan.md.tasks.json | 2 +- docs/OpcUaServer.md | 2 +- docs/ReadWriteOperations.md | 2 +- docs/security.md | 2 +- docs/v1/HistoricalDataAccess.md | 2 +- .../Entities/DriverInstance.cs | 2 -- .../DriverInstanceResilienceStatus.cs | 4 ++- .../IDriverCapabilityInvoker.cs | 4 +-- .../IPerCallHostResolver.cs | 2 +- .../Resilience/AlarmSurfaceInvoker.cs | 2 +- .../Resilience/CapabilityInvoker.cs | 2 +- .../DriverResiliencePipelineBuilder.cs | 4 +-- .../DriverResilienceStatusTracker.cs | 4 +-- .../AbCipDriver.cs | 2 +- .../AbCipHostAddress.cs | 2 +- .../FocasDriverOptions.cs | 2 +- .../Drivers/DriverHostActor.cs | 2 +- .../Drivers/DriverInstanceActor.cs | 2 +- .../UnwrappedCapabilityCallAnalyzer.cs | 4 +-- .../DriverResilienceOptionsTests.cs | 28 +++++++++++++++++++ 20 files changed, 52 insertions(+), 24 deletions(-) diff --git a/archreview/plans/R2-02-resilience-config-hardening-plan.md.tasks.json b/archreview/plans/R2-02-resilience-config-hardening-plan.md.tasks.json index 2db7944a..764340fc 100644 --- a/archreview/plans/R2-02-resilience-config-hardening-plan.md.tasks.json +++ b/archreview/plans/R2-02-resilience-config-hardening-plan.md.tasks.json @@ -13,7 +13,7 @@ { "id": 9, "subject": "S-8 docs + form: fix WriteIdempotentAttribute's false 'reads via reflection' claim; note the non-idempotent production default; disable Write/Ack retry cells in the razor", "status": "completed", "blockedBy": [7, 5] }, { "id": 10, "subject": "U-6 delete (Core): remove BulkheadMaxConcurrent/MaxQueue from DriverResilienceOptions + parser shape/merge/doc-example; add BulkheadKeys_InStoredJson_AreIgnored (migration-free contract)", "status": "completed", "blockedBy": [2, 6] }, { "id": 11, "subject": "U-6 delete (AdminUI): strip bulkhead fields from ResilienceFormModel + the two razor inputs; update ResilienceFormModelTests", "status": "completed", "blockedBy": [5, 10] }, - { "id": 12, "subject": "U-6 doc sweep (seam xmldocs, OTOPCUA0001 message + analyzer tests, operator docs; leave migrations/WedgeDetector/historical plans) + Options_properties_are_exactly_the_pipeline_wired_set knob-inertness guard", "status": "pending", "blockedBy": [10, 11] }, + { "id": 12, "subject": "U-6 doc sweep (seam xmldocs, OTOPCUA0001 message + analyzer tests, operator docs; leave migrations/WedgeDetector/historical plans) + Options_properties_are_exactly_the_pipeline_wired_set knob-inertness guard", "status": "completed", "blockedBy": [10, 11] }, { "id": 13, "subject": "C-7 RED tests: unknown top-level key / unknown capability / unknown per-policy field survive round-trip; malformed JSON sets ParseFailed and ToJson returns the original text", "status": "pending", "blockedBy": [11] }, { "id": 14, "subject": "C-7 implement: JsonObject-bag FromJson/ToJson (TagConfigJson idiom), ParseFailed + RawStoredJson, blank->null contract preserved, parser-interop test green", "status": "pending", "blockedBy": [13] }, { "id": 15, "subject": "C-7 razor: ParseFailed warning banner + input lockout + explicit discard button; raw pane shows the stored ResilienceConfig; live-/run on docker-dev :9200 (rebuild BOTH centrals; SQL-mangle + unknown-key survival checks; also drive task 5 warnings and task 9 disabled cells)", "status": "pending", "blockedBy": [14] }, diff --git a/docs/OpcUaServer.md b/docs/OpcUaServer.md index f2f2681f..f883e949 100644 --- a/docs/OpcUaServer.md +++ b/docs/OpcUaServer.md @@ -19,7 +19,7 @@ The lifecycle facade `OpcUaApplicationHost` (`src/Server/ZB.MOM.WW.OtOpcUa.OpcUa ## Resilience and capability dispatch -Driver-capability calls (`IReadable.ReadAsync`, `IWritable.WriteAsync`, `ITagDiscovery.DiscoverAsync`, `ISubscribable.SubscribeAsync/UnsubscribeAsync`, the `IHostConnectivityProbe` probe loop, `IAlarmSource` surfaces, and the four `IHistoryProvider` reads) are routed through a `CapabilityInvoker` (`src/Core/ZB.MOM.WW.OtOpcUa.Core/Resilience/CapabilityInvoker.cs`) so the Polly resilience pipeline (retry / timeout / breaker / bulkhead) applies. There is one invoker per `(DriverInstance, IDriver)` pair; all invokers share the process-singleton `DriverResiliencePipelineBuilder`, which keys pipelines on `(DriverInstanceId, hostName, DriverCapability)`. Per-instance resilience options come from `DriverTypeRegistry` (the driver's tier) plus per-instance JSON overrides parsed from `DriverInstance.ResilienceConfig` by `DriverResilienceOptionsParser`. +Driver-capability calls (`IReadable.ReadAsync`, `IWritable.WriteAsync`, `ITagDiscovery.DiscoverAsync`, `ISubscribable.SubscribeAsync/UnsubscribeAsync`, the `IHostConnectivityProbe` probe loop, `IAlarmSource` surfaces, and the four `IHistoryProvider` reads) are routed through a `CapabilityInvoker` (`src/Core/ZB.MOM.WW.OtOpcUa.Core/Resilience/CapabilityInvoker.cs`) so the Polly resilience pipeline (retry / timeout / breaker) applies. There is one invoker per `(DriverInstance, IDriver)` pair; all invokers share the process-singleton `DriverResiliencePipelineBuilder`, which keys pipelines on `(DriverInstanceId, hostName, DriverCapability)`. Per-instance resilience options come from `DriverTypeRegistry` (the driver's tier) plus per-instance JSON overrides parsed from `DriverInstance.ResilienceConfig` by `DriverResilienceOptionsParser`. The `OTOPCUA0001` Roslyn analyzer (`src/Tooling/ZB.MOM.WW.OtOpcUa.Analyzers/UnwrappedCapabilityCallAnalyzer.cs`, category `OtOpcUa.Resilience`, severity Warning) flags direct driver-capability calls that bypass the invoker. diff --git a/docs/ReadWriteOperations.md b/docs/ReadWriteOperations.md index bfe19a21..87522038 100644 --- a/docs/ReadWriteOperations.md +++ b/docs/ReadWriteOperations.md @@ -1,6 +1,6 @@ # Read/Write Operations -The v2 server routes OPC UA Read and Write operations to each driver's `IReadable` and `IWritable` capabilities through `CapabilityInvoker` so the Polly pipeline (retry / timeout / breaker / bulkhead) applies uniformly across Galaxy, Modbus, S7, AB CIP, AB Legacy, TwinCAT, FOCAS, and OPC UA Client drivers. The per-variable `OnReadValue` and `OnWriteValue` hooks described in the sections below live in `DriverNodeManager` (the planned ADR-002 Phase 7 Stream G successor to the v1 `DriverNodeManager`); `GenericDriverNodeManager` (`src/Core/ZB.MOM.WW.OtOpcUa.Core/OpcUa/GenericDriverNodeManager.cs`) handles address-space population and alarm routing during discovery. The current `OtOpcUaNodeManager` (`src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OtOpcUaNodeManager.cs`) is a push-model `CustomNodeManager2` that receives values from the Akka actor layer via `WriteValue`; OPC UA client reads return the cached pushed value. +The v2 server routes OPC UA Read and Write operations to each driver's `IReadable` and `IWritable` capabilities through `CapabilityInvoker` so the Polly pipeline (retry / timeout / breaker) applies uniformly across Galaxy, Modbus, S7, AB CIP, AB Legacy, TwinCAT, FOCAS, and OPC UA Client drivers. The per-variable `OnReadValue` and `OnWriteValue` hooks described in the sections below live in `DriverNodeManager` (the planned ADR-002 Phase 7 Stream G successor to the v1 `DriverNodeManager`); `GenericDriverNodeManager` (`src/Core/ZB.MOM.WW.OtOpcUa.Core/OpcUa/GenericDriverNodeManager.cs`) handles address-space population and alarm routing during discovery. The current `OtOpcUaNodeManager` (`src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OtOpcUaNodeManager.cs`) is a push-model `CustomNodeManager2` that receives values from the Akka actor layer via `WriteValue`; OPC UA client reads return the cached pushed value. ## Driver vs virtual dispatch diff --git a/docs/security.md b/docs/security.md index 7386632f..c39de9b4 100644 --- a/docs/security.md +++ b/docs/security.md @@ -320,7 +320,7 @@ Responses: `202 Accepted` (`{ outcome, deploymentId, revisionHash }`) when a dep ## OTOPCUA0001 Analyzer — Compile-Time Guard -Per-capability resilience (retry, timeout, circuit-breaker, bulkhead) is applied by `CapabilityInvoker` in `src/Core/ZB.MOM.WW.OtOpcUa.Core/Resilience/`. A driver-capability call made **outside** the invoker bypasses resilience entirely — which in production looks like inconsistent timeouts, un-wrapped retries, and unbounded blocking. +Per-capability resilience (retry, timeout, circuit-breaker) is applied by `CapabilityInvoker` in `src/Core/ZB.MOM.WW.OtOpcUa.Core/Resilience/`. A driver-capability call made **outside** the invoker bypasses resilience entirely — which in production looks like inconsistent timeouts, un-wrapped retries, and unbounded blocking. `OTOPCUA0001` (Roslyn analyzer at `src/Tooling/ZB.MOM.WW.OtOpcUa.Analyzers/UnwrappedCapabilityCallAnalyzer.cs`) fires with category `OtOpcUa.Resilience` and default severity **Warning** (per `AnalyzerReleases.Shipped.md`) when a method on one of the seven guarded capability interfaces (`IReadable`, `IWritable`, `ITagDiscovery`, `ISubscribable`, `IHostConnectivityProbe`, `IAlarmSource`, `IHistoryProvider` — all in `ZB.MOM.WW.OtOpcUa.Core.Abstractions`) is invoked **outside** a lambda passed to `CapabilityInvoker.ExecuteAsync` / `ExecuteWriteAsync`. `AlarmSurfaceInvoker` is **not** a wrapper home — its own implementation is covered transitively because it routes through the inner `CapabilityInvoker.ExecuteAsync`. The analyzer walks up the syntax tree from the call site, finds any enclosing invoker invocation, and verifies the call lives transitively inside that invocation's anonymous-function argument — a sibling pattern (do the call, then invoke `ExecuteAsync` on something unrelated nearby) does not satisfy the rule. diff --git a/docs/v1/HistoricalDataAccess.md b/docs/v1/HistoricalDataAccess.md index 8b5de525..387335f1 100644 --- a/docs/v1/HistoricalDataAccess.md +++ b/docs/v1/HistoricalDataAccess.md @@ -30,7 +30,7 @@ There is also a separate **push** path for persisting alarm transitions from any ## Dispatch through `CapabilityInvoker` -All four HistoryRead surfaces are wrapped by `CapabilityInvoker` (`Core/Resilience/CapabilityInvoker.cs`) with `DriverCapability.HistoryRead`. The Polly pipeline keyed on `(DriverInstanceId, HostName, DriverCapability.HistoryRead)` provides timeout, circuit-breaker, and bulkhead defaults per the driver's stability tier (see [docs/v2/driver-stability.md](v2/driver-stability.md)). +All four HistoryRead surfaces are wrapped by `CapabilityInvoker` (`Core/Resilience/CapabilityInvoker.cs`) with `DriverCapability.HistoryRead`. The Polly pipeline keyed on `(DriverInstanceId, HostName, DriverCapability.HistoryRead)` provides timeout and circuit-breaker defaults per the driver's stability tier (see [docs/v2/driver-stability.md](v2/driver-stability.md)). The dispatch point is `DriverNodeManager` in `ZB.MOM.WW.OtOpcUa.Server`. When the OPC UA stack calls `HistoryRead`, the node manager: diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/DriverInstance.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/DriverInstance.cs index 77fb4284..6bc962a2 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/DriverInstance.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/DriverInstance.cs @@ -35,8 +35,6 @@ public sealed class DriverInstance /// Null = use the driver's tier defaults. When populated, expected shape: /// /// { - /// "bulkheadMaxConcurrent": 16, - /// "bulkheadMaxQueue": 64, /// "capabilityPolicies": { /// "Read": { "timeoutSeconds": 5, "retryCount": 5, "breakerFailureThreshold": 3 }, /// "Write": { "timeoutSeconds": 5, "retryCount": 0, "breakerFailureThreshold": 5 } diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/DriverInstanceResilienceStatus.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/DriverInstanceResilienceStatus.cs index b057222a..cdec0b59 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/DriverInstanceResilienceStatus.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/DriverInstanceResilienceStatus.cs @@ -26,7 +26,9 @@ public sealed class DriverInstanceResilienceStatus /// Rolling count of consecutive Polly pipeline failures for this (instance, host). public int ConsecutiveFailures { get; set; } - /// Current Polly bulkhead depth (in-flight calls) for this (instance, host). + /// In-flight capability-call depth for this (instance, host). Formerly fed a Polly + /// concurrency-limiter strategy that was removed (R2-02/01/U-6); the column name is retained to avoid a + /// migration on a reader-less entity — it now carries the tracker's in-flight-call count. public int CurrentBulkheadDepth { get; set; } /// Most recent process recycle time (Tier C only; null for in-process tiers). diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IDriverCapabilityInvoker.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IDriverCapabilityInvoker.cs index f3c60bc5..f2ebdc42 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IDriverCapabilityInvoker.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IDriverCapabilityInvoker.cs @@ -2,7 +2,7 @@ namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions; /// /// 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 +/// capability calls (retry / circuit-breaker / in-flight accounting / tracker telemetry). The Runtime /// dispatch layer (DriverInstanceActor) consumes this interface instead of the concrete /// CapabilityInvoker so the actor assembly does NOT pull in /// ZB.MOM.WW.OtOpcUa.Core (which drags in Polly + driver hosting) — the same boundary @@ -15,7 +15,7 @@ namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions; /// this interface without adapters. Callers pass the resolved hostName per call (a /// multi-host driver resolves it from the tag reference via ; /// a single-host driver passes its ) so per-host breaker -/// isolation + bulkhead-depth accounting key on the right target. +/// isolation + in-flight-depth accounting key on the right target. /// public interface IDriverCapabilityInvoker { diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IPerCallHostResolver.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IPerCallHostResolver.cs index 8a52b4fc..e1084665 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IPerCallHostResolver.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IPerCallHostResolver.cs @@ -28,7 +28,7 @@ public interface IPerCallHostResolver /// /// Resolve the host name for the given driver-side full reference. Returned value is /// used as the hostName argument to the Phase 6.1 CapabilityInvoker so - /// per-host breaker isolation + per-host bulkhead accounting both kick in. + /// per-host breaker isolation + per-host in-flight accounting both kick in. /// /// The full reference of the tag or resource. /// The host name responsible for serving the reference. diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Core/Resilience/AlarmSurfaceInvoker.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Core/Resilience/AlarmSurfaceInvoker.cs index 193e5844..d586c0ac 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Core/Resilience/AlarmSurfaceInvoker.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Core/Resilience/AlarmSurfaceInvoker.cs @@ -52,7 +52,7 @@ public sealed class AlarmSurfaceInvoker /// /// Subscribe to alarm events for a set of source node ids, fanning out by resolved host - /// so per-host breakers / bulkheads apply. Returns one handle per host — callers that + /// so per-host breakers apply. Returns one handle per host — callers that /// don't care about per-host separation may concatenate them. Each returned handle wraps /// the driver's opaque handle together with its resolved host so /// routes through the same host's pipeline that the subscription was created on. diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Core/Resilience/CapabilityInvoker.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Core/Resilience/CapabilityInvoker.cs index 8e77b7de..79d8db97 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Core/Resilience/CapabilityInvoker.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Core/Resilience/CapabilityInvoker.cs @@ -40,7 +40,7 @@ public sealed class CapabilityInvoker : IDriverCapabilityInvoker /// 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 Admin /hosts can surface as the bulkhead-depth proxy. + /// Optional resilience-status tracker. When wired, every capability call records start/complete so Admin /hosts can surface as the in-flight-call-depth proxy. public CapabilityInvoker( DriverResiliencePipelineBuilder builder, string driverInstanceId, diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Core/Resilience/DriverResiliencePipelineBuilder.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Core/Resilience/DriverResiliencePipelineBuilder.cs index a0dcc7d9..51fa558a 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Core/Resilience/DriverResiliencePipelineBuilder.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Core/Resilience/DriverResiliencePipelineBuilder.cs @@ -15,7 +15,7 @@ namespace ZB.MOM.WW.OtOpcUa.Core.Resilience; /// /// /// Per-device isolation. Composition from outside-in: -/// Timeout → Retry (when capability permits) → Circuit Breaker (when tier permits) → Bulkhead. +/// Timeout → Retry (when capability permits) → Circuit Breaker (when tier permits). /// /// Pipeline resolution is lock-free on the hot path: the inner /// caches a per key; @@ -39,7 +39,7 @@ public sealed class DriverResiliencePipelineBuilder /// When non-null, every built pipeline wires Polly telemetry into /// the tracker — retries increment ConsecutiveFailures, breaker-open stamps /// LastBreakerOpenUtc, breaker-close resets failures. Feeds Admin /hosts + - /// the Polly bulkhead-depth column. Absent tracker means no telemetry (unit tests + + /// the in-flight-call-depth column. Absent tracker means no telemetry (unit tests + /// deployments that don't care about resilience observability). /// When non-null, retry / circuit-breaker-open / breaker-close events are /// logged (instance + host + capability + attempt/exception). This is the operator-facing diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Core/Resilience/DriverResilienceStatusTracker.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Core/Resilience/DriverResilienceStatusTracker.cs index c2fdeda7..9dcf3327 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Core/Resilience/DriverResilienceStatusTracker.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Core/Resilience/DriverResilienceStatusTracker.cs @@ -101,7 +101,7 @@ public sealed class DriverResilienceStatusTracker /// /// Record the entry of a capability call for this (instance, host). Increments the /// in-flight counter used as the - /// surface (a cheap stand-in for Polly bulkhead depth). Paired with + /// surface (in-flight capability-call depth). Paired with /// ; callers use try/finally. /// /// The driver instance identifier. @@ -159,7 +159,7 @@ public sealed record ResilienceStatusSnapshot /// /// In-flight capability calls against this (instance, host). Bumped on call entry + /// decremented on completion. Feeds DriverInstanceResilienceStatus.CurrentBulkheadDepth - /// for Admin /hosts — a cheap proxy for the Polly bulkhead depth until the full + /// for Admin /hosts — in-flight capability-call depth until the full /// telemetry observer lands. /// public int CurrentInFlight { get; init; } diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipDriver.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipDriver.cs index 698d476d..9b6a72c8 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipDriver.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipDriver.cs @@ -14,7 +14,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip; /// Wire layer is libplctag 1.6.x. Per-device host addresses use /// the ab://gateway[:port]/cip-path canonical form parsed via /// ; those strings become the hostName key -/// for Polly bulkhead + circuit-breaker isolation. +/// for Polly per-host circuit-breaker isolation. /// /// Tier A — in-process, shares server lifetime, no /// sidecar. is the Tier-B escape hatch for recovering diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipHostAddress.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipHostAddress.cs index 01138dc1..ad8d9554 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipHostAddress.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipHostAddress.cs @@ -3,7 +3,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip; /// /// Parsed ab://gateway[:port]/cip-path host-address string used by the AbCip driver /// as the hostName key across , -/// , and the Polly bulkhead key +/// , and the Polly per-host pipeline key /// (DriverInstanceId, hostName). /// /// diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Contracts/FocasDriverOptions.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Contracts/FocasDriverOptions.cs index 00236c8a..ef60b648 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Contracts/FocasDriverOptions.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Contracts/FocasDriverOptions.cs @@ -4,7 +4,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS; /// /// FOCAS driver configuration. One instance supports N CNC devices. Each device gets its own -/// (DriverInstanceId, HostAddress) bulkhead key at the Phase 6.1 resilience layer. +/// (DriverInstanceId, HostAddress) per-host pipeline key at the Phase 6.1 resilience layer. /// public sealed class FocasDriverOptions { diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DriverHostActor.cs b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DriverHostActor.cs index a449d209..42ffbe84 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DriverHostActor.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DriverHostActor.cs @@ -1627,7 +1627,7 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers else { // Attach the per-instance Phase 6.1 resilience invoker so this driver's capability calls - // route through the retry/breaker/bulkhead/telemetry pipeline. The tier defaults are layered + // route through the retry/breaker/telemetry pipeline. The tier defaults are layered // with the per-instance ResilienceConfig from the artifact; the pass-through factory yields a // no-op invoker on nodes without the real resilience factory bound. var invoker = _invokerFactory.Create(spec.DriverInstanceId, spec.DriverType, spec.ResilienceConfig); diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DriverInstanceActor.cs b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DriverInstanceActor.cs index 3405547f..9c711119 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DriverInstanceActor.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DriverInstanceActor.cs @@ -139,7 +139,7 @@ public sealed class DriverInstanceActor : ReceiveActor, IWithTimers private readonly IDriver _driver; /// Phase 6.1 resilience seam: every guarded driver-capability call this actor makes is - /// routed through this invoker (retry / breaker / bulkhead / tracker telemetry). Defaults to + /// routed through this invoker (retry / breaker / in-flight accounting / tracker telemetry). Defaults to /// (a genuine pass-through) when none is supplied — so /// unit tests + the F7 skeleton path behave exactly as a raw driver call. The fused Host builds a /// real per-instance invoker via and injects it at diff --git a/src/Tooling/ZB.MOM.WW.OtOpcUa.Analyzers/UnwrappedCapabilityCallAnalyzer.cs b/src/Tooling/ZB.MOM.WW.OtOpcUa.Analyzers/UnwrappedCapabilityCallAnalyzer.cs index 644e0f86..5c5bdc21 100644 --- a/src/Tooling/ZB.MOM.WW.OtOpcUa.Analyzers/UnwrappedCapabilityCallAnalyzer.cs +++ b/src/Tooling/ZB.MOM.WW.OtOpcUa.Analyzers/UnwrappedCapabilityCallAnalyzer.cs @@ -12,7 +12,7 @@ namespace ZB.MOM.WW.OtOpcUa.Analyzers; /// Diagnostic analyzer that flags direct invocations of Phase 6.1-wrapped driver-capability /// methods when the call is NOT already running inside a CapabilityInvoker.ExecuteAsync /// or CapabilityInvoker.ExecuteWriteAsync lambda. The wrapping is what gives us -/// per-host breaker isolation, retry semantics, bulkhead-depth accounting, and alarm-ack +/// per-host breaker isolation, retry semantics, in-flight-depth accounting, and alarm-ack /// idempotence guards — raw calls bypass all of that. /// /// @@ -102,7 +102,7 @@ public sealed class UnwrappedCapabilityCallAnalyzer : DiagnosticAnalyzer private static readonly DiagnosticDescriptor Rule = new( id: DiagnosticId, title: "Driver capability call must be wrapped in CapabilityInvoker", - messageFormat: "Call to '{0}' is not wrapped in CapabilityInvoker.ExecuteAsync / ExecuteWriteAsync. Without the wrapping, Phase 6.1 resilience (retry, breaker, bulkhead, tracker telemetry) is bypassed for this call.", + messageFormat: "Call to '{0}' is not wrapped in CapabilityInvoker.ExecuteAsync / ExecuteWriteAsync. Without the wrapping, Phase 6.1 resilience (retry, breaker, tracker telemetry) is bypassed for this call.", category: "OtOpcUa.Resilience", defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true, diff --git a/tests/Core/ZB.MOM.WW.OtOpcUa.Core.Tests/Resilience/DriverResilienceOptionsTests.cs b/tests/Core/ZB.MOM.WW.OtOpcUa.Core.Tests/Resilience/DriverResilienceOptionsTests.cs index 63c7667e..17920df5 100644 --- a/tests/Core/ZB.MOM.WW.OtOpcUa.Core.Tests/Resilience/DriverResilienceOptionsTests.cs +++ b/tests/Core/ZB.MOM.WW.OtOpcUa.Core.Tests/Resilience/DriverResilienceOptionsTests.cs @@ -8,6 +8,34 @@ namespace ZB.MOM.WW.OtOpcUa.Core.Tests.Resilience; [Trait("Category", "Unit")] public sealed class DriverResilienceOptionsTests { + /// + /// 01/U-6 knob-inertness guard (OVERALL theme #1): the public property set of + /// must be EXACTLY the pipeline-wired knobs. Every option a + /// parser can populate must map to a strategy the builder actually composes — a parsed-but-unapplied + /// knob (the bulkhead genre this pass deleted) is inertness the interface-forwarding and + /// unwrapped-dispatch guards can't catch. To add a knob: wire it in DriverResiliencePipelineBuilder, + /// add a behavior test that proves it engages, THEN add it to the expected set below. + /// (RecycleIntervalSeconds is itself Tier-C-dormant per 01/U-2 — a documented open question, + /// explicitly out of this pass's scope; it stays in the expected list as the recycle scheduler, not + /// the Polly pipeline, is its consumer.) + /// + [Fact] + public void Options_properties_are_exactly_the_pipeline_wired_set() + { + var expected = new[] { "Tier", "CapabilityPolicies", "RecycleIntervalSeconds" }; + + var actual = typeof(DriverResilienceOptions) + .GetProperties() + .Select(p => p.Name) + .OrderBy(n => n) + .ToArray(); + + actual.ShouldBe(expected.OrderBy(n => n).ToArray(), + "DriverResilienceOptions must expose only pipeline-wired knobs — a new option needs a builder " + + "strategy + a behavior test before it is added to the expected set (guards against the deleted " + + "bulkhead 'parsed-but-unapplied' genre; see 01/U-6)."); + } + /// Verifies that tier defaults cover every capability. /// The driver tier to test. [Theory]