docs(mesh-phase4): ServiceLevel semantics on DB-less nodes + driver ConfigDb cut
Claude-Session: https://claude.ai/code/session_01GASWkNEi68FSCtvr6rLoEW
This commit is contained in:
+28
-1
@@ -16,7 +16,7 @@ The runtime pieces live in:
|
||||
| `OpcUaPublishActor` | `OtOpcUa.Runtime.OpcUa` | Per-driver-node; subscribes to the `redundancy-state` topic, computes a health-aware ServiceLevel byte via `ServiceLevelCalculator` (see below), and forwards it to `IServiceLevelPublisher`. |
|
||||
| `IServiceLevelPublisher` / `SdkServiceLevelPublisher` | `OtOpcUa.Commons.OpcUa` / `OtOpcUa.OpcUaServer` | Writes the byte into the SDK's `Server.ServiceLevel` Variable. Production binds `DeferredServiceLevelPublisher`, which swaps in the real `SdkServiceLevelPublisher` once the SDK is up (it needs `IServerInternal`, available only after `StandardServer.Start`); until then writes route through `NullServiceLevelPublisher`. |
|
||||
| `ServiceLevelCalculator` | `OtOpcUa.Cluster.Redundancy` (`Core.Cluster`) | Pure function `(NodeHealthInputs) → byte` — the DB/probe-aware tiering (see truth table below). Covered by `ServiceLevelCalculatorTests`. **Now the live publish path** — `OpcUaPublishActor` calls it on every `HealthTick` and `RedundancyStateChanged` event. Moved to `Core.Cluster` so Runtime can reach it without a Runtime→ControlPlane reference. |
|
||||
| `DbHealthProbeActor` | `OtOpcUa.Runtime.Health` | Per-node; runs `SELECT 1` against ConfigDb every 5s. Read by the health endpoint AND by `OpcUaPublishActor` (the `DbReachable` ServiceLevel input). |
|
||||
| `DbHealthProbeActor` | `OtOpcUa.Runtime.Health` | Runs `SELECT 1` against ConfigDb every 5s. Read by the health endpoint AND by `OpcUaPublishActor` (the `DbReachable` ServiceLevel input). **Spawned only when a ConfigDb is present** (admin-role or fused admin+driver nodes) — a driver-only node has no `IDbContextFactory<OtOpcUaConfigDbContext>` to probe, so this actor is not spawned there at all; see "DB-less nodes" below. |
|
||||
| `PeerProbeSupervisor` | `OtOpcUa.Runtime.Health` | Per-node; subscribes to the `redundancy-state` topic and maintains one `PeerOpcUaProbeActor` child per OTHER driver-role peer (spawn on join, stop on departure), so every node is continuously probed by its peers. |
|
||||
| `PeerOpcUaProbeActor` | `OtOpcUa.Runtime.Health` | Spawned by `PeerProbeSupervisor`; pings a peer `opc.tcp://peer:4840` with a TCP connect (2s timeout) and publishes `OpcUaProbeResult` on the `redundancy-state` topic. A full secure-channel Hello handshake is a possible future upgrade; the TCP connect is the current real probe. |
|
||||
| `ClusterRoleInfo` | `OtOpcUa.Cluster` | Live view of cluster membership + role-leader; exposes `IClusterRoleInfo` to the rest of the host. (Akka's role-leader is *not* what elects the redundancy Primary — see below.) |
|
||||
@@ -56,6 +56,33 @@ The resulting truth table (all tiers are now reachable at runtime):
|
||||
> by the +10 bonus. Clients with the standard "pick highest ServiceLevel"
|
||||
> heuristic continue to prefer the primary.
|
||||
|
||||
#### DB-less (driver-only) nodes — per-cluster mesh Phase 4
|
||||
|
||||
**Client-visible change.** Per-cluster mesh Phase 4 cut the ConfigDb connection off driver-only
|
||||
nodes (Akka role `driver`, not `admin`) — `Program.cs` registers `AddOtOpcUaConfigDb` only when
|
||||
`hasAdmin`. With no ConfigDb to probe, `DbHealthProbeActor` is **not spawned** on such a node
|
||||
(`ServiceCollectionExtensions.WithOtOpcUaRuntimeActors` skips it whenever the resolved
|
||||
`IDbContextFactory<OtOpcUaConfigDbContext>` is null), and `OpcUaPublishActor` runs in a `dbLess`
|
||||
mode instead of the health-aware path above:
|
||||
|
||||
- `DbReachable` is fed **constant `true`** — LocalDb is the config store and is in-process, so it
|
||||
can never be "unreachable" the way a remote SQL Server can.
|
||||
- `Stale` is derived from the `redundancy-state` snapshot age alone (`(now − snapshotEntry.AsOfUtc)
|
||||
> 30 s`) — there is no DB-health term to fold in.
|
||||
- `OpcUaProbeOk` and `IsDriverPrimary` are unchanged (peer OPC UA probe, oldest-Up-member election).
|
||||
|
||||
**Consequence:** a healthy driver-only node publishes **240** as a follower or **250** as the
|
||||
driver Primary **even when central SQL is unreachable** — the survive-alone posture the fetch-and-
|
||||
cache design (Phase 3) exists to support. Contrast this with a `Direct`/DB-backed node (fused
|
||||
admin+driver, or any node still reading ConfigDb directly): an unreachable ConfigDb there still
|
||||
drives `DbReachable=false`, which forces `Stale=true` in the derivation above and the node drops to
|
||||
**100** (or **0** once the sample itself goes stale) via the health-aware tiering, not 240/250.
|
||||
Same byte, opposite meaning depending on which kind of node is publishing it — a client or operator
|
||||
reading ServiceLevel in isolation cannot tell "central is down and I don't care" (driver-only, 240)
|
||||
from "central is down and I'm degraded" (DB-backed, 100/0) without also knowing the node's role
|
||||
shape. `ServiceLevelCalculator.Compute` itself is unchanged; only the inputs a DB-less node feeds
|
||||
it changed — see `OpcUaPublishActor.RecomputeServiceLevel`'s `_dbLess` branch.
|
||||
|
||||
#### Backward-compatible fallback (legacy seam)
|
||||
|
||||
A node with no `DbHealthStatus` wired (e.g. early bootstrap window before the
|
||||
|
||||
Reference in New Issue
Block a user