docs(research): add alknet-channels POC summary — 3 targets validated, core refactor notes
This commit is contained in:
1 parent
5e07203b86
commit
6f1663a391
1 file changed
+463
@@ -0,0 +1,463 @@
|
||||
---
|
||||
status: complete
|
||||
last_updated: 2026-07-12
|
||||
---
|
||||
|
||||
# alknet-channels: POC Research Summary
|
||||
|
||||
**Status:** Research complete — all three high-leverage unknowns validated. The
|
||||
approach is viable; the remaining unknowns are spec-scope, not feasibility.
|
||||
**Date:** 2026-07-12
|
||||
**Scope:** Captures what the POC proved about the 9-byte chunk format, N-channel
|
||||
demux/mux, per-channel `Connection` presentation, and the tunnel handler — and
|
||||
what it surfaced for the coming `alknet-channels` crate spec and the light
|
||||
`alknet-core` refactor that should land alongside it.
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
A POC (`alknet-channels-poc`, `/workspace/alknet-channels-poc`) validated the
|
||||
three highest-leverage unknowns of the channels layer over a
|
||||
`tokio::io::duplex` stand-in transport:
|
||||
|
||||
1. **Chunk format + N-channel demux/mux** — the 9-byte format
|
||||
(`[channel_id:u32 be][stream_type:u8][length:u32 be][payload]`) is a clean
|
||||
generalization of TTY's 5-byte format. The sync core (`parse_header`/
|
||||
`write_header`) is pure, WASM-compatible by construction. The mpsc-bridged
|
||||
async shell scales to N concurrent channels with per-channel order
|
||||
preservation and cross-channel isolation, and bounded-buffer backpressure
|
||||
(DP-5 option c) works for the simple case.
|
||||
2. **Per-channel `Connection` presentation** — `Connection::from_stream` (from
|
||||
the transport-generalization work) is sufficient to present each channel as
|
||||
a `Connection` a `ProtocolHandler` can drive unchanged. A minimal echo
|
||||
handler runs through the full demux→Connection→handler→mux path with zero
|
||||
channels-layer awareness. The yield-once `accept_bi` contract composes for
|
||||
handlers that loop `accept_bi` (like `TtyAdapter`).
|
||||
3. **Tunnel handler** — the bidirectional pump pattern (same shape as TTY's
|
||||
`pump_session`, two pumps instead of three) works as a generic port proxy
|
||||
through channels. A `TunnelHandler` of ~15 lines proxies a channel to a
|
||||
local TCP echo server with no chunk-format or channels-layer awareness, and
|
||||
a tunnel channel coexists with an echo channel on one channels connection
|
||||
without interference.
|
||||
|
||||
**28 tests pass** (5 wire codec + 7 mpsc stream adapters + 5 demux + 3 mux + 5
|
||||
echo handler + 3 tunnel handler). Clippy is clean for the POC's own code; the
|
||||
only remaining warnings are upstream in `alknet-core` (`types.rs:500`
|
||||
`Connection::close` has unused `code`/`reason` params — pre-existing, not
|
||||
introduced by this POC).
|
||||
|
||||
**Stretch goal validated:** the sync core (`wire.rs`) compiles cleanly under
|
||||
`wasm32-unknown-unknown`. The "WASM compatibility by construction" claim holds
|
||||
for the pure-byte-manipulation core; the async shell and the `alknet-core`
|
||||
dependency graph are NOT WASM-clean yet (transitive `getrandom`/`rand` deps),
|
||||
which is a Phase 1 concern, not a POC concern.
|
||||
|
||||
The POC depends on `alknet-core` only (path dependency,
|
||||
`default-features = false` — no QUIC needed for the POC), and uses
|
||||
`tokio::io::duplex` as the stand-in transport. It does NOT depend on
|
||||
`alknet-tty`, `alknet-call`, or any other handler crate. The echo and tunnel
|
||||
handlers are POC-local stubs, not the real adapters.
|
||||
|
||||
---
|
||||
|
||||
## The Sync Core / Async Shell Split (Confirmed)
|
||||
|
||||
The POC confirms the split proposed in `poc-plan.md` §"Design Principles":
|
||||
|
||||
- **Sync core (`wire.rs`)**: `parse_header(&[u8; 9]) -> ChunkHeader` and
|
||||
`write_header(channel_id, stream_type, length, &mut [u8; 9])`. Pure
|
||||
functions, no async, no platform deps. Validates `MAX_CHUNK_LEN` (matches
|
||||
TTY's 16 MiB cap, preserving the framing-disambiguation soundness property
|
||||
from ADR-052 §5). Compiles under `wasm32-unknown-unknown`.
|
||||
- **Async shell (`demux.rs` / `mux.rs`)**: `Demux::run` reads 9-byte headers +
|
||||
payloads off the transport via `read_exact`, routes payloads to per-
|
||||
`(channel_id, stream_type)` bounded `mpsc::Sender<Bytes>`. `Mux` frames
|
||||
`Bytes` from per-channel `mpsc::Receiver<Bytes>` back onto the transport.
|
||||
- **Per-channel reassembly (`mpsc_stream.rs`)**: `MpscRecvStream`
|
||||
(`mpsc::Receiver<Bytes>` → `AsyncRead`, ~50 lines) and `MpscSendStream`
|
||||
(`mpsc::Sender<Bytes>` → `AsyncWrite` with backpressure, ~60 lines). These
|
||||
back the `RecvStream`/`SendStream` the handler sees via
|
||||
`RecvStream::from_stream` / `SendStream::from_stream`.
|
||||
|
||||
The split is as clean as the TTY POC's was. The "don't have to async" pattern
|
||||
lives in the demux/mux shell; the core byte manipulation is pure and
|
||||
WASM-portable.
|
||||
|
||||
---
|
||||
|
||||
## POC Target 1: Chunk Format + N-Channel Demux/Mux
|
||||
|
||||
**Question:** Does the 9-byte format decompose and recombine N concurrent
|
||||
streams correctly, with per-channel order preservation and cross-channel
|
||||
isolation?
|
||||
|
||||
**Answer:** Yes. The demux routes chunks to per-`(channel_id, stream_type)`
|
||||
bounded `mpsc::Sender`s; the mux frames `Bytes` from per-channel receivers
|
||||
back onto the transport. Three concurrent channels with interleaved writes
|
||||
round-trip with no cross-channel contamination and per-channel order
|
||||
preserved (`demux_three_concurrent_channels_no_cross_contamination`,
|
||||
`mux_demux_round_trip_three_concurrent_channels`).
|
||||
|
||||
**Lenient unknown-`channel_id` handling (OQ-CH-12):** a chunk with an
|
||||
unallocated `channel_id` (or an unallocated `stream_type` on an allocated
|
||||
channel) is dropped with a debug log and an error counter, and the demux
|
||||
continues (`demux_unknown_channel_drops_lenient`). This matches SSH's
|
||||
behavior and survives transient mis-ordering during teardown. The error
|
||||
counter is exposed via `Demux::stats()` for observability. This is the
|
||||
recommended v1 behavior from the plan; the POC validates it works.
|
||||
|
||||
**Zero-length sentinel (TTY convention carried forward):** a zero-length
|
||||
chunk is delivered as an empty `Bytes`, which `MpscRecvStream` interprets as
|
||||
EOF (`demux_zero_length_sentinel_delivered`, `recv_zero_length_sentinel_is_eof`).
|
||||
This is the same convention as TTY's zero-length stdin/stdout chunks. See
|
||||
§"Issues Surfaced" below for a subtlety about how shutdown propagates this.
|
||||
|
||||
**Chunk-too-large:** a chunk with `length > MAX_CHUNK_LEN` returns
|
||||
`ChunkTooLarge` and does not corrupt the stream
|
||||
(`demux_chunk_too_large_does_not_panic`).
|
||||
|
||||
**Backpressure (DP-5 option c):** each `(channel_id, stream_type)` has an
|
||||
independent bounded `mpsc` buffer. A slow reader on one channel does not block
|
||||
another channel's reads — the demux's per-chunk `route` awaits the matching
|
||||
sender without holding a global lock. The 1 MiB tunnel test
|
||||
(`tunnel_large_payload`) exercises this end-to-end and does not deadlock.
|
||||
|
||||
---
|
||||
|
||||
## POC Target 2: Per-Channel `Connection` Presentation
|
||||
|
||||
**Question:** Is `Connection::from_stream` sufficient to present each channel
|
||||
as a `Connection` a `ProtocolHandler` can drive unchanged?
|
||||
|
||||
**Answer:** Yes. The `EchoHandler` (~5 lines of handler code) runs through the
|
||||
full demux→Connection→handler→mux path with zero channels-layer awareness.
|
||||
It calls `connection.accept_bi().await`, gets a `(SendStream, RecvStream)`
|
||||
pair, and pumps via `tokio::io::copy`. The handler does not know it is inside
|
||||
a channels connection.
|
||||
|
||||
**Wiring (`ChannelEndpoint`):** the POC's local equivalent of the symmetric
|
||||
client/server types (OQ-CH-14). `channel_endpoint()` constructs both sides of
|
||||
a channels connection (server demux/mux + client demux/mux, connected by two
|
||||
`tokio::io::duplex` pipes). `open_echo_channel(channel_id)` allocates a
|
||||
channel on both sides: the server side wraps `MpscSendStream`/`MpscRecvStream`
|
||||
as `Connection::from_stream(..., b"alknet/echo", None)` and spawns
|
||||
`EchoHandler::handle`; the client side returns a `ClientChannel { send, recv }`
|
||||
the test drives. This is the POC-local shape Phase 1 will generalize into
|
||||
`ChannelsAdapter::handle` (server) and `ChannelClient` (client).
|
||||
|
||||
**ALPN threading:** `handler_sees_alpn` validates that `Connection::from_stream`'s
|
||||
`alpn` parameter threads through correctly — the handler's
|
||||
`connection.remote_alpn()` returns the ALPN the channel was opened with.
|
||||
|
||||
**Yield-once contract:** `handler_loops_accept_bi_gets_one_session` validates
|
||||
that a handler that loops `accept_bi` (like `TtyAdapter`) gets exactly one
|
||||
session per channel, then `ConnectionClosed` on the second call. The
|
||||
yield-once `Connection::from_stream` path composes correctly for looping
|
||||
handlers.
|
||||
|
||||
---
|
||||
|
||||
## POC Target 3: Tunnel Handler
|
||||
|
||||
**Question:** Does the bidirectional pump pattern (same shape as TTY's
|
||||
`pump_session`, two pumps instead of three) work as a generic port proxy
|
||||
through channels?
|
||||
|
||||
**Answer:** Yes. The `TunnelHandler` (~15 lines of handler code) opens a
|
||||
`TcpStream` to a fixed target and runs two `tokio::io::copy` pumps:
|
||||
`recv → tcp_write` (client → target) and `tcp_read → send` (target → client).
|
||||
The handler contains zero chunk-format or channels-layer awareness — it
|
||||
only sees `Connection`/`SendStream`/`RecvStream`.
|
||||
|
||||
**Shutdown-on-completion (subtlety):** the naive `tokio::try_join!(c2t, t2c)`
|
||||
deadlocks — `t2c` waits forever for the TCP echo server to close, which only
|
||||
happens once the handler closes `tcp_write`, which only happens after
|
||||
`try_join` returns. The POC fixes this by shutting down the opposite sink
|
||||
when each pump completes: `c2t` shuts down `tcp_write` on EOF, `t2c` shuts
|
||||
down `send` on EOF. This is the `pump_session` shape with an explicit
|
||||
shutdown-on-completion step that the TTY adapter doesn't need (because TTY's
|
||||
three pumps coordinate via the exit-code future). The tunnel's two-pump
|
||||
shape needs it. See §"Issues Surfaced" below.
|
||||
|
||||
**Tests:**
|
||||
- `tunnel_echo_round_trip` — 1 KiB round-trips through the tunnel to a TCP
|
||||
echo server.
|
||||
- `tunnel_large_payload` — 1 MiB round-trips without corruption or deadlock,
|
||||
exercising the bounded-buffer backpressure path (the tunnel handler's
|
||||
`AsyncRead` side and the channels demux must not deadlock when the TCP
|
||||
echo server is slower than the channel writer).
|
||||
- `tunnel_concurrent_with_echo_channel` — one tunnel channel and one echo
|
||||
channel on the same channels connection, both running concurrently, neither
|
||||
blocks the other. The multiplexing is transparent.
|
||||
|
||||
---
|
||||
|
||||
## Issues Surfaced (For the Spec and the Core Refactor)
|
||||
|
||||
These are the things the POC ran into that should be addressed in Phase 1 or
|
||||
in the light `alknet-core` refactor landing alongside it. They are grouped by
|
||||
where the fix likely lives.
|
||||
|
||||
### In `alknet-core` (the refactor)
|
||||
|
||||
#### 1. `Connection::from_stream` is yield-once — `BidiStreamSource` would be cleaner (OQ-CH-13, confirmed +EV)
|
||||
|
||||
The POC validates the yield-once path is sufficient, as the plan predicted.
|
||||
But wiring `ChannelEndpoint::open_echo_channel` made the awkwardness concrete:
|
||||
each channel constructs a fresh `Connection::from_stream` that yields one bidi
|
||||
stream, and the endpoint holds a bag of these connections (one per channel)
|
||||
rather than a single `ChannelConnection` that yields N streams. The
|
||||
`BidiStreamSource` trait proposed in OQ-CH-13 would make `ChannelConnection`
|
||||
a first-class peer of QUIC (many bidi streams) instead of a bag of yield-once
|
||||
connections:
|
||||
|
||||
```rust
|
||||
trait BidiStreamSource: Send + Sync {
|
||||
async fn accept_bi(&self) -> Result<(SendStream, RecvStream), StreamError>;
|
||||
async fn open_bi(&self) -> Result<(SendStream, RecvStream), StreamError>;
|
||||
}
|
||||
```
|
||||
|
||||
with `Connection` holding `Box<dyn BidiStreamSource>`, and QUIC/Iroh/Stream/
|
||||
Channels all implementing it. The POC confirms this is **additive** (existing
|
||||
callers keep working via a `from_stream`-backed default impl) and would
|
||||
clean up both the channels layer and the client-side endpoint (OQ-CH-14).
|
||||
**Recommendation: do this refactor.** It is not a major/pita break and it
|
||||
makes downstream substantially cleaner.
|
||||
|
||||
#### 2. `Connection::close` has unused params (`code`, `reason`) for the stream backend
|
||||
|
||||
`crates/alknet-core/src/types.rs:500` — the `Stream` backend of `Connection::close`
|
||||
takes `code: u32, reason: &str` and does nothing with them (clippy flags both
|
||||
as unused). The stream backend just drops the inner stream. This is a leak of
|
||||
the QUIC-centric `close(code, reason)` shape into a backend that has no such
|
||||
concept. The `BidiStreamSource` refactor is the natural place to give the
|
||||
stream backend a `close()` that doesn't take QUIC-shaped args, or to make
|
||||
the args `Option`-al / ignored-with-a-warning. Low priority but worth fixing
|
||||
while the trait is open.
|
||||
|
||||
#### 3. `AuthContext` construction is verbose for tests and POCs
|
||||
|
||||
Every POC handler test constructs an `AuthContext` literal with four `None`
|
||||
fields and a hardcoded ALPN. The real assembly layer gets one from the
|
||||
endpoint; POCs and tests repeat the boilerplate. A
|
||||
`AuthContext::test(alpn: &[u8])` or `AuthContext::anonymous(alpn)` helper in
|
||||
`alknet-core` (behind a `test-utils` feature, or just a `pub fn` if we don't
|
||||
mind it in the public API) would remove this. Small, but it recurs in every
|
||||
handler POC.
|
||||
|
||||
### In `alknet-channels` (the spec / Phase 1)
|
||||
|
||||
#### 4. `MpscSendStream::poll_shutdown` must emit a zero-length sentinel
|
||||
|
||||
The POC's `MpscSendStream::poll_shutdown` sends an empty `Bytes` (the EOF
|
||||
sentinel) before dropping the sender. Without this, the demux never sees EOF
|
||||
on the channel's `stream_type`, and `tokio::io::copy` in the handler never
|
||||
completes — the test hangs. The TTY crate's `pump_session` emits the
|
||||
zero-length stdout sentinel explicitly via `Chunk::stdout(Bytes::new())`;
|
||||
the channels layer's per-channel write pump does NOT forward a sentinel on
|
||||
sender-drop, so the send adapter must do it.
|
||||
|
||||
**Implication for Phase 1:** the `ChannelConnection` write-half contract
|
||||
must specify that `AsyncWrite::shutdown` (or the `BidiStreamSource`
|
||||
equivalent) emits a zero-length sentinel so the peer sees EOF. This is a
|
||||
wire-level invariant, not just an implementation detail — both sides must
|
||||
agree, or channels will hang on clean shutdown. The TTY crate already
|
||||
follows this convention; the channels crate should codify it.
|
||||
|
||||
#### 5. The mux needs dynamic registration (handle/runner split)
|
||||
|
||||
The plan's `Mux::run(self, transport)` shape (consume the mux, run pumps for
|
||||
pre-registered channels) does not compose with the "open a channel after the
|
||||
run loop started" API that `ChannelEndpoint::open_echo_channel` needs. The
|
||||
POC split `Mux` into `MuxHandle` (clone-able, `register(channel_id,
|
||||
stream_type) -> Sender<Bytes>` at any time) and `MuxRunner` (owns the
|
||||
transport, `select!`s on new-pump registrations). This is the shape Phase 1
|
||||
should adopt — the alternative (pre-registering all channels before run)
|
||||
doesn't match the dynamic `channel/open` model.
|
||||
|
||||
The split adds one `mpsc::UnboundedSender` + `Arc<Mutex<HashMap>>` per mux,
|
||||
which is cheap. The runner's `select!` loop exits when all `MuxHandle` clones
|
||||
drop (the `new_pumps` sender closes), which is the natural shutdown signal.
|
||||
|
||||
#### 6. The demux must drop all channel senders on transport EOF
|
||||
|
||||
`Demux::run` clears its `channels` map on exit (transport closed) so every
|
||||
handler's `MpscRecvStream` sees EOF even without an explicit zero-length
|
||||
sentinel arriving on the wire. Without this, `read_to_end` / `tokio::io::copy`
|
||||
in handlers hangs forever waiting for a sender that never drops because the
|
||||
demux task is holding the map. This is a teardown invariant: transport close
|
||||
→ all channel senders drop → all handlers see EOF → all handler tasks exit.
|
||||
Phase 1 should specify this as part of the `ChannelsAdapter::handle` contract.
|
||||
|
||||
#### 7. The tunnel's two-pump shape needs explicit shutdown-on-completion
|
||||
|
||||
`tokio::try_join!(c2t, t2c)` deadlocks for the tunnel: each pump waits for the
|
||||
other's EOF, which only comes once the *opposite* pump completes and shuts
|
||||
down its sink. The fix (shut down the peer's sink when one pump completes) is
|
||||
small but easy to get wrong. The TTY adapter doesn't have this problem
|
||||
because its three pumps coordinate via the `exit_code` future, which is a
|
||||
third signal. The tunnel has no such signal — it's purely pump-driven.
|
||||
|
||||
**Implication for Phase 1:** the tunnel handler (and any future handler
|
||||
with a pump-driven two-direction shape) must shut down the opposite sink on
|
||||
pump completion, not just `try_join` the two pumps. This is worth a
|
||||
documented pattern in the channels spec, or — if `BidiStreamSource` lands —
|
||||
a helper in `alknet-core` that encapsulates the "two-pump with
|
||||
shutdown-on-completion" shape so handlers don't reimplement it. The TTY
|
||||
crate's `pump_session` is the three-pump version; the two-pump version is
|
||||
the tunnel's shape and will recur (e.g. an SSH `direct-tcpip` channel is
|
||||
the same two-pump shape).
|
||||
|
||||
### Cosmetic / clippy
|
||||
|
||||
#### 8. "Very complex type" for the `reserve_owned` future
|
||||
|
||||
`MpscSendStream` stores an in-flight `reserve_owned()` future for
|
||||
backpressure. The concrete future type is anonymous (async fn), so it's
|
||||
boxed as `Pin<Box<dyn Future<Output = Result<OwnedPermit<Bytes>, SendError<()>>> + Send>>`.
|
||||
Clippy flags this as "very complex type." The POC factored it into a
|
||||
`type ReserveFut = ...` alias, which silences clippy but doesn't reduce the
|
||||
complexity. The clean fix is `tokio_util::io::PollSender`-style helper that
|
||||
wraps an `mpsc::Sender<T>` as `AsyncWrite` for `T: Into<Bytes>` — but that
|
||||
requires `tokio-util` as a dep and a small adapter. Phase 1 should consider
|
||||
whether to extract this adapter (it's ~50 lines and will recur in any
|
||||
mpsc-backed stream) or keep it POC-local. Low priority.
|
||||
|
||||
---
|
||||
|
||||
## What the POC Does NOT Validate
|
||||
|
||||
Following the docker POC summary's pattern:
|
||||
|
||||
1. **The call protocol.** `channel/open`, `channel/close`, `channel/control`,
|
||||
`channel/resources` are call-protocol operations. The POC uses a POC-local
|
||||
channel-allocation mechanism (`Demux::allocate_channel` /
|
||||
`MuxHandle::register`), not the real `OperationRegistry` path. The call
|
||||
protocol is already in production; its integration with channels is Phase 1's
|
||||
concern, not a POC unknown.
|
||||
2. **Real transport.** `tokio::io::duplex` stands in for QUIC/TCP/WebTransport.
|
||||
The transport-generalization work already validated `Connection::from_stream`
|
||||
over real transports; the POC reuses that.
|
||||
3. **ACL.** The call protocol's `AccessControl::check` gates `channel/open`
|
||||
in the real system; the POC has no auth.
|
||||
4. **Real adapters.** `EchoHandler` and `TunnelHandler` are POC-local stubs.
|
||||
The real `TtyAdapter` / `SshAdapter` / `DockerTtyBackend` are unchanged and
|
||||
integrate in Phase 1.
|
||||
5. **Recursive composition.** `alknet/channels` inside `alknet/channels` is a
|
||||
natural consequence of the `Connection` abstraction but is not a POC goal.
|
||||
6. **Hub relay.** The stretch goal (two `Demux`/`Mux` pairs bridged by a byte
|
||||
pump with `channel_id` remapping, OQ-CH-11) was not built — the three primary
|
||||
targets were the higher-leverage unknowns. The POC's demux/mux shape would
|
||||
support it, but the relay sketch is left for Phase 1.
|
||||
7. **WASM test harness.** The sync core compiles under
|
||||
`wasm32-unknown-unknown`; running the same tests in a WASM test harness
|
||||
(`wasm-bindgen-test`) was the secondary stretch and was not set up. The
|
||||
compile check validates the "no platform dependencies" claim for the core;
|
||||
the async shell and `alknet-core` dep graph are not WASM-clean yet (transitive
|
||||
`getrandom`/`rand`), which is a Phase 1 concern.
|
||||
|
||||
---
|
||||
|
||||
## Test Coverage
|
||||
|
||||
```
|
||||
running 28 tests
|
||||
test wire::tests::parse_max_length ... ok
|
||||
test wire::tests::parse_preserves_channel_id_max ... ok
|
||||
test wire::tests::parse_round_trip ... ok
|
||||
test wire::tests::parse_too_large ... ok
|
||||
test wire::tests::parse_zero_length ... ok
|
||||
test mpsc_stream::tests::recv_dropped_sender_is_eof ... ok
|
||||
test mpsc_stream::tests::recv_partial_then_leftover ... ok
|
||||
test mpsc_stream::tests::recv_reads_bytes_in_order ... ok
|
||||
test mpsc_stream::tests::recv_zero_length_sentinel_is_eof ... ok
|
||||
test mpsc_stream::tests::send_after_close_errors_broken_pipe ... ok
|
||||
test mpsc_stream::tests::send_backpressure_pends_then_completes ... ok
|
||||
test mpsc_stream::tests::send_writes_then_shutdown_closes ... ok
|
||||
test demux::tests::demux_chunk_too_large_does_not_panic ... ok
|
||||
test demux::tests::demux_routes_to_allocated_channel ... ok
|
||||
test demux::tests::demux_three_concurrent_channels_no_cross_contamination ... ok
|
||||
test demux::tests::demux_unknown_channel_drops_lenient ... ok
|
||||
test demux::tests::demux_zero_length_sentinel_delivered ... ok
|
||||
test mux::tests::mux_demux_round_trip_three_concurrent_channels ... ok
|
||||
test mux::tests::mux_frames_single_chunk_round_trip ... ok
|
||||
test mux::tests::mux_zero_length_sentinel_round_trips ... ok
|
||||
test echo_handler::tests::echo_single_channel ... ok
|
||||
test echo_handler::tests::echo_single_channel_1kib ... ok
|
||||
test echo_handler::tests::echo_three_concurrent_channels ... ok
|
||||
test echo_handler::tests::handler_loops_accept_bi_gets_one_session ... ok
|
||||
test echo_handler::tests::handler_sees_alpn ... ok
|
||||
test tunnel_handler::tests::tunnel_concurrent_with_echo_channel ... ok
|
||||
test tunnel_handler::tests::tunnel_echo_round_trip ... ok
|
||||
test tunnel_handler::tests::tunnel_large_payload ... ok
|
||||
|
||||
test result: ok. 28 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
|
||||
```
|
||||
|
||||
The 1 MiB `tunnel_large_payload` test exercises the bounded-buffer
|
||||
backpressure path end-to-end (channel writer faster than TCP echo server,
|
||||
no deadlock). The three `echo_*` tests validate the full
|
||||
demux→Connection→handler→mux path. The five `wire::*` tests are pure sync
|
||||
core, WASM-portable.
|
||||
|
||||
---
|
||||
|
||||
## POC Structure
|
||||
|
||||
```
|
||||
alknet-channels-poc/
|
||||
Cargo.toml — depends on alknet-core (path, no default features), tokio, tokio-util, bytes, async-trait, thiserror, tracing
|
||||
src/
|
||||
lib.rs — module docs; the three-step POC overview
|
||||
wire.rs — sync core: 9-byte ChunkHeader, parse_header, write_header (WASM-clean)
|
||||
mpsc_stream.rs — MpscRecvStream (AsyncRead), MpscSendStream (AsyncWrite + backpressure + sentinel-on-shutdown)
|
||||
demux.rs — Demux: route chunks to per-(channel,stream_type) bounded mpsc senders; lenient on unknown ids; drop senders on EOF
|
||||
mux.rs — MuxHandle/MuxRunner: dynamic per-channel write pumps; frames Bytes onto the shared transport
|
||||
echo_handler.rs — EchoHandler (ProtocolHandler) + ChannelEndpoint (POC-local client/server wiring) + open_echo_channel
|
||||
tunnel_handler.rs — TunnelHandler (two-pump TcpStream proxy) + tcp echo server test helper
|
||||
```
|
||||
|
||||
No `tests/` directory — tests are inline per-module (`#[cfg(test)] mod tests`),
|
||||
matching the alknet-core / alknet-tty convention (vs. the docker POC's
|
||||
separate `tests/integration.rs`, which matched bollard's style).
|
||||
|
||||
---
|
||||
|
||||
## Key Code-to-Concept Mappings
|
||||
|
||||
| POC concept | alknet-core equivalent | alknet-tty equivalent |
|
||||
|---|---|---|
|
||||
| `wire::parse_header`/`write_header` | — | `wire::ChunkReader::read_chunk` / `ChunkWriter::write_chunk` (5-byte, no `channel_id`) |
|
||||
| `MpscRecvStream` (`AsyncRead`) | backs `RecvStream::from_stream` | the channel-bridge adapter in the TTY POC |
|
||||
| `MpscSendStream` (`AsyncWrite`) | backs `SendStream::from_stream` | `TestStdinSink` in TTY adapter tests |
|
||||
| `Demux` | — | (new — TTY has no per-channel demux; it's single-channel) |
|
||||
| `MuxHandle`/`MuxRunner` | — | (new — TTY has `ChunkWriter` directly, no mux) |
|
||||
| `ChannelEndpoint` | the symmetric client/server types (OQ-CH-14) | — |
|
||||
| `Connection::from_stream(send, recv, alpn, addr)` | `types.rs:405` | `TtyAdapter::handle` receives a `Connection` the same way |
|
||||
| `EchoHandler::handle` | `ProtocolHandler::handle` | `TtyAdapter::handle` (loops `accept_bi`) |
|
||||
| `TunnelHandler::handle` two pumps | — | `pump_session` (three pumps — stdout/stderr + client→backend + exit) |
|
||||
| zero-length sentinel = EOF | — | `wire.rs` §Sentinels (ADR-052) |
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- POC plan: `docs/research/alknet-channels/poc-plan.md` — the detailed
|
||||
de-risk POC plan this summary reports against.
|
||||
- Phase 0 findings: `docs/research/alknet-channels/phase-0-findings.md` — the
|
||||
research doc this POC derisks (OQ-CH-12/13/14 all surfaced there).
|
||||
- TTY wire format: `crates/alknet-tty/src/wire.rs` — the 5-byte
|
||||
`ChunkReader`/`ChunkWriter` this POC generalizes to 9 bytes.
|
||||
- TTY adapter: `crates/alknet-tty/src/adapter.rs` — `TtyAdapter::handle` and
|
||||
`drive_session`/`pump_session` (the per-stream dispatch and three-pump
|
||||
bidirectional pattern the tunnel handler reuses with two pumps).
|
||||
- Core types: `crates/alknet-core/src/types.rs` — `Connection::from_stream`
|
||||
(`:405`), `SendStream::from_stream` (`:267`), `RecvStream::from_stream`
|
||||
(`:289`), `ProtocolHandler` (`:220`) — the integration points the POC
|
||||
validates.
|
||||
- Docker POC summary: `docs/research/alknet-docker/poc-summary.md` — the
|
||||
POC summary this doc mirrors in structure and tone.
|
||||
- TTY POC: `/workspace/alknet-tty-poc/` — the POC convention this POC follows
|
||||
(sibling-of-`@alkdev` location, standalone Cargo crate, inline tests).
|
||||
Reference in new issue
Block a user