fix(remote): standalone serve suite and upstream response release (S13 Step 11, part 3/3)
📦 What this MR does
Part 3 of 3 of S13 virtual/remote foundation — Step 11: standalone remote request.
Parts 1/3 (!1290 (merged)) and 2/3 (!1291 (merged)) have merged, so this branch is rebased onto main and targets it directly.
It completes the Standalone.Serve suite beyond part 2/3's one-test-per-acceptance-row core, and carries one production fix raised in review (see Review follow-ups below).
🎯 What it covers
- Leader-loss retry choreography:
TestStandalone_Serve_LeaderCancellationRetriesOnceandTestStandalone_Serve_LeaderAbandonmentRetriesOncepin the retry-once decision for both sentinels a single leader disconnect can surface as, so the outcome does not depend on race ordering. - Vanished-blob recovery budget:
TestStandalone_Serve_FreshHitVanishedBlobRefetches,TestStandalone_Serve_RevalidatedBlobVanishedRefetches,TestStandalone_Serve_RevalidatedBlobVanishedTwiceIsCacheFill, andTestStandalone_Serve_FallbackBlobVanishedIsUnavailable— exactly one unconditional re-fetch per Serve, on every path — withTestStandalone_Serve_FreshHitIntegrityFaultFailspinning the split the recovery must not blur. - Non-transport error partition:
TestStandalone_Serve_NonTransportFetchErrorsPropagate,TestStandalone_Serve_SingleFlightTimeoutPropagates,TestStandalone_Serve_RequestBuilderFailurePropagates,TestStandalone_Serve_LookupErrorPropagates, andTestStandalone_Serve_Upstream5xxNeverServesCache— local failures and deliberate upstream answers never route through the cache fallback. - Health-read degrade logging:
TestStandalone_Serve_HealthReadAbandonedLogsDebug— the gate proceeds on an unreadable status, WARN for a degraded read, DEBUG when the caller's own context ended. Both health-read tests pin level and message together on one record: asserting the level alone passes on any record of that level the flow emits, and the fetch and cache paths log too. - Coalescing fidelity:
TestStandalone_Serve_Follower304WithRefilledRowServes— a 304 vouches for the row, not the caller's pre-fetch snapshot. - Composed-flow coalescing proof:
TestStandalone_Serve_CoalescesConcurrentServesOverRealClient— N concurrent Serves over the realupstreamhttp.ClientandDoer(not fakes) produce exactly one upstream GET and one cache write, the proof Step 8 assigned to the first composition that wires the single flight into a flow.
Three tests part 2/3 already landed grow rather than gaining a near-duplicate beside them:
- The two unusable-upstream-response tables gain the undecoded-content-coding row. It reaches the same class by a different route: the 204/206 and 304 rows are rejected on the response's own shape, while an encoded 200 is a nominally fine response whose body the client cannot use.
TestStandalone_Serve_StaleHitETagDiffersRefillsgains the refilled row's blob reference, plus distinct stale and fresh bodies so that assertion is not vacuous.
🔨 Review follow-ups
- Duo — response leak in
upstreamhttp.Doer.Do. Fixed. A response returned alongside an error is now drained and closed throughdrainAndClose, the package's existing disposal for a body no caller will read — the same drain-then-closeFetcher.fetchOnceapplies on the other side of the seam, so the seam has one policy rather than one per direction.Client.Doreturns no such response — it cancels and returns a nil response on its single error path — so no live leak exists today, butremote.UpstreamDoerconstrains only the nil-error shape.Doernow holds its client as an unexported interface so the release can be driven directly;NewDoerstill takes the concrete*Client. - Duo —
recordDownloadreusing the 30s detached-write timeout. Already closed onmainby part 2/3's own review:detachedDownloadBumpTimeoutis its own 5s constant, with the reasoning Duo asked for recorded on it. - Review — the doer's disposal policy and the invariant behind it. Fixed. The release drains before closing (above), and
TestClient_Do_ErrorReturnsNoResponsepins the invariant that made the branch unreachable —Client.Doreports no error with a response attached — over the assembled client, across every route into its error path. Until now that invariant lived only in a doc comment. - Review — concurrency scaffolding in the standalone suite. Fixed. The three coordination handshakes are bounded by
awaitSignal, so a flow that never reaches the site closing the channel fails with a named message instead of hanging until the package timeout. The single-flight timeout test registers its slot-holder release witht.Cleanup(sync.OnceFunc(...)), so a failed assertion cannot strand that goroutine with its body open. - Review — plan-step coordinate in a test doc comment. Fixed. The composed-coalescing test's comment now states what the test proves and why no other test in the file can prove it.
- The AppSec review is answered in its own thread.
✅ Testing
go test ./internal/remote/... -race passes, and golangci-lint run ./internal/remote/... is clean. The Doer fix is pinned by TestDoer_Do_ReleasesResponsePairedWithError, which asserts the disposal order and fails on a bare Close, and by TestClient_Do_ErrorReturnsNoResponse, which holds the invariant that keeps the released branch unreachable from the assembled client.
No e2e scenario catalog entry is added or affected: the Doer change guards a shape the assembled client cannot produce, so no client-observable behavior changes.
📖 References
:bricks: Stacked MRs (review/merge bottom-up)
- feat(remote): serve taxonomy, transport marker,... (!1290 - merged) • David Fernandez • 19.3
- feat(remote): standalone remote request flow (S... (!1291 - merged) • David Fernandez, Dzmitry (Dima) Meshcharakou • 19.3
- fix(remote): standalone serve suite and upstrea... (!1292 - merged) • David Fernandez, Dzmitry (Dima) Meshcharakou • 19.3
👈
Related to #332 (closed)