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