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