mirror of
https://github.com/stablyai/orca.git
synced 2026-09-30 00:03:15 +00:00
* perf(git): bound git subprocess execution with an atomic admission scheduler Field traces (#16038, #11363) show Windows freeze storms driven by unbounded concurrent git children (12+ at once, 50-65s status convoys for 25+ minutes). Admit every main-process git child against atomic per-budget base+headroom counters (general / network / per-route), with reserved interactive capacity, ordering-only aging, close-bound permit release, a 120s fail-safe read timeout that feeds scheduler backoff, tier plumbing through every option carrier, and coalesced+jittered visibility pollers. Killswitch: ORCA_GIT_ADMISSION_DISABLED=1. Storm harness A/B: max concurrent children 65 -> 6, interactive p95 791ms -> 88ms; output-parity battery byte-identical with admission on vs off. * test(git): run the admission output-parity battery on every platform Parity needs real git, not the storm harness's PATH stub, so it must not share that file's POSIX gate - Windows is the platform where parity evidence matters. * fix(git): preserve interactive admission invariants * perf(git): keep admission queue drains linear * fix(git): close final admission gaps * perf(git): bound eligible route selection * fix(merge): remove unrelated stale snapshot changes * fix(git): preserve refresh lifecycle authority * test(git): align admission lifetime contracts * fix(git): harden admission across runtime paths * fix(git): restore freshness for bulk status reads * test(git): repoint delete-dialog source pins after admission plumbing The hydration effect now orders its targets through orderDeleteWorktreeStatusHydrationTargets and passes includeLineStats alongside the abort signal, so both literal anchors stopped matching. The invariants are unchanged and still pinned: dropping the signal, the main-worktree/folder filter, or getState-instead-of-subscribe each still reddens this test. * Fix git admission tier propagation and lock ordering Decode optional Git status tiers permissively and default runtime RPC status reads to the status lane while preserving renderer caller intent. Acquire the FETCH_HEAD mutex before atomic admission so same-repository fetch waiters hold no global or route permits. Preserve automatic pull-request refresh reasons, keep explicit hosted-review refreshes interactive, remove the dead candidate tier, and keep relay scheduling unchanged. Use tier-aware status lease keys because a shared lease cannot be safely promoted after its admission request is queued or granted. * test: align expectations with admission plumbing * refactor(child-process): move the process contract types to process-spec run-process.ts crossed its line cap after gaining the termination observer; the public types and defaults move out with re-exports so no caller changes. * chore: restore pnpm-lock.yaml to main (unintended local drift) --------- Co-authored-by: Merge Sim <sim@local>
147 lines
5.4 KiB
TypeScript
147 lines
5.4 KiB
TypeScript
import { getSshGitProviderGeneration } from '../providers/ssh-git-dispatch'
|
|
import { runCoalescedProbe, type CoalescedProbes } from './coalesced-probe'
|
|
import { isTransientGitProbeError, readRemoteUrl } from './remote-url-probe'
|
|
import { isStableMissingGitRemoteError } from './stable-missing-git-remote-error'
|
|
import type { GitAdmissionTier } from './command-runner/git-exec-options'
|
|
|
|
/**
|
|
* The "is this repo mine?" probe every forge integration runs: read the remote's
|
|
* URL once per repo/runtime, cache what the provider's parser made of it, and
|
|
* never cache an answer a failed probe never gave.
|
|
*/
|
|
|
|
const REPO_REF_CACHE_MAX_ENTRIES = 512
|
|
|
|
/**
|
|
* Why: "not this provider" only holds until someone edits the repo's remotes —
|
|
* and a repo first probed before it had any remote answers that way too.
|
|
* Nothing here watches `.git/config`, and a watcher cannot cover the SSH and WSL
|
|
* runtimes this cache also serves, so negatives expire instead: one probe per
|
|
* repo per interval is what lets a remote added mid-session be picked up without
|
|
* a restart. Positives stay, as they did before.
|
|
*/
|
|
export const NEGATIVE_ENTRY_TTL_MS = 5 * 60_000
|
|
|
|
type CachedRepoRef<Ref> = { value: Ref | null; expiresAt: number }
|
|
|
|
export type RemoteRefLocalGitOptions = {
|
|
wslDistro?: string
|
|
admissionTier?: GitAdmissionTier
|
|
}
|
|
|
|
export type RemoteRefProbeCache<Ref> = {
|
|
get(
|
|
repoPath: string,
|
|
remoteName: string,
|
|
connectionId?: string | null,
|
|
localGitOptions?: RemoteRefLocalGitOptions
|
|
): Promise<Ref | null>
|
|
clear(): void
|
|
size(): number
|
|
}
|
|
|
|
export function createRemoteRefProbeCache<Ref>(
|
|
parseRemoteUrl: (remoteUrl: string) => Ref | null
|
|
): RemoteRefProbeCache<Ref> {
|
|
const repoRefCache = new Map<string, CachedRepoRef<Ref>>()
|
|
const inFlight: CoalescedProbes<Ref | null> = new Map()
|
|
|
|
function remember(cacheKey: string, value: Ref | null): void {
|
|
repoRefCache.set(cacheKey, {
|
|
value,
|
|
expiresAt: value === null ? Date.now() + NEGATIVE_ENTRY_TTL_MS : Number.POSITIVE_INFINITY
|
|
})
|
|
while (repoRefCache.size > REPO_REF_CACHE_MAX_ENTRIES) {
|
|
const oldestKey = repoRefCache.keys().next().value
|
|
if (oldestKey === undefined) {
|
|
return
|
|
}
|
|
repoRefCache.delete(oldestKey)
|
|
}
|
|
}
|
|
|
|
async function probe(
|
|
cacheKey: string,
|
|
ownsKey: () => boolean,
|
|
repoPath: string,
|
|
remoteName: string,
|
|
connectionId: string | null | undefined,
|
|
localGitOptions: RemoteRefLocalGitOptions
|
|
): Promise<Ref | null> {
|
|
// Why: a probe abandoned as stale still runs, and its answer describes a repo
|
|
// state older than whatever the successor is about to store — or already has.
|
|
// It may still answer its own callers; it may not publish.
|
|
const publish = (value: Ref | null): void => {
|
|
if (ownsKey()) {
|
|
remember(cacheKey, value)
|
|
}
|
|
}
|
|
try {
|
|
const stdout = await readRemoteUrl(
|
|
{
|
|
repoPath,
|
|
connectionId,
|
|
...(localGitOptions.wslDistro ? { wslDistro: localGitOptions.wslDistro } : {}),
|
|
...(localGitOptions.admissionTier ? { admissionTier: localGitOptions.admissionTier } : {})
|
|
},
|
|
remoteName
|
|
)
|
|
// Why: null is the SSH runtime being disconnected, not an answer about the
|
|
// remote — and it costs no `git`, so there is nothing here to spare. It is
|
|
// deliberately the one negative with no TTL floor: flooring it would make a
|
|
// reconnected host wait the interval out for a probe it could serve now.
|
|
if (stdout === null) {
|
|
return null
|
|
}
|
|
const result = parseRemoteUrl(stdout)
|
|
publish(result)
|
|
return result
|
|
} catch (error) {
|
|
// Why: a probe killed on its deadline says nothing about the remote, and an
|
|
// SSH failure is usually a reconnect or tunnel state rather than an answer.
|
|
// Only "no such remote" is the repo itself saying it is not this provider's
|
|
// — anything else cached would poison it, on SSH for the generation's life.
|
|
if (isTransientGitProbeError(error)) {
|
|
return null
|
|
}
|
|
if (connectionId && !isStableMissingGitRemoteError(error)) {
|
|
return null
|
|
}
|
|
publish(null)
|
|
return null
|
|
}
|
|
}
|
|
|
|
return {
|
|
async get(repoPath, remoteName, connectionId, localGitOptions = {}) {
|
|
// Why: a reconnect retires the connection an answer came from, and with it
|
|
// the probe still running on it — stamping the generation stops a caller on
|
|
// the new connection from adopting either.
|
|
const runtimeKey = connectionId
|
|
? `${connectionId}:${getSshGitProviderGeneration(connectionId)}`
|
|
: `local:${localGitOptions.wslDistro ?? 'host'}`
|
|
const cacheKey = `${runtimeKey}\0${repoPath}\0${remoteName}`
|
|
const cached = repoRefCache.get(cacheKey)
|
|
if (cached) {
|
|
if (cached.expiresAt > Date.now()) {
|
|
return cached.value
|
|
}
|
|
repoRefCache.delete(cacheKey)
|
|
}
|
|
// Why: every branch of a repo resolves its forge through this probe, so a
|
|
// poll of the worktree list arrives as a burst of identical lookups. One
|
|
// young probe answers all of them instead of spawning a `git` per branch.
|
|
return runCoalescedProbe(inFlight, cacheKey, (ownsKey) =>
|
|
probe(cacheKey, ownsKey, repoPath, remoteName, connectionId, localGitOptions)
|
|
)
|
|
},
|
|
clear() {
|
|
repoRefCache.clear()
|
|
inFlight.clear()
|
|
},
|
|
size() {
|
|
return repoRefCache.size
|
|
}
|
|
}
|
|
}
|