Repository navigation
perf(billing): sum reporting-window usage from a shared prefix plus a short tail - #8974
Conversation
… short tail The soft-gate reporting-usage cache re-ran a payer's year-long ledger sum on every 30 s refill, in every process that missed. Hold the window's sum before a cutoff (now - 5 min) in Redis, refreshed by one process under a SET NX PX lease about every 10 minutes, and sum only the rows since the cutoff on each refill. Readers serve a stale prefix while the lease holder refreshes; a reader with no prefix waits up to 3 s for the holder's before summing the window. Without a ready Redis, or for a window that does not span the cutoff, the read is the exact whole-window sum as before. Exact billing callers are unchanged.
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
…n account deletion - Refresh the shared prefix about every 3 minutes instead of 10, so a top-up to a row older than the cutoff lags at most that long, and keep a stale prefix only for one refresh interval plus one lease. - With no prefix stored and the lease held elsewhere, sum the whole window at once instead of waiting, so no read is slower than before. - The lease holder sums its tail alongside the prefix. - Tag each prefix with a shared epoch read in the same MGET, and replace the epoch once an account deletion commits, so a deleted member's cascaded rows are never counted from a stored prefix.
There was a problem hiding this comment.
No issues found across 2 files
Confidence score: 5/5
- Automated review surfaced no issues in the provided summaries.
- No files require special attention.
Heads up: you’ve reached your flex budget. Increase your flex budget or wait for usage to reset.
Turn on auto-fix | Re-trigger cubic
|
…ow inside it - a committed cumulative top-up to a row older than the prefix cutoff lag bumps a per-payer prefix epoch, so the next soft-gate read sums the window again and sees the top-up as fast as an uncached read - prefixes are stored under both the global and the payer epoch; a mismatch on either counts as no prefix - epoch keys, readiness, and invalidation move to reporting-usage-epoch so usage-log can bump without an import cycle - simplify the prefix-plus-tail sum and drop history from comments
|
@cubic-dev-ai review this PR |
@waleedlatif1 I have started the AI code review. It will take a few minutes to complete. |
There was a problem hiding this comment.
All reported issues were addressed across 5 files
Heads up: you’ve reached your flex budget. Increase your flex budget or wait for usage to reset.
Turn on auto-fix | Re-trigger cubic
Summary
The reporting-window usage sum (
sum(cost) from usage_log where billing_entity_* = … and created_at in [start, end)) is already an index-only scan. It is expensive only because a year-long window covers so many index entries. The soft-gate cache inreporting-usage-cache.tsre-ran it on every 30 s refill, and processes that missed at the same time each ran their own copy.This PR changes only the soft-gate read path (
readSoftGateUsageCost, enterprise reporting windows, default pool).Prefix plus tail.
now − 5 min) for each payer and window, tagged with a global prefix epoch and a per-payer prefix epoch.[cutoff, end), a small index range.MGETreads the prefix and both epochs together. A prefix whose stored epochs differ from the current ones counts as absent.Single flight across processes.
SET NX PXlease held for one statement timeout (60 s).Within one process, coalescing is unchanged: LRU
fetchMethod.Top-ups to old rows.
recordCumulativeUsagereplaces the payer's epoch (usage:reporting:v1:prefix-epoch:<type>:<id>) after a top-up's transaction commits, when the topped-up row'screated_atis older than the cutoff lag (with a 1 min skew margin). Fire-and-forget, Redis-optional, and logged on failure.reporting-usage-epoch.ts, sousage-log.tscan bump without an import cycle.Deletions.
deleteUserAccount, the only path that deletesuserrows, and so the only one that cascadesusage_logrows away) replaces the global prefix epoch after its transaction commits.Unchanged.
getBillingPeriodUsageCostand its other callers) are untouched. The prefix and tail are calls to that same function with narrower bounds.Load (largest payer)
A payer whose old rows are topped up continuously falls back to today's whole-window cadence, still single-flight for the refresh, so it is never worse than today.
The refresh interval, cutoff lag, lease, and retention are named constants (
PREFIX_REFRESH_AFTER_MS,PREFIX_CUTOFF_LAG_MS,PREFIX_LEASE_MS,PREFIX_RETAIN_MS).Correctness contract
created_atdefaults tonow(), the DB transaction start. It makes the read low.Tests
Integration tests in
reporting-usage-cache.integration.tsrun against real Postgres and Redis:created_at, and old-row top-ups, the served value stays ≤ the exact sum, and equals it after the refresh interval.recordCumulativeUsagetop-up to a row inside the stored prefix bumps the payer epoch, and the next read equals the exact sum.Residual risks