refactor(core,http,tty,call): unify stream leaf — BiStream as the handler leaf (Phase 6)
Implement ADR-092 across the workspace: accept_bi/open_bi return BiStream (a concrete AsyncRead + AsyncWrite + Send + Unpin newtype), not the split (SendStream, RecvStream) pair. The join moves into core's BidiStreamSource impls (quinn/iroh via tokio::io::join, single-stream via boxed AsyncReadWrite); handlers receive the joined BiStream and never see the pair. Core (alknet-core/src/types.rs): - Add concrete BiStream struct boxing Box<dyn AsyncReadWrite + Unpin>, with AsyncRead + AsyncWrite impls. from_joined (pub, for downstream crates that produce split halves naturally — channels reassembly, tests) and from_bidi (pub(crate), for Connection::from_bidi) constructors. - Change BidiStreamSource::accept_bi/open_bi return types from (SendStream, RecvStream) to BiStream. Update QuinnBidiStreamSource, IrohBidiStreamSource, StreamBidiStreamSource impls to do the join once. - Collapse SendStream/RecvStream to thin newtypes over Box<dyn Async* + Send + Unpin>. Remove SendStreamKind/RecvStreamKind enums and the quinn/iroh per-call dispatch (the join happens once in the BidiStreamSource impl now). Keep SendStream::from_stream / RecvStream::from_stream per-half boxing for into_sub_streams() (ADR-074) and the future channels reassembly path. - Remove Connection::from_stream (split-pair constructor). Promote Connection::from_bidi to the only public stream constructor (the rule: the split never crosses a crate boundary as part of a constructor). - Update Connection::accept_bi/open_bi to return BiStream. Update from_source_tests and tests modules to use from_bidi and BiStream; add a SinkEmpty test helper (AsyncRead EOF + AsyncWrite discard) for Connection-level-only test connections. alknet-http (server/adapter.rs): - Drop the 44-line QuicStream wrapper — accept_bi returns BiStream which is already AsyncRead + AsyncWrite. HttpAdapter::handle becomes 4 lines. - Drop the 38-line QuicStreamDuplex test helper — tests use a single tokio::io::duplex whose ends are each AsyncRead + AsyncWrite natively. - Remove unused std::io / std::pin::Pin imports. alknet-tty (adapter.rs): - TtyAdapter::handle splits the BiStream from accept_bi via tokio::io::split for drive_session's separate AsyncWrite/AsyncRead args (the stdlib idiom for TcpStream-style duplex streams). alknet-call (protocol/*, client/*): - Dispatcher::run_loop accept_bi site: take BiStream, pass to handle_stream. - Dispatcher::handle_stream signature: take BiStream, split internally via tokio::io::split (was: take SendStream + RecvStream separately). - CallConnection::call_with_payload / subscribe_with_payload / write_envelope: split the BiStream from open_bi via tokio::io::split at the call site. - write_request / read_stream_until_closed: generic over AsyncWrite/AsyncRead (were: concrete SendStream/RecvStream) — accepts the ReadHalf/WriteHalf from tokio::io::split directly. - Add protocol/test_support.rs with sink_empty_connection() (replaces the 5 duplicated stub_connection() fns that used Connection::from_stream). - Update all test stubs (call_client.rs, protocol/connection.rs, protocol/dispatch.rs, protocol/adapter.rs, client/from_call.rs) to use Connection::from_bidi + the shared sink_empty_connection() helper. - Test handle_stream call sites: build BiStream::from_joined(recv, send) from the existing BufReader<Cursor> + duplex pair. Workspace test status: all 9 crates pass (116 + 307 + 18 + 3 + 17 + 301 + 34 + 61 + 23 + 5 + 6 + 8 + 82 + 4 + 3 + 6 + 12 + 1 = 1007 tests pass). One pre-existing failure remains in alknet-http (adapters::to_mcp::tests::search_returns_access_control_filtered_ops_excluding_subscriptions — handler kind mismatch, unrelated to Phase 6, fails on develop baseline).
This commit is contained in:
1 parent
249370345f
commit
b60a5844ba
10 files changed
+412
-403
No files matched your search
@@ -8,9 +8,7 @@
|
||||
//! from `gateway_routes`; `/openapi.json` serves the `to_openapi` projection
|
||||
//! of the registry.
|
||||
|
||||
use std::io;
|
||||
use std::path::PathBuf;
|
||||
use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
|
||||
use async_trait::async_trait;
|
||||
@@ -229,12 +227,14 @@ impl ProtocolHandler for HttpAdapter {
|
||||
let _ = connection.set_identity(identity);
|
||||
}
|
||||
|
||||
let (send, recv) = connection
|
||||
// `accept_bi` returns a `BiStream` (ADR-092) — already
|
||||
// `AsyncRead + AsyncWrite + Send + Unpin`. No wrapper needed; pass
|
||||
// it directly to `serve_io` via `TokioIo::new`.
|
||||
let stream = connection
|
||||
.accept_bi()
|
||||
.await
|
||||
.map_err(stream_error_to_handler)?;
|
||||
let io = QuicStream::new(send, recv);
|
||||
self.serve_io(io).await
|
||||
self.serve_io(stream).await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -268,51 +268,6 @@ fn stream_error_to_handler(e: StreamError) -> HandlerError {
|
||||
HandlerError::from(e)
|
||||
}
|
||||
|
||||
struct QuicStream {
|
||||
send: alknet_core::types::SendStream,
|
||||
recv: alknet_core::types::RecvStream,
|
||||
}
|
||||
|
||||
impl QuicStream {
|
||||
fn new(send: alknet_core::types::SendStream, recv: alknet_core::types::RecvStream) -> Self {
|
||||
Self { send, recv }
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncRead for QuicStream {
|
||||
fn poll_read(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
buf: &mut tokio::io::ReadBuf<'_>,
|
||||
) -> std::task::Poll<io::Result<()>> {
|
||||
Pin::new(&mut self.recv).poll_read(cx, buf)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncWrite for QuicStream {
|
||||
fn poll_write(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
buf: &[u8],
|
||||
) -> std::task::Poll<io::Result<usize>> {
|
||||
Pin::new(&mut self.send).poll_write(cx, buf)
|
||||
}
|
||||
|
||||
fn poll_flush(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
) -> std::task::Poll<io::Result<()>> {
|
||||
Pin::new(&mut self.send).poll_flush(cx)
|
||||
}
|
||||
|
||||
fn poll_shutdown(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
) -> std::task::Poll<io::Result<()>> {
|
||||
Pin::new(&mut self.send).poll_shutdown(cx)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -418,29 +373,26 @@ mod tests {
|
||||
async fn send_request_and_read_response(
|
||||
request: &[u8],
|
||||
) -> (String, tokio::task::JoinHandle<()>) {
|
||||
let (mut client_send, server_recv) = duplex(8 * 1024);
|
||||
let (server_send, mut client_recv) = duplex(8 * 1024);
|
||||
let server_io = QuicStreamDuplex {
|
||||
read: server_recv,
|
||||
write: server_send,
|
||||
};
|
||||
// One duplex: `server_io` is the server's end (passed to `serve_io`);
|
||||
// `client_io` is the client's end (writes requests, reads responses).
|
||||
// `tokio::io::duplex` yields two `DuplexStream`s, each
|
||||
// `AsyncRead + AsyncWrite + Send + Unpin` — the same bounds `BiStream`
|
||||
// exposes (ADR-092), so no wrapper is needed.
|
||||
let (server_io, mut client_io) = duplex(8 * 1024);
|
||||
|
||||
let adapter = HttpAdapter::new(provider(), empty_registry());
|
||||
let handle = tokio::spawn(async move {
|
||||
adapter.serve_io(server_io).await.ok();
|
||||
});
|
||||
|
||||
client_send.write_all(request).await.unwrap();
|
||||
client_send.flush().await.unwrap();
|
||||
client_io.write_all(request).await.unwrap();
|
||||
client_io.flush().await.unwrap();
|
||||
|
||||
let mut response = Vec::new();
|
||||
let mut buf = [0u8; 4096];
|
||||
loop {
|
||||
match tokio::time::timeout(
|
||||
std::time::Duration::from_secs(5),
|
||||
client_recv.read(&mut buf),
|
||||
)
|
||||
.await
|
||||
match tokio::time::timeout(std::time::Duration::from_secs(5), client_io.read(&mut buf))
|
||||
.await
|
||||
{
|
||||
Ok(Ok(0)) => break,
|
||||
Ok(Ok(n)) => response.extend_from_slice(&buf[..n]),
|
||||
@@ -453,45 +405,6 @@ mod tests {
|
||||
(response_str, handle)
|
||||
}
|
||||
|
||||
struct QuicStreamDuplex {
|
||||
read: tokio::io::DuplexStream,
|
||||
write: tokio::io::DuplexStream,
|
||||
}
|
||||
|
||||
impl AsyncRead for QuicStreamDuplex {
|
||||
fn poll_read(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
buf: &mut tokio::io::ReadBuf<'_>,
|
||||
) -> std::task::Poll<io::Result<()>> {
|
||||
Pin::new(&mut self.read).poll_read(cx, buf)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncWrite for QuicStreamDuplex {
|
||||
fn poll_write(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
buf: &[u8],
|
||||
) -> std::task::Poll<io::Result<usize>> {
|
||||
Pin::new(&mut self.write).poll_write(cx, buf)
|
||||
}
|
||||
|
||||
fn poll_flush(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
) -> std::task::Poll<io::Result<()>> {
|
||||
Pin::new(&mut self.write).poll_flush(cx)
|
||||
}
|
||||
|
||||
fn poll_shutdown(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
) -> std::task::Poll<io::Result<()>> {
|
||||
Pin::new(&mut self.write).poll_shutdown(cx)
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn handle_serves_http_request_over_mock_quic_stream() {
|
||||
let request = b"GET /healthz HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n";
|
||||
@@ -509,29 +422,21 @@ mod tests {
|
||||
let extra = Router::new().route("/v1/foo", get(|| async { (StatusCode::OK, "foo-body") }));
|
||||
let adapter = HttpAdapter::new(provider(), empty_registry()).with_extra_routes(extra);
|
||||
|
||||
let (mut client_send, server_recv) = duplex(8 * 1024);
|
||||
let (server_send, mut client_recv) = duplex(8 * 1024);
|
||||
let server_io = QuicStreamDuplex {
|
||||
read: server_recv,
|
||||
write: server_send,
|
||||
};
|
||||
let (server_io, mut client_io) = duplex(8 * 1024);
|
||||
|
||||
let handle = tokio::spawn(async move {
|
||||
adapter.serve_io(server_io).await.ok();
|
||||
});
|
||||
|
||||
let request = b"GET /v1/foo HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n";
|
||||
client_send.write_all(request).await.unwrap();
|
||||
client_send.flush().await.unwrap();
|
||||
client_io.write_all(request).await.unwrap();
|
||||
client_io.flush().await.unwrap();
|
||||
|
||||
let mut response = Vec::new();
|
||||
let mut buf = [0u8; 4096];
|
||||
loop {
|
||||
match tokio::time::timeout(
|
||||
std::time::Duration::from_secs(5),
|
||||
client_recv.read(&mut buf),
|
||||
)
|
||||
.await
|
||||
match tokio::time::timeout(std::time::Duration::from_secs(5), client_io.read(&mut buf))
|
||||
.await
|
||||
{
|
||||
Ok(Ok(0)) => break,
|
||||
Ok(Ok(n)) => response.extend_from_slice(&buf[..n]),
|
||||
@@ -556,29 +461,21 @@ mod tests {
|
||||
);
|
||||
let adapter = HttpAdapter::new(provider(), empty_registry()).with_extra_routes(extra);
|
||||
|
||||
let (mut client_send, server_recv) = duplex(8 * 1024);
|
||||
let (server_send, mut client_recv) = duplex(8 * 1024);
|
||||
let server_io = QuicStreamDuplex {
|
||||
read: server_recv,
|
||||
write: server_send,
|
||||
};
|
||||
let (server_io, mut client_io) = duplex(8 * 1024);
|
||||
|
||||
let handle = tokio::spawn(async move {
|
||||
adapter.serve_io(server_io).await.ok();
|
||||
});
|
||||
|
||||
let request = b"GET /healthz HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n";
|
||||
client_send.write_all(request).await.unwrap();
|
||||
client_send.flush().await.unwrap();
|
||||
client_io.write_all(request).await.unwrap();
|
||||
client_io.flush().await.unwrap();
|
||||
|
||||
let mut response = Vec::new();
|
||||
let mut buf = [0u8; 4096];
|
||||
loop {
|
||||
match tokio::time::timeout(
|
||||
std::time::Duration::from_secs(5),
|
||||
client_recv.read(&mut buf),
|
||||
)
|
||||
.await
|
||||
match tokio::time::timeout(std::time::Duration::from_secs(5), client_io.read(&mut buf))
|
||||
.await
|
||||
{
|
||||
Ok(Ok(0)) => break,
|
||||
Ok(Ok(n)) => response.extend_from_slice(&buf[..n]),
|
||||
@@ -597,25 +494,17 @@ mod tests {
|
||||
}
|
||||
|
||||
async fn serve_and_read(adapter: HttpAdapter, request: &[u8]) -> String {
|
||||
let (mut client_send, server_recv) = duplex(8 * 1024);
|
||||
let (server_send, mut client_recv) = duplex(8 * 1024);
|
||||
let server_io = QuicStreamDuplex {
|
||||
read: server_recv,
|
||||
write: server_send,
|
||||
};
|
||||
let (server_io, mut client_io) = duplex(8 * 1024);
|
||||
let handle = tokio::spawn(async move {
|
||||
adapter.serve_io(server_io).await.ok();
|
||||
});
|
||||
client_send.write_all(request).await.unwrap();
|
||||
client_send.flush().await.unwrap();
|
||||
client_io.write_all(request).await.unwrap();
|
||||
client_io.flush().await.unwrap();
|
||||
let mut response = Vec::new();
|
||||
let mut buf = [0u8; 4096];
|
||||
loop {
|
||||
match tokio::time::timeout(
|
||||
std::time::Duration::from_secs(5),
|
||||
client_recv.read(&mut buf),
|
||||
)
|
||||
.await
|
||||
match tokio::time::timeout(std::time::Duration::from_secs(5), client_io.read(&mut buf))
|
||||
.await
|
||||
{
|
||||
Ok(Ok(0)) => break,
|
||||
Ok(Ok(n)) => response.extend_from_slice(&buf[..n]),
|
||||
|
||||
Reference in new issue
Block a user