Drivers report Healthy with 0 errors while silently ignoring tags they skipped at init #541
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?
Split out of #540, whose addressing half is fixed. This is the half that made that defect invisible rather than merely wrong, and it is not a FOCAS problem.
The behaviour
Every driver builds its
RawPath -> definitiontable atInitializeAsyncand tolerates a per-tag miss: an unmappableTagConfig, an unparseable address, a capability-matrix rejection. The tag is logged and skipped, never enters the poll table, and every read of it returnsBadNodeIdUnknown.Tolerance is the right call — one bad tag must not fail a whole driver. The problem is what an operator can see afterwards: nothing. Skips happen at init and are not errors, so:
Healthyerrors/5 minstays0Last errorstays—Last readadvances live, because the driver's other tags are fineOn the Makino in #540,
focas-z-34184showed exactly that while ignoring 20 of its 22 tags. There is no surface anywhere in the AdminUI that would have said otherwise.It is fleet-wide
Eight drivers skip-and-log the same way:
FocasDriver.cs:183,:206,:213ModbusDriver.cs:228S7Driver.cs:194AbCipDriver.cs:292AbLegacyDriver.cs:139TwinCATDriver.cs:140SqlDriver.cs:710MqttDriver.cs(same shape)So a per-driver fix would be the wrong shape — it is one missing signal, eight times.
Why the #540 guardrails do not close it
#540 added a real address parse to
FocasTagConfigModel.Validate()(authoring) and toFocasTagDefinitionFactory.Inspect()(deploy gate). Those stop the specific cause found there, and only for FOCAS. They do not cover:dataTypereject, aTagConfigthat maps to nothingThe gate is upstream and per-cause. What is missing is a downstream statement of fact: this driver is not polling N of its M tags.
Suggested direction
Carry the count on the health snapshot and surface it on
/hosts, the same way #521 resolved per-host connectivity:SkippedTagsfield onDriverHealthChanged(a count plus a bounded list ofRawPath+ reason), populated from what each driver already logs/hosts, so "Healthy, 0 errors" cannot coexist with "ignoring most of my tags"DriverHostStatuswas dropped for exactly that reason)Deliberately not
Degraded. Latching a driver non-Healthy for a config problem would interact badly with #527: the comms-loss detector trips on 90 s without a successful read AND non-Healthy, so a driver whose tags were ALL skipped would have no successful reads, latch non-Healthy, and reconnect-loop against a perfectly healthy device forever. The signal has to be additive to the state, not a change of it.An
IDriverseam (GetSkippedTags()) would make this uniform across the eight, rather than eight separate counters.Fixed — merged to
masterand pushed, merge355e59aa(branchfix/541-skipped-tag-visibility, 4 commits).What shipped
A new optional capability
ISkippedTagReporterover a sharedSkippedTagLedger, carrying the authored count, the exact skipped count and a capped sample ofRawPath+ reason onDriverHealthChanged→ a Tags column on/hosts. Transport is the Phase 5 telemetry stream, not a table — the #521 lesson, since a driver-only node has no ConfigDb.Reading the column:
—12 tags20 / 22 skipped(amber)22 / 22 skipped(red)The suggested
IDriver.GetSkippedTags()became an optional capability instead, matching the existingIHostConnectivityProbe/IRediscoverableidiom, so "cannot report" stays distinguishable from "reports none". The ledger is shared precisely so the reset/commit/cap rules are one implementation rather than twelve — the duplication this issue was filed about.Two corrections to the issue
It is twelve drivers, not eight. Every driver in
DriverTypeNames.Allhas an authored-tag skip path. Unlisted here: Calculation (noscriptId), MTConnect (no readable DataItem id), Galaxy (noattributeRef), OpcUaClient (nonodeId). OpcUaClient's was the quietest of the twelve —NamespaceMapdrops such an entry with a barecontinueand not even a log line. A hand-maintained list would have shipped those four still invisible, soSkippedTagReporterFleetTestsdiscoversIDriverimplementations reflectively and a thirteenth driver cannot re-introduce the silence.Sql drops tags at two points, and the report is the union. The blob that does not map (
BuildTagTable) and an identifier the §8.1 catalog gate rejects against the live catalog (ApplyCatalogGateAsync). The second is what schema drift produces — a renamed column, a dropped view — months after a deployment sealed green. Reporting only the first would have that driver claim0 skippedwhile polling half its tags, which is worse than reporting nothing, because the column would then be actively reassuring.Design points that are load-bearing
Degraded— exactly as this issue argued. Confirmed against #527: a driver whose tags were all skipped has no successful reads by construction, so it would satisfy both halves of the conjunction and reconnect-loop forever against a healthy device.PublishHealthSnapshot's fingerprint, and covers the sample, not just the counts. A skip leaves every other fingerprint component identical to a clean driver's, so without it the dedup swallows precisely the publish carrying the signal — the trap that already bit the rediscovery and host-status channels. Counts alone would also miss a redeploy that fixes one tag and breaks another.host_statusesbeside it: a singular message field has real presence in proto3, and ahas_flag would collide with protoc's generatedHasSkippedTags.Live gate — docker-dev, two rounds
Record:
docs/plans/2026-08-05-skipped-tag-visibility-live-gate.md.Round one broke 3 of 13 tags on a Modbus driver:
3 / 13 SKIPPEDin amber, driver HEALTHY with 0 errors and an advancing last read — the exact combination this issue describes. It also found a defect the unit tests could not: two drivers that faulted before building a tag table rendered0 tagsagainst two authored tags apiece, because a never-committed ledger returned a zero report. A zero report is a positive claim — "this driver skipped nothing" — and making it on behalf of a driver that never looked is the same quiet inaccuracy this issue is about.GetSkippedTags()now returns null until the first committed build. Every unit test had driven a driver through a completed build first, which is why none caught it.Round two closed the two remaining gaps with one setup: 25 unmappable tags on a SITE-A driver gave
25 / 26 SKIPPEDwith the sample capped at exactly 20 and the tooltip reading(+5 more not shown); trimming to exactly 20 gave20 / 21with no note, pinning the off-by-one. SITE-A also settles the central proto path — Phase 6 gave it its own Akka mesh, DPS does not federate, and the lone ClusterClient reference is a point-to-point command path rather than gossip, soTelemetryProtoMapNode→ gRPC →TelemetryProtoMapCentralis the only route to the page. Central's view tracking a change made solely on the site node is the positive control.Unplanned finding: three of the four drivers showing skips on the rig were not planted.
sql-oracle-fixture,sql-postgres-fixtureandgate-modbus-renamedhad been silently ignoring their tags for an unknown length of time, each reporting Healthy with zero errors. The column surfaced them on its first run.Verification
Red-first three ways: dropping
SkippedTagDigestfrom the fingerprint fails the two dedup tests; removing the seam from one driver fails the fleet guard; dropping the catalog-gate leg fails the Sql union test. Full solution builds with zero new warnings against4e22f024(diffed a full-t:Rebuildcensus — an incremental build under-reports on this tree). All twelve driver suites plus Runtime / ControlPlane / AdminUI / Core / Commons pass.Not consumer-facing to ScadaBridge — AdminUI observability, no OPC UA wire-contract change. A skipped tag's node still materializes and still reads
BadNodeIdUnknownexactly as before; what changed is that an operator can now see it.