feat: maintain transactional inference and service spend counters - #1116
Conversation
Review · Status🟩 CompletedIronLoop completed the review and posted it to GitHub. ResultRun detailsAutomatic trigger · attempt 1 of 3 · completed in 3m 34s |
There was a problem hiding this comment.
Review · Summary
🟢 No actionable findings
No actionable issues found in the reviewed change.
Validation
- ✅ Transactional counter review — Static inspection confirms inference and service posting update their split counters within the existing transaction only after a new usage row is inserted; duplicate paths roll back before counter writes.
Review details
- Run:
b109401f-42cb-477e-b209-3c01329eed84 - Attempts: 1
|
Review — feat: maintain transactional inference and service spend counters Verified the change statically against the two posting paths, the migration, and every existing What I confirmed
Non-blocking notes for the follow-up slices
✅ |
henrypark133
left a comment
There was a problem hiding this comment.
Code Review (multi-agent)
Intent: Introduce transactional spend counters for inference/service usage on organization_balance and api_key_spend, incrementing atomically with usage while leaving existing readers unchanged.
Stats: 2 findings (from 2 raw, 2 after filter, 2 after dedup) across 2 files. Reviewers run: correctness, security, performance, design, coverage. Reviewers failed: none. Body-only: 0
regression-escape
- High Service-path counter overflow rollback is untested (
crates/api/tests/e2e_all/spend_counters.rs:156-236, confidence 85) — anchor: crates/api/tests/e2e_all/spend_counters.rs:156
The bigint-overflow/rollback guarantee for increment_api_key_spend is only tested for the inference path (spend_counters_ignore_duplicates_and_failed_posts drives OrganizationUsageRepository::record_usage). No test drives OrganizationServiceUsageRepository::record_usage into an api_key_spend/organization_balance overflow. Adversarial verification confirmed the code paths are structurally identical (transaction rolled back on drop before commit in both), so this is a real coverage gap rather than a live correctness bug -- but the duplicated control flow between the two repositories means a future edit to one path silently diverging would ship undetected.
concurrency
- Medium New api_key_spend UPSERT extends hold time of per-org FOR UPDATE lock (
crates/database/src/repositories/organization_usage.rs:100-236, confidence 60) (candidate — validate claim) — anchor: crates/database/src/repositories/organization_usage.rs:229
record_usage() (and the mirrored path in organization_service_usage.rs) takes an exclusive per-organization lock via lock_organization_accounting() before doing any writes, serializing all usage-recording transactions for that organization. This PR adds a 7th sequential statement (the increment_api_key_spend UPSERT) inside that already-serialized critical section, on the hot path hit by every chat completion / service call, directly adding latency to the org-wide bottleneck -- most relevant for high-volume orgs with several active API keys, which is exactly the case this feature targets.
- Combine balance and key counter writes into one SQL round trip - Cover service overflow rollback and retain exact SQLSTATE checks
|
Review pass: removed the extra database round trip from each accounting transaction and deleted the orphan helper. No new posting mode or generic spend abstraction was added. The staged reader follow-up is #1119 → #1120. Backfill computes full raw-history totals and subtracts counters from the same repeatable-read snapshot; adding that correction to the current counters preserves concurrent new postings. It is not an incremental “since rollout” scan. Missing counter rows are handled as zero by #1120; hard-deleting keys is not introduced. Test-file changes: Local aggregate validation (all four PRs plus review fixes): Formatting and diff checks passed. The final run used DATABASE_* configuration without PG* overrides. This local aggregate validation is not a production backfill or deployment. GitHub CI verified on GitHub still reports REVIEW_REQUIRED; a maintainer approval is needed before merge. |
lloydmak99
left a comment
There was a problem hiding this comment.
This PR maintains transactional inference and service spend counters by folding the per-key counter into the balance upsert via a single data-modifying CTE. I verified the positional params, CTE semantics, transactional rollback behavior, and migration safety, and found no merge-blocking issues.
- Positional params line up in both upserts — inference path
organization_usage.rs:200-238and service pathorganization_service_usage.rs:363-393(column lists, VALUES, DO UPDATE branches, and bind arrays all match). balance_upsertis a data-modifying CTE withON CONFLICT DO UPDATE ... RETURNING, so it always returns one row and the downstreamINSERT ... SELECT ... WHERE TRUEalways inserts exactly oneapi_key_spendrow — no double-counting or partial writes.- Both upserts run after
allocate_usageand beforecommit(); duplicate deliveries roll back first, and an overflow surfacesDatabaseError(22003)which drops the transaction. - Migration
V0081is rollout-safe:ADD COLUMN NOT NULL DEFAULT 0is metadata-only on modern PG, bothorganization_balancewriters are updated, and the reader (row_to_balance,organization_usage.rs:694) selects explicit named columns. Rolling-deploy undercount during the window is expected and handled by the follow-up backfill.
Checks: cargo fmt --all -- --check and git diff --check passed; cargo check -p database could not run (environment lacks the cc linker). Full build/e2e skipped as slow — CI and targeted spend_counters e2e were green.
* feat: maintain transactional inference and service spend counters * feat: reconcile historical spend counters before reader rollout * Address PR review feedback (#1116) - Combine balance and key counter writes into one SQL round trip - Cover service overflow rollback and retain exact SQLSTATE checks * Address PR review feedback (#1119) - Cover CLI batching, anomalies, timeout bounds, and argument validation - Clarify fixed apply deadlines and clean test databases after failures --------- Co-authored-by: Henry Park <16583448+henrypark133@users.noreply.github.com>
* feat: maintain transactional inference and service spend counters * feat: reconcile historical spend counters before reader rollout * perf: replace lifetime spending scans with ready counters * test: isolate billing totals from concurrent fixtures * Address PR review feedback (#1116) - Combine balance and key counter writes into one SQL round trip - Cover service overflow rollback and retain exact SQLSTATE checks * Address PR review feedback (#1119) - Cover CLI batching, anomalies, timeout bounds, and argument validation - Clarify fixed apply deadlines and clean test databases after failures * Address PR review feedback (#1120) - Reuse shared database test configuration for standalone reader tests - Clean isolated schemas after test errors and assertion panics --------- Co-authored-by: Henry Park <16583448+henrypark133@users.noreply.github.com>
* test: let backfill tests stop migrations at a schema version Structural, test-only. `run_backfill_test` now delegates to `run_backfill_test_at(None, ..)`, and the per-test database can be migrated to a given refinery version instead of the latest. No test behavior changes; the next commit uses it to rehearse a deploy from the pre-counter production schema (V0080). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix: make main deployable before spend counters are reconciled main contains #1116, #1119 and #1120, and deploys always take tip of main. On any database with existing organizations, V0082 leaves them unreconciled and the #1120 startup gate panicked, so every instance crash-looped; the backfill could not run because it needs every writer on the new code first. - Startup logs the unreconciled organization count instead of panicking. - The API-key list, key admission spend and billing summary read the counters for ready organizations and fall back to the pre-#1120 raw queries otherwise (today's production behavior). Fallbacks are marked for deletion once every environment is reconciled. - backfill-spend-counters --include-ready re-checks organizations already marked ready and repairs drift, e.g. usage posted by an older writer during a rolling deploy. --dry-run reports drift and exits nonzero without applying. - apply() commits only if the readiness marker is unchanged since its snapshot, so concurrent runs apply a correction once. For incomplete organizations this is the previous IS NULL guard. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Address PR review feedback (#1123) - Readers take the raw path when an organization has no balance row, not only when its marker is NULL. V0004 creates the row with the organization, so a missing row is an anomaly with possible history. Admission keeps returning 0 for a key that does not exist (no usage can reference it); billing counts organizations without balance rows as unready. - Fold the key-list readiness check into the listing query: one round trip when ready, a raw re-query only when not. - --dry-run fails while any organization is unreconciled, not only on drift, and logs readiness counts; the message separates the two. - Compute drift once; name the pre-counter schema version in the deploy rehearsal; document spend_counter_readiness; state the apply row-count error precisely. - Test fixtures in counter_readers.rs now create the ready organization they depend on instead of relying on a missing row being read as ready; assertions unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Henry Park <16583448+henrypark133@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Reverts #1127, #1123, #1120, #1119 and #1116. V0081 has not been applied in staging or production. The counters only served the admin billing summary (≈11% of prod DB load) and the per-key limit (≈1%). The analytics scans (≈60% of load) need a different fix that these counters didn't address. Meanwhile the counters required a hot-table migration, readiness fallbacks, a backfill binary, and a rollback runbook — real ongoing cost for a fix that only reached two of many query paths. Reverting returns billing summary, key list, and per-key limit behavior to exactly what production runs today; #1124 (cutting full scans in admin revenue and platform metrics reports) is kept. Co-authored-by: Henry Park <16583448+henrypark133@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Add transactional spending counters as the first counter rollout slice: separate inference/service totals on
organization_balanceand a per-keyapi_key_spendtable. Both existing posting transactions increment counters only after a new usage insert succeeds, so duplicate deliveries do not double-count and failures roll back usage, allocation, balances, and counters together.Readers remain unchanged in this PR. Historical totals require a separate backfill and readiness check before key lists, admission checks, or billing summaries can use these counters. This PR does not yet remove the lifetime scans or change spending-limit behavior.
Validation:
api_key_spenddid not exist.cargo nextest run --test e2e_all -E 'test(spend_counters::) | test(/^usage_recording::test_record_usage_idempotent_duplicate$/) | test(/^usage_provider_attribution::duplicate_usage_preserves_original_provider_attribution$/)' --no-fail-fast: 4 passed, 799 skipped, against dedicated PostgreSQL 16 with the repository's E2E bootstrap.cargo fmt --all -- --checkandgit diff --checkpassed. Independent source review found no correctness issues in the migration or posting changes.