b815446940
Both `IDriver` members had no production call site. Their only consumer was `MemoryTracking`, deleted with the driver-tier recycle machinery in #522, and `GetMemoryFootprint`'s doc-comment claimed "polled every 30s by Core" — which had not been true since v3, even before #522 removed the poller. Owner decision on #525 was (a) delete rather than (b) build the process-wide watchdog that would consume them. The driver-side implementations are in git history and were mostly reusable; what was always missing is the trigger, and a member nobody calls is how the misleading doc-comment survived this long. Removed from `IDriver`, all 12 driver implementations, and every test stub — plus the support code that existed only to feed them, which is the part a member-only deletion would have left behind as dead weight: - MTConnect's three per-entry byte-weight constants, its cached `ProbeModelDataItemCount`, and `CountDataItems` — a full device-tree walk run on every cache fill to populate a number with no remaining reader. - OpcUaClient's `_discoveredNodeCount` field and the discovery-pass write to it. - MQTT's `ApproxBytesPerAuthoredTag`. Two behavioural notes, neither a change in what the software does: - `AbCipTemplateCache` now has no production reset path. Its only caller was AB CIP's flush, which itself had no caller — so nothing changes except how visible the fact is. Recorded on the type, along with the correction that its comment's claim about a device `ReinitializeAsync` also flushing it was never true. A UDT redefined on a live controller keeps its stale shape until the driver instance is rebuilt. - MTConnect's `/probe` cache is still dropped, by `InvalidateProbeModel()` on an Agent restart. That is now `internal` rather than `private` so the tests which used the flush purely to empty the cache drive the real path instead — they now cover a case that actually occurs in production. Several unrelated doc comments cited `FlushOptionalCachesAsync` as the canonical CAS example and are repointed to it. Tests: the ones that existed only to assert on the removed members are deleted (3 OpcUaClient, 3 Galaxy, 2 MQTT, 1 MTConnect, 1 AbCip). Worth noting one was vacuous — `FlushOptionalCachesAsync_resets_footprint_counter` asserted a footprint of 0 before the flush and 0 after with no discovery in between, so it would have passed against a flush that did nothing at all. Full suite green except 7 failures verified identical on master before and after (4 AbLegacy integration with the compose profile down, 3 Host.IntegrationTests), checked by name rather than assumed. Docs corrected where they described this as live: `DriverLifecycle.md` (which carried the #525 placeholder), `drivers/AbCip.md`, `drivers/AbLegacy.md`, and `v2/driver-stability.md` — whose "polls this every 30 s" + per-driver `DriverConfig` memory budget were never built as written and are now banner-ed as design intent rather than description. Claude-Session: https://claude.ai/code/session_015p7wGqy3YpZNCpDzTpGMKo
346 lines
18 KiB
Markdown
346 lines
18 KiB
Markdown
# Driver Lifecycle & Server Infrastructure Contracts
|
|
|
|
Reference for the server-side infrastructure interfaces that surround a
|
|
driver but are **not** driver *capabilities* (read/write/subscribe/etc.,
|
|
documented in [ReadWriteOperations.md](ReadWriteOperations.md) and the
|
|
per-driver pages). These contracts live in
|
|
[`src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/`](../src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/)
|
|
so they carry no behavior — concrete implementations live in the driver
|
|
projects, the Runtime, and the ControlPlane. Each subsection below gives the
|
|
purpose, the key members, and where it is implemented/used.
|
|
|
|
The capability interfaces a driver opts into (`IReadable`, `IWritable`,
|
|
`ITagDiscovery`, `ISubscribable`, `IAlarmSource`, `IHistoryProvider`,
|
|
`IHostConnectivityProbe`, `IPerCallHostResolver`, `IRediscoverable`) are
|
|
covered elsewhere and discovered by the server via `is`-checks on the
|
|
`IDriver` instance. The interfaces here are the *plumbing* the server uses to
|
|
**create**, **probe**, **supervise**, **report on**, and **configure** those
|
|
drivers, plus the server-side historian read surface.
|
|
|
|
---
|
|
|
|
## IDriverFactory — creating drivers from config rows
|
|
|
|
[`Core.Abstractions/IDriverFactory.cs`](../src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IDriverFactory.cs)
|
|
|
|
Abstraction over the process-wide driver registry. The Runtime consumes this
|
|
instead of the concrete registry so the Runtime project does not pull in
|
|
`ZB.MOM.WW.OtOpcUa.Core` (which would drag in Polly + driver hosting).
|
|
|
|
Members:
|
|
|
|
- `IDriver? TryCreate(string driverType, string driverInstanceId, string driverConfigJson)`
|
|
— returns a new driver for the given type, or `null` when no factory is
|
|
registered for that type (missing assembly, typo). The `DriverHostActor`
|
|
logs and skips the row rather than failing the whole apply.
|
|
- `IReadOnlyCollection<string> SupportedTypes` — driver-type names this
|
|
factory can materialise; mostly for diagnostics and logs.
|
|
|
|
Implementations:
|
|
|
|
- `NullDriverFactory` (same file) returns `null` from every `TryCreate` and
|
|
exposes zero supported types. Bound when no concrete driver assemblies have
|
|
been registered (Mac dev path, smoke tests); the deployment becomes a no-op.
|
|
- `DriverFactoryRegistry`
|
|
([`Core/Hosting/DriverFactoryRegistry.cs`](../src/Core/ZB.MOM.WW.OtOpcUa.Core/Hosting/DriverFactoryRegistry.cs))
|
|
is the real process-singleton registry keyed by `DriverInstance.DriverType`
|
|
(case-insensitive). Each driver project ships a `Register(...)` extension;
|
|
`Register` records the factory **and** the driver's stability
|
|
[`DriverTier`](../src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/DriverTier.cs)
|
|
(defaults to Tier A). Registering the same type twice throws.
|
|
- `DriverFactoryRegistryAdapter`
|
|
([`Core/Hosting/DriverFactoryRegistryAdapter.cs`](../src/Core/ZB.MOM.WW.OtOpcUa.Core/Hosting/DriverFactoryRegistryAdapter.cs))
|
|
bridges the registry to the `IDriverFactory` abstraction.
|
|
|
|
Wiring: `DriverFactoryBootstrap.AddOtOpcUaDriverFactories`
|
|
([`Host/Drivers/DriverFactoryBootstrap.cs`](../src/Server/ZB.MOM.WW.OtOpcUa.Host/Drivers/DriverFactoryBootstrap.cs))
|
|
registers the singleton registry, runs every driver assembly's `Register`
|
|
extension, then binds `IDriverFactory` to the adapter. It must run **before**
|
|
`AddAkka` so the Runtime can resolve `IDriverFactory` when spawning the
|
|
`DriverHostActor`
|
|
([`Runtime/Drivers/DriverHostActor.cs`](../src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DriverHostActor.cs)).
|
|
The registry is skipped on admin-only nodes (they never run drivers); the
|
|
probe set is the exception — see [IDriverProbe](#idriverprobe--test-connect).
|
|
|
|
---
|
|
|
|
## IDriverProbe — Test Connect
|
|
|
|
[`Core.Abstractions/IDriverProbe.cs`](../src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IDriverProbe.cs)
|
|
|
|
A cheap test-connect probe for one driver type, backing the AdminUI **Test
|
|
Connect** button. An implementation deserializes a driver-config JSON, attempts
|
|
a cheap connection (TCP open, OPC UA session, gRPC ping — whatever the driver's
|
|
native protocol supports), and reports success/failure with latency. **Probes
|
|
must not mutate persistent state**: the AdminUI invokes them against the
|
|
transient config in the typed form, not against the persisted `DriverInstance`
|
|
row.
|
|
|
|
Members:
|
|
|
|
- `string DriverType { get; }` — the `DriverInstance.DriverType` string this
|
|
probe handles; used for DI lookup.
|
|
- `Task<DriverProbeResult> ProbeAsync(string configJson, TimeSpan timeout, CancellationToken ct)`
|
|
— never throws on connection failure; returns a result with `Ok = false`
|
|
and a message instead.
|
|
- `DriverProbeResult(bool Ok, string? Message, TimeSpan? Latency)` — outcome
|
|
record (`Message` is `null` on success; `Latency` is `null` on failure).
|
|
|
|
Implementations: every driver ships a `*DriverProbe` in its driver project
|
|
(e.g.
|
|
[`Driver.Modbus/ModbusDriverProbe.cs`](../src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus/ModbusDriverProbe.cs)
|
|
does a bare socket open/close). The historian backend is the external
|
|
HistorianGateway (consumed as a gRPC client package, not an `IDriver`), so it
|
|
has no driver probe.
|
|
|
|
Flow: the AdminUI's `AdminProbeService`
|
|
([`AdminUI/Clients/AdminProbeService.cs`](../src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Clients/AdminProbeService.cs))
|
|
dispatches a `TestDriverConnect` message through `IAdminOperationsClient` to the
|
|
cluster-singleton `AdminOperationsActor`
|
|
([`ControlPlane/AdminOperations/AdminOperationsActor.cs`](../src/Server/ZB.MOM.WW.OtOpcUa.ControlPlane/AdminOperations/AdminOperationsActor.cs)),
|
|
which holds the probes keyed by `DriverType` and invokes the matching one
|
|
(timeout clamped to `[1, 60]` seconds). Because the admin singleton is
|
|
admin-pinned, the probe set must be registered on admin nodes too — `Program.cs`
|
|
calls `AddOtOpcUaDriverProbes` in the `hasAdmin` block, and
|
|
`AddOtOpcUaDriverFactories` registers it for fused admin+driver nodes.
|
|
|
|
---
|
|
|
|
## IDriverSupervisor / process recycle — REMOVED (Gitea #522)
|
|
|
|
`IDriverSupervisor`, `MemoryRecycle`, `MemoryTracking` and
|
|
`ScheduledRecycleScheduler` **no longer exist.** This section used to describe
|
|
them as the Tier C protection layer.
|
|
|
|
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.
|
|
|
|
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.
|
|
|
|
**`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.
|
|
|
|
`IDriver` **no longer carries `GetMemoryFootprint()` or
|
|
`FlushOptionalCachesAsync()`** (Gitea **#525**). They existed to feed
|
|
`MemoryTracking`, deleted with the rest of the driver-tier recycle machinery in
|
|
**#522**; after that nothing called them, and their doc-comments still claimed
|
|
Core polled the footprint every 30 s, which had not been true since v3. The
|
|
sweep removed them from the interface, all 12 drivers, and every test stub,
|
|
along with the support code that existed only to feed them — MTConnect's
|
|
per-entry byte weights and its cached data-item count, OpcUaClient's
|
|
`_discoveredNodeCount`, and MQTT's per-tag byte estimate.
|
|
|
|
Two consequences worth knowing:
|
|
|
|
- `AbCipTemplateCache` now has **no production reset path**. Its only caller was
|
|
AB CIP's flush — which itself had no caller, so nothing has actually changed
|
|
in behaviour, only in how visible the fact is. A UDT redefined on a live
|
|
controller keeps its stale shape until the driver instance is rebuilt.
|
|
- MTConnect's `/probe` cache is still dropped, by `InvalidateProbeModel()` on an
|
|
Agent restart. The tests that used to reach it through the flush now drive
|
|
that path instead, so they cover a case that actually occurs.
|
|
|
|
If per-driver memory protection is wanted again, build it for the architecture
|
|
as it is now: a process-wide watchdog that can ask drivers to flush, not a
|
|
per-driver tier whose only remedy requires a supervisor that cannot exist
|
|
in-process. The deleted per-driver implementations are in git history and were
|
|
mostly reusable — what was always missing is the trigger.
|
|
|
|
---
|
|
|
|
## IDriverHealthPublisher — health pub/sub sink
|
|
|
|
[`Core.Abstractions/IDriverHealthPublisher.cs`](../src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IDriverHealthPublisher.cs)
|
|
|
|
A sink for driver-health state-change notifications. Implementations must be
|
|
non-blocking and safe to call from any thread.
|
|
|
|
Member:
|
|
|
|
- `void Publish(string clusterId, string driverInstanceId, DriverHealth health, int errorCount5Min)`
|
|
|
|
Implementations:
|
|
|
|
- `NullDriverHealthPublisher` (same file) is the drop-in no-op for tests and
|
|
dev-stub paths. A `DriverInstanceActor` defaults to it when no publisher is
|
|
supplied.
|
|
- `AkkaDriverHealthPublisher`
|
|
([`Runtime/Drivers/AkkaDriverHealthPublisher.cs`](../src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/AkkaDriverHealthPublisher.cs))
|
|
is the production binding: it forwards each transition as a
|
|
`DriverHealthChanged` message onto the cluster-wide `driver-health`
|
|
Akka DistributedPubSub topic.
|
|
|
|
Producer: `DriverInstanceActor`
|
|
([`Runtime/Drivers/DriverInstanceActor.cs`](../src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DriverInstanceActor.cs))
|
|
calls `Publish` when a driver's health transitions. The published snapshot is
|
|
consumed AdminUI-side and surfaced through the driver-status panel (read
|
|
in-process by the AdminUI bridge rather than dialing its own hub).
|
|
|
|
### Writing health around a teardown (Gitea #517)
|
|
|
|
Two rules, and the second is the counter-intuitive one:
|
|
|
|
1. **`GetHealth()` must report actual state, never a constant.** A hard-coded `Healthy` reports a
|
|
healthy driver before initialize and after shutdown alike. `CalculationDriver` did exactly this —
|
|
it was the fleet's one driver reporting Healthy with its timers stopped, deterministically rather
|
|
than as a race. A driver with no transport still has a running / not-running distinction.
|
|
|
|
2. **Land the terminal health value *after* the teardown — and, where the window is observable, write
|
|
it *before* as well.** #517's original suggestion was to move the write earlier so the race window
|
|
leans conservative. Moving it is wrong: teardown drains in-flight poll/probe loops, and those loops
|
|
write health as they unwind, so a driver that only wrote first could have a late loop overwrite the
|
|
shutdown state with `Healthy` and leave *that* as the resting value — with every connection closed.
|
|
The safe shape is a **bracket**: conservative value before, terminal value after.
|
|
|
|
Where the window is actually observable is worth knowing before adding brackets everywhere:
|
|
|
|
| Path | Caller | Can `GetHealth()` land mid-teardown? |
|
|
|---|---|---|
|
|
| `ShutdownAsync` | `DriverInstanceActor.PostStop` | **No** — actor thread, after the mailbox drains, so the health poll is serialised against it. |
|
|
| `InitializeAsync` failure | `DriverInstanceActor.InitializeAsync` | **Yes** — invoked on a fire-and-forget `Task.Run`, so the health-poll tick keeps calling `GetHealth()` throughout. |
|
|
|
|
So a driver whose `InitializeAsync` disposes anything on its failure path must bracket; only
|
|
`MTConnectDriver` does today, because it is the only one that tears down inside a `catch`. The
|
|
shutdown paths deliberately keep the single trailing write.
|
|
|
|
---
|
|
|
|
## IDriverConfigEditor — custom AdminUI config editor (plug-point)
|
|
|
|
[`Core.Abstractions/IDriverConfigEditor.cs`](../src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IDriverConfigEditor.cs)
|
|
|
|
An **optional** plug-point a driver can implement to provide a custom AdminUI
|
|
editor for its `DriverConfig` JSON. Drivers that don't implement it fall back to
|
|
the generic JSON editor with schema-driven validation. This is the contract
|
|
between the driver and the Admin Blazor app; the Admin app discovers
|
|
implementations and slots them into the Driver Detail screen.
|
|
|
|
Members:
|
|
|
|
- `string DriverType { get; }` — the driver type this editor handles.
|
|
- `Type EditorComponentType { get; }` — the Razor component type that renders
|
|
the editor (returned as `Type` so `Core.Abstractions` needs no Blazor
|
|
reference).
|
|
|
|
Status: this is a forward-looking plug-point. No driver ships a concrete
|
|
`IDriverConfigEditor` today — every driver uses the generic JSON editor — so
|
|
the interface currently has the contract defined but no implementations.
|
|
|
|
---
|
|
|
|
## IHistorianDataSource — server-side historian read surface
|
|
|
|
[`Core.Abstractions/Historian/IHistorianDataSource.cs`](../src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/Historian/IHistorianDataSource.cs)
|
|
|
|
The server-side historian read surface. Registered with the server's history
|
|
router and resolved **per OPC UA namespace**, independent of any driver's
|
|
lifecycle. This is distinct from the driver capability `IHistoryProvider`:
|
|
|
|
- `IHistoryProvider` is a *driver capability* — the server dispatches to it via
|
|
the driver instance.
|
|
- `IHistorianDataSource` is a *server registration* — the server resolves it by
|
|
namespace and calls it directly, so one historian (the HistorianGateway) can
|
|
serve many drivers' nodes, and drivers can restart without dropping history
|
|
availability.
|
|
|
|
The interface is `: IDisposable` and declares the full read surface as
|
|
**required** members (unlike `IHistoryProvider`, where at-time/event reads are
|
|
optional default-impl methods so legacy drivers can stay raw-only):
|
|
|
|
- `ReadRawAsync(fullReference, startUtc, endUtc, maxValuesPerNode, ct)` — raw
|
|
historical samples over a time range.
|
|
- `ReadProcessedAsync(fullReference, startUtc, endUtc, interval, aggregate, ct)`
|
|
— interval-bucketed aggregates (average/min/max/count); an empty bucket
|
|
returns a `BadNoData` sample.
|
|
- `ReadAtTimeAsync(fullReference, timestampsUtc, ct)` — one sample per requested
|
|
timestamp (OPC UA HistoryReadAtTime); the returned list matches the requested
|
|
length and order, gaps as Bad-quality snapshots.
|
|
- `ReadEventsAsync(sourceName, startUtc, endUtc, maxEvents, ct)` — historical
|
|
alarm/event records (OPC UA HistoryReadEvents); `sourceName` is `null` to
|
|
return all sources. `maxEvents` is a signed `int` so a non-positive value is a
|
|
"use the backend's default cap" sentinel.
|
|
- `GetHealthSnapshot()` — point-in-time health snapshot for diagnostics and
|
|
dashboards; pure observation, never blocks on backend I/O.
|
|
|
|
All values use the shared `DataValueSnapshot` / `HistoricalEvent` shapes;
|
|
backend-specific quality/type encodings are translated to OPC UA `StatusCode`
|
|
uints inside the data source.
|
|
|
|
Implementation:
|
|
|
|
- `GatewayHistorianDataSource`
|
|
([`Driver.Historian.Gateway/GatewayHistorianDataSource.cs`](../src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway/GatewayHistorianDataSource.cs))
|
|
— the read backend that talks gRPC to the external `ZB.MOM.WW.HistorianGateway`
|
|
(via the `ZB.MOM.WW.HistorianGateway.Client` package, behind the
|
|
`IHistorianGatewayClient` seam). The alarm-event drain target is the separate
|
|
`GatewayAlarmHistorianWriter` (the gateway `SendEvent` path; see
|
|
[AlarmHistorian.md](AlarmHistorian.md)).
|
|
|
|
The HistorianGateway is the sole historian backend; its config keys and
|
|
deployment prerequisites are in [Historian.md](Historian.md).
|
|
|
|
---
|
|
|
|
## Commons — shared cross-cutting primitives
|
|
|
|
[`src/Core/ZB.MOM.WW.OtOpcUa.Commons/`](../src/Core/ZB.MOM.WW.OtOpcUa.Commons/)
|
|
|
|
`ZB.MOM.WW.OtOpcUa.Commons` is the low-level shared library that the Runtime,
|
|
ControlPlane, AdminUI, and OPC UA server projects all reference. It holds
|
|
cross-cutting primitives with no driver- or host-specific behavior, so the
|
|
heavier projects can share message contracts and value types without taking a
|
|
dependency on each other. It references only `Akka` and the internal
|
|
`ZB.MOM.WW.Audit` package.
|
|
|
|
Folders:
|
|
|
|
- **`Messages/`** — Akka message contracts grouped by concern (`Admin`,
|
|
`Alerts`, `Deploy`, `Drivers`, `Fleet`, `Logging`, `Redundancy`). These are
|
|
the wire/inter-actor messages — e.g. `Messages/Admin/TestDriverConnect.cs`
|
|
(Test Connect request, see [IDriverProbe](#idriverprobe--test-connect)) and
|
|
`Messages/Drivers/DriverHealthChanged.cs` (the driver-health pub/sub payload,
|
|
see [IDriverHealthPublisher](#idriverhealthpublisher--health-pubsub-sink)).
|
|
- **`Interfaces/`** — cluster-facing client contracts such as
|
|
`IAdminOperationsClient`, `IClusterRoleInfo`, and `IFleetDiagnosticsClient`.
|
|
- **`Types/`** — strongly-typed identifier value types: `CorrelationId`,
|
|
`DeploymentId`, `ExecutionId`, `NodeId`, `RevisionHash`.
|
|
- **`Browsing/`** — live-browse abstractions (`BrowseNode`, `IBrowseSession`,
|
|
`IDriverBrowser`) backing the AdminUI address pickers.
|
|
- **`Engines/`** — evaluator seams (`IScriptedAlarmEvaluator`,
|
|
`IVirtualTagEvaluator`, `IAlarmActorStateStore`) consumed by the
|
|
[VirtualTags](VirtualTags.md) / [ScriptedAlarms](ScriptedAlarms.md) engines.
|
|
- **`OpcUa/`** — deferred-publish seams (`IOpcUaAddressSpaceSink`,
|
|
`IServiceLevelPublisher` and their `Deferred*` no-op stand-ins) so address-space
|
|
and [ServiceLevel](Redundancy.md) writes can be wired late.
|
|
- **`Observability/`** — `OtOpcUaTelemetry` (the shared ActivitySource/metrics
|
|
surface).
|
|
|
|
---
|
|
|
|
## See also
|
|
|
|
- [ReadWriteOperations.md](ReadWriteOperations.md) — the driver *capability*
|
|
interfaces (read/write/subscribe) and resilience pipeline.
|
|
- [ServiceHosting.md](ServiceHosting.md) — role gating, the Akka cluster, and
|
|
the external HistorianGateway backend.
|
|
- [AlarmHistorian.md](AlarmHistorian.md) — the store-and-forward SQLite alarm
|
|
sink that drains to `IAlarmHistorianWriter`.
|
|
- [Redundancy.md](Redundancy.md) — driver stability tiers in the redundancy
|
|
context.
|