runner.go

This commit is contained in:
Dmitry Sergeev
2026-09-17 20:30:00 +03:00
parent d341d22611
commit d3f0b533e3
4 changed files with 125 additions and 110 deletions
+43
View File
@@ -0,0 +1,43 @@
package app
import (
"context"
"errors"
"fmt"
)
type Runner struct {
Name string
Run func(context.Context) error
}
type runnerResult struct {
name string
err error
}
func RunConcurrent(ctx context.Context, runners ...Runner) error {
if len(runners) == 0 {
return nil
}
ctx, cancel := context.WithCancel(ctx)
defer cancel()
results := make(chan runnerResult, len(runners))
for _, runner := range runners {
runner := runner
go func() {
results <- runnerResult{name: runner.Name, err: runner.Run(ctx)}
}()
}
var resultErr error
for range runners {
result := <-results
if result.err != nil {
resultErr = errors.Join(resultErr, fmt.Errorf("%s flow: %w", result.name, result.err))
cancel()
}
}
return resultErr
}
+74
View File
@@ -0,0 +1,74 @@
package app_test
import (
"context"
"errors"
"mxl-pattern-generator/internal/app"
"strings"
"testing"
"time"
)
func TestRunConcurrentCancelsSiblingAndWaitsForCleanup(t *testing.T) {
wantErr := errors.New("writer failed")
peerStarted := make(chan struct{})
peerStopped := make(chan struct{})
err := app.RunConcurrent(context.Background(),
app.Runner{
Name: "video",
Run: func(ctx context.Context) error {
<-peerStarted
return wantErr
},
},
app.Runner{
Name: "audio",
Run: func(ctx context.Context) error {
close(peerStarted)
<-ctx.Done()
close(peerStopped)
return nil
},
},
)
if !errors.Is(err, wantErr) {
t.Fatalf("error = %v, want wrapped %v", err, wantErr)
}
if !strings.Contains(err.Error(), "video flow") {
t.Fatalf("error = %q, want runner name", err)
}
select {
case <-peerStopped:
default:
t.Fatal("runConcurrent returned before the sibling completed cleanup")
}
}
func TestRunConcurrentParentCancellationIsGraceful(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
started := make(chan struct{})
done := make(chan error, 1)
go func() {
done <- app.RunConcurrent(ctx, app.Runner{
Name: "video",
Run: func(ctx context.Context) error {
close(started)
<-ctx.Done()
return nil
},
})
}()
<-started
cancel()
select {
case err := <-done:
if err != nil {
t.Fatalf("runConcurrent: %v", err)
}
case <-time.After(time.Second):
t.Fatal("runConcurrent did not stop after parent cancellation")
}
}