You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
refsAsOf is stamped after the fetch and commit-graph write finish, so answered refs can be older than RefsTtl + RefsServeStaleFor promises #112
git reads the upstream's refs at the start of a fetch or clone. GitBranchStateCache records the refs time at the end:
Mirrors/MirrorFetcher.csFetchAsync runs git fetch, then WriteCommitGraphAsync (which can itself take up to another FetchTimeout), and only then calls mirrors.MarkFetched(directory) (~L314). MirrorStore.WriteMarker stamps that marker with timeProvider.GetUtcNow() (MirrorStore.cs ~L240).
CloneAsync stamps after the whole clone, the commit-graph write and the move into place (~L354).
Both return MirrorFetchResult.Current(mirrors.RefsFetchedAt(directory) ?? timeProvider.GetUtcNow()), which is also a post-fetch time.
So the recorded time is later than the moment the refs really matched the upstream, by up to the fetch time plus the commit-graph time.
Why it matters
This breaks documented guarantees:
The README and MirrorFetchResult describe refsAsOf as "when the refs were last known to match the upstream".
The RefsServeStaleFor docs (Configuration/GitBranchStateCacheOptions.cs ~L52–65) promise "Answered refs are never older than RefsTtl plus this, which the validator holds within AdmissionTtl". The validator's RefsTtl <= AdmissionTtl rule exists so that state does not outlive the authorization.
Example with defaults (RefsTtl 30 s, ServeStaleFor 30 s, AdmissionTtl 60 s, FetchTimeout 2 min):
A fetch starts at t0. The fetch takes 20 s and the commit-graph write 10 s, so refsAsOf is t0+30 s.
Answers are treated as fresh until t0+60 s and served stale until t0+90 s.
A push at t0+1 s is missing from answers for up to 90 s, which is past AdmissionTtl, and clients are told the data is 30 s newer than it is.
Suggested fix / acceptance criteria
Take the timestamp before starting the fetch or clone and pass it through (for example MarkFetched(directory, startedAt)). Use the same value in the ?? timeProvider.GetUtcNow() fallbacks.
Test with a fake TimeProvider that advances while git runs: RefsAsOf and the age-against-TTL check both use the pre-fetch time.
Update the existing assertions that expect the post-fetch time (MirrorFetcherRealGitTests ~L197/L259, StateFlowTests ~L391/L416/L541).
Related, but different: #106 (followers waiting on the commit-graph write) and #73.
Priority: Medium. This breaks the documented RefsTtl + RefsServeStaleFor ≤ AdmissionTtl freshness guarantee, and clients are told their data is newer than it is. Nothing crashes, and the error is bounded by fetch time plus commit-graph time.
Area:Mirrors/MirrorFetcher.cs (FetchAsync, CloneAsync) and MirrorStore.MarkFetched / WriteMarker
Activity
matt-edmondson commentedon Oct 30, 2026
Triage
RefsTtl + RefsServeStaleFor ≤ AdmissionTtlfreshness guarantee, and clients are told their data is newer than it is. Nothing crashes, and the error is bounded by fetch time plus commit-graph time.Mirrors/MirrorFetcher.cs(FetchAsync,CloneAsync) andMirrorStore.MarkFetched/WriteMarkerstartedAtbefore git runs and pass it toMarkFetchedand to the fallbacks. Update the tests that assert the post-fetch time.Generated by Claude Code