Skip to content

Support delete & stop transitions in build progress bar #4575

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 our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Oct 17, 2022
Merged
Show file tree
Hide file tree
Changes from all 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
41 changes: 29 additions & 12 deletions coderd/database/databasefake/databasefake.go
Original file line number Diff line number Diff line change
Expand Up @@ -235,33 +235,50 @@ func (q *fakeQuerier) GetTemplateDAUs(_ context.Context, templateID uuid.UUID) (
return rs, nil
}

func (q *fakeQuerier) GetTemplateAverageBuildTime(ctx context.Context, arg database.GetTemplateAverageBuildTimeParams) (float64, error) {
var times []float64
func (q *fakeQuerier) GetTemplateAverageBuildTime(ctx context.Context, arg database.GetTemplateAverageBuildTimeParams) (database.GetTemplateAverageBuildTimeRow, error) {
var emptyRow database.GetTemplateAverageBuildTimeRow
var (
startTimes []float64
stopTimes []float64
deleteTimes []float64
)
for _, wb := range q.workspaceBuilds {
if wb.Transition != database.WorkspaceTransitionStart {
continue
}
version, err := q.GetTemplateVersionByID(ctx, wb.TemplateVersionID)
if err != nil {
return -1, err
return emptyRow, err
}
if version.TemplateID != arg.TemplateID {
continue
}

job, err := q.GetProvisionerJobByID(ctx, wb.JobID)
if err != nil {
return -1, err
return emptyRow, err
}
if job.CompletedAt.Valid {
times = append(times, job.CompletedAt.Time.Sub(job.StartedAt.Time).Seconds())
took := job.CompletedAt.Time.Sub(job.StartedAt.Time).Seconds()
if wb.Transition == database.WorkspaceTransitionStart {
startTimes = append(startTimes, took)
} else if wb.Transition == database.WorkspaceTransitionStop {
stopTimes = append(stopTimes, took)
} else if wb.Transition == database.WorkspaceTransitionDelete {
deleteTimes = append(deleteTimes, took)
}
}
}
sort.Float64s(times)
if len(times) == 0 {
return -1, nil

tryMedian := func(fs []float64) float64 {
if len(fs) == 0 {
return -1
}
sort.Float64s(fs)
return fs[len(fs)/2]
}
return times[len(times)/2], nil
var row database.GetTemplateAverageBuildTimeRow
row.DeleteMedian = tryMedian(deleteTimes)
row.StopMedian = tryMedian(stopTimes)
row.StartMedian = tryMedian(startTimes)
return row, nil
}

func (q *fakeQuerier) ParameterValue(_ context.Context, id uuid.UUID) (database.ParameterValue, error) {
Expand Down
4 changes: 4 additions & 0 deletions coderd/database/dump.sql

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 7 additions & 4 deletions coderd/database/migrations/000059_file_id.up.sql
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
-- template to be able to push and read files used for template
-- versions they create.
-- Prior to this collisions on file.hash were not an issue
-- since users who could push files could also read all files.
--
-- since users who could push files could also read all files.
--
-- This migration also adds a 'files.id' column as the primary
-- key. As a side effect the provisioner_jobs must now reference
-- the files.id column since the 'hash' column is now ambiguous.
Expand All @@ -14,10 +14,13 @@ BEGIN;
-- Drop the primary key on hash.
ALTER TABLE files DROP CONSTRAINT files_pkey;

-- This extension is required by gen_random_uuid
CREATE EXTENSION IF NOT EXISTS pgcrypto;

-- Add an 'id' column and designate it the primary key.
ALTER TABLE files ADD COLUMN
ALTER TABLE files ADD COLUMN
id uuid NOT NULL PRIMARY KEY DEFAULT gen_random_uuid ();

-- Update the constraint to include the user who created it.
ALTER TABLE files ADD UNIQUE(hash, created_by);

Expand Down
2 changes: 1 addition & 1 deletion coderd/database/querier.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

25 changes: 18 additions & 7 deletions coderd/database/queries.sql.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 8 additions & 3 deletions coderd/database/queries/templates.sql
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,8 @@ RETURNING
-- name: GetTemplateAverageBuildTime :one
WITH build_times AS (
SELECT
EXTRACT(EPOCH FROM (pj.completed_at - pj.started_at))::FLOAT AS exec_time_sec
EXTRACT(EPOCH FROM (pj.completed_at - pj.started_at))::FLOAT AS exec_time_sec,
workspace_builds.transition
FROM
workspace_builds
JOIN template_versions ON
Expand All @@ -118,14 +119,18 @@ JOIN provisioner_jobs pj ON
workspace_builds.job_id = pj.id
WHERE
template_versions.template_id = @template_id AND
(workspace_builds.transition = 'start') AND
(pj.completed_at IS NOT NULL) AND (pj.started_at IS NOT NULL) AND
(pj.started_at > @start_time) AND
(pj.canceled_at IS NULL) AND
((pj.error IS NULL) OR (pj.error = ''))
ORDER BY
workspace_builds.created_at DESC
)
SELECT coalesce((PERCENTILE_DISC(0.5) WITHIN GROUP(ORDER BY exec_time_sec)), -1)::FLOAT
SELECT
-- Postgres offers no clear way to DRY this short of a function or other
-- complexities.
coalesce((PERCENTILE_DISC(0.5) WITHIN GROUP(ORDER BY exec_time_sec) FILTER (WHERE transition = 'start')), -1)::FLOAT AS start_median,
coalesce((PERCENTILE_DISC(0.5) WITHIN GROUP(ORDER BY exec_time_sec) FILTER (WHERE transition = 'stop')), -1)::FLOAT AS stop_median,
coalesce((PERCENTILE_DISC(0.5) WITHIN GROUP(ORDER BY exec_time_sec) FILTER (WHERE transition = 'delete')), -1)::FLOAT AS delete_median
FROM build_times
;
39 changes: 28 additions & 11 deletions coderd/metricscache/metricscache.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ type Cache struct {

templateDAUResponses atomic.Pointer[map[uuid.UUID]codersdk.TemplateDAUsResponse]
templateUniqueUsers atomic.Pointer[map[uuid.UUID]int]
templateAverageBuildTime atomic.Pointer[map[uuid.UUID]time.Duration]
templateAverageBuildTime atomic.Pointer[map[uuid.UUID]database.GetTemplateAverageBuildTimeRow]

done chan struct{}
cancel func()
Expand Down Expand Up @@ -130,9 +130,9 @@ func (c *Cache) refresh(ctx context.Context) error {
}

var (
templateDAUs = make(map[uuid.UUID]codersdk.TemplateDAUsResponse, len(templates))
templateUniqueUsers = make(map[uuid.UUID]int)
templateAverageBuildTimeSec = make(map[uuid.UUID]time.Duration)
templateDAUs = make(map[uuid.UUID]codersdk.TemplateDAUsResponse, len(templates))
templateUniqueUsers = make(map[uuid.UUID]int)
templateAverageBuildTimes = make(map[uuid.UUID]database.GetTemplateAverageBuildTimeRow)
)
for _, template := range templates {
rows, err := c.database.GetTemplateDAUs(ctx, template.ID)
Expand All @@ -141,6 +141,7 @@ func (c *Cache) refresh(ctx context.Context) error {
}
templateDAUs[template.ID] = convertDAUResponse(rows)
templateUniqueUsers[template.ID] = countUniqueUsers(rows)

templateAvgBuildTime, err := c.database.GetTemplateAverageBuildTime(ctx, database.GetTemplateAverageBuildTimeParams{
TemplateID: uuid.NullUUID{
UUID: template.ID,
Expand All @@ -151,14 +152,15 @@ func (c *Cache) refresh(ctx context.Context) error {
Valid: true,
},
})

if err != nil {
return err
}
templateAverageBuildTimeSec[template.ID] = time.Duration(float64(time.Second) * templateAvgBuildTime)
templateAverageBuildTimes[template.ID] = templateAvgBuildTime
}
c.templateDAUResponses.Store(&templateDAUs)
c.templateUniqueUsers.Store(&templateUniqueUsers)
c.templateAverageBuildTime.Store(&templateAverageBuildTimeSec)
c.templateAverageBuildTime.Store(&templateAverageBuildTimes)

return nil
}
Expand Down Expand Up @@ -239,17 +241,32 @@ func (c *Cache) TemplateUniqueUsers(id uuid.UUID) (int, bool) {
return resp, true
}

func (c *Cache) TemplateAverageBuildTime(id uuid.UUID) (time.Duration, bool) {
func (c *Cache) TemplateBuildTimeStats(id uuid.UUID) codersdk.TemplateBuildTimeStats {
var unknown codersdk.TemplateBuildTimeStats

m := c.templateAverageBuildTime.Load()
if m == nil {
// Data loading.
return -1, false
return unknown
}

resp, ok := (*m)[id]
if !ok || resp <= 0 {
if !ok {
// No data or not enough builds.
return -1, false
return unknown
}

convertMedian := func(m float64) *int64 {
if m <= 0 {
return nil
}
i := int64(m * 1000)
return &i
}

return codersdk.TemplateBuildTimeStats{
StartMillis: convertMedian(resp.StartMedian),
StopMillis: convertMedian(resp.StopMedian),
DeleteMillis: convertMedian(resp.DeleteMedian),
}
return resp, true
}
Loading