mirror of
https://github.com/stablyai/orca.git
synced 2026-10-23 08:03:39 +00:00
Tier GitHub PR lookup polling to prevent quota exhaustion (#12013)
* Tier GitHub PR lookup polling to prevent quota exhaustion The selected worktree (O(1)) checks per-minute; card list (O(N)) per-15-minutes. Introduce process-wide cache to collapse concurrent polling and gate lookups on available rate-limit budget with exponential backoff on failure. - Preserve last-known review during backoff - Invalidate cache when Orca opens a PR - Stop coordinator from double-charging * Tier GitHub PR lookup polling to prevent quota exhaustion - Return the latest reset time when both GitHub API buckets are rate-limited, preventing premature retries against still-blocked buckets. - Serve the last known review on transient lookup failures, preventing reviews from blinking out on temporary errors. - Discard in-flight lookups that predate an invalidation so stale answers cannot overwrite newly opened reviews. * fix: give rate-limit reset tests unique titles oxlint vitest/no-identical-title was failing static analysis because two cases shared the same describe title.
This commit is contained in:
@@ -170,6 +170,7 @@ import {
|
||||
getPRComments,
|
||||
getPRForBranch,
|
||||
getPRForBranchOutcome,
|
||||
getGitHubPRLookupRateLimitBlock,
|
||||
getRepoSlug,
|
||||
getRepoUpstream,
|
||||
getWorkItem,
|
||||
@@ -4609,3 +4610,70 @@ describe('GitHub GraphQL rate-limit guard', () => {
|
||||
expect(noteRateLimitSpendMock).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('getGitHubPRLookupRateLimitBlock', () => {
|
||||
beforeEach(() => {
|
||||
execFileAsyncMock.mockReset()
|
||||
ghExecFileAsyncMock.mockReset()
|
||||
getOwnerRepoMock.mockReset()
|
||||
getOwnerRepoMock.mockResolvedValue({ owner: 'acme', repo: 'widgets' })
|
||||
getRemoteUrlForRepoMock.mockReset()
|
||||
gitExecFileAsyncMock.mockReset()
|
||||
getRateLimitMock.mockReset()
|
||||
getRateLimitMock.mockResolvedValue(undefined)
|
||||
rateLimitGuardMock.mockReset()
|
||||
rateLimitGuardMock.mockReturnValue({ blocked: false })
|
||||
noteRateLimitSpendMock.mockReset()
|
||||
_resetOwnerRepoCache()
|
||||
})
|
||||
|
||||
it('reports no block while every lookup bucket has budget', async () => {
|
||||
await expect(getGitHubPRLookupRateLimitBlock('/repo-root')).resolves.toBeNull()
|
||||
expect(getRateLimitMock).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('reports a block when either lookup bucket is exhausted', async () => {
|
||||
rateLimitGuardMock.mockImplementation(((bucket: string) =>
|
||||
bucket === 'graphql'
|
||||
? { blocked: true, remaining: 4, limit: 5000, resetAt: 1_800_000_000 }
|
||||
: { blocked: false }) as () => RateLimitGuardResult)
|
||||
|
||||
await expect(getGitHubPRLookupRateLimitBlock('/repo-root')).resolves.toEqual({
|
||||
resetAt: 1_800_000_000
|
||||
})
|
||||
})
|
||||
|
||||
it('reports the latest reset when both lookup buckets are exhausted', async () => {
|
||||
rateLimitGuardMock.mockImplementation(((bucket: string) => ({
|
||||
blocked: true,
|
||||
remaining: 4,
|
||||
limit: 5000,
|
||||
// Why: core resets first, so returning it would retry into graphql's block.
|
||||
resetAt: bucket === 'core' ? 1_800_000_000 : 1_800_003_600
|
||||
})) as () => RateLimitGuardResult)
|
||||
|
||||
await expect(getGitHubPRLookupRateLimitBlock('/repo-root')).resolves.toEqual({
|
||||
resetAt: 1_800_003_600
|
||||
})
|
||||
})
|
||||
|
||||
it('reports the later reset when graphql outlasts core', async () => {
|
||||
// Retrying at the earlier reset would fail again on the bucket still blocked.
|
||||
rateLimitGuardMock.mockImplementation(((bucket: string) => ({
|
||||
blocked: true,
|
||||
remaining: 0,
|
||||
limit: 5000,
|
||||
resetAt: bucket === 'graphql' ? 1_800_000_600 : 1_800_000_000
|
||||
})) as () => RateLimitGuardResult)
|
||||
|
||||
await expect(getGitHubPRLookupRateLimitBlock('/repo-root')).resolves.toEqual({
|
||||
resetAt: 1_800_000_600
|
||||
})
|
||||
})
|
||||
|
||||
it('fails open when the exempt rate-limit probe itself fails', async () => {
|
||||
getRateLimitMock.mockRejectedValue(new Error('probe offline'))
|
||||
|
||||
await expect(getGitHubPRLookupRateLimitBlock('/repo-root')).resolves.toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user