@@ -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
1615var (
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
3522const (
@@ -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
0 commit comments