Skip to content

Commit c2baddc

Browse files
committed
fix multierror
1 parent 83c03a8 commit c2baddc

File tree

2 files changed

+68
-27
lines changed

2 files changed

+68
-27
lines changed

errsizedgroup.go

Lines changed: 32 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package syncs
33
import (
44
"context"
55
"fmt"
6-
"strings"
76
"sync"
87
)
98

@@ -64,7 +63,7 @@ func NewErrSizedGroup(size int, options ...GroupOption) *ErrSizedGroup {
6463
// The first call to return a non-nil error cancels the group if termOnError; its error will be
6564
// returned by Wait. If no termOnError all errors will be collected in multierror.
6665
func (g *ErrSizedGroup) Go(f func(ctx context.Context) error) {
67-
if g.canceled() && (!g.termOnError || len(g.err.Errors()) == 0) {
66+
if g.canceled() && (!g.termOnError || g.err.len() == 0) {
6867
g.errOnce.Do(func() {
6968
// don't repeat this error
7069
g.err.append(g.ctx.Err())
@@ -120,51 +119,57 @@ func (g *ErrSizedGroup) Go(f func(ctx context.Context) error) {
120119
// returns all errors (if any) wrapped with multierror from them.
121120
func (g *ErrSizedGroup) Wait() error {
122121
g.wg.Wait()
122+
g.err.makeStr()
123123
return g.err.ErrorOrNil()
124124
}
125125

126126
// MultiError is a thread safe container for multi-error type that implements error interface
127127
type MultiError struct {
128128
errors []error
129-
lock sync.Mutex
130-
}
131-
132-
func (m *MultiError) append(err error) *MultiError {
133-
m.lock.Lock()
134-
m.errors = append(m.errors, err)
135-
m.lock.Unlock()
136-
return m
129+
lock sync.RWMutex
130+
str string
137131
}
138132

139133
// ErrorOrNil returns nil if no errors or multierror if errors occurred
140134
func (m *MultiError) ErrorOrNil() error {
141-
m.lock.Lock()
142-
defer m.lock.Unlock()
143-
if len(m.errors) == 0 {
135+
if m.len() == 0 {
144136
return nil
145137
}
146138
return m
147139
}
148140

149141
// Error returns multi-error string
150142
func (m *MultiError) Error() string {
151-
m.lock.Lock()
152-
defer m.lock.Unlock()
153-
if len(m.errors) == 0 {
154-
return ""
155-
}
156-
157-
errs := []string{}
158-
159-
for n, e := range m.errors {
160-
errs = append(errs, fmt.Sprintf("[%d] {%s}", n, e.Error()))
161-
}
162-
return fmt.Sprintf("%d error(s) occurred: %s", len(m.errors), strings.Join(errs, ", "))
143+
return m.str
163144
}
164145

165146
// Errors returns all errors collected
166147
func (m *MultiError) Errors() []error {
167-
m.lock.Lock()
168-
defer m.lock.Unlock()
169148
return m.errors
170149
}
150+
151+
func (m *MultiError) append(err error) {
152+
m.lock.Lock()
153+
m.errors = append(m.errors, err)
154+
m.lock.Unlock()
155+
}
156+
157+
func (m *MultiError) len() int {
158+
m.lock.RLock()
159+
defer m.lock.RUnlock()
160+
return len(m.errors)
161+
}
162+
163+
func (m *MultiError) makeStr() {
164+
lenErrors := m.len()
165+
if lenErrors == 0 {
166+
return
167+
}
168+
errs := fmt.Sprintf("[0] {%s}", m.Errors()[0].Error())
169+
if lenErrors > 1 {
170+
for n, e := range m.Errors()[1:] {
171+
errs += fmt.Sprintf(", [%d] {%s}", n+1, e.Error())
172+
}
173+
}
174+
m.str = fmt.Sprintf("%d error(s) occurred: %s", lenErrors, errs)
175+
}

errsizedgroup_test.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,42 @@ func TestErrorSizedGroup_CancelWithPreemptive(t *testing.T) {
299299
require.LessOrEqual(t, c, uint32(110), "some of goroutines has to be terminated early")
300300
}
301301

302+
func TestErrorSizedGroup_CancelWithMultiError(t *testing.T) {
303+
ctx, cancel := context.WithCancel(context.Background())
304+
defer cancel()
305+
ewg := NewErrSizedGroup(10, Context(ctx))
306+
307+
ewg.Go(func(ctx context.Context) error {
308+
return errors.New("first error")
309+
})
310+
ewg.Go(func(ctx context.Context) error {
311+
return errors.New("second error")
312+
})
313+
for i := 0; i < 100; i++ {
314+
if i == 10 {
315+
cancel()
316+
}
317+
time.Sleep(1 * time.Millisecond) // prevent all the goroutines to be started at once
318+
ewg.Go(func(ctx context.Context) error {
319+
timer := time.NewTimer(1 * time.Millisecond)
320+
defer timer.Stop()
321+
select {
322+
case <-ctx.Done():
323+
return ctx.Err()
324+
case <-timer.C: // simulate some work
325+
}
326+
return nil
327+
})
328+
}
329+
330+
err := ewg.Wait()
331+
assert.NotNil(t, err)
332+
var merr *MultiError
333+
assert.True(t, errors.As(err, &merr))
334+
assert.Len(t, merr.Errors(), 3)
335+
require.EqualError(t, err, "3 error(s) occurred: [0] {first error}, [1] {second error}, [2] {context canceled}")
336+
}
337+
302338
// illustrates the use of a SizedGroup for concurrent, limited execution of goroutines.
303339
func ExampleErrSizedGroup_go() {
304340

0 commit comments

Comments
 (0)