Skip to content

Commit bf62df5

Browse files
committed
implement
Signed-off-by: Alex Wu <c.alexwu@gmail.com>
1 parent 462619d commit bf62df5

28 files changed

Lines changed: 2730 additions & 280 deletions

actions/k8s/client.go

Lines changed: 66 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -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.
997993
func 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

10711118
func 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 {
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
package k8s
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/stretchr/testify/assert"
8+
"github.com/stretchr/testify/require"
9+
"google.golang.org/protobuf/proto"
10+
corev1 "k8s.io/api/core/v1"
11+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
12+
"k8s.io/apimachinery/pkg/runtime"
13+
"sigs.k8s.io/controller-runtime/pkg/client/fake"
14+
15+
executorv1 "github.com/flyteorg/flyte/v2/executor/api/v1"
16+
"github.com/flyteorg/flyte/v2/gen/go/flyteidl2/common"
17+
"github.com/flyteorg/flyte/v2/gen/go/flyteidl2/core"
18+
"github.com/flyteorg/flyte/v2/gen/go/flyteidl2/task"
19+
)
20+
21+
func recoverRunSpec(sourceRun string, forceRerun ...string) *task.RunSpec {
22+
spec := &task.RunSpec{
23+
Relation: &common.Relation{
24+
RelatedTo: &common.RunIdentifier{
25+
Org: "org1", Project: "proj", Domain: "dev", Name: sourceRun,
26+
},
27+
RelationType: common.RelationType_RELATION_TYPE_RECOVER,
28+
},
29+
}
30+
if len(forceRerun) > 0 {
31+
spec.Recover = &task.Recover{ForceRerunActions: forceRerun}
32+
}
33+
return spec
34+
}
35+
36+
func TestApplyRunSpecToTaskAction_StampsRecoveryContext(t *testing.T) {
37+
taskAction := &executorv1.TaskAction{Spec: executorv1.TaskActionSpec{}}
38+
39+
require.NoError(t, applyRunSpecToTaskAction(taskAction, recoverRunSpec("r1", "a3", "a7")))
40+
41+
recoveryContext := taskAction.Spec.RecoveryContext
42+
require.NotNil(t, recoveryContext)
43+
assert.Equal(t, []string{"a3", "a7"}, recoveryContext.ForceRerunActions)
44+
45+
relation := &common.Relation{}
46+
require.NoError(t, proto.Unmarshal(recoveryContext.Relation, relation))
47+
assert.Equal(t, common.RelationType_RELATION_TYPE_RECOVER, relation.GetRelationType())
48+
assert.Equal(t, "r1", relation.GetRelatedTo().GetName())
49+
}
50+
51+
// rerun and spawn share RunSpec.relation with recover; only the type makes it a recovery.
52+
func TestApplyRunSpecToTaskAction_NonRecoveryRelationStampsNothing(t *testing.T) {
53+
for _, relationType := range []common.RelationType{
54+
common.RelationType_RELATION_TYPE_RERUN,
55+
common.RelationType_RELATION_TYPE_SPAWN,
56+
common.RelationType_RELATION_TYPE_UNSPECIFIED,
57+
} {
58+
t.Run(relationType.String(), func(t *testing.T) {
59+
spec := recoverRunSpec("r1")
60+
spec.Relation.RelationType = relationType
61+
62+
taskAction := &executorv1.TaskAction{Spec: executorv1.TaskActionSpec{}}
63+
require.NoError(t, applyRunSpecToTaskAction(taskAction, spec))
64+
assert.Nil(t, taskAction.Spec.RecoveryContext)
65+
})
66+
}
67+
}
68+
69+
func TestApplyRunSpecToTaskAction_NilRunSpecClearsRecoveryContext(t *testing.T) {
70+
taskAction := &executorv1.TaskAction{
71+
Spec: executorv1.TaskActionSpec{
72+
RecoveryContext: &executorv1.RecoveryContext{Relation: []byte("stale")},
73+
},
74+
}
75+
76+
require.NoError(t, applyRunSpecToTaskAction(taskAction, nil))
77+
assert.Nil(t, taskAction.Spec.RecoveryContext)
78+
}
79+
80+
func TestInheritRunContextFromParentTaskAction_CopiesRecoveryContext(t *testing.T) {
81+
parent := &executorv1.TaskAction{
82+
Spec: executorv1.TaskActionSpec{
83+
RecoveryContext: recoveryContextFor("source-run", "a3"),
84+
},
85+
}
86+
child := &executorv1.TaskAction{Spec: executorv1.TaskActionSpec{}}
87+
88+
inheritRunContextFromParentTaskAction(child, parent)
89+
90+
require.NotNil(t, child.Spec.RecoveryContext)
91+
assert.Equal(t, []string{"a3"}, child.Spec.RecoveryContext.ForceRerunActions)
92+
93+
child.Spec.RecoveryContext.ForceRerunActions[0] = "mutated"
94+
child.Spec.RecoveryContext.Relation[0] = 'X'
95+
assert.Equal(t, []string{"a3"}, parent.Spec.RecoveryContext.ForceRerunActions)
96+
assert.Equal(t, recoveryContextFor("source-run").Relation, parent.Spec.RecoveryContext.Relation)
97+
}
98+
99+
func TestInheritRunContextFromParentTaskAction_NoRecoveryContextOnParent(t *testing.T) {
100+
child := &executorv1.TaskAction{Spec: executorv1.TaskActionSpec{}}
101+
inheritRunContextFromParentTaskAction(child, &executorv1.TaskAction{})
102+
assert.Nil(t, child.Spec.RecoveryContext)
103+
}
104+
105+
// A condition action used to inherit nothing from its parent, so a subtree beneath one lost
106+
// the run context entirely.
107+
func TestEnqueueCondition_InheritsRunContextFromParent(t *testing.T) {
108+
scheme := runtime.NewScheme()
109+
require.NoError(t, corev1.AddToScheme(scheme))
110+
require.NoError(t, executorv1.AddToScheme(scheme))
111+
112+
interruptible := true
113+
parent := &executorv1.TaskAction{
114+
ObjectMeta: metav1.ObjectMeta{
115+
Name: "run1-a0",
116+
Namespace: "flyte",
117+
Annotations: map[string]string{"owner": "sdk"},
118+
Labels: map[string]string{"team": "platform"},
119+
},
120+
Spec: executorv1.TaskActionSpec{
121+
EnvVars: map[string]string{"TRACE_ID": "abc123"},
122+
Interruptible: &interruptible,
123+
RecoveryContext: recoveryContextFor("source-run", "a3"),
124+
},
125+
}
126+
c := &ActionsClient{
127+
recordedFilter: testFilter(),
128+
namespace: "flyte",
129+
k8sClient: fake.NewClientBuilder().
130+
WithScheme(scheme).
131+
WithObjects(parent).
132+
WithStatusSubresource(&executorv1.TaskAction{}).
133+
Build(),
134+
}
135+
136+
action := newConditionAction(core.SimpleType_BOOLEAN)
137+
require.NoError(t, c.Enqueue(context.Background(), action, nil))
138+
139+
created, err := c.GetTaskAction(context.Background(), action.ActionId)
140+
require.NoError(t, err)
141+
require.NotNil(t, created.Spec.RecoveryContext)
142+
assert.Equal(t, recoveryContextFor("source-run", "a3").Relation, created.Spec.RecoveryContext.Relation)
143+
assert.Equal(t, []string{"a3"}, created.Spec.RecoveryContext.ForceRerunActions)
144+
assert.Equal(t, "abc123", created.Spec.EnvVars["TRACE_ID"])
145+
require.NotNil(t, created.Spec.Interruptible)
146+
assert.True(t, *created.Spec.Interruptible)
147+
assert.Equal(t, "sdk", created.Annotations["owner"])
148+
assert.Equal(t, "platform", created.Labels["team"])
149+
}

0 commit comments

Comments
 (0)