-
Notifications
You must be signed in to change notification settings - Fork 3.8k
improvement(knowledge): cache admitted usage checks on the search path #7989
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
569afa2
improvement(knowledge): cache admitted usage checks on the search path
waleedlatif1 9d6e7e7
fix(knowledge): keep search-path refusals out of the shared usage gat…
waleedlatif1 f59978f
fix(knowledge): key the usage gate cache by billing period source
waleedlatif1 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,112 @@ | ||
| import { LRUCache } from 'lru-cache' | ||
| import { | ||
| type AttributedUsageLimitsResult, | ||
| type BillingAttributionSnapshot, | ||
| checkAttributedUsageLimits, | ||
| } from '@/lib/billing/core/billing-attribution' | ||
| import { coalesceLocally } from '@/lib/concurrency/singleflight' | ||
|
|
||
| /** | ||
| * How long a usage-gate answer stays usable on the high-frequency paths. | ||
| * | ||
| * The gate sums the payer's usage ledger for the billing period, which grows | ||
| * with the payer's activity, so a large organization scans its whole period on | ||
| * every uncached call. Bulk ingestion re-checks per document and knowledge | ||
| * search checks per query. Staleness is bounded by this TTL and fails in the | ||
| * harmless direction: a payer who crosses their limit keeps going for at most | ||
| * this long, which charges nobody wrongly. | ||
| */ | ||
| export const USAGE_GATE_TTL_MS = 60 * 1000 | ||
|
|
||
| /** | ||
| * Recent gate answers, admitted and refused, with `LRUCache` supplying the TTL | ||
| * and the size bound. Each entry point decides which of them it may serve. | ||
| */ | ||
| const gateCache = new LRUCache<string, AttributedUsageLimitsResult>({ | ||
| max: 10_000, | ||
| ttl: USAGE_GATE_TTL_MS, | ||
| }) | ||
|
|
||
| /** | ||
| * The gate depends on who pays, for which period (and how that period was | ||
| * derived), under which plan, and which member acts: the payer pool, its limit | ||
| * and the per-member cap are all part of the answer. The workspace is not, so | ||
| * every workspace of one payer shares an entry. | ||
| */ | ||
| function gateKey(attribution: BillingAttributionSnapshot): string { | ||
| const subscription = attribution.payerSubscription | ||
| return [ | ||
| attribution.billingEntity.type, | ||
| attribution.billingEntity.id, | ||
| attribution.billingPeriod.start, | ||
| attribution.billingPeriod.end, | ||
| attribution.billingPeriod.source ?? '', | ||
| attribution.billedAccountUserId, | ||
| attribution.actorUserId, | ||
| subscription?.id ?? '', | ||
| subscription?.plan ?? '', | ||
| subscription?.status ?? '', | ||
| subscription?.seats ?? '', | ||
| ].join(':') | ||
| } | ||
|
|
||
| /** | ||
| * Serves a cached answer the caller accepts, otherwise reads the gate. | ||
| * | ||
| * `cacheRefusals` governs both directions: a caller that must re-read refusals | ||
| * also never stores one, so a refusal read on the search path never reaches | ||
| * ingestion. The usage read fails closed (a ledger error comes back as | ||
| * exceeded), which makes that the only way a search-path outage stays out of | ||
| * the cache. A read that throws writes nothing. | ||
| * | ||
| * `coalesceLocally` collapses concurrent misses onto one ledger read and bounds | ||
| * a hung read at its settle deadline. The write stays on the value this caller | ||
| * received, so a producer that timed out and later resolved cannot overwrite a | ||
| * fresher answer. | ||
| * | ||
| * There is deliberately no invalidator: usage and limit changes land in other | ||
| * processes (execution workers, Stripe webhooks), so the TTL is the real bound. | ||
| */ | ||
| async function checkUsageLimitsThroughCache( | ||
| attribution: BillingAttributionSnapshot, | ||
| cacheRefusals: boolean | ||
| ): Promise<AttributedUsageLimitsResult> { | ||
| const key = gateKey(attribution) | ||
| const cached = gateCache.get(key) | ||
| if (cached !== undefined && (cacheRefusals || !cached.isExceeded)) return cached | ||
|
|
||
| const result = await coalesceLocally(`usage-gate:${key}`, () => | ||
| checkAttributedUsageLimits(attribution) | ||
| ) | ||
| if (cacheRefusals || !result.isExceeded) gateCache.set(key, result) | ||
| return result | ||
| } | ||
|
|
||
| /** | ||
| * {@link checkAttributedUsageLimits} for background ingestion. Serves admitted | ||
| * and refused answers alike: nothing on this path has a person waiting for a | ||
| * raised limit to apply, so a refused payer waits at most the TTL. | ||
| */ | ||
| export function checkIngestionUsageLimits( | ||
| attribution: BillingAttributionSnapshot | ||
| ): Promise<AttributedUsageLimitsResult> { | ||
| return checkUsageLimitsThroughCache(attribution, true) | ||
| } | ||
|
|
||
| /** | ||
| * {@link checkAttributedUsageLimits} for knowledge search. Serves only a cached | ||
| * admission: a refusal is always re-read, so a payer who just raised their limit | ||
| * or upgraded is never held behind a cached block while they wait on a search. | ||
| * Every other interactive caller (uploads, execution admission, the settings | ||
| * surfaces) keeps reading the gate fresh. | ||
| */ | ||
| export function checkSearchUsageLimits( | ||
| attribution: BillingAttributionSnapshot | ||
| ): Promise<AttributedUsageLimitsResult> { | ||
| return checkUsageLimitsThroughCache(attribution, false) | ||
| } | ||
|
|
||
| /** Drops every cached gate answer. Test seam; never called in production code. */ | ||
| export function resetUsageGateCache(): void { | ||
| gateCache.clear() | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.