@@ -808,3 +808,117 @@ def validate_policy_configuration(self) -> None:
808
808
)
809
809
self .logger .error (error_message )
810
810
raise RuntimeError (error_message )
811
+
812
+
813
+ class Recovery (Composite ):
814
+ """
815
+ A Recovery composite that wraps a main behaviour with a sequence of recovery behaviours.
816
+
817
+ .. graphviz:: dot/recovery.dot
818
+
819
+ Execution model:
820
+ - Tick the main behaviour first.
821
+ - If main returns SUCCESS or RUNNING, propagate that.
822
+ - If main returns FAILURE:
823
+ * Attempt the next recovery behaviour in sequence.
824
+ * If recovery RUNNING, propagate RUNNING.
825
+ * If recovery completes (SUCCESS or FAILURE), consume it and
826
+ retry main (if any recoveries remain).
827
+ - If all recoveries are exhausted and main still fails, return FAILURE.
828
+
829
+ Args:
830
+ name (:obj:`str`): the composite behaviour name
831
+ children ([:class:`~py_trees.behaviour.Behaviour`]): list of children,
832
+ where the first is the main behaviour and the rest are recovery behaviours
833
+ """
834
+
835
+ def __init__ (
836
+ self ,
837
+ name : str ,
838
+ children : typing .Optional [typing .Sequence [behaviour .Behaviour ]] = None ,
839
+ ):
840
+ super ().__init__ (name , children )
841
+ if not children or len (children ) < 1 :
842
+ raise ValueError ("Recovery requires at least a main behaviour" )
843
+
844
+ # Explicit references
845
+ self .main : behaviour .Behaviour = children [0 ]
846
+ self .recoveries : typing .List [behaviour .Behaviour ] = (
847
+ list (children [1 :]) if len (children ) > 1 else []
848
+ )
849
+ self .current_recovery_index : int = 0
850
+ self .running_main = True
851
+
852
+ def initialise (self ) -> None :
853
+ """Reset to the initial state: run main behaviour and restart recovery behaviours sequence."""
854
+ self .current_recovery_index = 0
855
+ self .running_main = True
856
+
857
+ def tick (self ) -> typing .Iterator [behaviour .Behaviour ]:
858
+ """
859
+ Tick over the children.
860
+
861
+ Yields:
862
+ :class:`~py_trees.behaviour.Behaviour`: a reference to itself or one of its children
863
+ """
864
+ self .logger .debug ("%s.tick()" % self .__class__ .__name__ )
865
+
866
+ if not self .children :
867
+ self .stop (common .Status .FAILURE )
868
+ yield self
869
+ return
870
+
871
+ # First try the main behaviour if we are not in the middle of a recovery
872
+ if self .running_main :
873
+ for node in self .main .tick ():
874
+ yield node
875
+ if node is self .main :
876
+ if node .status in (common .Status .SUCCESS , common .Status .RUNNING ):
877
+ self .status = node .status
878
+ yield self
879
+ return
880
+ elif node .status == common .Status .FAILURE :
881
+ # proceed to next recovery
882
+ self .running_main = False
883
+
884
+ # Try recoveries
885
+ while self .current_recovery_index < len (self .recoveries ):
886
+ recovery = self .recoveries [self .current_recovery_index ]
887
+ for node in recovery .tick ():
888
+ yield node
889
+ if node is recovery :
890
+ if node .status == common .Status .RUNNING :
891
+ self .status = common .Status .RUNNING
892
+ yield self
893
+ return
894
+ elif node .status == common .Status .SUCCESS :
895
+ self .status = common .Status .RUNNING
896
+ # consume this recovery and retry main
897
+ recovery .stop (common .Status .INVALID )
898
+ self .current_recovery_index += 1
899
+ self .main .stop (common .Status .INVALID )
900
+ self .running_main = True
901
+ yield self
902
+ return
903
+ elif node .status == common .Status .FAILURE :
904
+ # consume this recovery and move to next
905
+ recovery .stop (common .Status .INVALID )
906
+ self .current_recovery_index += 1
907
+ yield self
908
+
909
+ # No recoveries left → fail
910
+ self .status = common .Status .FAILURE
911
+ yield self
912
+
913
+ def stop (self , new_status : common .Status = common .Status .INVALID ) -> None :
914
+ """
915
+ Ensure that children are appropriately stopped and update status.
916
+
917
+ Args:
918
+ new_status : the composite is transitioning to this new status
919
+ """
920
+ for child in self .children :
921
+ if child .status != common .Status .INVALID :
922
+ child .stop (common .Status .INVALID )
923
+ self .current_recovery_index = 0
924
+ super ().stop (new_status )
0 commit comments