Files
lmxopcua/docs/plans/2026-08-05-skipped-tag-visibility-live-gate.md
T
Joseph Doherty 6e9e4fa5ab docs: live-gate round two — truncation note + central proto path both proven (#541)
25 unmappable tags on SITE-A's sa-modbus2 closed both gaps in one setup: chip
25/26 SKIPPED, sample capped at exactly MaxSample=20, tooltip '(+5 more not
shown)'. Trimming to 20 skipped gave 20/21 with NO truncation note, pinning
the off-by-one boundary.

SITE-A is the discriminator for the proto path. Phase 6 partitioned it into
its own Akka mesh -- central's MEMBERS panel lists only central-1/central-2,
and DistributedPubSub does not federate across meshes. The single site-a
reference in central's log is the ClusterClient receptionist, a
point-to-point command path, not gossip. So the report's only route to the
page is TelemetryProtoMapNode -> gRPC -> TelemetryProtoMapCentral, and
central's view tracking a change made solely on the site node is conclusive.

Also corrects round one's claim that the fused central pair meant telemetry
never crossed gRPC -- wrong about this rig. All six ClusterNode rows carry
GrpcPort 4056 and Telemetry__Mode=Grpc; the two 'Telemetry stream to node'
lines that suggested a two-node dial set were startup failure warnings.

Rig restored: test tags deleted, SITE-A redeployed, sa-modbus2 back to 1 tag.
2026-08-06 02:12:44 -04:00

7.4 KiB

Live gate — skipped-tag visibility (Gitea #541)

Date: 2026-08-05 Rig: local docker-dev/ (OrbStack), MAIN pair central-1 / central-2, AdminUI http://localhost:9200 Build under test: fix/541-skipped-tag-visibility Result: PASS. The gate found one defect unit tests did not (§4). Round two on 2026-08-06 closed the two items round one left unproven and corrected a wrong claim about the rig's topology (§6).

What needed proving

The unit suites pin the ledger semantics, the publish fingerprint, the proto round-trip and the view-model. None of them prove the thing an operator actually depends on: that the Tags column on /hosts renders the right cell, in the right colour, with actionable detail, against a driver that is genuinely skipping tags on a real deployment.

Method

The rig already carried eleven driver instances. Three of the thirteen tags on the pymodbus Modbus driver had their TagConfig.region rewritten to NoSuchRegion — the realistic shape of a bad blob — then MAIN was redeployed via POST /api/deployments. Backup of the original thirteen blobs taken first; all three restored and redeployed at the end (§5).

Results

Driver logs confirmed the skips reached the driver:

[WRN] Modbus tag config did not map to a definition; skipping. Driver=MAIN-modbus RawPath=pymodbus/plc/Probe3
[WRN] Modbus tag config did not map to a definition; skipping. Driver=MAIN-modbus RawPath=pymodbus/plc/Probe4
[WRN] Modbus tag config did not map to a definition; skipping. Driver=MAIN-modbus RawPath=pymodbus/plc/Probe5

/hosts then rendered — note every other column on the pymodbus row reads perfectly healthy, which is the entire point:

Driver Status Errors/5 min Last error Last read Tags Chip
pymodbus HEALTHY 0 — advancing 3 / 13 SKIPPED chip-warn (amber)
sql-oracle-fixture HEALTHY 0 — advancing 1 / 1 SKIPPED chip-bad (red)
sql-postgres-fixture HEALTHY 0 — advancing 1 / 1 SKIPPED chip-bad (red)
gate-modbus-renamed HEALTHY 0 — advancing 1 / 1 SKIPPED chip-bad (red)
abcip-browse HEALTHY 0 — advancing 5 tags none
adsfa (Calculation) HEALTHY 0 — — 0 tags none
opcua1 FAULTED 1 (config error) — — none
rtu-gate FAULTED 1 Connection refused — — none

Tooltip on the amber chip, verbatim:

3 of 13 authored tag(s) are not being polled. They report BadNodeIdUnknown and the driver's state does not reflect them. pymodbus/plc/Probe3: TagConfig did not map to a definition · pymodbus/plc/Probe4: … · pymodbus/plc/Probe5: …

and on a red one:

This driver is polling NONE of its 1 authored tag(s). …

Three of the four skipping drivers were not planted. sql-oracle-fixture, sql-postgres-fixture and gate-modbus-renamed were already misconfigured on this rig and had been silently skipping their tags for an unknown length of time, each reporting Healthy with zero errors. The column surfaced them on its first run — an unplanned but fairly direct demonstration of the defect being real rather than theoretical.

Restoring the three blobs and redeploying returned pymodbus to 13 tags with no chip, which also exercises the clear-on-clean-rebuild path live.

4. Defect the gate found — 0 tags on a driver that never built a table

The first pass rendered 0 tags for opcua1 and rtu-gate. Both are FAULTED, both have two authored tags in the ConfigDb, and neither ever completed InitializeAsync — so their ledgers had never committed and Snapshot() returned SkippedTagReport.Empty.

A zero report is a positive claim — "this driver skipped nothing" — and making it on behalf of a driver that never got as far as looking is precisely the quiet inaccuracy #541 exists to remove. Not caught by unit tests because every one of them drove a driver through a completed build first.

Fixed by making ISkippedTagReporter.GetSkippedTags() and SkippedTagLedger.Snapshot() return null until the first committed build, so the column shows the same em-dash it already used for a driver that cannot report at all. Both are no answer; neither is nothing skipped. Pinned by three new SkippedTagLedgerTests cases plus a corrected FOCAS assertion. Re-gated on a rebuilt image: both faulted drivers now render —, while adsfa — which genuinely has zero authored tags and did build — correctly still renders 0 tags. That pair is the discriminating case.

5. Rig left as found

The three pymodbus blobs were restored from the pre-change backup and MAIN redeployed; /hosts shows 13 tags. The three pre-existing skipping drivers were left exactly as they were — they are the rig's own configuration, not this gate's.

6. Round two (2026-08-06) — truncation note + the central proto path

Both items round one left unproven were closed by one setup, chosen so that a single observation settles both. One correction first: round one recorded "the rig's central pair is fused admin+driver, so the snapshot never crossed the Phase 5 gRPC telemetry stream." That was wrong about the rig. All six ClusterNode rows carry GrpcPort 4056, every node runs Telemetry__Mode=Grpc, and central dials all six — the two Telemetry stream to node … lines seen in round one were startup failure warnings, not the dial set.

Setup. 25 tags with region: NoSuchRegion added to sa-modbus2 on SITE-A, joining its one good tag (26 total), then SITE-A deployed. SITE-A is the discriminator: Phase 6 partitioned it into its own Akka mesh, so central's MEMBERS panel lists only central-1:4053 and central-2:4053. DistributedPubSub fans out within one mesh's mediator and does not federate, and the single akka.tcp://otopcua@site-a-1 reference in central's log is the ClusterClient receptionist — a point-to-point command path, not gossip. So a SITE-A driver's health can reach central's /hosts by exactly one route: TelemetryProtoMapNode → gRPC TelemetryStreamService/Subscribe → TelemetryProtoMapCentral.

Result — 25 skipped of 26 authored:

Fact Observed
Chip 25 / 26 SKIPPED, chip-warn
Driver status HEALTHY, 0 errors (as ever)
Sample entries in tooltip 20 (= SkippedTagLedger.MaxSample)
Truncation note (+5 more not shown)
Skips logged on site-a-1 25

That proves the truncation note, and simultaneously proves the proto path: the counts and all 20 sample entries with their RawPath + reason survived the wire.

Positive control, doubling as the cap boundary. Five of the 25 broken tags were deleted and SITE-A redeployed. Central's page tracked the change made only on the site node — 20 / 21 SKIPPED, sample still 20, and no truncation note, correct at exactly MaxSample (the off-by-one that would have read "+0 more not shown"). A live-updating view of a node with no gossip path is conclusive.

Rig restored: all 25 test tags deleted, SITE-A redeployed, sa-modbus2 back to 1 tag; the deployment revision hash returned to the pre-test 6178a8f3….

Still not proven live

Nothing outstanding from the original two. Remaining unit-only coverage is the ledger's concurrency guard (a torn read under a real init/publish interleaving) — inherently hard to force on a rig, and the lock makes it a code-inspection matter rather than an observational one.