Repository navigation
diagnostic_bridge: keep the numbers behind a fault, and stop confirming every planned STALE - #691
Conversation
5f282dc to
2745747
Compare
bburda
left a comment
There was a problem hiding this comment.
Thanks for this one too :) The description explains every decision, which made the review much easier, and the STALE severity code looks good to me.
There are many comments, so a short guide. The UTF-8 crash and the missing PostgreSQL catch can take the fault manager down, so I would start with those two. The race with the capture thread, the moving timestamp and the duplicated topic values all go away if the evidence moves to its own table. I am happy to discuss that design with you before you start on it.
A few things outside this diff also need an update:
src/ros2_medkit_fault_manager/README.md(freeze-frame section and theretain_on_clearparagraph) and thecapture()comment insnapshot_capture.hppstill say that a frame exists only with a capture config and is served once the snapshots are gone.docs/api/rest.rst, Snapshot Types, describesfreeze_frameas data captured at confirmation and does not mentionx-reported.src/ros2_medkit_diagnostic_bridge/design/index.rststill showsmap_to_severity(level), the fixed STALE -> CRITICAL mapping and the "STALE as CRITICAL" section.src/ros2_medkit_fault_reporter/README.mdand its design doc do not list thereport()overload with evidence.src/ros2_medkit_msgs/README.mdhas noevidencerow forReportFault, and no note that reporters and the fault manager now have to be updated together.
Also, src/ros2_medkit_diagnostic_bridge/test/test_integration.test.py runs the real bridge with the real fault manager, but it still only checks STALE -> CRITICAL. Could you add a STALE status with an override there, against confirmation_threshold: -3, and an ERROR status whose values you read back with GetFault? A check through the gateway REST fault detail would also catch the broken docs example.
2745747 to
237d92a
Compare
|
Thanks for the detailed review, and for reproducing the UTF-8 crash. I rebased on main and pushed 237d92a with the fixes that don't depend on the storage design:
The new launch test runs the real bridge with the real fault manager at
I left out the gateway REST check for now, because the REST shape changes with the new table. I'll add it with that change. For the evidence table I'd like to agree on the design with you first. My proposal is in the thread on |
|
Thanks for the answers :) Pushed 1545c13, which moves the evidence to its own table as agreed and fixes the remaining comments. Each thread has the details. I also added the gateway REST check I left out last time: One small new parameter: I ran the fault_manager unit and launch tests, the diagnostic_bridge launch tests, the new gateway test and the linters on Jazzy, and the PostgreSQL storage tests against a real PostgreSQL server. |
|
@bburda ready for another look when you have time :) |
…nfirming every planned STALE A DiagnosticStatus carries more than the bridge was willing to use. Key-values were read only to pick a fault code and then dropped, so a node that publishes outlier counts and gate statistics - the numbers it already computed to decide it was unhappy - left a fault record saying it complained but not what it saw. A FAILED report now carries them: ReportFault.srv gains an evidence field of diagnostic_msgs/KeyValue, the type the bridge already receives, and the fault manager keeps them in the fault's freeze frame under a reserved key that cannot collide with a topic name. Written on every FAILED report rather than at confirmation, because a code that keeps approaching confirmation without reaching it is the case the near-miss series exists for. Two existing paths had to learn about the key: a confirmation capture rebuilds the frame from the topics it sampled and would have erased it, and the read path served the frame only when no snapshots remained, which would have hidden the one thing the snapshots do not contain. Bounded per fault code in entries and value length; an over-long value is dropped whole and reported, never truncated into half a number. STALE was mapped to CRITICAL unconditionally, and CRITICAL bypasses debounce, so a GPS that goes STALE in every tunnel confirmed a CRITICAL fault on its first sample and the operator's debounce settings never ran. stale_severity and stale_severity_overrides make that a decision: longest matching prefix on the status name, the same rule the fault manager's entity thresholds use. Only STALE is configurable - the other levels are facts about the status, not deployment choices. An override that does not name a severity is reported and ignored rather than defaulted, because applying CRITICAL to a typo hands back exactly the behaviour the operator was configuring their way out of. docs/config/diagnostic-bridge.rst documented STALE as SEVERITY_ERROR. The bridge has always sent CRITICAL; the table now says what the code does. Closes selfpatch#668
…erity - Replace invalid UTF-8 in evidence keys and values instead of throwing. A Latin-1 byte in a diagnostic value used to crash the fault manager. - Catch IgnorableConnectionException around the new storage calls in handle_report_fault and handle_get_fault. - Rename the evidence key to x-medkit-reported. - Add a 128-byte key limit and count the value limit in bytes. - Remove REQ_INTEROP_109 and its @verifies tags. - Add a bridge launch test at confirmation_threshold -3: STALE with and without an override, ERROR values read back with GetFault, and a non-UTF-8 value. - Update the docs and comments that still described the old behavior.
Evidence was merged into the freeze-frame row, which the capture thread
also writes. Move it to a reported_evidence table keyed by
(fault_code, occurrence, source_id, key) in the memory, SQLite and
PostgreSQL backends. Only the report handler writes it, so the two
paths never write the same row, and preserve_reported_evidence is gone.
- The occurrence is occurrence_count after the report. A new occurrence
starts with no evidence. With snapshots.retain_on_clear on, the last
snapshots.max_evidence_occurrences occurrences are kept (default 10).
- GetFault serves the current occurrence in the freeze frame as
"x-medkit": {"reported": {"<source_id>": {"<key>":
{"value", "reported_at"}}}}. With per-topic snapshots served, the
frame holds only the evidence. captured_at_ns stays the capture time.
- reported_at is kept per key.
- At most 32 keys per occurrence over all sources. An oversized update
removes the stored key. Nothing is written when every entry is dropped.
- Drop REQ_INTEROP_110 and its @verifies tags.
- Docs: the STALE warning starts with the current behaviour, and the
lines about the behaviour before this PR are removed.
- Tests: storage tests for all three backends, a launch test with a
capture config, a clear and a new occurrence, and a gateway test that
reads the evidence through the REST fault detail.
A wrapped PlantUML note line and a wrapped list item broke the docs build. Put each on one line.
a0f8ccc to
757cb53
Compare
|
Rebased on main now that #690 is merged, and pushed 757cb53. The conflicts were only where both PRs added code next to each other ( Ran the fault_manager, diagnostic_bridge and PostgreSQL tests, the linters and the docs build again on the rebased branch. |
What this is
Both halves of #668, which are the same complaint about the same message: a
DiagnosticStatuscarries more than the bridge was willing to use.The numbers behind the fault (#668 part 1)
A sensor fusion node publishes
rejected_fixes=37,nis=0.03. The bridge read thosekey-values only to pick a fault code and dropped the rest, so the fault record said a node
complained but not what it saw.
A FAILED report now carries them.
ReportFault.srvgainsdiagnostic_msgs/KeyValue[] evidence- the type the bridge already receives, andros2_medkit_msgsalready depends ondiagnostic_msgs, so this costs no new dependency andno conversion. The fault manager keeps them in the fault's freeze frame under
x-reportedand
GET /api/v1/apps/{app}/faults/{code}serves them:A field rather than a separate attach call. The issue left this open. The evidence is
part of the event: a second call can fail on its own and leave a fault with no numbers, or
numbers with no fault. It also keeps the door open to putting evidence on near-miss entries
later without another API change - not done here, and out of scope.
Four decisions inside that are worth looking at:
confirmation without reaching it is exactly the case the near-miss series exists for, and
evidence written only at confirmation would miss it.
x-reported, not a flat merge. The frame's other keys are topic names, always fullyqualified and so always starting with
/. A reserved key that cannot collide is what letsa reader tell a value the reporter asserted from one the fault manager sampled.
snapshot_capture.cpprebuilds the frame from thetopics it sampled and writes it whole, so evidence reported before a confirm was silently
erased by it. Which of the two writes lands last is a matter of when the fault confirmed,
not of intent, so the capture path now carries the key across. There is a test for exactly
this ordering.
retention was on), because a frame of sampled topic values duplicates the snapshots beside
it. Evidence is not in them at all, so a frame carrying evidence is now always served.
Bounded per fault code: 32 entries, 512 characters per value. Over-long values are dropped
whole rather than truncated - half a number read back later is worse than a logged absence -
and drops are reported with a throttled warning. A key already stored can still be updated,
so a steady reporter at the bound can refresh its own numbers instead of freezing at whatever
the first report said.
STALE is a decision, not a fact (#668 part 2)
map_to_severitysent STALE to CRITICAL unconditionally, and CRITICAL bypasses debounce(
fault_storage.cpp:148), so a GPS that goes STALE in every tunnel confirmed a CRITICALfault on its first sample - the debounce settings an operator chose never got to run.
Two parameters:
Only STALE is configurable. WARN and ERROR are facts about the status; STALE is the only
level a node reaches by design, and it is the only one that needed an operator's judgement.
Prefix matching because diagnostic names are conventionally
<component>: <check>, so acomponent prefix covers every check it publishes without listing them. Longest match wins,
the same rule the fault manager's entity thresholds already use - one matching rule for an
operator to learn, not two.
A typo is ignored, not defaulted. An unparseable override is reported and skipped,
leaving
stale_severityin force. Applying CRITICAL to a misspelling would hand back exactlythe immediate-confirm behaviour the operator was configuring their way out of, and it would
look like the configuration had been accepted.
One pre-existing doc bug fixed
docs/config/diagnostic-bridge.rstdocumented STALE asSEVERITY_ERROR (2). The bridge hasalways sent CRITICAL. The table now says what the code does, with a warning explaining why
the difference matters.
Tests
test_reported_evidence.cpp- 14 cases: the merge, provenance separation, later reportsupdating keys, both bounds and their exact boundary, an unreadable stored frame,
"everything dropped writes no key", the capture-path preservation in both directions, and
what the read path gates on.
test_diagnostic_bridge.cpp- 10 new cases: the default, the global parameter, overridebeating default, prefix and longest-prefix matching, a non-CRITICAL default combined with a
CRITICAL override, both unparseable paths, case-insensitivity, and the parser itself.
test_reported_evidence.test.py- the node end to end: the issue's own example read backthrough
GetFault, evidence stored while the fault is still PREFAILED, a later reportupdating, a report without evidence not erasing it, and a code that never carried any
still getting no frame at all.
REQ_INTEROP_109andREQ_INTEROP_110added, both traced by@verifies.Existing bridge tests were updated for the signature change:
map_to_severityis a methodnow that STALE resolves through configuration.
Verified locally
ROS 2 Jazzy / Ubuntu 24.04.
colcon testoverros2_medkit_diagnostic_bridge,ros2_medkit_fault_managerandros2_medkit_fault_reporter: 54 tests, 0 failures,every linter included. Whole workspace built (
--packages-skip ros2_medkit_opcua) becauseros2_medkit_msgschanged.Numbering starts at
REQ_INTEROP_109to leave 107/108 to #690, which is open against thesame tree.
Closes #668