Skip to content

D256 — The client pulls the KBs' manifests concurrently, and reports errors in KB order

Decision. fetchCandidates issues the per-KB sync_pull calls concurrently, at most four in flight (maxConcurrentPulls). Each call only fetches, decodes and hash-checks its own KB's artifacts; everything that depends on order runs afterwards over the results in target order: the error that is reported (the first failing KB in that order, not the first to fail in time) and the cross-KB signature check over seen.

Why. Each sync_pull can wait on a server-side fetch of its KB's remote (D93), and the server serializes git work per KB only, so in series a sync cost the sum of those waits (about 22 s for five KBs, where the slowest one took about 7 s). Concurrently it costs about the slowest one. Keeping the order-sensitive steps sequential keeps the result and every message identical to the sequential loop, so a failing run names the same KB on every attempt. What it costs: a run that fails on its first KB still waits for the others' responses, and a server sees up to four fetches from one client at once.

Alternatives rejected. - One goroutine per KB with no bound: a large setup would start a fetch against every remote at once, for no gain once the slowest KB dominates. - Returning the first error in time and cancelling the rest: the reported KB would change between runs, and an operator retrying would chase a different message each time. - golang.org/x/sync/errgroup: it cancels on the first error, which is the behaviour above; a sync.WaitGroup with a channel semaphore needs no new direct dependency.

Consequences. pullTarget must stay free of state shared between targets; the per-target client.MCPClient is a copy (WithKB), which is what makes this safe. A future "continue past a KB that cannot be pulled" (#350) can read the per-KB results directly instead of stopping at the first error.