Skip to content

Commit 61ea19f

Browse files
authored
fix: race condition inside an expanded step regarding dependencies (#277)
Children steps Dependencies array was shared between all the child steps and the parent step, every modification to one child steps was propagated to the other child. Impact was detected on template steps with `foreach_strategy` set to `sequence` as the child steps might be launched all together, instead of waiting the previous one. Signed-off-by: Romain Beuque <556072+rbeuque74@users.noreply.github.com>
1 parent 90d475a commit 61ea19f

File tree

2 files changed

+69
-4
lines changed

2 files changed

+69
-4
lines changed

engine/engine.go

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020

2121
"github.com/ovh/utask"
2222
"github.com/ovh/utask/engine/step"
23+
"github.com/ovh/utask/engine/step/condition"
2324
"github.com/ovh/utask/engine/values"
2425
"github.com/ovh/utask/models/resolution"
2526
"github.com/ovh/utask/models/runnerinstance"
@@ -738,6 +739,18 @@ func expandStep(s *step.Step, res *resolution.Resolution) {
738739
// generate all children steps
739740
for i, item := range items {
740741
childStepName := fmt.Sprintf("%s-%d", s.Name, i)
742+
743+
// copy all slices from the parent step to prevent array pointer
744+
// to be shared between multiple steps
745+
dependencies := make([]string, len(s.Dependencies))
746+
customStates := make([]string, len(s.CustomStates))
747+
conditions := make([]*condition.Condition, len(s.Conditions))
748+
resources := make([]string, len(s.Resources))
749+
copy(dependencies, s.Dependencies)
750+
copy(customStates, s.CustomStates)
751+
copy(conditions, s.Conditions)
752+
copy(resources, s.Resources)
753+
741754
res.Steps[childStepName] = &step.Step{
742755
Name: childStepName,
743756
Description: fmt.Sprintf("%d - %s", i, s.Description),
@@ -747,10 +760,10 @@ func expandStep(s *step.Step, res *resolution.Resolution) {
747760
State: step.StateTODO,
748761
RetryPattern: s.RetryPattern,
749762
MaxRetries: s.MaxRetries,
750-
Dependencies: s.Dependencies,
751-
CustomStates: s.CustomStates,
752-
Conditions: s.Conditions,
753-
Resources: s.Resources,
763+
Dependencies: dependencies,
764+
CustomStates: customStates,
765+
Conditions: conditions,
766+
Resources: resources,
754767
Item: item,
755768
}
756769

engine/engine_test.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -702,6 +702,58 @@ func TestForeachWithChainedIterations(t *testing.T) {
702702
td.Cmp(t, res.Steps["generateItems-4"].Dependencies, []string{"generateItems-3"})
703703
}
704704

705+
func TestForeachWithChainedIterationsWithDepOnParent(t *testing.T) {
706+
_, require := td.AssertRequire(t)
707+
res, err := createResolution("foreach.yaml", map[string]interface{}{
708+
"list": []interface{}{"a", "b", "c", "d", "e"},
709+
}, nil)
710+
require.Nil(err)
711+
require.NotNil(res)
712+
713+
res.Steps["generateItems"].Dependencies = []string{"emptyLoop"}
714+
res.Steps["generateItems"].Conditions[0].Then["this"] = "DONE"
715+
res.Steps["generateItems"].Conditions = append(
716+
res.Steps["generateItems"].Conditions,
717+
&condition.Condition{
718+
Type: condition.CHECK,
719+
If: []*condition.Assert{
720+
{
721+
Value: "{{.iterator}}",
722+
Operator: condition.EQ,
723+
Expected: "d",
724+
},
725+
},
726+
Then: map[string]string{
727+
"this": "SERVER_ERROR",
728+
},
729+
},
730+
)
731+
res.Steps["generateItems"].ForEachStrategy = "sequence"
732+
err = updateResolution(res)
733+
require.Nil(err)
734+
735+
res, err = runResolution(res)
736+
require.NotNil(res)
737+
require.Nil(err)
738+
require.Cmp(res.State, resolution.StateError)
739+
740+
td.Cmp(t, res.Steps["emptyLoop"].State, step.StateDone) // running on empty collection is ok
741+
td.Cmp(t, res.Steps["concatItems"].State, step.StateTODO)
742+
td.Cmp(t, res.Steps["finalStep"].State, step.StateTODO)
743+
td.Cmp(t, res.Steps["bStep"].State, "B")
744+
td.Cmp(t, res.Steps["generateItems-0"].State, step.StateDone)
745+
td.Cmp(t, res.Steps["generateItems-1"].State, step.StateDone)
746+
td.Cmp(t, res.Steps["generateItems-2"].State, step.StateDone)
747+
td.Cmp(t, res.Steps["generateItems-3"].State, step.StateServerError)
748+
td.Cmp(t, res.Steps["generateItems-4"].State, step.StateTODO)
749+
td.Cmp(t, res.Steps["generateItems"].Dependencies, []string{"emptyLoop", "generateItems-0:ANY", "generateItems-1:ANY", "generateItems-2:ANY", "generateItems-3:ANY", "generateItems-4:ANY"})
750+
td.Cmp(t, res.Steps["generateItems-0"].Dependencies, []string{"emptyLoop"})
751+
td.Cmp(t, res.Steps["generateItems-1"].Dependencies, []string{"emptyLoop", "generateItems-0"})
752+
td.Cmp(t, res.Steps["generateItems-2"].Dependencies, []string{"emptyLoop", "generateItems-1"})
753+
td.Cmp(t, res.Steps["generateItems-3"].Dependencies, []string{"emptyLoop", "generateItems-2"})
754+
td.Cmp(t, res.Steps["generateItems-4"].Dependencies, []string{"emptyLoop", "generateItems-3"})
755+
}
756+
705757
func TestForeachWithPreRun(t *testing.T) {
706758
input := map[string]interface{}{}
707759
res, err := createResolution("foreachAndPreRun.yaml", input, nil)

0 commit comments

Comments
 (0)