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
Next Next commit
Support delete & stop in build progress bar
  • Loading branch information
ammario committed Oct 16, 2022
commit 0ba618dc2bf136f63000e656c93c20248d99f1d6
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(startTimes)/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
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
;
29 changes: 18 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,22 @@ 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) TemplateAverageBuildTime(id uuid.UUID) (*codersdk.TemplateBuildTimeStats, bool) {
m := c.templateAverageBuildTime.Load()
if m == nil {
// Data loading.
return -1, false
return nil, false
}

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

return &codersdk.TemplateBuildTimeStats{
StartMillis: int64(resp.StartMedian * 1000),
StopMillis: int64(resp.StopMedian * 1000),
DeleteMillis: int64(resp.DeleteMedian * 1000),
}, true
}
2 changes: 1 addition & 1 deletion coderd/templates.go
Original file line number Diff line number Diff line change
Expand Up @@ -792,7 +792,7 @@ func (api *API) convertTemplate(
ActiveVersionID: template.ActiveVersionID,
WorkspaceOwnerCount: workspaceOwnerCount,
ActiveUserCount: activeCount,
AverageBuildTimeMillis: averageBuildTimeMillis,
BuildTimeStats: averageBuildTimeMillis,
Description: template.Description,
Icon: template.Icon,
MaxTTLMillis: time.Duration(template.MaxTtl).Milliseconds(),
Expand Down
4 changes: 2 additions & 2 deletions coderd/templates_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -594,7 +594,7 @@ func TestTemplateMetrics(t *testing.T) {
})
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
require.Equal(t, -1, template.ActiveUserCount)
require.EqualValues(t, -1, template.AverageBuildTimeMillis)
require.EqualValues(t, -1, template.BuildTimeStats)

coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
Expand Down Expand Up @@ -661,7 +661,7 @@ func TestTemplateMetrics(t *testing.T) {
template, err = client.Template(ctx, template.ID)
require.NoError(t, err)
require.Equal(t, 1, template.ActiveUserCount)
require.Greater(t, template.AverageBuildTimeMillis, int64(1))
require.Greater(t, template.BuildTimeStats, int64(1))

workspaces, err = client.Workspaces(ctx, codersdk.WorkspaceFilter{})
require.NoError(t, err)
Expand Down
23 changes: 14 additions & 9 deletions codersdk/templates.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,20 @@ type Template struct {
ActiveVersionID uuid.UUID `json:"active_version_id"`
WorkspaceOwnerCount uint32 `json:"workspace_owner_count"`
// ActiveUserCount is set to -1 when loading.
ActiveUserCount int `json:"active_user_count"`
// AverageBuildTimeMillis is set to -1 when there aren't enough recent builds.
AverageBuildTimeMillis int64 `json:"average_build_time_ms"`
Description string `json:"description"`
Icon string `json:"icon"`
MaxTTLMillis int64 `json:"max_ttl_ms"`
MinAutostartIntervalMillis int64 `json:"min_autostart_interval_ms"`
CreatedByID uuid.UUID `json:"created_by_id"`
CreatedByName string `json:"created_by_name"`
ActiveUserCount int `json:"active_user_count"`
BuildTimeStats TemplateBuildTimeStats `json:"build_time_stats"`
Description string `json:"description"`
Icon string `json:"icon"`
MaxTTLMillis int64 `json:"max_ttl_ms"`
MinAutostartIntervalMillis int64 `json:"min_autostart_interval_ms"`
CreatedByID uuid.UUID `json:"created_by_id"`
CreatedByName string `json:"created_by_name"`
}

type TemplateBuildTimeStats struct {
StartMillis int64 `json:"start_ms"`
StopMillis int64 `json:"stop_ms"`
DeleteMillis int64 `json:"delete_ms"`
}

type UpdateActiveTemplateVersion struct {
Expand Down