Skip to content

Commit d85d80c

Browse files
facontidavideclaude
andcommitted
refactor(FileLogger2): proper thread sync without TSAN suppressions
Changes: - Replace spin-wait with condition_variable for thread startup sync - Remove early callback dropping - queue is always safe to access - Remove TSAN suppressions - proper sync approach doesn't need them - Delete move ops in base class for consistency with derived class - Simplify base constructor - no mutex hack needed The key insight: the queue is protected by queue_mutex and initialized before StatusChangeLogger subscribes to callbacks (base-from-member idiom), so callbacks can safely enqueue at any time. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 195136d commit d85d80c

3 files changed

Lines changed: 14 additions & 30 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 & 18 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,13 +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.
118-
if(!_p->ready.load(std::memory_order_acquire))
119-
{
120-
return;
121-
}
122-
115+
// The queue is always safe to access - it's protected by queue_mutex and
116+
// initialized before StatusChangeLogger subscribes to callbacks.
123117
Transition trans{};
124118
trans.timestamp_usec = uint64_t(ToUsec(timestamp - _p->first_timestamp));
125119
trans.node_uid = node.UID();
@@ -142,13 +136,13 @@ void FileLogger2::writerLoop()
142136
// local buffer in this thread
143137
std::deque<Transition> transitions;
144138

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.
139+
// Signal that the writer thread is ready.
140+
// The mutex + notify establishes happens-before with the constructor's wait.
148141
{
149142
std::scoped_lock lock(_p->queue_mutex);
150-
_p->ready.store(true, std::memory_order_release);
143+
_p->ready.store(true, std::memory_order_relaxed);
151144
}
145+
_p->queue_cv.notify_all();
152146

153147
while(_p->loop)
154148
{

tests/tsan_suppressions.txt

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,3 @@
22

33
# ZeroMQ false positives
44
race:zmq::epoll_t::add_fd
5-
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
14-
race:BT::FileLogger2::writerLoop

0 commit comments

Comments
 (0)