feat: consumer-session — TunnelSession (open/adopt, data planes, teardown)

- TunnelSession (src/consumer.rs): open (ChannelClient::open_channel)
  + adopt (ChannelManager::adopt_channel) construction, substrate-
  shaped data planes (raw halves for stream; ADR-003 codec + pending
  frame queue for UDP), pump_against (session-owned pump_bidi handle),
  and teardown ownership per ADR-005: close (abort + ungraceful reap),
  join (await + reap + copy counts, pump-less (0,0,reaped)), Drop
  (abort + sync reap, never leaks). No Clone (compile_fail doc-test).
- open_reverse_channel on the CallConnection (the reverse path's
  two-step: call then adopt — the POC's honest shape).
- Error surfaces (src/error.rs): TunnelOpenError (typed ADR-049 §4
  surface via open_ref/establishment_reason), TunnelIoError (wrong-
  substrate, TruncatedDatagram fail-loud, ChannelTaken), ReverseOpenError.
- Tests: tests/consumer_session.rs (12) over two harness topologies —
  forward (wire_forward/ForwardTopology: consumer connect-side pure
  client, producer serving via adapter) and reverse (the POC shape);
  teardown matrix, W4 half-close, out-of-band close + self-reap (W3),
  F-2 empty datagram, AdoptFailed-on-collision (bogus-id adopt parks
  by design — documented), concurrent sessions. Harness: all_substrate_dial.
- Adopt takes substrate (task-note deviation: the data plane shape
  rode the open call; cannot be inferred from the ID).

Verified: cargo test green (38), clippy -D warnings (native + wasm32),
fmt clean, cargo check --target wasm32-unknown-unknown passes.
This commit is contained in:
2026-09-08 09:33:12 +00:00
parent 2b35da4395
commit fb2389bd62
8 changed files with 1431 additions and 18 deletions
+66 -2
View File
@@ -1,7 +1,7 @@
---
id: tunnels/consumer-session
name: Consumer half — TunnelSession (open/adopt, data planes, teardown)
status: pending
status: completed
depends_on: [tunnels/params, tunnels/wire-codec, tunnels/producer-open-op]
scope: broad
risk: high
@@ -99,6 +99,70 @@ matrix (close/join/Drop paths — no leaked channel entries asserted via
> Agent fills during implementation.
- **`adopt` takes `substrate: Substrate`** (a deviation from the task
sketch's `adopt(manager, channel_id, alpn)`): the substrate shapes
the data plane (raw halves vs the UDP codec) and cannot be inferred
from the channel ID. The ALPN is observability-only (alkcall
manager semantics); the substrate rode the open call that produced
the ID — passing it to `adopt` keeps the two calls consistent.
- **`pump_against` takes the whole accepted stream** (`S: AsyncRead +
AsyncWrite`) and splits internally — `tokio::io::split` at the
pump boundary, matching the POC's `pump_against(accepted)`. Returns
the session (builder-style); the pump handle is session-owned and
private (close/join/Drop are the only handles needed).
- **`take_halves(self)` returns `TakenHalves { session, read, write
}`** (not bare halves): the session must stay alive as the
channel-entry owner (reap via `close`/`join`/`Drop`); returning
bare halves would strand the entry. `join` on the held session
completes immediately (`(0, 0, reaped)` — pump-less).
- **`open_reverse_channel`'s hub_call is `&CallConnection`** (not
`&Arc<CallConnection>`) — borrowing is the honest shape; callers
hold the Arc.
- **Fields are `Option` + idempotent `Drop`:** a `Drop`-impl type
cannot be destructured, so `take_halves`/`pump_against`/`close`/
`join` take fields out via `.take()`; an emptied session drops as
a no-op. Teardown is idempotent (`teardown_channel` on a reaped
entry is `Err(UnknownChannel)`, ignored).
- **Bogus-ID adopt succeeds by design** (the manager parks early
arrivals for any not-yet-seen ID — the adoption-race cover,
alkcall ADR-047 §5); the honest `AdoptFailed` probe is the
ID-collision path (same ID adopted twice), pinned in the tests.
- **`recv_datagram` keeps a pending queue**: one chunk may batch
several frames; `DatagramReader::feed` decodes all completions per
feed, so frames beyond the first queue for subsequent
`recv_datagram` calls (dropped frames were a bug the tests caught).
- **Harness gained `wire_forward`** (`ForwardTopology`): the forward
path's consumer is the connect side (`from_connection`, pure
consumer) and the producer accepts via the adapter; the accept-side
transport-identity posture attaches the dialer's identity in the
install hook (modeled; the transport resolves it out-of-band for
real). `all_substrate_dial` = stream echo + framed UDP echo.
## Summary
> Agent fills this on completion.
> Agent fills this on completion.
Implemented `TunnelSession` (src/consumer.rs, ~415 lines) per
consumer.md + ADR-005: both construction paths (`open` via
`ChannelClient::open_channel`, `adopt` via `ChannelManager::
adopt_channel` + `open_reverse_channel` on the `CallConnection`),
the substrate-shaped data planes (raw halves for stream; the ADR-003
codec with a pending frame queue for UDP), pump ownership
(`pump_against` spawns `pump_bidi` and holds the handle), and the
full teardown matrix — `close` (abort + ungraceful reap), `join`
(await + reap + copy counts; pump-less `(0, 0, reaped)`), `Drop`
(abort + sync reap, never leaks). No `Clone` (compile_fail doc-test).
Tests: `tests/consumer_session.rs` — 12 integration tests over the
two harness topologies: forward round-trip (session halves), take-
halves + pump-less join, datagram round-trip incl. the F-2 empty
datagram, wrong-substrate typed errors, oversize-at-frame-time,
reverse open+adopt+pump round-trip, W4 half-close, out-of-band close
+ self-reap (the W3 split), Drop-no-leak (pump + pump-less),
AdoptFailed-on-collision, failed-open-typed (no phantom session),
concurrent reverse sessions. Harness additions: `wire_forward`
(`ForwardTopology`) + `all_substrate_dial`/`framed_udp_echo_dial`.
Verified: `cargo test` green (38 total), clippy `-D warnings` clean
(native + wasm32), fmt clean, wasm32 check passes (default crate
stays wasm-clean).