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:
- Another client's request fetches the mirror at t=0.
- The artist pushes commit X at t=5.
- Their editor sends
POST .../state with base: X at t=10.
- 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.
What happens
HandleStateAsyncfirst callsfetcher.EnsureCurrentAsync. That call does not fetch if the mirror's refs were fetched less thanRefsTtlago; the default is 30s (GitBranchStateCache/Mirrors/MirrorFetcher.cs:62-66). Only after that does the handler check whether the mirror holdsbase(Endpoints/BranchStateHandler.cs, theContainsCommitAsynccheck). If the mirror doesn't hold it, the answer is409 unknown-baseand 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
baseis the client's@{upstream}, which moves as soon as that client pushes or pulls directly from the forge. Clients poll about every 30s andRefsTtlis 30s. A typical sequence:POST .../statewithbase: Xat t=10.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, whichObservability/BranchStateMetrics.csdescribes 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
UnknownBaseRefetchMinAge, a few seconds), and keep the existing coalescing.Acceptance criteria
RefsTtl, gets a 200 answer computed against the refreshed refs.refsAsOfin the response reflects the refetch.StateFlowTests(scripted git) cover all of the above.