mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
bddb808b25
Fixes all our Go file imports to match the preferred spec that we've _mostly_ been using. For example: ``` import ( "context" "time" "github.com/prometheus/client_golang/prometheus" "golang.org/x/xerrors" "gopkg.in/natefinch/lumberjack.v2" "cdr.dev/slog/v3" "github.com/coder/coder/v2/codersdk/agentsdk" "github.com/coder/serpent" ) ``` 3 groups: standard library, 3rd partly libs, Coder libs. This PR makes the change across the codebase. The PR in the stack above modifies our formatting to maintain this state of affairs, and is a separate PR so it's possible to review that one in detail.
344 lines
9.9 KiB
Go
344 lines
9.9 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/v3"
|
|
"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 (
|
|
ctx = testutil.Context(t, testutil.WaitShort)
|
|
log = testutil.Logger(t)
|
|
clock = quartz.NewMock(t)
|
|
)
|
|
|
|
trapTickerFunc := clock.Trap().TickerFunc("metricscache")
|
|
defer trapTickerFunc.Close()
|
|
|
|
cache, db := newMetricsCache(t, log, clock, metricscache.Intervals{
|
|
TemplateBuildTimes: time.Minute,
|
|
}, 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,
|
|
})
|
|
|
|
// Wait for both ticker functions to be created (template build times and deployment stats)
|
|
trapTickerFunc.MustWait(ctx).MustRelease(ctx)
|
|
trapTickerFunc.MustWait(ctx).MustRelease(ctx)
|
|
|
|
clock.Advance(time.Minute).MustWait(ctx)
|
|
|
|
count, ok := cache.TemplateWorkspaceOwners(template.ID)
|
|
require.True(t, ok, "TemplateWorkspaceOwners should be populated")
|
|
require.Equal(t, 0, count, "should have 0 owners initially")
|
|
|
|
dbgen.Workspace(t, db, database.WorkspaceTable{
|
|
OrganizationID: org.ID,
|
|
TemplateID: template.ID,
|
|
OwnerID: user1.ID,
|
|
})
|
|
|
|
clock.Advance(time.Minute).MustWait(ctx)
|
|
|
|
count, _ = cache.TemplateWorkspaceOwners(template.ID)
|
|
require.Equal(t, 1, count, "should have 1 owner after adding workspace")
|
|
|
|
workspace2 := dbgen.Workspace(t, db, database.WorkspaceTable{
|
|
OrganizationID: org.ID,
|
|
TemplateID: template.ID,
|
|
OwnerID: user2.ID,
|
|
})
|
|
|
|
clock.Advance(time.Minute).MustWait(ctx)
|
|
|
|
count, _ = cache.TemplateWorkspaceOwners(template.ID)
|
|
require.Equal(t, 2, count, "should have 2 owners after adding second workspace")
|
|
|
|
// 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,
|
|
})
|
|
|
|
clock.Advance(time.Minute).MustWait(ctx)
|
|
|
|
count, _ = cache.TemplateWorkspaceOwners(template.ID)
|
|
require.Equal(t, 1, count, "should have 1 owner after deleting workspace")
|
|
}
|
|
|
|
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 (
|
|
ctx = testutil.Context(t, testutil.WaitShort)
|
|
log = testutil.Logger(t)
|
|
clock = quartz.NewMock(t)
|
|
)
|
|
|
|
clock.Set(someDay)
|
|
|
|
trapTickerFunc := clock.Trap().TickerFunc("metricscache")
|
|
|
|
defer trapTickerFunc.Close()
|
|
cache, db := newMetricsCache(t, log, clock, metricscache.Intervals{
|
|
TemplateBuildTimes: time.Minute,
|
|
}, false)
|
|
|
|
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,
|
|
})
|
|
}
|
|
|
|
// Wait for both ticker functions to be created (template build times and deployment stats)
|
|
trapTickerFunc.MustWait(ctx).MustRelease(ctx)
|
|
trapTickerFunc.MustWait(ctx).MustRelease(ctx)
|
|
|
|
clock.Advance(time.Minute).MustWait(ctx)
|
|
|
|
if tt.want.loads {
|
|
wantTransition := codersdk.WorkspaceTransition(tt.args.transition)
|
|
gotStats := cache.TemplateBuildTimeStats(template.ID)
|
|
ts := gotStats[wantTransition]
|
|
require.NotNil(t, ts.P50, "P50 should be set for %v", wantTransition)
|
|
require.Equal(t, tt.want.buildTimeMs, *ts.P50, "P50 should match expected value for %v", wantTransition)
|
|
|
|
for transition, ts := range gotStats {
|
|
if transition == wantTransition {
|
|
// Checked above
|
|
continue
|
|
}
|
|
require.Empty(t, ts, "%v", transition)
|
|
}
|
|
} else {
|
|
stats := cache.TemplateBuildTimeStats(template.ID)
|
|
requireBuildTimeStatsEmpty(t, stats)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCache_DeploymentStats(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var (
|
|
ctx = testutil.Context(t, testutil.WaitShort)
|
|
log = testutil.Logger(t)
|
|
clock = quartz.NewMock(t)
|
|
)
|
|
|
|
tickerTrap := clock.Trap().TickerFunc("metricscache")
|
|
defer tickerTrap.Close()
|
|
|
|
cache, db := newMetricsCache(t, log, clock, metricscache.Intervals{
|
|
DeploymentStats: time.Minute,
|
|
}, 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)
|
|
|
|
// Wait for both ticker functions to be created (template build times and deployment stats)
|
|
tickerTrap.MustWait(ctx).MustRelease(ctx)
|
|
tickerTrap.MustWait(ctx).MustRelease(ctx)
|
|
|
|
clock.Advance(time.Minute).MustWait(ctx)
|
|
|
|
stat, ok := cache.DeploymentStats()
|
|
require.True(t, ok, "cache should be populated after refresh")
|
|
require.Equal(t, int64(1), stat.SessionCount.VSCode)
|
|
}
|