Skip to content

Requests right after a client pushes usually get 409 unknown-base: an unknown base never triggers a fetch while refs are within RefsTtl #65

Description

@matt-edmondson

What happens

HandleStateAsync first calls fetcher.EnsureCurrentAsync. That call does not fetch if the mirror's refs were fetched less than RefsTtl ago; the default is 30s (GitBranchStateCache/Mirrors/MirrorFetcher.cs:62-66). Only after that does the handler check whether the mirror holds base (Endpoints/BranchStateHandler.cs, the ContainsCommitAsync check). If the mirror doesn't hold it, the answer is 409 unknown-base and the client falls back to its local computation.

So a base that is missing only because the mirror lags the forge by up to 30 seconds gets the same answer as a base that will never exist.

Why it is common rather than a corner case

base is the client's @{upstream}, which moves as soon as that client pushes or pulls directly from the forge. Clients poll about every 30s and RefsTtl is 30s. A typical sequence:

  1. Another client's request fetches the mirror at t=0.
  2. The artist pushes commit X at t=5.
  3. Their editor sends POST .../state with base: X at t=10.
  4. The mirror still counts as current (10s < 30s), so nothing is fetched. cat-file -e X^{commit} fails, and the answer is 409.

The next heartbeat at t=40 would succeed, but by then the fallback has cost the client its own fetch plus log and diff, which is the work this service exists to remove.

It also inflates gitbranchstatecache.unknown_base, which Observability/BranchStateMetrics.cs describes as the counter to alert on. On a busy team most of that count would be this harmless lag rather than clients drifting from the integration point.

Suggested fix

  • When the base is unknown and the refs were served from the TTL shortcut (this request did not fetch them itself), run one fetch through the existing single-flight path, so concurrent requests share it.
  • Re-read the refs and check the base again. Return 409 only if it is still missing.
  • Bound this so a caller sending random bases cannot force a fetch on every request. For example, skip the forced fetch when the refs are younger than a small minimum age (a new option such as UnknownBaseRefetchMinAge, a few seconds), and keep the existing coalescing.

Acceptance criteria

  • A request whose base exists upstream, pushed after the mirror's last fetch and within RefsTtl, gets a 200 answer computed against the refreshed refs.
  • A base that does not exist upstream still gets 409, after at most one extra fetch shared by concurrent requests.
  • Repeated unknown-base requests within the minimum age trigger no further fetches.
  • refsAsOf in the response reflects the refetch.
  • StateFlowTests (scripted git) cover all of the above.

Activity

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

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions