Skip to main content

hydra_sync/
server.rs

1//! Core implementation for the `HydraSync` server.
2use crate::channel::{ClosedError, FullError};
3use crate::crypto::{NONCE_LEN, TAG_LEN};
4use crate::protocol::StatusCode;
5use crate::protocol::{
6    Role, perform_server_handshake, read_join_header, write_server_read_write_len,
7    write_status_code,
8};
9use crate::session::SessionMap;
10use crate::{ChannelOverflowStrategy, error, info, trace, warn};
11use anyhow::Result;
12use bytes::BytesMut;
13use hex::encode;
14use std::net::SocketAddr;
15use std::sync::Arc;
16use std::sync::atomic::{AtomicU64, Ordering};
17use std::time::Duration;
18use tokio::io::{AsyncReadExt, AsyncWriteExt};
19use tokio::net::{TcpListener, TcpStream};
20use tokio::sync::Notify;
21
22/// `HydraServer`- A light-weight, E2E `(AES-GCM)`, multi-threaded SPMC broadcast server using `TCP`.
23/// It implements internal _ring buffer_ configured by [`HydraConfig`],
24/// uses **little memory** and **minimal copy** as possible.
25///
26/// ```no_run
27/// use hydra_sync::server::HydraServer;
28///
29/// #[tokio::main]
30/// async fn main() {
31///     let (server, addr) = HydraServer::bind_default().await.unwrap();
32///     println!("Server running on: {}", addr);
33///     tokio::spawn(async move { server.run().await });
34/// }
35/// ```
36pub struct HydraServer {
37    listener: TcpListener,
38    config: Arc<HydraConfig>,
39    sessions: Arc<SessionMap>,
40}
41
42/// Configuration for the `HydraServer` features.
43pub struct HydraConfig {
44    /// Track the number of active connections to the server.
45    curr_connections: Arc<AtomicU64>,
46    /// Maximum number of concurrent connections allowed.
47    pub max_connections: u64,
48    /// Timeout in milliseconds for accepting new connections when capacity is reached.
49    pub accept_timeout: u64,
50    /// Fixed length of a single packet/frame in bytes to read/write `(excluding AES-GCM overhead)`.
51    /// This length is used to `read/write EXACT` data from/to the network stream.
52    pub fixed_packet_length: u64,
53    /// Capacity of the internal channel for each session.
54    pub channel_capacity: u64,
55    /// Strategy for handling channel overflow when consumers cannot keep up with producers.
56    pub overflow_mode: ChannelOverflowStrategy,
57}
58
59impl HydraConfig {
60    /// Creates `HydraConfig` with the specified parameters.
61    /// > NOTE: `fixed_packet_length` is the length of the plaintext data, the actual read/write length will be `fixed_packet_length + AES-GCM overhead (28 bytes)`.
62    /// > So this function adds the AES-GCM overhead to the `fixed_packet_length` (make sure to consider for it, if `HydraConfig` is constructed without calling this function).
63    pub fn config(
64        max_connections: u64,
65        accept_timeout: u64,
66        fixed_packet_length: u64,
67        channel_capacity: u64,
68        overflow_mode: ChannelOverflowStrategy,
69    ) -> Self {
70        if fixed_packet_length == 0 {
71            panic!("fixed_packet_length must be greater than 0");
72        }
73        Self {
74            curr_connections: Arc::new(AtomicU64::new(0)),
75            max_connections,
76            accept_timeout,
77            fixed_packet_length: fixed_packet_length + (NONCE_LEN + TAG_LEN) as u64, // add aes-gcm overhead
78            channel_capacity,
79            overflow_mode,
80        }
81    }
82}
83
84impl Default for HydraConfig {
85    /// Default HydraConfig
86    /// - max_connections: 32
87    /// - accept_timeout: 500 ms
88    /// - fixed_packet_length: 4 MB + `AES-GCM overhead (28 bytes)`
89    /// - channel_capacity: 256
90    /// - overflow_mode: DropPacket
91    fn default() -> Self {
92        Self {
93            curr_connections: Arc::new(AtomicU64::new(0)),
94            max_connections: 32,
95            accept_timeout: 500,
96            fixed_packet_length: (4 << 20) + (NONCE_LEN + TAG_LEN) as u64, // 4 MB + AES-GCM overhead
97            channel_capacity: 256,
98            overflow_mode: ChannelOverflowStrategy::DropPacket,
99        }
100    }
101}
102
103impl HydraServer {
104    /// Binds the server with random local port and default [`HydraConfig`], returns the port.
105    pub async fn bind_default() -> Result<(Self, SocketAddr)> {
106        let addr = "127.0.0.1:0".parse::<SocketAddr>()?;
107        let (server, server_addr) = HydraServer::bind(addr, HydraConfig::default()).await?;
108        Ok((server, server_addr))
109    }
110
111    /// Binds the server to the specified [`SocketAddr`] with the provided [`HydraConfig`].
112    pub async fn bind(
113        socket_addr: SocketAddr,
114        hydra_config: HydraConfig,
115    ) -> Result<(Self, SocketAddr)> {
116        let server_tcp = TcpListener::bind(socket_addr).await?;
117        let server_addr = server_tcp.local_addr()?;
118        Ok((
119            Self {
120                listener: server_tcp,
121                config: Arc::new(hydra_config),
122                sessions: Arc::new(SessionMap::init()),
123            },
124            server_addr,
125        ))
126    }
127
128    /// Starts the server, accepting incoming connections and handling them concurrently.
129    /// Overview:
130    /// * Accept incoming `TCP connections`, spawn handler tasks and `allocate read/write buffers` or reject if max connections reached.
131    /// * Perform handshake, read/decrypt `JoinHeader`, determine role (Producer/Consumer/Admin).
132    /// * For Producers: Create valid session, read data (exactly **fixed_packet_length**, including `AES-GCM` (28 bytes) overhead, with `read_exact()`, so it may truncate)
133    ///   and broadcast `(cheap-copy)` to all Consumers in the session (Write behavior is configured by [`ChannelOverflowStrategy`]).
134    /// * For Consumers: Validate session, read from `local ring buffer` from producer handle, write to TCP stream.
135    /// * `LOG_LEVEL` & `LOG_FILE_DIR` env vars can be set to control `logging verbosity` and output file (defaults to `info` level and stdout and file).
136    /// * EOF check are gracefully handled by closing the connection without logging an error.
137    pub async fn run(&self) -> Result<()> {
138        loop {
139            if self.config.curr_connections.fetch_add(1, Ordering::Relaxed)
140                >= self.config.max_connections
141            {
142                self.config.curr_connections.fetch_sub(1, Ordering::Relaxed);
143                warn!(
144                    "Max connections reached: {}, waiting {} ms before accepting new connections",
145                    self.config.max_connections, self.config.accept_timeout
146                );
147                tokio::time::sleep(Duration::from_millis(self.config.accept_timeout)).await;
148                continue;
149            }
150
151            match self.listener.accept().await {
152                Ok((stream, peer_addr)) => {
153                    stream.set_nodelay(true).ok();
154                    let connections = Arc::clone(&self.config.curr_connections);
155                    let sessions = Arc::clone(&self.sessions);
156                    let read_write_len = self.config.fixed_packet_length;
157                    let channel_capacity = self.config.channel_capacity;
158                    let overflow_mode = self.config.overflow_mode;
159                    tokio::spawn(async move {
160                        trace!("Accepted connection from: {}", peer_addr);
161                        if let Err(e) = Self::handle_connection(
162                            stream,
163                            read_write_len,
164                            channel_capacity,
165                            overflow_mode,
166                            sessions,
167                        )
168                        .await
169                        {
170                            error!("Connection handling error: {} from: {}", e, peer_addr);
171                        }
172                        connections.fetch_sub(1, Ordering::Release);
173                    });
174                }
175                Err(e) => {
176                    self.config.curr_connections.fetch_sub(1, Ordering::Release);
177                    error!("Connection accepting error: {}", e);
178                }
179            }
180        }
181    }
182
183    /// Handles an individual client connection.
184    async fn handle_connection(
185        mut stream: TcpStream,
186        read_write_len: u64,
187        channel_capacity: u64,
188        on_channel_overflow: ChannelOverflowStrategy,
189        sessions: Arc<SessionMap>,
190    ) -> Result<()> {
191        stream.set_nodelay(true).ok();
192        let client_addr = stream.peer_addr()?;
193        let (mut read_h, mut writer_h) = stream.split();
194        let mut read_buffer = BytesMut::zeroed(read_write_len as usize);
195        let transport_key = perform_server_handshake(&mut read_h, &mut writer_h).await?;
196        let join_header = read_join_header(&mut read_h, &transport_key, &mut read_buffer).await?;
197
198        // determine role and handle accordingly
199        let role = match Role::from_u8(join_header.role) {
200            Ok(role) => {
201                info!(
202                    "Client (ip: {}) joined with role: {}, session_uuid: {}...",
203                    client_addr,
204                    role.to_string(),
205                    &encode(join_header.session_id)[..12]
206                );
207                role
208            }
209            Err(_) => {
210                warn!(
211                    "Client (ip: {}) joining with invalid role: {:#04x}",
212                    client_addr, join_header.role
213                );
214                write_status_code(
215                    &mut writer_h,
216                    StatusCode::ErrInvalidRole,
217                    &transport_key,
218                    &mut read_buffer,
219                )
220                .await?;
221                return Ok(());
222            }
223        };
224
225        // handle roles
226        match role {
227            Role::Admin => unimplemented!(),
228            Role::Producer => {
229                // create session for producer, if already exists, return error
230                if !sessions.try_create_session(join_header.session_id) {
231                    warn!(
232                        "Producer (ip: {}) failed to create session_uuid: {}...",
233                        client_addr,
234                        &encode(join_header.session_id)[..12]
235                    );
236                    // send error code
237                    write_status_code(
238                        &mut writer_h,
239                        StatusCode::ErrSessionAlreadyOccupied,
240                        &transport_key,
241                        &mut read_buffer,
242                    )
243                    .await?;
244                    return Ok(());
245                }
246
247                info!(
248                    "Producer (ip: {}) created session_uuid: {}...",
249                    client_addr,
250                    &encode(join_header.session_id)[..12]
251                );
252                // on successful session creation,
253                // send read_len to producer
254                write_status_code(
255                    &mut writer_h,
256                    StatusCode::Success,
257                    &transport_key,
258                    &mut read_buffer,
259                )
260                .await?;
261                write_server_read_write_len(
262                    &mut writer_h,
263                    read_write_len - (NONCE_LEN + TAG_LEN) as u64, // announce raw payload capacity (encryption overhead is internal)
264                    &transport_key,
265                    &mut read_buffer,
266                )
267                .await?;
268
269                // read from producer and write to all consumers with fixed read_write len
270                let mut write_buffer = BytesMut::zeroed(read_write_len as usize);
271                let mut dead_consumers = vec![]; // 'client to drop' collection
272                let mut pending_consumers = vec![]; // 'slow client' collection
273
274                loop {
275                    // react_exact to fill buf, this does not extend buf anyway,
276                    // this will return an error if the stream is closed or if it cannot fill the buffer exactly
277                    // 'reads' may get truncated and may send corrupted data to consumers
278                    match read_h.read_exact(&mut write_buffer).await {
279                        Ok(_) => {
280                            let data = write_buffer.split().freeze(); // zero-copy 'freeze' to Bytes
281
282                            // get session ref (TODO; get read lock, maybe block other map shards? idk)
283                            if let Some(session) = sessions.try_get_session(join_header.session_id)
284                            {
285                                // non-blocking pass over every consumer ring,
286                                // fast consumers are served immediately, regardless of where a
287                                // slow one sits in the (arbitrary) DashMap iteration order
288                                for mut entry in session.consumer_local_buffers.iter_mut() {
289                                    // rm consumers that disconnected and their rings
290                                    if entry.value_mut().is_closed() {
291                                        dead_consumers.push(*entry.key());
292                                        continue;
293                                    }
294                                    // broadcast away
295                                    match entry.value_mut().push(data.clone()) {
296                                        Ok(()) => {}
297                                        Err(FullError) => match on_channel_overflow {
298                                            ChannelOverflowStrategy::DropPacket => {
299                                                // skip, don't push latest packet to local consumer queue
300                                            }
301                                            ChannelOverflowStrategy::DropClient => {
302                                                // collect the 'lagged' client
303                                                dead_consumers.push(*entry.key());
304                                            }
305                                            ChannelOverflowStrategy::BackPressure => {
306                                                // collect all slow consumer
307                                                pending_consumers.push(*entry.key());
308                                            }
309                                        },
310                                    }
311                                }
312
313                                // blocking delivery pass for full rings only,
314                                // parks until space frees up; woken by pop() or by the
315                                // consumer disconnecting by ClosedError
316                                for id in pending_consumers.drain(..) {
317                                    loop {
318                                        enum Step {
319                                            Pushed,
320                                            Remove,
321                                            Wait(Arc<Notify>),
322                                        }
323                                        let step: Step = 'lock: {
324                                            // get lock on the consumer ring, or remove the entry
325                                            let Some(mut entry) =
326                                                session.consumer_local_buffers.get_mut(&id)
327                                            else {
328                                                break 'lock Step::Remove;
329                                            };
330                                            // check if closed, or remove
331                                            let p = entry.value_mut();
332                                            if p.is_closed() {
333                                                break 'lock Step::Remove;
334                                            }
335                                            // TODO: clone inside a loop?!
336                                            match p.push(data.clone()) {
337                                                Ok(()) => Step::Pushed, // set 'we are done, exit outside'
338                                                Err(FullError) => Step::Wait(p.space_notify()), // set 'we wait outside'
339                                            }
340                                        };
341                                        match step {
342                                            Step::Pushed => break,
343                                            Step::Remove => {
344                                                dead_consumers.push(id);
345                                                break;
346                                            }
347                                            // no lock held here, the guard was dropped at 'lock
348                                            Step::Wait(notify) => notify.notified().await,
349                                        }
350                                    }
351                                }
352
353                                // remove dead consumer queues
354                                for id in dead_consumers.drain(..) {
355                                    session.consumer_local_buffers.remove(&id);
356                                }
357                            }
358
359                            write_buffer.resize(read_write_len as usize, 0);
360                        }
361                        Err(_) => {
362                            error!(
363                                "Producer (ip: {}) read error, closing connection for session_uuid: {}...",
364                                client_addr,
365                                &encode(join_header.session_id)[..12]
366                            );
367                            break;
368                        }
369                    }
370                }
371
372                info!(
373                    "Removing session_uuid: {}...",
374                    &encode(join_header.session_id)[..12]
375                );
376
377                // remove session and return (close stream) on disconnect or error
378                sessions.remove_session(join_header.session_id);
379            }
380
381            Role::Consumer => {
382                // check session exists, if not, return error
383                let session = match sessions.try_get_session(join_header.session_id) {
384                    Some(s) => s,
385                    None => {
386                        warn!(
387                            "Consumer (ip: {}) failed to join session_uuid: {}...",
388                            client_addr,
389                            &encode(join_header.session_id)[..12]
390                        );
391                        write_status_code(
392                            &mut writer_h,
393                            StatusCode::ErrSessionNotFound,
394                            &transport_key,
395                            &mut read_buffer,
396                        )
397                        .await?;
398                        return Ok(());
399                    }
400                };
401
402                info!(
403                    "Consumer (ip: {}) joined session_uuid: {}...",
404                    client_addr,
405                    &encode(join_header.session_id)[..12]
406                );
407                // on success, send write_len to consumer
408                write_status_code(
409                    &mut writer_h,
410                    StatusCode::Success,
411                    &transport_key,
412                    &mut read_buffer,
413                )
414                .await?;
415                write_server_read_write_len(
416                    &mut writer_h,
417                    read_write_len - (NONCE_LEN + TAG_LEN) as u64, // announce raw payload capacity (encryption overhead is internal)
418                    &transport_key,
419                    &mut read_buffer,
420                )
421                .await?;
422
423                // add new consumer to session, this adds local ring buffer for this consumer, and returns a consumer handle
424                let (consumer_id, mut consumer) =
425                    session.add_consumer(channel_capacity as usize)?;
426
427                let mut peek = [0u8; 1]; // for checking conn alive
428                loop {
429                    tokio::select! {
430                        // block (async) until a packet arrives or peer drops;
431                        // no busy waiting, woken exactly when the ring has data
432                        popped = consumer.pop_async() => {
433                            match popped {
434                                Ok(data) => {
435                                    if writer_h.write_all(&data).await.is_err() {
436                                        let _ = writer_h.shutdown().await;
437                                        error!(
438                                            "Consumer (ip: {}) write error, closing connection for session: {}...",
439                                            client_addr,
440                                            &encode(join_header.session_id)[..12]
441                                        );
442                                        break // if write fails (broken pipe and such issues)
443                                    }
444                                }
445                                Err(ClosedError) => {
446                                    let _ = writer_h.flush().await;
447                                    let _ = writer_h.shutdown().await;
448                                    info!(
449                                        "Consumer (ip: {}) session closed, closing connection for session: {}...",
450                                        client_addr,
451                                        &encode(join_header.session_id)[..12]
452                                    );
453                                    break // producer gone / session removed
454                                },
455                            }
456                        }
457                        result = read_h.read(&mut peek) => {
458                            match result {
459                                Ok(0) => break, // EOF check
460                                Err(_) => {
461                                    let _ = writer_h.flush().await;
462                                    let _ = writer_h.shutdown().await;
463                                    info!(
464                                        "Consumer (ip: {}) peer read error, closing connection for session: {}...",
465                                        client_addr,
466                                        &encode(join_header.session_id)[..12]
467                                    );
468                                    break // if read fails or connection closed, break
469                                },
470                                _ => {}
471                            }
472                        }
473                    }
474                }
475
476                // remove own queue from the session so it doesn't linger as a zombie
477                if let Some(session) = sessions.try_get_session(join_header.session_id) {
478                    session.consumer_local_buffers.remove(&consumer_id);
479                }
480            }
481        }
482
483        // TODO; consumer queue and idx still remains in the session map
484
485        Ok(())
486    }
487
488    pub async fn shutdown(&self) -> Result<()> {
489        unimplemented!()
490    }
491}