coder/scaletest/dashboard/run.go

131 lines
3.5 KiB
Go

package dashboard
import (
"context"
"errors"
"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
if cfg.WaitLoaded == nil {
cfg.WaitLoaded = waitForWorkspacesPageLoaded
}
if cfg.ActionFunc == nil {
cfg.ActionFunc = clickRandomElement
}
if cfg.Screenshot == nil {
cfg.Screenshot = Screenshot
}
if cfg.RandIntn == nil {
cfg.RandIntn = rand.Intn
}
return &Runner{
client: client,
cfg: cfg,
metrics: metrics,
}
}
func (r *Runner) Run(ctx context.Context, _ string, _ io.Writer) error {
err := r.runUntilDeadlineExceeded(ctx)
// If the context deadline exceeded, don't return an error.
// This just means the test finished.
if err == nil || errors.Is(err, context.DeadlineExceeded) {
return nil
}
return err
}
func (r *Runner) runUntilDeadlineExceeded(ctx context.Context) error {
if r.client == nil {
return xerrors.Errorf("client is nil")
}
me, err := r.client.User(ctx, codersdk.Me)
if err != nil {
return xerrors.Errorf("get scaletest user: %w", err)
}
//nolint:gocritic
r.cfg.Logger.Info(ctx, "running as user", slog.F("username", me.Username))
if len(me.OrganizationIDs) == 0 {
return xerrors.Errorf("user has no organizations")
}
cdpCtx, cdpCancel, err := initChromeDPCtx(ctx, r.cfg.Logger, r.client.URL, r.client.SessionToken(), r.cfg.Headless)
if err != nil {
return xerrors.Errorf("init chromedp ctx: %w", err)
}
defer cdpCancel()
t := time.NewTicker(1) // First one should be immediate
defer t.Stop()
r.cfg.Logger.Info(ctx, "waiting for workspaces page to load")
loadWorkspacePageDeadline := time.Now().Add(r.cfg.Interval)
if err := r.cfg.WaitLoaded(cdpCtx, loadWorkspacePageDeadline); err != nil {
return xerrors.Errorf("wait for workspaces page to load: %w", err)
}
for {
select {
case <-cdpCtx.Done():
return nil
case <-t.C:
var offset time.Duration
if r.cfg.Jitter > 0 {
offset = time.Duration(r.cfg.RandIntn(int(2*r.cfg.Jitter)) - int(r.cfg.Jitter))
}
wait := r.cfg.Interval + offset
actionCompleteByDeadline := time.Now().Add(wait)
t.Reset(wait)
l, act, err := r.cfg.ActionFunc(cdpCtx, r.cfg.Logger, r.cfg.RandIntn, actionCompleteByDeadline)
if err != nil {
r.cfg.Logger.Error(ctx, "calling ActionFunc", slog.Error(err))
sPath, sErr := r.cfg.Screenshot(cdpCtx, me.Username)
if sErr != nil {
r.cfg.Logger.Error(ctx, "screenshot failed", slog.Error(sErr))
}
r.cfg.Logger.Info(ctx, "screenshot saved", slog.F("path", sPath))
continue
}
start := time.Now()
err = act(cdpCtx)
elapsed := time.Since(start)
r.metrics.ObserveDuration(string(l), elapsed)
if err != nil {
r.metrics.IncErrors(string(l))
//nolint:gocritic
r.cfg.Logger.Error(ctx, "action failed", slog.F("label", l), slog.Error(err))
sPath, sErr := r.cfg.Screenshot(cdpCtx, me.Username+"-"+string(l))
if sErr != nil {
r.cfg.Logger.Error(ctx, "screenshot failed", slog.Error(sErr))
}
r.cfg.Logger.Info(ctx, "screenshot saved", slog.F("path", sPath))
} else {
//nolint:gocritic
r.cfg.Logger.Info(ctx, "action success", slog.F("label", l))
}
}
}
}
func (*Runner) Cleanup(_ context.Context, _ string, _ io.Writer) error {
return nil
}