Skip to content

Commit 5f009fb

Browse files
author
Måns Thörnvik
committed
tweaks
1 parent 1c7656a commit 5f009fb

3 files changed

Lines changed: 146 additions & 102 deletions

File tree

arbiter.go

Lines changed: 131 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -22,33 +22,36 @@ import (
2222
"github.com/maansaake/arbiter/pkg/subcommand/gen"
2323
"github.com/maansaake/arbiter/pkg/traffic"
2424
"github.com/spf13/cobra"
25+
"github.com/spf13/pflag"
2526
"github.com/trebent/zerologr"
2627
)
2728

2829
const (
29-
durationDefault = time.Minute * 5
30-
reportPathDefault = "report.yaml"
30+
defaultDuration = time.Minute * 5
31+
defaultReportPath = "report.yaml"
32+
defaultInteractive = false
3133
)
3234

3335
var (
3436
// global flag vars.
3537
//nolint:gochecknoglobals // modified by flag parsing
36-
duration = durationDefault
37-
38-
// logger.
39-
//nolint:gochecknoglobals // glob log
40-
startLogger = zerologr.New(&zerologr.Opts{Console: true}).WithName("start")
38+
duration time.Duration
4139

4240
// report.
43-
reportPath = reportPathDefault //nolint:gochecknoglobals // modified by flag parsing
41+
reportPath string //nolint:gochecknoglobals // modified by flag parsing
4442

4543
// interactive enables the live TUI dashboard.
4644
//nolint:gochecknoglobals // modified by flag parsing
47-
interactive = false
45+
interactive bool
4846

4947
// rootCmd holds the cobra root command for Usage access.
5048
//nolint:gochecknoglobals // package-level command for Usage access
5149
rootCmd *cobra.Command
50+
51+
// ErrStopping is returned when there was an error stopping traffic or modules.
52+
// It is used to wrap any errors from traffic or module stopping to allow
53+
// callers to check for this specific case.
54+
ErrStopping = errors.New("error stopping traffic or modules")
5255
)
5356

5457
//nolint:gochecknoinits // sets up global logger at package load
@@ -65,79 +68,38 @@ func Usage() {
6568
// Run the Arbiter. Blocks until SIGINT, SIGTERM or when the test duration
6669
// runs out (5 minute default).
6770
func Run(modules module.Modules) error {
68-
// TODO: change to support > 1 module
69-
if len(modules) != 1 {
70-
return fmt.Errorf("currently only 1 module is supported, got %d", len(modules))
71-
}
72-
7371
if err := module.Validate(modules); err != nil {
7472
return err
7573
}
7674

7775
// Reset to defaults on each Run call.
78-
duration = durationDefault
79-
reportPath = reportPathDefault
80-
interactive = false
76+
duration = defaultDuration
77+
reportPath = defaultReportPath
78+
interactive = defaultInteractive
8179

80+
// Root cmd that all subcommands are added to.
8281
rootCmd = &cobra.Command{
8382
Use: "arbiter",
84-
Short: "Arbiter load testing framework.",
83+
Short: "Arbiter load testing.",
8584
SilenceErrors: true,
8685
SilenceUsage: true,
8786
}
8887

89-
rootCmd.PersistentFlags().
90-
DurationVarP(&duration, "duration", "d", durationDefault, "The duration of the test run, minimum 1 second.")
91-
rootCmd.PersistentFlags().
92-
StringVarP(&reportPath, "report-path", "r", reportPathDefault, "Path to the final report.")
93-
rootCmd.PersistentFlags().
94-
BoolVarP(&interactive, "interactive", "i", false, "Start in interactive TUI mode with a live progress bar and per-operation statistics.")
95-
96-
cliCmd, err := cli.NewCommand(modules, run)
88+
cliCmd, fileCmd, err := buildRunnerCmds(modules)
9789
if err != nil {
9890
return err
9991
}
10092

101-
preRunE := func(_ *cobra.Command, _ []string) error {
102-
if duration < 1*time.Second {
103-
return errors.New("duration must be at least 1 second")
104-
}
105-
106-
if reportPath == "" {
107-
return errors.New("report path cannot be empty")
108-
}
109-
110-
stat, err := os.Stat(reportPath) //nolint:govet // shad
111-
if err == nil && stat.IsDir() {
112-
return errors.New("report path cannot be a directory")
113-
}
114-
115-
return nil
116-
}
117-
cliCmd.PreRunE = preRunE
118-
11993
rootCmd.AddCommand(
12094
cliCmd,
95+
fileCmd,
12196
&cobra.Command{
12297
Use: gen.FlagsetName,
12398
Short: "Generate a test model file.",
12499
RunE: func(_ *cobra.Command, args []string) error {
125100
return gen.Generate(args, modules)
126101
},
127102
},
128-
&cobra.Command{
129-
Use: file.FlagsetName,
130-
Short: "Run from a test model file.",
131-
PreRunE: preRunE,
132-
RunE: func(_ *cobra.Command, args []string) error {
133-
meta, err := file.Parse(args, modules) //nolint:govet // shad
134-
if err != nil {
135-
return err
136-
}
137-
138-
return run(meta)
139-
},
140-
},
141103
)
142104

143105
return rootCmd.Execute()
@@ -148,76 +110,62 @@ func Run(modules module.Modules) error {
148110
// or when the test duration runs out. Will immediately exit if any module
149111
// returns an error from its call to Run().
150112
func run(metadata module.Metadata) error {
151-
startLogger.Info("Starting modules")
113+
zerologr.Info("Starting modules")
152114

153115
if err := startModules(metadata); err != nil {
154-
startLogger.Error(err, "Start failure")
116+
zerologr.Error(err, "Start failure")
155117
return err
156118
}
157-
startLogger.Info("All modules started")
119+
zerologr.Info("All modules started")
158120

159121
reporter := setupReporter(metadata)
160122

123+
// Start signal interceptor for SIGINT and SIGTERM
124+
signalCtx, signalCancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
125+
defer signalCancel()
126+
161127
// Start traffic and monitor, with a timeout of: test >duration<
162-
background := context.Background()
163-
deadlineCtx, deadlineCancel := context.WithTimeout(background, duration)
164-
defer deadlineCancel()
165-
startLogger.Info("Traffic will run for: " + duration.String())
166-
167-
// Separate the reporter context to allow for finishing reporting separately from stopping traffic.
168-
reporterCtx, reporterCancel := context.WithCancel(background)
169-
defer reporterCancel()
170-
reporter.Start(reporterCtx)
171-
172-
// Suppress log output while the TUI is active to prevent interference
173-
// with the alternate-screen renderer.
174-
if interactive {
175-
zerologr.Set(logr.Discard())
176-
startLogger = logr.Discard()
177-
}
128+
timeoutCtx, timeoutCancel := context.WithTimeout(signalCtx, duration)
129+
defer timeoutCancel()
130+
zerologr.Info("Traffic will run for: " + duration.String())
131+
132+
reporter.Start(signalCtx)
178133

179-
if err := traffic.Run(deadlineCtx, metadata, reporter); err != nil {
180-
startLogger.Error(err, "Failed to start traffic")
134+
if err := traffic.Run(timeoutCtx, metadata, reporter); err != nil {
135+
zerologr.Error(err, "Failed to start traffic")
181136
return err
182137
}
183138

184-
// Start signal interceptor for SIGINT and SIGTERM
185-
signalCtx, signalCancel := signal.NotifyContext(background, syscall.SIGINT, syscall.SIGTERM)
186-
defer signalCancel()
187-
startLogger.Info("Awaiting stop signal")
139+
zerologr.Info("Awaiting completion (SIGINT/SIGTERM or duration timeout)")
188140
select {
189141
case <-signalCtx.Done():
190-
startLogger.Info("Got stop signal")
191-
case <-deadlineCtx.Done():
192-
startLogger.Info("Deadline exceeded")
142+
zerologr.Info("Got stop signal")
143+
case <-timeoutCtx.Done():
144+
zerologr.Info("Deadline exceeded")
193145
}
194-
deadlineCancel()
195-
signalCancel()
196-
197-
startLogger = startLogger.WithName("stopping")
198146

147+
// stopErr accumulates any errors from stopping traffic and modules, and finalising the report,
148+
// to be returned at the end of the function.
199149
var stopErr error
200-
stopErr = traffic.Stop()
201-
if stopErr != nil {
202-
startLogger.Error(stopErr, "Error when stopping traffic")
150+
if stopErr = traffic.Stop(); stopErr != nil {
151+
zerologr.Error(stopErr, "Error when stopping traffic")
152+
stopErr = fmt.Errorf("%w: traffic stop: %w", ErrStopping, stopErr)
203153
}
154+
signalCancel() // Cancel the signal context to unblock the reporter if it's waiting on it.
204155

205-
// Stop it here to allow the scheduler to report all before shutting down.
206-
reporterCancel()
207-
208-
startLogger.Info("Stopping modules")
156+
zerologr.Info("Stopping modules")
209157
for _, m := range metadata {
210158
if moduleStopErr := m.Stop(); moduleStopErr != nil {
211-
startLogger.Error(moduleStopErr, "Module stop reported an error", "module", m.Name())
212-
stopErr = errors.Join(stopErr, fmt.Errorf("module %s: %w", m.Name(), moduleStopErr))
159+
zerologr.Error(moduleStopErr, "Module stop reported an error", "module", m.Name())
160+
stopErr = errors.Join(stopErr, fmt.Errorf("module %s stop: %w", m.Name(), moduleStopErr))
213161
}
214162
}
215163

216-
startLogger.Info("Finalising report")
164+
zerologr.Info("Finalising report")
217165
reporterStopErr := reporter.Finalise()
218166
if reporterStopErr != nil {
219-
startLogger.Error(reporterStopErr, "Error when finalising report")
220-
stopErr = errors.Join(stopErr, reporterStopErr)
167+
zerologr.Error(reporterStopErr, "Error when finalising report")
168+
stopErr = errors.Join(stopErr, fmt.Errorf("reporter stop: %w", reporterStopErr))
221169
}
222170

223171
return stopErr
@@ -226,7 +174,7 @@ func run(metadata module.Metadata) error {
226174
// Starts the input modules and logs any errors.
227175
func startModules(meta []*module.Meta) error {
228176
for _, m := range meta {
229-
startLogger.Info("Starting", "module", m.Name())
177+
zerologr.Info("Starting", "module", m.Name())
230178
if err := m.Run(); err != nil {
231179
return fmt.Errorf("failed to start module %s: %w", m.Name(), err)
232180
}
@@ -248,3 +196,84 @@ func setupReporter(metadata module.Metadata) report.Reporter {
248196

249197
return yamlR
250198
}
199+
200+
func buildRunnerCmds(modules module.Modules) (*cobra.Command, *cobra.Command, error) {
201+
// The runner flagset is passed to cli and file commands that run tests.
202+
runnerFlagSet := buildRunnerFlagSet()
203+
204+
cliCmd, err := cli.NewCommand(modules, run)
205+
if err != nil {
206+
return nil, nil, err
207+
}
208+
cliCmd.Flags().AddFlagSet(runnerFlagSet)
209+
210+
runnerPreRunE := func(_ *cobra.Command, _ []string) error {
211+
if duration < 1*time.Second {
212+
return errors.New("duration must be at least 1 second")
213+
}
214+
215+
if reportPath == "" {
216+
return errors.New("report path cannot be empty")
217+
}
218+
219+
// err is fine since the file does not have to exist prior to the test ending.
220+
stat, err := os.Stat(reportPath) //nolint:govet // shad
221+
if err == nil && stat.IsDir() {
222+
return errors.New("report path cannot be a directory")
223+
}
224+
225+
if interactive {
226+
// Suppress log output while the TUI is active to prevent interference
227+
// TODO: create file sink instead to be able to capture logs in interactive mode as well
228+
zerologr.Set(logr.Discard())
229+
} else {
230+
zerologr.Set(zerologr.New(&zerologr.Opts{Console: true, Caller: true}).WithName("arbiter"))
231+
}
232+
233+
return nil
234+
}
235+
cliCmd.PreRunE = runnerPreRunE
236+
237+
fileCmd := &cobra.Command{
238+
Use: file.FlagsetName,
239+
Short: "Run from a test model file.",
240+
PreRunE: runnerPreRunE,
241+
RunE: func(_ *cobra.Command, args []string) error {
242+
meta, err := file.Parse(args, modules) //nolint:govet // shad
243+
if err != nil {
244+
return err
245+
}
246+
247+
return run(meta)
248+
},
249+
}
250+
fileCmd.Flags().AddFlagSet(runnerFlagSet)
251+
252+
return cliCmd, fileCmd, nil
253+
}
254+
255+
func buildRunnerFlagSet() *pflag.FlagSet {
256+
runnerFlagSet := &pflag.FlagSet{}
257+
runnerFlagSet.DurationVarP(
258+
&duration,
259+
"duration",
260+
"d",
261+
defaultDuration,
262+
"The duration of the test run, minimum 1 second.",
263+
)
264+
runnerFlagSet.StringVarP(
265+
&reportPath,
266+
"report-path",
267+
"r",
268+
defaultReportPath,
269+
"Path to the final report.",
270+
)
271+
runnerFlagSet.BoolVarP(
272+
&interactive,
273+
"interactive",
274+
"i",
275+
defaultInteractive,
276+
"Start in interactive TUI mode with a live progress bar and per-operation statistics.",
277+
)
278+
return runnerFlagSet
279+
}

examples/samplemod/main.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package main
22

33
import (
4+
"errors"
45
"fmt"
56
"os"
67

@@ -15,6 +16,12 @@ func main() {
1516
os.Exit(0)
1617
}
1718

19+
// This inspection allows us to provide a more specific error message when the error is related to stopping traffic or modules,
20+
if errors.Is(err, arbiter.ErrStopping) {
21+
fmt.Fprintf(os.Stderr, "Arbiter stopped with error: %v\n\n", err)
22+
os.Exit(1)
23+
}
24+
1825
fmt.Fprintf(os.Stderr, "Error running the Arbiter: %v\n\n", err)
1926
os.Exit(1)
2027
}

pkg/subcommand/cli/cli.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,17 @@ func NewCommand(modules module.Modules, run func(module.Metadata) error) (*cobra
2222
cmd := &cobra.Command{
2323
Use: FlagsetName,
2424
Short: "Run using CLI flags.",
25+
Long: `Run using CLI flags. Flags are generated from the provided modules and their operations.
26+
Each module's arguments for operations and pure args are prefixed with the module name.
27+
28+
Included modules are:`,
2529
}
2630

2731
for i, mod := range modules {
32+
cmd.Long = fmt.Sprintf(`%s
33+
34+
%s: %s`, cmd.Long, mod.Name(), mod.Desc())
35+
2836
meta := &module.Meta{Module: mod}
2937
metadata[i] = meta
3038

0 commit comments

Comments
 (0)