Skip to content

Commit b6709b6

Browse files
dvadovszfacontidavideclaude
authored
reset a child's status when its halt() throws in ControlNode::haltChild (#1273)
* reset a child's status when its halt() throws in ControlNode::haltChild Previously the child stayed RUNNING, so the next halt of the parent halted it again and threw a second time. When that happened inside ~Tree(), the process terminated. Callers still see the exception. This covers haltChild() and haltChildren(), which calls it. ControlNode::resetChildren() and DecoratorNode::resetChild() have the same pattern and are left for a follow-up. * move the throwing-halt status reset into TreeNode::haltNode() The previous commit reset a child's status in ControlNode::haltChild() when its halt() threw, so the next reset does not halt it again. But built-in nodes halt their children through ControlNode::resetChildren() and DecoratorNode::resetChild()/haltChild(), and Tree::haltTree() halts the root directly: all of them go through TreeNode::haltNode() and kept the bug (the node stays RUNNING, is halted a second time, and ~Tree() -> haltTree() throws again from a destructor, i.e. std::terminate). Put the guard in the single choke point, TreeNode::haltNode(): try { halt(); } catch(...) { resetStatus(); throw; }, and drop the now-redundant try/catch in ControlNode::haltChild(). The exception still propagates. The existing _onHalted post-condition code is unchanged. Add tests with built-in parents (Sequence, RetryUntilSuccessful): haltTree() throws once, every node is IDLE afterwards, a second haltTree() does not throw and onHalted() is not called again. No ABI change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Davide Faconti <davide.faconti@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 12bfc0a commit b6709b6

3 files changed

Lines changed: 134 additions & 1 deletion

File tree

‎src/tree_node.cpp‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,17 @@ NodeStatus TreeNode::executeTick()
158158

159159
void TreeNode::haltNode()
160160
{
161-
halt();
161+
try
162+
{
163+
halt();
164+
}
165+
catch(...)
166+
{
167+
// Don't leave the node RUNNING, or the next reset would halt it again
168+
// (and throw again, possibly from a destructor).
169+
resetStatus();
170+
throw;
171+
}
162172

163173
const auto& parse_executor = _p->post_parsed[size_t(PostCond::ON_HALTED)];
164174
if(parse_executor)

‎tests/CMakeLists.txt‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ set(BT_TESTS
4949
gtest_switch.cpp
5050
gtest_tree.cpp
5151
gtest_try_catch.cpp
52+
gtest_halt_child.cpp
5253
gtest_exception_tracking.cpp
5354
gtest_updates.cpp
5455
gtest_wakeup.cpp

‎tests/gtest_halt_child.cpp‎

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
#include "behaviortree_cpp/bt_factory.h"
2+
#include "behaviortree_cpp/control_node.h"
3+
4+
#include <stdexcept>
5+
6+
#include <gtest/gtest.h>
7+
8+
using BT::NodeStatus;
9+
10+
namespace
11+
{
12+
// RUNNING until halted, and its onHalted() throws. Counts the halts.
13+
class ThrowOnHalt : public BT::StatefulActionNode
14+
{
15+
public:
16+
ThrowOnHalt(const std::string& name, const BT::NodeConfig& config, int* halts)
17+
: StatefulActionNode(name, config), halts_(halts)
18+
{}
19+
static BT::PortsList providedPorts()
20+
{
21+
return {};
22+
}
23+
NodeStatus onStart() override
24+
{
25+
return NodeStatus::RUNNING;
26+
}
27+
NodeStatus onRunning() override
28+
{
29+
return NodeStatus::RUNNING;
30+
}
31+
void onHalted() override
32+
{
33+
(*halts_)++;
34+
throw std::runtime_error("halt failed");
35+
}
36+
37+
private:
38+
int* halts_;
39+
};
40+
41+
// Ticks its only child and halts it on request through haltChild().
42+
class HaltsChild : public BT::ControlNode
43+
{
44+
public:
45+
HaltsChild(const std::string& name, const BT::NodeConfig& config)
46+
: ControlNode(name, config)
47+
{}
48+
static BT::PortsList providedPorts()
49+
{
50+
return {};
51+
}
52+
NodeStatus tick() override
53+
{
54+
setStatus(NodeStatus::RUNNING);
55+
return children_nodes_[0]->executeTick();
56+
}
57+
void halt() override
58+
{
59+
haltChild(0);
60+
resetStatus();
61+
}
62+
};
63+
} // namespace
64+
65+
// A child whose halt() throws must not stay RUNNING, or the next haltChild()
66+
// halts it again and throws a second time.
67+
TEST(ControlNodeHaltChild, ThrowingHaltIsNotRepeated)
68+
{
69+
int halts = 0;
70+
BT::BehaviorTreeFactory factory;
71+
factory.registerNodeType<ThrowOnHalt>("ThrowOnHalt", &halts);
72+
factory.registerNodeType<HaltsChild>("HaltsChild");
73+
auto tree = factory.createTreeFromText(R"(
74+
<root BTCPP_format="4"><BehaviorTree>
75+
<HaltsChild><ThrowOnHalt/></HaltsChild>
76+
</BehaviorTree></root>)");
77+
78+
EXPECT_EQ(tree.tickOnce(), NodeStatus::RUNNING);
79+
auto* node = dynamic_cast<HaltsChild*>(tree.rootNode());
80+
EXPECT_THROW(node->halt(), std::runtime_error);
81+
EXPECT_EQ(halts, 1);
82+
EXPECT_NO_THROW(node->halt());
83+
EXPECT_EQ(halts, 1);
84+
EXPECT_EQ(tree.subtrees[0]->nodes[1]->status(), NodeStatus::IDLE);
85+
}
86+
87+
namespace
88+
{
89+
// Built-in parents halt their children via resetChildren()/resetChild(), not
90+
// haltChild(): the failed child must end up IDLE there too.
91+
void checkBuiltinParentThrowingHalt(const char* body)
92+
{
93+
int halts = 0;
94+
BT::BehaviorTreeFactory factory;
95+
factory.registerNodeType<ThrowOnHalt>("ThrowOnHalt", &halts);
96+
auto tree =
97+
factory.createTreeFromText(std::string(R"(<root BTCPP_format="4"><BehaviorTree>)") +
98+
body + "</BehaviorTree></root>");
99+
100+
EXPECT_EQ(tree.tickOnce(), NodeStatus::RUNNING);
101+
EXPECT_ANY_THROW(tree.haltTree());
102+
EXPECT_EQ(halts, 1);
103+
for(const auto& node : tree.subtrees[0]->nodes)
104+
{
105+
EXPECT_EQ(node->status(), NodeStatus::IDLE) << node->name();
106+
}
107+
EXPECT_NO_THROW(tree.haltTree());
108+
EXPECT_EQ(halts, 1);
109+
// ~Tree() calls haltTree() again: it must not throw (-> terminate).
110+
}
111+
} // namespace
112+
113+
TEST(ControlNodeHaltChild, ThrowingHaltInSequence)
114+
{
115+
checkBuiltinParentThrowingHalt("<Sequence><ThrowOnHalt/></Sequence>");
116+
}
117+
118+
TEST(ControlNodeHaltChild, ThrowingHaltInRetryDecorator)
119+
{
120+
checkBuiltinParentThrowingHalt(
121+
R"(<RetryUntilSuccessful num_attempts="3"><ThrowOnHalt/></RetryUntilSuccessful>)");
122+
}

0 commit comments

Comments
 (0)