fix: Unit 2 — channel 0 single-stream call mode (C-01, C-25 #1)
Channel 0 was dead in both directions (C-01): the call protocol's stream-per-request model (open_bi per call) is incompatible with channel 0's single yield-once BiStream — every call_open_op failed with StreamClosed on the connect side, and channel 0's Connection was a black hole on the accept side. The fix is single-stream call mode (ADR-036 amendment): all EventEnvelope frames are multiplexed on channel 0's one BiStream. Changes: - CallConnection gains single_stream_writer: Option<Arc<SharedFrameWriter>> and new_single_stream() constructor. call_with_payload, subscribe_with_payload, publish_with_payload, and abort branch on is_single_stream() — in single-stream mode they write frames through the shared writer (mutex-serialized) instead of opening a fresh open_bi per call. - Dispatcher::run_loop_single_stream reads frames off channel 0's read half, dispatches call.requested, writes responses through the shared writer, and routes in-flight call.published/call.completed/ call.aborted to the matching Pub sink's chunk_tx by request_id. - ChannelsAdapter::handle's InstallChannelZero hook now receives channel 0's Connection (built by the adapter) and runs the single-stream dispatch loop on it — closing the accept-side black hole. Mux runner is spawned BEFORE install_channel_zero so mux.register(0) can complete. - ChannelClient::from_connection uses CallConnection::new_single_stream and spawns a read pump (read_single_stream_until_closed) that routes channel-0 response frames into the PendingRequestMap via dispatch_envelope — closing the connect-side StreamClosed path. - ADR-036 amendment records the single-stream-mode decision (two-way door: implementation detail, wire format unchanged). Acceptance gate (C-25 #1): three end-to-end tests wire ChannelClient ↔ ChannelsAdapter over a real tokio::io::duplex pair carrying the channels 8-byte chunk header wire format: - channel_0_end_to_end_call_round_trip: a Query op round-trip - channel_0_end_to_end_unknown_op_returns_not_found: NOT_FOUND - channel_0_end_to_end_publish_delivers_chunks: a Pub op with 3 chunks Verification: 437 tests pass (was 434; +3 e2e), clippy clean, fmt clean, doc warnings unchanged (4, pre-existing C-09).
This commit is contained in:
1 parent
502488cc72
commit
495e04ed43
5 files changed
+925
-68
No files matched your search
+42
-34
@@ -5,9 +5,12 @@
|
||||
//! route each chunk's payload to the matching `channel_id`'s
|
||||
//! reassembly buffer.
|
||||
//!
|
||||
//! The `preinstall_channel_0` step (the hook for `channels-call` to
|
||||
//! install the `CallAdapter` on channel 0) is exposed via a callback.
|
||||
//! `channels-call` provides the implementation; this adapter calls it.
|
||||
//! Channel 0 is pre-negotiated as `alknet/call` (ADR-036). The
|
||||
//! `install_channel_zero` hook (ADR-036 amendment — single-stream call
|
||||
//! mode) receives channel 0's `Connection` (already constructed by the
|
||||
//! adapter from the reassembled read half + the mux write half) and
|
||||
//! runs the call dispatch loop on it. `channels-call` provides the
|
||||
//! implementation; this adapter calls it.
|
||||
//!
|
||||
//! See `docs/architecture/channels-adapter.md` for the full contract.
|
||||
|
||||
@@ -29,17 +32,21 @@ use super::wire::CHUNK_HEADER_LEN;
|
||||
pub const CHANNELS_ALPN: &[u8] = b"alknet/channels";
|
||||
|
||||
/// The hook for `channels-call` to install the `CallAdapter` on
|
||||
/// channel 0. The adapter calls this after allocating channel 0's
|
||||
/// reassembly buffer; `channels-call` wraps it as a `Connection` via
|
||||
/// `Connection::from_source(ChannelBidiStreamSource, alpn)` and hands
|
||||
/// it to the `CallAdapter`.
|
||||
/// channel 0 (ADR-036 amendment — single-stream call mode). The
|
||||
/// adapter calls this after constructing channel 0's `Connection`
|
||||
/// (from the reassembled read half + the mux write half); the hook
|
||||
/// runs the call dispatch loop on it.
|
||||
///
|
||||
/// The callback receives the `ChannelManager` (so `channels-call` can
|
||||
/// construct the `ChannelBidiStreamSource` from the reassembled read
|
||||
/// half + the mux write half) and the `AuthContext` (so the
|
||||
/// `CallAdapter` can resolve the peer's identity).
|
||||
pub type InstallChannelZero =
|
||||
Arc<dyn Fn(&ChannelManager, &AuthContext) -> tokio::task::JoinHandle<()> + Send + Sync>;
|
||||
/// The callback receives channel 0's `Connection` (carrying the
|
||||
/// `alknet/call` ALPN) and the `AuthContext` (so the call dispatch
|
||||
/// loop can resolve the peer's identity). The `ChannelManager` is
|
||||
/// accessible via the connection's `BidiStreamSource` (the channel-0
|
||||
/// source closes over it), so the hook does not need it as a separate
|
||||
/// argument; the manager is passed for callers that want to reach it
|
||||
/// (e.g., to register channel lifecycle ops' `ChannelCore`).
|
||||
pub type InstallChannelZero = Arc<
|
||||
dyn Fn(ChannelManager, Connection, AuthContext) -> tokio::task::JoinHandle<()> + Send + Sync,
|
||||
>;
|
||||
|
||||
/// `ChannelsAdapter` — the `ProtocolHandler` for `alknet/channels`.
|
||||
/// Its `handle()` receives one `Connection`, installs channel 0 via
|
||||
@@ -174,33 +181,34 @@ impl ProtocolHandler for ChannelsAdapter {
|
||||
connection.remote_addr(),
|
||||
);
|
||||
|
||||
// 3. Install channel 0 (pre-negotiated as `alknet/call`,
|
||||
// ADR-036). The `install_channel_zero` hook (provided by
|
||||
// `channels-call`) wraps channel 0's reassembly buffer as
|
||||
// a `Connection` and hands it to the `CallAdapter`.
|
||||
let (channel0_send, channel0_recv) = manager
|
||||
.install_channel_zero(None)
|
||||
.await
|
||||
.map_err(|e| HandlerError::Internal(format!("channel 0 install failed: {e}").into()))?;
|
||||
let channel0_source =
|
||||
super::source::channel_source(channel0_recv, channel0_send, connection.remote_addr());
|
||||
let _channel0_conn = Connection::from_source(channel0_source, b"alknet/call".to_vec());
|
||||
let _handler_task = (self.install_channel_zero)(&manager, auth);
|
||||
// The install hook owns the call-adapter task; the manager's
|
||||
// clear_all on transport EOF will not abort it (it was passed
|
||||
// as None). The task is tied to the connection lifetime via
|
||||
// the manager's mux handle — when the mux runner ends, the
|
||||
// call adapter's write half drops, and the call adapter's
|
||||
// dispatch loop ends.
|
||||
|
||||
// 4. Spawn the mux runner (write side).
|
||||
// Spawn the mux runner BEFORE installing channel 0 —
|
||||
// `install_channel_zero` calls `mux.register(0).await` which
|
||||
// needs the runner to be draining `new_pumps`.
|
||||
let _mux_task = tokio::spawn(async move {
|
||||
if let Err(e) = mux_runner.run().await {
|
||||
warn!(error = %e, "mux runner ended with error");
|
||||
}
|
||||
});
|
||||
|
||||
// 5. Run the demux loop (read side). This blocks until
|
||||
// 3. Install channel 0 (pre-negotiated as `alknet/call`,
|
||||
// ADR-036). Channel 0's reassembly buffer + mux write half
|
||||
// are joined into a `BiStream` and wrapped in a
|
||||
// `Connection` (via `ChannelBidiStreamSource`). The
|
||||
// `install_channel_zero` hook (provided by `channels-call`)
|
||||
// runs the call dispatch loop on this `Connection` — the
|
||||
// single-stream call mode (ADR-036 amendment) multiplexes
|
||||
// all `EventEnvelope` frames on channel 0's one `BiStream`.
|
||||
let (channel0_send, channel0_recv) = manager
|
||||
.install_channel_zero(None)
|
||||
.await
|
||||
.map_err(|e| HandlerError::Internal(format!("channel 0 install failed: {e}").into()))?;
|
||||
let channel0_source =
|
||||
super::source::channel_source(channel0_recv, channel0_send, connection.remote_addr());
|
||||
let channel0_conn = Connection::from_source(channel0_source, b"alknet/call".to_vec());
|
||||
let _handler_task =
|
||||
(self.install_channel_zero)(manager.clone(), channel0_conn, auth.clone());
|
||||
|
||||
// 4. Run the demux loop (read side). This blocks until
|
||||
// transport EOF, then clears the channel map (REQ-CH-02).
|
||||
Self::run_demux_loop(&manager, Box::new(reader)).await;
|
||||
|
||||
|
||||
+335
-24
@@ -5,6 +5,14 @@
|
||||
//! `open_channel(alpn, params)` to open data channels via the
|
||||
//! per-ALPN open ops on channel 0.
|
||||
//!
|
||||
//! Channel 0 runs in single-stream call mode (ADR-036 amendment):
|
||||
//! all `EventEnvelope` frames for call operations are multiplexed on
|
||||
//! channel 0's one `BiStream`. `CallConnection::new_single_stream`
|
||||
//! holds the framed write half; the client spawns a read pump that
|
||||
//! reads `EventEnvelope` frames off channel 0's reassembled read half
|
||||
//! (fed by the demux) and routes them into the `PendingRequestMap`
|
||||
//! via `dispatch_envelope`.
|
||||
//!
|
||||
//! The dial (TLS, QUIC, WebSocket) lives in the consumer —
|
||||
//! `ChannelClient` is transport-agnostic by construction (ADR-043).
|
||||
//!
|
||||
@@ -41,34 +49,72 @@ impl ChannelClient {
|
||||
/// Construct from an established `Connection` (the consumer dials
|
||||
/// the transport and establishes the `alknet/channels` ALPN).
|
||||
/// Installs channel 0 (pre-negotiated as `alknet/call`,
|
||||
/// ADR-036), wraps it as a `CallConnection`, spawns the demux and
|
||||
/// mux tasks, and returns the client.
|
||||
/// ADR-036), wraps it as a `CallConnection` in single-stream call
|
||||
/// mode (ADR-036 amendment), spawns the demux and mux tasks, and
|
||||
/// returns the client.
|
||||
///
|
||||
/// The `CallAdapter`'s dispatch loop runs on channel 0; the
|
||||
/// `ChannelClient` holds the `CallConnection` so the consumer can
|
||||
/// call `open_channel` (which calls the per-ALPN open ops on
|
||||
/// channel 0).
|
||||
/// The call dispatch loop on channel 0 is driven by the client:
|
||||
/// a read pump task reads `EventEnvelope` frames off channel 0's
|
||||
/// reassembled read half (fed by the demux) and routes them into
|
||||
/// the `PendingRequestMap` via `dispatch_envelope`, resolving
|
||||
/// pending calls. The `CallConnection` holds the framed write
|
||||
/// half so `call_open_op` can write `call.requested` frames.
|
||||
pub async fn from_connection(connection: Connection) -> Result<Self, StreamError> {
|
||||
let remote_addr = connection.remote_addr();
|
||||
let bidi = connection.accept_bi().await?;
|
||||
let (reader, writer) = tokio::io::split(bidi);
|
||||
let (mux_handle, mux_runner) = MuxRunner::new(Box::new(writer));
|
||||
let manager = ChannelManager::with_defaults(mux_handle, connection.remote_addr());
|
||||
|
||||
// Spawn the mux runner BEFORE installing channel 0 —
|
||||
// `install_channel_zero` calls `mux.register(0).await` which
|
||||
// sends a `Registration` to the runner and waits for the
|
||||
// response. The runner must be draining `new_pumps` for the
|
||||
// registration to complete.
|
||||
let _mux_task = tokio::spawn(async move {
|
||||
if let Err(e) = mux_runner.run().await {
|
||||
tracing::warn!(error = %e, "channel client: mux runner ended with error");
|
||||
}
|
||||
});
|
||||
|
||||
let manager = ChannelManager::with_defaults(mux_handle, remote_addr);
|
||||
|
||||
// Install channel 0 — the call adapter's read/write halves.
|
||||
let (channel0_send, channel0_recv) = manager
|
||||
.install_channel_zero(None)
|
||||
.await
|
||||
.map_err(|_| StreamError::StreamClosed)?;
|
||||
let channel0_source =
|
||||
super::source::channel_source(channel0_recv, channel0_send, connection.remote_addr());
|
||||
let channel0_conn = Connection::from_source(channel0_source, b"alknet/call".to_vec());
|
||||
let call_connection = CallConnection::new(channel0_conn);
|
||||
|
||||
// Spawn the mux runner (write side).
|
||||
let _mux_task = tokio::spawn(async move {
|
||||
if let Err(e) = mux_runner.run().await {
|
||||
tracing::warn!(error = %e, "channel client: mux runner ended with error");
|
||||
}
|
||||
// Join channel 0's reassembled read half + mux write half into
|
||||
// a `BiStream` (via `channel_source`), wrap as a `Connection`
|
||||
// (for ALPN/addr/identity), then yield the `BiStream` once via
|
||||
// `accept_bi` and split it into a `SharedFrameWriter` (the
|
||||
// framed write half for `call.requested` etc.) + a raw read
|
||||
// half (for the read pump). This is the single-stream call
|
||||
// mode (ADR-036 amendment): all `EventEnvelope` frames are
|
||||
// multiplexed on channel 0's one `BiStream`.
|
||||
let channel0_source =
|
||||
super::source::channel_source(channel0_recv, channel0_send, remote_addr);
|
||||
let channel0_conn = Connection::from_source(channel0_source, b"alknet/call".to_vec());
|
||||
let channel0_bidi = channel0_conn.accept_bi().await?;
|
||||
let (single_stream_writer, single_stream_reader) =
|
||||
crate::protocol::connection::split_single_stream(channel0_bidi);
|
||||
|
||||
let call_connection =
|
||||
CallConnection::new_single_stream(channel0_conn, single_stream_writer);
|
||||
|
||||
// Spawn the read pump for channel 0's response frames. Reads
|
||||
// `EventEnvelope` frames off channel 0's reassembled read half
|
||||
// (fed by the demux) and routes them into the
|
||||
// `PendingRequestMap` via `dispatch_envelope`, resolving
|
||||
// pending calls. This is the single-stream analogue of
|
||||
// `read_stream_until_closed` in stream-per-request mode.
|
||||
let pending_map = Arc::clone(call_connection.pending());
|
||||
let _read_pump = tokio::spawn(async move {
|
||||
crate::protocol::connection::read_single_stream_until_closed(
|
||||
single_stream_reader,
|
||||
&pending_map,
|
||||
)
|
||||
.await;
|
||||
});
|
||||
|
||||
// Spawn the demux loop (read side). The loop reads 8-byte
|
||||
@@ -98,12 +144,10 @@ impl ChannelClient {
|
||||
/// `channels/<alpn>/pub`) on channel 0. Returns the
|
||||
/// `ResponseEnvelope` (which carries `channel_id` on success).
|
||||
///
|
||||
/// The consumer uses this to open a data channel: call
|
||||
/// `channels/tty/sub` with the ALPN-specific params; the responder
|
||||
/// allocates the `channel_id` and returns it; the consumer then
|
||||
/// reads/writes on the channel's `BiStream` (obtained via
|
||||
/// `manager.open_channel_stream(channel_id)` or the relay
|
||||
/// machinery).
|
||||
/// In single-stream call mode (ADR-036 amendment), this writes
|
||||
/// `call.requested` through channel 0's shared frame writer and
|
||||
/// awaits the response via the `PendingRequestMap` (resolved by
|
||||
/// the read pump from the demux).
|
||||
pub async fn call_open_op(&self, operation_id: &str, input: Value) -> ResponseEnvelope {
|
||||
let guard = self.call_connection.lock().await;
|
||||
match guard.as_ref() {
|
||||
@@ -127,11 +171,278 @@ impl ChannelClient {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::channels::adapter::ChannelsAdapter;
|
||||
use crate::core::auth::{AuthContext, IdentityProvider};
|
||||
use crate::core::types::Connection;
|
||||
use crate::protocol::connection::split_single_stream;
|
||||
use crate::protocol::dispatch::Dispatcher;
|
||||
use crate::registry::registration::{
|
||||
make_handler, HandlerKind, HandlerRegistration, OperationProvenance,
|
||||
};
|
||||
use crate::registry::spec::{AccessControl, OperationSpec, OperationType, Visibility};
|
||||
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
||||
use std::sync::Arc;
|
||||
|
||||
const TEST_ADDR: SocketAddr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 4321);
|
||||
|
||||
fn external_query_spec(name: &str) -> OperationSpec {
|
||||
OperationSpec::new(
|
||||
name,
|
||||
OperationType::Query,
|
||||
Visibility::External,
|
||||
serde_json::json!({}),
|
||||
serde_json::json!({}),
|
||||
vec![],
|
||||
AccessControl::default(),
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
struct NoopIdProvider;
|
||||
impl IdentityProvider for NoopIdProvider {
|
||||
fn resolve_from_fingerprint(&self, _: &str) -> Option<crate::core::auth::Identity> {
|
||||
None
|
||||
}
|
||||
fn resolve_from_token(
|
||||
&self,
|
||||
_: &crate::core::auth::AuthToken,
|
||||
) -> Option<crate::core::auth::Identity> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Build the `install_channel_zero` hook for the accept side: it
|
||||
/// yields channel 0's `BiStream` once, splits it into the shared
|
||||
/// writer + reader, constructs a single-stream `CallConnection`,
|
||||
/// and runs `Dispatcher::run_loop_single_stream` on it. Returns
|
||||
/// the `JoinHandle` for the spawned dispatch loop.
|
||||
fn make_install_channel_zero(
|
||||
registry: Arc<crate::registry::registration::OperationRegistry>,
|
||||
) -> crate::channels::adapter::InstallChannelZero {
|
||||
Arc::new(move |_manager, channel0_conn, _auth| {
|
||||
let registry = Arc::clone(®istry);
|
||||
let provider: Arc<dyn IdentityProvider> = Arc::new(NoopIdProvider);
|
||||
tokio::spawn(async move {
|
||||
let channel0_bidi = match channel0_conn.accept_bi().await {
|
||||
Ok(s) => s,
|
||||
Err(_) => return,
|
||||
};
|
||||
let (writer, reader) = split_single_stream(channel0_bidi);
|
||||
let call_connection = Arc::new(CallConnection::new_single_stream(
|
||||
channel0_conn,
|
||||
Arc::clone(&writer),
|
||||
));
|
||||
let dp = Dispatcher::new(registry, provider);
|
||||
dp.run_loop_single_stream(call_connection, reader, writer)
|
||||
.await;
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn channel_client_is_not_send_safe_across_await_by_default() {
|
||||
// Smoke test: ChannelClient compiles. The actual send/sync
|
||||
// bounds are exercised by the integration tests.
|
||||
let _ = std::marker::PhantomData::<ChannelClient>;
|
||||
}
|
||||
|
||||
/// C-25 #1 — the end-to-end acceptance gate for Unit 2 (channel 0
|
||||
/// single-stream call mode, ADR-036 amendment). Wires
|
||||
/// `ChannelClient` (connect side) ↔ `ChannelsAdapter` (accept
|
||||
/// side) over a real `tokio::io::duplex` pair carrying the
|
||||
/// channels 8-byte chunk header wire format. A `call.requested`
|
||||
/// from the client is demuxed on channel 0, dispatched by the
|
||||
/// server's single-stream `Dispatcher::run_loop_single_stream`,
|
||||
/// and the `call.responded` is muxed back to the client's read
|
||||
/// pump, resolving the pending call. This would have caught C-01
|
||||
/// immediately (every `call_open_op` failed with
|
||||
/// `StreamClosed` on the connect side; channel 0 was a black hole
|
||||
/// on the accept side).
|
||||
#[tokio::test]
|
||||
async fn channel_0_end_to_end_call_round_trip() {
|
||||
let mut registry = crate::registry::registration::OperationRegistry::new();
|
||||
registry
|
||||
.register(HandlerRegistration::new(
|
||||
external_query_spec("echo/run"),
|
||||
HandlerKind::Once(make_handler(|input, ctx| async move {
|
||||
ResponseEnvelope::ok(ctx.request_id, input)
|
||||
})),
|
||||
OperationProvenance::Local,
|
||||
None,
|
||||
None,
|
||||
crate::core::types::Capabilities::new(),
|
||||
))
|
||||
.unwrap();
|
||||
let registry = Arc::new(registry);
|
||||
|
||||
let (client_end, server_end) = tokio::io::duplex(64 * 1024);
|
||||
let client_conn =
|
||||
Connection::from_bidi(client_end, b"alknet/channels".to_vec(), Some(TEST_ADDR));
|
||||
let server_conn =
|
||||
Connection::from_bidi(server_end, b"alknet/channels".to_vec(), Some(TEST_ADDR));
|
||||
|
||||
let adapter = ChannelsAdapter::new(make_install_channel_zero(Arc::clone(®istry)));
|
||||
let auth = AuthContext::anonymous(b"alknet/channels");
|
||||
let _server_handle = tokio::spawn(async move {
|
||||
let _ = crate::core::types::ProtocolHandler::handle(&adapter, server_conn, &auth).await;
|
||||
});
|
||||
|
||||
let client = ChannelClient::from_connection(client_conn)
|
||||
.await
|
||||
.expect("channel client init");
|
||||
|
||||
let response = tokio::time::timeout(
|
||||
std::time::Duration::from_secs(10),
|
||||
client.call_open_op("echo/run", serde_json::json!({ "msg": "hi" })),
|
||||
)
|
||||
.await
|
||||
.expect("channel-0 round-trip timed out");
|
||||
|
||||
assert!(
|
||||
response.result.is_ok(),
|
||||
"channel-0 round-trip should succeed, got {:?}",
|
||||
response.result
|
||||
);
|
||||
assert_eq!(response.result.unwrap(), serde_json::json!({ "msg": "hi" }));
|
||||
}
|
||||
|
||||
/// C-25 #1 (negative case) — an unknown op over channel 0 returns
|
||||
/// `NOT_FOUND`, proving the single-stream dispatch path reaches
|
||||
/// the registry's not-found branch end-to-end (not just
|
||||
/// `StreamClosed` from a dead channel 0 as in C-01).
|
||||
#[tokio::test]
|
||||
async fn channel_0_end_to_end_unknown_op_returns_not_found() {
|
||||
let registry = Arc::new(crate::registry::registration::OperationRegistry::new());
|
||||
|
||||
let (client_end, server_end) = tokio::io::duplex(64 * 1024);
|
||||
let client_conn =
|
||||
Connection::from_bidi(client_end, b"alknet/channels".to_vec(), Some(TEST_ADDR));
|
||||
let server_conn =
|
||||
Connection::from_bidi(server_end, b"alknet/channels".to_vec(), Some(TEST_ADDR));
|
||||
|
||||
let adapter = ChannelsAdapter::new(make_install_channel_zero(Arc::clone(®istry)));
|
||||
let auth = AuthContext::anonymous(b"alknet/channels");
|
||||
let _server_handle = tokio::spawn(async move {
|
||||
let _ = crate::core::types::ProtocolHandler::handle(&adapter, server_conn, &auth).await;
|
||||
});
|
||||
|
||||
let client = ChannelClient::from_connection(client_conn)
|
||||
.await
|
||||
.expect("channel client init");
|
||||
|
||||
let response = tokio::time::timeout(
|
||||
std::time::Duration::from_secs(10),
|
||||
client.call_open_op("no/such/op", serde_json::json!({})),
|
||||
)
|
||||
.await
|
||||
.expect("channel-0 unknown-op timed out");
|
||||
|
||||
match response.result {
|
||||
Err(e) => assert_eq!(e.code, "NOT_FOUND", "unknown op over channel 0"),
|
||||
other => panic!("expected NOT_FOUND, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// C-25 #1 (Pub over channel 0) — a `publish()` through channel
|
||||
/// 0's single-stream mode delivers chunks to the responder's
|
||||
/// `SinkHandler` and returns the handler's response. Proves the
|
||||
/// single-stream dispatch loop's in-flight sink routing works
|
||||
/// (`call.published` frames arriving after `call.requested` are
|
||||
/// routed to the matching sink's `chunk_tx`).
|
||||
#[tokio::test]
|
||||
async fn channel_0_end_to_end_publish_delivers_chunks() {
|
||||
use futures::stream::StreamExt;
|
||||
|
||||
let mut registry = crate::registry::registration::OperationRegistry::new();
|
||||
let counting_sink = crate::registry::registration::make_sink_handler(
|
||||
|_input, ctx, mut stream| async move {
|
||||
let mut count = 0u32;
|
||||
let mut last = serde_json::Value::Null;
|
||||
while let Some(item) = stream.next().await {
|
||||
match item {
|
||||
Ok(v) => {
|
||||
count += 1;
|
||||
last = v;
|
||||
}
|
||||
Err(_) => break,
|
||||
}
|
||||
}
|
||||
ResponseEnvelope::ok(
|
||||
ctx.request_id,
|
||||
serde_json::json!({ "count": count, "last": last }),
|
||||
)
|
||||
},
|
||||
);
|
||||
registry
|
||||
.register(HandlerRegistration::new(
|
||||
OperationSpec::new(
|
||||
"fs/upload",
|
||||
OperationType::Pub,
|
||||
Visibility::External,
|
||||
serde_json::json!({}),
|
||||
serde_json::json!({}),
|
||||
vec![],
|
||||
AccessControl::default(),
|
||||
None,
|
||||
),
|
||||
HandlerKind::Sink(counting_sink),
|
||||
OperationProvenance::Local,
|
||||
None,
|
||||
None,
|
||||
crate::core::types::Capabilities::new(),
|
||||
))
|
||||
.unwrap();
|
||||
let registry = Arc::new(registry);
|
||||
|
||||
let (client_end, server_end) = tokio::io::duplex(64 * 1024);
|
||||
let client_conn =
|
||||
Connection::from_bidi(client_end, b"alknet/channels".to_vec(), Some(TEST_ADDR));
|
||||
let server_conn =
|
||||
Connection::from_bidi(server_end, b"alknet/channels".to_vec(), Some(TEST_ADDR));
|
||||
|
||||
let adapter = ChannelsAdapter::new(make_install_channel_zero(Arc::clone(®istry)));
|
||||
let auth = AuthContext::anonymous(b"alknet/channels");
|
||||
let _server_handle = tokio::spawn(async move {
|
||||
let _ = crate::core::types::ProtocolHandler::handle(&adapter, server_conn, &auth).await;
|
||||
});
|
||||
|
||||
let client = ChannelClient::from_connection(client_conn)
|
||||
.await
|
||||
.expect("channel client init");
|
||||
|
||||
let call_conn = client
|
||||
.take_call_connection()
|
||||
.await
|
||||
.expect("call connection present");
|
||||
|
||||
let chunks = vec![
|
||||
serde_json::json!({"chunk": 1}),
|
||||
serde_json::json!({"chunk": 2}),
|
||||
serde_json::json!({"chunk": 3}),
|
||||
];
|
||||
let stream: std::pin::Pin<
|
||||
Box<dyn futures::stream::Stream<Item = serde_json::Value> + Send>,
|
||||
> = Box::pin(futures::stream::iter(chunks.clone()));
|
||||
let response = tokio::time::timeout(
|
||||
std::time::Duration::from_secs(10),
|
||||
call_conn.publish("fs/upload", serde_json::json!({"path": "/x"}), stream),
|
||||
)
|
||||
.await
|
||||
.expect("channel-0 publish timed out");
|
||||
|
||||
assert!(
|
||||
response.result.is_ok(),
|
||||
"publish should succeed, got {:?}",
|
||||
response.result
|
||||
);
|
||||
let out = response.result.unwrap();
|
||||
assert_eq!(
|
||||
out["count"],
|
||||
serde_json::json!(3),
|
||||
"responder saw all 3 chunks"
|
||||
);
|
||||
assert_eq!(
|
||||
out["last"],
|
||||
serde_json::json!({"chunk": 3}),
|
||||
"last chunk matches"
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user