Skip to main content

beam/adapters/
ws_conn.rs

1//! Per-connection WebSocket actor — bridges Gun protocol messages over WebSocket.
2//!
3//! [`WsConn`] wraps a single WebSocket connection (either inbound via
4//! [`crate::adapters::WsServer`] or outbound via
5//! [`crate::adapters::OutgoingWebsocketManager`]). It:
6//!
7//! - Sends outgoing messages as `WsMessage::Text` on the WebSocket
8//! - Receives incoming messages, parses them via [`Message::try_from`], and
9//!   forwards to the [`crate::router::Router`]
10//! - Sends a `Message::Hi` on startup to register with the router
11//!
12//! # Lifecycle
13//!
14//! 1. `pre_start`: Send `Hi` message, spawn receive loop
15//! 2. `handle`: Forward outgoing messages to WebSocket
16//! 3. `stopping`: Log shutdown
17//!
18//! The receive loop runs as a child task and calls `ctx.stop()` when the
19//! WebSocket closes, triggering actor shutdown.
20
21use crate::actor::{Actor, ActorContext};
22use crate::message::Message;
23use futures_util::{
24    SinkExt, StreamExt,
25    stream::{SplitSink, SplitStream},
26};
27use std::sync::Arc;
28
29use async_trait::async_trait;
30
31use log::{debug, info};
32use web_time::Duration;
33
34use tokio_websockets::{Message as WsMessage, WebSocketStream};
35
36/// A per-connection WebSocket actor that bridges Gun protocol messages.
37///
38/// Created by [`crate::adapters::WsServer`] (inbound) or
39/// [`crate::adapters::OutgoingWebsocketManager`] (outbound). Each `WsConn`
40/// manages a single WebSocket connection and translates between the
41/// Gun wire format (text) and [`Message`] enum values.
42/// A per-connection WebSocket actor that bridges Gun protocol messages.
43///
44/// Generic over the underlying stream type `S` (plain `TcpStream` or
45/// `TlsStream<TcpStream>`). Created by [`crate::adapters::WsServer`]
46/// (inbound) or [`crate::adapters::OutgoingWebsocketManager`] (outbound).
47/// Each `WsConn` manages a single WebSocket connection and translates
48/// between the Gun wire format (text) and [`Message`] enum values.
49pub struct WsConn<S> {
50    /// Write half of the WebSocket (for sending messages in `handle`).
51    ws_sink: Option<SplitSink<WebSocketStream<S>, WsMessage>>,
52    /// Read half of the WebSocket (moved into receive loop in `pre_start`).
53    ws_stream: Option<SplitStream<WebSocketStream<S>>>,
54    allow_public_space: bool,
55    /// Reusable serialization buffer — eliminates per-message allocation.
56    send_buf: Vec<u8>,
57}
58
59impl<S> WsConn<S>
60where
61    S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send + 'static,
62{
63    /// Creates a new `WsConn` from a `WebSocketStream`.
64    pub fn new(ws: WebSocketStream<S>, allow_public_space: bool) -> Self {
65        let (sink, stream) = futures_util::StreamExt::split(ws);
66        Self {
67            ws_sink: Some(sink),
68            ws_stream: Some(stream),
69            allow_public_space,
70            send_buf: Vec::with_capacity(512),
71        }
72    }
73
74    /// Serialize a message into `send_buf` and feed it as a WS text frame
75    /// into the sink's internal buffer.
76    ///
77    /// With `flush_threshold(usize::MAX)` configured on the WebSocket
78    /// builder, `feed()` never triggers an implicit flush — it simply
79    /// queues the frame. The caller is responsible for calling
80    /// `flush()` after all messages are queued.
81    async fn send_msg(&mut self, msg: &Arc<Message>, ctx: &ActorContext) {
82        msg.to_writer(&mut self.send_buf);
83        ctx.metrics.record_serialization();
84        if let Some(sink) = &mut self.ws_sink {
85            // Transfer ownership of send_buf into the WS frame — zero copy.
86            // Replace with a fresh buffer for the next message.
87            let buf = std::mem::take(&mut self.send_buf);
88            let _ = sink
89                .feed(WsMessage::text(
90                    String::from_utf8(buf).expect("wire format is valid UTF-8"),
91                ))
92                .await;
93        }
94        ctx.metrics.record_ws_sent();
95    }
96
97    /// Flush the WS sink's internal buffer to the underlying TCP stream.
98    async fn flush_sink(&mut self) {
99        if let Some(sink) = &mut self.ws_sink {
100            let _ = sink.flush().await;
101        }
102    }
103}
104
105#[async_trait]
106impl<S> Actor for WsConn<S>
107where
108    S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send + 'static,
109{
110    /// Fallback single-message handler. Feeds one message then flushes.
111    /// Used when the actor runtime calls `handle` instead of `handle_batch`.
112    async fn handle(&mut self, msg: Arc<Message>, ctx: &ActorContext) {
113        self.send_msg(&msg, ctx).await;
114        self.flush_sink().await;
115    }
116
117    /// Batch handler — feeds all messages into the WS frame buffer without
118    /// flushing. Because `flush_threshold` is set to `usize::MAX`, `feed()`
119    /// never triggers an implicit flush and never blocks on I/O.
120    ///
121    /// Flushing is handled by a dedicated background task spawned in
122    /// `pre_start`, which calls `flush()` on a timer. This decouples the
123    /// actor's message processing from socket I/O — the actor never
124    /// suspends waiting for the TCP buffer to drain.
125    ///
126    /// # Cooperative Scheduling
127    ///
128    /// On `current_thread` runtime, `feed()` with `flush_threshold(MAX)`
129    /// completes without suspending, so a full batch of 64 messages can
130    /// be processed without yielding to other tasks. We call
131    /// `yield_now()` every 16 messages to ensure the relay's router and
132    /// other actors get scheduled. Without this, on `current_thread`, a
133    /// sender's WsConn can starve the relay's receive loop, causing a
134    /// deadlock where the relay never processes incoming puts.
135    async fn handle_batch(&mut self, batch: &mut Vec<Arc<Message>>, ctx: &ActorContext) {
136        if self.ws_sink.is_some() {
137            let mut count = 0;
138            for msg in batch.drain(..) {
139                self.send_msg(&msg, ctx).await;
140                count += 1;
141                if count % 16 == 0 {
142                    crate::tokio_spawn::yield_now().await;
143                }
144            }
145            // Single flush per batch — not per message. This is the key
146            // difference from `sink.send()` (which flushes per message).
147            // One flush per 64 messages dramatically reduces I/O syscalls
148            // while still delivering data to TCP promptly.
149            self.flush_sink().await;
150        } else {
151            batch.clear();
152        }
153    }
154
155    async fn pre_start(&mut self, ctx: &ActorContext) {
156        // Send Hi message to register with the relay.
157        let hi = Message::Hi {
158            from: ctx.addr.clone(),
159            peer_id: ctx.peer_id.read().clone(),
160        };
161        hi.to_writer(&mut self.send_buf);
162        ctx.metrics.record_serialization();
163        if let Some(sink) = &mut self.ws_sink {
164            let buf = std::mem::take(&mut self.send_buf);
165            let _ = sink
166                .feed(WsMessage::text(
167                    String::from_utf8(buf).expect("wire format is valid UTF-8"),
168                ))
169                .await;
170        }
171        ctx.metrics.record_ws_sent();
172        self.flush_sink().await;
173
174        // Move the read half into a child task for the receive loop.
175        let reader = self.ws_stream.take().expect("ws_stream already taken");
176        let ctx2 = ctx.clone();
177        let allow_public_space = self.allow_public_space;
178        ctx.child_task(async move {
179            let mut reader = reader;
180            while let Some(result) = reader.next().await {
181                let ws_msg = match result {
182                    Ok(m) => m,
183                    Err(_e) => {
184                        break;
185                    }
186                };
187                if ws_msg.is_text() {
188                    let text = ws_msg.as_text().unwrap_or("");
189                    if text.is_empty() {
190                        continue;
191                    }
192                    ctx2.metrics.record_ws_received();
193                    match Message::try_from(text, ctx2.addr.clone(), allow_public_space) {
194                        Ok(msgs) => {
195                            ctx2.metrics.record_parsed();
196                            for msg in msgs {
197                                let _ = ctx2.router.read().send(msg);
198                            }
199                        }
200                        Err(e) => {
201                            debug!("[WS] parse error: {} (len={})", e, text.len());
202                        }
203                    }
204                } else if ws_msg.is_binary() {
205                    debug!("[WS] binary frame (ignored)");
206                } else if ws_msg.is_close() {
207                    debug!("[WS] close frame received from peer");
208                    break;
209                } else if ws_msg.is_ping() {
210                    debug!("[WS] ping frame (ignored)");
211                } else if ws_msg.is_pong() {
212                    debug!("[WS] pong frame (ignored)");
213                }
214            }
215            debug!("[WS] receive loop ended — stopping actor");
216            ctx2.stop();
217        });
218    }
219
220    async fn stopping(&mut self, _context: &ActorContext) {
221        info!("WsConn stopping — sending WebSocket Close frame");
222        if let Some(sink) = &mut self.ws_sink {
223            let close_result =
224                crate::tokio_time::timeout(Duration::from_secs(2), sink.close()).await;
225            match close_result {
226                Ok(Ok(())) => debug!("WsConn Close frame acknowledged"),
227                Ok(Err(e)) => debug!("WsConn Close error (non-fatal): {}", e),
228                Err(_) => debug!("WsConn Close timed out — connection dropped"),
229            }
230        }
231    }
232}