Declare the rosbag storage plugins, default to mcap, and serialize the plugin loader - #610
Merged
Merged
Conversation
bburda
force-pushed
the
fix/602-humble-binarydeb-and-blackbox-storage
branch
from
August 12, 2026 19:16
6d8edfa to
46c4869
Compare
Base automatically changed from
fix/602-humble-binarydeb-and-blackbox-storage
to
main
August 13, 2026 10:11
…lare the storage plugins The package declared no rosbag2 storage plugin at all. `rosbag2_storage` is the plugin interface and `libsqlite3-dev` is the C library the fault database uses; neither provides a backend. An installed fault manager therefore found no storage, logged a warning and disabled black-box capture, which the README leads with. The Humble binarydeb job is the only place these packages meet a minimal dependency closure, and it is where this surfaced. The default format returns to mcap, which is what rosbag2 has defaulted to since Iron and what Foxglove and Lichtblick open without conversion. This restores the default the project chose at 0.4.0; it was moved to sqlite3 in a later fix on the stated premise that sqlite3 always ships, and that premise was false for the same reason as above. Both plugins are now declared as exec and test dependencies, and the runtime stage of the image installs them, so the published image can write the format it advertises. Storage resolution is symmetric. Whichever format is configured, if its plugin fails to load the other one is probed and used, with a warning naming the missing package; capture disables only when neither loads, naming both packages. BREAKING CHANGE: an installation that upgrades without setting `snapshots.rosbag.format` starts producing `.mcap` bags instead of `.db3`, and the bulk-data download filename changes with it. Anything reading the old extension directly has to follow.
…s-wide `rosbag2_cpp::Writer` loads its storage plugin inside `open()`, and `~Writer` can unload it. The racing state is class_loader's process-global library registry, so the failure is a double unload of one shared handle: the winner calls `dlclose` and zeroes the `rcutils_shared_library_t`, the loser's `dlclose` returns `shared object not open` and it then calls the now-null `allocator.deallocate`. The faulting frame is `rcutils_unload_shared_library`. Two threads doing nothing but open and close a writer on one storage id crashed in 20 of 20 runs, on mcap and on sqlite3 alike, so this is not specific to either backend. It reproduces on the merge base as well. A process-wide mutex now serializes construction, `open()` and destruction, including inside the storage probe, which took no lock at all. A control with `open()` outside that lock still leaked failures, which is why it is inside. `writer_mutex_` keeps guarding writer data access and is no longer held across an open or a close, so a close cannot stall the post-roll callbacks. Every destruction site moves the writer out under `writer_mutex_` and destroys it under the plugin mutex; the reverse order closes a deadlock cycle with `open_bag_writer`. The mutex is deliberately leaked, because one destroyed at static-destruction time while another thread is closing a writer reopens the window. Measured: 140 of 140 runs clean over 112000 operations per backend, against a baseline of 0 of 20. Close under the lock costs about 0.37 ms for an empty bag and about 1.1 ms for a 256 MB one, the largest a single flush can produce. The earlier reasoning that moved the close out of the lock was right about the I/O cost and wrong about where it was safe to pay it, so both halves are kept.
The warning that fires when neither storage backend loads named the ROS package names, rosbag2_storage_mcap and rosbag2_storage_default_plugins. Those are not what an operator installs: the installable names carry the distro prefix and use hyphens, so the reader is left to work out the translation at the moment black-box capture has just disabled itself. The fallback warning next to it already printed the installable name, so the two disagreed on what a package name is. Both now go through one helper that returns the installable name, and the disable warning prints both names as a command line that can be pasted. The config reference listed rosbag.format twice with contradictory defaults, sqlite3 in one row and mcap in the other. The mcap row is the correct one; the stale row is removed. The fault manager README called rosbag2_storage_default_plugins "the sqlite3 plugin", which holds on humble but not on jazzy or lyrical, where it is a metapackage that pulls in mcap as well. A test captures the rcutils output around the constructor for all three storage outcomes and requires the installable name in each message.
bburda
force-pushed
the
fix/blackbox-storage-plugins-and-loader-race
branch
from
August 13, 2026 10:35
c2fa0ec to
ee73ba8
Compare
bburda
marked this pull request as ready for review
August 13, 2026 10:35
Contributor
There was a problem hiding this comment.
Pull request overview
This PR fixes rosbag “black-box” capture reliability in ros2_medkit_fault_manager by (1) ensuring storage backend plugins are actually present in installed/test dependency closures, (2) switching the default bag format back to MCAP, and (3) preventing a known process-global rosbag2/class_loader unload race by serializing plugin load/unload paths.
Changes:
- Declare rosbag2 storage backends as runtime + test dependencies, update Docker runtime stage accordingly, and validate the manifest via a new pytest.
- Default rosbag capture format to
mcapagain, with symmetric fallback to the other backend when a configured plugin is unavailable, plus operator-actionable warnings. - Add locking around writer open/close/probe paths to avoid storage plugin loader SIGSEGV; expand unit/integration test coverage and update docs/design notes.
Reviewed changes
Copilot reviewed 19 out of 19 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| src/ros2_medkit_gateway/src/http/handlers/bulkdata_handlers.cpp | Clarifies “historical” sqlite3 default for older bag metadata. |
| src/ros2_medkit_gateway/README.md | Updates gateway docs to reflect mcap default and fallback behavior. |
| src/ros2_medkit_fault_manager/test/test_rosbag_storage_dependency.py | New manifest-level test to enforce plugin dependency declarations (and no conditional deps). |
| src/ros2_medkit_fault_manager/test/test_rosbag_integration.test.py | Improves buffering readiness logic by accounting for post-fault diversion using events. |
| src/ros2_medkit_fault_manager/test/test_rosbag_default_format.test.py | New launch test asserting the on-disk default format is .mcap when unset. |
| src/ros2_medkit_fault_manager/test/test_rosbag_capture.cpp | Expands unit/integration tests for default format, symmetric fallback, warnings, and concurrency race regression. |
| src/ros2_medkit_fault_manager/src/rosbag_capture.cpp | Implements symmetric backend resolution + package-name hints; adds process-wide plugin mutex around writer open/close/probe to avoid unload race. |
| src/ros2_medkit_fault_manager/src/fault_manager_node.cpp | Changes declared parameter default for snapshots.rosbag.format to mcap. |
| src/ros2_medkit_fault_manager/README.md | Documents default mcap, fallback behavior, and plugin dependencies. |
| src/ros2_medkit_fault_manager/package.xml | Adds exec/test dependencies for rosbag2_storage_mcap and rosbag2_storage_default_plugins; adds pytest deps. |
| src/ros2_medkit_fault_manager/include/ros2_medkit_fault_manager/snapshot_capture.hpp | Updates RosbagConfig default format and documentation. |
| src/ros2_medkit_fault_manager/include/ros2_medkit_fault_manager/rosbag_capture.hpp | Documents new lock ordering including plugin_mutex() -> writer_mutex_. |
| src/ros2_medkit_fault_manager/design/index.rst | Adds design rationale and measurements for the plugin loader serialization lock. |
| src/ros2_medkit_fault_manager/config/snapshots.yaml | Updates default format to mcap and describes fallback/dependency expectations. |
| src/ros2_medkit_fault_manager/CMakeLists.txt | Registers new pytest manifest test and new launch test for default format. |
| QUALITY_DECLARATION.md | Adds rosbag storage plugin packages to declared dependency list. |
| docs/tutorials/snapshots.rst | Updates tutorial docs for mcap default and symmetric fallback semantics. |
| docs/config/fault-manager.rst | Updates config docs for new default and clarifies behavior. |
| Dockerfile | Ensures runtime image installs both rosbag storage plugins needed at runtime. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
mfaferek93
reviewed
Aug 13, 2026
ConstructorFallsBackOnUnknownFormat ran against the real probe and accepted either backend, so it could not fail. Delete the normalisation and an unknown format still resolves to sqlite3: its own probe fails, and because the format is not literally "sqlite3" the symmetric fallback picks sqlite3. Both paths end on the same value, so the assertion held either way and the documented rule - an unknown format string falls back to sqlite3 - had no coverage. The case now injects a probe that reports every backend usable, which leaves normalisation as the only thing that can change the format, and asserts the value exactly. Deleting the normalisation fails it. The reason the assertion had been relaxed no longer applies: it allowed for a host carrying mcap but not sqlite3, and both plugins are test dependencies of this package now, as the parametrised bag tests in the same binary already require. Also declare rcutils/logging.h, which the log capture uses directly and was reaching only through rclcpp.hpp, and drop an unused launch_testing.markers import. launch_testing exports asserts from its own __init__, so nothing in that file depended on the removed import.
mfaferek93
approved these changes
Aug 13, 2026
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.
Pull Request
Summary
Two defects, both surfaced by the same build farm job once its launch-test noise was out of the way.
1. Black-box rosbag capture had no storage plugin in its install closure.
rosbag2_storageis the plugin interface andlibsqlite3-devis the C library the fault database uses. Neither provides a storage backend. An installed fault manager therefore found no backend, logged a warning and disabled black-box capture, which is a feature the README leads with. The Humble binarydeb job is the only place these packages meet a minimal dependency closure, which is why this had been invisible.The default format returns to
mcap. That is what rosbag2 has defaulted to since Iron and what Foxglove and Lichtblick open without a conversion step. This restores the default this project chose at 0.4.0; it was moved tosqlite3in a later fix on the stated premise that sqlite3 always ships, and that premise was false for the same reason as above. Both plugins are now exec and test dependencies, and the runtime stage of the image installs them, so the published image can write the format it advertises.Storage resolution is symmetric now: whichever format is configured, if its plugin fails to load the other is probed and used, naming the missing package. Capture disables only when neither loads. Before, a configured
sqlite3that failed to load disabled capture even when mcap was usable, and blamed a broken base install.Both warnings name the package an operator installs (
ros-$ROS_DISTRO-rosbag2-storage-mcap), not the ROS package name, which differs by the distro prefix and by underscore-versus-hyphen.2. A SIGSEGV in the storage plugin loader.
rosbag2_cpp::Writerloads its storage plugin insideopen(), and~Writercan unload it. The racing state isclass_loader's process-global library registry, so the crash is a double unload of one shared handle: the winner callsdlcloseand zeroes thercutils_shared_library_t, the loser'sdlclosereturnsshared object not openand it then calls the now-nullallocator.deallocate. The faulting frame isrcutils_unload_shared_library.This is a bug in
class_loader, not in this package; what changes here is that this package stops entering it. It reproduces on the merge base, and on both backends, so it is neither new nor caused by the default change above. It is worth reporting upstream, and a minimal reproduction exists that does not involve this code.Issue
Closes #619
Type
Testing
The default format is proven on disk, not from a reported field. A launch test starts a fault manager with no
snapshots.rosbag.formatparameter, triggers a fault, and requires a.mcapfile in the resulting bag directory. A unit case pins the struct default separately, and a manifest test pins both plugin declarations, including that neither carries aconditionattribute that would silently drop them on one distro.The loader race, measured. A two-thread program that does nothing but open and close a writer on one storage id: 0 of 20 runs clean at baseline, on mcap and on sqlite3 alike. With the plugin mutex, 140 of 140 runs clean over 112000 operations per backend. A control with
open()outside the lock still leaked failures, which is why it is inside. A regression test crosses two captures' plugin traffic in one process; a per-thread lock makes it crash, and a reversed lock order makes it hang, which ctest reports as a named timeout.The install hints are checked against the log, not against the source. A test captures the rcutils output around the constructor for all three storage outcomes and requires the apt package name in each message. Two mutations were run against it: putting the ROS package names back in the disable warning, and dropping the package name from the fallback hint. Both fail the test.
Cost. The plugin mutex is held across a close: about 0.37 ms for an empty bag and about 1.1 ms for a 256 MB one, which is the largest a single flush can produce.
writer_mutex_is no longer held across an open or a close, so a close cannot stall the post-roll callbacks.Suites.
ros2_medkit_fault_managerand the full lint set clean, locally, on this branch rebased on currentmain.What this cannot prove. The dependency half is proven by the next Humble binarydeb run, not by a green suite on machines that have both plugins installed anyway.
Note for reviewers
Breaking: an installation that upgrades without setting
snapshots.rosbag.formatstarts producing.mcapbags instead of.db3, and the bulk-data download filename changes with it. Anything reading the old extension directly has to follow.One test's measurement was corrected alongside:
test_rosbag_integration's helper counted messages arriving at the test node's own subscription and assumed the fault manager had buffered a comparable amount, but the fault manager buffers nothing while a post-fault recording is in flight. The assertion was right and the instrument feeding it was not.Checklist