Skip to content
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

fix: omap race #1972

Merged
merged 1 commit into from
Dec 30, 2024
Merged
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
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
67 changes: 47 additions & 20 deletions taskfile/ast/include.go
Original file line number Diff line number Diff line change
@@ -1,34 +1,42 @@
package ast

import (
"sync"

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

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

// Include represents information about included taskfiles
type Include struct {
Namespace string
Taskfile string
Dir string
Optional bool
Internal bool
Aliases []string
Excludes []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
Excludes []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 @@ -39,30 +47,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 @@ -77,6 +101,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 @@ -207,6 +243,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
Loading