diff --git a/cmd/cue/cmd/testdata/script/cmd_issue4195.txtar b/cmd/cue/cmd/testdata/script/cmd_issue4195.txtar new file mode 100644 index 000000000..575f6c3f7 --- /dev/null +++ b/cmd/cue/cmd/testdata/script/cmd_issue4195.txtar @@ -0,0 +1,49 @@ +# Regression test for https://github.com/cue-lang/cue/issues/4195 +# +# When two tasks run concurrently and a comprehension over one task's +# result contains conditionals depending on the other task's result, +# the flow engine could discover "phantom" tasks from both conditional +# branches during an intermediate evaluation. These phantom tasks were +# never pruned and would be dispatched later, failing because their +# conditional branch no longer existed in the configuration. + +exec cue cmd main + +-- input/foo.json -- +-- input/bar.json -- +-- output/foo.json -- +-- cue.mod/module.cue -- +module: "test.example" +language: version: "v0.9.0" +-- main_tool.cue -- +package repro + +import ( + "tool/cli" + "tool/exec" + "tool/file" + "list" + "path" +) + +command: main: { + inputs: file.Glob & {glob: "input/*.json"} + // Delay "outputs" so that "inputs" is always processed first, + // causing initTasks to discover phantom conditional tasks. + delay: exec.Run & {cmd: ["testcmd", "sleep_and_print", "50ms"]} + outputs: file.Glob & {glob: "output/*.json", $after: delay} + + for _, inputpath in inputs.files { + (inputpath): { + _output: path.Join(["output", path.Base(inputpath)], path.Unix) + _done: list.Contains(outputs.files, _output) + + if _done { + print: cli.Print & {text: "\(inputpath) already in outputs"} + } + if !_done { + work: cli.Print & {text: "\(inputpath) not yet done"} + } + } + } +} diff --git a/tools/flow/run.go b/tools/flow/run.go index 7ab53f89c..6b936ae7d 100644 --- a/tools/flow/run.go +++ b/tools/flow/run.go @@ -67,11 +67,18 @@ func (c *Controller) runLoop() { waiting = true case Ready: - running = true - t.state = Running c.updateTaskValue(t) + // If the task's path no longer resolves in the configuration + // (e.g. because a conditional guard was eliminated as the + // config became more concrete), skip this task. + if !t.v.Exists() { + t.state = Terminated + continue + } + + running = true t.ctxt = eval.NewContext(value.ToInternal(t.v)) go func(t *Task) {