Skip to content

Execution Graph to detect cyclic tasks #2280

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions call.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ type Call struct {
Vars *ast.Vars
Silent bool
Indirect bool // True if the task was called by another task
Vertex *ast.TaskExecutionVertex
}
3 changes: 2 additions & 1 deletion errors/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,11 @@ const (
CodeTaskRunError
CodeTaskInternal
CodeTaskNameConflict
CodeTaskCalledTooManyTimes
CodeTaskCalledTooManyTimes // Depreciated: replaced by CodeTaskCyclicExecutionDetected.
CodeTaskCancelled
CodeTaskMissingRequiredVars
CodeTaskNotAllowedVars
CodeTaskCyclicExecutionDetected
)

// TaskError extends the standard error interface with a Code method. This code will
Expand Down
31 changes: 19 additions & 12 deletions errors/errors_task.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,23 +93,30 @@ func (err *TaskNameFlattenConflictError) Code() int {
return CodeTaskNameConflict
}

// TaskCalledTooManyTimesError is returned when the maximum task call limit is
// exceeded. This is to prevent infinite loops and cyclic dependencies.
type TaskCalledTooManyTimesError struct {
// TaskCyclicExecutionDetectedError is returned when the Execution Graph detects
// a cyclic execution condition.
type TaskCyclicExecutionDetectedError struct {
TaskName string
MaximumTaskCall int
CallingTaskName string
}

func (err *TaskCalledTooManyTimesError) Error() string {
return fmt.Sprintf(
`task: Maximum task call exceeded (%d) for task %q: probably an cyclic dep or infinite loop`,
err.MaximumTaskCall,
err.TaskName,
)
func (err *TaskCyclicExecutionDetectedError) Error() string {
if len(err.CallingTaskName) > 0 {
return fmt.Sprintf(
`task: Cyclic task call execution detected for task %q (calling task %q)`,
err.TaskName,
err.CallingTaskName,
)
} else {
return fmt.Sprintf(
`task: Cyclic task call execution detected for task %q`,
err.TaskName,
)
}
}

func (err *TaskCalledTooManyTimesError) Code() int {
return CodeTaskCalledTooManyTimes
func (err *TaskCyclicExecutionDetectedError) Code() int {
return CodeTaskCyclicExecutionDetected
}

// TaskCancelledByUserError is returned when the user does not accept an optional prompt to continue.
Expand Down
5 changes: 3 additions & 2 deletions executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,11 @@ type (
UserWorkingDir string
EnableVersionCheck bool

graph *ast.TaskExecutionGraph

fuzzyModel *fuzzy.Model

concurrencySemaphore chan struct{}
taskCallCount map[string]*int32
mkdirMutexMap map[string]*sync.Mutex
executionHashes map[string]context.Context
executionHashesMutex sync.Mutex
Expand All @@ -92,9 +93,9 @@ func NewExecutor(opts ...ExecutorOption) *Executor {
OutputStyle: ast.Output{},
TaskSorter: sort.AlphaNumericWithRootTasksFirst,
UserWorkingDir: "",
graph: ast.NewTaskExecutionGraph(),
fuzzyModel: nil,
concurrencySemaphore: nil,
taskCallCount: map[string]*int32{},
mkdirMutexMap: map[string]*sync.Mutex{},
executionHashes: map[string]context.Context{},
executionHashesMutex: sync.Mutex{},
Expand Down
196 changes: 196 additions & 0 deletions executor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -978,3 +978,199 @@ func TestIncludeChecksum(t *testing.T) {
WithPostProcessFn(PPRemoveAbsolutePaths),
)
}

func TestExecutionGraph(t *testing.T) {
t.Parallel()

NewExecutorTest(t,
WithName("loop-cycle"),
WithExecutorOptions(
task.WithDir("testdata/cyclic"),
),
WithTask("loop-cycle"),
WithRunError(),
)

NewExecutorTest(t,
WithName("long-loop-cycle"),
WithExecutorOptions(
task.WithDir("testdata/cyclic"),
),
WithTask("long-loop-cycle"),
WithRunError(),
)

NewExecutorTest(t,
WithName("A"),
WithExecutorOptions(
task.WithDir("testdata/cyclic"),
),
WithTask("A"),
)

NewExecutorTest(t,
WithName("A"),
WithExecutorOptions(
task.WithDir("testdata/cyclic"),
),
WithTask("A"),
WithVar("CYCLEBACK", "A"),
WithRunError(),
)

NewExecutorTest(t,
WithName("call-foo"),
WithExecutorOptions(
task.WithDir("testdata/cyclic"),
),
WithTask("call-foo"),
)
NewExecutorTest(t,
WithName("call-bar"),
WithExecutorOptions(
task.WithDir("testdata/cyclic"),
),
WithTask("call-bar"),
)

NewExecutorTest(t,
WithName("call-converge"),
WithExecutorOptions(
task.WithDir("testdata/cyclic"),
),
WithTask("call-converge"),
)

NewExecutorTest(t,
WithName("call-converge-cyclic"),
WithExecutorOptions(
task.WithDir("testdata/cyclic"),
),
WithTask("call-converge-cyclic"),
WithRunError(),
)

NewExecutorTest(t,
WithName("deps-foo"),
WithExecutorOptions(
task.WithDir("testdata/cyclic"),
),
WithTask("deps-foo"),
)
NewExecutorTest(t,
WithName("deps-bar"),
WithExecutorOptions(
task.WithDir("testdata/cyclic"),
),
WithTask("deps-bar"),
)

NewExecutorTest(t,
WithName("deps-converge"),
WithExecutorOptions(
task.WithDir("testdata/cyclic"),
),
WithTask("deps-converge"),
)

NewExecutorTest(t,
WithName("deps-converge-cyclic"),
WithExecutorOptions(
task.WithDir("testdata/cyclic"),
),
WithTask("deps-converge-cyclic"),
WithRunError(),
)

NewExecutorTest(t,
WithName("call-deps"),
WithExecutorOptions(
task.WithDir("testdata/cyclic"),
),
WithTask("call-deps"),
)

NewExecutorTest(t,
WithName("call-deps-cyclic"),
WithExecutorOptions(
task.WithDir("testdata/cyclic"),
),
WithTask("call-deps-cyclic"),
WithRunError(),
)

NewExecutorTest(t,
WithName("for-staggered"),
WithExecutorOptions(
task.WithDir("testdata/cyclic"),
),
WithTask("for-staggered"),
)

NewExecutorTest(t,
WithName("for-staggered-cyclic"),
WithExecutorOptions(
task.WithDir("testdata/cyclic"),
),
WithTask("for-staggered-cyclic"),
WithRunError(),
)

NewExecutorTest(t,
WithName("for-staggered-cyclic-A"),
WithExecutorOptions(
task.WithDir("testdata/cyclic"),
),
WithTask("for-staggered-cyclic-A"),
WithRunError(),
)

NewExecutorTest(t,
WithName("for-duplicate"),
WithExecutorOptions(
task.WithDir("testdata/cyclic"),
),
WithTask("for-duplicate"),
)

NewExecutorTest(t,
WithName("for-duplicate-cyclic"),
WithExecutorOptions(
task.WithDir("testdata/cyclic"),
),
WithTask("for-duplicate-cyclic"),
WithRunError(),
)

newFoo := func() {
err := os.WriteFile(filepathext.SmartJoin("testdata/cyclic", "foo.txt"), []byte("foo"), 0o666)
require.NoError(t, err)
}
newFoo()
NewExecutorTest(t,
WithName("sources"),
WithExecutorOptions(
task.WithDir("testdata/cyclic"),
),
WithTask("sources"),
)

newFoo()
NewExecutorTest(t,
WithName("sources-cyclic"),
WithExecutorOptions(
task.WithDir("testdata/cyclic"),
),
WithTask("sources-cyclic"),
WithRunError(),
)

newFoo()
NewExecutorTest(t,
WithName("sources-modify-cyclic"),
WithExecutorOptions(
task.WithDir("testdata/cyclic"),
),
WithTask("sources-modify-cyclic"),
)
}
2 changes: 0 additions & 2 deletions setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -251,10 +251,8 @@ func (e *Executor) setupDefaults() {
func (e *Executor) setupConcurrencyState() {
e.executionHashes = make(map[string]context.Context)

e.taskCallCount = make(map[string]*int32, e.Taskfile.Tasks.Len())
e.mkdirMutexMap = make(map[string]*sync.Mutex, e.Taskfile.Tasks.Len())
for k := range e.Taskfile.Tasks.Keys(nil) {
e.taskCallCount[k] = new(int32)
e.mkdirMutexMap[k] = &sync.Mutex{}
}

Expand Down
Loading
Loading