Skip to content

Commit 8843447

Browse files
haltTree() from another thread now stops tickWhileRunning() (fixes #686) (#1201)
tickRoot() kept ticking after a concurrent haltTree(), restarting the tree that had just been halted. haltTree() resets the root to IDLE, so a RUNNING tick result with an IDLE root identifies the halt without adding any state to Tree (whose inline defaulted move constructor makes new members an ABI break). haltTree() also emits the wake-up signal to interrupt the sleep. Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
1 parent 661a329 commit 8843447

2 files changed

Lines changed: 86 additions & 1 deletion

File tree

‎src/bt_factory.cpp‎

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -679,6 +679,12 @@ void Tree::haltTree()
679679
BT::applyRecursiveVisitor(rootNode(), visitor);
680680

681681
rootNode()->resetStatus();
682+
683+
// interrupt the sleep of tickWhileRunning(), if called from another thread
684+
if(wake_up_)
685+
{
686+
wake_up_->emitSignal();
687+
}
682688
}
683689

684690
TreeNode* Tree::rootNode() const
@@ -767,14 +773,21 @@ NodeStatus Tree::tickRoot(TickOption opt, std::chrono::milliseconds sleep_time)
767773
throw RuntimeError("Empty Tree");
768774
}
769775

776+
// haltTree() resets the root to IDLE. If that happens (from another thread)
777+
// while the last tick returned RUNNING, we must not tick again, because that
778+
// would restart the tree (issue #686).
779+
const auto halted = [&]() {
780+
return status == NodeStatus::RUNNING && rootNode()->status() == NodeStatus::IDLE;
781+
};
782+
770783
while(status == NodeStatus::IDLE ||
771784
(opt == TickOption::WHILE_RUNNING && status == NodeStatus::RUNNING))
772785
{
773786
status = rootNode()->executeTick();
774787

775788
// Inner loop. The previous tick might have triggered the wake-up
776789
// in this case, unless TickOption::EXACTLY_ONCE, we tick again
777-
while(opt != TickOption::EXACTLY_ONCE && status == NodeStatus::RUNNING &&
790+
while(opt != TickOption::EXACTLY_ONCE && !halted() && status == NodeStatus::RUNNING &&
778791
wake_up_->waitFor(std::chrono::milliseconds(0)))
779792
{
780793
status = rootNode()->executeTick();
@@ -788,6 +801,10 @@ NodeStatus Tree::tickRoot(TickOption opt, std::chrono::milliseconds sleep_time)
788801
{
789802
sleep(std::chrono::milliseconds(sleep_time));
790803
}
804+
if(halted())
805+
{
806+
return NodeStatus::IDLE;
807+
}
791808
}
792809

793810
return status;

‎tests/gtest_wakeup.cpp‎

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
#include "behaviortree_cpp/bt_factory.h"
22

3+
#include <atomic>
4+
#include <future>
5+
#include <thread>
6+
37
#include <gtest/gtest.h>
48

59
using namespace BT;
@@ -54,3 +58,67 @@ TEST(WakeUp, BasicTest)
5458
// the wake-up should fire well under that threshold.
5559
ASSERT_LT(dT, 100);
5660
}
61+
62+
// Issue #686: haltTree() called from another thread must stop
63+
// tickWhileRunning(), instead of letting it tick (and restart) the tree again.
64+
TEST(WakeUp, HaltTreeStopsTickWhileRunning_Issue686)
65+
{
66+
static std::atomic_int starts;
67+
static std::atomic_bool give_up;
68+
starts = 0;
69+
give_up = false;
70+
71+
class RunForever : public StatefulActionNode
72+
{
73+
public:
74+
RunForever(const std::string& name, const NodeConfig& config)
75+
: StatefulActionNode(name, config)
76+
{}
77+
static PortsList providedPorts()
78+
{
79+
return {};
80+
}
81+
NodeStatus onStart() override
82+
{
83+
starts++;
84+
return NodeStatus::RUNNING;
85+
}
86+
NodeStatus onRunning() override
87+
{
88+
// escape hatch, so that a failing test does not hang forever
89+
return give_up ? NodeStatus::SUCCESS : NodeStatus::RUNNING;
90+
}
91+
void onHalted() override
92+
{}
93+
};
94+
95+
BehaviorTreeFactory factory;
96+
factory.registerNodeType<RunForever>("RunForever");
97+
98+
auto tree = factory.createTreeFromText(R"(
99+
<root BTCPP_format="4">
100+
<BehaviorTree ID="MainTree">
101+
<RunForever/>
102+
</BehaviorTree>
103+
</root>)");
104+
105+
auto result = std::async(std::launch::async, [&] {
106+
return tree.tickWhileRunning(std::chrono::milliseconds(10000));
107+
});
108+
109+
while(starts == 0)
110+
{
111+
std::this_thread::sleep_for(std::chrono::milliseconds(1));
112+
}
113+
tree.haltTree();
114+
115+
// haltTree must also interrupt the 10 seconds sleep
116+
const bool returned =
117+
result.wait_for(std::chrono::seconds(2)) == std::future_status::ready;
118+
give_up = true;
119+
tree.emitWakeUpSignal();
120+
121+
ASSERT_TRUE(returned);
122+
ASSERT_EQ(result.get(), NodeStatus::IDLE);
123+
ASSERT_EQ(starts, 1);
124+
}

0 commit comments

Comments
 (0)