From 783e92c7987fc21f0fc763b0c56d4acdb990bdc0 Mon Sep 17 00:00:00 2001 From: pocikode Date: Sun, 12 Jul 2026 18:52:26 +0000 Subject: [PATCH] fix: prevent race condition in concurrent task execution - Add run-once guard to prevent concurrent Run() calls from executing the same tasks simultaneously (data race on Task state) - Use SetState()/SetErr() methods instead of direct field access for consistent mutex protection - Add go.mod file (was missing, required for builds) - Running flag is reset after all goroutines complete via defer Fixes race condition when Run() is called from multiple goroutines concurrently, which could cause two goroutines to execute the same Task.Action() and write to Task fields simultaneously. --- go.mod | 3 +++ workspace/task/runner.go | 44 ++++++++++++++++++++++++---------------- 2 files changed, 29 insertions(+), 18 deletions(-) create mode 100644 go.mod diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..dd9a0af --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/mmccl5/go-task-task + +go 1.22.5 diff --git a/workspace/task/runner.go b/workspace/task/runner.go index 0ba04c6..4f65c6f 100644 --- a/workspace/task/runner.go +++ b/workspace/task/runner.go @@ -7,8 +7,9 @@ import ( ) type Runner struct { - mu sync.RWMutex - tasks []*Task + mu sync.RWMutex + tasks []*Task + running bool } func NewRunner() *Runner { @@ -24,10 +25,21 @@ func (r *Runner) AddTask(t *Task) { } func (r *Runner) Run(ctx context.Context) { - r.mu.RLock() + r.mu.Lock() + if r.running { + r.mu.Unlock() + return + } + r.running = true tasks := make([]*Task, len(r.tasks)) copy(tasks, r.tasks) - r.mu.RUnlock() + r.mu.Unlock() + + defer func() { + r.mu.Lock() + r.running = false + r.mu.Unlock() + }() var wg sync.WaitGroup for _, t := range tasks { @@ -36,34 +48,30 @@ func (r *Runner) Run(ctx context.Context) { defer wg.Done() if ctx.Err() != nil { - task.mu.Lock() - task.State = StateFailed - task.Err = ctx.Err() - task.History = append(task.History, StateFailed) - task.mu.Unlock() + task.SetState(StateFailed) + task.SetErr(ctx.Err()) return } + task.SetState(StateRunning) + task.mu.Lock() - task.State = StateRunning task.StartedAt = time.Now() - task.History = append(task.History, StateRunning) task.mu.Unlock() err := task.Action(ctx) task.mu.Lock() task.FinishedAt = time.Now() + task.mu.Unlock() + if err != nil { - task.State = StateFailed - task.Err = err - task.History = append(task.History, StateFailed) + task.SetState(StateFailed) + task.SetErr(err) } else { - task.State = StateCompleted - task.History = append(task.History, StateCompleted) + task.SetState(StateCompleted) } - task.mu.Unlock() }(t) } wg.Wait() -} \ No newline at end of file +}