refactor(drivers): delete the inert driver-tier recycle machinery (#522)

The tier system was documented, operator-authorable, and inert. Deleted rather than activated,
because its premise is gone rather than merely unused.

"Tier C" meant a driver running out-of-process behind an IDriverSupervisor that could restart its
Host without tearing down the OPC UA session. No such process exists anywhere: Galaxy reaches
MXAccess over gRPC to the external mxaccessgw sidecar (PR 7.2 retired the in-process
Galaxy.Host/Proxy/Shared projects) and FOCAS has run in-process since its managed wire client
landed 2026-04-24. Consistently, IDriverSupervisor had ZERO implementations and there was nothing
for one to implement against.

The issue understated the inertness. It says the Tier-C-only protections never engaged, implying
the Tier A/B parts did. They did not: nothing constructs MemoryTracking, MemoryRecycle or
ScheduledRecycleScheduler outside their own unit tests, so the whole Core/Stability recycle layer
was dead — meaning option (a), "pass real tiers", was never a flag flip. It would have meant
writing the wiring that never existed AND arming it.

Deleted: MemoryTracking, MemoryRecycle, ScheduledRecycleScheduler, IDriverSupervisor, the
vestigial DriverTypeRegistry (referenced only by its own tests), and the RecycleIntervalSeconds
knob — which the AdminUI let an operator author and the parser validated while it configured
nothing.

DriverTier itself SURVIVES and is load-bearing: DriverResilienceOptions.GetTierDefaults supplies
the real per-capability timeout/retry/breaker policies via DriverFactoryRegistry.GetTier and
DriverCapabilityInvokerFactory. Only the isolation-and-recycle layer above it is gone.

Deliberately NOT deleted:

- WedgeDetector came along in the same directory and is equally dead in production, but it is
  tier-agnostic and is not recycle machinery — it only shares the folder. Restored rather than
  swept up in a decision that was not about it.
- IDriver.GetMemoryFootprint() and FlushOptionalCachesAsync() lose their only consumer here.
  Removing them touches all 12 drivers and every test stub, so they are documented as
  consumerless and filed as #525 instead of buried in this diff.

Compatibility: a deployed ResilienceConfig blob still carrying "recycleIntervalSeconds" parses
cleanly (unknown keys are ignored — guarded by a new test, because a blob that suddenly failed to
parse would fall back to tier defaults and silently discard the operator's real overrides), and
the AdminUI's preserve-unknown-keys bag keeps the key rather than rewriting stored config on an
unrelated edit.

The 01/U-6 knob-inertness guard carried an explicit carve-out admitting RecycleIntervalSeconds was
dormant and out of scope; that carve-out is now gone, so the expected set is literally what the
test's name claims.

Note: Host.IntegrationTests has 2 failures (DriverProbeRegistrationTests.is_idempotent,
PrimaryGateFailoverTests) — verified pre-existing by reproducing both on clean master dc9d947b.

Claude-Session: https://claude.ai/code/session_015p7wGqy3YpZNCpDzTpGMKo
This commit is contained in:
Joseph Doherty
2026-07-30 04:44:04 -04:00
parent 30d0697c28
commit cbb882293b
23 changed files with 153 additions and 1145 deletions
+33 -25
View File
@@ -106,37 +106,45 @@ calls `AddOtOpcUaDriverProbes` in the `hasAdmin` block, and
---
## IDriverSupervisor — Tier C out-of-process recycle
## IDriverSupervisor / process recycle — REMOVED (Gitea #522)
[`Core.Abstractions/IDriverSupervisor.cs`](../src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IDriverSupervisor.cs)
`IDriverSupervisor`, `MemoryRecycle`, `MemoryTracking` and
`ScheduledRecycleScheduler` **no longer exist.** This section used to describe
them as the Tier C protection layer.
The process-level supervisor contract a **Tier C** (out-of-process) driver's
topology provides. Its concern is restarting the out-of-process Host when a
hard fault is detected (memory breach, wedge, scheduled recycle window). Tier
A/B drivers run in-process and do **not** have a supervisor — recycling them
would kill every OPC UA session and every co-hosted driver. The Core.Stability
layer only invokes this interface after asserting the tier.
They were removed because the thing they acted on is gone. "Tier C" meant a
driver running **out-of-process behind a supervisor that could restart its Host**
without tearing down the OPC UA session or co-hosted drivers. No such process
remains anywhere: Galaxy reaches MXAccess over gRPC to the external
`mxaccessgw` sidecar (PR 7.2 retired the in-process `Galaxy.Host` / `Proxy` /
`Shared` projects), and FOCAS has run in-process since its managed wire client
landed 2026-04-24. Consistent with that, `IDriverSupervisor` had **zero
implementations**, and `MemoryTracking` / `MemoryRecycle` /
`ScheduledRecycleScheduler` were constructed **only in their own unit tests**
so this was not dormant-but-ready code, it was code with no path to running.
Members:
The operator-facing half went too: `RecycleIntervalSeconds` was authorable
through the AdminUI's driver resilience section and validated by the parser
while configuring nothing. A stored `ResilienceConfig` still carrying the key
parses cleanly (unknown keys are ignored) and the AdminUI preserves it rather
than silently rewriting an operator's config.
- `string DriverInstanceId { get; }` — the driver instance this supervisor
governs.
- `Task RecycleAsync(string reason, CancellationToken cancellationToken)`
request a terminate+restart of the Host process; implementations are
expected to be idempotent under repeat calls during an in-flight recycle.
**`DriverTier` itself survives and is load-bearing** — do not delete it while
following this note. `DriverResilienceOptions.GetTierDefaults(tier)` supplies
the real per-capability timeout / retry / breaker policies, resolved via
`DriverFactoryRegistry.GetTier` and applied by `DriverCapabilityInvokerFactory`.
Every driver runs Tier A because no factory passes a tier, so today that means
one uniform policy set.
Callers (both in
[`Core/Stability/`](../src/Core/ZB.MOM.WW.OtOpcUa.Core/Stability/)):
⚠️ Two `IDriver` members are now **consumerless**: `GetMemoryFootprint()` and
`FlushOptionalCachesAsync()` existed to feed `MemoryTracking`. All 12 drivers
still implement them and nothing calls them. They were left in place rather
than swept, since removing them touches every driver and every test stub —
tracked as Gitea **#525**.
- `ScheduledRecycleScheduler`
([`Core/Stability/ScheduledRecycleScheduler.cs`](../src/Core/ZB.MOM.WW.OtOpcUa.Core/Stability/ScheduledRecycleScheduler.cs))
— opt-in periodic recycle. A `TickAsync` method advanced by the caller's
ambient scheduler decides whether the configured interval has elapsed and, if
so, drives `RecycleAsync`. Its constructor throws unless the tier is C, making
in-process misuse structurally impossible.
- `MemoryRecycle`
([`Core/Stability/MemoryRecycle.cs`](../src/Core/ZB.MOM.WW.OtOpcUa.Core/Stability/MemoryRecycle.cs))
— on a memory hard-breach, calls `RecycleAsync` (when a supervisor is wired).
If per-driver memory protection is wanted again, build it for the architecture
as it is now: a process-wide watchdog, not a per-driver tier whose only remedy
requires a supervisor that cannot exist in-process.
---
+1 -1
View File
@@ -27,7 +27,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) 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 the driver's tier — the `DriverTier` passed at factory registration, resolved via `DriverFactoryRegistry.GetTier` (every driver is Tier A today, since no factory passes one) — plus per-instance JSON overrides parsed from `DriverInstance.ResilienceConfig` by `DriverResilienceOptionsParser`. (It was never `DriverTypeRegistry`; that class was vestigial and was deleted in Gitea #522.)
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.
+1 -1
View File
@@ -26,7 +26,7 @@ The project was originally called **LmxOpcUa** (a single-driver Galaxy/MXAccess
| [OpcUaServer.md](OpcUaServer.md) | Top-level server architecture — Core, driver dispatch, Config DB, generations |
| [AddressSpace.md](AddressSpace.md) | `GenericDriverNodeManager` + `ITagDiscovery` + `IAddressSpaceBuilder` |
| [ReadWriteOperations.md](ReadWriteOperations.md) | OPC UA Read/Write → `CapabilityInvoker``IReadable`/`IWritable` |
| [DriverLifecycle.md](DriverLifecycle.md) | Server-side driver lifecycle + infrastructure contracts (`IDriverFactory`, `IDriverProbe`, `IDriverSupervisor`, `IDriverHealthPublisher`, `IDriverConfigEditor`, `IHistorianDataSource`) + the Commons library |
| [DriverLifecycle.md](DriverLifecycle.md) | Server-side driver lifecycle + infrastructure contracts (`IDriverFactory`, `IDriverProbe`, `IDriverHealthPublisher`, `IDriverConfigEditor`, `IHistorianDataSource`) + the Commons library |
| [Subscriptions.md](v1/Subscriptions.md) | Monitored items → `ISubscribable` + per-driver subscription refcount (v1 archive) |
| [AlarmTracking.md](AlarmTracking.md) | `IAlarmSource` + `AlarmSurfaceInvoker` + OPC UA alarm conditions — native Galaxy alarms end-to-end (live) |
| [AlarmTracking.md](v1/AlarmTracking.md) | Original alarm-tracking write-up (v1 archive) |
+2 -2
View File
@@ -17,9 +17,9 @@ Each driver opts into only the capabilities it supports. Every async capability
Driver **factories** are registered at startup in `DriverFactoryRegistry` (`src/Core/ZB.MOM.WW.OtOpcUa.Core/Hosting/DriverFactoryRegistry.cs`) — all 12 in one place, `src/Server/ZB.MOM.WW.OtOpcUa.Host/Drivers/DriverFactoryBootstrap.cs:147-164`. Type names are the `DriverTypeNames` constants (`src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/DriverTypeNames.cs`), guarded bidirectionally by `DriverTypeNamesGuardTests`.
> ⚠️ **`DriverTypeRegistry` / `DriverTypeMetadata` are vestigial.** No production code constructs or reads them — the class is referenced only by its own unit tests. Its `AllowedNamespaceKinds` field was retired along with the v3 `Namespace` entity (`DriverTypeRegistry.cs:97`), and the `SystemPlatform` namespace kind no longer exists.
> ⚠️ **`DriverTypeRegistry` / `DriverTypeMetadata` were DELETED (Gitea #522, 2026-07-30).** They were vestigial — no production code constructed or read them, only their own unit tests did — and `AllowedNamespaceKinds` had already been orphaned by the retirement of the v3 `Namespace` entity. Older docs and plans that name them as the driver-metadata or tier authority describe something that no longer exists.
>
> **Stability tier** likewise does not come from that registry: it is the `DriverTier tier = DriverTier.A` parameter on `DriverFactoryRegistry.cs:46`, and **no factory in `src/Drivers/` passes a tier**, so every shipped driver runs Tier A. The Tier-C-only protections described in [docs/v2/driver-stability.md](../v2/driver-stability.md) — `MemoryRecycle` hard-breach kill (`MemoryRecycle.cs:54`) and `ScheduledRecycleScheduler` (`:43`) — are therefore dormant for every driver.
> **Stability tier** never came from that registry anyway: it is the `DriverTier tier = DriverTier.A` parameter on `DriverFactoryRegistry.cs:46`, and **no factory in `src/Drivers/` passes a tier**, so every shipped driver runs Tier A. The Tier-C recycle protections that [docs/v2/driver-stability.md](../v2/driver-stability.md) describes — `MemoryTracking`, `MemoryRecycle`, `ScheduledRecycleScheduler`, `IDriverSupervisor` — are **gone**, not merely dormant: they required an out-of-process driver Host that no longer exists anywhere (Galaxy is gRPC-to-`mxaccessgw`, FOCAS is in-process), `IDriverSupervisor` had no implementations, and the trackers were only ever constructed in tests. The `Tier` column below is therefore uniformly A, and the tier's remaining effect is `DriverResilienceOptions.GetTierDefaults` — the per-capability timeout / retry / breaker policy set, which **is** live.
## Ground-truth driver list
+21 -16
View File
@@ -1,24 +1,29 @@
# Driver Stability & Isolation — OtOpcUa v2
> ⚠️ **The tier assignments below are NOT what runs (verified against source 2026-07-27).**
> **Every one of the 12 drivers runs as Tier A.** `DriverFactoryRegistry` defaults `DriverTier tier =
> DriverTier.A` and **no factory in `src/Drivers/` passes a tier**, so nothing ever selects B or C. The
> Galaxy and FOCAS Tier-C assignments in the table below are aspirational.
> ⚠️ **HISTORICAL DESIGN RECORD — the isolation model below was never built, and its machinery was
> DELETED on 2026-07-30 (Gitea #522).** Read this document for the reasoning, not for what runs.
>
> Consequences worth knowing:
> **What actually runs:** all 12 drivers are Tier A, in-process. `DriverFactoryRegistry` defaults
> `DriverTier tier = DriverTier.A` and no factory in `src/Drivers/` passes a tier, so nothing ever
> selected B or C. The Galaxy and FOCAS Tier-C assignments in the table below are aspirational.
>
> - The **Tier-C-only protections are dormant for every driver** — `MemoryRecycle` gates on
> `when _tier == DriverTier.C`, and `ScheduledRecycleScheduler` refuses unless Tier C. Neither has ever
> engaged in production. `IDriverSupervisor` has zero implementations, yet the config parser still
> validates `RecycleIntervalSeconds`, so an operator can author a recycle interval that does nothing.
> - `DriverTypeRegistry` — which this document's model implies is the tier authority — is **vestigial**:
> referenced only by its own test, with nothing registering metadata at startup.
> - The **separate-Windows-service hosting** described for Tier C does not exist either. Galaxy reaches
> MXAccess over gRPC to the external **mxaccessgw** sidecar; the `Galaxy.Proxy`/`Host`/`Shared` pattern
> named below was retired in PR 7.2, and FOCAS runs in-process like everything else.
> **What was deleted, and why deletion rather than activation:** `MemoryTracking`, `MemoryRecycle`,
> `ScheduledRecycleScheduler`, `IDriverSupervisor`, the vestigial `DriverTypeRegistry`, and the
> operator-authorable `RecycleIntervalSeconds` knob. The premise was gone, not merely unused — Tier C
> meant an **out-of-process Host a supervisor could restart**, and no such process exists: Galaxy
> reaches MXAccess over gRPC to the external **mxaccessgw** sidecar (the `Galaxy.Proxy`/`Host`/`Shared`
> pattern named below was retired in PR 7.2) and FOCAS has run in-process since 2026-04-24.
> Consistently, `IDriverSupervisor` had zero implementations and the trackers were constructed only in
> their own unit tests. Activating the tiers would have armed a recycle that had nothing to recycle.
>
> `docs/drivers/README.md` says Tier A for Galaxy and FOCAS and is the accurate one. Keep-or-delete of the
> tier machinery is tracked as Gitea **#522**; this document is retained as the design record.
> **What survives, and must not be deleted while following this document:** `DriverTier` itself and
> `DriverResilienceOptions.GetTierDefaults(tier)` — the real per-capability timeout / retry / breaker
> policies, resolved via `DriverFactoryRegistry.GetTier` and applied by `DriverCapabilityInvokerFactory`.
> The tier enum is live; only the isolation-and-recycle layer built on top of it is gone.
>
> `docs/drivers/README.md` is the accurate per-driver reference. If per-driver memory protection is
> wanted again, build it for the architecture as it is now — a process-wide watchdog, not a per-driver
> tier whose only remedy needs a supervisor that cannot exist in-process.
>
> **Status**: DRAFT — companion to `plan.md`. Defines the stability tier model, per-driver hosting decisions, cross-cutting protections every driver process must apply, and the canonical worked example (FOCAS) for the high-risk tier.
>