Replace compute-billing push with the pull API#893
Merged
Conversation
Billing now pulls compute usage from duckgres instead of duckgres pushing capture events to PostHog ingestion (docs/design/billing-pull-api.md). The per-connection metering is unchanged; what changes is the bucket key and how data leaves: - Migration 000015 widens the buffer key to (org_id, team_id, query_source, cpu, mem_gib, bucket_start) — team_id backfilled from the org's default_team_id, worker size as exact NUMERIC decimals — adds the single global ack cursor and drops the per-org push-drain state table. - The meter records the org's default team (config-snapshot lookup) and the session's duckgres.query_source GUC (via server.ConnectionBilling) alongside the provisioned worker size; distinct keys accumulate and bill separately. Metering is now always on for the remote backend. - GET /api/v1/billing/usage aggregates all closed buckets since the last ack into one row per key per UTC day, with watermark_low (cursor) and watermark_high (newest closed minute: now − 60s − 30s grace). POST /api/v1/billing/ack advances the cursor monotonically and deletes buckets ≤ watermark_high in one transaction; idempotent, and an ack beyond the latest closed bucket is rejected so unserved buckets can never be deleted. Both routes are internal-secret/admin-gated inside the audited /api/v1 group. - A leader-only safety GC hard-deletes buckets older than 30 days and WARNs with the dropped count (billing not keeping up = alertable). - The push path is gone: leader drain + capture client, and the --billing-ingest-url/--billing-ingest-token flags with their DUCKGRES_BILLING_INGEST_URL/_TOKEN envs. e2e: compute_usage_metering_wired becomes compute_usage_pull_api — two real connections (standard + endpoints GUC) must surface as separate usage rows carrying the provisioned default_team_id and worker size, then ack must advance the cursor, delete the acked buckets, stay idempotent on re-ack, and reject a future watermark.
Test Impact PlanDeterministic summary of how this PR changes tests, CI runners, and coverage-risk signals. Summary
Signals
Coverage risk: likely reduced Warnings
|
Its only production caller is kubernetes-tagged multitenant.go, so the plain build's linter flagged it unused. The new test exercises the real registration and locks in that both billing routes sit behind the admin-gating middleware.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements
docs/design/billing-pull-api.md(#860): billing pulls compute usage from duckgres and acks a watermark; duckgres deletes acked buckets. The push path (leader drain → PostHog ingestion capture events) is removed. Per-connection metering (wall-clock × provisioned worker size, 60s buckets) is unchanged.What changed
Bucket key + cursor (migration 000015)
duckgres_org_compute_usagekey widens to(org_id, team_id, query_source, cpu, mem_gib, bucket_start).team_idis backfilled from the org'sdefault_team_idfor pre-existing rows; worker size is stored as exactNUMERICdecimals (vCPU / GiB — canonical decimal text computed with integer math, no floats), so fractional sizes group exactly.duckgres_compute_billing_cursor(single global ack cursor). The per-org push-drain state table is dropped.Meter
team_id(org'sdefault_team_idfrom the config snapshot) andquery_source(theduckgres.query_sourcesession GUC viaserver.ConnectionBilling, defaultstandard; a mid-connection change bills the whole connection under the final value) alongside the provisioned worker size. Distinct keys accumulate — and bill — separately.Pull API (internal-secret bearer → admin, registered inside the audited
/api/v1group)GET /api/v1/billing/usage— aggregates all closed buckets in(cursor, watermark_high]into one row per key per UTC day;watermark_high= newest closed minute (now − 60s − 30s grace, grace > flush interval so every CP's contribution has landed). Response size is bounded by active keys × days, so billing downtime can't make it explode.POST /api/v1/billing/ack {watermark_high}— advances the cursor monotonically + deletes buckets≤ watermark_highin one transaction. Idempotent; an ack beyond the latest closed bucket is rejected (400) so unserved buckets can never be deleted.Push removal
compute_drain.go/compute_capture.go(+tests); removed--billing-ingest-url/--billing-ingest-tokenandDUCKGRES_BILLING_INGEST_URL/_TOKENeverywhere (flags, env resolution, README, CLAUDE.md). Chart-side secret wiring cleanup happens in the charts repo separately.Tests
compute_meter_test.go: key separation by query_source and worker size, team resolution, unknown-team tolerance, remainder carry with size.compute_billing_api_test.go(new): watermark window math, never-acked serves everything, empty-window short-circuit, ack advance/bounds/idempotency, store errors → 500, GC loop.tests/configstore/migrations_postgres_test.go: v15, new columns, cursor table present, drain-state table absent.harness.sh:compute_usage_metering_wired→compute_usage_pull_api— two real connections (one standard, one withSET duckgres.query_source='endpoints') must surface as separate usage rows carrying the org's provisioneddefault_team_idand a positive worker size; then ack advances the cursor (next GET'swatermark_lowequals it), deletes ≥1 bucket, re-ack is a no-op, and a future watermark is rejected with 400.For the billing team (consumer contract)
Pull loop:
GET /api/v1/billing/usage→ process →POST /api/v1/billing/ackwith the exactwatermark_highfrom the GET. Cross-checkwatermark_lowagainst your recorded last ack (mismatch = desync, alert). Auth:Authorization: Bearer <internal secret>over the existing posthog → mw duckgres ingress.🤖 Generated with Claude Code