mirror of
https://github.com/stablyai/orca.git
synced 2026-10-19 08:03:13 +00:00
fix(main): enumerate git-common worktrees every tick to catch coarse-FS add/remove
The worktrees-dir readdir was gated on the dir's mtime:ctime:ino:size signature, so on a coarse-mtime/FAT filesystem a same-granule add+remove (all four fields collide) went undetected until the ~30s index backstop. A single readdir of a small dir is negligible next to the per-entry structural stats that already run every tick, so always enumerate — the listing is the authoritative add/remove signal. The expensive per-entry index read stays gated on each entry's own dir signature; onFullScan now reflects the ungated index-metadata backstop fan-out (the real periodic cost) rather than the readdir. Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
@@ -132,21 +132,20 @@ async function snapshotGitCommon(
|
||||
dirSignature(worktreesDir),
|
||||
includePrimary ? snapshotPrimaryCheckoutSignatures(commonDirPath) : new Map<string, string>()
|
||||
])
|
||||
const shouldReadWorktreesDir =
|
||||
forceFullScan || !previous || previous.worktreesDirSignature !== worktreesDirSignature
|
||||
// Why: enumerate the worktrees dir EVERY tick rather than gating the readdir on its stat signature.
|
||||
// A single readdir of a small dir is negligible next to the per-entry structural stats that already
|
||||
// run each tick, and the signature gate could miss a same-granule add+remove on a coarse-mtime/FAT
|
||||
// filesystem (its size/mtime/ino/ctime all collide), leaving a linked worktree add/remove undetected
|
||||
// until the ~30s index backstop (#9882 review). The listing is the authoritative add/remove signal.
|
||||
let entryPaths: string[]
|
||||
if (shouldReadWorktreesDir) {
|
||||
try {
|
||||
const entries = await readdir(worktreesDir, { withFileTypes: true })
|
||||
entryPaths = entries
|
||||
.filter((entry) => entry.isDirectory())
|
||||
.map((entry) => join(worktreesDir, entry.name))
|
||||
} catch {
|
||||
// Missing worktrees dir is normal for repos without linked worktrees.
|
||||
entryPaths = []
|
||||
}
|
||||
} else {
|
||||
entryPaths = [...previous.entries.keys()]
|
||||
try {
|
||||
const entries = await readdir(worktreesDir, { withFileTypes: true })
|
||||
entryPaths = entries
|
||||
.filter((entry) => entry.isDirectory())
|
||||
.map((entry) => join(worktreesDir, entry.name))
|
||||
} catch {
|
||||
// Missing worktrees dir is normal for repos without linked worktrees.
|
||||
entryPaths = []
|
||||
}
|
||||
|
||||
const entries = new Map<string, GitCommonEntrySnapshot>()
|
||||
@@ -156,13 +155,14 @@ async function snapshotGitCommon(
|
||||
entries.set(entryPath, await snapshotGitCommonEntry(entryPath, previousEntry, forceFullScan))
|
||||
})
|
||||
)
|
||||
// Why: structural leaves are re-stat'd every tick now, so the only gated work is the worktrees-dir
|
||||
// readdir; onFullScan reflects that enumeration running (vs a gated skip when the dir is unchanged).
|
||||
// Why: the expensive per-entry `index` read stays gated on each entry's own dir signature; onFullScan
|
||||
// now reflects an ungated index-metadata backstop fan-out (forceFullScan) — the real periodic cost —
|
||||
// rather than the always-run worktrees-dir readdir.
|
||||
return {
|
||||
worktreesDirSignature,
|
||||
entries,
|
||||
primarySignatures,
|
||||
didFullScan: shouldReadWorktreesDir
|
||||
didFullScan: forceFullScan
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -304,7 +304,10 @@ describe('worktree git-common polling gate (non-darwin)', () => {
|
||||
cleanups.push(() => watch.unsubscribe())
|
||||
}
|
||||
|
||||
it('skips worktrees readdir and entry metadata fan-out on idle ticks', async () => {
|
||||
it('skips the ungated index-metadata backstop on idle ticks', async () => {
|
||||
// Why: idle ticks still re-stat structural leaves and list the (small) worktrees dir cheaply, but the
|
||||
// heavier ungated per-entry index fan-out (onFullScan) must NOT run until the backstop — and no
|
||||
// spurious events are emitted while nothing changes.
|
||||
const commonDir = await makePollingCommonDir()
|
||||
const entry = join(commonDir, 'worktrees', 'idle')
|
||||
await mkdir(join(entry, 'logs'), { recursive: true })
|
||||
@@ -320,7 +323,10 @@ describe('worktree git-common polling gate (non-darwin)', () => {
|
||||
expect(received.flat()).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('fans out when linked worktrees are added and removed', async () => {
|
||||
it('detects linked worktree add and remove from the every-tick readdir', async () => {
|
||||
// Why: the worktrees-dir listing runs every tick (not gated on its stat signature), so an add/remove
|
||||
// surfaces within one poll interval even on a coarse-mtime filesystem whose dir signature would not
|
||||
// move — without waiting on the index backstop (onFullScan).
|
||||
const commonDir = await makePollingCommonDir()
|
||||
const received: WorktreeBasePollEvent[][] = []
|
||||
const fullScans = vi.fn()
|
||||
@@ -331,20 +337,17 @@ describe('worktree git-common polling gate (non-darwin)', () => {
|
||||
await vi.waitFor(() => {
|
||||
expect(received.flat()).toContainEqual({ type: 'create', path: entry })
|
||||
})
|
||||
const scansAfterAdd = fullScans.mock.calls.length
|
||||
expect(scansAfterAdd).toBeGreaterThan(0)
|
||||
|
||||
await rm(entry, { recursive: true })
|
||||
await vi.waitFor(() => {
|
||||
expect(received.flat()).toContainEqual({ type: 'delete', path: entry })
|
||||
})
|
||||
expect(fullScans.mock.calls.length).toBeGreaterThan(scansAfterAdd)
|
||||
})
|
||||
|
||||
it('detects an in-place structural (HEAD) write on a known entry every tick, without a readdir', async () => {
|
||||
it('detects an in-place structural (HEAD) write on a known entry every tick, without the index backstop', async () => {
|
||||
// Why: a raw HEAD/gitdir/config.worktree rewrite does not bump the entry-dir mtime, so the
|
||||
// structural leaves are re-stat'd every tick (never gated) — the change surfaces within one tick
|
||||
// and does NOT require the gated worktrees-dir readdir (onFullScan).
|
||||
// and does NOT require the ungated index-metadata backstop (onFullScan).
|
||||
const commonDir = await makePollingCommonDir()
|
||||
const entry = join(commonDir, 'worktrees', 'structural')
|
||||
await mkdir(entry)
|
||||
|
||||
Reference in New Issue
Block a user