Repository navigation
perf: replace lifetime spending scans with reconciled counters - #1120
Conversation
Review · Status🟩 CompletedIronLoop completed the review and posted it to GitHub. ResultRun detailsAutomatic trigger · attempt 1 of 3 · completed in 4m 10s |
There was a problem hiding this comment.
Review · Summary
🟢 No actionable findings
No actionable issues found in the reviewed change.
Validation
- ✅ Counter-reader consistency — Static inspection traced each changed reader to its transactional counter writer and readiness prerequisite; ordering, deletion filtering, and admission semantics remain preserved.
- ✅ Reader isolation coverage — The added database coverage exercises key listing, admission spend, and billing totals without raw usage tables present.
Review details
- Run:
82d09bbc-a662-4817-b283-85dd9d16ef98 - Attempts: 1
Review —
|
henrypark133
left a comment
There was a problem hiding this comment.
Code Review (multi-agent)
Intent: Replace lifetime usage-log scans in API-key lists, admission checks, and billing summary with reconciled transactional spend counters for performance.
Stats: 3 findings (from 3 raw, 3 after filter, 3 after dedup) across 3 files. Reviewers run: correctness, security, performance, design, coverage. Reviewers failed: none. Body-only: 0.
Mechanical
- High Production
.expect()introduced on the new spend-counter readiness check (crates/api/src/main.rs:46-48, confidence 90) — anchor: crates/api/src/main.rs:44
database::ensure_spend_counters_ready(...).await.expect(...)will panic and crash the process at startup if spend counters are incomplete. This mirrors the existing.expect("Usage reporting index prerequisites are not satisfied")pattern three lines above for the same fail-fast-at-startup purpose, so it is likely intentional rather than an oversight — flagged per the mechanical unwrap/expect check for author confirmation. If intended, no change needed; just confirm this only runs once at process start, not on any hot/retry path.
Design & Maintainability
- Medium New test file re-implements Postgres connection/pool-config boilerplate already in spend_counter_backfill.rs (
crates/database/tests/counter_readers.rs:12-41, confidence 72) — anchor: crates/database/tests/spend_counter_backfill.rs:415-470
env_value/pool_config/new_poolreimplement the same env-var-driven connection config already in spend_counter_backfill.rs'spool_config/test_database, differing only in database-selection strategy (schema search_path vs. CREATE DATABASE + migrations). No sharedtests/supportmodule exists to extend into. Suggested fix: extract a shared helper parameterized by selection strategy.
Coverage & Verification
- Medium New test file hand-rolls a subset schema instead of running real migrations, risking future silent drift from production schema (
crates/database/tests/counter_readers.rs:345-403, confidence 72, verified) — anchor: crates/database/tests/spend_counter_backfill.rs:415-434
scoped_pool()builds a 5-table schema subset via inlineCREATE TABLEinstead ofdatabase::migrations::run()like the siblingspend_counter_backfill.rs. Adversarial verification confirmed the hand-rolled schema already diverges from the real migrated schema fororganizations/organization_limits_history(missing columns), though none of the currently-missing columns are read by the 3 repository methods under test — so today's tests pass correctly. The risk is forward-looking: a future migration adding a required column these tests' tables would not be caught here. Suggested fix: usedatabase::migrations::run()scoped via search_path, mirroring the sibling file.
Also flagged by: design/Medium (same file, related root cause — no shared connection-config/migration harness between the two test files).
- Combine balance and key counter writes into one SQL round trip - Cover service overflow rollback and retain exact SQLSTATE checks
- Cover CLI batching, anomalies, timeout bounds, and argument validation - Clarify fixed apply deadlines and clean test databases after failures
- Reuse shared database test configuration for standalone reader tests - Clean isolated schemas after test errors and assertion panics
|
Review pass: reader tests reuse the existing support connection configuration, use the actual database name for standalone runs, and clean up their isolated schemas after test-body errors or panics. The missing-raw-table proof remains intact. Rollout is strictly staged: deploy #1116 everywhere and drain all older writers before applying V0082 from #1119, then run backfill to completion, then deploy #1120. Deploying both migrations in the first writer rollout violates that prerequisite; readiness markers do not detect such a violation, and this PR does not provide an audit/force-repair command. Returning to a writer version that does not maintain counters likewise invalidates readiness and requires reconciliation before these readers can be used. The global startup gate remains intentional and runs once before traffic. A partial backfill blocks this new reader deployment; keep the previous reader release serving until reconciliation is complete. Route-specific fallback or an environment bypass would create another spending-read mode and is not added in this review pass. This also applies to an existing dev/staging database. 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 |
lloydmak99
left a comment
There was a problem hiding this comment.
Straightforward perf change: three lifetime-spending log scans are replaced by reads against the reconciled api_key_spend/organization_balance counters, guarded by a startup readiness gate. The rewrites are semantically equivalent to what they replace, and the writer paths maintain the counters, so admission and ordering semantics are preserved.
Non-blocking follow-up:
crates/api/src/main.rs:30—ensure_spend_counters_readyis a global startup gate that.expect()s, so it hard-requires the #1116→#1119 backfill to complete before this deploys; an unreconciledorganization_balancerow will crash-loop pods (and blockscargo run --bin apiagainst a pre-backfill dev/staging DB). Confirmed intentional staged-rollout design — just make sure the deploy runbook orders the backfill first.
Checks: cargo fmt --all -- --check passed; full workspace compile skipped locally (no cc linker / cold cache), CI reported green on the latest head.
# Conflicts: # .config/nextest.toml # .github/workflows/test.yml
* 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>
API-key lists, API-key admission checks, and the platform billing summary currently sum lifetime usage logs for every read. Replace those aggregates with the transactional counters maintained by #1116 and reconciled by #1119.
Stack: #1116 → #1119 → this PR. Deploy and run the #1119 backfill before deploying these readers. API startup now refuses incomplete or missing organization balances, using the existing database-prerequisite pattern. There is no raw-query fallback or feature flag.
api_key_spendrow per key; workspace visibility, deleted-key filtering, sorting, pagination, and unused-key zeroes remain unchanged.inference_spentonly, preserving the existing policy that excludes service charges.organization_balance. The legacy total remains independent of the inference/service splits, preserving historical discrepancies.Repository tests invoke the actual reader methods in an isolated schema with no raw usage tables. All three fail on the original queries and pass on the counter queries. They cover inference/service/mixed/unused/deleted keys, both sort directions, workspace isolation, pagination beyond the end, admission service exclusion, and legacy billing totals. The existing API-key ordering fixture now posts through the real accounting repositories. Both database test targets are included in PostgreSQL CI.
This removes the lifetime usage-log scans from these three paths. Date-range analytics and rollups are separate work. No production backfill or deployment was performed, and no Markdown plans are committed.
Validation: 25 selected PostgreSQL/API regressions passed (784 unrelated tests skipped), including all API-key tests, writer counters, backfill concurrency/rollback, billing summary, and scan-free reader tests. The existing billing-summary test now uses the established exclusive nextest override because its exact before/after platform totals race parallel organization creation; all assertions remain intact. Formatting passed.
cargo clippy -p database -p api --all-targets --all-features -- -D warningspassed.