chore: replace GetManagedAgentCount query with aggregate table (#19636)

- Removes GetManagedAgentCount query
- Adds new table `usage_events_daily` which stores aggregated usage
events by the type and UTC day
- Adds trigger to update the values in this table when a new row is
inserted into `usage_events`
- Adds a migration that adds `usage_events_daily` rows for existing data
in `usage_events`
- Adds tests for the trigger
- Adds tests for the backfill query in the migration

Since the `usage_events` table is unreleased currently, this migration
will do nothing on real deployments and will only affect preview
deployments such as dogfood.

Closes https://github.com/coder/internal/issues/943
This commit is contained in:
Dean Sheather
2025-08-30 03:39:37 +10:00
committed by GitHub
parent 433f9c4a38
commit 39bf3ba628
18 changed files with 488 additions and 116 deletions
+4 -4
View File
@@ -984,10 +984,10 @@ func (api *API) CheckBuildUsage(ctx context.Context, store database.Store, templ
// This check is intentionally not committed to the database. It's fine if
// it's not 100% accurate or allows for minor breaches due to build races.
// nolint:gocritic // Requires permission to read all workspaces to read managed agent count.
managedAgentCount, err := store.GetManagedAgentCount(agpldbauthz.AsSystemRestricted(ctx), database.GetManagedAgentCountParams{
StartTime: managedAgentLimit.UsagePeriod.Start,
EndTime: managedAgentLimit.UsagePeriod.End,
// nolint:gocritic // Requires permission to read all usage events.
managedAgentCount, err := store.GetTotalUsageDCManagedAgentsV1(agpldbauthz.AsSystemRestricted(ctx), database.GetTotalUsageDCManagedAgentsV1Params{
StartDate: managedAgentLimit.UsagePeriod.Start,
EndDate: managedAgentLimit.UsagePeriod.End,
})
if err != nil {
return wsbuilder.UsageCheckResponse{}, xerrors.Errorf("get managed agent count: %w", err)
+12 -3
View File
@@ -125,10 +125,19 @@ func Entitlements(
ExternalWorkspaceCount: int64(len(externalWorkspaces)),
ExternalTemplateCount: int64(len(externalTemplates)),
ManagedAgentCountFn: func(ctx context.Context, startTime time.Time, endTime time.Time) (int64, error) {
// This is not super accurate, as the start and end times will be
// truncated to the date in UTC timezone. This is an optimization
// so we can use an aggregate table instead of scanning the usage
// events table.
//
// High accuracy is not super necessary, as we give buffers in our
// licenses (e.g. higher hard limit) to account for additional
// usage.
//
// nolint:gocritic // Requires permission to read all workspaces to read managed agent count.
return db.GetManagedAgentCount(dbauthz.AsSystemRestricted(ctx), database.GetManagedAgentCountParams{
StartTime: startTime,
EndTime: endTime,
return db.GetTotalUsageDCManagedAgentsV1(dbauthz.AsSystemRestricted(ctx), database.GetTotalUsageDCManagedAgentsV1Params{
StartDate: startTime,
EndDate: endTime,
})
},
})
+9 -4
View File
@@ -827,12 +827,17 @@ func TestEntitlements(t *testing.T) {
GetActiveUserCount(gomock.Any(), false).
Return(int64(1), nil)
mDB.EXPECT().
GetManagedAgentCount(gomock.Any(), gomock.Cond(func(params database.GetManagedAgentCountParams) bool {
// gomock doesn't seem to compare times very nicely.
if !assert.WithinDuration(t, licenseOpts.NotBefore, params.StartTime, time.Second) {
GetTotalUsageDCManagedAgentsV1(gomock.Any(), gomock.Cond(func(params database.GetTotalUsageDCManagedAgentsV1Params) bool {
// gomock doesn't seem to compare times very nicely, so check
// them manually.
//
// The query truncates these times to the date in UTC timezone,
// but we still check that we're passing in the correct
// timestamp in the first place.
if !assert.WithinDuration(t, licenseOpts.NotBefore, params.StartDate, time.Second) {
return false
}
if !assert.WithinDuration(t, licenseOpts.ExpiresAt, params.EndTime, time.Second) {
if !assert.WithinDuration(t, licenseOpts.ExpiresAt, params.EndDate, time.Second) {
return false
}
return true