mirror of
https://github.com/coder/coder.git
synced 2025-07-03 16:13:58 +00:00
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.
336 lines
9.4 KiB
Go
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)
|
|
}
|