Skip to content

Commit 7d7be79

Browse files
committed
[usage] Use attribution ID to reduce DB queries for usage report
1 parent 247205c commit 7d7be79

File tree

4 files changed

+95
-292
lines changed

4 files changed

+95
-292
lines changed

components/usage/pkg/controller/billing.go

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,19 @@
44

55
package controller
66

7-
import "github.com/gitpod-io/gitpod/usage/pkg/stripe"
7+
import (
8+
"context"
9+
"github.com/gitpod-io/gitpod/usage/pkg/stripe"
10+
"time"
11+
)
812

913
type BillingController interface {
10-
Reconcile(report []TeamUsage)
14+
Reconcile(ctx context.Context, now time.Time, report UsageReport)
1115
}
1216

1317
type NoOpBillingController struct{}
1418

15-
func (b *NoOpBillingController) Reconcile(report []TeamUsage) {}
19+
func (b *NoOpBillingController) Reconcile(_ context.Context, _ time.Time, _ UsageReport) {}
1620

1721
type StripeBillingController struct {
1822
sc *stripe.Client
@@ -22,12 +26,7 @@ func NewStripeBillingController(sc *stripe.Client) *StripeBillingController {
2226
return &StripeBillingController{sc: sc}
2327
}
2428

25-
func (b *StripeBillingController) Reconcile(report []TeamUsage) {
26-
// Convert the usage report to sum all entries for the same team.
27-
var summedReport = make(map[string]int64)
28-
for _, usageEntry := range report {
29-
summedReport[usageEntry.TeamID] += usageEntry.WorkspaceSeconds
30-
}
31-
32-
b.sc.UpdateUsage(summedReport)
29+
func (b *StripeBillingController) Reconcile(ctx context.Context, now time.Time, report UsageReport) {
30+
runtimeReport := report.RuntimeSummaryForTeams(now)
31+
b.sc.UpdateUsage(runtimeReport)
3332
}

components/usage/pkg/controller/reconciler.go

Lines changed: 37 additions & 139 deletions
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,6 @@ type UsageReconcileStatus struct {
4545

4646
WorkspaceInstances int
4747
InvalidWorkspaceInstances int
48-
49-
Workspaces int
50-
51-
Teams int
52-
53-
Report []TeamUsage
5448
}
5549

5650
func (u *UsageReconciler) Reconcile() error {
@@ -60,7 +54,7 @@ func (u *UsageReconciler) Reconcile() error {
6054
startOfCurrentMonth := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, time.UTC)
6155
startOfNextMonth := startOfCurrentMonth.AddDate(0, 1, 0)
6256

63-
status, err := u.ReconcileTimeRange(ctx, startOfCurrentMonth, startOfNextMonth)
57+
status, report, err := u.ReconcileTimeRange(ctx, startOfCurrentMonth, startOfNextMonth)
6458
if err != nil {
6559
return err
6660
}
@@ -75,7 +69,7 @@ func (u *UsageReconciler) Reconcile() error {
7569
defer f.Close()
7670

7771
enc := json.NewEncoder(f)
78-
err = enc.Encode(status.Report)
72+
err = enc.Encode(report)
7973
if err != nil {
8074
return fmt.Errorf("failed to marshal report to JSON: %w", err)
8175
}
@@ -89,7 +83,7 @@ func (u *UsageReconciler) Reconcile() error {
8983
return nil
9084
}
9185

92-
func (u *UsageReconciler) ReconcileTimeRange(ctx context.Context, from, to time.Time) (*UsageReconcileStatus, error) {
86+
func (u *UsageReconciler) ReconcileTimeRange(ctx context.Context, from, to time.Time) (*UsageReconcileStatus, UsageReport, error) {
9387
now := u.nowFunc().UTC()
9488
log.Infof("Gathering usage data from %s to %s", from, to)
9589
status := &UsageReconcileStatus{
@@ -98,7 +92,7 @@ func (u *UsageReconciler) ReconcileTimeRange(ctx context.Context, from, to time.
9892
}
9993
instances, invalidInstances, err := u.loadWorkspaceInstances(ctx, from, to)
10094
if err != nil {
101-
return nil, fmt.Errorf("failed to load workspace instances: %w", err)
95+
return nil, nil, fmt.Errorf("failed to load workspace instances: %w", err)
10296
}
10397
status.WorkspaceInstances = len(instances)
10498
status.InvalidWorkspaceInstances = len(invalidInstances)
@@ -108,129 +102,38 @@ func (u *UsageReconciler) ReconcileTimeRange(ctx context.Context, from, to time.
108102
}
109103
log.WithField("workspace_instances", instances).Debug("Successfully loaded workspace instances.")
110104

111-
workspaces, err := u.loadWorkspaces(ctx, instances)
112-
if err != nil {
113-
return nil, fmt.Errorf("failed to load workspaces for workspace instances in time range: %w", err)
114-
}
115-
status.Workspaces = len(workspaces)
116-
117-
// match workspaces to teams
118-
teams, err := u.loadTeamsForWorkspaces(ctx, workspaces)
119-
if err != nil {
120-
return nil, fmt.Errorf("failed to load teams for workspaces: %w", err)
121-
}
122-
status.Teams = len(teams)
123-
124-
report, err := generateUsageReport(teams, now)
125-
if err != nil {
126-
return nil, fmt.Errorf("failed to generate usage report: %w", err)
127-
}
128-
status.Report = report
129-
130-
u.billingController.Reconcile(status.Report)
131-
132-
return status, nil
133-
}
134-
135-
func generateUsageReport(teams []teamWithWorkspaces, maxStopTime time.Time) ([]TeamUsage, error) {
136-
var report []TeamUsage
137-
for _, team := range teams {
138-
var teamTotalRuntime int64
139-
for _, workspace := range team.Workspaces {
140-
for _, instance := range workspace.Instances {
141-
teamTotalRuntime += instance.WorkspaceRuntimeSeconds(maxStopTime)
142-
}
143-
}
105+
instancesByAttributionID := groupInstancesByAttributionID(instances)
144106

145-
report = append(report, TeamUsage{
146-
TeamID: team.TeamID.String(),
147-
WorkspaceSeconds: teamTotalRuntime,
148-
})
149-
}
150-
return report, nil
151-
}
107+
u.billingController.Reconcile(ctx, now, instancesByAttributionID)
152108

153-
type teamWithWorkspaces struct {
154-
TeamID uuid.UUID
155-
Workspaces []workspaceWithInstances
109+
return status, instancesByAttributionID, nil
156110
}
157111

158-
func (u *UsageReconciler) loadTeamsForWorkspaces(ctx context.Context, workspaces []workspaceWithInstances) ([]teamWithWorkspaces, error) {
159-
// find owner IDs of these workspaces
160-
var ownerIDs []uuid.UUID
161-
for _, workspace := range workspaces {
162-
ownerIDs = append(ownerIDs, workspace.Workspace.OwnerID)
163-
}
112+
type UsageReport map[db.AttributionID][]db.WorkspaceInstance
164113

165-
// Retrieve memberships. This gives a link between an Owner and a Team they belong to.
166-
memberships, err := db.ListTeamMembershipsForUserIDs(ctx, u.conn, ownerIDs)
167-
if err != nil {
168-
return nil, fmt.Errorf("failed to list team memberships: %w", err)
169-
}
114+
func (u UsageReport) RuntimeSummaryForTeams(maxStopTime time.Time) map[string]int64 {
115+
attributedUsage := map[string]int64{}
170116

171-
membershipsByUserID := map[uuid.UUID]db.TeamMembership{}
172-
for _, membership := range memberships {
173-
// User can belong to multiple teams. For now, we're choosing the membership at random.
174-
membershipsByUserID[membership.UserID] = membership
175-
}
117+
for attribution, instances := range u {
118+
entity, id := attribution.Values()
119+
if entity != "team" {
120+
continue
121+
}
176122

177-
// Convert workspaces into a lookup so that we can index into them by Owner ID, needed for joining Teams with Workspaces
178-
workspacesByOwnerID := map[uuid.UUID][]workspaceWithInstances{}
179-
for _, workspace := range workspaces {
180-
workspacesByOwnerID[workspace.Workspace.OwnerID] = append(workspacesByOwnerID[workspace.Workspace.OwnerID], workspace)
181-
}
123+
var runtime uint64
124+
for _, instance := range instances {
125+
runtime += instance.WorkspaceRuntimeSeconds(maxStopTime)
126+
}
182127

183-
// Finally, join the datasets
184-
// Because we iterate over memberships, and not workspaces, we're in effect ignoring Workspaces which are not in a team.
185-
// This is intended as we focus on Team usage for now.
186-
var teamsWithWorkspaces []teamWithWorkspaces
187-
for userID, membership := range membershipsByUserID {
188-
teamsWithWorkspaces = append(teamsWithWorkspaces, teamWithWorkspaces{
189-
TeamID: membership.TeamID,
190-
Workspaces: workspacesByOwnerID[userID],
191-
})
128+
attributedUsage[id] = int64(runtime)
192129
}
193130

194-
return teamsWithWorkspaces, nil
195-
}
196-
197-
type workspaceWithInstances struct {
198-
Workspace db.Workspace
199-
Instances []db.WorkspaceInstance
131+
return attributedUsage
200132
}
201133

202-
func (u *UsageReconciler) loadWorkspaces(ctx context.Context, instances []db.WorkspaceInstance) ([]workspaceWithInstances, error) {
203-
var workspaceIDs []string
204-
for _, instance := range instances {
205-
workspaceIDs = append(workspaceIDs, instance.WorkspaceID)
206-
}
207-
208-
workspaces, err := db.ListWorkspacesByID(ctx, u.conn, toSet(workspaceIDs))
209-
if err != nil {
210-
return nil, fmt.Errorf("failed to find workspaces for provided workspace instances: %w", err)
211-
}
212-
213-
workspacesByID := map[string]db.Workspace{}
214-
for _, workspace := range workspaces {
215-
workspacesByID[workspace.ID] = workspace
216-
}
217-
218-
// We need to also add the instances to corresponding records, a single workspace can have multiple instances
219-
instancesByWorkspaceID := map[string][]db.WorkspaceInstance{}
220-
for _, instance := range instances {
221-
instancesByWorkspaceID[instance.WorkspaceID] = append(instancesByWorkspaceID[instance.WorkspaceID], instance)
222-
}
223-
224-
// Flatten results into a list
225-
var workspacesWithInstances []workspaceWithInstances
226-
for workspaceID, workspace := range workspacesByID {
227-
workspacesWithInstances = append(workspacesWithInstances, workspaceWithInstances{
228-
Workspace: workspace,
229-
Instances: instancesByWorkspaceID[workspaceID],
230-
})
231-
}
232-
233-
return workspacesWithInstances, nil
134+
type invalidWorkspaceInstance struct {
135+
reason string
136+
workspaceInstanceID uuid.UUID
234137
}
235138

236139
func (u *UsageReconciler) loadWorkspaceInstances(ctx context.Context, from, to time.Time) ([]db.WorkspaceInstance, []invalidWorkspaceInstance, error) {
@@ -246,11 +149,6 @@ func (u *UsageReconciler) loadWorkspaceInstances(ctx context.Context, from, to t
246149
return trimmed, invalid, nil
247150
}
248151

249-
type invalidWorkspaceInstance struct {
250-
reason string
251-
workspaceInstanceID uuid.UUID
252-
}
253-
254152
func validateInstances(instances []db.WorkspaceInstance) (valid []db.WorkspaceInstance, invalid []invalidWorkspaceInstance) {
255153
for _, i := range instances {
256154
// i is a pointer to the current element, we need to assign it to ensure we're copying the value, not the current pointer.
@@ -302,20 +200,20 @@ func trimStartStopTime(instances []db.WorkspaceInstance, maximumStart, minimumSt
302200
return updated
303201
}
304202

305-
func toSet(items []string) []string {
306-
m := map[string]struct{}{}
307-
for _, i := range items {
308-
m[i] = struct{}{}
309-
}
310-
311-
var result []string
312-
for s := range m {
313-
result = append(result, s)
314-
}
315-
return result
316-
}
317-
318203
type TeamUsage struct {
319204
TeamID string `json:"team_id"`
320205
WorkspaceSeconds int64 `json:"workspace_seconds"`
321206
}
207+
208+
func groupInstancesByAttributionID(instances []db.WorkspaceInstance) map[db.AttributionID][]db.WorkspaceInstance {
209+
result := map[db.AttributionID][]db.WorkspaceInstance{}
210+
for _, instance := range instances {
211+
if _, ok := result[instance.UsageAttributionID]; !ok {
212+
result[instance.UsageAttributionID] = []db.WorkspaceInstance{}
213+
}
214+
215+
result[instance.UsageAttributionID] = append(result[instance.UsageAttributionID], instance)
216+
}
217+
218+
return result
219+
}

0 commit comments

Comments
 (0)