@@ -63,7 +63,8 @@ type ActionsClient struct {
6363 bufferSize int
6464 runClient workflowconnect.InternalRunServiceClient
6565 // recordedFilter deduplicates RecordAction calls across watch reconnects.
66- recordedFilter fastcheck.Filter
66+ recordedFilter fastcheck.Filter
67+ recoveryMetrics * recoveryMetrics
6768
6869 // Watch management
6970 mu sync.RWMutex
@@ -119,6 +120,7 @@ func NewActionsClient(k8sClient client.WithWatch, sharedCache ctrlcache.Cache, n
119120 return nil , fmt .Errorf ("actions: failed to create RecordAction dedup filter (size=%d): %w" , recordFilterSize , err )
120121 }
121122 c .recordedFilter = filter
123+ c .recoveryMetrics = newRecoveryMetrics (scope )
122124
123125 return c , nil
124126}
@@ -138,17 +140,8 @@ func (c *ActionsClient) Enqueue(ctx context.Context, action *actions.Action, run
138140 return fmt .Errorf ("failed to ensure namespace %s: %w" , c .namespace , err )
139141 }
140142 taskAction := c .newTaskActionCR (actionID , executorv1 .ActionTypeTask , isRoot )
141- // Set OwnerReference to parent so K8s cascades deletion to children.
142- if ! isRoot {
143- parentTaskAction , err := c .setParentOwnership (ctx , taskAction , actionID .Run , * action .ParentActionName )
144- if err != nil {
145- return err
146- }
147- // For child actions, inherit parent's run context
148- inheritRunContextFromParentTaskAction (taskAction , parentTaskAction )
149- } else {
150- // For root action, apply the RunSpec to TaskAction
151- applyRunSpecToTaskAction (taskAction , runSpec )
143+ if err := c .applyRunContext (ctx , taskAction , action , runSpec , isRoot ); err != nil {
144+ return err
152145 }
153146
154147 // Build and set the ActionSpec for the executor.
@@ -157,6 +150,7 @@ func (c *ActionsClient) Enqueue(ctx context.Context, action *actions.Action, run
157150 return fmt .Errorf ("failed to set action spec: %w" , err )
158151 }
159152 taskAction .Spec .CacheKey = extractTaskCacheKey (action )
153+ taskAction .Spec .RecoveredFrom = c .resolveRecoveredFrom (ctx , taskAction , action , isRoot )
160154
161155 // Embed the inline TaskTemplate if present.
162156 if err := embedTaskTemplate (action , taskAction , runSpec ); err != nil {
@@ -179,17 +173,16 @@ func (c *ActionsClient) Enqueue(ctx context.Context, action *actions.Action, run
179173 return fmt .Errorf ("failed to ensure namespace %s: %w" , c .namespace , err )
180174 }
181175 taskAction := c .newTaskActionCR (actionID , executorv1 .ActionTypeCondition , isRoot )
182- if ! isRoot {
183- if _ , err := c .setParentOwnership (ctx , taskAction , actionID .Run , * action .ParentActionName ); err != nil {
184- return err
185- }
176+ if err := c .applyRunContext (ctx , taskAction , action , runSpec , isRoot ); err != nil {
177+ return err
186178 }
187179
188180 actionSpec := buildActionSpec (action , runSpec )
189181 if err := taskAction .Spec .SetActionSpec (actionSpec ); err != nil {
190182 return fmt .Errorf ("failed to set action spec: %w" , err )
191183 }
192184 taskAction .Spec .ActionType = executorv1 .ActionTypeCondition
185+ taskAction .Spec .RecoveredFrom = c .resolveRecoveredFrom (ctx , taskAction , action , isRoot )
193186 condBytes , err := proto .Marshal (cond )
194187 if err != nil {
195188 return fmt .Errorf ("failed to marshal condition spec: %w" , err )
@@ -866,6 +859,9 @@ func GetPhaseFromConditions(taskAction *executorv1.TaskAction) common.ActionPhas
866859 switch cond .Type {
867860 case string (executorv1 .ConditionTypeSucceeded ):
868861 if cond .Status == "True" {
862+ if cond .Reason == string (executorv1 .ConditionReasonRecovered ) {
863+ return common .ActionPhase_ACTION_PHASE_RECOVERED
864+ }
869865 return common .ActionPhase_ACTION_PHASE_SUCCEEDED
870866 }
871867 case string (executorv1 .ConditionTypeFailed ):
@@ -995,6 +991,11 @@ func buildTaskActionName(actionID *common.ActionIdentifier) string {
995991// It uses the same path structure as the executor's ComputeActionOutputPath so that
996992// the SDK can find outputs written by the executor.
997993func BuildOutputUri (ctx context.Context , ta * executorv1.TaskAction ) string {
994+ // A recovered action wrote nothing under this run's base; its result is the source run's.
995+ // RecoveredFrom carries the outputs file, this returns the directory the SDK joins onto.
996+ if ta .Spec .RecoveredFrom != nil {
997+ return plugin .OutputPrefixOf (ta .Spec .RecoveredFrom .OutputUri )
998+ }
998999 if ta .Spec .RunOutputBase == "" {
9991000 return ""
10001001 }
@@ -1037,13 +1038,57 @@ func buildActionSpec(action *actions.Action, runSpec *task.RunSpec) *workflow.Ac
10371038 return actionSpec
10381039}
10391040
1040- func applyRunSpecToTaskAction (taskAction * executorv1.TaskAction , runSpec * task.RunSpec ) {
1041+ // applyRunContext sets the OwnerReference on the parent and applies the run context reaching
1042+ // this action: from the RunSpec for the root, from the parent CR for everything else.
1043+ func (c * ActionsClient ) applyRunContext (
1044+ ctx context.Context ,
1045+ taskAction * executorv1.TaskAction ,
1046+ action * actions.Action ,
1047+ runSpec * task.RunSpec ,
1048+ isRoot bool ,
1049+ ) error {
1050+ if isRoot {
1051+ return applyRunSpecToTaskAction (taskAction , runSpec )
1052+ }
1053+ parentTaskAction , err := c .setParentOwnership (ctx , taskAction , action .ActionId .Run , * action .ParentActionName )
1054+ if err != nil {
1055+ return err
1056+ }
1057+ // child actions inherit run context from parent
1058+ inheritRunContextFromParentTaskAction (taskAction , parentTaskAction )
1059+ return nil
1060+ }
1061+
1062+ // recoveryContextFromRunSpec returns nil for any run that is not a recovery.
1063+ func recoveryContextFromRunSpec (runSpec * task.RunSpec ) (* executorv1.RecoveryContext , error ) {
1064+ relation := runSpec .GetRelation ()
1065+ if relation .GetRelationType () != common .RelationType_RELATION_TYPE_RECOVER {
1066+ return nil , nil
1067+ }
1068+ relationBytes , err := proto .Marshal (relation )
1069+ if err != nil {
1070+ return nil , fmt .Errorf ("failed to marshal recovery relation: %w" , err )
1071+ }
1072+ return & executorv1.RecoveryContext {
1073+ Relation : relationBytes ,
1074+ ForceRerunActions : runSpec .GetRecover ().GetForceRerunActions (),
1075+ }, nil
1076+ }
1077+
1078+ func applyRunSpecToTaskAction (taskAction * executorv1.TaskAction , runSpec * task.RunSpec ) error {
10411079 if runSpec == nil {
10421080 taskAction .Spec .EnvVars = nil
10431081 taskAction .Spec .Interruptible = nil
1044- return
1082+ taskAction .Spec .RecoveryContext = nil
1083+ return nil
10451084 }
10461085
1086+ recoveryContext , err := recoveryContextFromRunSpec (runSpec )
1087+ if err != nil {
1088+ return err
1089+ }
1090+ taskAction .Spec .RecoveryContext = recoveryContext
1091+
10471092 taskAction .Spec .EnvVars = keyValuePairsToMap (runSpec .GetEnvs ().GetValues ())
10481093 if runSpec .GetInterruptible () != nil {
10491094 value := runSpec .GetInterruptible ().GetValue ()
@@ -1066,12 +1111,15 @@ func applyRunSpecToTaskAction(taskAction *executorv1.TaskAction, runSpec *task.R
10661111 taskAction .Annotations [key ] = value
10671112 }
10681113 }
1114+
1115+ return nil
10691116}
10701117
10711118func inheritRunContextFromParentTaskAction (taskAction * executorv1.TaskAction , parentTaskAction * executorv1.TaskAction ) {
10721119 if taskAction == nil || parentTaskAction == nil {
10731120 return
10741121 }
1122+ taskAction .Spec .RecoveryContext = parentTaskAction .Spec .RecoveryContext .DeepCopy ()
10751123 taskAction .Spec .EnvVars = cloneStringMap (parentTaskAction .Spec .EnvVars )
10761124 if len (parentTaskAction .Annotations ) > 0 {
10771125 if taskAction .Annotations == nil {
0 commit comments