Skip to content

Commit 4f672ea

Browse files
dvadovszfacontidavideclaude
authored
add Finally control node for guaranteed cleanup (#1229)
* 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. * add Finally control node for guaranteed cleanup * Finally: guard concurrent halt(), noexcept halt, document blocking halt - A halt() that arrives while another one runs (e.g. Tree::haltTree() from another thread while the tick thread is in the halt_timeout_msec loop) returns at once, so cleanup is never ticked concurrently. An atomic halting_ flag is set with compare_exchange and cleared by RAII. - halt() is noexcept. The status reset at its end notifies observers that may throw, so it is wrapped and reported like the rest. - A pending main exception dropped by a halt is now printed. - Document that halt_timeout_msec > 10 blocks the halting parent and any enclosing Timeout, adds up across nested Finally nodes, and gives foreign-thread halts a single tick; that the default single tick cuts an asynchronous cleanup short; and "10 or less" means a single tick. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ME1hnSFSzibU5phxboGsjk --------- Co-authored-by: Davide Faconti <davide.faconti@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 2e63a35 commit 4f672ea

8 files changed

Lines changed: 1206 additions & 0 deletions

File tree

‎CMakeLists.txt‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,7 @@ list(APPEND BT_SOURCE
187187
src/controls/sequence_node.cpp
188188
src/controls/sequence_with_memory_node.cpp
189189
src/controls/switch_node.cpp
190+
src/controls/finally_node.cpp
190191
src/controls/try_catch_node.cpp
191192
src/controls/while_do_else_node.cpp
192193

‎include/behaviortree_cpp/behavior_tree.h‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
#include "behaviortree_cpp/actions/updated_action.h"
2626
#include "behaviortree_cpp/condition_node.h"
2727
#include "behaviortree_cpp/controls/fallback_node.h"
28+
#include "behaviortree_cpp/controls/finally_node.h"
2829
#include "behaviortree_cpp/controls/if_then_else_node.h"
2930
#include "behaviortree_cpp/controls/parallel_all_node.h"
3031
#include "behaviortree_cpp/controls/parallel_node.h"
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
#pragma once
2+
3+
#include "behaviortree_cpp/control_node.h"
4+
5+
#include <atomic>
6+
#include <exception>
7+
#include <thread>
8+
9+
namespace BT
10+
{
11+
/**
12+
* @brief The Finally node ticks its first child ("main") and then ticks its
13+
* second child ("cleanup"), like try/finally.
14+
*
15+
* - Cleanup runs after main returns SUCCESS or FAILURE. If main returns SKIPPED on
16+
* its first tick it did nothing, so cleanup is not run and the node returns
17+
* SKIPPED. If main is skipped after it returned RUNNING, for example by _while,
18+
* cleanup runs and the node returns SKIPPED.
19+
* - If main throws (any type), main is halted, cleanup runs, and then the
20+
* exception is rethrown.
21+
* - The node returns main's status, or FAILURE if cleanup fails.
22+
* - If this node is halted while main or cleanup is RUNNING, main is halted and
23+
* cleanup gets one tick, like TryCatch with catch_on_halt="true". A cleanup that
24+
* needs several ticks is cut short by the final halt of its subtree. That tick
25+
* runs inside halt(), on the calling thread and under any lock the caller holds,
26+
* so a cleanup action that blocks also blocks the caller.
27+
* - The single tick suits synchronous cleanup. An asynchronous cleanup child (e.g. a
28+
* StatefulActionNode) gets onStart() and then onHalted() right away. Use
29+
* "halt_timeout_msec" for asynchronous cleanup on halt.
30+
* - To let a multi-step cleanup finish, set "halt_timeout_msec" above 10 (the tick
31+
* period). Then halt() ticks cleanup once, and again every 10 ms until it finishes,
32+
* fails or throws, or the timeout runs out, in which case cleanup is halted
33+
* unfinished. No tick starts after the timeout, and a tick that blocks is not cut
34+
* short. With 10 or less, cleanup gets a single tick.
35+
* - That loop BLOCKS the halting parent, e.g. a ReactiveSequence, for up to
36+
* halt_timeout_msec, and nothing else in the tree is ticked meanwhile. An
37+
* enclosing Timeout that holds its mutex during the child's tick is delayed by up
38+
* to the same amount, and nested Finally nodes with the option set add up their
39+
* timeouts. Keep the value short.
40+
* - The loop runs only when halt() is called from the thread that last ticked this
41+
* node. A halt from another thread, such as a Timeout decorator's timer or a
42+
* service callback calling Tree::haltTree(), gives cleanup the single tick and
43+
* prints a note to stderr. A Finally nested inside that halt also gets a single
44+
* tick. If halt() is called while another halt() of this node is still running,
45+
* it returns at once and leaves the cleanup to the first one. halt() is not
46+
* synchronized with a concurrent tick().
47+
* - An exception thrown by cleanup in tick() halts cleanup, leaves the node
48+
* IDLE and propagates. It replaces main's status, or main's exception,
49+
* which is dropped. The next tick starts with main.
50+
* - halt() never throws, because it also runs from ~Tree(). It prints to
51+
* stderr when cleanup throws, fails or times out, and when halting a child
52+
* throws. A pending exception from main is printed and dropped by a halt.
53+
*
54+
* Requires exactly 2 children, checked when the XML is loaded and on tick.
55+
*/
56+
class FinallyNode : public ControlNode
57+
{
58+
public:
59+
FinallyNode(const std::string& name, const NodeConfig& config);
60+
61+
~FinallyNode() override = default;
62+
63+
FinallyNode(const FinallyNode&) = delete;
64+
FinallyNode& operator=(const FinallyNode&) = delete;
65+
FinallyNode(FinallyNode&&) = delete;
66+
FinallyNode& operator=(FinallyNode&&) = delete;
67+
68+
static PortsList providedPorts()
69+
{
70+
return { InputPort<unsigned>(
71+
"halt_timeout_msec", kDefaultHaltTimeoutMsec,
72+
"When halted, how long to keep ticking cleanup before halting it "
73+
"unfinished, in milliseconds. 10 (the tick period) or less: a single "
74+
"tick, which cuts an asynchronous cleanup short. Above 10, halt() blocks "
75+
"the halting parent, and any enclosing Timeout, for up to this long, and "
76+
"nested Finally nodes add up; halts from another thread get a single "
77+
"tick. Keep it short") };
78+
}
79+
80+
void halt() noexcept override;
81+
82+
private:
83+
static constexpr unsigned kDefaultHaltTimeoutMsec = 0;
84+
85+
// Written in tick(), read in halt(), which may run on another thread.
86+
std::atomic<std::thread::id> tick_thread_{};
87+
// Set while a halt() runs, so a concurrent halt() from another thread does not tick
88+
// cleanup at the same time.
89+
std::atomic<bool> halting_{ false };
90+
bool in_cleanup_ = false;
91+
bool main_started_ = false;
92+
NodeStatus main_status_ = NodeStatus::IDLE;
93+
std::exception_ptr main_exception_;
94+
95+
void haltChildNoThrow(size_t i) noexcept;
96+
void finishCleanupDuringHalt(bool foreign_thread);
97+
98+
BT::NodeStatus tick() override;
99+
};
100+
101+
} // namespace BT

‎src/bt_factory.cpp‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ BehaviorTreeFactory::BehaviorTreeFactory() : _p(new PImpl)
134134
registerNodeType<IfThenElseNode>("IfThenElse");
135135
registerNodeType<WhileDoElseNode>("WhileDoElse");
136136
registerNodeType<TryCatchNode>("TryCatch");
137+
registerNodeType<FinallyNode>("Finally");
137138

138139
registerNodeType<InverterNode>("Inverter");
139140

0 commit comments

Comments
 (0)