Skip to content

Commit 65de2a7

Browse files
mtGitHub CopilotCopilot
authored
refactor(traffic): replace package-vars with Scheduler interface and struct (#28)
Move pkg/traffic from package-level mutable state to a proper struct-based design with an exported Scheduler interface. Changes: - Add Opts struct (Logger, WorkerLimit, SampleTolerancePerc) - Add Scheduler interface with Run/Stop methods - Add unexported scheduler struct implementing Scheduler - Add New(opts *Opts) Scheduler constructor with defaulting - Remove package-level Run(), Stop() functions and all package vars (reporter, workloads, stopChan, logger, SampleTolerancePerc) - Inject reporter, stopChan, logger into workload struct fields - workload.worker uses parent.logger instead of package-level logger - Update arbiter.go to create traffic.New(&traffic.Opts{...}) and call sched.Run() / sched.Stop() on the instance - Update all tests to instantiate via New() and call methods on instance Co-authored-by: GitHub Copilot <copilot@github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent b0f5c62 commit 65de2a7

5 files changed

Lines changed: 138 additions & 70 deletions

File tree

arbiter.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -273,8 +273,13 @@ func (a *abtr) run(metadata module.Metadata) error {
273273
defer reporterCancel()
274274
reporter.Start(reporterCtx)
275275

276+
sched := traffic.New(&traffic.Opts{
277+
Logger: logger,
278+
WorkerLimit: workerLimit.Value(),
279+
})
280+
276281
// Run traffic.
277-
if err := traffic.Run(timeoutCtx, metadata, reporter, workerLimit.Value()); err != nil {
282+
if err := sched.Run(timeoutCtx, metadata, reporter); err != nil {
278283
reporter.ReportError(err) // Report is done in case of early traffic failure, to highlight issues in the TUI.
279284
logger.Error(err, "Failed to start traffic")
280285
return err
@@ -296,7 +301,7 @@ func (a *abtr) run(metadata module.Metadata) error {
296301
// stopErr accumulates any errors from stopping traffic and modules, and finalising the report,
297302
// to be returned at the end of the function.
298303
var stopErr error
299-
if stopErr = traffic.Stop(); stopErr != nil {
304+
if stopErr = sched.Stop(); stopErr != nil {
300305
logger.Error(stopErr, "Error when stopping traffic")
301306
stopErr = fmt.Errorf("%w: traffic stop: %w", ErrStopping, stopErr)
302307
}

pkg/traffic/scheduler.go

Lines changed: 74 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -8,28 +8,15 @@ import (
88
"time"
99

1010
"github.com/go-logr/logr"
11-
abtrlog "github.com/maansaake/arbiter/internal/log"
1211
"github.com/maansaake/arbiter/pkg/module"
1312
"github.com/maansaake/arbiter/pkg/report"
1413
)
1514

1615
var (
17-
reporter report.Reporter //nolint:gochecknoglobals // package-level state for traffic scheduler
18-
19-
workloads []*workload //nolint:gochecknoglobals // package-level state for traffic scheduler
20-
21-
// Stop stuff.
22-
stopChan chan *workload //nolint:gochecknoglobals // package-level state for traffic scheduler
23-
24-
// logger is the package logger for the traffic package.
25-
logger logr.Logger //nolint:gochecknoglobals // package-level state for traffic scheduler
26-
2716
ErrNoOpsToSchedule = errors.New("there were no operations to schedule")
2817
ErrZeroRate = errors.New("operation has a zero rate")
2918
ErrCleanupTimeout = errors.New("cleanup timed out")
3019
ErrRateIssue = errors.New("rate issue")
31-
32-
SampleTolerancePerc = 0.05 //nolint:gochecknoglobals // exported config var for tests
3320
)
3421

3522
const (
@@ -38,75 +25,125 @@ const (
3825
defaultSampleIntervalSeconds = 10
3926
cleanupTimeout = 5 * time.Second
4027
minRateForDefaultSample = 30
28+
defaultSampleTolerancePerc = 0.05
4129
)
4230

31+
// Opts configures a Scheduler.
32+
type Opts struct {
33+
// Logger is used for traffic scheduler logs. Defaults to a discard logger if not set.
34+
Logger logr.Logger
35+
// WorkerLimit is the maximum number of concurrent workers per workload. Defaults to DefaultWorkerLimit.
36+
WorkerLimit int
37+
// SampleTolerancePerc is the tolerance percentage used when comparing sampled rates in tests.
38+
// Defaults to 0.05 (5%).
39+
SampleTolerancePerc float64
40+
}
41+
42+
// Scheduler runs traffic against registered modules.
43+
type Scheduler interface {
44+
// Run starts traffic generation for the given modules, reporting results to reporter.
45+
// It is asynchronous: it returns once the goroutines are launched and monitors ctx
46+
// to stop gracefully when it is cancelled.
47+
Run(ctx context.Context, metadata module.Metadata, reporter report.Reporter) error
48+
// Stop waits for all workloads to finish after the context passed to Run is cancelled.
49+
Stop() error
50+
}
51+
52+
type scheduler struct {
53+
logger logr.Logger
54+
workerLimit int
55+
sampleTolerancePerc float64
56+
57+
workloads []*workload
58+
stopChan chan *workload
59+
}
60+
61+
// New creates a Scheduler with the given options. A nil opts uses all defaults.
62+
func New(opts *Opts) Scheduler {
63+
if opts == nil {
64+
opts = &Opts{}
65+
}
66+
if opts.WorkerLimit == 0 {
67+
opts.WorkerLimit = DefaultWorkerLimit
68+
}
69+
if opts.SampleTolerancePerc == 0 {
70+
opts.SampleTolerancePerc = defaultSampleTolerancePerc
71+
}
72+
return &scheduler{
73+
logger: opts.Logger,
74+
workerLimit: opts.WorkerLimit,
75+
sampleTolerancePerc: opts.SampleTolerancePerc,
76+
}
77+
}
78+
4379
// Run traffic for the input modules using their exposed operations. Traffic
4480
// generation will make operation calls at the specified rates and report
4581
// problems to the reporter. Run() is asynchronous and returns once the main
4682
// go-routine has been started. Run() will monitor the context's done channel
4783
// and stop gracefully once it's closed.
48-
func Run(
84+
func (s *scheduler) Run(
4985
ctx context.Context,
5086
metadata module.Metadata,
51-
r report.Reporter,
52-
workerLimit int,
87+
reporter report.Reporter,
5388
) error {
54-
logger = abtrlog.GetLogger()
55-
56-
logger.Info("Running traffic generator")
57-
// Run initialisation of traffic synchronously
58-
reporter = r
89+
s.logger.Info("Running traffic generator")
5990

60-
workloads = make([]*workload, 0, len(metadata))
91+
s.workloads = make([]*workload, 0, len(metadata))
6192
for _, meta := range metadata {
6293
for _, op := range meta.Ops() {
6394
if op.Disabled {
64-
logger.Info("Skipping disabled operation", "mod", meta.Name(), "op", op.Name)
95+
s.logger.Info("Skipping disabled operation", "mod", meta.Name(), "op", op.Name)
6596
continue
6697
}
6798

6899
if op.Rate == 0 {
69100
return fmt.Errorf("%w: %s", ErrZeroRate, op.Name)
70101
}
71102

72-
workloads = append(workloads, &workload{
73-
workerLimit: workerLimit,
103+
s.workloads = append(s.workloads, &workload{
104+
workerLimit: s.workerLimit,
74105
statLock: &sync.Mutex{},
75106
mod: meta.Name(),
76107
op: op,
108+
reporter: reporter,
109+
logger: s.logger,
77110
})
78111
}
79112
}
80113

81-
if len(workloads) == 0 {
114+
if len(s.workloads) == 0 {
82115
return ErrNoOpsToSchedule
83116
}
84117

85118
// Create stop channel that workloads will report to when stopping.
86-
stopChan = make(chan *workload, len(workloads))
119+
s.stopChan = make(chan *workload, len(s.workloads))
120+
for _, wl := range s.workloads {
121+
wl.stopChan = s.stopChan
122+
}
87123

88-
// Run the workload in a separate go-routine, runs until context is done
89-
for _, workload := range workloads {
90-
go workload.run(ctx)
124+
// Run the workloads in separate go-routines, each runs until context is done.
125+
for _, wl := range s.workloads {
126+
go wl.run(ctx)
91127
}
92128

93129
return nil
94130
}
95131

96-
func Stop() error {
97-
logger.Info("Stopping traffic generator", "workload_count", len(workloads))
132+
// Stop waits for all workloads to finish and returns any error encountered.
133+
func (s *scheduler) Stop() error {
134+
s.logger.Info("Stopping traffic generator", "workload_count", len(s.workloads))
98135

99136
stopCount := 0
100137
for {
101138
select {
102139
case <-time.After(cleanupTimeout):
103-
logger.Error(ErrCleanupTimeout, "Cleanup timed out after "+cleanupTimeout.String())
140+
s.logger.Error(ErrCleanupTimeout, "Cleanup timed out after "+cleanupTimeout.String())
104141
return ErrCleanupTimeout
105-
case workload := <-stopChan:
106-
logger.Info("Workload stopped", "mod", workload.mod, "op", workload.op.Name)
142+
case wl := <-s.stopChan:
143+
s.logger.Info("Workload stopped", "mod", wl.mod, "op", wl.op.Name)
107144
stopCount++
108-
if stopCount == len(workloads) {
109-
logger.Info("All workloads have stopped")
145+
if stopCount == len(s.workloads) {
146+
s.logger.Info("All workloads have stopped")
110147
return nil
111148
}
112149
}
@@ -117,7 +154,6 @@ func getSampleInterval(op *module.Op) time.Duration {
117154
if op.Rate < minRateForDefaultSample {
118155
// Minimum 5 samples, this should be a super corner case. Add some time
119156
// to allow the 5th invocation to fire.
120-
121157
return time.Minute/time.Duration(op.Rate)*5 + 250*time.Millisecond
122158
}
123159

pkg/traffic/scheduler_test.go

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,21 @@ import (
77
"testing"
88
"time"
99

10+
"github.com/go-logr/logr"
1011
"github.com/maansaake/arbiter/pkg/module"
1112
modulemock "github.com/maansaake/arbiter/pkg/module/mock"
1213
reportmock "github.com/maansaake/arbiter/pkg/report/mock"
1314
log "github.com/trebent/zerologr"
1415
)
1516

17+
// newTestScheduler creates a Scheduler pre-configured for tests.
18+
func newTestScheduler() Scheduler {
19+
return New(&Opts{
20+
Logger: logr.Discard(),
21+
WorkerLimit: DefaultWorkerLimit,
22+
})
23+
}
24+
1625
func TestRunAndAwaitStop(t *testing.T) {
1726
opWg := sync.WaitGroup{}
1827
opWg.Add(2)
@@ -30,7 +39,8 @@ func TestRunAndAwaitStop(t *testing.T) {
3039
},
3140
}
3241
ctx, cancel := context.WithCancel(context.Background())
33-
err := Run(ctx, []*module.Meta{{Module: mod}}, reportmock.NewMock(), DefaultWorkerLimit)
42+
sched := newTestScheduler()
43+
err := sched.Run(ctx, []*module.Meta{{Module: mod}}, reportmock.NewMock())
3444
if err != nil {
3545
t.Fatal(err)
3646
}
@@ -39,14 +49,15 @@ func TestRunAndAwaitStop(t *testing.T) {
3949
opWg.Wait()
4050

4151
cancel()
42-
err = Stop()
52+
err = sched.Stop()
4353
if err != nil {
4454
t.Fatal(err)
4555
}
4656
}
4757

4858
func TestRunNoOps(t *testing.T) {
49-
err := Run(context.TODO(), []*module.Meta{{Module: modulemock.NewMock()}}, nil, DefaultWorkerLimit)
59+
sched := newTestScheduler()
60+
err := sched.Run(context.TODO(), []*module.Meta{{Module: modulemock.NewMock()}}, nil)
5061
if err != nil && !errors.Is(err, ErrNoOpsToSchedule) {
5162
t.Fatal("unexpected error type")
5263
}
@@ -63,7 +74,8 @@ func TestRunZeroRate(t *testing.T) {
6374
Rate: 0,
6475
},
6576
}
66-
err := Run(context.TODO(), []*module.Meta{{Module: mod}}, nil, DefaultWorkerLimit)
77+
sched := newTestScheduler()
78+
err := sched.Run(context.TODO(), []*module.Meta{{Module: mod}}, nil)
6779
if err != nil && !errors.Is(err, ErrZeroRate) {
6880
t.Fatal("unexpected error type")
6981
}
@@ -90,15 +102,16 @@ func TestReportOpToReporter(t *testing.T) {
90102

91103
reporter := reportmock.NewMock()
92104
ctx, cancel := context.WithCancel(context.Background())
93-
if err := Run(ctx, []*module.Meta{{Module: mod}}, reporter, DefaultWorkerLimit); err != nil {
105+
sched := newTestScheduler()
106+
if err := sched.Run(ctx, []*module.Meta{{Module: mod}}, reporter); err != nil {
94107
t.Fatal(err)
95108
}
96109

97110
wg.Wait()
98111
cancel()
99112

100113
// This should ensure the reporter mock has received the Op report.
101-
Stop()
114+
sched.Stop()
102115

103116
if reporter.OpResults[0].Duration == 0 {
104117
t.Fatal("duration was not reported to reporter")
@@ -123,15 +136,16 @@ func TestReportOpDurationOverrideToReporter(t *testing.T) {
123136

124137
reporter := reportmock.NewMock()
125138
ctx, cancel := context.WithCancel(context.Background())
126-
if err := Run(ctx, []*module.Meta{{Module: mod}}, reporter, DefaultWorkerLimit); err != nil {
139+
sched := newTestScheduler()
140+
if err := sched.Run(ctx, []*module.Meta{{Module: mod}}, reporter); err != nil {
127141
t.Fatal(err)
128142
}
129143

130144
wg.Wait()
131145
cancel()
132146

133147
// This should ensure the reporter mock has received the Op report.
134-
Stop()
148+
sched.Stop()
135149

136150
if reporter.OpResults[0].Duration != 12*time.Millisecond {
137151
t.Fatal("duration override was not used")
@@ -156,15 +170,16 @@ func TestReportOpErr(t *testing.T) {
156170

157171
reporter := reportmock.NewMock()
158172
ctx, cancel := context.WithCancel(context.Background())
159-
if err := Run(ctx, []*module.Meta{{Module: mod}}, reporter, DefaultWorkerLimit); err != nil {
173+
sched := newTestScheduler()
174+
if err := sched.Run(ctx, []*module.Meta{{Module: mod}}, reporter); err != nil {
160175
t.Fatal(err)
161176
}
162177

163178
wg.Wait()
164179
cancel()
165180

166181
// This should ensure the reporter mock has received the Op report.
167-
Stop()
182+
sched.Stop()
168183

169184
if len(reporter.OpResults) != 0 {
170185
t.Fatal("unexpected op results found")

pkg/traffic/worker.go

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,27 +14,33 @@ type worker struct {
1414
}
1515

1616
func (worker *worker) run(ctx context.Context) {
17-
logger.Info("Starting worker", "mod", worker.parent.mod, "op", worker.parent.op.Name)
17+
worker.parent.logger.Info("Starting worker", "mod", worker.parent.mod, "op", worker.parent.op.Name)
1818
worker.done = make(chan bool)
1919

2020
for {
2121
select {
2222
case <-ctx.Done():
23-
logger.Info("Context closed, stopping worker", "mod", worker.parent.mod, "op", worker.parent.op.Name)
23+
worker.parent.logger.Info(
24+
"Context closed, stopping worker",
25+
"mod",
26+
worker.parent.mod,
27+
"op",
28+
worker.parent.op.Name,
29+
)
2430
worker.ticker.Stop()
2531

2632
close(worker.done)
2733
return
2834
case t := <-worker.ticker.C:
29-
logger.V(workerVerboseLogLevel).
35+
worker.parent.logger.V(workerVerboseLogLevel).
3036
Info("Worker tick", "time", t, "mod", worker.parent.mod, "op", worker.parent.op.Name)
3137
worker.parent.doOp()
3238
}
3339
}
3440
}
3541

3642
func (worker *worker) reset(tickerInterval time.Duration) {
37-
logger.Info(
43+
worker.parent.logger.Info(
3844
"Resetting worker ticker",
3945
"mod",
4046
worker.parent.mod,

0 commit comments

Comments
 (0)