Extracts the two-pump data-plane helper alknet ADR-078 deferred until the shapes converged (they have: alktunnels POC pump_halves + alktty's channels session). Purely additive. - channels::pump::pump_bidi(channel, peer_read, peer_write) -> (u64, u64): two joined pumps, shutdown-on-completion per direction; copy counts for observability. The channel side is a single AsyncRead + AsyncWrite value (the accept_bi BiStream); the peer side takes split halves — the establisher's natural dial result (into_split). - Return (u64, u64), not the review sketch's io::Result<(u64, u64)>: both pumps swallow copy errors by contract (mid-stream error = abrupt close, no error channel mid-stream per ADR-049 §6), so an Err state would be dead code. Deviation recorded in ADR-050. - alktty's three-pump session does not fit (exit future as a third signal) and stays as-is, per the review's scope. - Tests reproduce the POC's two-pump semantics through the helper: bidirectional flow with exact counts, EOF-from-one-side completes the other's shutdown (clean EOF at the far end), dead-source = EOF-shaped teardown. - ADR-050 records the decision, deviations, and two-way door type. Verification: cargo test (625 passed, +2), clippy -D warnings, fmt --check, doc clean, test --all-features clean, wasm32-unknown-unknown check clean.
4.4 KiB
ADR-050: pump_bidi — the two-pump helper, extracted upstream
Status
Accepted — implemented in alkcall 0.6.0 (review 007 Unit 3 / R-03). Pins alknet ADR-078's two-pump contract in one place.
Context
alknet ADR-078 defined the two-pump data-plane contract for forwarding
channels (one pump per direction; each pump shuts the opposite sink
down on completion — try_join! alone deadlocks) and deferred helper
extraction until a second two-pump consumer existed and the shapes
converged. Review 007 (R-03) supplied both halves of that test from
the alktunnels UDP POC:
- Producer side: the POC's
pump_halves— twotokio::io::copypumps over split channel-vs-substrate halves, each shutting down the opposite sink on completion, joined. - Consumer side:
TunnelSession::take_halves+ the assembly layer's copy — the same shape modulo channel side.
alktty's channels session already implements the shape (its
drive_session_pre_negotiated awaited inline); alkhttp's ferry does
not pump. The shapes converged; the review asked this sweep to either
extract the helper or record the decision not to — an un-extracted
helper would surface during alktunnels Phase 1 as the same
fix→publish→update treadmill for a purely additive change.
Decision
Extract the helper as channels::pump_bidi:
pub async fn pump_bidi<C, R, W>(channel: C, peer_read: R, mut peer_write: W) -> (u64, u64)
where
C: AsyncRead + AsyncWrite + Unpin,
R: AsyncRead + Unpin,
W: AsyncWrite + Unpin,
Two pumps, joined: channel → peer_write and peer_read → channel.
On each pump's completion the opposite sink is shut down
(shutdown-on-completion — the half-close the contract specifies).
Returns the two copy counts for observability.
Three deliberate deviations from the review's sketch, all semantics-first:
- Return
(u64, u64), notio::Result<(u64, u64)>. There is no meaningfulErrstate: both pumps swallow copy errors by contract — a mid-stream error is indistinguishable from an abrupt close (no error channel exists mid-stream, per ADR-049 §6's pinned EOF-shaped semantics), and shutdown-of-the-opposite-sink runs either way. A returnedErrwould be dead code; the counts (withunwrap_or(0)on error) are the observability. - Peer side takes split halves (
peer_read: R,peer_write: W), not oneAsyncRead + AsyncWritevalue. The dominant producer shape is the establisher's dial result:TcpStream::into_splithalves (tunnels), pty pair halves (TTY). Taking halves matches the POC (pump_halves) and avoids forcing consumers to re-join halves just to satisfy a bound. The channel side stays a single value — that is what the pump handler'saccept_biyields. - Not
async fnoverAsyncWriteExtbounds on both sides. The channel side needstokio::io::split(one value, two pumps), so its bound isAsyncRead + AsyncWrite + Unpin; theAsyncWriteExtbound the sketch had on every parameter is the extension trait only where shutdown is called.
alktty's three-pump session does not fit the helper (the exit future is a third signal) and stays as-is — the helper serves the two-pump shape, exactly as the review scoped.
Compatibility
Purely additive: a new pub fn in channels::pump, re-exported in the
module docs; no existing surface changes. Consumers may adopt it at
their own pace (alktunnels Phase 1 gets it for free; alktty's
channels session can migrate later if it wants; the POC's shape is
now canonical).
Door type
Two-way. A helper function is trivially removable or re-shapable; no wire surface, no trait, no state.
Verification gate
The POC's two-pump semantics reproduced through the helper, as unit
tests in src/channels/pump.rs: data flows both directions with
exact copy counts; EOF from one side completes the other's
shutdown-on-completion (the far end observes clean EOF, not an
error); a dead source is EOF-shaped teardown, not an Err.
References
- Review 007 R-03 (the extraction ask + the convergence evidence)
- alknet ADR-078 (the two-pump contract; shutdown-on-completion)
- alktunnels
poc-summary.md§Issues Surfaced #4 (the producer-side shape;pump_halvesin the POC's producer.rs) - alktty
src/channels.rs(the channels session shape; three-pump carve-out) - ADR-049 amendment 2 (R-02 — the lifetime contract the helper's
callers must respect: await
pump_bidi(..).awaitinline inside the handler task)