mirror of
https://github.com/stablyai/orca.git
synced 2026-11-15 10:27:33 +00:00
* perf(github): share and pace timed pull request reads on the host
Reuse exact completed results for automatic refreshes, back off unchanged
settled pull requests, and route server revalidation through the existing
host budget while preserving foreground reads and older clients.
Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
* fix(github): preserve shared producer settlement and selected demand
Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
* test(github): cover paired receipt retries and watch fairness
* test(github): explain shared fake-clock fixture
* test(github): typecheck host and renderer integration together
* test(github): name exported fixture mock types
* Revert "test(github): typecheck host and renderer integration together"
This reverts commit 756a38787c.
* test(github): place host and renderer loops in cross-layer suite
* fix(github): preserve refresh policy and shared callers
Use the same validated freshness for cached queue reads and their follow-up. Keep forced mergeability rereads and detach cancelled consumers independently from the shared lookup.
Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
* fix(github): refresh settled rows when exposed again
Use the same finite foreground age to admit and consume the queued read. A recent exact result can still satisfy it, and settled rows do not acquire a recurring timer.
Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
* fix(github): accept only current PR read completions
Keep shared publication and retry planning on the accepted actual read. Preserve legacy own replies, terminal loading and ordinary work after a superseded lookup, and honor the accepted error gate before automatic recovery.
Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
* test(github): follow moved request contracts
* test(github): explain the partial event fixture type assertion
* fix(github): preserve queued deadlines and detachable PR waits
* fix(github): compare refresh wakes on the elapsed clock
* test(github): format queued refresh deadline cases
* fix(github): charge actual producers and fence retry gates
* fix(github): preserve fresh intent and producer error policy
* fix(github): retain owned cold-read retry gates
---------
Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
174 lines
6.1 KiB
TypeScript
174 lines
6.1 KiB
TypeScript
import type {
|
|
GitHubPRRefreshCandidate,
|
|
GitHubPRRefreshReason,
|
|
PRRefreshOutcome,
|
|
PRSnapshotReply
|
|
} from '../../../../shared/github/pull-requests/refresh/pull-request-refresh-types'
|
|
import { preparePRBranchRead } from '../../client/lookup/pr-for-branch-outcome'
|
|
import { deliverablePRBranchOutcome } from '../../client/lookup/pr-branch-read-resources'
|
|
import {
|
|
aliasFromCandidate,
|
|
refreshKey,
|
|
validateCandidate,
|
|
isBackground,
|
|
hostedReviewOptionArgs,
|
|
MANUAL_MERGEABILITY_PENDING_REFRESH_MS
|
|
} from './pr-refresh-candidate-policy'
|
|
import {
|
|
blockedWslDistroForPath,
|
|
wslDistroForPath
|
|
} from '../../../wsl/filesystem/wsl-unc-access-gate'
|
|
import { prRefreshRateLimitPausedUntil } from './pr-refresh-rate-limit-gate'
|
|
import type { PRRefreshQueue } from './pr-refresh-queue'
|
|
import type { PRRefreshRetryState } from './pr-refresh-retry-state'
|
|
import type { PRRefreshVisibility } from './pr-refresh-visibility'
|
|
import type { PRRefreshEventPublisher } from './pr-refresh-event-publisher'
|
|
import type { PRRefreshQueueDrainer } from './pr-refresh-queue-drainer'
|
|
|
|
type ImmediateRefreshOwners = {
|
|
queue: PRRefreshQueue
|
|
retry: PRRefreshRetryState
|
|
visibility: PRRefreshVisibility
|
|
events: PRRefreshEventPublisher
|
|
drainer: PRRefreshQueueDrainer
|
|
}
|
|
export async function runImmediatePRRefresh(
|
|
{ queue, retry, visibility, events, drainer }: ImmediateRefreshOwners,
|
|
candidate: GitHubPRRefreshCandidate,
|
|
reason: GitHubPRRefreshReason = 'manual',
|
|
options: { userRefresh?: boolean } = {}
|
|
): Promise<PRSnapshotReply> {
|
|
const alias = aliasFromCandidate(candidate)
|
|
const key = refreshKey(candidate)
|
|
const existing = queue.get(key)
|
|
const aliasMap = new Map(existing ? existing.aliases : [])
|
|
aliasMap.set(alias.cacheKey, alias)
|
|
const aliases = Array.from(aliasMap.values())
|
|
const skippedReason = validateCandidate(candidate)
|
|
if (skippedReason) {
|
|
queue.removeInvalidAlias(key, alias)
|
|
const outcome: PRRefreshOutcome = {
|
|
kind: 'upstream-error',
|
|
errorType: 'unknown',
|
|
message: `Cannot refresh PR for this worktree: ${skippedReason}`,
|
|
fetchedAt: Date.now()
|
|
}
|
|
events.broadcast({ aliases: [alias], reason, status: 'skipped', skippedReason })
|
|
return outcome
|
|
}
|
|
// E9: renderer fallbacks reach here with automatic reasons; only a manual refresh may start a stopped distro.
|
|
if (
|
|
isBackground(reason) &&
|
|
wslDistroForPath(candidate.repoPath, candidate.localGitOptions?.wslDistro) &&
|
|
(await blockedWslDistroForPath(candidate.repoPath, candidate.localGitOptions?.wslDistro))
|
|
) {
|
|
events.broadcast({ aliases: [alias], reason, status: 'skipped', skippedReason: 'fresh' })
|
|
// Local callers keep their cached PR on an upstream error; nothing is recorded or retried here.
|
|
return {
|
|
kind: 'upstream-error',
|
|
errorType: 'unknown',
|
|
message: 'WSL distro is not running',
|
|
fetchedAt: Date.now()
|
|
}
|
|
}
|
|
|
|
const primaryGateUntil = await prRefreshRateLimitPausedUntil(candidate, false)
|
|
const gateUntil = Math.max(primaryGateUntil ?? 0, retry.manualGateUntil(key))
|
|
if (gateUntil > Date.now()) {
|
|
queue.protectBackgroundUntil(key, gateUntil)
|
|
queue.set(key, {
|
|
key,
|
|
candidate,
|
|
aliases: aliasMap,
|
|
reason,
|
|
priority: 40,
|
|
dueAt: gateUntil,
|
|
queuedAt: queue.nextOrder()
|
|
})
|
|
events.broadcast({
|
|
aliases,
|
|
reason,
|
|
status: 'paused',
|
|
pausedUntil: gateUntil,
|
|
skippedReason: 'rate-limit'
|
|
})
|
|
drainer.schedule(Math.max(1_000, gateUntil - Date.now()))
|
|
return {
|
|
kind: 'upstream-error',
|
|
errorType: 'rate_limited',
|
|
message: 'GitHub is temporarily limiting requests. Try again after the limit resets.',
|
|
fetchedAt: Date.now(),
|
|
nextAutoRetryAt: gateUntil,
|
|
retryDisabledUntil: gateUntil
|
|
}
|
|
}
|
|
|
|
queue.delete(key)
|
|
const requestSequence = events.nextSequence()
|
|
const requestStartedAt = Date.now()
|
|
queue.noteRequestStarted(key, requestSequence)
|
|
events.broadcast({ aliases, reason, status: 'in-flight', requestStartedAt }, requestSequence)
|
|
const prepared = await preparePRBranchRead(
|
|
candidate.repoPath,
|
|
candidate.branch,
|
|
candidate.linkedPRNumber ?? null,
|
|
candidate.connectionId ?? null,
|
|
candidate.linkedPRNumber == null ? (candidate.fallbackPRNumber ?? null) : null,
|
|
...hostedReviewOptionArgs(candidate, reason, options)
|
|
)
|
|
try {
|
|
const result = await prepared.readResult(
|
|
drainer.scheduler.settleRead(key, candidate, aliases, prepared, requestSequence)
|
|
)
|
|
if (!result) {
|
|
throw new Error('An unrestricted PR read must return its own result')
|
|
}
|
|
const outcome = deliverablePRBranchOutcome(prepared, result)
|
|
if (!outcome) {
|
|
if (queue.ownsRequest(key, requestSequence)) {
|
|
events.broadcast(
|
|
{ aliases, reason, status: 'skipped', skippedReason: 'fresh' },
|
|
requestSequence
|
|
)
|
|
drainer.scheduler.completeWithoutOutcome(
|
|
key,
|
|
candidate,
|
|
aliases,
|
|
40,
|
|
undefined,
|
|
!isBackground(reason)
|
|
)
|
|
}
|
|
return {
|
|
revalidation: { invalidated: true, retryAfterMs: drainer.hostedJobs.retryAfterMs(prepared) }
|
|
}
|
|
}
|
|
if (!queue.ownsRequest(key, requestSequence)) {
|
|
events.broadcast({ aliases, reason, outcome, requestStartedAt }, requestSequence)
|
|
return outcome
|
|
}
|
|
let plannedRetryAt: number | undefined
|
|
let broadcastOutcome = outcome
|
|
if (outcome.kind === 'upstream-error' && visibility.has(key)) {
|
|
plannedRetryAt =
|
|
outcome.nextAutoRetryAt ??
|
|
retry.nextVisibleErrorRetryAt(key, visibility.candidate(key, candidate))
|
|
broadcastOutcome = retry.withErrorSchedule(outcome, plannedRetryAt)
|
|
}
|
|
retry.noteManualGate(key, broadcastOutcome)
|
|
events.broadcast(
|
|
{ aliases, reason, outcome: broadcastOutcome, requestStartedAt },
|
|
requestSequence
|
|
)
|
|
drainer.scheduleVisibleFollowUp(key, candidate, outcome, 40, aliases, undefined, {
|
|
plannedRetryAt,
|
|
...(reason === 'manual'
|
|
? { pendingMergeabilityDelayMs: MANUAL_MERGEABILITY_PENDING_REFRESH_MS }
|
|
: {})
|
|
})
|
|
return broadcastOutcome
|
|
} finally {
|
|
prepared.release()
|
|
}
|
|
}
|