Skip to main content

karyon_p2p/peer/
connection.rs

1use std::collections::{HashMap, HashSet};
2use std::sync::Arc;
3
4use async_channel::Sender;
5use async_trait::async_trait;
6use log::{debug, error};
7
8use karyon_core::async_util::{AsyncQueue, TaskGroup, TaskResult};
9use karyon_net::{FramedReader, FramedWriter};
10
11#[cfg(feature = "quic")]
12use karyon_core::async_runtime::io::{AsyncReadExt, AsyncWriteExt};
13#[cfg(feature = "quic")]
14use karyon_net::{framed, quic::QuicConn, StreamMux};
15
16#[cfg(feature = "quic")]
17use crate::{message::StreamInit, peer::ConnDirection, util::encode};
18
19use crate::{
20    codec::PeerNetMsgCodec,
21    conn_queue::QueuedConn,
22    message::{PeerNetCmd, PeerNetMsg, ProtocolMsg, ShutdownMsg},
23    protocol::{ProtocolEvent, ProtocolID},
24    util::decode,
25    Error, Result,
26};
27
28const SEND_QUEUE_SIZE: usize = 128;
29const RECV_QUEUE_SIZE: usize = 128;
30
31/// Per-peer wire abstraction. Hides single-pipe (TCP/TLS) vs.
32/// stream-mux (QUIC) framing from the layers above.
33#[async_trait]
34pub(crate) trait Wire: Send + Sync {
35    /// Send pre-encoded payload bytes for `proto_id`.
36    async fn send(&self, proto_id: &ProtocolID, payload: Vec<u8>) -> Result<()>;
37    /// Pop the next event for `proto_id`. Blocks until a message
38    /// arrives or shutdown is broadcast.
39    async fn recv(&self, proto_id: &ProtocolID) -> Result<ProtocolEvent>;
40    /// Graceful close. Pushes Shutdown to every recv queue and signals
41    /// the wire (transport-specific).
42    async fn shutdown(&self) -> Result<()>;
43}
44
45/// Build the right `Wire` for the post-handshake `QueuedConn`.
46/// Picks `MuxConnection` when QUIC is in use, `SingleConnection` otherwise.
47pub(crate) async fn from_queued(
48    queued: QueuedConn,
49    negotiated: &HashSet<ProtocolID>,
50    proto_ids: impl IntoIterator<Item = ProtocolID> + Clone,
51    task_group: &TaskGroup,
52    stop_chan: Sender<Result<()>>,
53) -> Result<Arc<dyn Wire>> {
54    #[cfg(feature = "quic")]
55    if queued.quic_conn.is_some() {
56        let conn = MuxConnection::from_queued(queued, negotiated, proto_ids, task_group).await?;
57        return Ok(Arc::new(conn) as Arc<dyn Wire>);
58    }
59
60    let _ = negotiated;
61    let conn = SingleConnection::from_queued(queued, proto_ids, task_group, stop_chan);
62    Ok(Arc::new(conn))
63}
64
65/// TCP / TLS path: one shared writer drains `send_queue`; a single
66/// reader demuxes incoming `PeerNetMsg`s into the matching `recv_queues`.
67pub(crate) struct SingleConnection {
68    send_queue: Arc<AsyncQueue<PeerNetMsg>>,
69    recv_queues: HashMap<ProtocolID, Arc<AsyncQueue<ProtocolEvent>>>,
70}
71
72impl SingleConnection {
73    /// Spawn the writer + demux reader and return the connection.
74    pub(crate) fn from_queued(
75        queued: QueuedConn,
76        proto_ids: impl IntoIterator<Item = ProtocolID>,
77        task_group: &TaskGroup,
78        stop_chan: Sender<Result<()>>,
79    ) -> Self {
80        let send_queue = AsyncQueue::new(SEND_QUEUE_SIZE);
81        let recv_queues = build_recv_queues(proto_ids);
82
83        spawn_writer_task(task_group, queued.writer, send_queue.clone());
84        spawn_demux_reader(task_group, queued.reader, recv_queues.clone(), stop_chan);
85
86        Self {
87            send_queue,
88            recv_queues,
89        }
90    }
91}
92
93#[async_trait]
94impl Wire for SingleConnection {
95    async fn send(&self, proto_id: &ProtocolID, payload: Vec<u8>) -> Result<()> {
96        let proto_msg = ProtocolMsg {
97            protocol_id: proto_id.clone(),
98            payload,
99        };
100        let net_msg = PeerNetMsg::new(PeerNetCmd::Protocol, &proto_msg)?;
101        self.send_queue.push(net_msg).await;
102        Ok(())
103    }
104
105    async fn recv(&self, proto_id: &ProtocolID) -> Result<ProtocolEvent> {
106        match self.recv_queues.get(proto_id) {
107            Some(q) => Ok(q.recv().await),
108            None => Err(Error::UnsupportedProtocol(proto_id.clone())),
109        }
110    }
111
112    async fn shutdown(&self) -> Result<()> {
113        let m = PeerNetMsg::new(PeerNetCmd::Shutdown, ShutdownMsg(0))?;
114        self.send_queue.push(m).await;
115        broadcast_shutdown(&self.recv_queues).await;
116        Ok(())
117    }
118}
119
120/// QUIC path: one stream per protocol, each with its own writer task.
121/// `send_queues` and `recv_queues` are keyed by protocol id.
122#[cfg(feature = "quic")]
123pub(crate) struct MuxConnection {
124    send_queues: HashMap<ProtocolID, Arc<AsyncQueue<PeerNetMsg>>>,
125    recv_queues: HashMap<ProtocolID, Arc<AsyncQueue<ProtocolEvent>>>,
126    quic_conn: QuicConn,
127}
128
129#[cfg(feature = "quic")]
130impl MuxConnection {
131    /// Open / accept one QUIC stream per negotiated protocol and spawn
132    /// a reader + writer task for each.
133    pub(crate) async fn from_queued(
134        queued: QueuedConn,
135        negotiated: &HashSet<ProtocolID>,
136        proto_ids: impl IntoIterator<Item = ProtocolID>,
137        task_group: &TaskGroup,
138    ) -> Result<Self> {
139        let quic_conn = queued
140            .quic_conn
141            .ok_or_else(|| Error::InvalidMsg("MuxConnection requires a QUIC conn".into()))?;
142
143        let recv_queues = build_recv_queues(proto_ids);
144        let (send_queues, streams) =
145            setup_quic_streams(&quic_conn, &queued.direction, negotiated).await?;
146
147        for stream in streams {
148            let q_send = send_queues
149                .get(&stream.proto_id)
150                .expect("send queue installed in setup_quic_streams")
151                .clone();
152            let q_recv = recv_queues
153                .get(&stream.proto_id)
154                .expect("recv queue installed in build_recv_queues")
155                .clone();
156            spawn_writer_task(task_group, stream.writer, q_send);
157            spawn_quic_reader(task_group, stream.proto_id, stream.reader, q_recv);
158        }
159
160        Ok(Self {
161            send_queues,
162            recv_queues,
163            quic_conn,
164        })
165    }
166}
167
168#[cfg(feature = "quic")]
169#[async_trait]
170impl Wire for MuxConnection {
171    async fn send(&self, proto_id: &ProtocolID, payload: Vec<u8>) -> Result<()> {
172        let proto_msg = ProtocolMsg {
173            protocol_id: proto_id.clone(),
174            payload,
175        };
176        let net_msg = PeerNetMsg::new(PeerNetCmd::Protocol, &proto_msg)?;
177        match self.send_queues.get(proto_id) {
178            Some(q) => {
179                q.push(net_msg).await;
180                Ok(())
181            }
182            None => Err(Error::UnsupportedProtocol(proto_id.clone())),
183        }
184    }
185
186    async fn recv(&self, proto_id: &ProtocolID) -> Result<ProtocolEvent> {
187        match self.recv_queues.get(proto_id) {
188            Some(q) => Ok(q.recv().await),
189            None => Err(Error::UnsupportedProtocol(proto_id.clone())),
190        }
191    }
192
193    async fn shutdown(&self) -> Result<()> {
194        self.quic_conn.close(0, b"shutdown");
195        broadcast_shutdown(&self.recv_queues).await;
196        Ok(())
197    }
198}
199
200/// One QUIC protocol stream's halves, returned from `setup_quic_streams`.
201#[cfg(feature = "quic")]
202struct MuxStream {
203    proto_id: ProtocolID,
204    reader: FramedReader<PeerNetMsgCodec>,
205    writer: FramedWriter<PeerNetMsgCodec>,
206}
207
208/// Spawn a task that drains `queue` into `writer`. Exits when the
209/// writer fails (peer hung up).
210fn spawn_writer_task(
211    task_group: &TaskGroup,
212    mut writer: FramedWriter<PeerNetMsgCodec>,
213    queue: Arc<AsyncQueue<PeerNetMsg>>,
214) {
215    task_group.spawn_then(
216        async move {
217            loop {
218                let msg = queue.recv().await;
219                if writer.send_msg(msg).await.is_err() {
220                    break;
221                }
222            }
223            Ok::<(), Error>(())
224        },
225        |res: TaskResult<Result<()>>| async move {
226            debug!("Peer writer task ended: {res}");
227        },
228    );
229}
230
231/// Spawn the single-pipe reader task. Reads `PeerNetMsg`s, routes
232/// `Protocol` payloads into the matching recv queue, signals the peer
233/// via `stop_chan` on Shutdown / error.
234fn spawn_demux_reader(
235    task_group: &TaskGroup,
236    mut reader: FramedReader<PeerNetMsgCodec>,
237    recv_queues: HashMap<ProtocolID, Arc<AsyncQueue<ProtocolEvent>>>,
238    stop_chan: Sender<Result<()>>,
239) {
240    task_group.spawn_then(
241        async move {
242            loop {
243                let msg = match reader.recv_msg().await {
244                    Ok(m) => m,
245                    Err(e) => {
246                        let _ = stop_chan.try_send(Err(e.into()));
247                        break;
248                    }
249                };
250                match msg.header.command {
251                    PeerNetCmd::Protocol => {
252                        let proto_msg: ProtocolMsg = match decode(&msg.payload) {
253                            Ok((m, _)) => m,
254                            Err(e) => {
255                                let _ = stop_chan.try_send(Err(e));
256                                break;
257                            }
258                        };
259                        match recv_queues.get(&proto_msg.protocol_id) {
260                            Some(q) => {
261                                q.push(ProtocolEvent::Message(proto_msg.payload)).await;
262                            }
263                            None => {
264                                error!("No recv queue for protocol {}", proto_msg.protocol_id);
265                            }
266                        }
267                    }
268                    PeerNetCmd::Shutdown => {
269                        let _ = stop_chan.try_send(Err(Error::PeerShutdown));
270                        break;
271                    }
272                    command => {
273                        let _ = stop_chan.try_send(Err(Error::InvalidMsg(format!(
274                            "Unexpected msg {command:?}"
275                        ))));
276                        break;
277                    }
278                }
279            }
280            Ok::<(), Error>(())
281        },
282        |res: TaskResult<Result<()>>| async move {
283            debug!("Peer reader task ended: {res}");
284        },
285    );
286}
287
288/// Spawn a per-stream QUIC reader task. The stream is already keyed
289/// by protocol id, so messages go straight into `recv_queue`.
290#[cfg(feature = "quic")]
291fn spawn_quic_reader(
292    task_group: &TaskGroup,
293    proto_id: ProtocolID,
294    mut reader: FramedReader<PeerNetMsgCodec>,
295    recv_queue: Arc<AsyncQueue<ProtocolEvent>>,
296) {
297    task_group.spawn_then(
298        async move {
299            loop {
300                let msg = match reader.recv_msg().await {
301                    Ok(m) => m,
302                    Err(_) => break,
303                };
304                match msg.header.command {
305                    PeerNetCmd::Protocol => {
306                        let proto_msg: ProtocolMsg = decode(&msg.payload)?.0;
307                        recv_queue
308                            .push(ProtocolEvent::Message(proto_msg.payload))
309                            .await;
310                    }
311                    PeerNetCmd::Shutdown => break,
312                    cmd => {
313                        error!("Unexpected msg on QUIC stream {proto_id}: {cmd:?}");
314                    }
315                }
316            }
317            Ok::<(), Error>(())
318        },
319        |res: TaskResult<Result<()>>| async move {
320            debug!("QUIC stream reader task ended: {res}");
321        },
322    );
323}
324
325/// Open / accept one QUIC stream per negotiated protocol and return
326/// the per-stream send queues plus the reader/writer halves.
327#[cfg(feature = "quic")]
328async fn setup_quic_streams(
329    quic_conn: &QuicConn,
330    direction: &ConnDirection,
331    negotiated: &HashSet<ProtocolID>,
332) -> Result<(
333    HashMap<ProtocolID, Arc<AsyncQueue<PeerNetMsg>>>,
334    Vec<MuxStream>,
335)> {
336    let mut queues = HashMap::new();
337    let mut streams = Vec::new();
338
339    match direction {
340        ConnDirection::Outbound => {
341            for proto_id in negotiated.iter() {
342                let mut stream = quic_conn.open_stream().await?;
343
344                let init = StreamInit {
345                    protocol_id: proto_id.clone(),
346                };
347                let encoded = encode(&init)?;
348                stream.write_all(&encoded).await?;
349                stream.flush().await?;
350
351                let conn = framed(stream, PeerNetMsgCodec::new());
352                let (reader, writer) = conn.split();
353
354                let q = AsyncQueue::new(SEND_QUEUE_SIZE);
355                queues.insert(proto_id.clone(), q);
356                streams.push(MuxStream {
357                    proto_id: proto_id.clone(),
358                    reader,
359                    writer,
360                });
361            }
362        }
363        ConnDirection::Inbound => {
364            let expected = negotiated.len();
365            let mut received = 0;
366
367            while received < expected {
368                let mut stream = quic_conn.accept_stream().await?;
369
370                let mut header_buf = vec![0u8; 256];
371                let n = stream.read(&mut header_buf).await?;
372                if n == 0 {
373                    continue;
374                }
375
376                let (init, _): (StreamInit, _) = decode(&header_buf[..n])?;
377
378                if !negotiated.contains(&init.protocol_id) {
379                    error!("Unsupported protocol: {}", init.protocol_id);
380                    continue;
381                }
382
383                let conn = framed(stream, PeerNetMsgCodec::new());
384                let (reader, writer) = conn.split();
385
386                let q = AsyncQueue::new(SEND_QUEUE_SIZE);
387                queues.insert(init.protocol_id.clone(), q);
388                streams.push(MuxStream {
389                    proto_id: init.protocol_id,
390                    reader,
391                    writer,
392                });
393
394                received += 1;
395            }
396        }
397    }
398
399    Ok((queues, streams))
400}
401
402/// One bounded recv queue per protocol id.
403fn build_recv_queues(
404    proto_ids: impl IntoIterator<Item = ProtocolID>,
405) -> HashMap<ProtocolID, Arc<AsyncQueue<ProtocolEvent>>> {
406    proto_ids
407        .into_iter()
408        .map(|id| (id, AsyncQueue::new(RECV_QUEUE_SIZE)))
409        .collect()
410}
411
412/// Push `ProtocolEvent::Shutdown` into every recv queue, waking
413/// blocked `recv` callers.
414async fn broadcast_shutdown(queues: &HashMap<ProtocolID, Arc<AsyncQueue<ProtocolEvent>>>) {
415    for q in queues.values() {
416        q.push(ProtocolEvent::Shutdown).await;
417    }
418}