From 1bb21b670369d02668ab188099367a19ec3d5350 Mon Sep 17 00:00:00 2001 From: "Xinyue.Wang" Date: Mon, 26 Jun 2023 13:54:34 -0700 Subject: [PATCH 1/7] add wait any --- wait_any.go | 35 +++++++++++++++++++++ wait_any_test.go | 80 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 115 insertions(+) create mode 100644 wait_any.go create mode 100644 wait_any_test.go diff --git a/wait_any.go b/wait_any.go new file mode 100644 index 0000000..74d4084 --- /dev/null +++ b/wait_any.go @@ -0,0 +1,35 @@ +package asynctask + +import ( + "context" + "fmt" +) + +// WaitAny block current thread til any of task finished. +// first error from any tasks passed in will be returned +// first task end without error will end wait and return nil +func WaitAny(ctx context.Context, tasks ...Waitable) error { + tasksCount := len(tasks) + if tasksCount == 0 { + return nil + } + + // tried to close channel before exit this func, + // but it's complicated with routines, and we don't want to delay the return. + // per https://stackoverflow.com/questions/8593645/is-it-ok-to-leave-a-channel-open, its ok to leave channel open, eventually it will be garbage collected. + // this assumes the tasks eventually finish, otherwise we will have a routine leak. + errorCh := make(chan error, tasksCount) + + for _, tsk := range tasks { + go waitOne(ctx, tsk, errorCh) + } + + for { + select { + case err := <-errorCh: + return err + case <-ctx.Done(): + return fmt.Errorf("WaitAny %w", ctx.Err()) + } + } +} diff --git a/wait_any_test.go b/wait_any_test.go new file mode 100644 index 0000000..f40acc6 --- /dev/null +++ b/wait_any_test.go @@ -0,0 +1,80 @@ +package asynctask_test + +import ( + "fmt" + "testing" + "time" + + "github.com/Azure/go-asynctask" + "github.com/stretchr/testify/assert" +) + +func TestWaitAny(t *testing.T) { + t.Parallel() + ctx, cancelTaskExecution := newTestContextWithTimeout(t, 2*time.Second) + + start := time.Now() + countingTsk3 := asynctask.Start(ctx, getCountingTask(10, "countingPer2ms", 2*time.Millisecond)) + result := "something" + completedTsk := asynctask.NewCompletedTask(&result) + + err := asynctask.WaitAny(ctx, countingTsk3, completedTsk) + elapsed := time.Since(start) + assert.NoError(t, err) + // should finish after right away + assert.True(t, elapsed < 2*time.Millisecond, fmt.Sprintf("actually elapsed: %v", elapsed)) + + countingTsk1 := asynctask.Start(ctx, getCountingTask(10, "countingPer40ms", 40*time.Millisecond)) + countingTsk2 := asynctask.Start(ctx, getCountingTask(10, "countingPer20ms", 20*time.Millisecond)) + countingTsk3 = asynctask.Start(ctx, getCountingTask(10, "countingPer2ms", 2*time.Millisecond)) + start = time.Now() + err = asynctask.WaitAny(ctx, countingTsk1, countingTsk2, countingTsk3) + elapsed = time.Since(start) + assert.NoError(t, err) + cancelTaskExecution() + + // should finish right after countingTsk3 + assert.True(t, elapsed >= 20*time.Millisecond && elapsed < 200*time.Millisecond, fmt.Sprintf("actually elapsed: %v", elapsed)) +} + +func TestWaitAnyErrorCase(t *testing.T) { + t.Parallel() + ctx, cancelTaskExecution := newTestContextWithTimeout(t, 3*time.Second) + + start := time.Now() + errorTsk := asynctask.Start(ctx, getErrorTask("expected error", 10*time.Millisecond)) + result := "something" + completedTsk := asynctask.NewCompletedTask(&result) + err := asynctask.WaitAny(ctx, errorTsk, completedTsk) + assert.NoError(t, err) + elapsed := time.Since(start) + // should finish after right away + assert.True(t, elapsed < 20*time.Millisecond, fmt.Sprintf("actually elapsed: %v", elapsed)) + + countingTsk := asynctask.Start(ctx, getCountingTask(10, "countingPer40ms", 40*time.Millisecond)) + errorTsk = asynctask.Start(ctx, getErrorTask("expected error", 10*time.Millisecond)) + panicTsk := asynctask.Start(ctx, getPanicTask(20*time.Millisecond)) + err = asynctask.WaitAny(ctx, countingTsk, errorTsk, panicTsk) + assert.Error(t, err) + completedTskState := completedTsk.State() + assert.Equal(t, asynctask.StateCompleted, completedTskState, "completed task should finished") + + countingTskState := countingTsk.State() + panicTskState := panicTsk.State() + errTskState := errorTsk.State() + elapsed = time.Since(start) + cancelTaskExecution() // all assertion variable captured, cancel counting task + + assert.Equal(t, "expected error", err.Error(), "expecting first error") + // should only finish after longest task. + assert.True(t, elapsed >= 10*time.Millisecond && elapsed < 20*time.Millisecond, fmt.Sprintf("actually elapsed: %v", elapsed)) + + assert.Equal(t, asynctask.StateRunning, countingTskState, "countingTask should NOT finished") + assert.Equal(t, asynctask.StateFailed, errTskState, "error task should failed") + assert.Equal(t, asynctask.StateRunning, panicTskState, "panic task should Not failed") + + // counting task do testing.Logf in another go routine + // while testing.Logf would cause DataRace error when test is already finished: https://github.com/golang/go/issues/40343 + // wait minor time for the go routine to finish. + time.Sleep(1 * time.Millisecond) +} From ddf77a56d49f3f96463ffabe0675b1c286396eff Mon Sep 17 00:00:00 2001 From: "Xinyue.Wang" Date: Tue, 27 Jun 2023 15:50:05 -0700 Subject: [PATCH 2/7] add FailFast --- wait_any.go | 42 +++++++++++++++++++++++++++++++++++++--- wait_any_test.go | 50 ++++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 85 insertions(+), 7 deletions(-) diff --git a/wait_any.go b/wait_any.go index 74d4084..f51a9ce 100644 --- a/wait_any.go +++ b/wait_any.go @@ -5,15 +5,25 @@ import ( "fmt" ) +// WaitAnyOptions defines options for WaitAny function +type WaitAnyOptions struct { + // FailFast set to true will indicate WaitAny to return on first error it sees. + FailFast bool +} + // WaitAny block current thread til any of task finished. -// first error from any tasks passed in will be returned +// first error from any tasks passed in will be returned if FailFast is set. // first task end without error will end wait and return nil -func WaitAny(ctx context.Context, tasks ...Waitable) error { +func WaitAny(ctx context.Context, options *WaitAnyOptions, tasks ...Waitable) error { tasksCount := len(tasks) if tasksCount == 0 { return nil } + if options == nil { + options = &WaitAnyOptions{} + } + // tried to close channel before exit this func, // but it's complicated with routines, and we don't want to delay the return. // per https://stackoverflow.com/questions/8593645/is-it-ok-to-leave-a-channel-open, its ok to leave channel open, eventually it will be garbage collected. @@ -24,12 +34,38 @@ func WaitAny(ctx context.Context, tasks ...Waitable) error { go waitOne(ctx, tsk, errorCh) } + runningTasks := tasksCount + var errList []error for { select { case err := <-errorCh: - return err + runningTasks-- + if err != nil { + // return immediately after receive first error if FailFast is set. + if options.FailFast { + return err + } + errList = append(errList, err) + } else { + return nil + } case <-ctx.Done(): return fmt.Errorf("WaitAny %w", ctx.Err()) } + + // are we finished yet? + if runningTasks == 0 { + break + } + } + + // we have at least 1 error when FailFast is not set, return first one. + // caller can get error for individual task by using Wait(), + // it would return immediately after this WaitAny() + if len(errList) > 0 { + return errList[0] } + + // no error at all. + return nil } diff --git a/wait_any_test.go b/wait_any_test.go index f40acc6..0a7e355 100644 --- a/wait_any_test.go +++ b/wait_any_test.go @@ -18,7 +18,7 @@ func TestWaitAny(t *testing.T) { result := "something" completedTsk := asynctask.NewCompletedTask(&result) - err := asynctask.WaitAny(ctx, countingTsk3, completedTsk) + err := asynctask.WaitAny(ctx, nil, countingTsk3, completedTsk) elapsed := time.Since(start) assert.NoError(t, err) // should finish after right away @@ -28,7 +28,7 @@ func TestWaitAny(t *testing.T) { countingTsk2 := asynctask.Start(ctx, getCountingTask(10, "countingPer20ms", 20*time.Millisecond)) countingTsk3 = asynctask.Start(ctx, getCountingTask(10, "countingPer2ms", 2*time.Millisecond)) start = time.Now() - err = asynctask.WaitAny(ctx, countingTsk1, countingTsk2, countingTsk3) + err = asynctask.WaitAny(ctx, &asynctask.WaitAnyOptions{FailFast: true}, countingTsk1, countingTsk2, countingTsk3) elapsed = time.Since(start) assert.NoError(t, err) cancelTaskExecution() @@ -45,7 +45,49 @@ func TestWaitAnyErrorCase(t *testing.T) { errorTsk := asynctask.Start(ctx, getErrorTask("expected error", 10*time.Millisecond)) result := "something" completedTsk := asynctask.NewCompletedTask(&result) - err := asynctask.WaitAny(ctx, errorTsk, completedTsk) + err := asynctask.WaitAny(ctx, nil, errorTsk, completedTsk) + assert.NoError(t, err) + elapsed := time.Since(start) + // should finish after right away + assert.True(t, elapsed < 20*time.Millisecond, fmt.Sprintf("actually elapsed: %v", elapsed)) + completedTskState := completedTsk.State() + assert.Equal(t, asynctask.StateCompleted, completedTskState, "completed task should finished") + + countingTsk := asynctask.Start(ctx, getCountingTask(10, "countingPer40ms", 40*time.Millisecond)) + errorTsk = asynctask.Start(ctx, getErrorTask("expected error", 10*time.Millisecond)) + panicTsk := asynctask.Start(ctx, getPanicTask(20*time.Millisecond)) + err = asynctask.WaitAny(ctx, nil, countingTsk, errorTsk, panicTsk) + // there is a succeed task + assert.NoError(t, err) + elapsed = time.Since(start) + + countingTskState := countingTsk.State() + panicTskState := panicTsk.State() + errTskState := errorTsk.State() + cancelTaskExecution() // all assertion variable captured, cancel counting task + + // should only finish after longest task. + assert.True(t, elapsed >= 40*10*time.Millisecond, fmt.Sprintf("actually elapsed: %v", elapsed)) + + assert.Equal(t, asynctask.StateCompleted, countingTskState, "countingTask should NOT finished") + assert.Equal(t, asynctask.StateFailed, errTskState, "error task should failed") + assert.Equal(t, asynctask.StateFailed, panicTskState, "panic task should Not failed") + + // counting task do testing.Logf in another go routine + // while testing.Logf would cause DataRace error when test is already finished: https://github.com/golang/go/issues/40343 + // wait minor time for the go routine to finish. + time.Sleep(1 * time.Millisecond) +} + +func TestWaitAnyErrorWithFailFastCase(t *testing.T) { + t.Parallel() + ctx, cancelTaskExecution := newTestContextWithTimeout(t, 3*time.Second) + + start := time.Now() + errorTsk := asynctask.Start(ctx, getErrorTask("expected error", 10*time.Millisecond)) + result := "something" + completedTsk := asynctask.NewCompletedTask(&result) + err := asynctask.WaitAny(ctx, &asynctask.WaitAnyOptions{FailFast: true}, errorTsk, completedTsk) assert.NoError(t, err) elapsed := time.Since(start) // should finish after right away @@ -54,7 +96,7 @@ func TestWaitAnyErrorCase(t *testing.T) { countingTsk := asynctask.Start(ctx, getCountingTask(10, "countingPer40ms", 40*time.Millisecond)) errorTsk = asynctask.Start(ctx, getErrorTask("expected error", 10*time.Millisecond)) panicTsk := asynctask.Start(ctx, getPanicTask(20*time.Millisecond)) - err = asynctask.WaitAny(ctx, countingTsk, errorTsk, panicTsk) + err = asynctask.WaitAny(ctx, &asynctask.WaitAnyOptions{FailFast: true}, countingTsk, errorTsk, panicTsk) assert.Error(t, err) completedTskState := completedTsk.State() assert.Equal(t, asynctask.StateCompleted, completedTskState, "completed task should finished") From 545e7de116ee7cb7cc8a0a621bfb0c2e8edfce89 Mon Sep 17 00:00:00 2001 From: "Xinyue.Wang" Date: Tue, 27 Jun 2023 15:57:07 -0700 Subject: [PATCH 3/7] fix test data race --- wait_any_test.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/wait_any_test.go b/wait_any_test.go index 0a7e355..87c8a22 100644 --- a/wait_any_test.go +++ b/wait_any_test.go @@ -35,6 +35,11 @@ func TestWaitAny(t *testing.T) { // should finish right after countingTsk3 assert.True(t, elapsed >= 20*time.Millisecond && elapsed < 200*time.Millisecond, fmt.Sprintf("actually elapsed: %v", elapsed)) + + // counting task do testing.Logf in another go routine + // while testing.Logf would cause DataRace error when test is already finished: https://github.com/golang/go/issues/40343 + // wait minor time for the go routine to finish. + time.Sleep(1 * time.Millisecond) } func TestWaitAnyErrorCase(t *testing.T) { @@ -108,7 +113,7 @@ func TestWaitAnyErrorWithFailFastCase(t *testing.T) { cancelTaskExecution() // all assertion variable captured, cancel counting task assert.Equal(t, "expected error", err.Error(), "expecting first error") - // should only finish after longest task. + // should finsh after first error assert.True(t, elapsed >= 10*time.Millisecond && elapsed < 20*time.Millisecond, fmt.Sprintf("actually elapsed: %v", elapsed)) assert.Equal(t, asynctask.StateRunning, countingTskState, "countingTask should NOT finished") From 4e44750e4093ec6a41aa0cca8fb3392a6c2fe3c6 Mon Sep 17 00:00:00 2001 From: "Xinyue.Wang" Date: Tue, 27 Jun 2023 16:08:58 -0700 Subject: [PATCH 4/7] add test coverage --- wait_any_test.go | 57 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) diff --git a/wait_any_test.go b/wait_any_test.go index 87c8a22..75fb650 100644 --- a/wait_any_test.go +++ b/wait_any_test.go @@ -24,10 +24,10 @@ func TestWaitAny(t *testing.T) { // should finish after right away assert.True(t, elapsed < 2*time.Millisecond, fmt.Sprintf("actually elapsed: %v", elapsed)) + start = time.Now() countingTsk1 := asynctask.Start(ctx, getCountingTask(10, "countingPer40ms", 40*time.Millisecond)) countingTsk2 := asynctask.Start(ctx, getCountingTask(10, "countingPer20ms", 20*time.Millisecond)) countingTsk3 = asynctask.Start(ctx, getCountingTask(10, "countingPer2ms", 2*time.Millisecond)) - start = time.Now() err = asynctask.WaitAny(ctx, &asynctask.WaitAnyOptions{FailFast: true}, countingTsk1, countingTsk2, countingTsk3) elapsed = time.Since(start) assert.NoError(t, err) @@ -42,6 +42,31 @@ func TestWaitAny(t *testing.T) { time.Sleep(1 * time.Millisecond) } +func TestWaitAnyContextCancel(t *testing.T) { + t.Parallel() + ctx, cancelTaskExecution := newTestContextWithTimeout(t, 2*time.Second) + + start := time.Now() + + countingTsk1 := asynctask.Start(ctx, getCountingTask(10, "countingPer40ms", 40*time.Millisecond)) + countingTsk2 := asynctask.Start(ctx, getCountingTask(10, "countingPer20ms", 20*time.Millisecond)) + go func() { + time.Sleep(5 * time.Millisecond) + cancelTaskExecution() + }() + err := asynctask.WaitAny(ctx, nil, countingTsk1, countingTsk2) + elapsed := time.Since(start) + assert.Error(t, err) + assert.Equal(t, "WaitAny context canceled", err.Error(), "expecting context canceled error") + // should finish right after countingTsk3 + assert.True(t, elapsed >= 5*time.Millisecond && elapsed < 200*time.Millisecond, fmt.Sprintf("actually elapsed: %v", elapsed)) + + // counting task do testing.Logf in another go routine + // while testing.Logf would cause DataRace error when test is already finished: https://github.com/golang/go/issues/40343 + // wait minor time for the go routine to finish. + time.Sleep(1 * time.Millisecond) +} + func TestWaitAnyErrorCase(t *testing.T) { t.Parallel() ctx, cancelTaskExecution := newTestContextWithTimeout(t, 3*time.Second) @@ -58,6 +83,7 @@ func TestWaitAnyErrorCase(t *testing.T) { completedTskState := completedTsk.State() assert.Equal(t, asynctask.StateCompleted, completedTskState, "completed task should finished") + start = time.Now() countingTsk := asynctask.Start(ctx, getCountingTask(10, "countingPer40ms", 40*time.Millisecond)) errorTsk = asynctask.Start(ctx, getErrorTask("expected error", 10*time.Millisecond)) panicTsk := asynctask.Start(ctx, getPanicTask(20*time.Millisecond)) @@ -84,6 +110,34 @@ func TestWaitAnyErrorCase(t *testing.T) { time.Sleep(1 * time.Millisecond) } +func TestWaitAnyAllFailCase(t *testing.T) { + t.Parallel() + ctx, cancelTaskExecution := newTestContextWithTimeout(t, 3*time.Second) + + start := time.Now() + errorTsk := asynctask.Start(ctx, getErrorTask("expected error", 10*time.Millisecond)) + panicTsk := asynctask.Start(ctx, getPanicTask(20*time.Millisecond)) + err := asynctask.WaitAny(ctx, nil, errorTsk, panicTsk) + assert.Error(t, err) + + panicTskState := panicTsk.State() + errTskState := errorTsk.State() + elapsed := time.Since(start) + cancelTaskExecution() // all assertion variable captured, cancel counting task + + assert.Equal(t, "expected error", err.Error(), "expecting first error") + // should finsh after both error + assert.True(t, elapsed >= 20*time.Millisecond, fmt.Sprintf("actually elapsed: %v", elapsed)) + + assert.Equal(t, asynctask.StateFailed, errTskState, "error task should failed") + assert.Equal(t, asynctask.StateFailed, panicTskState, "panic task should Not failed") + + // counting task do testing.Logf in another go routine + // while testing.Logf would cause DataRace error when test is already finished: https://github.com/golang/go/issues/40343 + // wait minor time for the go routine to finish. + time.Sleep(1 * time.Millisecond) +} + func TestWaitAnyErrorWithFailFastCase(t *testing.T) { t.Parallel() ctx, cancelTaskExecution := newTestContextWithTimeout(t, 3*time.Second) @@ -98,6 +152,7 @@ func TestWaitAnyErrorWithFailFastCase(t *testing.T) { // should finish after right away assert.True(t, elapsed < 20*time.Millisecond, fmt.Sprintf("actually elapsed: %v", elapsed)) + start = time.Now() countingTsk := asynctask.Start(ctx, getCountingTask(10, "countingPer40ms", 40*time.Millisecond)) errorTsk = asynctask.Start(ctx, getErrorTask("expected error", 10*time.Millisecond)) panicTsk := asynctask.Start(ctx, getPanicTask(20*time.Millisecond)) From 8110e1a011ccd4b373b5869bbc0e9ce989be313d Mon Sep 17 00:00:00 2001 From: "Xinyue.Wang" Date: Tue, 27 Jun 2023 16:23:46 -0700 Subject: [PATCH 5/7] add another test case --- wait_any_test.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/wait_any_test.go b/wait_any_test.go index 75fb650..b0c4257 100644 --- a/wait_any_test.go +++ b/wait_any_test.go @@ -9,6 +9,14 @@ import ( "github.com/stretchr/testify/assert" ) +func TestWaitAnyNoTask(t *testing.T) { + t.Parallel() + ctx, _ := newTestContextWithTimeout(t, 2*time.Second) + + err := asynctask.WaitAny(ctx, nil) + assert.NoError(t, err) +} + func TestWaitAny(t *testing.T) { t.Parallel() ctx, cancelTaskExecution := newTestContextWithTimeout(t, 2*time.Second) From a612d9bc0ce75b52e9455b7ffaff1125858d5428 Mon Sep 17 00:00:00 2001 From: "Xinyue.Wang" Date: Tue, 27 Jun 2023 16:39:04 -0700 Subject: [PATCH 6/7] address comment --- wait_any.go | 20 ++++++++------------ wait_any_test.go | 8 ++++---- 2 files changed, 12 insertions(+), 16 deletions(-) diff --git a/wait_any.go b/wait_any.go index f51a9ce..3bd7c60 100644 --- a/wait_any.go +++ b/wait_any.go @@ -7,12 +7,12 @@ import ( // WaitAnyOptions defines options for WaitAny function type WaitAnyOptions struct { - // FailFast set to true will indicate WaitAny to return on first error it sees. - FailFast bool + // FailOnAnyError set to true will indicate WaitAny to return on first error it sees. + FailOnAnyError bool } // WaitAny block current thread til any of task finished. -// first error from any tasks passed in will be returned if FailFast is set. +// first error from any tasks passed in will be returned if FailOnAnyError is set. // first task end without error will end wait and return nil func WaitAny(ctx context.Context, options *WaitAnyOptions, tasks ...Waitable) error { tasksCount := len(tasks) @@ -41,12 +41,13 @@ func WaitAny(ctx context.Context, options *WaitAnyOptions, tasks ...Waitable) er case err := <-errorCh: runningTasks-- if err != nil { - // return immediately after receive first error if FailFast is set. - if options.FailFast { + // return immediately after receive first error if FailOnAnyError is set. + if options.FailOnAnyError { return err } errList = append(errList, err) } else { + // return immediately after first task completed. return nil } case <-ctx.Done(): @@ -59,13 +60,8 @@ func WaitAny(ctx context.Context, options *WaitAnyOptions, tasks ...Waitable) er } } - // we have at least 1 error when FailFast is not set, return first one. + // we have at least 1 error when FailOnAnyError is not set, return first one. // caller can get error for individual task by using Wait(), // it would return immediately after this WaitAny() - if len(errList) > 0 { - return errList[0] - } - - // no error at all. - return nil + return errList[0] } diff --git a/wait_any_test.go b/wait_any_test.go index b0c4257..754c99d 100644 --- a/wait_any_test.go +++ b/wait_any_test.go @@ -36,7 +36,7 @@ func TestWaitAny(t *testing.T) { countingTsk1 := asynctask.Start(ctx, getCountingTask(10, "countingPer40ms", 40*time.Millisecond)) countingTsk2 := asynctask.Start(ctx, getCountingTask(10, "countingPer20ms", 20*time.Millisecond)) countingTsk3 = asynctask.Start(ctx, getCountingTask(10, "countingPer2ms", 2*time.Millisecond)) - err = asynctask.WaitAny(ctx, &asynctask.WaitAnyOptions{FailFast: true}, countingTsk1, countingTsk2, countingTsk3) + err = asynctask.WaitAny(ctx, &asynctask.WaitAnyOptions{FailOnAnyError: true}, countingTsk1, countingTsk2, countingTsk3) elapsed = time.Since(start) assert.NoError(t, err) cancelTaskExecution() @@ -146,7 +146,7 @@ func TestWaitAnyAllFailCase(t *testing.T) { time.Sleep(1 * time.Millisecond) } -func TestWaitAnyErrorWithFailFastCase(t *testing.T) { +func TestWaitAnyErrorWithFailOnAnyErrorCase(t *testing.T) { t.Parallel() ctx, cancelTaskExecution := newTestContextWithTimeout(t, 3*time.Second) @@ -154,7 +154,7 @@ func TestWaitAnyErrorWithFailFastCase(t *testing.T) { errorTsk := asynctask.Start(ctx, getErrorTask("expected error", 10*time.Millisecond)) result := "something" completedTsk := asynctask.NewCompletedTask(&result) - err := asynctask.WaitAny(ctx, &asynctask.WaitAnyOptions{FailFast: true}, errorTsk, completedTsk) + err := asynctask.WaitAny(ctx, &asynctask.WaitAnyOptions{FailOnAnyError: true}, errorTsk, completedTsk) assert.NoError(t, err) elapsed := time.Since(start) // should finish after right away @@ -164,7 +164,7 @@ func TestWaitAnyErrorWithFailFastCase(t *testing.T) { countingTsk := asynctask.Start(ctx, getCountingTask(10, "countingPer40ms", 40*time.Millisecond)) errorTsk = asynctask.Start(ctx, getErrorTask("expected error", 10*time.Millisecond)) panicTsk := asynctask.Start(ctx, getPanicTask(20*time.Millisecond)) - err = asynctask.WaitAny(ctx, &asynctask.WaitAnyOptions{FailFast: true}, countingTsk, errorTsk, panicTsk) + err = asynctask.WaitAny(ctx, &asynctask.WaitAnyOptions{FailOnAnyError: true}, countingTsk, errorTsk, panicTsk) assert.Error(t, err) completedTskState := completedTsk.State() assert.Equal(t, asynctask.StateCompleted, completedTskState, "completed task should finished") From 8d6c81f893716ea29f43f7709fe4d8251ae38c7d Mon Sep 17 00:00:00 2001 From: "Xinyue.Wang" Date: Tue, 27 Jun 2023 16:53:01 -0700 Subject: [PATCH 7/7] fix comment --- wait_any.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wait_any.go b/wait_any.go index 3bd7c60..1d5cb51 100644 --- a/wait_any.go +++ b/wait_any.go @@ -60,7 +60,7 @@ func WaitAny(ctx context.Context, options *WaitAnyOptions, tasks ...Waitable) er } } - // we have at least 1 error when FailOnAnyError is not set, return first one. + // when all tasks failed and FailOnAnyError is not set, return first one. // caller can get error for individual task by using Wait(), // it would return immediately after this WaitAny() return errList[0]