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.
105 lines
4.4 KiB
Markdown
105 lines
4.4 KiB
Markdown
# 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` — two `tokio::io::copy`
|
|
pumps 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`:
|
|
|
|
```rust
|
|
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:
|
|
|
|
1. **Return `(u64, u64)`, not `io::Result<(u64, u64)>`.** There is no
|
|
meaningful `Err` state: 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 returned `Err` would be dead code; the counts
|
|
(with `unwrap_or(0)` on error) are the observability.
|
|
2. **Peer side takes split halves (`peer_read: R`, `peer_write: W`),
|
|
not one `AsyncRead + AsyncWrite` value.** The dominant producer
|
|
shape is the establisher's dial result: `TcpStream::into_split`
|
|
halves (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's `accept_bi` yields.
|
|
3. **Not `async fn` over `AsyncWriteExt` bounds on both sides.** The
|
|
channel side needs `tokio::io::split` (one value, two pumps), so
|
|
its bound is `AsyncRead + AsyncWrite + Unpin`; the `AsyncWriteExt`
|
|
bound 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_halves` in 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(..).await` inline inside the
|
|
handler task) |