- Notifications
You must be signed in to change notification settings - Fork1.1k
feat(scaletest): add runner for prebuilds#20571
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.
Already on GitHub?Sign in to your account
base:main
Are you sure you want to change the base?
Uh oh!
There was an error while loading.Please reload this page.
Changes fromall commits
File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading.Please reload this page.
Jump to
Uh oh!
There was an error while loading.Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,164 @@ | ||
| package prebuilds_test | ||
| import ( | ||
| "io" | ||
| "strconv" | ||
| "sync" | ||
| "testing" | ||
| "github.com/prometheus/client_golang/prometheus" | ||
| "github.com/stretchr/testify/require" | ||
| "golang.org/x/sync/errgroup" | ||
| "github.com/coder/coder/v2/codersdk" | ||
| "github.com/coder/coder/v2/enterprise/coderd/coderdenttest" | ||
| "github.com/coder/coder/v2/enterprise/coderd/license" | ||
| "github.com/coder/coder/v2/scaletest/prebuilds" | ||
| "github.com/coder/coder/v2/testutil" | ||
| "github.com/coder/quartz" | ||
| ) | ||
| func TestRun(t *testing.T) { | ||
| t.Parallel() | ||
| t.Skip("This test takes several minutes to run, and is intended as a manual regression test") | ||
MemberAuthor
| ||
| ctx := testutil.Context(t, testutil.WaitSuperLong*3) | ||
| client, user := coderdenttest.New(t, &coderdenttest.Options{ | ||
| LicenseOptions: &coderdenttest.LicenseOptions{ | ||
| Features: license.Features{ | ||
| codersdk.FeatureWorkspacePrebuilds: 1, | ||
| codersdk.FeatureExternalProvisionerDaemons: 1, | ||
| }, | ||
| }, | ||
| }) | ||
| // This is a real Terraform provisioner | ||
| _ = coderdenttest.NewExternalProvisionerDaemonTerraform(t, client, user.OrganizationID, nil) | ||
| numTemplates := 2 | ||
| numPresets := 1 | ||
| numPresetPrebuilds := 1 | ||
| //nolint:gocritic // It's fine to use the owner user to pause prebuilds | ||
| err := client.PutPrebuildsSettings(ctx, codersdk.PrebuildsSettings{ | ||
| ReconciliationPaused: true, | ||
| }) | ||
| require.NoError(t, err) | ||
| setupBarrier := new(sync.WaitGroup) | ||
| setupBarrier.Add(numTemplates) | ||
| deletionBarrier := new(sync.WaitGroup) | ||
| deletionBarrier.Add(numTemplates) | ||
| metrics := prebuilds.NewMetrics(prometheus.NewRegistry()) | ||
| eg, runCtx := errgroup.WithContext(ctx) | ||
| runners := make([]*prebuilds.Runner, 0, numTemplates) | ||
| for i := range numTemplates { | ||
| cfg := prebuilds.Config{ | ||
| OrganizationID: user.OrganizationID, | ||
| NumPresets: numPresets, | ||
| NumPresetPrebuilds: numPresetPrebuilds, | ||
| TemplateVersionJobTimeout: testutil.WaitSuperLong * 2, | ||
| PrebuildWorkspaceTimeout: testutil.WaitSuperLong * 2, | ||
| Metrics: metrics, | ||
| SetupBarrier: setupBarrier, | ||
| DeletionBarrier: deletionBarrier, | ||
| Clock: quartz.NewReal(), | ||
| } | ||
| err := cfg.Validate() | ||
| require.NoError(t, err) | ||
| runner := prebuilds.NewRunner(client, cfg) | ||
| runners = append(runners, runner) | ||
| eg.Go(func() error { | ||
| return runner.Run(runCtx, strconv.Itoa(i), io.Discard) | ||
| }) | ||
| } | ||
| // Wait for all runners to reach the setup barrier (templates created) | ||
| setupBarrier.Wait() | ||
| // Resume prebuilds to trigger prebuild creation | ||
| err = client.PutPrebuildsSettings(ctx, codersdk.PrebuildsSettings{ | ||
| ReconciliationPaused: false, | ||
| }) | ||
| require.NoError(t, err) | ||
| err = eg.Wait() | ||
| require.NoError(t, err) | ||
| //nolint:gocritic // Owner user is fine here as we want to view all workspaces | ||
| workspaces, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{}) | ||
| require.NoError(t, err) | ||
| expectedWorkspaces := numTemplates * numPresets * numPresetPrebuilds | ||
| require.Equal(t, workspaces.Count, expectedWorkspaces) | ||
| // Now run Cleanup which measures deletion | ||
| // First pause prebuilds again | ||
| err = client.PutPrebuildsSettings(ctx, codersdk.PrebuildsSettings{ | ||
| ReconciliationPaused: true, | ||
| }) | ||
| require.NoError(t, err) | ||
| cleanupEg, cleanupCtx := errgroup.WithContext(ctx) | ||
| for i, runner := range runners { | ||
| cleanupEg.Go(func() error { | ||
| return runner.Cleanup(cleanupCtx, strconv.Itoa(i), io.Discard) | ||
| }) | ||
| } | ||
| // Wait for all runners to reach the deletion barrier (template versions updated to 0 prebuilds) | ||
| deletionBarrier.Wait() | ||
| // Resume prebuilds to trigger prebuild deletion | ||
| err = client.PutPrebuildsSettings(ctx, codersdk.PrebuildsSettings{ | ||
| ReconciliationPaused: false, | ||
| }) | ||
| require.NoError(t, err) | ||
| err = cleanupEg.Wait() | ||
| require.NoError(t, err) | ||
| // Verify all prebuild workspaces were deleted | ||
| workspaces, err = client.Workspaces(ctx, codersdk.WorkspaceFilter{}) | ||
| require.NoError(t, err) | ||
| require.Equal(t, workspaces.Count, 0) | ||
| for _, runner := range runners { | ||
| metrics := runner.GetMetrics() | ||
| require.Contains(t, metrics, prebuilds.PrebuildsTotalLatencyMetric) | ||
| require.Contains(t, metrics, prebuilds.PrebuildJobCreationLatencyMetric) | ||
| require.Contains(t, metrics, prebuilds.PrebuildJobAcquiredLatencyMetric) | ||
| creationLatency, ok := metrics[prebuilds.PrebuildsTotalLatencyMetric].(int64) | ||
| require.True(t, ok) | ||
| jobCreationLatency, ok := metrics[prebuilds.PrebuildJobCreationLatencyMetric].(int64) | ||
| require.True(t, ok) | ||
| jobAcquiredLatency, ok := metrics[prebuilds.PrebuildJobAcquiredLatencyMetric].(int64) | ||
| require.True(t, ok) | ||
| require.Greater(t, creationLatency, int64(0)) | ||
| require.Greater(t, jobCreationLatency, int64(0)) | ||
| require.Greater(t, jobAcquiredLatency, int64(0)) | ||
| require.Contains(t, metrics, prebuilds.PrebuildDeletionTotalLatencyMetric) | ||
| require.Contains(t, metrics, prebuilds.PrebuildDeletionJobCreationLatencyMetric) | ||
| require.Contains(t, metrics, prebuilds.PrebuildDeletionJobAcquiredLatencyMetric) | ||
| deletionLatency, ok := metrics[prebuilds.PrebuildDeletionTotalLatencyMetric].(int64) | ||
| require.True(t, ok) | ||
| deletionJobCreationLatency, ok := metrics[prebuilds.PrebuildDeletionJobCreationLatencyMetric].(int64) | ||
| require.True(t, ok) | ||
| deletionJobAcquiredLatency, ok := metrics[prebuilds.PrebuildDeletionJobAcquiredLatencyMetric].(int64) | ||
| require.True(t, ok) | ||
| require.Greater(t, deletionLatency, int64(0)) | ||
| require.Greater(t, deletionJobCreationLatency, int64(0)) | ||
| require.Greater(t, deletionJobAcquiredLatency, int64(0)) | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| package loadtestutil | ||
| import ( | ||
| "archive/tar" | ||
| "bytes" | ||
| "path/filepath" | ||
| "slices" | ||
| ) | ||
| func CreateTarFromFiles(files map[string][]byte) ([]byte, error) { | ||
| buf := new(bytes.Buffer) | ||
| writer := tar.NewWriter(buf) | ||
| dirs := []string{} | ||
| for name, content := range files { | ||
| // We need to add directories before any files that use them. But, we only need to do this | ||
| // once. | ||
| dir := filepath.Dir(name) | ||
| if dir != "." && !slices.Contains(dirs, dir) { | ||
| dirs = append(dirs, dir) | ||
| err := writer.WriteHeader(&tar.Header{ | ||
| Name: dir, | ||
| Mode: 0o755, | ||
| Typeflag: tar.TypeDir, | ||
| }) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| } | ||
| err := writer.WriteHeader(&tar.Header{ | ||
| Name: name, | ||
| Size: int64(len(content)), | ||
| Mode: 0o644, | ||
| }) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| _, err = writer.Write(content) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| } | ||
| // `writer.Close()` function flushes the writer buffer, and adds extra padding to create a legal tarball. | ||
| err := writer.Close() | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| return buf.Bytes(), nil | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| package prebuilds | ||
| import ( | ||
| "sync" | ||
| "time" | ||
| "github.com/google/uuid" | ||
| "golang.org/x/xerrors" | ||
| "github.com/coder/quartz" | ||
| ) | ||
| type Config struct { | ||
| // OrganizationID is the ID of the organization to create the prebuilds in. | ||
| OrganizationID uuid.UUID `json:"organization_id"` | ||
| // NumPresets is the number of presets the template should have. | ||
| NumPresets int `json:"num_presets"` | ||
| // NumPresetPrebuilds is the number of prebuilds per preset. | ||
| // Total prebuilds = NumPresets * NumPresetPrebuilds | ||
| NumPresetPrebuilds int `json:"num_preset_prebuilds"` | ||
| // TemplateVersionJobTimeout is how long to wait for template version | ||
| // provisioning jobs to complete. | ||
| TemplateVersionJobTimeout time.Duration `json:"template_version_job_timeout"` | ||
| // PrebuildWorkspaceTimeout is how long to wait for all prebuild | ||
| // workspaces to be created and completed. | ||
| PrebuildWorkspaceTimeout time.Duration `json:"prebuild_workspace_timeout"` | ||
| Metrics *Metrics `json:"-"` | ||
| // SetupBarrier is used to ensure all templates have been created | ||
| // before unpausing prebuilds. | ||
| SetupBarrier *sync.WaitGroup `json:"-"` | ||
| // DeletionBarrier is used to ensure all templates have been updated | ||
| // with 0 prebuilds before resuming prebuilds. | ||
| DeletionBarrier *sync.WaitGroup `json:"-"` | ||
| Clock quartz.Clock `json:"-"` | ||
| } | ||
| func (c Config) Validate() error { | ||
| if c.TemplateVersionJobTimeout <= 0 { | ||
| return xerrors.New("template_version_job_timeout must be greater than 0") | ||
| } | ||
| if c.PrebuildWorkspaceTimeout <= 0 { | ||
| return xerrors.New("prebuild_workspace_timeout must be greater than 0") | ||
| } | ||
| if c.SetupBarrier == nil { | ||
| return xerrors.New("setup barrier must be set") | ||
| } | ||
| if c.DeletionBarrier == nil { | ||
| return xerrors.New("deletion barrier must be set") | ||
| } | ||
| if c.Metrics == nil { | ||
| return xerrors.New("metrics must be set") | ||
| } | ||
| if c.Clock == nil { | ||
| return xerrors.New("clock must be set") | ||
| } | ||
| return nil | ||
| } |
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.