What's wrong
In DiffCache.GetAsync (GitBranchStateCache/Diffs/DiffCache.cs:59-81), a follower waits for the leader with WaitForLeaderAsync(DiffTimeout). If the leader's result is not true and published, the follower calls ComputeAsync itself. The leader reports only a bool, ticket.Complete(outcome.Succeeded), and a timeout counts as false. So a follower cannot tell "the leader ran this exact diff and it timed out" from "the leader was abandoned". Failures are also never cached, so every client heartbeat repeats the whole cycle.
MirrorFetcher makes the opposite decision for the same situation (Mirrors/MirrorFetcher.cs:111-116): "Trying again here would multiply exactly the load the coalescer exists to prevent."
Failure scenario
A pathological merge-base..tip diff (a huge vendor drop or a generated-file rewrite) runs past DiffTimeout (default 2 min). N clients are asking about that branch:
- The leader times out at 2 min.
- All N followers, having waited up to 2 min, each start their own
git diff-tree on exactly the diff already known to be too slow. That is N more processes, each running up to another 2 min.
- Every follower's response takes about 4 min and still ends in
timeout.
- On the next heartbeat (30 s) the cache is still empty, so the stampede repeats.
Reproduction
With the repo's test harness (ServiceFixture + ScriptedGit with DiffTimesOut = true and a 1 s delay on diff-tree), 10 concurrent requests for the same uncached diff produced 10 diff-tree invocations. All returned 200 with the branch marked partial/timeout.
Suggested fix
- Carry the outcome kind through
IWorkTicket, e.g. Succeeded / TimedOut / Abandoned, instead of a bool.
- When the leader timed out, followers return
DiffOutcome.Timeout() without recomputing. They recompute only when the leader was abandoned (cancelled, or the process died).
- Optionally cache a timeout per
DiffKey for a short period, e.g. one heartbeat interval, so repeated heartbeats don't re-run a diff known to be too slow.
Acceptance criteria
- With a diff that times out, N concurrent requests produce exactly one
diff-tree invocation, and every follower gets a timeout outcome within about DiffTimeout.
- A leader that is cancelled or abandoned still lets a follower compute, which keeps the existing fallback behaviour.
Related, not duplicates: #50 (a non-UTF-8 path stalls a diff) and #44 (clone leader cancellation, on the mirror side).
What's wrong
In
DiffCache.GetAsync(GitBranchStateCache/Diffs/DiffCache.cs:59-81), a follower waits for the leader withWaitForLeaderAsync(DiffTimeout). If the leader's result is nottrueand published, the follower callsComputeAsyncitself. The leader reports only a bool,ticket.Complete(outcome.Succeeded), and a timeout counts asfalse. So a follower cannot tell "the leader ran this exact diff and it timed out" from "the leader was abandoned". Failures are also never cached, so every client heartbeat repeats the whole cycle.MirrorFetchermakes the opposite decision for the same situation (Mirrors/MirrorFetcher.cs:111-116): "Trying again here would multiply exactly the load the coalescer exists to prevent."Failure scenario
A pathological merge-base..tip diff (a huge vendor drop or a generated-file rewrite) runs past
DiffTimeout(default 2 min). N clients are asking about that branch:git diff-treeon exactly the diff already known to be too slow. That is N more processes, each running up to another 2 min.timeout.Reproduction
With the repo's test harness (
ServiceFixture+ScriptedGitwithDiffTimesOut = trueand a 1 s delay ondiff-tree), 10 concurrent requests for the same uncached diff produced 10diff-treeinvocations. All returned 200 with the branch marked partial/timeout.Suggested fix
IWorkTicket, e.g.Succeeded/TimedOut/Abandoned, instead of a bool.DiffOutcome.Timeout()without recomputing. They recompute only when the leader was abandoned (cancelled, or the process died).DiffKeyfor a short period, e.g. one heartbeat interval, so repeated heartbeats don't re-run a diff known to be too slow.Acceptance criteria
diff-treeinvocation, and every follower gets a timeout outcome within aboutDiffTimeout.Related, not duplicates: #50 (a non-UTF-8 path stalls a diff) and #44 (clone leader cancellation, on the mirror side).