Files
coder/enterprise/coderd/usage/generator_internal_test.go
T
Jaayden Halko 20c376a575 fix: enforce uniqueness and hour alignment for agent runtime usage events (#27983)
The usage generator writes `hb_agent_runtime_v1` rows with `created_at`
at the UTC hourly bucket start and exactly one row per bucket, but
nothing in the schema enforced either invariant. A duplicate bucket row
under a different id would be double-counted by any consumer summing
`runtime_ms`, and a misaligned `created_at` would skew which usage
period a bucket is attributed to.

This replaces the non-unique partial index
`idx_usage_events_agent_runtime` (from migration 000561) with a unique
index of the same shape and adds an hour-alignment `CHECK` constraint.
Both statements validate existing rows: every supported writer has
always produced conforming data, so a pre-existing violator is anomalous
and failing the migration loudly beats silently rewriting usage rows.
`generateBucket` treats a unique violation on the bucket index as
another replica having won the race, mirroring the existing `ON CONFLICT
(id)` no-op for committed rows.

The `coderd/notifications` sync commit and its revert cancel out (the
drift they addressed was fixed on main by #27979); the PR's net diff is
only the usage-event changes.

Part 1 of a 3-PR stack splitting up #27796 (see there for review
history). Stack: this PR → #27984 → #27985.
2026-08-17 16:23:45 +07:00

44 lines
1.3 KiB
Go

package usage
import (
"testing"
"time"
"github.com/lib/pq"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
"cdr.dev/slog/v3/sloggers/slogtest"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/dbmock"
"github.com/coder/coder/v2/testutil"
"github.com/coder/quartz"
)
// TestGenerateBucketUniqueViolation pins that a unique violation on the
// bucket index resolves the bucket as complete: another writer already
// recorded it. TestGeneratorConcurrentReplicas also reaches this path, but
// only when its goroutines actually interleave; this case cannot pass by
// scheduling accident.
func TestGenerateBucketUniqueViolation(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitShort)
ctrl := gomock.NewController(t)
mDB := dbmock.NewMockStore(ctrl)
gen := NewGenerator(quartz.NewMock(t), slogtest.Make(t, nil), mDB, NewDBInserter())
mDB.EXPECT().
GetTotalChatMessageRuntimeMsInRange(gomock.Any(), gomock.Any()).
Return(int64(1000), nil)
mDB.EXPECT().
InsertUsageEvent(gomock.Any(), gomock.Any()).
Return(&pq.Error{
Code: "23505", // unique_violation
Constraint: string(database.UniqueIndexUsageEventsAgentRuntime),
})
bucket := time.Date(2025, 3, 10, 10, 0, 0, 0, time.UTC)
require.NoError(t, gen.generateBucket(ctx, bucket))
}