fix(scaletest): fix flake in Test_Runner/Cleanup (#10252)

* fix(scaletest/createworkspaces): address flake in Test_Runner/CleanupPendingBuild

* fix(scaletest): pass io.Writer to Cleanup()

* add some extra logs to workspacebuild cleanup

* fixup! fix(scaletest): pass io.Writer to Cleanup()

* remove race

* fmt

* address PR comments
This commit is contained in:
Cian Johnston 2023-10-16 12:37:12 +01:00 committed by GitHub
parent 1be24dcb5c
commit dd86100f33
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 61 additions and 32 deletions

View File

@ -1241,7 +1241,7 @@ func (r *runnableTraceWrapper) Run(ctx context.Context, id string, logs io.Write
return r.runner.Run(ctx2, id, logs) return r.runner.Run(ctx2, id, logs)
} }
func (r *runnableTraceWrapper) Cleanup(ctx context.Context, id string) error { func (r *runnableTraceWrapper) Cleanup(ctx context.Context, id string, logs io.Writer) error {
c, ok := r.runner.(harness.Cleanable) c, ok := r.runner.(harness.Cleanable)
if !ok { if !ok {
return nil return nil
@ -1253,7 +1253,7 @@ func (r *runnableTraceWrapper) Cleanup(ctx context.Context, id string) error {
ctx, span := r.tracer.Start(ctx, r.spanName+" cleanup") ctx, span := r.tracer.Start(ctx, r.spanName+" cleanup")
defer span.End() defer span.End()
return c.Cleanup(ctx, id) return c.Cleanup(ctx, id, logs)
} }
// newScaleTestUser returns a random username and email address that can be used // newScaleTestUser returns a random username and email address that can be used

View File

@ -176,13 +176,14 @@ resourceLoop:
} }
// Cleanup implements Cleanable. // Cleanup implements Cleanable.
func (r *Runner) Cleanup(ctx context.Context, id string) error { func (r *Runner) Cleanup(ctx context.Context, id string, logs io.Writer) error {
if r.cfg.NoCleanup { if r.cfg.NoCleanup {
_, _ = fmt.Fprintln(logs, "skipping cleanup")
return nil return nil
} }
if r.workspacebuildRunner != nil { if r.workspacebuildRunner != nil {
err := r.workspacebuildRunner.Cleanup(ctx, id) err := r.workspacebuildRunner.Cleanup(ctx, id, logs)
if err != nil { if err != nil {
return xerrors.Errorf("cleanup workspace: %w", err) return xerrors.Errorf("cleanup workspace: %w", err)
} }
@ -191,6 +192,7 @@ func (r *Runner) Cleanup(ctx context.Context, id string) error {
if r.userID != uuid.Nil { if r.userID != uuid.Nil {
err := r.client.DeleteUser(ctx, r.userID) err := r.client.DeleteUser(ctx, r.userID)
if err != nil { if err != nil {
_, _ = fmt.Fprintf(logs, "failed to delete user %q: %v\n", r.userID.String(), err)
return xerrors.Errorf("delete user: %w", err) return xerrors.Errorf("delete user: %w", err)
} }
} }

View File

@ -177,8 +177,13 @@ func Test_Runner(t *testing.T) {
require.Contains(t, logsStr, "Opening reconnecting PTY connection to agent") require.Contains(t, logsStr, "Opening reconnecting PTY connection to agent")
require.Contains(t, logsStr, "Opening connection to workspace agent") require.Contains(t, logsStr, "Opening connection to workspace agent")
err = runner.Cleanup(ctx, "1") cleanupLogs := bytes.NewBuffer(nil)
err = runner.Cleanup(ctx, "1", cleanupLogs)
require.NoError(t, err) require.NoError(t, err)
cleanupLogsStr := cleanupLogs.String()
require.Contains(t, cleanupLogsStr, "deleting workspace")
require.NotContains(t, cleanupLogsStr, "canceling workspace build") // The build should have already completed.
require.Contains(t, cleanupLogsStr, "Build succeeded!")
// Ensure the user and workspace were deleted. // Ensure the user and workspace were deleted.
users, err = client.Users(ctx, codersdk.UsersRequest{}) users, err = client.Users(ctx, codersdk.UsersRequest{})
@ -217,7 +222,7 @@ func Test_Runner(t *testing.T) {
}, },
ProvisionApply: []*proto.Response{ ProvisionApply: []*proto.Response{
{ {
Type: &proto.Response_Log{Log: &proto.Log{}}, Type: &proto.Response_Log{Log: &proto.Log{}}, // This provisioner job will never complete.
}, },
}, },
}) })
@ -257,24 +262,36 @@ func Test_Runner(t *testing.T) {
close(done) close(done)
}() }()
// Wait for the workspace build job to be picked up.
require.Eventually(t, func() bool { require.Eventually(t, func() bool {
workspaces, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{}) workspaces, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{})
if err != nil { if err != nil {
return false return false
} }
if len(workspaces.Workspaces) == 0 {
return false
}
return len(workspaces.Workspaces) > 0 ws := workspaces.Workspaces[0]
}, testutil.WaitShort, testutil.IntervalFast) t.Logf("checking build: %s | %s", ws.LatestBuild.Transition, ws.LatestBuild.Job.Status)
// There should be only one build at present.
if ws.LatestBuild.Transition != codersdk.WorkspaceTransitionStart {
t.Errorf("expected build transition %s, got %s", codersdk.WorkspaceTransitionStart, ws.LatestBuild.Transition)
return false
}
return ws.LatestBuild.Job.Status == codersdk.ProvisionerJobRunning
}, testutil.WaitShort, testutil.IntervalMedium)
cancelFunc() cancelFunc()
<-done <-done
// When we run the cleanup, it should be canceled // When we run the cleanup, it should be canceled
cleanupLogs := bytes.NewBuffer(nil)
cancelCtx, cancelFunc = context.WithCancel(ctx) cancelCtx, cancelFunc = context.WithCancel(ctx)
done = make(chan struct{}) done = make(chan struct{})
go func() { go func() {
// This will return an error as the "delete" operation will never complete. // This will return an error as the "delete" operation will never complete.
_ = runner.Cleanup(cancelCtx, "1") _ = runner.Cleanup(cancelCtx, "1", cleanupLogs)
close(done) close(done)
}() }()
@ -311,9 +328,11 @@ func Test_Runner(t *testing.T) {
} }
} }
return false return false
}, testutil.WaitShort, testutil.IntervalFast) }, testutil.WaitShort, testutil.IntervalMedium)
cancelFunc() cancelFunc()
<-done <-done
cleanupLogsStr := cleanupLogs.String()
require.Contains(t, cleanupLogsStr, "canceling workspace build")
}) })
t.Run("NoCleanup", func(t *testing.T) { t.Run("NoCleanup", func(t *testing.T) {
@ -447,7 +466,8 @@ func Test_Runner(t *testing.T) {
require.Contains(t, logsStr, "Opening reconnecting PTY connection to agent") require.Contains(t, logsStr, "Opening reconnecting PTY connection to agent")
require.Contains(t, logsStr, "Opening connection to workspace agent") require.Contains(t, logsStr, "Opening connection to workspace agent")
err = runner.Cleanup(ctx, "1") cleanupLogs := bytes.NewBuffer(nil)
err = runner.Cleanup(ctx, "1", cleanupLogs)
require.NoError(t, err) require.NoError(t, err)
// Ensure the user and workspace were not deleted. // Ensure the user and workspace were not deleted.

View File

@ -125,6 +125,6 @@ func (r *Runner) runUntilDeadlineExceeded(ctx context.Context) error {
} }
} }
func (*Runner) Cleanup(_ context.Context, _ string) error { func (*Runner) Cleanup(_ context.Context, _ string, _ io.Writer) error {
return nil return nil
} }

View File

@ -112,7 +112,7 @@ func Test_TestHarness(t *testing.T) {
RunFn: func(_ context.Context, _ string, _ io.Writer) error { RunFn: func(_ context.Context, _ string, _ io.Writer) error {
return nil return nil
}, },
CleanupFn: func(_ context.Context, _ string) error { CleanupFn: func(_ context.Context, _ string, _ io.Writer) error {
panic(testPanicMessage) panic(testPanicMessage)
}, },
}) })
@ -150,7 +150,7 @@ func Test_TestHarness(t *testing.T) {
RunFn: func(_ context.Context, _ string, _ io.Writer) error { RunFn: func(_ context.Context, _ string, _ io.Writer) error {
return nil return nil
}, },
CleanupFn: func(_ context.Context, _ string) error { CleanupFn: func(_ context.Context, _ string, _ io.Writer) error {
return nil return nil
}, },
}) })
@ -295,7 +295,7 @@ func fakeTestFns(err, cleanupErr error) testFns {
RunFn: func(_ context.Context, _ string, _ io.Writer) error { RunFn: func(_ context.Context, _ string, _ io.Writer) error {
return err return err
}, },
CleanupFn: func(_ context.Context, _ string) error { CleanupFn: func(_ context.Context, _ string, _ io.Writer) error {
return cleanupErr return cleanupErr
}, },
} }

View File

@ -28,7 +28,7 @@ type Runnable interface {
type Cleanable interface { type Cleanable interface {
Runnable Runnable
// Cleanup should clean up any lingering resources from the test. // Cleanup should clean up any lingering resources from the test.
Cleanup(ctx context.Context, id string) error Cleanup(ctx context.Context, id string, logs io.Writer) error
} }
// AddRun creates a new *TestRun with the given name, ID and Runnable, adds it // AddRun creates a new *TestRun with the given name, ID and Runnable, adds it
@ -131,7 +131,7 @@ func (r *TestRun) Cleanup(ctx context.Context) (err error) {
} }
}() }()
err = c.Cleanup(ctx, r.id) err = c.Cleanup(ctx, r.id, r.logs)
//nolint:revive // we use named returns because we mutate it in a defer //nolint:revive // we use named returns because we mutate it in a defer
return return
} }

View File

@ -16,7 +16,7 @@ import (
type testFns struct { type testFns struct {
RunFn func(ctx context.Context, id string, logs io.Writer) error RunFn func(ctx context.Context, id string, logs io.Writer) error
// CleanupFn is optional if no cleanup is required. // CleanupFn is optional if no cleanup is required.
CleanupFn func(ctx context.Context, id string) error CleanupFn func(ctx context.Context, id string, logs io.Writer) error
} }
// Run implements Runnable. // Run implements Runnable.
@ -25,12 +25,12 @@ func (fns testFns) Run(ctx context.Context, id string, logs io.Writer) error {
} }
// Cleanup implements Cleanable. // Cleanup implements Cleanable.
func (fns testFns) Cleanup(ctx context.Context, id string) error { func (fns testFns) Cleanup(ctx context.Context, id string, logs io.Writer) error {
if fns.CleanupFn == nil { if fns.CleanupFn == nil {
return nil return nil
} }
return fns.CleanupFn(ctx, id) return fns.CleanupFn(ctx, id, logs)
} }
func Test_TestRun(t *testing.T) { func Test_TestRun(t *testing.T) {
@ -49,7 +49,7 @@ func Test_TestRun(t *testing.T) {
atomic.AddInt64(&runCalled, 1) atomic.AddInt64(&runCalled, 1)
return nil return nil
}, },
CleanupFn: func(ctx context.Context, id string) error { CleanupFn: func(ctx context.Context, id string, logs io.Writer) error {
atomic.AddInt64(&cleanupCalled, 1) atomic.AddInt64(&cleanupCalled, 1)
return nil return nil
}, },
@ -93,7 +93,7 @@ func Test_TestRun(t *testing.T) {
RunFn: func(ctx context.Context, id string, logs io.Writer) error { RunFn: func(ctx context.Context, id string, logs io.Writer) error {
return nil return nil
}, },
CleanupFn: func(ctx context.Context, id string) error { CleanupFn: func(ctx context.Context, id string, logs io.Writer) error {
atomic.AddInt64(&cleanupCalled, 1) atomic.AddInt64(&cleanupCalled, 1)
return nil return nil
}, },

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"fmt" "fmt"
"io" "io"
"net/http"
"time" "time"
"github.com/google/uuid" "github.com/google/uuid"
@ -106,25 +107,31 @@ func NewCleanupRunner(client *codersdk.Client, workspaceID uuid.UUID) *CleanupRu
// Run implements Runnable. // Run implements Runnable.
func (r *CleanupRunner) Run(ctx context.Context, _ string, logs io.Writer) error { func (r *CleanupRunner) Run(ctx context.Context, _ string, logs io.Writer) error {
ctx, span := tracing.StartSpan(ctx)
defer span.End()
logs = loadtestutil.NewSyncWriter(logs)
logger := slog.Make(sloghuman.Sink(logs)).Leveled(slog.LevelDebug)
if r.workspaceID == uuid.Nil { if r.workspaceID == uuid.Nil {
return nil return nil
} }
ctx, span := tracing.StartSpan(ctx) logger.Info(ctx, "deleting workspace", slog.F("workspace_id", r.workspaceID))
defer span.End()
logs = loadtestutil.NewSyncWriter(logs)
logger := slog.Make(sloghuman.Sink(logs)).Leveled(slog.LevelDebug)
r.client.SetLogger(logger) r.client.SetLogger(logger)
r.client.SetLogBodies(true) r.client.SetLogBodies(true)
ws, err := r.client.Workspace(ctx, r.workspaceID) ws, err := r.client.Workspace(ctx, r.workspaceID)
if err != nil { if err != nil {
var sdkErr *codersdk.Error
if xerrors.As(err, &sdkErr) && sdkErr.StatusCode() == http.StatusNotFound {
logger.Info(ctx, "workspace not found, skipping delete", slog.F("workspace_id", r.workspaceID))
return nil
}
return err return err
} }
build, err := r.client.WorkspaceBuild(ctx, ws.LatestBuild.ID) build, err := r.client.WorkspaceBuild(ctx, ws.LatestBuild.ID)
if err == nil && build.Job.Status.Active() { if err == nil && build.Job.Status.Active() {
// mark the build as canceled // mark the build as canceled
logger.Info(ctx, "canceling workspace build", slog.F("build_id", build.ID), slog.F("workspace_id", r.workspaceID))
if err = r.client.CancelWorkspaceBuild(ctx, build.ID); err == nil { if err = r.client.CancelWorkspaceBuild(ctx, build.ID); err == nil {
// Wait for the job to cancel before we delete it // Wait for the job to cancel before we delete it
_ = waitForBuild(ctx, logs, r.client, build.ID) // it will return a "build canceled" error _ = waitForBuild(ctx, logs, r.client, build.ID) // it will return a "build canceled" error
@ -151,12 +158,11 @@ func (r *CleanupRunner) Run(ctx context.Context, _ string, logs io.Writer) error
} }
// Cleanup implements Cleanable by wrapping CleanupRunner. // Cleanup implements Cleanable by wrapping CleanupRunner.
func (r *Runner) Cleanup(ctx context.Context, id string) error { func (r *Runner) Cleanup(ctx context.Context, id string, w io.Writer) error {
// TODO: capture these logs
return (&CleanupRunner{ return (&CleanupRunner{
client: r.client, client: r.client,
workspaceID: r.workspaceID, workspaceID: r.workspaceID,
}).Run(ctx, id, io.Discard) }).Run(ctx, id, w)
} }
func waitForBuild(ctx context.Context, w io.Writer, client *codersdk.Client, buildID uuid.UUID) error { func waitForBuild(ctx context.Context, w io.Writer, client *codersdk.Client, buildID uuid.UUID) error {

View File

@ -180,7 +180,8 @@ func Test_Runner(t *testing.T) {
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspaces[0].LatestBuild.ID) coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspaces[0].LatestBuild.ID)
coderdtest.AwaitWorkspaceAgents(t, client, workspaces[0].ID) coderdtest.AwaitWorkspaceAgents(t, client, workspaces[0].ID)
err = runner.Cleanup(ctx, "1") cleanupLogs := bytes.NewBuffer(nil)
err = runner.Cleanup(ctx, "1", cleanupLogs)
require.NoError(t, err) require.NoError(t, err)
}) })

View File

@ -169,7 +169,7 @@ func (r *Runner) Run(ctx context.Context, _ string, logs io.Writer) error {
} }
// Cleanup does nothing, successfully. // Cleanup does nothing, successfully.
func (*Runner) Cleanup(context.Context, string) error { func (*Runner) Cleanup(context.Context, string, io.Writer) error {
return nil return nil
} }