|
| 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 |
0 commit comments