Keep more than one rosbag per fault, and stop losing the evidence around it - #623
Open
mfaferek93 wants to merge 8 commits into
Open
Keep more than one rosbag per fault, and stop losing the evidence around it#623mfaferek93 wants to merge 8 commits into
mfaferek93 wants to merge 8 commits into
Conversation
Split the three grains the schema conflated: a row is a (fault, recording) LINK, bytes belong to file_path, and a bag dies with its last row. - recording_id becomes a stored, indexed column, backfilled in C++ so the basename rule has one implementation - rebuild rosbag_files to drop the column-level UNIQUE(fault_code) that SQLite cannot ALTER away, replaced by a named UNIQUE INDEX on (fault_code, file_path) - in-memory backend moves from a map keyed by fault code to a flat row vector with a seq counter, because a burst stamps one created_at_ns on every row - get_rosbag_file now orders deterministically instead of taking any row - per-fault retention cap, keep-newest, enforced atomically with the insert Refs #620
A fault can now hold several black-box recordings and every one of them is separately addressable. Bulk-data emits one descriptor per recording instead of per fault, and a URL carrying a bare fault code still serves that fault's newest recording.
A rosbag entry is a pointer to bytes; one with no resolvable recording id is a download button that cannot work. Every such snapshot this endpoint has emitted carried a bulk_data_uri and consumers dereference it unguarded, so dropping the entry keeps that invariant instead of inventing a shape for them to crash on. Adds the HTTP end-to-end suite for a fault holding several recordings.
Contributor
There was a problem hiding this comment.
Pull request overview
This PR fixes the “one rosbag per fault_code” limitation by introducing a recording-centric identity (recording_id) and a retention policy (snapshots.rosbag.max_bags_per_fault, default 1 to preserve prior behavior). It updates the fault manager’s storage schema/migration and the gateway’s bulk-data surface so multiple recordings per fault are addressable and downloadable while keeping legacy fault-code URLs working via a compatibility fallback.
Changes:
- Fault manager: change rosbag storage grain to one row per (fault_code, file_path) link, persist/index
recording_id, add per-fault retention (keep-newest), and migrate legacy SQLite schemas automatically/idempotently. - Gateway: address rosbag bulk-data by recording id, fold list output into one descriptor per recording, and authorize downloads against the union of faults attached to that recording (with legacy fault-code URL fallback).
- Messages/docs/tests: extend
GetRosbag/ListRosbags/Snapshotsemantics, update documentation, and add unit + integration coverage for history/compatibility/migration.
Reviewed changes
Copilot reviewed 35 out of 35 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| src/ros2_medkit_msgs/srv/ListRosbags.srv | Adds recording_ids[] parallel to fault_codes[] for per-link rows. |
| src/ros2_medkit_msgs/srv/GetRosbag.srv | Adds request recording_id (preferred) and response recording_id + fault_codes[]. |
| src/ros2_medkit_msgs/msg/Snapshot.msg | Clarifies bulk_data_id now carries rosbag recording id (not fault code). |
| src/ros2_medkit_msgs/CHANGELOG.rst | Documents service/message surface changes and coordinated deployment requirement. |
| src/ros2_medkit_integration_tests/test/features/test_rosbag_history_download.test.py | New E2E HTTP test covering multi-recording history + legacy fault-code URL behavior. |
| src/ros2_medkit_integration_tests/test/features/test_rosbag_boundary_download.test.py | Updates descriptor lookup and download expectations to recording-id model. |
| src/ros2_medkit_integration_tests/test/features/test_external_app_fault_rollup.test.py | Updates rollup/bulk-data URI assertions for recording ids + legacy URL parity. |
| src/ros2_medkit_integration_tests/test/features/test_bulk_data_api.test.py | Updates descriptor schema expectations (fault_codes[], recording_id, id equality). |
| src/ros2_medkit_integration_tests/CMakeLists.txt | Extends timeout for the new longer-running history download test. |
| src/ros2_medkit_gateway/test/test_bulkdata_handlers.cpp | Adds unit tests for descriptor folding + attached-fault-code download authorization. |
| src/ros2_medkit_gateway/src/ros2/transports/ros2_fault_service_transport.cpp | Populates new GetRosbag/ListRosbags fields and tightens parallel-array validation. |
| src/ros2_medkit_gateway/src/ros2/conversions/fault_msg_conversions.cpp | Renames rosbag snapshot JSON field to bulk_data_id (recording id). |
| src/ros2_medkit_gateway/src/http/handlers/sse_fault_handler.cpp | Updates SSE comment to reflect legacy fault-code URL now being compatibility behavior. |
| src/ros2_medkit_gateway/src/http/handlers/fault_handlers.cpp | Builds bulk-data URIs from bulk_data_id (recording id) and omits unaddressable entries. |
| src/ros2_medkit_gateway/src/http/handlers/bulkdata_handlers.cpp | Implements descriptor folding (per recording), recording-id downloads, and union authorization. |
| src/ros2_medkit_gateway/README.md | Updates SSE consumer guidance re: newest recording vs listing for older recordings. |
| src/ros2_medkit_gateway/include/ros2_medkit_gateway/core/http/handlers/bulkdata_handlers.hpp | Documents/declares recording id helpers and folding utilities. |
| src/ros2_medkit_gateway/CHANGELOG.rst | Documents the recording-id bulk-data API behavior and compatibility fallback. |
| src/ros2_medkit_fault_manager/test/test_sqlite_storage.cpp | Adds migration + retention + link/recording deletion unit coverage for SQLite backend. |
| src/ros2_medkit_fault_manager/test/test_rosbag_storage_parity.cpp | New cross-backend parity suite for retention/link/ordering behavior. |
| src/ros2_medkit_fault_manager/test/test_rosbag_history.test.py | New launch test driving confirm/clear/reconfirm history against SQLite backend services. |
| src/ros2_medkit_fault_manager/test/test_rosbag_capture.cpp | Updates quota sweep failing-storage hook to new delete-by-recording API. |
| src/ros2_medkit_fault_manager/src/sqlite_fault_storage.cpp | Implements schema changes, migration, per-fault cap trimming, and recording-based deletion. |
| src/ros2_medkit_fault_manager/src/rosbag_capture.cpp | Stores recording_id and evicts over quota by recording (not by fault code). |
| src/ros2_medkit_fault_manager/src/fault_storage.cpp | Adds shared rosbag_recording_id() utility and rewrites in-memory backend to link-grain. |
| src/ros2_medkit_fault_manager/src/fault_manager_node.cpp | Adds recording-id validation and recording-first GetRosbag lookup with fallback-to-fault-code. |
| src/ros2_medkit_fault_manager/include/ros2_medkit_fault_manager/sqlite_fault_storage.hpp | Exposes new rosbag APIs (cap, list, drop link, delete recording, lookup by recording). |
| src/ros2_medkit_fault_manager/include/ros2_medkit_fault_manager/snapshot_capture.hpp | Adds config field max_bags_per_fault. |
| src/ros2_medkit_fault_manager/include/ros2_medkit_fault_manager/fault_storage.hpp | Defines recording-id semantics and new storage APIs for link/recording operations. |
| src/ros2_medkit_fault_manager/config/snapshots.yaml | Documents new max_bags_per_fault and recording-id URL semantics. |
| src/ros2_medkit_fault_manager/CMakeLists.txt | Registers new gtest + launch tests and sets timeouts. |
| src/ros2_medkit_fault_manager/CHANGELOG.rst | Documents retention policy, schema grain change, migration approach, and fixed latent defects. |
| docs/tutorials/snapshots.rst | Updates bulk-data URIs/listing examples and explains per-recording listing behavior. |
| docs/config/fault-manager.rst | Documents rosbag.max_bags_per_fault and its fairness semantics + interaction with auto_cleanup. |
| docs/api/rest.rst | Updates REST API examples and clarifies “newest recording” compatibility URL semantics. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
The fault manager has always stamped a recording, but only the freeze-frame branch of the transport forwarded it, so every rosbag reached a client with no capture time and the UIs rendered "N/A". Harmless while a fault held one recording; with several it removes the only field that places an occurrence.
Two ways a confirmation's evidence went missing, both live before #620 and both worse once a fault keeps a history. A capture writes one row per topic and the cap counted rows, rejecting the tail once full: a capture straddling the limit was stored in part, giving a reading with holes that reads like "that topic was not publishing". Captures are now stored as a unit and the cap drops whole ones, oldest first. auto_cleanup deletes every recording of a fault, so the first acknowledgement wiped the trail max_bags_per_fault had just been raised to collect. With a history configured the cap governs; at the default of one nothing changes.
Recordings now survive an acknowledgement once a history is configured, but clear_fault still deleted the value snapshots captured beside them, so a fault was left holding bags whose readings were gone. New snapshots.retain_on_clear, off by default, keeps both or neither.
clang-tidy treats the redeclaration as an error; the using-directive above it already brings the alias in.
Missing <algorithm> for std::sort, copyright year on a new file, and an element-wise fault_codes conversion so the peer-tolerance the function documents is actually true.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #620.
Only the newest rosbag was ever kept per fault code. That was enforced by the schema, not by a retention policy, so no configuration could change it: a fault that kept coming back held on to its freeze frame and threw away every recording but the last.
The fix separates three things the old code treated as one:
(fault_code, file_path), one rowfile_path, named publicly byrecording_idfile_pathRows are per link, bytes are per path, and a bag is unlinked when its last row goes. The reverse relation already worked: a burst writes one row per attached fault, all pointing at one bag, so the sharing logic and the quota needed no changes. This adds the mirror image.
Retention
snapshots.rosbag.max_bags_per_fault, default 1, which is what the code did before: a new recording replaces the old one. Shipping at parity means a regression report is about the plumbing rather than the policy. Past the cap the oldest goes, matching the direction the storage quota already evicts in.It is more a fairness knob than a depth knob.
max_total_storage_mbis the real disk bound, and a big per-fault cap lets one flapping fault eat the budget and push out everyone else's black box. Docs say so.Not consistent with
snapshots.max_per_fault, which rejects new snapshots once full, and deliberately so. Rejecting a new recording would mean a technician standing next to a machine that is faulting right now downloads a bag from three days ago.URLs
/bulk-data/rosbags/{id}takes a recording id now. Our own docs and the SSE payload tell clients to build that URL from a fault code, so an id that names no recording is retried as one and serves that fault's newest bag. Old addresses keep working, hencefeatand notfeat!.Four bugs found on the way
None were reachable while a fault could only hold one recording:
get_rosbag_filehad noORDER BYand would have served an arbitrary recordingdelete_rosbag_fileanddelete_rosbag_filesread only the firstfile_path, so deleting a fault removed every row but left all but one bag on disk, unreachable and still counted against a quota that sums rowsMigration
SQLite cannot drop a column-level
UNIQUEandCREATE TABLE IF NOT EXISTSdoes nothing to an existing database, so this is the documented table rebuild. It runs on first open, is idempotent, and decides whether it is needed by looking at the schema (PRAGMA index_list, origin'u') rather than a version counter - fresh, migrated and already-migrated databases all answer correctly with no bookkeeping.Uniqueness is a
CREATE UNIQUE INDEXon(fault_code, file_path)now. Keyed on the path because that is what identifies the thing on disk: if two bags ever produced the same basename, keying onrecording_idwould silently replace one, while keying on the path degrades the same collision to a mislabeled download.Tests
6871 pass. Each suite below was also run with the fix reverted, to check it goes red for the right reason.
test_rosbag_storage_parity.cppis new: 20 cases, each run against both storage backends from one body. The in-memory backend enforced one-per-fault independently, sostorage_type: memorycould have kept the old behaviour with nothing failing. It caught two real divergences - the delete-path bug above, andINSERT OR REPLACErenumbering a refreshed row so the backends ordered a tie group differently.test_rosbag_history.test.pydrives confirm, acknowledge, confirm against SQLite, plus cap eviction and both ways of addressing a bag. Setmax_bags_per_faultto 1 and its first case fails, which is the reported bug.test_rosbag_history_download.test.pydoes the same over HTTP through the gateway, off a real failure:lidar_sensorchecksmin_range >= max_rangeand reports itself, the test only moves the parameters. It asserts two descriptors with different ids, different downloaded bytes, bothbulk_data_uris resolving, and the fault-code URL still serving the newest.Migration coverage builds a legacy database by hand and checks that no
origin='u'index survives, every row does, ids keep their order,recording_idis backfilled, and opening twice more changes nothing.Three more places evidence went missing
Found while checking the above against a running box, and fixed here because
shipping the retention change without them leaves a fault whose evidence does
not line up.
A recording reached clients with no capture time. The fault manager has
always stamped one, but only the freeze-frame branch of the transport forwarded
it, so every bag arrived without one and the UIs rendered "N/A". Harmless while
a fault held one recording; with several the timestamp is the only field that
places an occurrence.
A capture was stored in part. A confirmation writes one snapshot row per
topic and the cap counted rows, rejecting the tail once full - so a capture
straddling the limit landed with some topics present and the rest silently
absent, which on the wire reads exactly like "that topic was not publishing".
Captures are now written as a unit and the cap drops whole ones, oldest first.
A capture larger than the cap is kept whole rather than torn.
Acknowledging a fault deleted the history it was told to keep.
auto_cleanupdefaults on and deletes every recording of the fault, so raisingmax_bags_per_faultto collect a trail bought nothing: the firstacknowledgement wiped it. With a history configured the cap governs retention;
at the default of one, nothing changes.
clear_faultdeleted the valuesnapshots on the same edge, so the new
snapshots.retain_on_clearkeeps thosetoo - both survive an acknowledgement or neither does, rather than a fault being
left holding bags whose readings are gone.
Each has a test that goes red when its fix is reverted, on both storage backends
where the storage layer is involved.
Consumers
Four needed changes, each with its own PR: ros2_medkit_web_ui#95, platform#270 (fleet_ui), ros2_medkit_foxglove_extension#21, ros2_medkit_mcp#24. Both web UIs were checked in a browser against a gateway holding two real bags. The service type hashes change, so the gateway and fault manager deploy together.