Skip to content

refsAsOf is stamped after the fetch and commit-graph write finish, so answered refs can be older than RefsTtl + RefsServeStaleFor promises #112

Open

Description

@matt-edmondson

What's wrong

git reads the upstream's refs at the start of a fetch or clone. GitBranchStateCache records the refs time at the end:

  • Mirrors/MirrorFetcher.cs FetchAsync 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):

  1. A fetch starts at t0. The fetch takes 20 s and the commit-graph write 10 s, so refsAsOf is t0+30 s.
  2. Answers are treated as fresh until t0+60 s and served stale until t0+90 s.
  3. 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.

Activity

  1. matt-edmondson commented on Oct 30, 2026

    @matt-edmondson
    ContributorAuthor

    Triage


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions