Skip to main content

snarkos_node_tcp/protocols/
writing.rs

1// Copyright (c) 2019-2026 Provable Inc.
2// This file is part of the snarkOS library.
3
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at:
7
8// http://www.apache.org/licenses/LICENSE-2.0
9
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16use std::{any::Any, collections::HashMap, io, net::SocketAddr, sync::Arc, time::Duration};
17
18use async_trait::async_trait;
19use futures_util::sink::SinkExt;
20#[cfg(feature = "locktick")]
21use locktick::parking_lot::RwLock;
22#[cfg(not(feature = "locktick"))]
23use parking_lot::RwLock;
24use tokio::{
25    io::AsyncWrite,
26    sync::{mpsc, oneshot},
27    time::timeout,
28};
29use tokio_util::codec::{Encoder, FramedWrite};
30use tracing::*;
31
32#[cfg(doc)]
33use crate::{Config, Tcp, protocols::Handshake};
34use crate::{
35    Connection,
36    ConnectionSide,
37    P2P,
38    connections::{DisconnectOrigin, create_connection_span},
39    protocols::{DisconnectOnDrop, Protocol, ProtocolHandler, ReturnableConnection},
40};
41
42type WritingSenders = Arc<RwLock<HashMap<SocketAddr, mpsc::Sender<WrappedMessage>>>>;
43
44/// Can be used to specify and enable writing, i.e. sending outbound messages. If the [`Handshake`]
45/// protocol is enabled too, it goes into force only after the handshake has been concluded.
46#[async_trait]
47pub trait Writing: P2P
48where
49    Self: Clone + Send + Sync + 'static,
50{
51    /// The depth of per-connection queues used to send outbound messages; the greater it is, the more outbound
52    /// messages the node can enqueue. Setting it to a large value is not recommended, as doing it might
53    /// obscure potential issues with your implementation (like slow serialization) or network.
54    ///
55    /// The default value is 1024.
56    fn message_queue_depth(&self) -> usize {
57        1024
58    }
59
60    /// The maximum time allowed for a single message write, both encoding it and flushing it to
61    /// the underlying stream, before the connection is considered dead.
62    ///
63    /// This has to leave room for encoding the largest message we can produce, as blocks can
64    /// currently be quite large.
65    const TIMEOUT: Duration = Duration::from_secs(10);
66
67    /// The type of the outbound messages; unless their serialization is expensive and the message
68    /// is broadcasted (in which case it would get serialized multiple times), serialization should
69    /// be done in the implementation of [`Self::Codec`].
70    type Message: Send;
71
72    /// The user-supplied [`Encoder`] used to write outbound messages to the target stream.
73    type Codec: Encoder<Self::Message, Error = io::Error> + Send;
74
75    /// Prepares the node to send messages.
76    async fn enable_writing(&self) {
77        let (conn_sender, mut conn_receiver) = mpsc::channel(self.tcp().config().max_connections as usize);
78
79        // the conn_senders are used to send messages from the Tcp to individual connections
80        let conn_senders: WritingSenders = Default::default();
81        // procure a clone to create the WritingHandler with
82        let senders = conn_senders.clone();
83
84        // use a channel to know when the writing task is ready
85        let (tx_writing, rx_writing) = oneshot::channel();
86
87        // the task spawning tasks sending messages to all the streams
88        let self_clone = self.clone();
89        let writing_task = tokio::spawn(async move {
90            trace!(parent: self_clone.tcp().span(), "spawned the Writing handler task");
91            tx_writing.send(()).unwrap(); // safe; the channel was just opened
92
93            // these objects are sent from `Tcp::adapt_stream`
94            while let Some(returnable_conn) = conn_receiver.recv().await {
95                self_clone.handle_new_connection(returnable_conn, &conn_senders).await;
96            }
97        });
98        let _ = rx_writing.await;
99        self.tcp().tasks.lock().push(writing_task);
100
101        // register the WritingHandler with the Tcp
102        let hdl = Box::new(WritingHandler { handler: ProtocolHandler(conn_sender), senders });
103        assert!(self.tcp().protocols.writing.set(hdl).is_ok(), "the Writing protocol was enabled more than once!");
104    }
105
106    /// Creates an [`Encoder`] used to write the outbound messages to the target stream.
107    /// The `side` param indicates the connection side **from the node's perspective**.
108    fn codec(&self, addr: SocketAddr, side: ConnectionSide) -> Self::Codec;
109
110    /// Sends the provided message to the specified [`SocketAddr`]. Returns as soon as the message is queued to
111    /// be sent, without waiting for the actual delivery; instead, the caller is provided with a [`oneshot::Receiver`]
112    /// which can be used to determine when and whether the message has been delivered.
113    ///
114    /// # Errors
115    ///
116    /// The following errors can be returned:
117    /// - [`io::ErrorKind::NotConnected`] if the node is not connected to the provided address
118    /// - [`io::ErrorKind::Other`] if the outbound message queue for this address is full
119    /// - [`io::ErrorKind::Unsupported`] if [`Writing::enable_writing`] hadn't been called yet
120    fn unicast(&self, addr: SocketAddr, message: Self::Message) -> io::Result<oneshot::Receiver<io::Result<()>>> {
121        // access the protocol handler
122        if let Some(handler) = self.tcp().protocols.writing.get() {
123            // find the message sender for the given address
124            if let Some(sender) = handler.senders.read().get(&addr).cloned() {
125                let (msg, delivery) = WrappedMessage::new(Box::new(message));
126                sender
127                    .try_send(msg)
128                    .map_err(|e| {
129                        let conn_span = create_connection_span(addr, self.tcp().span());
130                        error!(parent: conn_span, "can't send a message: {e}");
131                        io::ErrorKind::Other.into()
132                    })
133                    .map(|_| delivery)
134            } else {
135                Err(io::ErrorKind::NotConnected.into())
136            }
137        } else {
138            Err(io::ErrorKind::Unsupported.into())
139        }
140    }
141
142    /// Broadcasts the provided message to all connected peers. Returns as soon as the message is queued to
143    /// be sent to all the peers, without waiting for the actual delivery. This method doesn't provide the
144    /// means to check when and if the messages actually get delivered; you can achieve that by calling
145    /// [`Writing::unicast`] for each address returned by [`Tcp::connected_addrs`].
146    ///
147    /// # Errors
148    ///
149    /// Returns [`io::ErrorKind::Unsupported`] if [`Writing::enable_writing`] hadn't been called yet.
150    fn broadcast(&self, message: Self::Message) -> io::Result<()>
151    where
152        Self::Message: Clone,
153    {
154        // access the protocol handler
155        if let Some(handler) = self.tcp().protocols.writing.get() {
156            let senders = handler.senders.read().clone();
157            for (addr, message_sender) in senders {
158                let (msg, _delivery) = WrappedMessage::new(Box::new(message.clone()));
159                let _ = message_sender.try_send(msg).map_err(|e| {
160                    let conn_span = create_connection_span(addr, self.tcp().span());
161                    error!(parent: conn_span, "can't send a message: {e}");
162                });
163            }
164
165            Ok(())
166        } else {
167            Err(io::ErrorKind::Unsupported.into())
168        }
169    }
170}
171
172/// This trait is used to restrict access to methods that would otherwise be public in [`Writing`].
173#[async_trait]
174trait WritingInternal: Writing {
175    /// Writes the given message to the network stream and returns the number of written bytes.
176    async fn write_to_stream<W: AsyncWrite + Unpin + Send>(
177        &self,
178        message: Self::Message,
179        writer: &mut FramedWrite<W, Self::Codec>,
180    ) -> Result<usize, <Self::Codec as Encoder<Self::Message>>::Error>;
181
182    /// Applies the [`Writing`] protocol to a single connection.
183    async fn handle_new_connection(&self, (conn, conn_returner): ReturnableConnection, conn_senders: &WritingSenders);
184}
185
186#[async_trait]
187impl<W: Writing> WritingInternal for W {
188    async fn write_to_stream<A: AsyncWrite + Unpin + Send>(
189        &self,
190        message: Self::Message,
191        writer: &mut FramedWrite<A, Self::Codec>,
192    ) -> Result<usize, <Self::Codec as Encoder<Self::Message>>::Error> {
193        // Guard against write starvation. `feed` is covered as well as `flush`, as `FramedWrite`
194        // flushes from within `feed` once its buffer is over the backpressure boundary, so a peer
195        // that has stopped reading blocks `feed` too.
196        let write = async {
197            writer.feed(message).await?;
198            let len = writer.write_buffer().len();
199            writer.flush().await?;
200            Ok(len)
201        };
202        match timeout(W::TIMEOUT, write).await {
203            Ok(result) => result,
204            Err(_) => Err(io::Error::new(io::ErrorKind::TimedOut, "write timed out")),
205        }
206    }
207
208    async fn handle_new_connection(
209        &self,
210        (mut conn, conn_returner): ReturnableConnection,
211        conn_senders: &WritingSenders,
212    ) {
213        let addr = conn.addr();
214        let codec = self.codec(addr, !conn.side());
215        let writer = conn.writer.take().expect("missing connection writer!");
216        let mut framed = FramedWrite::new(writer, codec);
217
218        let (outbound_message_sender, mut outbound_message_receiver) = mpsc::channel(self.message_queue_depth());
219
220        // register the connection's message sender with the Writing protocol handler
221        conn_senders.write().insert(addr, outbound_message_sender);
222
223        // this will automatically drop the sender upon a disconnect
224        let sender_cleanup = SenderCleanup { addr, senders: Arc::clone(conn_senders) };
225
226        // use a channel to know when the writer task is ready
227        let (tx_writer, rx_writer) = oneshot::channel();
228
229        // the task for writing outbound messages
230        let self_clone = self.clone();
231        let conn_stats = Arc::clone(conn.stats());
232        let conn_span = conn.span().clone();
233        let writer_task = tokio::spawn(Box::pin(async move {
234            let node = self_clone.tcp();
235            trace!(parent: &conn_span, "spawned a task for writing messages");
236            tx_writer.send(()).unwrap(); // safe; the channel was just opened
237
238            // move the cleanup into the task that gets aborted on disconnect
239            let _sender_cleanup = sender_cleanup;
240
241            // disconnect automatically regardless of how this task concludes
242            let _conn_cleanup = DisconnectOnDrop::new(node.clone(), addr, DisconnectOrigin::Writing);
243
244            while let Some(wrapped_msg) = outbound_message_receiver.recv().await {
245                let msg = wrapped_msg.msg.downcast().unwrap();
246
247                match self_clone.write_to_stream(*msg, &mut framed).await {
248                    Ok(len) => {
249                        let _ = wrapped_msg.delivery_notification.send(Ok(()));
250                        conn_stats.register_sent_message(len);
251                        node.stats().register_sent_message(len);
252                        trace!(parent: &conn_span, "sent {len}B");
253                    }
254                    Err(e) => {
255                        error!(parent: &conn_span, "couldn't send a message: {e}");
256                        let _ = wrapped_msg.delivery_notification.send(Err(e));
257                        break;
258                    }
259                }
260            }
261        }));
262        let _ = rx_writer.await;
263        conn.tasks.push(writer_task);
264
265        // return the Connection to the Tcp, resuming Tcp::adapt_stream
266        if conn_returner.send(Ok(conn)).is_err() {
267            unreachable!("couldn't return a Connection to the Tcp");
268        }
269    }
270}
271
272/// Used to queue messages for delivery.
273struct WrappedMessage {
274    msg: Box<dyn Any + Send>,
275    delivery_notification: oneshot::Sender<io::Result<()>>,
276}
277
278impl WrappedMessage {
279    fn new(msg: Box<dyn Any + Send>) -> (Self, oneshot::Receiver<io::Result<()>>) {
280        let (tx, rx) = oneshot::channel();
281        let wrapped_msg = Self { msg, delivery_notification: tx };
282
283        (wrapped_msg, rx)
284    }
285}
286
287/// The handler object dedicated to the [`Writing`] protocol.
288pub(crate) struct WritingHandler {
289    handler: ProtocolHandler<Connection, io::Result<Connection>>,
290    senders: WritingSenders,
291}
292
293impl Protocol<Connection, io::Result<Connection>> for WritingHandler {
294    async fn trigger(&self, item: ReturnableConnection) {
295        self.handler.trigger(item).await;
296    }
297}
298
299struct SenderCleanup {
300    addr: SocketAddr,
301    senders: WritingSenders,
302}
303
304impl Drop for SenderCleanup {
305    fn drop(&mut self) {
306        self.senders.write().remove(&self.addr);
307    }
308}
309
310#[cfg(test)]
311mod tests {
312    use super::*;
313    use crate::{Config, ConnectionSide, Tcp, protocols::Reading};
314    use bytes::Bytes;
315    use std::net::{IpAddr, Ipv4Addr};
316    use tokio::time::Instant;
317    use tokio_util::codec::BytesCodec;
318
319    fn test_config() -> Config {
320        Config { listener_ip: Some(IpAddr::V4(Ipv4Addr::LOCALHOST)), ..Default::default() }
321    }
322
323    #[derive(Clone)]
324    struct TestNode(Tcp);
325    impl P2P for TestNode {
326        fn tcp(&self) -> &Tcp {
327            &self.0
328        }
329    }
330    #[async_trait]
331    impl Writing for TestNode {
332        type Codec = BytesCodec;
333        type Message = Bytes;
334
335        const TIMEOUT: Duration = Duration::from_millis(200);
336
337        fn codec(&self, _a: SocketAddr, _s: ConnectionSide) -> Self::Codec {
338            Default::default()
339        }
340    }
341    #[async_trait]
342    impl Reading for TestNode {
343        type Codec = BytesCodec;
344        type Message = bytes::BytesMut;
345
346        fn codec(&self, _a: SocketAddr, _s: ConnectionSide) -> Self::Codec {
347            Default::default()
348        }
349
350        async fn process_message(&self, _s: SocketAddr, _m: Self::Message) -> io::Result<()> {
351            Ok(())
352        }
353    }
354
355    fn flood(sender: &TestNode, peer: SocketAddr) {
356        let msg = Bytes::from(vec![0u8; 1024 * 1024]);
357        for _ in 0..64 {
358            if sender.unicast(peer, msg.clone()).is_err() {
359                break;
360            }
361        }
362    }
363    async fn disconnected(sender: &TestNode, peer: SocketAddr, within: Duration) -> bool {
364        let deadline = Instant::now() + within;
365        while Instant::now() < deadline {
366            if !sender.tcp().is_connected(peer) {
367                return true;
368            }
369            tokio::time::sleep(Duration::from_millis(50)).await;
370        }
371        false
372    }
373
374    #[tokio::test]
375    async fn stalled_peer_is_disconnected() {
376        let sender = TestNode(Tcp::new(test_config()));
377        sender.tcp().enable_listener().await.unwrap();
378        sender.enable_writing().await;
379        let receiver = Tcp::new(test_config());
380        let ip = receiver.enable_listener().await.unwrap();
381        sender.tcp().connect(ip).await.unwrap();
382        let peer = *sender.tcp().connected_addrs().first().unwrap();
383        flood(&sender, peer);
384        assert!(disconnected(&sender, peer, Duration::from_secs(10)).await, "stalled peer not disconnected");
385    }
386
387    #[tokio::test]
388    async fn reading_peer_is_kept() {
389        let sender = TestNode(Tcp::new(test_config()));
390        sender.tcp().enable_listener().await.unwrap();
391        sender.enable_writing().await;
392        let receiver = TestNode(Tcp::new(test_config()));
393        let ip = receiver.tcp().enable_listener().await.unwrap();
394        receiver.enable_reading().await;
395        sender.tcp().connect(ip).await.unwrap();
396        let peer = *sender.tcp().connected_addrs().first().unwrap();
397        flood(&sender, peer);
398        assert!(!disconnected(&sender, peer, Duration::from_secs(3)).await, "reading peer was disconnected");
399    }
400}