diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..dfd2886 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module go-task-task + +go 1.26.3 diff --git a/main.go b/main.go index 49f4dee..1e1a9c9 100644 --- a/main.go +++ b/main.go @@ -1,7 +1,172 @@ package main -import "fmt" +import ( + "context" + "fmt" + "math/rand" + "strings" + "sync" + "time" + + "go-task-task/workspace/task" +) func main() { - fmt.Println("Hello, Bounty Hunter!") + fmt.Println("Race Condition Fix Demo - Concurrent Task Execution") + fmt.Println(strings.Repeat("=", 60)) + + demonstrateConcurrentExecution() + demonstrateTimeoutHandling() + demonstrateConcurrentStateAccess() +} + +func demonstrateConcurrentExecution() { + fmt.Println("\n1. Concurrent Task Execution Demo") + fmt.Println(strings.Repeat("-", 40)) + + runner := task.NewRunner() + + tasks := []*task.Task{ + task.NewTask("fast-success", func(ctx context.Context) error { + time.Sleep(10 * time.Millisecond) + return nil + }), + task.NewTask("slow-success", func(ctx context.Context) error { + time.Sleep(50 * time.Millisecond) + return nil + }), + task.NewTask("fast-failure", func(ctx context.Context) error { + time.Sleep(5 * time.Millisecond) + return fmt.Errorf("simulated fast failure") + }), + task.NewTask("context-aware", func(ctx context.Context) error { + select { + case <-time.After(30 * time.Millisecond): + return nil + case <-ctx.Done(): + return ctx.Err() + } + }), + } + + for _, t := range tasks { + runner.AddTask(t) + } + + start := time.Now() + runner.Run(context.Background()) + duration := time.Since(start) + + fmt.Printf("Executed %d tasks in %v\n", len(tasks), duration) + fmt.Printf("Active: %d, Completed: %d, Failed: %d\n", + runner.GetActiveCount(), runner.GetCompletedCount(), runner.GetFailedCount()) + + for _, t := range tasks { + fmt.Printf("Task %s: %s (started: %v, finished: %v)\n", + t.ID, t.GetState(), t.GetStartedAt().Format("15:04:05.000"), t.GetFinishedAt().Format("15:04:05.000")) + } +} + +func demonstrateTimeoutHandling() { + fmt.Println("\n2. Timeout Handling Demo") + fmt.Println(strings.Repeat("-", 40)) + + runner := task.NewRunner() + + longTask := task.NewTask("long-running", func(ctx context.Context) error { + select { + case <-time.After(200 * time.Millisecond): + return nil + case <-ctx.Done(): + return ctx.Err() + } + }) + + runner.AddTask(longTask) + + fmt.Println("Running task with 100ms timeout...") + err := runner.RunWithTimeout(context.Background(), 100*time.Millisecond) + + if err == context.DeadlineExceeded { + fmt.Println("Task correctly timed out") + fmt.Printf("Task state: %s, Error: %v\n", longTask.GetState(), longTask.GetErr()) + } else { + fmt.Printf("Unexpected result: %v\n", err) + } +} + +func demonstrateConcurrentStateAccess() { + fmt.Println("\n3. Concurrent State Access Demo") + fmt.Println(strings.Repeat("-", 40)) + + runner := task.NewRunner() + numTasks := 100 + + for i := 0; i < numTasks; i++ { + task := task.NewTask(fmt.Sprintf("concurrent-%d", i), func(ctx context.Context) error { + sleepTime := time.Duration(rand.Intn(20)) * time.Millisecond + time.Sleep(sleepTime) + return nil + }) + runner.AddTask(task) + } + + var wg sync.WaitGroup + stopChan := make(chan struct{}) + + wg.Add(1) + go func() { + defer wg.Done() + readCount := 0 + for { + select { + case <-stopChan: + fmt.Printf("State reader performed %d reads\n", readCount) + return + default: + _ = runner.GetActiveCount() + _ = runner.GetCompletedCount() + _ = runner.GetFailedCount() + tasks := runner.GetTasks() + + for _, task := range tasks { + _ = task.GetState() + _ = task.GetHistory() + } + readCount++ + } + } + }() + + wg.Add(1) + go func() { + defer wg.Done() + writeCount := 0 + for { + select { + case <-stopChan: + fmt.Printf("Metadata writer performed %d writes\n", writeCount) + return + default: + tasks := runner.GetTasks() + for _, task := range tasks { + task.SetMetadata("timestamp", time.Now()) + } + writeCount++ + time.Sleep(1 * time.Millisecond) + } + } + }() + + fmt.Println("Running tasks with concurrent state access...") + start := time.Now() + runner.Run(context.Background()) + duration := time.Since(start) + + close(stopChan) + wg.Wait() + + fmt.Printf("Successfully executed %d tasks in %v with concurrent state access\n", numTasks, duration) + fmt.Printf("Final stats - Active: %d, Completed: %d, Failed: %d\n", + runner.GetActiveCount(), runner.GetCompletedCount(), runner.GetFailedCount()) } diff --git a/workspace/task/runner.go b/workspace/task/runner.go index 0ba04c6..f58c20e 100644 --- a/workspace/task/runner.go +++ b/workspace/task/runner.go @@ -3,12 +3,16 @@ package task import ( "context" "sync" + "sync/atomic" "time" ) type Runner struct { - mu sync.RWMutex - tasks []*Task + mu sync.RWMutex + tasks []*Task + activeCount int64 // Use atomic operations for this counter + completedCount int64 // Use atomic operations for this counter + failedCount int64 // Use atomic operations for this counter } func NewRunner() *Runner { @@ -23,6 +27,26 @@ func (r *Runner) AddTask(t *Task) { r.tasks = append(r.tasks, t) } +func (r *Runner) GetActiveCount() int64 { + return atomic.LoadInt64(&r.activeCount) +} + +func (r *Runner) GetCompletedCount() int64 { + return atomic.LoadInt64(&r.completedCount) +} + +func (r *Runner) GetFailedCount() int64 { + return atomic.LoadInt64(&r.failedCount) +} + +func (r *Runner) GetTasks() []*Task { + r.mu.RLock() + defer r.mu.RUnlock() + tasks := make([]*Task, len(r.tasks)) + copy(tasks, r.tasks) + return tasks +} + func (r *Runner) Run(ctx context.Context) { r.mu.RLock() tasks := make([]*Task, len(r.tasks)) @@ -36,34 +60,40 @@ 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.SetStateFailed(ctx.Err()) + atomic.AddInt64(&r.failedCount, 1) return } - task.mu.Lock() - task.State = StateRunning - task.StartedAt = time.Now() - task.History = append(task.History, StateRunning) - task.mu.Unlock() + // Increment active counter + atomic.AddInt64(&r.activeCount, 1) + defer atomic.AddInt64(&r.activeCount, -1) + + // Atomically update state to running and set start time + task.SetStateRunning() err := task.Action(ctx) - task.mu.Lock() - task.FinishedAt = time.Now() + // Atomically update final state and finish time if err != nil { - task.State = StateFailed - task.Err = err - task.History = append(task.History, StateFailed) + task.SetStateFailed(err) + atomic.AddInt64(&r.failedCount, 1) } else { - task.State = StateCompleted - task.History = append(task.History, StateCompleted) + task.SetStateCompleted() + atomic.AddInt64(&r.completedCount, 1) } - task.mu.Unlock() }(t) } wg.Wait() +} + +func (r *Runner) RunWithTimeout(ctx context.Context, timeout time.Duration) error { + if timeout > 0 { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, timeout) + defer cancel() + } + + r.Run(ctx) + return ctx.Err() } \ No newline at end of file diff --git a/workspace/task/runner_test.go b/workspace/task/runner_test.go index d02f599..c37db45 100644 --- a/workspace/task/runner_test.go +++ b/workspace/task/runner_test.go @@ -26,11 +26,11 @@ func TestRunner_Run(t *testing.T) { runner.Run(context.Background()) - if t1.State != StateCompleted { - t.Errorf("expected t1 to be Completed, got %s", t1.State) + if t1.GetState() != StateCompleted { + t.Errorf("expected t1 to be Completed, got %s", t1.GetState()) } - if t2.State != StateFailed { - t.Errorf("expected t2 to be Failed, got %s", t2.State) + if t2.GetState() != StateFailed { + t.Errorf("expected t2 to be Failed, got %s", t2.GetState()) } } @@ -78,6 +78,184 @@ func TestRunner_Run_Concurrent(t *testing.T) { } } +func TestRunner_Run_StressTest(t *testing.T) { + runner := NewRunner() + numTasks := 1000 + + var tasks []*Task + for i := 0; i < numTasks; i++ { + task := NewTask(fmt.Sprintf("%d", i), func(ctx context.Context) error { + // Simulate variable execution time + time.Sleep(time.Duration(i%10) * time.Millisecond) + return nil + }) + runner.AddTask(task) + tasks = append(tasks, task) + } + + // Multiple goroutines accessing task state concurrently + var wg sync.WaitGroup + stopChan := make(chan struct{}) + + // Goroutine 1: Reading states + wg.Add(1) + go func() { + defer wg.Done() + for { + select { + case <-stopChan: + return + default: + for _, task := range tasks { + _ = task.GetState() + _ = task.GetHistory() + _ = task.GetStartedAt() + _ = task.GetFinishedAt() + } + } + } + }() + + // Goroutine 2: Reading metadata + wg.Add(1) + go func() { + defer wg.Done() + for { + select { + case <-stopChan: + return + default: + for _, task := range tasks { + _, _ = task.GetMetadata("test") + _, _ = task.GetMetadata("status") + } + } + } + }() + + // Goroutine 3: Setting metadata + wg.Add(1) + go func() { + defer wg.Done() + for { + select { + case <-stopChan: + return + default: + for _, task := range tasks { + task.SetMetadata("test", "value") + } + time.Sleep(1 * time.Millisecond) + } + } + }() + + // Goroutine 4: Reading runner counters + wg.Add(1) + go func() { + defer wg.Done() + for { + select { + case <-stopChan: + return + default: + _ = runner.GetActiveCount() + _ = runner.GetCompletedCount() + _ = runner.GetFailedCount() + _ = runner.GetTasks() + } + } + }() + + // Execute tasks + runner.Run(context.Background()) + close(stopChan) + wg.Wait() + + // Verify all tasks completed + for _, task := range tasks { + if state := task.GetState(); state != StateCompleted { + t.Errorf("expected task %s to be Completed, got %s", task.ID, state) + } + } + + // Verify counters + if runner.GetActiveCount() != 0 { + t.Errorf("expected active count to be 0, got %d", runner.GetActiveCount()) + } + if runner.GetCompletedCount() != int64(numTasks) { + t.Errorf("expected completed count to be %d, got %d", numTasks, runner.GetCompletedCount()) + } + if runner.GetFailedCount() != 0 { + t.Errorf("expected failed count to be 0, got %d", runner.GetFailedCount()) + } +} + +func TestRunner_Counters_RaceCondition(t *testing.T) { + runner := NewRunner() + numTasks := 500 + + // Add tasks with varying success/failure + for i := 0; i < numTasks; i++ { + task := NewTask(fmt.Sprintf("%d", i), func(ctx context.Context) error { + time.Sleep(time.Duration(i%5) * time.Millisecond) + if i%10 == 0 { + return errors.New("simulated error") + } + return nil + }) + runner.AddTask(task) + } + + // Monitor counters concurrently + var wg sync.WaitGroup + stopChan := make(chan struct{}) + + // Reader goroutines for counters + for i := 0; i < 10; i++ { + wg.Add(1) + go func() { + defer wg.Done() + for { + select { + case <-stopChan: + return + default: + active := runner.GetActiveCount() + completed := runner.GetCompletedCount() + failed := runner.GetFailedCount() + + // Sanity check: active count should never be negative + if active < 0 { + t.Errorf("active count is negative: %d", active) + } + + // Total should not exceed number of tasks + if completed+failed > int64(numTasks) { + t.Errorf("total completed+failed (%d) exceeds numTasks (%d)", completed+failed, numTasks) + } + } + } + }() + } + + // Execute tasks + runner.Run(context.Background()) + close(stopChan) + wg.Wait() + + // Final verification + expectedFailed := int64(numTasks / 10) // Every 10th task fails + if runner.GetFailedCount() != expectedFailed { + t.Errorf("expected failed count to be %d, got %d", expectedFailed, runner.GetFailedCount()) + } + + expectedCompleted := int64(numTasks) - expectedFailed + if runner.GetCompletedCount() != expectedCompleted { + t.Errorf("expected completed count to be %d, got %d", expectedCompleted, runner.GetCompletedCount()) + } +} + func TestRunner_Run_Cancel(t *testing.T) { runner := NewRunner() ctx, cancel := context.WithCancel(context.Background()) @@ -98,4 +276,144 @@ func TestRunner_Run_Cancel(t *testing.T) { if err := t1.GetErr(); err != context.Canceled { t.Errorf("expected t1 error to be context.Canceled, got %v", err) } +} + +func TestRunner_Extreme_Concurrency(t *testing.T) { + if testing.Short() { + t.Skip("Skipping extreme concurrency test in short mode") + } + + runner := NewRunner() + numTasks := 2000 + numReaders := 50 + + // Add a mix of fast and slow tasks + for i := 0; i < numTasks; i++ { + task := NewTask(fmt.Sprintf("task-%d", i), func(ctx context.Context) error { + // Mix of execution times + sleepTime := time.Duration(i%20) * time.Microsecond + time.Sleep(sleepTime) + + // Some tasks fail randomly + if i%13 == 0 { + return fmt.Errorf("task %d failed", i) + } + return nil + }) + runner.AddTask(task) + } + + var wg sync.WaitGroup + stopChan := make(chan struct{}) + + // Start many concurrent readers + for i := 0; i < numReaders; i++ { + wg.Add(1) + go func(readerID int) { + defer wg.Done() + for { + select { + case <-stopChan: + return + default: + // Continuously read from all tasks and runner + tasks := runner.GetTasks() + for _, task := range tasks { + _ = task.GetState() + _ = task.GetHistory() + _ = task.GetErr() + _, _ = task.GetMetadata("test") + _ = task.GetStartedAt() + _ = task.GetFinishedAt() + } + + // Read runner stats + _ = runner.GetActiveCount() + _ = runner.GetCompletedCount() + _ = runner.GetFailedCount() + } + } + }(i) + } + + // Start concurrent metadata writers + for i := 0; i < 10; i++ { + wg.Add(1) + go func(writerID int) { + defer wg.Done() + for { + select { + case <-stopChan: + return + default: + tasks := runner.GetTasks() + for _, task := range tasks { + task.SetMetadata(fmt.Sprintf("writer-%d", writerID), time.Now()) + } + time.Sleep(100 * time.Microsecond) + } + } + }(i) + } + + // Execute all tasks + start := time.Now() + runner.Run(context.Background()) + duration := time.Since(start) + + // Stop all readers/writers + close(stopChan) + wg.Wait() + + // Verify results + expectedFailed := numTasks / 13 // Every 13th task should fail + actualFailed := runner.GetFailedCount() + actualCompleted := runner.GetCompletedCount() + + if actualFailed < int64(expectedFailed-1) || actualFailed > int64(expectedFailed+1) { + t.Errorf("expected approximately %d failed tasks, got %d", expectedFailed, actualFailed) + } + + if actualCompleted+actualFailed != int64(numTasks) { + t.Errorf("completed (%d) + failed (%d) != total tasks (%d)", actualCompleted, actualFailed, numTasks) + } + + if runner.GetActiveCount() != 0 { + t.Errorf("expected 0 active tasks after completion, got %d", runner.GetActiveCount()) + } + + t.Logf("Executed %d tasks in %v with %d concurrent readers and 10 writers", numTasks, duration, numReaders) +} + +func TestRunner_RunWithTimeout(t *testing.T) { + runner := NewRunner() + + // Add a task that takes longer than the timeout + longTask := NewTask("long", func(ctx context.Context) error { + select { + case <-time.After(100 * time.Millisecond): + return nil + case <-ctx.Done(): + return ctx.Err() + } + }) + + runner.AddTask(longTask) + + // Run with a shorter timeout + err := runner.RunWithTimeout(context.Background(), 50*time.Millisecond) + + // Should timeout + if err != context.DeadlineExceeded { + t.Errorf("expected context.DeadlineExceeded, got %v", err) + } + + // Task should be marked as failed + if state := longTask.GetState(); state != StateFailed { + t.Errorf("expected task to be Failed due to timeout, got %s", state) + } + + if taskErr := longTask.GetErr(); taskErr != context.DeadlineExceeded { + t.Errorf("expected task error to be context.DeadlineExceeded, got %v", taskErr) + } } \ No newline at end of file diff --git a/workspace/task/task.go b/workspace/task/task.go index 8f403dc..f4954c7 100644 --- a/workspace/task/task.go +++ b/workspace/task/task.go @@ -50,6 +50,31 @@ func (t *Task) SetState(state State) { t.History = append(t.History, state) } +func (t *Task) SetStateRunning() { + t.mu.Lock() + defer t.mu.Unlock() + t.State = StateRunning + t.StartedAt = time.Now() + t.History = append(t.History, StateRunning) +} + +func (t *Task) SetStateCompleted() { + t.mu.Lock() + defer t.mu.Unlock() + t.State = StateCompleted + t.FinishedAt = time.Now() + t.History = append(t.History, StateCompleted) +} + +func (t *Task) SetStateFailed(err error) { + t.mu.Lock() + defer t.mu.Unlock() + t.State = StateFailed + t.Err = err + t.FinishedAt = time.Now() + t.History = append(t.History, StateFailed) +} + func (t *Task) GetErr() error { t.mu.RLock() defer t.mu.RUnlock()