1
0
mirror of https://github.com/go-task/task.git synced 2025-03-19 21:17:46 +02:00

fix: orderedmap race condition (#1972)

This commit is contained in:
Pete Davison 2024-12-30 17:58:45 +00:00 committed by GitHub
parent 2965841eb7
commit fd3532812e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 166 additions and 68 deletions

View File

@ -140,11 +140,11 @@ func ReplaceVarsWithExtra(vars *ast.Vars, cache *Cache, extra map[string]any) *a
return nil return nil
} }
var newVars ast.Vars newVars := ast.NewVars()
_ = vars.Range(func(k string, v ast.Var) error { _ = vars.Range(func(k string, v ast.Var) error {
newVars.Set(k, ReplaceVarWithExtra(v, cache, extra)) newVars.Set(k, ReplaceVarWithExtra(v, cache, extra))
return nil return nil
}) })
return &newVars return newVars
} }

View File

@ -183,7 +183,7 @@ func TestRequires(t *testing.T) {
buff.Reset() buff.Reset()
require.NoError(t, e.Setup()) require.NoError(t, e.Setup())
vars := &ast.Vars{} vars := ast.NewVars()
vars.Set("foo", ast.Var{Value: "bar"}) vars.Set("foo", ast.Var{Value: "bar"})
require.NoError(t, e.Run(context.Background(), &ast.Call{ require.NoError(t, e.Run(context.Background(), &ast.Call{
Task: "missing-var", Task: "missing-var",

View File

@ -1,6 +1,8 @@
package ast package ast
import ( import (
"sync"
"github.com/elliotchance/orderedmap/v2" "github.com/elliotchance/orderedmap/v2"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
@ -8,8 +10,9 @@ import (
"github.com/go-task/task/v3/internal/deepcopy" "github.com/go-task/task/v3/internal/deepcopy"
) )
type (
// Include represents information about included taskfiles // Include represents information about included taskfiles
type Include struct { Include struct {
Namespace string Namespace string
Taskfile string Taskfile string
Dir string Dir string
@ -21,14 +24,19 @@ type Include struct {
Vars *Vars Vars *Vars
Flatten bool Flatten bool
} }
// Includes is an ordered map of namespaces to includes.
// Includes represents information about included taskfiles Includes struct {
type Includes struct {
om *orderedmap.OrderedMap[string, *Include] 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]
)
type 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 { func NewIncludes(els ...*IncludeElement) *Includes {
includes := &Includes{ includes := &Includes{
om: orderedmap.NewOrderedMap[string, *Include](), om: orderedmap.NewOrderedMap[string, *Include](),
@ -39,20 +47,31 @@ func NewIncludes(els ...*IncludeElement) *Includes {
return includes return includes
} }
// Len returns the number of includes in the Includes map.
func (includes *Includes) Len() int { func (includes *Includes) Len() int {
if includes == nil || includes.om == nil { if includes == nil || includes.om == nil {
return 0 return 0
} }
defer includes.mutex.RUnlock()
includes.mutex.RLock()
return includes.om.Len() 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) { func (includes *Includes) Get(key string) (*Include, bool) {
if includes == nil || includes.om == nil { if includes == nil || includes.om == nil {
return &Include{}, false return &Include{}, false
} }
defer includes.mutex.RUnlock()
includes.mutex.RLock()
return includes.om.Get(key) 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 { func (includes *Includes) Set(key string, value *Include) bool {
if includes == nil { if includes == nil {
includes = NewIncludes() includes = NewIncludes()
@ -60,9 +79,14 @@ func (includes *Includes) Set(key string, value *Include) bool {
if includes.om == nil { if includes.om == nil {
includes.om = orderedmap.NewOrderedMap[string, *Include]() includes.om = orderedmap.NewOrderedMap[string, *Include]()
} }
defer includes.mutex.Unlock()
includes.mutex.Lock()
return includes.om.Set(key, value) 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 { func (includes *Includes) Range(f func(k string, v *Include) error) error {
if includes == nil || includes.om == nil { if includes == nil || includes.om == nil {
return nil return nil
@ -77,6 +101,9 @@ func (includes *Includes) Range(f func(k string, v *Include) error) error {
// UnmarshalYAML implements the yaml.Unmarshaler interface. // UnmarshalYAML implements the yaml.Unmarshaler interface.
func (includes *Includes) UnmarshalYAML(node *yaml.Node) error { func (includes *Includes) UnmarshalYAML(node *yaml.Node) error {
if includes == nil || includes.om == nil {
*includes = *NewIncludes()
}
switch node.Kind { switch node.Kind {
case yaml.MappingNode: case yaml.MappingNode:
// NOTE: orderedmap does not have an unmarshaler, so we have to decode // NOTE: orderedmap does not have an unmarshaler, so we have to decode

View File

@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"slices" "slices"
"strings" "strings"
"sync"
"github.com/elliotchance/orderedmap/v2" "github.com/elliotchance/orderedmap/v2"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
@ -12,13 +13,25 @@ import (
"github.com/go-task/task/v3/internal/filepathext" "github.com/go-task/task/v3/internal/filepathext"
) )
// Tasks represents a group of tasks type (
type Tasks struct { // Tasks is an ordered map of task names to Tasks.
Tasks struct {
om *orderedmap.OrderedMap[string, *Task] 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
}
)
type TaskElement orderedmap.Element[string, *Task] // 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 { func NewTasks(els ...*TaskElement) *Tasks {
tasks := &Tasks{ tasks := &Tasks{
om: orderedmap.NewOrderedMap[string, *Task](), om: orderedmap.NewOrderedMap[string, *Task](),
@ -29,20 +42,31 @@ func NewTasks(els ...*TaskElement) *Tasks {
return tasks return tasks
} }
// Len returns the number of variables in the Tasks map.
func (tasks *Tasks) Len() int { func (tasks *Tasks) Len() int {
if tasks == nil || tasks.om == nil { if tasks == nil || tasks.om == nil {
return 0 return 0
} }
defer tasks.mutex.RUnlock()
tasks.mutex.RLock()
return tasks.om.Len() 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) { func (tasks *Tasks) Get(key string) (*Task, bool) {
if tasks == nil || tasks.om == nil { if tasks == nil || tasks.om == nil {
return &Task{}, false return &Task{}, false
} }
defer tasks.mutex.RUnlock()
tasks.mutex.RLock()
return tasks.om.Get(key) 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 { func (tasks *Tasks) Set(key string, value *Task) bool {
if tasks == nil { if tasks == nil {
tasks = NewTasks() tasks = NewTasks()
@ -50,9 +74,14 @@ func (tasks *Tasks) Set(key string, value *Task) bool {
if tasks.om == nil { if tasks.om == nil {
tasks.om = orderedmap.NewOrderedMap[string, *Task]() tasks.om = orderedmap.NewOrderedMap[string, *Task]()
} }
defer tasks.mutex.Unlock()
tasks.mutex.Lock()
return tasks.om.Set(key, value) 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 { func (tasks *Tasks) Range(f func(k string, v *Task) error) error {
if tasks == nil || tasks.om == nil { if tasks == nil || tasks.om == nil {
return nil return nil
@ -65,10 +94,13 @@ func (tasks *Tasks) Range(f func(k string, v *Task) error) error {
return nil return nil
} }
// Keys returns a slice of all the keys in the Tasks map.
func (tasks *Tasks) Keys() []string { func (tasks *Tasks) Keys() []string {
if tasks == nil { if tasks == nil {
return nil return nil
} }
defer tasks.mutex.RUnlock()
tasks.mutex.RLock()
var keys []string var keys []string
for pair := tasks.om.Front(); pair != nil; pair = pair.Next() { for pair := tasks.om.Front(); pair != nil; pair = pair.Next() {
keys = append(keys, pair.Key) keys = append(keys, pair.Key)
@ -76,10 +108,13 @@ func (tasks *Tasks) Keys() []string {
return keys return keys
} }
// Values returns a slice of all the values in the Tasks map.
func (tasks *Tasks) Values() []*Task { func (tasks *Tasks) Values() []*Task {
if tasks == nil { if tasks == nil {
return nil return nil
} }
defer tasks.mutex.RUnlock()
tasks.mutex.RLock()
var values []*Task var values []*Task
for pair := tasks.om.Front(); pair != nil; pair = pair.Next() { for pair := tasks.om.Front(); pair != nil; pair = pair.Next() {
values = append(values, pair.Value) values = append(values, pair.Value)
@ -87,11 +122,10 @@ func (tasks *Tasks) Values() []*Task {
return values return values
} }
type MatchingTask struct { // FindMatchingTasks returns a list of tasks that match the given call. A task
Task *Task // matches a call if its name is equal to the call's task name or if it matches
Wildcards []string // 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 { func (t *Tasks) FindMatchingTasks(call *Call) []*MatchingTask {
if call == nil { if call == nil {
return nil return nil
@ -117,6 +151,8 @@ func (t *Tasks) FindMatchingTasks(call *Call) []*MatchingTask {
} }
func (t1 *Tasks) Merge(t2 *Tasks, include *Include, includedTaskfileVars *Vars) error { 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 { 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 // We do a deep copy of the task struct here to ensure that no data can
// be changed elsewhere once the taskfile is merged. // be changed elsewhere once the taskfile is merged.
@ -207,6 +243,9 @@ func (t1 *Tasks) Merge(t2 *Tasks, include *Include, includedTaskfileVars *Vars)
} }
func (t *Tasks) UnmarshalYAML(node *yaml.Node) error { func (t *Tasks) UnmarshalYAML(node *yaml.Node) error {
if t == nil || t.om == nil {
*t = *NewTasks()
}
switch node.Kind { switch node.Kind {
case yaml.MappingNode: case yaml.MappingNode:
// NOTE: orderedmap does not have an unmarshaler, so we have to decode // NOTE: orderedmap does not have an unmarshaler, so we have to decode

View File

@ -2,6 +2,7 @@ package ast
import ( import (
"strings" "strings"
"sync"
"github.com/elliotchance/orderedmap/v2" "github.com/elliotchance/orderedmap/v2"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
@ -11,52 +12,74 @@ import (
"github.com/go-task/task/v3/internal/experiments" "github.com/go-task/task/v3/internal/experiments"
) )
// Vars is a string[string] variables map. type (
type Vars struct { // Vars is an ordered map of variable names to values.
Vars struct {
om *orderedmap.OrderedMap[string, Var] om *orderedmap.OrderedMap[string, Var]
mutex sync.RWMutex
} }
// A VarElement is a key-value pair that is used for initializing a Vars
// structure.
VarElement orderedmap.Element[string, Var]
)
type VarElement orderedmap.Element[string, Var] // NewVars creates a new instance of Vars and initializes it with the provided
// set of elements, if any. The elements are added in the order they are passed.
func NewVars(els ...*VarElement) *Vars { func NewVars(els ...*VarElement) *Vars {
vs := &Vars{ vars := &Vars{
om: orderedmap.NewOrderedMap[string, Var](), om: orderedmap.NewOrderedMap[string, Var](),
} }
for _, el := range els { for _, el := range els {
vs.Set(el.Key, el.Value) vars.Set(el.Key, el.Value)
} }
return vs return vars
} }
func (vs *Vars) Len() int { // Len returns the number of variables in the Vars map.
if vs == nil || vs.om == nil { func (vars *Vars) Len() int {
if vars == nil || vars.om == nil {
return 0 return 0
} }
return vs.om.Len() defer vars.mutex.RUnlock()
vars.mutex.RLock()
return vars.om.Len()
} }
func (vs *Vars) Get(key string) (Var, bool) { // Get returns the value the the variable with the provided key and a boolean
if vs == nil || vs.om == nil { // that indicates if the value was found or not. If the value is not found, the
// returned variable is a zero value and the bool is false.
func (vars *Vars) Get(key string) (Var, bool) {
if vars == nil || vars.om == nil {
return Var{}, false return Var{}, false
} }
return vs.om.Get(key) defer vars.mutex.RUnlock()
vars.mutex.RLock()
return vars.om.Get(key)
} }
func (vs *Vars) Set(key string, value Var) bool { // Set sets the value of the variable with the provided key to the provided
if vs == nil { // value. If the variable already exists, its value is updated. If the variable
vs = NewVars() // does not exist, it is created.
func (vars *Vars) Set(key string, value Var) bool {
if vars == nil {
vars = NewVars()
} }
if vs.om == nil { if vars.om == nil {
vs.om = orderedmap.NewOrderedMap[string, Var]() vars.om = orderedmap.NewOrderedMap[string, Var]()
} }
return vs.om.Set(key, value) defer vars.mutex.Unlock()
vars.mutex.Lock()
return vars.om.Set(key, value)
} }
func (vs *Vars) Range(f func(k string, v Var) error) error { // Range calls the provided function for each variable in the map. The function
if vs == nil || vs.om == nil { // receives the variable's key and value as arguments. If the function returns
// an error, the iteration stops and the error is returned.
func (vars *Vars) Range(f func(k string, v Var) error) error {
if vars == nil || vars.om == nil {
return nil return nil
} }
for pair := vs.om.Front(); pair != nil; pair = pair.Next() { for pair := vars.om.Front(); pair != nil; pair = pair.Next() {
if err := f(pair.Key, pair.Value); err != nil { if err := f(pair.Key, pair.Value); err != nil {
return err return err
} }
@ -64,11 +87,13 @@ func (vs *Vars) Range(f func(k string, v Var) error) error {
return nil return nil
} }
// ToCacheMap converts Vars to a map containing only the static // ToCacheMap converts Vars to an unordered map containing only the static
// variables // variables
func (vs *Vars) ToCacheMap() (m map[string]any) { func (vars *Vars) ToCacheMap() (m map[string]any) {
m = make(map[string]any, vs.Len()) defer vars.mutex.RUnlock()
for pair := vs.om.Front(); pair != nil; pair = pair.Next() { vars.mutex.RLock()
m = make(map[string]any, vars.Len())
for pair := vars.om.Front(); pair != nil; pair = pair.Next() {
if pair.Value.Sh != nil && *pair.Value.Sh != "" { if pair.Value.Sh != nil && *pair.Value.Sh != "" {
// Dynamic variable is not yet resolved; trigger // Dynamic variable is not yet resolved; trigger
// <no value> to be used in templates. // <no value> to be used in templates.
@ -83,31 +108,38 @@ func (vs *Vars) ToCacheMap() (m map[string]any) {
return return
} }
// Wrapper around OrderedMap.Merge to ensure we don't get nil pointer errors // Merge loops over other and merges it values with the variables in vars. If
func (vs *Vars) Merge(other *Vars, include *Include) { // the include parameter is not nil and its it is an advanced import, the
if vs == nil || vs.om == nil || other == nil { // directory is set set to the value of the include parameter.
func (vars *Vars) Merge(other *Vars, include *Include) {
if vars == nil || vars.om == nil || other == nil {
return return
} }
defer other.mutex.RUnlock()
other.mutex.RLock()
for pair := other.om.Front(); pair != nil; pair = pair.Next() { for pair := other.om.Front(); pair != nil; pair = pair.Next() {
if include != nil && include.AdvancedImport { if include != nil && include.AdvancedImport {
pair.Value.Dir = include.Dir pair.Value.Dir = include.Dir
} }
vs.om.Set(pair.Key, pair.Value) vars.om.Set(pair.Key, pair.Value)
} }
} }
// DeepCopy creates a new instance of Vars and copies
// data by value from the source struct.
func (vs *Vars) DeepCopy() *Vars { func (vs *Vars) DeepCopy() *Vars {
if vs == nil { if vs == nil {
return nil return nil
} }
defer vs.mutex.RUnlock()
vs.mutex.RLock()
return &Vars{ return &Vars{
om: deepcopy.OrderedMap(vs.om), om: deepcopy.OrderedMap(vs.om),
} }
} }
func (vs *Vars) UnmarshalYAML(node *yaml.Node) error { func (vs *Vars) UnmarshalYAML(node *yaml.Node) error {
if vs == nil || vs.om == nil {
*vs = *NewVars()
}
vs.om = orderedmap.NewOrderedMap[string, Var]() vs.om = orderedmap.NewOrderedMap[string, Var]()
switch node.Kind { switch node.Kind {
case yaml.MappingNode: case yaml.MappingNode: