Skip to content

improvement(billing): self-heal null usage limits and debounce api-key last-used writes#5000

Merged
waleedlatif1 merged 3 commits into
stagingfrom
improvement/db-hotpath-fixes
Jun 12, 2026
Merged

improvement(billing): self-heal null usage limits and debounce api-key last-used writes#5000
waleedlatif1 merged 3 commits into
stagingfrom
improvement/db-hotpath-fixes

Conversation

@waleedlatif1

Copy link
Copy Markdown
Collaborator

Summary

  • getUserUsageLimit now self-heals a null currentUsageLimit instead of throwing. Org-scoped members carry a null per-user limit by design, so a user whose subscription stops being org-scoped without a resync was left null and failed closed (Cannot determine usage status - blocking execution) on every run. The limit now falls back to the same plan default syncUsageLimitsFromSubscription would set (plan minimum for paid, free tier otherwise), writes it back guarded on currentUsageLimit IS NULL, and logs a warning
  • updateApiKeyLastUsed is debounced: the write only fires when the stored lastUsed is missing or older than 60s. The column is display-only, and unconditionally rewriting the same row on every authenticated request serializes concurrent requests for the same key behind row locks
  • Added unit tests for both behaviors

Type of Change

  • Bug fix
  • Improvement

Testing

  • New unit tests for the self-heal (stored limit, missing row, free fallback, paid fallback) and the debounce (guarded predicate, error swallow)
  • Full lib/billing + lib/api-key suites pass (186 tests), typecheck clean

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

@vercel

vercel Bot commented Jun 12, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped Jun 12, 2026 6:30pm

Request Review

@cursor

cursor Bot commented Jun 12, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Changes server-side usage enforcement and billing limit persistence, which can affect whether executions are allowed; API key changes are display-only with guarded writes.

Overview
Fixes two execution-path issues: billing usage limits and API key lastUsed writes.

getUserUsageLimit no longer throws when userStats.currentUsageLimit is null (e.g. after leaving org-scoped billing without a resync). It returns the same plan default syncUsageLimitsFromSubscription would use—free-tier cap or paid per-user minimum—optionally persists it with an IS NULL guard, re-reads if another writer won the race, and still returns the fallback if the heal write fails so execution is not blocked.

updateApiKeyLastUsed only updates lastUsed when it is missing or older than a 10-minute staleness window, reducing per-request row updates and lock contention on hot keys; DB errors remain swallowed so auth does not fail.

New unit tests cover heal paths (stored limit, missing row, free/paid fallbacks, concurrency, failed write) and API key last-used behavior (guarded predicate, error swallow).

Reviewed by Cursor Bugbot for commit 1814df5. Configure here.

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

Comment thread apps/sim/lib/billing/core/usage.ts
@greptile-apps

greptile-apps Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes two independent resiliency issues: null currentUsageLimit rows that previously failed closed on every execution now self-heal to the plan default, and unconditional lastUsed writes that serialized concurrent API-key requests behind row locks are replaced with a staleness-guarded update.

  • getUserUsageLimit self-heal — when currentUsageLimit IS NULL, a guarded UPDATE … WHERE currentUsageLimit IS NULL writes the plan default (paid minimum or free tier), re-reads if a concurrent writer won the race, and wraps everything in try/catch so a failed write still resolves to the fallback rather than blocking execution.
  • updateApiKeyLastUsed debounce — the write is now conditional on the stored value being missing or older than 10 minutes (LAST_USED_STALENESS_WINDOW_MS), eliminating row-lock contention on hot keys. New unit tests cover both happy paths, the concurrent-writer race, write failures, and the staleness predicate structure.

Confidence Score: 5/5

Both changes are safe to merge: the self-heal is fully guarded against concurrent writers and transient DB failures, and the debounced lastUsed write is best-effort on a display-only field.

The null-limit self-heal correctly handles all failure modes (write fails → returns fallback, concurrent writer wins → returns concurrent value, no row → throws as before). The staleness-guarded lastUsed update is a net improvement with no regression risk. All new code is covered by unit tests.

No files require special attention; the minor log-message accuracy issue in usage.ts is non-blocking.

Important Files Changed

Filename Overview
apps/sim/lib/billing/core/usage.ts Self-heal logic for null currentUsageLimit is well-structured: guarded UPDATE prevents concurrent double-writes, re-read on zero rows handles the race, and a try/catch ensures failures fall back to the computed default rather than failing closed.
apps/sim/lib/api-key/service.ts Staleness-guarded lastUsed write eliminates per-request row locks; LAST_USED_STALENESS_WINDOW_MS is 10 minutes (correctly documented in code), though PR description incorrectly states 60s.
apps/sim/lib/billing/core/usage.test.ts New test file covers stored limit, missing row, free fallback, paid fallback, concurrent writer, and write-failure scenarios — good branch coverage.
apps/sim/lib/api-key/service.test.ts Two new tests verify the staleness predicate structure and that DB errors are swallowed; both are meaningful and non-trivial.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[getUserUsageLimit called] --> B{isOrgScoped?}
    B -- yes --> C[return org limit]
    B -- no --> D[SELECT currentUsageLimit FROM userStats]
    D --> E{row found?}
    E -- no --> F[throw: No user stats record]
    E -- yes --> G{currentUsageLimit IS NULL?}
    G -- no --> H[return stored limit]
    G -- yes --> I[compute fallbackLimit\npaid to perUserMinimum\nfree to freeTierLimit]
    I --> J[UPDATE userStats SET limit=fallback\nWHERE userId=X AND limit IS NULL RETURNING]
    J -- try throws --> K[logger.error / return fallbackLimit]
    J -- healed.length > 0 --> L[logger.warn / return fallbackLimit]
    J -- healed.length == 0 --> M[re-read concurrent value]
    M -- found --> N[return concurrent value]
    M -- not found --> O[logger.warn / return fallbackLimit]
Loading

Reviews (4): Last reviewed commit: "improvement(api-key): widen last-used st..." | Re-trigger Greptile

@greptile-apps

greptile-apps Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes two independent billing and API-key issues: getUserUsageLimit now self-heals a null currentUsageLimit (left behind when a user exits an org-scoped subscription without a resync) instead of throwing and blocking every execution; updateApiKeyLastUsed is debounced by pushing a staleness predicate into the SQL WHERE clause so the row is only rewritten once per 60 seconds.

  • Self-heal (usage.ts): reads userStats.currentUsageLimit; if null, derives a plan-appropriate fallback, writes it back guarded with IS NULL, and returns the fallback — preventing the "Cannot determine usage status" hard block.
  • Debounce (service.ts): replaces the unconditional UPDATE with UPDATE … WHERE id = ? AND (lastUsed IS NULL OR lastUsed < now-60s), eliminating row-lock serialization on high-traffic keys.
  • Tests: 186 tests across lib/billing and lib/api-key cover all four heal branches and both debounce branches.

Confidence Score: 3/5

The debounce change is safe to merge as-is; the self-heal in usage.ts has a gap where a transient DB error during the heal write still propagates to the caller, re-introducing the fail-closed behaviour the PR aims to eliminate.

The self-heal write (db.update inside getUserUsageLimit) is not wrapped in try-catch. If the database is unavailable at that moment the exception propagates, and execution is blocked again — exactly the failure mode the PR fixes. The fallback value is already computed before the write, so a single try-catch around the update would fully close the gap.

apps/sim/lib/billing/core/usage.ts — the null-heal path at lines 550-564 needs a try-catch around the db.update call.

Important Files Changed

Filename Overview
apps/sim/lib/billing/core/usage.ts Adds self-heal path for null currentUsageLimit; the heal write is unguarded and propagates DB errors to callers, defeating its own purpose in degraded environments.
apps/sim/lib/api-key/service.ts Debounces updateApiKeyLastUsed via a DB-level predicate; logic is correct and the implementation cleanly avoids in-memory state while preserving error swallowing.
apps/sim/lib/billing/core/usage.test.ts New test suite covers all four self-heal cases (stored limit, missing row, free fallback, paid fallback); no test for the write-failure scenario in the heal path.
apps/sim/lib/api-key/service.test.ts Adds two tests verifying the debounce WHERE predicate structure and error-swallowing behaviour; both are well-formed.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[getUserUsageLimit] --> B{isOrgScoped?}
    B -- yes --> C[return orgLimit]
    B -- no --> D[query userStats.currentUsageLimit]
    D --> E{row exists?}
    E -- no --> F[throw: No user stats record found]
    E -- yes --> G{limit is null?}
    G -- no --> H[return stored limit]
    G -- yes --> I[compute fallbackLimit\npaid? getPerUserMinimumLimit\nfree? getFreeTierLimit]
    I --> J[db.update userStats\nset currentUsageLimit\nWHERE userId AND isNull]
    J -- success --> K[logger.warn + return fallbackLimit]
    J -- throws ❌ --> L[exception propagates\nfail-closed again]

    style L fill:#ff4444,color:#fff
    style F fill:#ffaa00,color:#fff
Loading

Reviews (1): Last reviewed commit: "improvement(billing): self-heal null usa..." | Re-trigger Greptile

Comment thread apps/sim/lib/billing/core/usage.ts Outdated
Comment thread apps/sim/lib/billing/core/usage.test.ts
Comment thread apps/sim/lib/billing/core/usage.ts Outdated
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit eb7bf1d. Configure here.

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 1814df5. Configure here.

@waleedlatif1 waleedlatif1 merged commit 37e7121 into staging Jun 12, 2026
15 checks passed
@waleedlatif1 waleedlatif1 deleted the improvement/db-hotpath-fixes branch June 12, 2026 18:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant