What's wrong
The service never checks that a mirror is a usable repository before trusting it:
MirrorStore.Exists (GitBranchStateCache/Mirrors/MirrorStore.cs:76) is a bare Directory.Exists. MirrorFetcher (Mirrors/MirrorFetcher.cs:59) re-clones only when that check is false. Otherwise it fetches into whatever is there.
WriteMarker (MirrorStore.cs:198-209) calls Directory.CreateDirectory(directory) before writing .last-used or .refs-fetched-at inside the mirror directory. MarkUsed runs from MirrorFetcher.cs:204 and BranchStateHandler.cs:169/375, so it can create or recreate the mirror directory on its own.
- The idle reaper's
MirrorStore.Delete (MirrorStore.cs:126-132) is a plain recursive delete. It is called from MirrorMaintenanceService and shares no lock with the fetcher.
Failure scenario
- The idle sweep starts deleting a large
mirror.git, and either:
- the pod is stopped mid-delete (rolling update or eviction), or
- a request for that repository arrives mid-delete.
MarkUsed recreates the directory and .last-used, and the recursive delete fails with "directory not empty". That failure is only logged.
mirror.git is now a directory that is not a working bare repository. It may hold nothing but .last-used.
- Every later request sees
Exists == true and fetches instead of cloning. The fetch fails and is treated as stale rather than missing. MarkUsed runs again, and for-each-ref fails, so the request gets 502 refs-unreadable.
- Each request refreshes
.last-used, so the idle sweep never reaps the broken mirror.
The only recovery is to delete the directory by hand on the volume.
Suggested fix
- Reap atomically. Rename the mirror to a tombstone name, for example
mirror.git.deleting-<guid>, then delete the tombstone. A crash mid-delete then leaves only a tombstone, which the next sweep can remove.
- Stop markers creating the mirror.
WriteMarker should not create the mirror directory. If the directory is gone, skip the marker.
- Validate before trusting. Treat a directory that fails
git rev-parse --is-bare-repository (or lacks HEAD/objects) as missing, and re-clone it, into a temporary directory and then rename, as the clone path already does if that applies.
Acceptance criteria
- A mirror directory holding only a
.last-used file is re-cloned on the next request, which succeeds.
- A reap that is interrupted, or that races a request, never leaves a
mirror.git the fetcher will trust.
MarkUsed on a missing mirror does not create the directory.
- Tests cover each of these.
What's wrong
The service never checks that a mirror is a usable repository before trusting it:
MirrorStore.Exists(GitBranchStateCache/Mirrors/MirrorStore.cs:76) is a bareDirectory.Exists.MirrorFetcher(Mirrors/MirrorFetcher.cs:59) re-clones only when that check is false. Otherwise it fetches into whatever is there.WriteMarker(MirrorStore.cs:198-209) callsDirectory.CreateDirectory(directory)before writing.last-usedor.refs-fetched-atinside the mirror directory.MarkUsedruns fromMirrorFetcher.cs:204andBranchStateHandler.cs:169/375, so it can create or recreate the mirror directory on its own.MirrorStore.Delete(MirrorStore.cs:126-132) is a plain recursive delete. It is called fromMirrorMaintenanceServiceand shares no lock with the fetcher.Failure scenario
mirror.git, and either:MarkUsedrecreates the directory and.last-used, and the recursive delete fails with "directory not empty". That failure is only logged.mirror.gitis now a directory that is not a working bare repository. It may hold nothing but.last-used.Exists == trueand fetches instead of cloning. The fetch fails and is treated as stale rather than missing.MarkUsedruns again, andfor-each-reffails, so the request gets 502refs-unreadable..last-used, so the idle sweep never reaps the broken mirror.The only recovery is to delete the directory by hand on the volume.
Suggested fix
mirror.git.deleting-<guid>, then delete the tombstone. A crash mid-delete then leaves only a tombstone, which the next sweep can remove.WriteMarkershould not create the mirror directory. If the directory is gone, skip the marker.git rev-parse --is-bare-repository(or lacksHEAD/objects) as missing, and re-clone it, into a temporary directory and then rename, as the clone path already does if that applies.Acceptance criteria
.last-usedfile is re-cloned on the next request, which succeeds.mirror.gitthe fetcher will trust.MarkUsedon a missing mirror does not create the directory.