common: Update ErrorCollector to use mutex and simplify error collection in concurrent operations (#2090)

* refactor: Update ErrorCollector to use mutex and simplify error collection in concurrent operations

* glorious: nits

* linter: fix

* another find

* Apply suggestion from @gbjk

Co-authored-by: Gareth Kirwan <gbjkirwan@gmail.com>

* Apply suggestion from @gbjk

Co-authored-by: Gareth Kirwan <gbjkirwan@gmail.com>

* one liner defer

* Update common/common.go

Co-authored-by: Gareth Kirwan <gbjkirwan@gmail.com>

* gk: nits

* Update common/common_test.go

Co-authored-by: Scott <gloriousCode@users.noreply.github.com>

* thrasher-: nits

---------

Co-authored-by: shazbert <ryan.oharareid@thrasher.io>
Co-authored-by: Gareth Kirwan <gbjkirwan@gmail.com>
Co-authored-by: shazbert <shazbert@DESKTOP-3QKKR6J.localdomain>
Co-authored-by: Scott <gloriousCode@users.noreply.github.com>
This commit is contained in:
Ryan O'Hara-Reid
2025-11-11 15:08:48 +11:00
committed by GitHub
parent fefb866b02
commit 497e13dc62
6 changed files with 63 additions and 107 deletions

View File

@@ -553,31 +553,33 @@ func ExcludeError(err, excl error) error {
}
// ErrorCollector allows collecting a stream of errors from concurrent go routines
// Users should call e.Wg.Done and send errors to e.C
type ErrorCollector struct {
C chan error
Wg sync.WaitGroup
errs error
wg sync.WaitGroup
m sync.Mutex
}
// CollectErrors returns an ErrorCollector with WaitGroup and Channel buffer set to n
func CollectErrors(n int) *ErrorCollector {
e := &ErrorCollector{
C: make(chan error, n),
}
e.Wg.Add(n)
return e
}
// Collect runs waits for e.Wg to be Done, closes the error channel, and return a error collection
// Collect waits for the internal wait group to be done and returns an error collection
// State is reset after each Collect, so successive calls are okay
func (e *ErrorCollector) Collect() (errs error) {
e.Wg.Wait()
close(e.C)
for err := range e.C {
if err != nil {
errs = AppendError(errs, err)
}
e.wg.Wait()
e.m.Lock()
defer func() { e.errs = nil; e.m.Unlock() }()
return e.errs
}
// Go runs a function in a goroutine and collects any error it returns
func (e *ErrorCollector) Go(f func() error) {
if err := NilGuard(f); err != nil {
panic(err)
}
return
e.wg.Go(func() {
if err := f(); err != nil {
e.m.Lock()
e.errs = AppendError(e.errs, err)
e.m.Unlock()
}
})
}
// StartEndTimeCheck provides some basic checks which occur

View File

@@ -590,23 +590,22 @@ func TestGenerateRandomString(t *testing.T) {
}
}
// TestErrorCollector exercises the error collector
func TestErrorCollector(t *testing.T) {
e := CollectErrors(4)
var e ErrorCollector
require.Panics(t, func() { e.Go(nil) }, "Go with nil function must panic")
for i := range 4 {
go func() {
e.Go(func() error {
if i%2 == 0 {
e.C <- errors.New("Collected error")
} else {
e.C <- nil
return errors.New("collected error")
}
e.Wg.Done()
}()
return nil
})
}
v := e.Collect()
errs, ok := v.(*multiError)
require.True(t, ok, "Must return a multiError")
assert.Len(t, errs.Unwrap(), 2, "Should have 2 errors")
assert.NoError(t, e.Collect(), "should return nil when a previous collection emptied the errors")
}
// TestBatch ensures the Batch function does not regress into common behavioural faults if implementation changes