OPC UA adapter: ServiceLevel-aware failover (prerequisite for OtOpcUa Warm redundancy, lmxopcua#717) #138
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Prerequisite for lmxopcua#717 (Warm non-transparent redundancy, spec
docs/plans/2026-11-11-warm-redundancy-717-design.md§5 in OtOpcUa).ScadaBridge fails over between OPC UA endpoints only after
FailoverRetryCountconsecutive unstable disconnects (DataConnectionLayer/Actors/DataConnectionActor.cs:655-690) and never readsServiceLevel(i=2267),RedundancySupportorServerUriArray. Under OtOpcUa Warm, the standby keeps a healthy session but servesBadOutOfServicedata at a standby ServiceLevel band (< 200), so a ScadaBridge connection on the standby would sit on Bad quality indefinitely and never fail over: an outage.Needed: make the OPC UA adapter ServiceLevel-aware. Monitor
i=2267on the active endpoint, and treat a sustained Degraded band (< 200 for N s, configurable) while the other endpoint reports a higher level as a failover trigger (and prefer the higher-ServiceLevel endpoint on connect). Also add a test for the unsolicited RefreshStart/RefreshEnd pair OtOpcUa sends on promotion.Blocking: no OtOpcUa tier may set
OpcUa:Redundancy:Mode=Warmuntil this ships and is deployed to that tier.Implemented and pushed to
mainas712c5117(not deployed).Behaviour
An OPC UA connection with a backup now polls
Server_ServiceLevel(i=2267) on the connected endpoint every 2 s. MxGateway connections, single-endpoint connections and disabled connections never read it.i=2267, dispose; 15 s budget). It fails over only if the other endpoint reports a Healthy level (200 or more) that is higher than the connected one.SwitchToOtherEndpoint(). The ServiceLevel switch then runsBecomeReconnecting(ReconnectCause.ServiceLevelFailover): zero delay, no unstable-disconnect accounting, andReSubscribeAll/ReSubscribeAllAlarmson the new endpoint. Site event:Failover from Primary to Backup (ServiceLevel), with both levels. Poll and probe results are fenced by adapter generation.Config knobs
"serviceLevelFailover": {"enabled": true, "degradedWindowSeconds": 10, "minDwellSeconds": 60}.nullmeans those defaults.--primary-config(documented in Component-CLI).ScadaBridge:DataConnection:ServiceLevelPollInterval(2 s)ServiceLevelProbeTimeout(15 s)Tests
DataConnectionServiceLevelFailoverTests: 19 Akka TestKit cases. Each trigger and non-trigger case is covered, and every "no switch" case has a positive control (the reads or the probe did happen).OpcUaServiceLevelTests: 6 cases.Disconnected.SecureOpcUaTestServer.SetServiceLevel): the reali=2267read tracks 250/240/90/50/1.DataConnectionFactory: standby primary 90 + backup 250 ends up on the backup; 240 + 250 stays on the primary.RealOpcUaPromotionRefreshTests: an unsolicited RefreshStart / conditions / RefreshEnd on an existing item is framed as its own snapshot with a fresh epoch, and live derivation resumes after it.DataConnectionPromotionRefreshTests: the existing snapshot handling forwards it in order and reconciles the cache. A condition the promoted node no longer retains is gone from a late subscriber's replay.serviceLevelFailovernull and adds no flat keys) and two bUnit form tests.Enabledignored, a Healthy sample not breaking the streak, unreachable treated as switchable, policy read from the backup, no-backup check removed.ZB.MOM.WW.ScadaBridge.slnx: 0 warnings, 0 errors.ExecutionAwareLoggingTests.DisabledLevel_PublishesNothingfailed once under full-suite load. It is unrelated to this change and passes in isolation and on the rerun.Keeping this open
I'm leaving this issue open as the deploy gate for lmxopcua#717. The blocking condition in the description is "ships and is deployed to that tier", and nothing is deployed: DEV vd03 runs
424db155and QA runs0e540291. Close it when a tier that will setOpcUa:Redundancy:Mode=Warmruns712c5117or later. On QA, confirm the OtOpcUa pair's connection has the backup configured: the feature is inert without one.Docs:
docs/requirements/Component-DataConnectionLayer.md(new section ServiceLevel-aware failover, settings tables, alarm note),docs/components/DataConnectionLayer.md,docs/requirements/Component-CLI.md.Deployed to QA vq03/vq04 inside build
8e32f353(2026-11-13, record ScadaBridge8ba5d5d7;712c5117is an ancestor) and to DEV vd03 in the same build (581e25ca).It does nothing on either tier yet: QA ScadaBridge has a single data connection (
MxGateway, no backup) and no OPC UA connection, so no connection polls ServiceLevel. Stays open as the lmxopcua#717/#907 prerequisite until a QA OPC UA primary/backup connection to the OtOpcUa pair exists and the Warm flip is gated.Gated live on QA, 2026-11-13, against OtOpcUa running
Mode=Warm(lmxopcua#907). Closing.QA runs
8e32f353, which contains712c5117. New test connectionWarmCanary:serviceLevelFailoverleft at its defaults: enabled, 10 s, 60 s;Records: ScadaBridge
docs/deployment/deployment-records/2026-11-13-qa-warmcanary-opcua-connection.md(933da568) and lmxopcuadocs/deploy/2026-11-13-qa-warm-717.md(1862fab6).AdapterDisconnected→ 3 connect retries →Failing over from "Primary" to "Backup""Backup" endpoint reports ServiceLevel 50 (Degraded band, < 200) — will check the other endpoint now (connect-time preference), thenFailing over from "Backup" to "Primary" (ServiceLevel 50 Degraded for 4s; "Primary" reports 250)"Primary" endpoint reports ServiceLevel 0 …(+0.77 s), thenServiceLevel is unknown — degraded watch cleared(+6.8 s), then the connectivity failoverReconnected successfully on "Primary"13:56:52.401 →reports ServiceLevel 90 … connect-time preference.413 →Failing over from "Primary" to "Backup" (ServiceLevel 90 Degraded for 0s; "Backup" reports 250).436 →Reconnected … "Backup".475MxGatewaywas unaffected throughout: Connected, 2196/2196 Good. The artifact redeploys were hash-guarded no-ops for it.Not shown live:
Follow-up #141. A graceful stop of the Warm Primary shows ServiceLevel 0 for only about 6 s, which is shorter than the 10 s window. The streak is then cleared as unknown when the server closes, so that case still costs the 66 s connectivity retry cycle. This is not a regression; #141 proposes probing the other endpoint at once on a disconnect that follows a degraded sample.
https://claude.ai/code/session_01GT67ACmpMR7b9LGRPLijMi