Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

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

Open
ethanndickson wants to merge1 commit intomain
base:main
Choose a base branch
Loading
fromethan/prebuilds-runner
Open
Show file tree
Hide file tree
Changes fromall commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
164 changes: 164 additions & 0 deletionsenterprise/scaletest/prebuilds/run_test.go
View file
Open in desktop
Original file line numberDiff line numberDiff 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")
Copy link
MemberAuthor

@ethanndicksonethanndicksonOct 30, 2025
edited
Loading

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

I had a brief attempt at doing this with a fake API client, and it just becomes so complex so quickly. Because we're uploading the template from within the runner, we can't use the echo provisioner either, which would be considerably faster. With the Terraform provisioner it takes about 2 minutes.


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))
}
}
48 changes: 2 additions & 46 deletionsscaletest/dynamicparameters/template.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,12 @@
package dynamicparameters

import (
"archive/tar"
"bytes"
"context"
_ "embed"
"encoding/json"
"fmt"
"io"
"path/filepath"
"slices"
"strings"
"text/template"
"time"
Expand All@@ -20,6 +17,7 @@ import (
"cdr.dev/slog"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/cryptorand"
"github.com/coder/coder/v2/scaletest/loadtestutil"
"github.com/coder/quartz"
)

Expand DownExpand Up@@ -89,48 +87,6 @@ func GetModuleFiles() map[string][]byte {
}
}

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
}

func TemplateTarData() ([]byte, error) {
mainTF, err := TemplateContent()
if err != nil {
Expand All@@ -144,7 +100,7 @@ func TemplateTarData() ([]byte, error) {
for k, v := range moduleFiles {
files[k] = v
}
tarData, err :=createTarFromFiles(files)
tarData, err :=loadtestutil.CreateTarFromFiles(files)
if err != nil {
return nil, xerrors.Errorf("failed to create tarball: %w", err)
}
Expand Down
50 changes: 50 additions & 0 deletionsscaletest/loadtestutil/files.go
View file
Open in desktop
Original file line numberDiff line numberDiff 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
}
69 changes: 69 additions & 0 deletionsscaletest/prebuilds/config.go
View file
Open in desktop
Original file line numberDiff line numberDiff 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
}
Loading
Loading

[8]ページ先頭

©2009-2025 Movatter.jp