Skip to content

Commit 5057dd7

Browse files
facontidavideclaude
andcommitted
refactor(FileLogger2): proper thread sync with minimal TSAN suppression
Changes: - Replace spin-wait with condition_variable for thread startup sync - Keep ready check in callback to prevent deadlock during construction - Delete move ops in base class for consistency with derived class - Add minimal TSAN suppression for writerLoop only (TSAN limitation) The callback must check the ready flag because the constructor holds queue_mutex while waiting for the writer thread. Without this check, callbacks during tree.tickWhileRunning() would deadlock trying to acquire the same mutex. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 195136d commit 5057dd7

3 files changed

Lines changed: 20 additions & 23 deletions

File tree

include/behaviortree_cpp/loggers/bt_file_logger_v2.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ struct FileLogger2PImplBase
2424
~FileLogger2PImplBase();
2525
FileLogger2PImplBase(const FileLogger2PImplBase&) = delete;
2626
FileLogger2PImplBase& operator=(const FileLogger2PImplBase&) = delete;
27-
FileLogger2PImplBase(FileLogger2PImplBase&&) = default;
28-
FileLogger2PImplBase& operator=(FileLogger2PImplBase&&) = default;
27+
FileLogger2PImplBase(FileLogger2PImplBase&&) = delete;
28+
FileLogger2PImplBase& operator=(FileLogger2PImplBase&&) = delete;
2929
};
3030
} // namespace detail
3131

src/loggers/bt_file_logger_v2.cpp

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,8 @@ struct detail::FileLogger2Private
3838
detail::FileLogger2PImplBase::FileLogger2PImplBase()
3939
: _p(std::make_unique<FileLogger2Private>())
4040
{
41-
// Acquire mutex once to establish happens-before relationship for TSAN.
42-
// This must happen before StatusChangeLogger's constructor subscribes to
43-
// callbacks that may access the mutex from other threads.
44-
std::scoped_lock lock(_p->queue_mutex);
41+
// _p is now ready for use. StatusChangeLogger (constructed next) will
42+
// subscribe to callbacks that may immediately start queuing transitions.
4543
}
4644

4745
// Destructor must be defined where FileLogger2Private is complete
@@ -95,10 +93,11 @@ FileLogger2::FileLogger2(const BT::Tree& tree, std::filesystem::path const& file
9593

9694
_p->writer_thread = std::thread(&FileLogger2::writerLoop, this);
9795

98-
// Wait for writer thread to signal it's ready
99-
while(!_p->ready.load(std::memory_order_acquire))
96+
// Wait for writer thread to signal it's ready using condition variable
10097
{
101-
std::this_thread::yield();
98+
std::unique_lock lock(_p->queue_mutex);
99+
_p->queue_cv.wait(lock,
100+
[this]() { return _p->ready.load(std::memory_order_relaxed); });
102101
}
103102
}
104103

@@ -113,8 +112,8 @@ FileLogger2::~FileLogger2()
113112
void FileLogger2::callback(Duration timestamp, const TreeNode& node,
114113
NodeStatus /*prev_status*/, NodeStatus status)
115114
{
116-
// Ignore callbacks that arrive before the writer thread is ready.
117-
// This can happen during StatusChangeLogger construction.
115+
// Don't queue callbacks until writer thread is ready - the constructor
116+
// holds queue_mutex while waiting, so we'd deadlock.
118117
if(!_p->ready.load(std::memory_order_acquire))
119118
{
120119
return;
@@ -142,13 +141,13 @@ void FileLogger2::writerLoop()
142141
// local buffer in this thread
143142
std::deque<Transition> transitions;
144143

145-
// Signal that the writer thread is ready to accept callbacks.
146-
// IMPORTANT: This must happen AFTER acquiring the mutex for the first time,
147-
// to establish proper synchronization with the callback thread.
144+
// Signal that the writer thread is ready.
145+
// The mutex + notify establishes happens-before with the constructor's wait.
148146
{
149147
std::scoped_lock lock(_p->queue_mutex);
150-
_p->ready.store(true, std::memory_order_release);
148+
_p->ready.store(true, std::memory_order_relaxed);
151149
}
150+
_p->queue_cv.notify_all();
152151

153152
while(_p->loop)
154153
{

tests/tsan_suppressions.txt

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,10 @@
33
# ZeroMQ false positives
44
race:zmq::epoll_t::add_fd
55

6-
# FileLogger2 false positive: TSAN doesn't properly track happens-before
7-
# relationship between mutex creation in make_unique and first cross-thread
8-
# access, even though std::thread::thread provides the synchronization.
9-
# The mutex is properly protected: base class constructor acquires/releases it
10-
# before StatusChangeLogger subscribes to callbacks, and writer thread acquires
11-
# it before setting ready flag.
12-
mutex:BT::FileLogger2
13-
race:BT::FileLogger2::callback
6+
# FileLogger2: TSAN doesn't track happens-before between make_unique and
7+
# first mutex access in spawned thread. The synchronization is correct:
8+
# - Base class creates _p (with mutex) before StatusChangeLogger subscribes
9+
# - Writer thread signals ready only after acquiring mutex
10+
# - Constructor waits on ready before returning
11+
# This is a TSAN limitation, not a real race.
1412
race:BT::FileLogger2::writerLoop

0 commit comments

Comments
 (0)