Skip to main content

wsio_server/namespace/
mod.rs

1use std::sync::Arc;
2
3use anyhow::{
4    Result,
5    anyhow,
6};
7use arc_swap::ArcSwap;
8use futures_util::{
9    SinkExt,
10    StreamExt,
11};
12use http::{
13    HeaderMap,
14    Uri,
15};
16use hyper::upgrade::{
17    OnUpgrade,
18    Upgraded,
19};
20use hyper_util::rt::TokioIo;
21use kikiutils::{
22    atomic::enum_cell::AtomicEnumCell,
23    types::fx_collections::FxDashMap,
24};
25use num_enum::{
26    IntoPrimitive,
27    TryFromPrimitive,
28};
29use roaring::RoaringTreemap;
30use serde::Serialize;
31use tokio::{
32    join,
33    select,
34    spawn,
35    time::timeout,
36};
37use tokio_tungstenite::{
38    WebSocketStream,
39    tungstenite::{
40        Message,
41        protocol::Role,
42    },
43};
44use tokio_util::task::TaskTracker;
45
46pub(crate) mod builder;
47mod config;
48pub mod operators;
49
50use self::{
51    config::WsIoServerNamespaceConfig,
52    operators::broadcast::WsIoServerNamespaceBroadcastOperator,
53};
54use crate::{
55    WsIoServer,
56    connection::WsIoServerConnection,
57    core::{
58        packet::WsIoPacket,
59        utils::lifecycle::WsIoLifecycleCompletionSlot,
60    },
61    runtime::{
62        WsIoServerRuntime,
63        WsIoServerRuntimeStatus,
64    },
65};
66
67// Enums
68#[repr(u8)]
69#[derive(Debug, Eq, IntoPrimitive, PartialEq, TryFromPrimitive)]
70enum NamespaceStatus {
71    Running,
72    Stopped,
73    Stopping,
74}
75
76// Structs
77#[derive(Debug)]
78pub struct WsIoServerNamespace {
79    pub(crate) config: WsIoServerNamespaceConfig,
80    connection_ids: ArcSwap<RoaringTreemap>,
81    connections: FxDashMap<u64, Arc<WsIoServerConnection>>,
82    connection_task_tracker: TaskTracker,
83    rooms: FxDashMap<String, RoaringTreemap>,
84    runtime: Arc<WsIoServerRuntime>,
85    shutdown_completion: Arc<WsIoLifecycleCompletionSlot>,
86    status: AtomicEnumCell<NamespaceStatus>,
87}
88
89impl WsIoServerNamespace {
90    fn new(config: WsIoServerNamespaceConfig, runtime: Arc<WsIoServerRuntime>) -> Arc<Self> {
91        Arc::new(Self {
92            config,
93            connection_ids: ArcSwap::new(Arc::new(RoaringTreemap::new())),
94            connections: FxDashMap::default(),
95            connection_task_tracker: TaskTracker::new(),
96            rooms: FxDashMap::default(),
97            runtime,
98            shutdown_completion: Arc::new(WsIoLifecycleCompletionSlot::default()),
99            status: AtomicEnumCell::new(NamespaceStatus::Running),
100        })
101    }
102
103    // Private methods
104    async fn handle_upgraded_request(
105        self: &Arc<Self>,
106        headers: HeaderMap,
107        request_uri: Uri,
108        upgraded: Upgraded,
109    ) -> Result<()> {
110        #[cfg(feature = "tracing")]
111        tracing::debug!(
112            namespace = self.config.path,
113            request_path = request_uri.path(),
114            "handling upgraded WebSocket request"
115        );
116
117        // Create ws stream
118        let mut ws_stream =
119            WebSocketStream::from_raw_socket(TokioIo::new(upgraded), Role::Server, Some(self.config.websocket_config))
120                .await;
121
122        // Check runtime and namespace status
123        if !self.runtime.status.is(WsIoServerRuntimeStatus::Running) || !self.status.is(NamespaceStatus::Running) {
124            #[cfg(feature = "tracing")]
125            tracing::debug!(
126                namespace = self.config.path,
127                runtime_status = ?self.runtime.status.get(),
128                namespace_status = ?self.status.get(),
129                "rejecting upgraded request because server or namespace is not running"
130            );
131
132            ws_stream
133                .send((*self.encode_packet_to_message(&WsIoPacket::new_disconnect()).await?).clone())
134                .await?;
135
136            let _ = ws_stream.close(None).await;
137            return Ok(());
138        }
139
140        // Create connection
141        let (connection, mut message_rx, event_queue_rx) =
142            WsIoServerConnection::new(headers, Arc::clone(self), request_uri);
143
144        connection.start_event_dispatcher(event_queue_rx).await;
145
146        #[cfg(feature = "tracing")]
147        tracing::debug!(
148            namespace = self.config.path,
149            connection_id = connection.id(),
150            "accepted WebSocket connection"
151        );
152
153        // Split ws stream and spawn read and write tasks
154        let (mut ws_stream_writer, mut ws_stream_reader) = ws_stream.split();
155        let connection_clone = Arc::clone(&connection);
156        let mut read_ws_stream_task = spawn(async move {
157            while let Some(message) = ws_stream_reader.next().await {
158                if match message {
159                    Ok(Message::Binary(bytes)) => {
160                        // Treat any single-byte binary frame as a client heartbeat and ignore it
161                        if bytes.len() == 1 {
162                            continue;
163                        }
164
165                        connection_clone.handle_incoming_packet(bytes).await
166                    },
167                    Ok(Message::Close(_)) => {
168                        #[cfg(feature = "tracing")]
169                        tracing::debug!(connection_id = connection_clone.id(), "server read task received close frame");
170                        break;
171                    },
172                    Err(_err) => {
173                        #[cfg(feature = "tracing")]
174                        tracing::debug!(connection_id = connection_clone.id(), error = %_err, "server read task failed");
175                        break;
176                    },
177                    Ok(Message::Text(_)) => Err(anyhow!("text WebSocket frames are not supported")),
178                    _ => Ok(()),
179                }
180                .is_err()
181                {
182                    #[cfg(feature = "tracing")]
183                    tracing::debug!(
184                        connection_id = connection_clone.id(),
185                        "server read task stopped after packet handling error"
186                    );
187
188                    break;
189                }
190            }
191        });
192
193        let mut write_ws_stream_task = spawn(async move {
194            while let Some(message) = message_rx.recv().await {
195                let message = (*message).clone();
196                let is_close = matches!(message, Message::Close(_));
197                if ws_stream_writer.send(message).await.is_err() {
198                    #[cfg(feature = "tracing")]
199                    tracing::debug!("server write task failed to send message");
200                    break;
201                }
202
203                if is_close {
204                    #[cfg(feature = "tracing")]
205                    tracing::debug!("server write task sent close frame");
206                    let _ = ws_stream_writer.close().await;
207                    break;
208                }
209            }
210        });
211
212        // Try to init connection
213        match connection.init().await {
214            Ok(()) => {
215                #[cfg(feature = "tracing")]
216                tracing::debug!(connection_id = connection.id(), "server connection initialized");
217                // Wait for either read or write task to finish
218                select! {
219                    _ = &mut read_ws_stream_task => {
220                        #[cfg(feature = "tracing")]
221                        tracing::debug!(connection_id = connection.id(), "server read task finished; aborting write task");
222                        write_ws_stream_task.abort();
223                        let _ = write_ws_stream_task.await;
224                    },
225                    _ = &mut write_ws_stream_task => {
226                        #[cfg(feature = "tracing")]
227                        tracing::debug!(connection_id = connection.id(), "server write task finished; aborting read task");
228                        read_ws_stream_task.abort();
229                        let _ = read_ws_stream_task.await;
230                    },
231                }
232            },
233            Err(_err) => {
234                #[cfg(feature = "tracing")]
235                tracing::debug!(connection_id = connection.id(), error = %_err, "server connection initialization failed");
236                // Close connection
237                read_ws_stream_task.abort();
238                connection.close();
239                let _ = join!(read_ws_stream_task, write_ws_stream_task);
240            },
241        }
242
243        // Cleanup connection
244        connection.cleanup().await;
245
246        #[cfg(feature = "tracing")]
247        tracing::debug!(connection_id = connection.id(), "server connection stopped");
248        Ok(())
249    }
250
251    async fn shutdown_inner(self: &Arc<Self>) {
252        match self.status.get() {
253            NamespaceStatus::Stopped => return,
254            NamespaceStatus::Running => {
255                #[cfg(feature = "tracing")]
256                tracing::info!(namespace = self.config.path, "shutting down namespace");
257                self.status.store(NamespaceStatus::Stopping);
258            },
259            NamespaceStatus::Stopping => unreachable!(),
260        }
261
262        self.close_all().await;
263        self.connection_task_tracker.close();
264        self.connection_task_tracker.wait().await;
265
266        self.status.store(NamespaceStatus::Stopped);
267
268        #[cfg(feature = "tracing")]
269        tracing::info!(namespace = self.config.path, "namespace stopped");
270    }
271
272    // Protected methods
273    #[inline]
274    pub(crate) fn add_connection_id_to_room(&self, room_name: &str, connection_id: u64) {
275        self.rooms
276            .entry(room_name.to_owned())
277            .or_default()
278            .insert(connection_id);
279    }
280
281    #[inline]
282    pub(crate) async fn encode_packet_to_message(&self, packet: &WsIoPacket) -> Result<Arc<Message>> {
283        let bytes = self.config.packet_codec.encode(packet)?;
284        let bytes = self.config.packet_transformer.encode(bytes).await?;
285        Ok(Arc::new(Message::Binary(bytes)))
286    }
287
288    #[inline]
289    pub(crate) fn handle_on_upgrade_request(
290        self: &Arc<Self>,
291        headers: HeaderMap,
292        on_upgrade: OnUpgrade,
293        request_uri: Uri,
294    ) {
295        #[cfg(feature = "tracing")]
296        tracing::trace!(
297            namespace = self.config.path,
298            request_path = request_uri.path(),
299            "spawning WebSocket upgrade task"
300        );
301
302        let namespace = Arc::clone(self);
303        self.connection_task_tracker.spawn(async move {
304            match timeout(namespace.config.http_request_upgrade_timeout, on_upgrade).await {
305                Ok(Ok(upgraded)) => {
306                    if let Err(_err) = namespace.handle_upgraded_request(headers, request_uri, upgraded).await {
307                        #[cfg(feature = "tracing")]
308                        tracing::debug!(namespace = namespace.config.path, error = %_err, "upgraded request handling failed");
309                    }
310                },
311                Ok(Err(_err)) => {
312                    #[cfg(feature = "tracing")]
313                    tracing::warn!(namespace = namespace.config.path, error = %_err, "HTTP upgrade failed");
314                },
315                Err(_err) => {
316                    #[cfg(feature = "tracing")]
317                    tracing::warn!(
318                        namespace = namespace.config.path,
319                        error = %_err,
320                        timeout_ms = u64::try_from(namespace.config.http_request_upgrade_timeout.as_millis())
321                            .unwrap_or(u64::MAX),
322                        "HTTP upgrade timed out"
323                    );
324                },
325            }
326        });
327    }
328
329    #[inline]
330    pub(crate) fn insert_connection(&self, connection: &Arc<WsIoServerConnection>) {
331        self.connections.insert(connection.id(), Arc::clone(connection));
332        self.runtime.insert_connection_id(connection.id());
333        self.connection_ids.rcu(|old_connection_ids| {
334            let mut new_connection_ids = (**old_connection_ids).clone();
335            new_connection_ids.insert(connection.id());
336            new_connection_ids
337        });
338    }
339
340    #[inline]
341    pub(crate) fn remove_connection(&self, id: u64) {
342        self.connections.remove(&id);
343        self.runtime.remove_connection_id(id);
344        self.connection_ids.rcu(|old_connection_ids| {
345            let mut new_connection_ids = (**old_connection_ids).clone();
346            new_connection_ids.remove(id);
347            new_connection_ids
348        });
349    }
350
351    #[inline]
352    pub(crate) fn remove_connection_id_from_room(&self, room_name: &str, connection_id: u64) {
353        if let Some(mut entry) = self.rooms.get_mut(room_name) {
354            entry.remove(connection_id);
355        }
356
357        self.rooms.remove_if(room_name, |_, entry| entry.is_empty());
358    }
359
360    // Public methods
361    pub async fn close_all(self: &Arc<Self>) {
362        WsIoServerNamespaceBroadcastOperator::new(Arc::clone(self))
363            .close()
364            .await;
365    }
366
367    #[inline]
368    pub fn connection_count(&self) -> usize {
369        self.connections.len()
370    }
371
372    pub async fn disconnect_all(self: &Arc<Self>) -> Result<()> {
373        WsIoServerNamespaceBroadcastOperator::new(Arc::clone(self))
374            .disconnect()
375            .await
376    }
377
378    pub async fn emit<D: Serialize>(self: &Arc<Self>, event: impl AsRef<str>, data: Option<&D>) -> Result<()> {
379        WsIoServerNamespaceBroadcastOperator::new(Arc::clone(self))
380            .emit(event, data)
381            .await
382    }
383
384    #[inline]
385    pub fn except(
386        self: &Arc<Self>,
387        room_names: impl IntoIterator<Item = impl Into<String>>,
388    ) -> WsIoServerNamespaceBroadcastOperator {
389        WsIoServerNamespaceBroadcastOperator::new(Arc::clone(self)).except(room_names)
390    }
391
392    #[inline]
393    pub fn path(&self) -> &str {
394        &self.config.path
395    }
396
397    #[inline]
398    pub fn server(&self) -> WsIoServer {
399        WsIoServer(Arc::clone(&self.runtime))
400    }
401
402    pub async fn shutdown(self: &Arc<Self>) {
403        let namespace = Arc::clone(self);
404        self.shutdown_completion
405            .wait_or_spawn(move || async move { namespace.shutdown_inner().await })
406            .await;
407    }
408
409    #[inline]
410    pub fn to(
411        self: &Arc<Self>,
412        room_names: impl IntoIterator<Item = impl Into<String>>,
413    ) -> WsIoServerNamespaceBroadcastOperator {
414        WsIoServerNamespaceBroadcastOperator::new(Arc::clone(self)).to(room_names)
415    }
416}
417
418#[cfg(test)]
419mod tests {
420    use std::time::Duration;
421
422    use tokio::{
423        task::yield_now,
424        time::sleep,
425    };
426
427    use super::*;
428    use crate::WsIoServer;
429
430    fn create_test_namespace() -> Arc<WsIoServerNamespace> {
431        WsIoServer::builder()
432            .build()
433            .new_namespace_builder("/test")
434            .register()
435            .unwrap()
436    }
437
438    #[test]
439    fn test_namespace_new() {
440        let namespace = create_test_namespace();
441        assert_eq!(namespace.path(), "/test");
442        assert_eq!(namespace.connection_count(), 0);
443    }
444
445    #[test]
446    fn test_namespace_add_remove_connection_id_to_room() {
447        let namespace = create_test_namespace();
448        namespace.add_connection_id_to_room("room1", 1);
449        namespace.add_connection_id_to_room("room1", 2);
450        namespace.add_connection_id_to_room("room2", 3);
451
452        assert_eq!(namespace.rooms.get("room1").unwrap().len(), 2);
453        assert!(namespace.rooms.get("room1").unwrap().contains(1));
454        assert!(namespace.rooms.get("room1").unwrap().contains(2));
455        assert_eq!(namespace.rooms.get("room2").unwrap().len(), 1);
456
457        namespace.remove_connection_id_from_room("room1", 1);
458        assert_eq!(namespace.rooms.get("room1").unwrap().len(), 1);
459        assert!(namespace.rooms.get("room1").unwrap().contains(2));
460
461        namespace.remove_connection_id_from_room("room1", 2);
462        namespace.remove_connection_id_from_room("room2", 3);
463
464        assert!(!namespace.rooms.contains_key("room1"));
465        assert!(!namespace.rooms.contains_key("room2"));
466    }
467
468    #[tokio::test]
469    async fn test_namespace_encode_packet_to_message() {
470        let namespace = create_test_namespace();
471        let packet = WsIoPacket::new_disconnect();
472        let message = namespace.encode_packet_to_message(&packet).await.unwrap();
473
474        assert!(matches!(&*message, Message::Binary(_)));
475    }
476
477    #[tokio::test]
478    async fn test_namespace_shutdown_idempotent() {
479        let namespace = create_test_namespace();
480        Arc::clone(&namespace).shutdown().await;
481        // Shutting down again should be safe
482        namespace.shutdown().await;
483    }
484
485    #[tokio::test]
486    async fn test_namespace_concurrent_shutdown_is_idempotent() {
487        let namespace = create_test_namespace();
488        namespace.connection_task_tracker.spawn(async {
489            sleep(Duration::from_millis(10)).await;
490        });
491
492        let first = Arc::clone(&namespace);
493        let first_task = spawn(async move { first.shutdown().await });
494        while !namespace.status.is(NamespaceStatus::Stopping) {
495            yield_now().await;
496        }
497
498        namespace.shutdown().await;
499        first_task.await.unwrap();
500    }
501
502    #[tokio::test]
503    async fn test_broadcast_operator_disconnect_with_no_connections() {
504        let namespace = create_test_namespace();
505        namespace.to(["room1"]).disconnect().await.unwrap();
506    }
507
508    #[tokio::test]
509    async fn test_broadcast_operator_emit_requires_running() {
510        let namespace = create_test_namespace();
511        // Shutdown to make status invalid
512        Arc::clone(&namespace).shutdown().await;
513
514        let err = namespace
515            .to(["room1"])
516            .emit("event", Option::<&()>::None)
517            .await
518            .unwrap_err();
519
520        assert!(err.to_string().contains("invalid status"));
521    }
522}