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:
+33
-25
@@ -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
@@ -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
@@ -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) |
|
||||
|
||||
@@ -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
@@ -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.
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user