feat: backfill historical spend counters with readiness checks - #1119
Conversation
Review · Status🟩 CompletedIronLoop completed the review and posted it to GitHub. ResultRun detailsAutomatic trigger · attempt 1 of 3 · completed in 6m 15s |
There was a problem hiding this comment.
Review · Summary
🟢 No actionable findings
No actionable issues found in the reviewed change.
Validation
- ✅ Static review — Reviewed the migration, reconciliation flow, CLI packaging, and CI coverage wiring against the current transaction and accounting-lock behavior.
Review details
- Run:
a718bb8a-139f-43ff-9d96-19b98670706b - Attempts: 1
Review —
|
henrypark133
left a comment
There was a problem hiding this comment.
Code Review (multi-agent)
Intent: Backfill historical spend counters with a repeatable-read reconciliation operator and per-organization readiness markers so future counter-based readers don't undercount pre-existing spend.
Stats: 5 findings (from 7 raw, 7 after filter, 6 after anchor gate, 5 after dedup) across 3 files. Reviewers run: correctness, security, performance, design, coverage. Reviewers failed: none. Body-only: 0.
Correctness / Regression-escape
- High No test exercises
prepare()for an org with noorganization_balancerow via the real batch path (crates/database/src/spend_counters_backfill.rs:60-79, confidence 92) — anchor: crates/database/src/spend_counters_backfill.rs:78
incomplete_organizations()includes orgs with noorganization_balancerow (LEFT JOIN).prepare()then bails hard with"has no organization_balance row", which propagates via?throughreconcile_oneand aborts the entire all-organization batch loop, skippingensure_spend_counters_readyfor every remaining org — not just the one with the missing row. No test exercises this path (onlyensure_spend_counters_readyis tested for the missing-balance case, neverprepare()/the batch path). Verified independently in the source.
Also flagged by: correctness/Medium (same root cause, framed as an error-handling design question: shouldreconcile_oneskip+report instead of hard-aborting the batch?)
Coverage
- High
incomplete_organizations()has zero test coverage (crates/database/src/spend_counters_backfill.rs:334-358, confidence 93) — anchor: crates/database/src/spend_counters_backfill.rs:334
No test callsincomplete_organizationsdirectly or indirectly (not even through the binary — nothing in the test suite, CI workflow, or an e2e harness invokesbackfill-spend-counters). Untested: the LEFT JOIN including orgs with no balance row, excluding ready orgs, theafter-cursor pagination, and ordering. - Medium
parse_argshas no unit tests (crates/database/src/bin/backfill-spend-counters.rs:83-174, confidence 58) — anchor: crates/database/src/bin/backfill-spend-counters.rs:83 (candidate — validate claim)
No#[cfg(test)]module exists for this pure function. Untested:--organizationUUID parsing,--statement-timeout-secondsparsing and its zero-timeout bail,--help, and the unknown-flag bail. - Medium
LOCK_TIMEOUT/APPLY_TIMEOUT5s caps andvalidate_timeoutbounds are untested (crates/database/src/spend_counters_backfill.rs:22-23, confidence 52) — anchor: crates/database/src/spend_counters_backfill.rs:22 (candidate — validate claim)
No test holds the accounting lock on one connection while callingapply()on another to confirmlock_timeoutactually fires a bounded error, and no test callsprepare()with a zero or> i32::MAXtimeout to hitvalidate_timeout's bail. The PR body explicitly calls out the 5s cap as a safety property.
Performance / Concurrency
- High Backfill
apply()holds the hot-path org accounting lock during live traffic (crates/database/src/spend_counters_backfill.rs:200-260, confidence 78) — anchor: crates/database/src/spend_counters_backfill.rs:212
apply()takes the sameFOR UPDATEorganization row lock as live usage-recording writers (organization_usage.rs/organization_service_usage.rsvialock_organization_accounting), then runs a multi-row UPSERT intoapi_key_spendsized by the org's full historical distinct API-key count plus theorganization_balanceUPDATE, all in one transaction before commit. Per the PR's rollout plan this operator runs against a database already receiving live inference/service traffic.statement_timeout(5s) applies per-statement, not per-transaction, so total lock-hold time across the 3 sequential statements isn't capped at 5s — worst case is closer to 10-15s for a large organization. Verified independently: mechanism and magnitude hold up; only the word "unbounded" was an overstatement (it's bounded by API-key count and per-statement timeout, not literally unbounded).
Fix: Chunk theapi_key_spendUPSERT into small batches committed incrementally under short-lived lock acquisitions, or throttle per-organizationapply()calls, so lock hold time is bounded independent of historical API-key count.
🤖 Generated with Claude Code multi-agent code review.
|
✅ OpenCodeReview: Review partially complete: 0 finding(s); 5 of 8 selected item(s) failed. |
- 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
|
Review pass: added production-entry-point, pagination, missing-row, argument, and lock-timeout coverage. CLI help now says Deliberate choices retained after a strict design review:
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 CI follow-up after retargeting to mainHead
Failed run and rerun. Investigate |
lloydmak99
left a comment
There was a problem hiding this comment.
Pure-addition change: a new V0082 migration, a reconciliation module + operator binary, tests, and CI/Docker wiring, with no modifications to existing reader/writer paths.
The reconciliation algebra checks out: prepare() captures raw_snapshot − counted_snapshot under a single REPEATABLE READ READ ONLY snapshot, and apply() adds that delta to current counters under the same org accounting lock the C1 writers use, so counted_final = raw_now. Double-apply is prevented by re-reading spend_counters_ready_at under the lock and gating the balance UPDATE on IS NULL (confirmed by the concurrent-worker test). The migration is rolling-update safe: ADD COLUMN then a separate SET DEFAULT NOW() leaves existing rows NULL for backfill with no table rewrite, and no SELECT */RETURNING * on organization_balance breaks old pods. Usage-log FKs are RESTRICT, so the api_key_spend upsert can't orphan-fail, and the keyset loop terminates cleanly.
No blocking correctness, data-loss, security, or API-contract issues found.
Checks: cargo fmt --all -- --check and git diff --check passed; CI cargo audit / cargo deny advisories passed. Compilation and DB-backed tests couldn't run locally (no cc linker / no local PostgreSQL) — those run in CI via cargo nextest.
Existing spend counters only contain charges recorded after #1116. Add historical reconciliation so later readers can replace lifetime usage scans without undercounting old spending.
This PR is stacked on #1116. It adds a per-organization readiness marker and a
backfill-spend-countersoperator shipped in the application image. The operator captures raw-minus-counter corrections in a repeatable-read snapshot, releases that snapshot, then adds corrections under the existing organization accounting lock. Counter updates and readiness commit together; competing workers and reruns skip completed organizations. Historical scans have a configurable deadline; apply statements and lock waits are capped at five seconds. Legacytotal_spentremains unchanged.Rollout order is required:
/app/backfill-spend-counterswith the normal database environment (optionally--organization UUIDor--statement-timeout-seconds N). The command does not run migrations. All-organization mode verifies readiness before reporting completion.Readers stay unchanged in this prerequisite PR; #1120 switches key lists, API-key admission checks, and billing summaries together. No production backfill was run by this change.
Validation: three PostgreSQL integration tests passed, covering real inference/service postings between snapshot and apply, competing workers, reruns, service-only and deleted keys, counter-only negative drift, both key and organization overflow rollback, empty/inactive/missing balances, and migration defaults. The tests fail against a no-op reconciliation. The actual CLI reconciled a historical fixture and reran without changing its totals. The new test target is included in PostgreSQL CI. No Markdown plans are included.
Checks:
cargo nextest run -p database --test spend_counter_backfill(3 passed),cargo clippy -p database --all-targets --all-features -- -D warnings, and formatting passed.