package dashboard import ( "context" "fmt" "io" "math/rand" "time" "golang.org/x/xerrors" "cdr.dev/slog" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/scaletest/harness" ) type Runner struct { client *codersdk.Client cfg Config metrics Metrics } var ( _ harness.Runnable = &Runner{} _ harness.Cleanable = &Runner{} ) func NewRunner(client *codersdk.Client, metrics Metrics, cfg Config) *Runner { client.Trace = cfg.Trace return &Runner{ client: client, cfg: cfg, metrics: metrics, } } func (r *Runner) Run(ctx context.Context, _ string, _ io.Writer) error { me, err := r.client.User(ctx, codersdk.Me) if err != nil { return err } if len(me.OrganizationIDs) == 0 { return xerrors.Errorf("user has no organizations") } c := &cache{} if err := c.fill(ctx, r.client); err != nil { return err } p := &Params{ client: r.client, me: me, c: c, } rolls := make(chan int) go func() { t := time.NewTicker(r.randWait()) defer t.Stop() for { select { case <-ctx.Done(): return case <-t.C: rolls <- rand.Intn(r.cfg.RollTable.max() + 1) // nolint:gosec t.Reset(r.randWait()) } } }() for { select { case <-ctx.Done(): return nil case n := <-rolls: act := r.cfg.RollTable.choose(n) go r.do(ctx, act, p) } } } func (*Runner) Cleanup(_ context.Context, _ string) error { return nil } func (r *Runner) do(ctx context.Context, act RollTableEntry, p *Params) { select { case <-ctx.Done(): r.cfg.Logger.Info(ctx, "context done, stopping") return default: var errored bool cancelCtx, cancel := context.WithTimeout(ctx, r.cfg.MaxWait) defer cancel() start := time.Now() err := act.Fn(cancelCtx, p) cancel() elapsed := time.Since(start) if err != nil { errored = true r.cfg.Logger.Error( //nolint:gocritic ctx, "action failed", slog.Error(err), slog.F("action", act.Label), slog.F("elapsed", elapsed), ) } else { r.cfg.Logger.Info(ctx, "completed successfully", slog.F("action", act.Label), slog.F("elapsed", elapsed), ) } codeLabel := "200" if apiErr, ok := codersdk.AsError(err); ok { codeLabel = fmt.Sprintf("%d", apiErr.StatusCode()) } else if xerrors.Is(err, context.Canceled) { codeLabel = "timeout" } r.metrics.ObserveDuration(act.Label, elapsed) r.metrics.IncStatuses(act.Label, codeLabel) if errored { r.metrics.IncErrors(act.Label) } } } func (r *Runner) randWait() time.Duration { // nolint:gosec // This is not for cryptographic purposes. Chill, gosec. Chill. wait := time.Duration(rand.Intn(int(r.cfg.MaxWait) - int(r.cfg.MinWait))) return r.cfg.MinWait + wait }