docs(plans): add the single-flight wait to S32's write-deadline budgets

Please land this before the Step 15 or Step 16 MR opens. It brings the write-deadline budgets in the S32 container virtual plan in line with how Step 14 (composition root) arms the standalone container reads. As written, the plan would have Steps 15 and 16 arm virtual deadlines that are short by the single-flight wait and that never re-arm once the fetch returns.

This MR is related to S32 Step 14.

Why

Step 14 puts the standalone container fill behind the one flight registry. A read queued past max_concurrent_fills_per_repo, or following a slow leader, can wait up to single_flight_wait_timeout (default 5m) before its handshake starts. The Step 14 branch arms each standalone read twice:

  • Before the fetch, to a budget that opens on that wait, in WithRemoteBlobFill and WithManifestFill. Without the wait, at the defaults, a queued read was cut at 45s with an empty reply.
  • As the fetch returns, to remote.FillTailBudget: the token exchange, the upstream request, detachedCacheOpTimeout and the grace (75s at the defaults). A drained answer runs the post-Commit upsert after the fetch returns, under that 30s bound of its own, so a tail without it truncated a slow upsert's answer. Nothing is written before the fetch returns, so every write runs under the tail. A single arm held a stalled client's connection for the whole 5m45s, and a follower whose leader left can enter the flight a second time and wait again, past a budget that counts one wait.

A virtual winner's Fetch acquires the same flight (virtualLazyFlight in internal/format/oci/virtual_assembly.go). That wait starts after winner selection, outside resolve_timeout, as S32 §Configuration states. The plan derived both virtual budgets without the wait and without the second arm. With the wait and no second arm, a virtual read whose client stops reading holds its connection for 7m15s at the defaults.

Step MRs do not edit the plan file (AGENTS.md guardrail 4), so the correction ships here rather than in the Step 14 MR.

What changes

  • The research paragraph on the arming helpers (L391-L410) and ### Every virtual route arms its own write deadline (L583-L604) state both standalone arms. The re-arm, not "a read enters its flight once", is why one wait term is enough.
  • Both virtual expressions gain single_flight_wait_timeout, once each, and a new paragraph has each virtual arm re-arm to the tail once Resolve returns (L623-L664). The winner's Fetch runs inside Resolve, so on a fetching path that is the standalone arms' instant, and warm serves and error answers write under the tail too.
  • Step 11 (L4133-L4143), Step 15 (L4620-L4631) and Step 16 (L4721-L4747) carry the wait term and the re-arm.
  • A new paragraph (L608-L621) scopes the second-wait argument to HTTP/1.1. On HTTP/2 a fired stream deadline resets the stream, so the wait term covers the first wait and nothing covers a second.
  • The e2e ownership sentence scopes Step 18 to the virtual-repository scenarios, since Step 14 adds two rows to the oci catalog's remote section.
  • Step 14's entry (L4470-L4659) lists the arm changes in its Files, Acceptance, and Tests, and names fillTail, responseTail, rearmingManifestFiller, and leaderLossRetry as symbols it introduces.

What does not change

  • The revalidation budget pays the wait once, not twice: its second fetch runs inside the leader that entered the flight.
  • The only existing figures that move are Step 14's size (~420 to ~500 source) and the two plan-wide source sums built from it. The new figure 7m15s is resolve_timeout plus the standalone 5m45s at the defaults.
  • !2904 (merged) edits the same file at other ranges and merges cleanly with this one. Its Step 16 entry passes "the instant it arms" to logStreamFailure. After this lands, that instant is the re-armed one.

🤖 Generated with Claude Code

Edited by Sylvia Shen

Merge request reports

Loading
Loading