438484fdca
v2-ci / unit-tests (push) Has been cancelled
Probe 'failover <primary> <backup> <mode> <dir>' drives ScadaBridge's own DataConnectionActor (production primary/backup machine, real DataConnectionFactory, default DataConnectionOptions, FailoverRetryCount 3) against two servers; run.py loses the primary (SIGKILL or SIGSTOP) once the probe reports a steady primary, and the probe requires Bad quality on loss, "Connected to backup", the backup's own value, a fresh ConditionRefresh replaying the native condition, and a write via the backup. rig-failover.sh runs the same probe against the local docker-dev central pair. Matrix legs gain a UNS subscription case and a Server-object alarm subscription on the reconnected session whose ConditionRefresh must replay the active condition (11 cases). run.py gains --versions, --suites, --failover-modes and --failover-loss. Claude-Session: https://claude.ai/code/session_01GT67ACmpMR7b9LGRPLijMi
99 lines
5.9 KiB
Markdown
99 lines
5.9 KiB
Markdown
# Actual ScadaBridge consumer compatibility
|
||
|
||
This isolated process references the real ScadaBridge `OpcUaDataConnection` and
|
||
`RealOpcUaClient`, with their own dependency graph. It never loads the candidate
|
||
SDK into that process. The verified consumer source is
|
||
`0e89966da670e289bd875166295bb0e85d8cf8b2`, pinned to SDK 1.5.378.106.
|
||
|
||
Build with an explicit clean consumer checkout:
|
||
|
||
```sh
|
||
dotnet build scripts/validation/scadabridge-sdk-compatibility/Probe.csproj \
|
||
-c Release -p:ScadaBridgeSourceRoot=/absolute/ScadaBridge
|
||
```
|
||
|
||
First build the separate application hosts with the parent
|
||
`scripts/e2e/test-opcua-sdk-compatibility.ps1` harness. Then choose a new output
|
||
directory and pass its original and candidate `ProcessHost.dll` paths:
|
||
|
||
```sh
|
||
python3 scripts/validation/scadabridge-sdk-compatibility/run.py \
|
||
--baseline-host /absolute/matrix/build-baseline/bin/Release/net10.0/ProcessHost.dll \
|
||
--candidate-host /absolute/matrix/build-candidate/bin/Release/net10.0/ProcessHost.dll \
|
||
--probe scripts/validation/scadabridge-sdk-compatibility/bin/Release/net10.0/Probe.dll \
|
||
--results /absolute/fresh-consumer-evidence
|
||
```
|
||
|
||
The helper runs each application version with None, Sign and SignAndEncrypt,
|
||
plus two trust negative controls per secure profile (server does not trust the
|
||
client, client does not trust the server) that must fail closed. None and Sign
|
||
are enabled only in their own test fixtures (`OPCUA_MATRIX_SERVER_SECURITY=none|sign`);
|
||
the ordinary compatibility host remains strict SignAndEncrypt. Before each secure
|
||
leg the probe's `prepare` step provisions the consumer identity through
|
||
ScadaBridge's own certificate code and exports only its public certificate, which
|
||
the runner places in the server's trusted store before the server starts.
|
||
`OPCUA_MATRIX_ACL=off` disables the server's node-ACL check; with it on, masters
|
||
after lmxopcua#808 deny the three UNS cases because this fixture creates the `Eq`
|
||
folder without a deployed generation (fixture limitation, see lmxopcua#782). The task key is
|
||
generated per process pair, issuer/trusted/rejected stores are task-local, and
|
||
server public certificates are explicitly copied. Autoaccept remains false.
|
||
Never commit an entire output directory: it contains the fixture's private PKI.
|
||
|
||
The consumed subset covers API-key activation, raw/UNS scalar and Int32[]
|
||
roundtrips, durable hierarchical browse including the native alarm, data quality
|
||
and timestamps on raw and UNS subscriptions, alarm filter/ConditionRefresh,
|
||
disconnect/reconnect, and (ScadaBridge#125) a Server-object alarm subscription on
|
||
the reconnected session whose ConditionRefresh must replay the active native
|
||
condition (11 cases per trusted leg). History
|
||
and Part 9 acknowledgment are not exposed by the inspected consumer interfaces;
|
||
the parent Shared/CLI matrix covers those application services instead.
|
||
|
||
ScadaBridge#121 (consumer never configured an application certificate) is
|
||
fixed in ScadaBridge `ff835447`/`36c64a6b`. Re-run 2026-11-12 with ScadaBridge
|
||
`424db155`: None, Sign and SignAndEncrypt 9/9 against the SDK 1.5.378 baseline
|
||
(`83ed880e`), the SDK 2.0.1 candidate (`ebdc5892`) and master `182fb353` with
|
||
ACL off; all trust negative controls failed closed. The runner retains the
|
||
nonzero result and each actual failure; it does not loosen trust or change
|
||
expected results to return success. No ScadaBridge production source or user PKI is modified.
|
||
|
||
## Redundancy failover (ScadaBridge#125)
|
||
|
||
`run.py` also runs a `failover` suite (`--suites matrix,failover`, both by
|
||
default; `--versions candidate` runs one application version only). Each leg
|
||
starts two independent servers and runs `Probe failover <primary> <backup>
|
||
<mode> <dir>`, which drives ScadaBridge's own `DataConnectionActor` (the
|
||
production primary/backup state machine, the real `DataConnectionFactory`,
|
||
production `DataConnectionOptions` defaults, `FailoverRetryCount = 3`). Once the
|
||
probe has a steady primary (Good values on a raw and a UNS tag, a write, the
|
||
Server-object alarm subscription with its ConditionRefresh) it writes
|
||
`primary-ready`; the runner then loses the primary and writes `primary-killed`.
|
||
The probe must then see Bad quality pushed, "Connected to backup", Good values
|
||
whose first value is the backup's own (proving the source), a fresh
|
||
ConditionRefresh replaying the native condition, and a write through the backup.
|
||
Two loss shapes run per security mode (`--failover-modes None,SignAndEncrypt`,
|
||
`--failover-loss kill,freeze`):
|
||
|
||
- `kill` — SIGKILL; the OS closes the primary's sockets, so the client sees the loss at once.
|
||
- `freeze` — SIGSTOP; the sockets stay open and silent (hung host / dead link), so only the
|
||
consumer's session keep-alive can detect it.
|
||
|
||
ScadaBridge does not read `ServiceLevel`/`ServerRedundancy`: its failover is
|
||
endpoint-level (primary/backup connection details), so two independent fixture
|
||
servers exercise exactly what it consumes. `rig-failover.sh <Probe.dll> <dir>`
|
||
runs the same probe against the real local docker-dev pair (central-1 `:4840`,
|
||
central-2 `:4841`), `docker kill`s central-1 once the primary is steady and
|
||
starts it again afterwards. The rig admits anonymous sessions read-only and has
|
||
no active native alarm, so that variant checks values and ConditionRefresh
|
||
completion only. Local docker-dev only; never point it at a wonder box.
|
||
|
||
Run 2026-11-12 (ScadaBridge#125) with ScadaBridge `94ae1860` (SDK client
|
||
1.5.378.106), ACL off, against the SDK 1.5.378.182 baseline (`83ed880e`) and
|
||
master `a28adba2` (SDK 2.0.1): matrix 11/11 in None, Sign and SignAndEncrypt on
|
||
both, all 8 trust controls failed closed (0x80130000 / 0x801A0000); failover 8/8
|
||
legs (11/11 cases each). Loss-to-backup timings are the same on both versions:
|
||
`kill` about 20 s (three 5 s reconnect attempts, then the backup), `freeze` about
|
||
20 s to detect and 86–130 s to reach the backup, because each reconnect attempt
|
||
waits out a connect timeout against the silent primary. `rig-failover.sh` passed
|
||
9/9 against the docker-dev pair (image SDK 2.0.1), central-1 killed, about 20 s
|
||
to central-2.
|