Skip to content

Commit c6393f7

Browse files
authored
Fixed possible infinite loop in Groot2Publisher when destructor is ca… (#1058)
* Fixed possible infinite loop in Groot2Publisher when destructor is called * Made heartbeat read/writes an atomic operation in Groot2Publisher * Cleanup gtest_groot2_publisher.cpp * Added tsan_suppressions file to suppress TSAN warnings from zeromq * Replaced atomics with mutex for type safety with chrono * Fixed suppressions path
1 parent e0e6def commit c6393f7

File tree

5 files changed

+71
-8
lines changed

5 files changed

+71
-8
lines changed

.github/workflows/cmake_ubuntu_sanitizers.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ jobs:
6363
GTEST_COLOR: "On"
6464
ASAN_OPTIONS: "color=always"
6565
UBSAN_OPTIONS: "halt_on_error=1:print_stacktrace=1:color=always"
66-
TSAN_OPTIONS: "color=always"
66+
TSAN_OPTIONS: "suppressions=../../../tests/tsan_suppressions.txt:color=always"
6767
# There is a known issue with TSAN on recent kernel versions. Without the vm.mmap_rnd_bits=28
6868
# workaround all binaries with TSan enabled crash with "FATAL: ThreadSanitizer: unexpected memory mapping"
6969
run: sudo sysctl vm.mmap_rnd_bits=28 && ctest --test-dir build/${{env.BUILD_TYPE}} --output-on-failure

src/loggers/groot2_publisher.cpp

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ struct Groot2Publisher::PImpl
8181

8282
std::string tree_xml;
8383

84-
std::atomic_bool active_server = false;
84+
std::atomic_bool active_server = true;
8585
std::thread server_thread;
8686

8787
std::mutex status_mutex;
@@ -98,7 +98,8 @@ struct Groot2Publisher::PImpl
9898
std::unordered_map<uint16_t, Monitor::Hook::Ptr> pre_hooks;
9999
std::unordered_map<uint16_t, Monitor::Hook::Ptr> post_hooks;
100100

101-
std::chrono::system_clock::time_point last_heartbeat;
101+
std::mutex last_heartbeat_mutex;
102+
std::chrono::steady_clock::time_point last_heartbeat;
102103
std::chrono::milliseconds max_heartbeat_delay = std::chrono::milliseconds(5000);
103104

104105
std::atomic_bool recording = false;
@@ -241,7 +242,6 @@ void Groot2Publisher::serverLoop()
241242
{
242243
auto const serialized_uuid = CreateRandomUUID();
243244

244-
_p->active_server = true;
245245
auto& socket = _p->server;
246246

247247
auto sendErrorReply = [&socket](const std::string& msg) {
@@ -252,7 +252,10 @@ void Groot2Publisher::serverLoop()
252252
};
253253

254254
// initialize _p->last_heartbeat
255-
_p->last_heartbeat = std::chrono::system_clock::now();
255+
{
256+
const std::unique_lock lk(_p->last_heartbeat_mutex);
257+
_p->last_heartbeat = std::chrono::steady_clock::now();
258+
}
256259

257260
while(_p->active_server)
258261
{
@@ -261,8 +264,12 @@ void Groot2Publisher::serverLoop()
261264
{
262265
continue;
263266
}
267+
264268
// this heartbeat will help establishing if Groot is connected or not
265-
_p->last_heartbeat = std::chrono::system_clock::now();
269+
{
270+
const std::unique_lock lk(_p->last_heartbeat_mutex);
271+
_p->last_heartbeat = std::chrono::steady_clock::now();
272+
}
266273

267274
std::string const request_str = requestMsg[0].to_string();
268275
if(request_str.size() != Monitor::RequestHeader::size())
@@ -512,10 +519,13 @@ void Groot2Publisher::heartbeatLoop()
512519
{
513520
std::this_thread::sleep_for(std::chrono::milliseconds(10));
514521

515-
auto now = std::chrono::system_clock::now();
522+
auto now = std::chrono::steady_clock::now();
516523
const bool prev_heartbeat = has_heartbeat;
517524

518-
has_heartbeat = (now - _p->last_heartbeat < _p->max_heartbeat_delay);
525+
{
526+
const std::unique_lock lk(_p->last_heartbeat_mutex);
527+
has_heartbeat = (now - _p->last_heartbeat < _p->max_heartbeat_delay);
528+
}
519529

520530
// if we loose or gain heartbeat, disable/enable all breakpoints
521531
if(has_heartbeat != prev_heartbeat)

tests/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ set(BT_TESTS
1212
gtest_enums.cpp
1313
gtest_factory.cpp
1414
gtest_fallback.cpp
15+
gtest_groot2_publisher.cpp
1516
gtest_parallel.cpp
1617
gtest_preconditions.cpp
1718
gtest_ports.cpp

tests/gtest_groot2_publisher.cpp

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
#include <chrono>
2+
#include <cstdlib>
3+
#include <future>
4+
5+
#include <behaviortree_cpp/loggers/groot2_protocol.h>
6+
#include <behaviortree_cpp/loggers/groot2_publisher.h>
7+
#include <gtest/gtest.h>
8+
9+
using namespace std::chrono_literals;
10+
11+
namespace
12+
{
13+
static const char* xml_text = R"(
14+
<root BTCPP_format="4">
15+
<BehaviorTree ID="MainTree">
16+
<ThrowRuntimeError/>
17+
</BehaviorTree>
18+
</root>
19+
)";
20+
21+
void throwRuntimeError()
22+
{
23+
BT::BehaviorTreeFactory factory;
24+
factory.registerSimpleAction("ThrowRuntimeError", [](BT::TreeNode&) -> BT::NodeStatus {
25+
throw BT::RuntimeError("Oops!");
26+
});
27+
28+
auto tree = factory.createTreeFromText(xml_text);
29+
BT::Groot2Publisher publisher(tree);
30+
EXPECT_THROW(tree.tickExactlyOnce(), BT::RuntimeError);
31+
}
32+
} // namespace
33+
34+
TEST(Groot2PublisherTest, EnsureNoInfiniteLoopOnThrow)
35+
{
36+
EXPECT_EXIT(
37+
{
38+
auto fut = std::async(std::launch::async, throwRuntimeError);
39+
auto status = fut.wait_for(1s); // shouldn't run for more than 1 second
40+
if(status != std::future_status::ready)
41+
{
42+
std::cerr << "Test took too long. Possible infinite loop.\n";
43+
std::exit(EXIT_FAILURE);
44+
}
45+
std::exit(EXIT_SUCCESS);
46+
},
47+
::testing::ExitedWithCode(EXIT_SUCCESS), ".*");
48+
}

tests/tsan_suppressions.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# ThreadSanitizer suppressions file for behaviortree_cpp_test
2+
3+
# ZeroMQ false positives
4+
race:zmq::epoll_t::add_fd

0 commit comments

Comments
 (0)