Skip to content

Commit

Permalink
fix: orderedmap race condition
Browse files Browse the repository at this point in the history
  • Loading branch information
pd93 committed Dec 29, 2024
1 parent 117259f commit 9d131f6
Show file tree
Hide file tree
Showing 5 changed files with 165 additions and 67 deletions.
4 changes: 2 additions & 2 deletions internal/templater/templater.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,11 +140,11 @@ func ReplaceVarsWithExtra(vars *ast.Vars, cache *Cache, extra map[string]any) *a
return nil
}

var newVars ast.Vars
newVars := ast.NewVars()
_ = vars.Range(func(k string, v ast.Var) error {
newVars.Set(k, ReplaceVarWithExtra(v, cache, extra))
return nil
})

return &newVars
return newVars
}
2 changes: 1 addition & 1 deletion task_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ func TestRequires(t *testing.T) {
buff.Reset()
require.NoError(t, e.Setup())

vars := &ast.Vars{}
vars := ast.NewVars()
vars.Set("foo", ast.Var{Value: "bar"})
require.NoError(t, e.Run(context.Background(), &ast.Call{
Task: "missing-var",
Expand Down
65 changes: 46 additions & 19 deletions taskfile/ast/include.go
Original file line number Diff line number Diff line change
@@ -1,32 +1,40 @@
package ast

import (
"sync"

"github.com/elliotchance/orderedmap/v2"
"gopkg.in/yaml.v3"

"github.com/go-task/task/v3/errors"
)

// Include represents information about included taskfiles
type Include struct {
Namespace string
Taskfile string
Dir string
Optional bool
Internal bool
Aliases []string
AdvancedImport bool
Vars *Vars
Flatten bool
}

// Includes represents information about included taskfiles
type Includes struct {
om *orderedmap.OrderedMap[string, *Include]
}

type IncludeElement orderedmap.Element[string, *Include]
type (
// Include represents information about included taskfiles
Include struct {
Namespace string
Taskfile string
Dir string
Optional bool
Internal bool
Aliases []string
AdvancedImport bool
Vars *Vars
Flatten bool
}
// Includes is an ordered map of namespaces to includes.
Includes struct {
om *orderedmap.OrderedMap[string, *Include]
mutex sync.RWMutex
}
// An IncludeElement is a key-value pair that is used for initializing an
// Includes structure.
IncludeElement orderedmap.Element[string, *Include]
)

// NewIncludes creates a new instance of Includes and initializes it with the
// provided set of elements, if any. The elements are added in the order they
// are passed.
func NewIncludes(els ...*IncludeElement) *Includes {
includes := &Includes{
om: orderedmap.NewOrderedMap[string, *Include](),
Expand All @@ -37,30 +45,46 @@ func NewIncludes(els ...*IncludeElement) *Includes {
return includes
}

// Len returns the number of includes in the Includes map.
func (includes *Includes) Len() int {
if includes == nil || includes.om == nil {
return 0
}
defer includes.mutex.RUnlock()
includes.mutex.RLock()
return includes.om.Len()
}

// Get returns the value the the include with the provided key and a boolean
// that indicates if the value was found or not. If the value is not found, the
// returned include is a zero value and the bool is false.
func (includes *Includes) Get(key string) (*Include, bool) {
if includes == nil || includes.om == nil {
return &Include{}, false
}
defer includes.mutex.RUnlock()
includes.mutex.RLock()
return includes.om.Get(key)
}

// Set sets the value of the include with the provided key to the provided
// value. If the include already exists, its value is updated. If the include
// does not exist, it is created.
func (includes *Includes) Set(key string, value *Include) bool {
if includes == nil {
includes = NewIncludes()
}
if includes.om == nil {
includes.om = orderedmap.NewOrderedMap[string, *Include]()
}
defer includes.mutex.Unlock()
includes.mutex.Lock()
return includes.om.Set(key, value)
}

// Range calls the provided function for each include in the map. The function
// receives the include's key and value as arguments. If the function returns
// an error, the iteration stops and the error is returned.
func (includes *Includes) Range(f func(k string, v *Include) error) error {
if includes == nil || includes.om == nil {
return nil
Expand All @@ -75,6 +99,9 @@ func (includes *Includes) Range(f func(k string, v *Include) error) error {

// UnmarshalYAML implements the yaml.Unmarshaler interface.
func (includes *Includes) UnmarshalYAML(node *yaml.Node) error {
if includes == nil || includes.om == nil {
*includes = *NewIncludes()
}
switch node.Kind {
case yaml.MappingNode:
// NOTE: orderedmap does not have an unmarshaler, so we have to decode
Expand Down
61 changes: 50 additions & 11 deletions taskfile/ast/tasks.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"slices"
"strings"
"sync"

"github.com/elliotchance/orderedmap/v2"
"gopkg.in/yaml.v3"
Expand All @@ -12,13 +13,25 @@ import (
"github.com/go-task/task/v3/internal/filepathext"
)

// Tasks represents a group of tasks
type Tasks struct {
om *orderedmap.OrderedMap[string, *Task]
}

type TaskElement orderedmap.Element[string, *Task]
type (
// Tasks is an ordered map of task names to Tasks.
Tasks struct {
om *orderedmap.OrderedMap[string, *Task]
mutex sync.RWMutex
}
// A TaskElement is a key-value pair that is used for initializing a Tasks
// structure.
TaskElement orderedmap.Element[string, *Task]
// MatchingTask represents a task that matches a given call. It includes the
// task itself and a list of wildcards that were matched.
MatchingTask struct {
Task *Task
Wildcards []string
}
)

// NewTasks creates a new instance of Tasks and initializes it with the provided
// set of elements, if any. The elements are added in the order they are passed.
func NewTasks(els ...*TaskElement) *Tasks {
tasks := &Tasks{
om: orderedmap.NewOrderedMap[string, *Task](),
Expand All @@ -29,30 +42,46 @@ func NewTasks(els ...*TaskElement) *Tasks {
return tasks
}

// Len returns the number of variables in the Tasks map.
func (tasks *Tasks) Len() int {
if tasks == nil || tasks.om == nil {
return 0
}
defer tasks.mutex.RUnlock()
tasks.mutex.RLock()
return tasks.om.Len()
}

// Get returns the value the the task with the provided key and a boolean that
// indicates if the value was found or not. If the value is not found, the
// returned task is a zero value and the bool is false.
func (tasks *Tasks) Get(key string) (*Task, bool) {
if tasks == nil || tasks.om == nil {
return &Task{}, false
}
defer tasks.mutex.RUnlock()
tasks.mutex.RLock()
return tasks.om.Get(key)
}

// Set sets the value of the task with the provided key to the provided value.
// If the task already exists, its value is updated. If the task does not exist,
// it is created.
func (tasks *Tasks) Set(key string, value *Task) bool {
if tasks == nil {
tasks = NewTasks()
}
if tasks.om == nil {
tasks.om = orderedmap.NewOrderedMap[string, *Task]()
}
defer tasks.mutex.Unlock()
tasks.mutex.Lock()
return tasks.om.Set(key, value)
}

// Range calls the provided function for each task in the map. The function
// receives the task's key and value as arguments. If the function returns an
// error, the iteration stops and the error is returned.
func (tasks *Tasks) Range(f func(k string, v *Task) error) error {
if tasks == nil || tasks.om == nil {
return nil
Expand All @@ -65,33 +94,38 @@ func (tasks *Tasks) Range(f func(k string, v *Task) error) error {
return nil
}

// Keys returns a slice of all the keys in the Tasks map.
func (tasks *Tasks) Keys() []string {
if tasks == nil {
return nil
}
defer tasks.mutex.RUnlock()
tasks.mutex.RLock()
var keys []string
for pair := tasks.om.Front(); pair != nil; pair = pair.Next() {
keys = append(keys, pair.Key)
}
return keys
}

// Values returns a slice of all the values in the Tasks map.
func (tasks *Tasks) Values() []*Task {
if tasks == nil {
return nil
}
defer tasks.mutex.RUnlock()
tasks.mutex.RLock()
var values []*Task
for pair := tasks.om.Front(); pair != nil; pair = pair.Next() {
values = append(values, pair.Value)
}
return values
}

type MatchingTask struct {
Task *Task
Wildcards []string
}

// FindMatchingTasks returns a list of tasks that match the given call. A task
// matches a call if its name is equal to the call's task name or if it matches
// a wildcard pattern. The function returns a list of MatchingTask structs, each
// containing a task and a list of wildcards that were matched.
func (t *Tasks) FindMatchingTasks(call *Call) []*MatchingTask {
if call == nil {
return nil
Expand All @@ -117,6 +151,8 @@ func (t *Tasks) FindMatchingTasks(call *Call) []*MatchingTask {
}

func (t1 *Tasks) Merge(t2 *Tasks, include *Include, includedTaskfileVars *Vars) error {
defer t2.mutex.RUnlock()
t2.mutex.RLock()
err := t2.Range(func(name string, v *Task) error {
// We do a deep copy of the task struct here to ensure that no data can
// be changed elsewhere once the taskfile is merged.
Expand Down Expand Up @@ -201,6 +237,9 @@ func (t1 *Tasks) Merge(t2 *Tasks, include *Include, includedTaskfileVars *Vars)
}

func (t *Tasks) UnmarshalYAML(node *yaml.Node) error {
if t == nil || t.om == nil {
*t = *NewTasks()
}
switch node.Kind {
case yaml.MappingNode:
// NOTE: orderedmap does not have an unmarshaler, so we have to decode
Expand Down
Loading

0 comments on commit 9d131f6

Please sign in to comment.