mirror of https://github.com/coder/coder.git
fix: use unique workspace owners over unique users (#11044)
This commit is contained in:
parent
091fdd6761
commit
8aea6040c8
|
@ -2053,6 +2053,13 @@ func (q *querier) GetWorkspaceResourcesCreatedAfter(ctx context.Context, created
|
|||
return q.db.GetWorkspaceResourcesCreatedAfter(ctx, createdAt)
|
||||
}
|
||||
|
||||
func (q *querier) GetWorkspaceUniqueOwnerCountByTemplateIDs(ctx context.Context, templateIds []uuid.UUID) ([]database.GetWorkspaceUniqueOwnerCountByTemplateIDsRow, error) {
|
||||
if err := q.authorizeContext(ctx, rbac.ActionRead, rbac.ResourceSystem); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return q.db.GetWorkspaceUniqueOwnerCountByTemplateIDs(ctx, templateIds)
|
||||
}
|
||||
|
||||
func (q *querier) GetWorkspaces(ctx context.Context, arg database.GetWorkspacesParams) ([]database.GetWorkspacesRow, error) {
|
||||
prep, err := prepareSQLFilter(ctx, q.auth, rbac.ActionRead, rbac.ResourceWorkspace.Type)
|
||||
if err != nil {
|
||||
|
|
|
@ -4520,6 +4520,36 @@ func (q *FakeQuerier) GetWorkspaceResourcesCreatedAfter(_ context.Context, after
|
|||
return resources, nil
|
||||
}
|
||||
|
||||
func (q *FakeQuerier) GetWorkspaceUniqueOwnerCountByTemplateIDs(_ context.Context, templateIds []uuid.UUID) ([]database.GetWorkspaceUniqueOwnerCountByTemplateIDsRow, error) {
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
|
||||
workspaceOwners := make(map[uuid.UUID]map[uuid.UUID]struct{})
|
||||
for _, workspace := range q.workspaces {
|
||||
if workspace.Deleted {
|
||||
continue
|
||||
}
|
||||
if !slices.Contains(templateIds, workspace.TemplateID) {
|
||||
continue
|
||||
}
|
||||
_, ok := workspaceOwners[workspace.TemplateID]
|
||||
if !ok {
|
||||
workspaceOwners[workspace.TemplateID] = make(map[uuid.UUID]struct{})
|
||||
}
|
||||
workspaceOwners[workspace.TemplateID][workspace.OwnerID] = struct{}{}
|
||||
}
|
||||
resp := make([]database.GetWorkspaceUniqueOwnerCountByTemplateIDsRow, 0)
|
||||
for _, templateID := range templateIds {
|
||||
count := len(workspaceOwners[templateID])
|
||||
resp = append(resp, database.GetWorkspaceUniqueOwnerCountByTemplateIDsRow{
|
||||
TemplateID: templateID,
|
||||
UniqueOwnersSum: int64(count),
|
||||
})
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (q *FakeQuerier) GetWorkspaces(ctx context.Context, arg database.GetWorkspacesParams) ([]database.GetWorkspacesRow, error) {
|
||||
if err := validateDatabaseType(arg); err != nil {
|
||||
return nil, err
|
||||
|
|
|
@ -1229,6 +1229,13 @@ func (m metricsStore) GetWorkspaceResourcesCreatedAfter(ctx context.Context, cre
|
|||
return resources, err
|
||||
}
|
||||
|
||||
func (m metricsStore) GetWorkspaceUniqueOwnerCountByTemplateIDs(ctx context.Context, templateIds []uuid.UUID) ([]database.GetWorkspaceUniqueOwnerCountByTemplateIDsRow, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.GetWorkspaceUniqueOwnerCountByTemplateIDs(ctx, templateIds)
|
||||
m.queryLatencies.WithLabelValues("GetWorkspaceUniqueOwnerCountByTemplateIDs").Observe(time.Since(start).Seconds())
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m metricsStore) GetWorkspaces(ctx context.Context, arg database.GetWorkspacesParams) ([]database.GetWorkspacesRow, error) {
|
||||
start := time.Now()
|
||||
workspaces, err := m.s.GetWorkspaces(ctx, arg)
|
||||
|
|
|
@ -2568,6 +2568,21 @@ func (mr *MockStoreMockRecorder) GetWorkspaceResourcesCreatedAfter(arg0, arg1 in
|
|||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaceResourcesCreatedAfter", reflect.TypeOf((*MockStore)(nil).GetWorkspaceResourcesCreatedAfter), arg0, arg1)
|
||||
}
|
||||
|
||||
// GetWorkspaceUniqueOwnerCountByTemplateIDs mocks base method.
|
||||
func (m *MockStore) GetWorkspaceUniqueOwnerCountByTemplateIDs(arg0 context.Context, arg1 []uuid.UUID) ([]database.GetWorkspaceUniqueOwnerCountByTemplateIDsRow, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetWorkspaceUniqueOwnerCountByTemplateIDs", arg0, arg1)
|
||||
ret0, _ := ret[0].([]database.GetWorkspaceUniqueOwnerCountByTemplateIDsRow)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetWorkspaceUniqueOwnerCountByTemplateIDs indicates an expected call of GetWorkspaceUniqueOwnerCountByTemplateIDs.
|
||||
func (mr *MockStoreMockRecorder) GetWorkspaceUniqueOwnerCountByTemplateIDs(arg0, arg1 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaceUniqueOwnerCountByTemplateIDs", reflect.TypeOf((*MockStore)(nil).GetWorkspaceUniqueOwnerCountByTemplateIDs), arg0, arg1)
|
||||
}
|
||||
|
||||
// GetWorkspaces mocks base method.
|
||||
func (m *MockStore) GetWorkspaces(arg0 context.Context, arg1 database.GetWorkspacesParams) ([]database.GetWorkspacesRow, error) {
|
||||
m.ctrl.T.Helper()
|
||||
|
|
|
@ -253,6 +253,7 @@ type sqlcQuerier interface {
|
|||
GetWorkspaceResourcesByJobID(ctx context.Context, jobID uuid.UUID) ([]WorkspaceResource, error)
|
||||
GetWorkspaceResourcesByJobIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceResource, error)
|
||||
GetWorkspaceResourcesCreatedAfter(ctx context.Context, createdAt time.Time) ([]WorkspaceResource, error)
|
||||
GetWorkspaceUniqueOwnerCountByTemplateIDs(ctx context.Context, templateIds []uuid.UUID) ([]GetWorkspaceUniqueOwnerCountByTemplateIDsRow, error)
|
||||
GetWorkspaces(ctx context.Context, arg GetWorkspacesParams) ([]GetWorkspacesRow, error)
|
||||
GetWorkspacesEligibleForTransition(ctx context.Context, now time.Time) ([]Workspace, error)
|
||||
InsertAPIKey(ctx context.Context, arg InsertAPIKeyParams) (APIKey, error)
|
||||
|
|
|
@ -10734,6 +10734,44 @@ func (q *sqlQuerier) GetWorkspaceByWorkspaceAppID(ctx context.Context, workspace
|
|||
return i, err
|
||||
}
|
||||
|
||||
const getWorkspaceUniqueOwnerCountByTemplateIDs = `-- name: GetWorkspaceUniqueOwnerCountByTemplateIDs :many
|
||||
SELECT
|
||||
template_id, COUNT(DISTINCT owner_id) AS unique_owners_sum
|
||||
FROM
|
||||
workspaces
|
||||
WHERE
|
||||
template_id = ANY($1 :: uuid[]) AND deleted = false
|
||||
GROUP BY template_id
|
||||
`
|
||||
|
||||
type GetWorkspaceUniqueOwnerCountByTemplateIDsRow struct {
|
||||
TemplateID uuid.UUID `db:"template_id" json:"template_id"`
|
||||
UniqueOwnersSum int64 `db:"unique_owners_sum" json:"unique_owners_sum"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) GetWorkspaceUniqueOwnerCountByTemplateIDs(ctx context.Context, templateIds []uuid.UUID) ([]GetWorkspaceUniqueOwnerCountByTemplateIDsRow, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getWorkspaceUniqueOwnerCountByTemplateIDs, pq.Array(templateIds))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetWorkspaceUniqueOwnerCountByTemplateIDsRow
|
||||
for rows.Next() {
|
||||
var i GetWorkspaceUniqueOwnerCountByTemplateIDsRow
|
||||
if err := rows.Scan(&i.TemplateID, &i.UniqueOwnersSum); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getWorkspaces = `-- name: GetWorkspaces :many
|
||||
SELECT
|
||||
workspaces.id, workspaces.created_at, workspaces.updated_at, workspaces.owner_id, workspaces.organization_id, workspaces.template_id, workspaces.deleted, workspaces.name, workspaces.autostart_schedule, workspaces.ttl, workspaces.last_used_at, workspaces.dormant_at, workspaces.deleting_at, workspaces.automatic_updates,
|
||||
|
|
|
@ -287,6 +287,15 @@ WHERE
|
|||
AND LOWER("name") = LOWER(@name)
|
||||
ORDER BY created_at DESC;
|
||||
|
||||
-- name: GetWorkspaceUniqueOwnerCountByTemplateIDs :many
|
||||
SELECT
|
||||
template_id, COUNT(DISTINCT owner_id) AS unique_owners_sum
|
||||
FROM
|
||||
workspaces
|
||||
WHERE
|
||||
template_id = ANY(@template_ids :: uuid[]) AND deleted = false
|
||||
GROUP BY template_id;
|
||||
|
||||
-- name: InsertWorkspace :one
|
||||
INSERT INTO
|
||||
workspaces (
|
||||
|
|
|
@ -52,6 +52,7 @@ type Cache struct {
|
|||
deploymentDAUResponses atomic.Pointer[map[int]codersdk.DAUsResponse]
|
||||
templateDAUResponses atomic.Pointer[map[int]map[uuid.UUID]codersdk.DAUsResponse]
|
||||
templateUniqueUsers atomic.Pointer[map[uuid.UUID]int]
|
||||
templateWorkspaceOwners atomic.Pointer[map[uuid.UUID]int]
|
||||
templateAverageBuildTime atomic.Pointer[map[uuid.UUID]database.GetTemplateAverageBuildTimeRow]
|
||||
deploymentStatsResponse atomic.Pointer[codersdk.DeploymentStats]
|
||||
|
||||
|
@ -206,6 +207,7 @@ func (c *Cache) refreshTemplateDAUs(ctx context.Context) error {
|
|||
var (
|
||||
templateDAUs = make(map[int]map[uuid.UUID]codersdk.DAUsResponse, len(templates))
|
||||
templateUniqueUsers = make(map[uuid.UUID]int)
|
||||
templateWorkspaceOwners = make(map[uuid.UUID]int)
|
||||
templateAverageBuildTimes = make(map[uuid.UUID]database.GetTemplateAverageBuildTimeRow)
|
||||
)
|
||||
|
||||
|
@ -214,7 +216,9 @@ func (c *Cache) refreshTemplateDAUs(ctx context.Context) error {
|
|||
return xerrors.Errorf("deployment daus: %w", err)
|
||||
}
|
||||
|
||||
ids := make([]uuid.UUID, 0, len(templates))
|
||||
for _, template := range templates {
|
||||
ids = append(ids, template.ID)
|
||||
for _, tzOffset := range templateTimezoneOffsets {
|
||||
rows, err := c.database.GetTemplateDAUs(ctx, database.GetTemplateDAUsParams{
|
||||
TemplateID: template.ID,
|
||||
|
@ -249,6 +253,17 @@ func (c *Cache) refreshTemplateDAUs(ctx context.Context) error {
|
|||
}
|
||||
templateAverageBuildTimes[template.ID] = templateAvgBuildTime
|
||||
}
|
||||
|
||||
owners, err := c.database.GetWorkspaceUniqueOwnerCountByTemplateIDs(ctx, ids)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("get workspace unique owner count by template ids: %w", err)
|
||||
}
|
||||
|
||||
for _, owner := range owners {
|
||||
templateWorkspaceOwners[owner.TemplateID] = int(owner.UniqueOwnersSum)
|
||||
}
|
||||
|
||||
c.templateWorkspaceOwners.Store(&templateWorkspaceOwners)
|
||||
c.templateDAUResponses.Store(&templateDAUs)
|
||||
c.templateUniqueUsers.Store(&templateUniqueUsers)
|
||||
c.templateAverageBuildTime.Store(&templateAverageBuildTimes)
|
||||
|
@ -469,6 +484,21 @@ func (c *Cache) TemplateBuildTimeStats(id uuid.UUID) codersdk.TemplateBuildTimeS
|
|||
}
|
||||
}
|
||||
|
||||
func (c *Cache) TemplateWorkspaceOwners(id uuid.UUID) (int, bool) {
|
||||
m := c.templateWorkspaceOwners.Load()
|
||||
if m == nil {
|
||||
// Data loading.
|
||||
return -1, false
|
||||
}
|
||||
|
||||
resp, ok := (*m)[id]
|
||||
if !ok {
|
||||
// Probably no data.
|
||||
return -1, false
|
||||
}
|
||||
return resp, true
|
||||
}
|
||||
|
||||
func (c *Cache) DeploymentStats() (codersdk.DeploymentStats, bool) {
|
||||
deploymentStats := c.deploymentStatsResponse.Load()
|
||||
if deploymentStats == nil {
|
||||
|
|
|
@ -254,6 +254,74 @@ func TestCache_TemplateUsers(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestCache_TemplateWorkspaceOwners(t *testing.T) {
|
||||
t.Parallel()
|
||||
var ()
|
||||
|
||||
var (
|
||||
db = dbmem.New()
|
||||
cache = metricscache.New(db, slogtest.Make(t, nil), metricscache.Intervals{
|
||||
TemplateDAUs: testutil.IntervalFast,
|
||||
})
|
||||
)
|
||||
|
||||
defer cache.Close()
|
||||
|
||||
user1 := dbgen.User(t, db, database.User{})
|
||||
user2 := dbgen.User(t, db, database.User{})
|
||||
template := dbgen.Template(t, db, database.Template{
|
||||
Provisioner: database.ProvisionerTypeEcho,
|
||||
})
|
||||
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.Workspace{
|
||||
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.Workspace{
|
||||
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.Workspace{
|
||||
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())
|
||||
}
|
||||
|
|
|
@ -831,7 +831,12 @@ func (api *API) convertTemplate(
|
|||
template database.Template,
|
||||
) codersdk.Template {
|
||||
templateAccessControl := (*(api.Options.AccessControlStore.Load())).GetTemplateAccessControl(template)
|
||||
activeCount, _ := api.metricsCache.TemplateUniqueUsers(template.ID)
|
||||
|
||||
owners := 0
|
||||
o, ok := api.metricsCache.TemplateWorkspaceOwners(template.ID)
|
||||
if ok {
|
||||
owners = o
|
||||
}
|
||||
|
||||
buildTimeStats := api.metricsCache.TemplateBuildTimeStats(template.ID)
|
||||
|
||||
|
@ -849,7 +854,7 @@ func (api *API) convertTemplate(
|
|||
DisplayName: template.DisplayName,
|
||||
Provisioner: codersdk.ProvisionerType(template.Provisioner),
|
||||
ActiveVersionID: template.ActiveVersionID,
|
||||
ActiveUserCount: activeCount,
|
||||
ActiveUserCount: owners,
|
||||
BuildTimeStats: buildTimeStats,
|
||||
Description: template.Description,
|
||||
Icon: template.Icon,
|
||||
|
|
Loading…
Reference in New Issue