Skip to content

fix(gateway): frame a component's loss-of-comms fault from the apps it hosts - #681

Draft
bburda wants to merge 4 commits into
fix/persist-entity-freeze-framesfrom
fix/comms-lost-frame-from-hosted-entities
Draft

fix(gateway): frame a component's loss-of-comms fault from the apps it hosts#681
bburda wants to merge 4 commits into
fix/persist-entity-freeze-framesfrom
fix/comms-lost-frame-from-hosted-entities

Conversation

@bburda

@bburda bburda commented Sep 10, 2026

Copy link
Copy Markdown
Collaborator

Summary

A PLC_COMMS_LOST fault reported by a plugin-backed component carried a rosbag and no freeze frame. A component holds no data of its own: no DataProvider, and x-plc-data answers for apps only. The apps it hosts kept serving the last row read before the link died.

The capture now descends into the entities a reporting source hosts when the source yields nothing: one frame per hosted app that has values to read, named after the app, with x-medkit.source. An alarm-only app contributes no entry, and the component gets none. A component with its own provider keeps its single frame. App faults are unchanged. The hosting relation comes from faults::resolve_entity_source_fqns, the resolver /components/<id>/faults uses.

The tutorial and the REST reference now say what a plugin-backed frame carries: source and captured_at always, connected: false and source_timestamp only when the plugin's payload reports them. The OPC UA plugin's data provider reports neither, so its frames carry the last known values without connected: false. That is a separate item on the plugin.

Stacked on #660 and #680, which touch the same file.

Issue

Type

  • Bug fix
  • New feature or tests
  • Breaking change
  • Documentation only

Testing

Five unit tests in test_entity_freeze_frame_capture.cpp: hosted apps read through their providers (two frames, no link flag), one data-bearing and one alarm-only app (one frame), hosted apps read through the route, a component with its own provider (one frame, with a positive control), and an app fault (one frame).

New scenario run_comms_lost_frame_test.sh: writes a value, kills the OPC UA server, asserts the component's fault carries a frame named after the hosted app holding that value. Passes on this branch, fails on the base with "snapshots": []. It runs in the plugin's alarm CI job and is documented in the plugin README.

test_all.sh 18/18, alarm and discovery suites green. Gateway suite: 3955 tests, 0 failures on the stacked tree.

Checklist

  • Breaking changes are clearly described (and announced in docs / changelog if needed)
  • Tests were added or updated if needed
  • Docs were updated if behavior or public API changed

…t hosts

A protocol bridge reports PLC_COMMS_LOST under the PLC runtime component,
and a component holds no data values of its own: it exports no
DataProvider, and the x-plc-data route answers for apps only. The fault
detail therefore carried a rosbag and no freeze_frame at all, on the
confirm edge and after a restart, while the apps that component hosts kept
serving the last row they read before the link died.

EntityFreezeFrameCapture now falls back to the entities a reporting source
hosts when that source read nothing itself, one freeze_frame entry per
hosted app, named after the app. The gateway resolves them through
faults::resolve_entity_source_fqns, the same helper /components/<id>/faults
scopes by, so the frames come from exactly the sources that endpoint
attributes to the component. The component itself gets no entry, there is
nothing on it to read. A component that serves its own values is
unchanged, and so is every app fault.

Adds a docker scenario that writes a distinctive value, kills the OPC-UA
server, and asserts the component's fault detail carries that value in a
frame named after the hosted app.

Stacks on fix/opcua-rescan-comms-lost-freeze-source (PR #660), which
touches the same file.
The tutorial promised that a hosted-app entry carries connected: false and
source_timestamp. On the shipped path it carries neither. An app with node-map
entries is data-bearing, so the gateway resolves the plugin's DataProvider for
it, and OpcuaPlugin::list_data returns items and nothing else. The link flag
and the timestamp are added by the x-plc-data route, which the provider path
never consults. The API reference already stated the conditional form, so the
two documents disagreed.

Both now say the same thing: the two fields ride a frame only when the
plugin's payload reports them, a frame read through a DataProvider names
source and captured_at and need not carry either, and the OPC UA plugin's
list_data reports neither today. The reference also gains the naming rule a
component's frames follow, and the plugin changelog gains the behaviour entry.

Pinned by a unit test in the shipped shape: providers for the hosted apps,
none for the component, a route fetcher that would report connected: false if
consulted, and the assertion that the frames name the provider path and carry
no link flag. The route-path test keeps its coverage under a name that says
which path it exercises, which is what let the wrong sentence through.

Adding connected and source_timestamp to list_data is deliberately not done
here: that payload is read by every list_data consumer.
… it lives

The scenario was invoked by nothing: not the plugin workflow, not test_all.sh,
not the README. A regression it would catch had no standing guard and no
discoverable entry point.

It now runs in the alarm job, right after the alarm suite, which builds the
same two images from the same Dockerfile, so the scenario's build is a cache
hit rather than a second cold build. That job's failure dump learns the two
container names. The README gains the invocation next to the discovery-race
one.

Corrects the reason the other suites cannot see this case, which the script's
own header had wrong: the alarm suite does stop its server once, for the
reconnect check. What none of the suites here do is read a fault's
environment_data, so a detail that lost its freeze-frame looks exactly like
one that kept it.

Not added to test_all.sh, which does not carry the alarm suite either.
"One entry per hosted app" is false for an App that exists only to host alarm
events. An event_alarms entry, and the auto_alarms fallback App, is registered
under the component but owns no node-map entries, so has_data() is false, no
DataProvider resolves for it, and its x-plc-data route answers 404. The engine
is right to skip it, there is nothing to read. The three sentences describing
the behaviour were not: the API reference, the tutorial and this plugin's
changelog now all say one entry per hosted app that has values to read, and
name the alarm-only app as the case that yields none.

Pinned by a unit test in the shipped shape: a data-bearing hosted app with a
provider, an alarm-only one with no provider whose route answers nothing, and
the assertion that exactly one frame comes back, named after the app that had
values. The alarm-only app's read is counted so the test cannot pass by never
reaching it.

The README paragraph also reasoned about the wrong two suites. It now names
the two this file actually documents: the OpenPLC one keeps its server up for
the whole run, the discovery race does take its server away but asserts on
what discovery names the component. Neither reads a fault's environment_data.
@bburda bburda self-assigned this Sep 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant