Files
coder/coderd/metricscache/metricscache_test.go
Danielle Maywood 6dd51f92fb chore: test metricscache on postgres (#16711)
metricscache_test has been running tests against dbmem only, instead of
against postgres. Unfortunately the implementations of
GetTemplateAverageBuildTime have diverged between dbmem and postgres.
This change gets the tests working on Postgres and test for the
behaviour postgres provides.
2025-02-27 09:43:51 +00:00

336 lines
9.4 KiB
Go

package metricscache_test
import (
"context"
"database/sql"
"encoding/json"
"sync/atomic"
"testing"
"time"
"github.com/google/uuid"
"github.com/prometheus/client_golang/prometheus"
"github.com/stretchr/testify/require"
"cdr.dev/slog"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/dbauthz"
"github.com/coder/coder/v2/coderd/database/dbgen"
"github.com/coder/coder/v2/coderd/database/dbtestutil"
"github.com/coder/coder/v2/coderd/metricscache"
"github.com/coder/coder/v2/coderd/rbac"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/testutil"
"github.com/coder/quartz"
)
func date(year, month, day int) time.Time {
return time.Date(year, time.Month(month), day, 0, 0, 0, 0, time.UTC)
}
func newMetricsCache(t *testing.T, log slog.Logger, clock quartz.Clock, intervals metricscache.Intervals, usage bool) (*metricscache.Cache, database.Store) {
t.Helper()
accessControlStore := &atomic.Pointer[dbauthz.AccessControlStore]{}
var acs dbauthz.AccessControlStore = dbauthz.AGPLTemplateAccessControlStore{}
accessControlStore.Store(&acs)
var (
auth = rbac.NewStrictCachingAuthorizer(prometheus.NewRegistry())
db, _ = dbtestutil.NewDB(t)
dbauth = dbauthz.New(db, auth, log, accessControlStore)
cache = metricscache.New(dbauth, log, clock, intervals, usage)
)
t.Cleanup(func() { cache.Close() })
return cache, db
}
func TestCache_TemplateWorkspaceOwners(t *testing.T) {
t.Parallel()
var ()
var (
log = testutil.Logger(t)
clock = quartz.NewReal()
cache, db = newMetricsCache(t, log, clock, metricscache.Intervals{
TemplateBuildTimes: testutil.IntervalFast,
}, false)
)
org := dbgen.Organization(t, db, database.Organization{})
user1 := dbgen.User(t, db, database.User{})
user2 := dbgen.User(t, db, database.User{})
template := dbgen.Template(t, db, database.Template{
OrganizationID: org.ID,
Provisioner: database.ProvisionerTypeEcho,
CreatedBy: user1.ID,
})
require.Eventuallyf(t, func() bool {
count, ok := cache.TemplateWorkspaceOwners(template.ID)
return ok && count == 0
}, testutil.WaitShort, testutil.IntervalMedium,
"TemplateWorkspaceOwners never populated 0 owners",
)
dbgen.Workspace(t, db, database.WorkspaceTable{
OrganizationID: org.ID,
TemplateID: template.ID,
OwnerID: user1.ID,
})
require.Eventuallyf(t, func() bool {
count, _ := cache.TemplateWorkspaceOwners(template.ID)
return count == 1
}, testutil.WaitShort, testutil.IntervalMedium,
"TemplateWorkspaceOwners never populated 1 owner",
)
workspace2 := dbgen.Workspace(t, db, database.WorkspaceTable{
OrganizationID: org.ID,
TemplateID: template.ID,
OwnerID: user2.ID,
})
require.Eventuallyf(t, func() bool {
count, _ := cache.TemplateWorkspaceOwners(template.ID)
return count == 2
}, testutil.WaitShort, testutil.IntervalMedium,
"TemplateWorkspaceOwners never populated 2 owners",
)
// 3rd workspace should not be counted since we have the same owner as workspace2.
dbgen.Workspace(t, db, database.WorkspaceTable{
OrganizationID: org.ID,
TemplateID: template.ID,
OwnerID: user1.ID,
})
db.UpdateWorkspaceDeletedByID(context.Background(), database.UpdateWorkspaceDeletedByIDParams{
ID: workspace2.ID,
Deleted: true,
})
require.Eventuallyf(t, func() bool {
count, _ := cache.TemplateWorkspaceOwners(template.ID)
return count == 1
}, testutil.WaitShort, testutil.IntervalMedium,
"TemplateWorkspaceOwners never populated 1 owner after delete",
)
}
func clockTime(t time.Time, hour, minute, sec int) time.Time {
return time.Date(t.Year(), t.Month(), t.Day(), hour, minute, sec, t.Nanosecond(), t.Location())
}
func requireBuildTimeStatsEmpty(t *testing.T, stats codersdk.TemplateBuildTimeStats) {
require.Empty(t, stats[codersdk.WorkspaceTransitionStart])
require.Empty(t, stats[codersdk.WorkspaceTransitionStop])
require.Empty(t, stats[codersdk.WorkspaceTransitionDelete])
}
func TestCache_BuildTime(t *testing.T) {
t.Parallel()
someDay := date(2022, 10, 1)
type jobParams struct {
startedAt time.Time
completedAt time.Time
}
type args struct {
rows []jobParams
transition database.WorkspaceTransition
}
type want struct {
buildTimeMs int64
loads bool
}
tests := []struct {
name string
args args
want want
}{
{"empty", args{}, want{-1, false}},
{
"one/start", args{
rows: []jobParams{
{
startedAt: clockTime(someDay, 10, 1, 0),
completedAt: clockTime(someDay, 10, 1, 10),
},
},
transition: database.WorkspaceTransitionStart,
}, want{10 * 1000, true},
},
{
"two/stop", args{
rows: []jobParams{
{
startedAt: clockTime(someDay, 10, 1, 0),
completedAt: clockTime(someDay, 10, 1, 10),
},
{
startedAt: clockTime(someDay, 10, 1, 0),
completedAt: clockTime(someDay, 10, 1, 50),
},
},
transition: database.WorkspaceTransitionStop,
}, want{10 * 1000, true},
},
{
"three/delete", args{
rows: []jobParams{
{
startedAt: clockTime(someDay, 10, 1, 0),
completedAt: clockTime(someDay, 10, 1, 10),
},
{
startedAt: clockTime(someDay, 10, 1, 0),
completedAt: clockTime(someDay, 10, 1, 50),
},
{
startedAt: clockTime(someDay, 10, 1, 0),
completedAt: clockTime(someDay, 10, 1, 20),
},
},
transition: database.WorkspaceTransitionDelete,
}, want{20 * 1000, true},
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
var (
log = testutil.Logger(t)
clock = quartz.NewMock(t)
cache, db = newMetricsCache(t, log, clock, metricscache.Intervals{
TemplateBuildTimes: testutil.IntervalFast,
}, false)
)
clock.Set(someDay)
org := dbgen.Organization(t, db, database.Organization{})
user := dbgen.User(t, db, database.User{})
template := dbgen.Template(t, db, database.Template{
CreatedBy: user.ID,
OrganizationID: org.ID,
})
templateVersion := dbgen.TemplateVersion(t, db, database.TemplateVersion{
OrganizationID: org.ID,
CreatedBy: user.ID,
TemplateID: uuid.NullUUID{UUID: template.ID, Valid: true},
})
workspace := dbgen.Workspace(t, db, database.WorkspaceTable{
OrganizationID: org.ID,
OwnerID: user.ID,
TemplateID: template.ID,
})
gotStats := cache.TemplateBuildTimeStats(template.ID)
requireBuildTimeStatsEmpty(t, gotStats)
for buildNumber, row := range tt.args.rows {
job := dbgen.ProvisionerJob(t, db, nil, database.ProvisionerJob{
OrganizationID: org.ID,
InitiatorID: user.ID,
Type: database.ProvisionerJobTypeWorkspaceBuild,
StartedAt: sql.NullTime{Time: row.startedAt, Valid: true},
CompletedAt: sql.NullTime{Time: row.completedAt, Valid: true},
})
dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{
BuildNumber: int32(1 + buildNumber),
WorkspaceID: workspace.ID,
InitiatorID: user.ID,
TemplateVersionID: templateVersion.ID,
JobID: job.ID,
Transition: tt.args.transition,
})
}
if tt.want.loads {
wantTransition := codersdk.WorkspaceTransition(tt.args.transition)
require.Eventuallyf(t, func() bool {
stats := cache.TemplateBuildTimeStats(template.ID)
return stats[wantTransition] != codersdk.TransitionStats{}
}, testutil.WaitLong, testutil.IntervalMedium,
"BuildTime never populated",
)
gotStats = cache.TemplateBuildTimeStats(template.ID)
for transition, stats := range gotStats {
if transition == wantTransition {
require.Equal(t, tt.want.buildTimeMs, *stats.P50)
} else {
require.Empty(
t, stats, "%v", transition,
)
}
}
} else {
var stats codersdk.TemplateBuildTimeStats
require.Never(t, func() bool {
stats = cache.TemplateBuildTimeStats(template.ID)
requireBuildTimeStatsEmpty(t, stats)
return t.Failed()
}, testutil.WaitShort/2, testutil.IntervalMedium,
"BuildTimeStats populated", stats,
)
}
})
}
}
func TestCache_DeploymentStats(t *testing.T) {
t.Parallel()
var (
log = testutil.Logger(t)
clock = quartz.NewMock(t)
cache, db = newMetricsCache(t, log, clock, metricscache.Intervals{
DeploymentStats: testutil.IntervalFast,
}, false)
)
err := db.InsertWorkspaceAgentStats(context.Background(), database.InsertWorkspaceAgentStatsParams{
ID: []uuid.UUID{uuid.New()},
CreatedAt: []time.Time{clock.Now()},
WorkspaceID: []uuid.UUID{uuid.New()},
UserID: []uuid.UUID{uuid.New()},
TemplateID: []uuid.UUID{uuid.New()},
AgentID: []uuid.UUID{uuid.New()},
ConnectionsByProto: json.RawMessage(`[{}]`),
RxPackets: []int64{0},
RxBytes: []int64{1},
TxPackets: []int64{0},
TxBytes: []int64{1},
ConnectionCount: []int64{1},
SessionCountVSCode: []int64{1},
SessionCountJetBrains: []int64{0},
SessionCountReconnectingPTY: []int64{0},
SessionCountSSH: []int64{0},
ConnectionMedianLatencyMS: []float64{10},
Usage: []bool{false},
})
require.NoError(t, err)
var stat codersdk.DeploymentStats
require.Eventually(t, func() bool {
var ok bool
stat, ok = cache.DeploymentStats()
return ok
}, testutil.WaitLong, testutil.IntervalMedium)
require.Equal(t, int64(1), stat.SessionCount.VSCode)
}