Skip to content

Commit 3f330b1

Browse files
authored
Merge pull request #1772 from lightninglabs/parslice-capture-naming
fn: fix loop var capture and clarify naming in `ParSliceErrCollect`
2 parents 8b04ad8 + f0ea040 commit 3f330b1

File tree

1 file changed

+41
-17
lines changed

1 file changed

+41
-17
lines changed

fn/concurrency.go

Lines changed: 41 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,12 @@ func ParSlice[V any](ctx context.Context, s []V, f ErrFunc[V]) error {
2626
errGroup.SetLimit(runtime.GOMAXPROCS(0))
2727

2828
for _, v := range s {
29+
// Snapshot v now so the goroutine sees the intended value
30+
// even if the caller mutates the slice s later. This is a
31+
// shallow copy only, if V contains pointers, their contents can
32+
// still change.
2933
v := v
34+
3035
errGroup.Go(func() error {
3136
return f(ctx, v)
3237
})
@@ -43,39 +48,58 @@ func ParSlice[V any](ctx context.Context, s []V, f ErrFunc[V]) error {
4348
func ParSliceErrCollect[V any](ctx context.Context, s []V,
4449
f ErrFunc[V]) (map[int]error, error) {
4550

46-
errGroup, ctx := errgroup.WithContext(ctx)
51+
errGroup, groupCtx := errgroup.WithContext(ctx)
4752
errGroup.SetLimit(runtime.GOMAXPROCS(0))
4853

49-
var instanceErrorsMutex sync.Mutex
50-
instanceErrors := make(map[int]error, len(s))
54+
var (
55+
instanceErrorsMu sync.Mutex
56+
instanceErrors = make(map[int]error, len(s))
57+
)
5158

5259
for idx := range s {
60+
// Snapshot s[idx] now so the goroutine sees the intended value
61+
// even if the caller mutates the slice later. This is a shallow
62+
// copy only, if V contains pointers, their contents can still
63+
// change.
64+
v := s[idx]
65+
5366
errGroup.Go(func() error {
54-
err := f(ctx, s[idx])
67+
// If already canceled, skip work without signaling an
68+
// error.
69+
select {
70+
case <-groupCtx.Done():
71+
return nil
72+
default:
73+
}
74+
75+
err := f(groupCtx, v)
5576
if err != nil {
56-
instanceErrorsMutex.Lock()
77+
instanceErrorsMu.Lock()
5778
instanceErrors[idx] = err
58-
instanceErrorsMutex.Unlock()
79+
instanceErrorsMu.Unlock()
5980
}
6081

61-
// Avoid returning an error here, as that would cancel
62-
// the errGroup and terminate all slice element
63-
// processing instances. Instead, collect the error and
64-
// return it later.
82+
// Do not return an error here. If we did, errGroup
83+
// would cancel the context and stop all other element
84+
// processors. Instead, record the error locally
85+
// and return it after all goroutines finish.
6586
return nil
6687
})
6788
}
6889

69-
// Now we will wait/block for all goroutines to finish.
70-
//
71-
// The goroutines that are executing in parallel should not return an
72-
// error, but the Wait call may return an error if the context is
73-
// canceled or timed out.
74-
err := errGroup.Wait()
75-
if err != nil {
90+
// Wait for all goroutines to finish. In this design, goroutines do not
91+
// return errors, so Wait should normally return nil. We handle context
92+
// cancellation separately with an explicit ctx.Err() check.
93+
if err := errGroup.Wait(); err != nil {
7694
return nil, fmt.Errorf("failed to wait on error group in "+
7795
"ParSliceErrorCollect: %w", err)
7896
}
7997

98+
// If the caller's context was canceled or timed out, surface that.
99+
// Return whatever per-item errors were collected before cancellation.
100+
if err := ctx.Err(); err != nil {
101+
return instanceErrors, err
102+
}
103+
80104
return instanceErrors, nil
81105
}

0 commit comments

Comments
 (0)