fix(acpmux): link to a paced old peer in byte-sized pages, inside a progress-based deadline
Follow-up to #28542. Against a peer daemon from before #28182 (a fleet Mac with about 3,700 rows, 16.6 MB without tools), #28542's count-page fallback links, but slowly: the first two attempts hit "handshake timed out after 90s" and the third connected after 81 s. That peer paces its list builds (two connection lists back to back, then a window each; four builds, then one per window), and 512-row pages meant two refused whole lists plus eight pages.
Asked for: "(a) make the handshake timeout progress-based: each page that arrives resets or extends the deadline (keep an overall cap, and name it); (b) skip the doomed whole-list attempts on a peer known to be old or large: remember per peer that the last handshake needed pages and start with pages next time ...; (c) larger count pages". And: "byte-sized count pages (start 2048, aim for about 8 MiB, ~3 pages, so inside the burst), plus a list.paced retry on the whole attempts."
What changes (peer.rs)
- Byte-sized count pages. The first page asks for 2048 rows. Each next page asks for as many rows as fit in about 8 MiB at the last page's mean row size (clamped to 1..65536). A page refused as too large still halves. 3,700 rows of about 4.4 KB take two pages, inside the peer's build burst.
list.pacedon the whole attempts. Both whole watch attempts (with and without tools) wait theretryAfterMsthe peer names and ask again, as pages already did.- Progress-based deadline. Each page of a paged watch list moves the handshake deadline on by the handshake's own (90 s by default), up to
HANDSHAKE_MOST(5 min) from the start in all. A frame read after the deadline passed cannot finish the handshake. The timeout message now gives the real elapsed time. - Pages at once next time. A peer whose list the last handshake read in pages of rows is read so at once on the next handshake, as long as it reports the same build. A new build of the peer tries the whole list again.
Tests (peer_handshake_tests.rs)
A fake old-style peer models that build's pacing on a paused clock: per-connection pacing (a burst of two, then each list costs its share of a window), build pacing (four builds, then one per 15 s window), list.paced past a 30 s wait, whole lists refused over the message cap, and keyset count pages.
an_old_peer_that_paces_its_lists_links_inside_the_handshake_deadline: 3,700 rows × 4.4 KB link inside the deadline, every row once, in at most three pages.a_peer_whose_list_needed_pages_of_rows_is_read_in_them_at_once_next_time: the second link asks for no whole list.a_paged_watch_list_slower_than_the_deadline_in_all_still_links: five pages 40 s apart (200 s in all, past the 90 s deadline) still link.a_paged_watch_list_is_cut_at_the_handshake_cap: pages that keep coming in time, with the last one due right atHANDSHAKE_MOST, are cut there and never connect.select!is unbiased, so the reader checks the deadline before acting on any frame while the link is not ready.
Red on base (the first three: "handshake timed out after 90s"), green with the fix. The other handshake tests still pass.
Performance
Harness: peer_handshake_tests fake old-style peer (paused clock, 3,700 rows × 4.4 KB, 15 s pacing window). The before column comes from base with ACPMUX_PEER_TIMEOUT_SECS=200, so it can finish.
| metric | before | after | delta | harness or run link |
|---|---|---|---|---|
| first link to an old paced peer | 90.0 s (2 whole lists + 8 pages) | 23.3 s (2 whole lists + 2 pages) | −66.7 s (−74%) | an_old_peer_that_paces_its_lists_links_inside_the_handshake_deadline |
| next link to the same peer | 90.0 s (2 whole lists + 8 pages) | 0 s (0 whole lists + 2 pages) | −90 s | a_peer_whose_list_needed_pages_of_rows_is_read_in_them_at_once_next_time |
| links that time out at the default 90 s deadline | 3 of 3 in the fake; 2 of 3 on the real peer | 0 | −3 in the fake (−100%); −2 on the real peer | same |
There's no new timer: the existing handshake sleep is reset per page and ends with the handshake.
Risk: normal. It's client-side only and changes nothing for a peer that answers the whole list. The per-peer memory is one Option<String> that the next handshake reads.
Agent session: agent session
🤖 Generated with Claude Code