feat: Add Finally control node for guaranteed cleanup - #36
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configuration
📒 Files selected for processing (10)
Included review availability: This review used your included allowance. 9 included reviews remain after this review. Your included PR review attempts over the past 7 days set your current allowance at 10 reviews per hour. 📝 SummarySummary by CodeRabbit
WalkthroughThis change adds a built-in ChangesFinally control node
Priority: ➖ Normal Merge Risk: ⚪ Minimal · up to The Finally node is ready to merge after normal checks; no outstanding behavior issue was established. Caution Pre-merge checks failedPlease resolve all errors before merging. Addressing warnings is optional.
❌ Failed checks (1 error)
✅ Passed checks (3 passed)
Full details: Human Review CheckExplanation The PR adds a public
Comment |
de12692 to
3951326
Compare
Finally ticks its first child (main), then always ticks its second child (cleanup): after main returns SUCCESS, FAILURE or SKIPPED, after main throws (printed to stderr, returned as FAILURE), and synchronously when the node is halted while main is RUNNING. It returns main's status, or FAILURE if cleanup fails. A cleanup exception propagates from tick() and is printed during halt(), because halt() also runs from ~Tree(). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> (cherry picked from commit 8094d46)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Halting main from the exception path, and every halt inside halt(), now prints instead of propagating, so cleanup still runs and ~Tree() cannot terminate. Non-std exceptions are caught too. FinallyNode joins TreeNode's friends to reset a child whose halt() threw, since ControlNode::resetChildren() would otherwise halt it again. Adds tests for these paths, a ReactiveSequence parent, a cleanup retry after a throw, and a SKIPPED cleanup. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ControlNode::haltChild now resets the child's status before rethrowing, so the next reset doesn't halt it again. This replaces the TreeNode friend that Finally needed. Finally also reports a cleanup FAILURE during halt, documents that halt-time cleanup is synchronous and runs on the calling thread, and gains SubTree and nested-Finally tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When main throws, Finally halts main, runs cleanup, then rethrows the exception instead of returning FAILURE. This matches finally in other languages, keeps the NodeExecutionError context, and stops errors such as a missing port from being retried as ordinary failures. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A bare rethrow outside a catch handler trips Sonar cpp:S1039, so the printer takes an exception_ptr. Tests throw a local TestError instead of std::runtime_error (cpp:S112). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
e9dafc4 to
c4c6e3b
Compare
[written by AI]
Adds a
Finallycontrol node with two children, main and cleanup. It ticks main, then ticks cleanup, which gives Objectives try/finally semantics for resetting scene state such as collision rules.The same change is proposed upstream in BehaviorTree#1229, for upstream issue BehaviorTree#1228. This PR does not wait for it, since upstream may decline the node. If upstream merges it, the next upstream port brings it in and we can drop the fork copy.
halt()never throws, because~Tree()callshaltTree()and a throw there callsstd::terminate. It prints exceptions and a cleanup FAILURE to stderr instead.ControlNode::haltChildnow resets the child's status before rethrowing an exception from itshalt(). Without that, the next reset would halt the child again and throw a second time.Finallywithout exactly 2 children.Related: PickNikRobotics/moveit_pro#20169, milestone 10.2.0. The first commit here is a cherry-pick of the upstream commit. The later fix commits are squashed into that upstream commit, and the Finally files,
control_node.cppand the tests are identical on both branches.TryCatch, which arrived with the 4.9.0 port, doesn't cover this. It skips the catch child on SUCCESS and does not catch exceptions.Differences from the issue text:
finallyin other languages, keeps theNodeExecutionErrorcontext for loggers, and stops a programming error such as a missing required port from being retried by aRetryUntilSuccessfulas if it were an ordinary failure. The tradeoff is that an outerFallbackcan't recover from it.Validation:
behaviortree_cpp_picknik_testpasses all 561 tests locally (Release, GCC) after the rebase ontomain-picknik, and upstream'sbehaviortree_cpp_testpasses all 565 on the upstream branch. 31FinallyTestcases cover sync and async main and cleanup, a halt in each phase, exceptions from main, from cleanup, and from main's ownhalt(), non-std exceptions, SKIPPED main and cleanup, retry after a throw, nestedFinally,SubTreeandReactiveSequenceparents, and child-count validation. I checked that the key tests fail without their fix. Removing thehaltChildreset aborts the test binary withstd::terminate.Reviewers run:
picknik:code-reviewer,picknik:platform-architect-bot,picknik:sonar-bot, and the CodeRabbit CLI, with findings applied. Deferred:cpp:S3656on the protected gtest fixture members, to match upstream'sgtest_try_catch.cpp.cpp:S2738andcpp:S1181on the catch-alls inhalt(), which exist so~Tree()can't terminate.roboticist, frontend, security, documentation, licensing and compatibility bots: skipped, since no trigger paths are touched and the change is a pure addition.
🤖 Generated with Claude Code