Skip to content

Commit 60eab2f

Browse files
committed
fix(fault_manager): serialize rosbag leg and bound cooldown map under storms
The bounded capture pool may run pool_size jobs concurrently. RosbagCapture is single-writer (one ring buffer, one active writer, one recording state machine), so two distinct fault codes confirming at once could race on its shared state and corrupt/truncate a bag. Serialize just the rosbag leg under a mutex in the pool job; snapshot capture is independent and stays parallel. Also bound last_capture_times_: a storm of distinct fault codes previously left one permanent entry per code (cooldown on). Sweep entries older than the cooldown while holding last_capture_mutex_ - they can never gate a capture again. Correct the capture-block comment to describe the real invariant (single-threaded executor serializes confirmations; the mutex only guards the cooldown map).
1 parent 192a38e commit 60eab2f

1 file changed

Lines changed: 25 additions & 6 deletions

File tree

src/ros2_medkit_fault_manager/src/fault_manager_node.cpp

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -248,13 +248,19 @@ FaultManagerNode::FaultManagerNode(const rclcpp::NodeOptions & options) : Node("
248248
if (snapshot_capture_ || rosbag_capture_) {
249249
auto snap = snapshot_capture_;
250250
auto bag = rosbag_capture_;
251+
// RosbagCapture is single-writer (one shared ring buffer + one active writer +
252+
// one recording state machine), so on_fault_confirmed() is not safe for the
253+
// pool_size concurrent calls the pool contract allows. Serialize just the rosbag
254+
// leg under this mutex; snapshot capture is independent and stays parallel.
255+
auto rosbag_mutex = std::make_shared<std::mutex>();
251256
capture_pool_ = std::make_unique<CaptureThreadPool>(
252257
static_cast<std::size_t>(capture_pool_size_), static_cast<std::size_t>(capture_queue_depth_),
253-
capture_queue_full_policy_, get_logger(), [snap, bag](const std::string & fault_code) {
258+
capture_queue_full_policy_, get_logger(), [snap, bag, rosbag_mutex](const std::string & fault_code) {
254259
if (snap) {
255260
snap->capture(fault_code);
256261
}
257262
if (bag) {
263+
std::lock_guard<std::mutex> bag_lock(*rosbag_mutex);
258264
bag->on_fault_confirmed(fault_code);
259265
}
260266
});
@@ -443,9 +449,11 @@ void FaultManagerNode::handle_report_fault(
443449
}
444450
// Note: PREFAILED/PREPASSED status changes don't emit events (debounce in progress)
445451

446-
// Capture snapshots/rosbag when a fault confirms. Bounded pool (issue #441):
447-
// the cooldown check, enqueue, and cooldown update are atomic under
448-
// last_capture_mutex_ to prevent a concurrent same-fault double-enqueue.
452+
// Capture snapshots/rosbag when a fault confirms via the bounded pool (issue #441).
453+
// handle_report_fault runs on the single-threaded executor, so confirmations are
454+
// already serialized; last_capture_mutex_ only guards last_capture_times_ itself
455+
// (the cooldown check, the update below, and the expired-entry sweep). It is taken
456+
// solely when the cooldown is enabled, since the map is otherwise never touched.
449457
if (just_confirmed && capture_pool_) {
450458
const std::string fault_code = request->fault_code;
451459
const bool cooldown_enabled = snapshot_recapture_cooldown_sec_ > 0.0;
@@ -456,10 +464,21 @@ void FaultManagerNode::handle_report_fault(
456464

457465
bool on_cooldown = false;
458466
if (cooldown_enabled) {
467+
const auto cooldown = std::chrono::duration<double>(snapshot_recapture_cooldown_sec_);
468+
const auto sweep_now = std::chrono::steady_clock::now();
469+
// Bound the map (issue #441): a storm of distinct fault codes would otherwise
470+
// leave one permanent entry per code. Entries older than the cooldown can never
471+
// gate a capture again, so drop them while we hold the lock.
472+
for (auto it = last_capture_times_.begin(); it != last_capture_times_.end();) {
473+
if (sweep_now - it->second >= cooldown) {
474+
it = last_capture_times_.erase(it);
475+
} else {
476+
++it;
477+
}
478+
}
459479
auto it = last_capture_times_.find(fault_code);
460480
if (it != last_capture_times_.end()) {
461-
auto elapsed = std::chrono::steady_clock::now() - it->second;
462-
on_cooldown = elapsed < std::chrono::duration<double>(snapshot_recapture_cooldown_sec_);
481+
on_cooldown = (sweep_now - it->second) < cooldown;
463482
}
464483
}
465484

0 commit comments

Comments
 (0)