hydra_sync/server.rs
1//! Core implementation for the `HydraSync` server.
2use crate::channel::{ClosedError, FullError};
3use crate::client::ServerMetrics;
4use crate::crypto::{NONCE_LEN, TAG_LEN};
5use crate::protocol::StatusCode;
6use crate::protocol::{
7 Role, perform_server_handshake, read_join_header, write_server_metrics,
8 write_server_read_write_len, write_status_code,
9};
10use crate::session::SessionMap;
11use crate::{ChannelOverflowStrategy, START_TIME, error, get_uptime_hrs, info, trace, warn};
12use anyhow::Result;
13use bytes::BytesMut;
14use hex::encode;
15use std::net::SocketAddr;
16use std::ops::{Deref, DerefMut};
17use std::sync::Arc;
18use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
19use std::time::Duration;
20use tokio::io::{AsyncReadExt, AsyncWriteExt};
21use tokio::net::{TcpListener, TcpStream};
22use tokio::sync::Notify;
23
24/// Padded atomic64 to avoid false sharing on hot counters (64B cache line).
25#[repr(C, align(64))]
26struct PaddedAtomicU64(AtomicU64);
27impl PaddedAtomicU64 {
28 const fn pad(v: u64) -> Self {
29 Self(AtomicU64::new(v))
30 }
31}
32impl Deref for PaddedAtomicU64 {
33 type Target = AtomicU64;
34 fn deref(&self) -> &Self::Target {
35 &self.0
36 }
37}
38impl DerefMut for PaddedAtomicU64 {
39 fn deref_mut(&mut self) -> &mut Self::Target {
40 &mut self.0
41 }
42}
43
44/// `HydraServer`- A light-weight, E2E `(AES-GCM)`, multi-threaded SPMC broadcast server using `TCP`.
45/// It implements internal _ring buffer_ configured by [`HydraConfig`], uses **little memory** and **minimal copy** as possible.
46///
47/// ```no_run
48/// use hydra_sync::server::HydraServer;
49///
50/// #[tokio::main]
51/// async fn main() {
52/// let (server, addr) = HydraServer::bind_default().await.unwrap();
53/// println!("Server running on: {}", addr);
54/// let server_runner = server.clone();
55/// tokio::spawn(async move { server_runner.run().await }); // run in background
56///
57/// server.shutdown().await.unwrap(); // shutdown gracefully
58/// }
59/// ```
60pub struct HydraServer {
61 listener: TcpListener,
62 config: Arc<HydraConfig>,
63 /*
64 field below are used for collecting server metrics
65 */
66 /// Global session concurrent map.
67 session_map: Arc<SessionMap>,
68 /// Track the number of active connections to the server.
69 active_connections: Arc<AtomicU64>,
70 /// Total number of sessions ever created.
71 total_sessions: Arc<PaddedAtomicU64>,
72 /// Total number of bytes relayed through the server.
73 total_bytes: Arc<PaddedAtomicU64>,
74 /// Flag to indicate if the server is shutting down gracefully
75 /// (server will stop accepting new connections and wait for existing sessions to finish).
76 shutdown_flag: Arc<AtomicBool>,
77}
78
79/// Configuration for the `HydraServer` features.
80pub struct HydraConfig {
81 /// Maximum number of concurrent connections allowed.
82 pub max_connections: u64,
83 /// Timeout in milliseconds for accepting new connections when capacity is reached.
84 pub accept_timeout: u64,
85 /// Fixed length of a single packet/frame in bytes to read/write `(excluding AES-GCM overhead)`.
86 /// This length is used to `read/write EXACT` data from/to the network stream.
87 pub fixed_packet_length: u64,
88 /// Capacity of the internal channel for each session.
89 pub channel_capacity: u64,
90 /// Token for authenticating observer clients for getting [`ServerMetrics`] from server ring's buffer.
91 pub observer_token: Option<[u8; 64]>,
92 /// Strategy for handling channel overflow when consumers cannot keep up with producers.
93 pub overflow_mode: ChannelOverflowStrategy,
94}
95
96impl HydraConfig {
97 /// Creates `HydraConfig` with the specified parameters.
98 /// > 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)`.
99 /// > 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).
100 pub fn config(
101 max_connections: u64,
102 accept_timeout: u64,
103 fixed_packet_length: u64,
104 channel_capacity: u64,
105 observer_token: Option<[u8; 64]>,
106 overflow_mode: ChannelOverflowStrategy,
107 ) -> Self {
108 if fixed_packet_length == 0 {
109 panic!("fixed_packet_length must be greater than 0");
110 }
111 Self {
112 max_connections,
113 accept_timeout,
114 fixed_packet_length: fixed_packet_length + (NONCE_LEN + TAG_LEN) as u64, // add aes-gcm overhead
115 channel_capacity,
116 observer_token,
117 overflow_mode,
118 }
119 }
120}
121
122impl Default for HydraConfig {
123 /// Default HydraConfig
124 /// - max_connections: 32
125 /// - accept_timeout: 500 ms
126 /// - fixed_packet_length: 4 MB + `AES-GCM overhead (28 bytes)`
127 /// - channel_capacity: 256
128 /// - observer_token: randomly generated
129 /// - overflow_mode: DropPacket
130 fn default() -> Self {
131 Self {
132 max_connections: 32,
133 accept_timeout: 500,
134 fixed_packet_length: (4 << 20) + (NONCE_LEN + TAG_LEN) as u64, // 4 MB + AES-GCM overhead
135 channel_capacity: 256,
136 observer_token: Some(rand::random::<[u8; 64]>()),
137 overflow_mode: ChannelOverflowStrategy::DropPacket,
138 }
139 }
140}
141
142impl HydraServer {
143 /// Binds the server with random local port and default [`HydraConfig`], returns the port.
144 pub async fn bind_default() -> Result<(Arc<Self>, SocketAddr)> {
145 let addr = "127.0.0.1:0".parse::<SocketAddr>()?;
146 let (server, server_addr) = HydraServer::bind(addr, HydraConfig::default()).await?;
147 Ok((server, server_addr))
148 }
149
150 /// Binds the server to the specified [`SocketAddr`] with the provided [`HydraConfig`].
151 pub async fn bind(
152 socket_addr: SocketAddr,
153 hydra_config: HydraConfig,
154 ) -> Result<(Arc<Self>, SocketAddr)> {
155 let server_tcp = TcpListener::bind(socket_addr).await?;
156 let server_addr = server_tcp.local_addr()?;
157 Ok((
158 Arc::new(Self {
159 listener: server_tcp,
160 config: Arc::new(hydra_config),
161 session_map: Arc::new(SessionMap::init()),
162 active_connections: Arc::new(AtomicU64::new(0)),
163 total_sessions: Arc::new(PaddedAtomicU64::pad(0)),
164 total_bytes: Arc::new(PaddedAtomicU64::pad(0)),
165 shutdown_flag: Arc::new(AtomicBool::new(false)),
166 }),
167 server_addr,
168 ))
169 }
170
171 /// Starts the server, accepting incoming connections and handling them concurrently.
172 /// Overview:
173 /// * Accept incoming `TCP connections`, spawn handler tasks and `allocate read/write buffers` or reject if max connections reached.
174 /// * Perform handshake, read/decrypt `JoinHeader`, determine role (Producer/Consumer/Admin).
175 /// * For Producers: Create valid session, read data (exactly **fixed_packet_length**, including `AES-GCM` (28 bytes) overhead, with `read_exact()`, so it may truncate)
176 /// and broadcast `(cheap-copy)` to all Consumers in the session (Write behavior is configured by [`ChannelOverflowStrategy`]).
177 /// * For Consumers: Validate session, read from `local ring buffer` from producer handle, write to TCP stream.
178 /// * `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).
179 /// * EOF check are gracefully handled by closing the connection without logging an error.
180 pub async fn run(&self) -> Result<()> {
181 START_TIME.set(chrono::Local::now()).ok();
182
183 loop {
184 // check shutdown token
185 if self.shutdown_flag.load(Ordering::Relaxed) {
186 // return if no active connections
187 if self.active_connections.load(Ordering::Relaxed) == 0 {
188 return Ok(());
189 }
190 loop {
191 // check current connections, every 3 seconds, wait for them to finish and return
192 tokio::time::sleep(Duration::from_secs(3)).await;
193 if self.active_connections.load(Ordering::Relaxed) > 0 {
194 info!(
195 "Server is shutting down, waiting for {} active connections to finish...",
196 self.active_connections.load(Ordering::Relaxed)
197 );
198 } else {
199 info!("Server has stopped.");
200 return Ok(());
201 }
202 }
203 }
204
205 if self.active_connections.fetch_add(1, Ordering::Relaxed)
206 >= self.config.max_connections
207 {
208 self.active_connections.fetch_sub(1, Ordering::Relaxed);
209 warn!(
210 "Max connections reached: {}, waiting {} ms before accepting new connections",
211 self.config.max_connections, self.config.accept_timeout
212 );
213 tokio::time::sleep(Duration::from_millis(self.config.accept_timeout)).await;
214 continue;
215 }
216
217 tokio::select! {
218 // accept new connection
219 result = self.listener.accept() => {
220 match result {
221 Ok((stream, peer_addr)) => {
222 stream.set_nodelay(true).ok();
223 let config = Arc::clone(&self.config);
224 let session_map = Arc::clone(&self.session_map);
225 let connections = Arc::clone(&self.active_connections);
226 let total_sessions = Arc::clone(&self.total_sessions);
227 let total_bytes = Arc::clone(&self.total_bytes);
228 let is_shutting_down = Arc::clone(&self.shutdown_flag);
229 // spawn client_handler task
230 tokio::spawn(async move {
231 trace!("Accepted connection from: {}", peer_addr);
232 if let Err(e) = Self::handle_connection(
233 stream,
234 session_map,
235 config,
236 total_sessions,
237 total_bytes,
238 is_shutting_down,
239 )
240 .await
241 {
242 error!("Connection handling error: {} from: {}", e, peer_addr);
243 }
244 connections.fetch_sub(1, Ordering::Release);
245 });
246 }
247 Err(e) => {
248 self.active_connections.fetch_sub(1, Ordering::Release);
249 error!("Connection accepting error: {}", e);
250 }
251 }
252 }
253 // shutdown signal, then break blocking accept()
254 _ = async {
255 while !self.shutdown_flag.load(Ordering::Relaxed) {
256 tokio::time::sleep(Duration::from_millis(100)).await;
257 }
258 } => {
259 self.active_connections.fetch_sub(1, Ordering::Release);
260 continue;
261 }
262 }
263 }
264 }
265
266 /// Handles an individual client connection.
267 async fn handle_connection(
268 mut stream: TcpStream,
269 sessions: Arc<SessionMap>,
270 config: Arc<HydraConfig>,
271 total_sessions: Arc<PaddedAtomicU64>,
272 total_bytes: Arc<PaddedAtomicU64>,
273 is_shutting_down: Arc<AtomicBool>,
274 ) -> Result<()> {
275 stream.set_nodelay(true).ok();
276 let client_addr = stream.peer_addr()?;
277 let (mut read_h, mut writer_h) = stream.split();
278 let mut read_buffer = BytesMut::zeroed(config.fixed_packet_length as usize);
279 let transport_key = perform_server_handshake(&mut read_h, &mut writer_h).await?;
280 let join_header = read_join_header(&mut read_h, &transport_key, &mut read_buffer).await?;
281
282 // determine role and handle accordingly
283 let role = match Role::from_u8(join_header.role) {
284 Ok(role) => {
285 info!(
286 "Client (ip: {}) joined with role: {}, session_uuid: {}...",
287 client_addr,
288 role.to_string(),
289 &encode(join_header.uuid_or_token)[..12]
290 );
291 role
292 }
293 Err(_) => {
294 warn!(
295 "Client (ip: {}) joining with invalid role: {:#04x}",
296 client_addr, join_header.role
297 );
298 write_status_code(
299 &mut writer_h,
300 StatusCode::ErrInvalidRole,
301 &transport_key,
302 &mut read_buffer,
303 )
304 .await?;
305 return Ok(());
306 }
307 };
308
309 // handle roles
310 match role {
311 Role::Producer => {
312 // create session for producer, if already exists, return error
313 if !sessions.try_create_session(join_header.uuid_or_token) {
314 warn!(
315 "Producer (ip: {}) failed to create session_uuid: {}...",
316 client_addr,
317 &encode(join_header.uuid_or_token)[..12]
318 );
319
320 // send error code
321 write_status_code(
322 &mut writer_h,
323 StatusCode::ErrSessionAlreadyOccupied,
324 &transport_key,
325 &mut read_buffer,
326 )
327 .await?;
328 return Ok(());
329 }
330
331 info!(
332 "Producer (ip: {}) created session_uuid: {}...",
333 client_addr,
334 &encode(join_header.uuid_or_token)[..12]
335 );
336
337 total_sessions.fetch_add(1, Ordering::Relaxed);
338 // on successful session creation, send read_len to producer
339 write_status_code(
340 &mut writer_h,
341 StatusCode::Success,
342 &transport_key,
343 &mut read_buffer,
344 )
345 .await?;
346 write_server_read_write_len(
347 &mut writer_h,
348 config.fixed_packet_length - (NONCE_LEN + TAG_LEN) as u64, // announce raw payload capacity (encryption overhead is internal)
349 &transport_key,
350 &mut read_buffer,
351 )
352 .await?;
353
354 // read from producer and write to all consumers with fixed read_write len
355 let mut write_buffer = BytesMut::zeroed(config.fixed_packet_length as usize);
356 let mut dead_consumers = vec![]; // 'client to drop' collection
357 let mut pending_consumers = vec![]; // 'slow client' collection
358
359 loop {
360 // react_exact to fill buf, this does not extend buf anyway,
361 // this will return an error if the stream is closed or if it cannot fill the buffer exactly
362 // 'reads' may get truncated and may send corrupted data to consumers
363 match read_h.read_exact(&mut write_buffer).await {
364 Ok(_) => {
365 let data = write_buffer.split().freeze(); // zero-copy 'freeze' to Bytes
366
367 // get session ref (TODO; get read lock, maybe block other map shards? idk)
368 if let Some(session) =
369 sessions.try_get_session(join_header.uuid_or_token)
370 {
371 // non-blocking pass over every consumer ring,
372 // fast consumers are served immediately, regardless of where a
373 // slow one sits in the (arbitrary) DashMap iteration order
374 for mut entry in session.consumer_local_buffers.iter_mut() {
375 // rm consumers that disconnected and their rings
376 if entry.value_mut().is_closed() {
377 dead_consumers.push(*entry.key());
378 continue;
379 }
380 // broadcast away
381 let data_len = data.len() as u64;
382 match entry.value_mut().push(data.clone()) {
383 Ok(()) => {
384 total_bytes.fetch_add(data_len, Ordering::Relaxed);
385 }
386 Err(FullError) => match config.overflow_mode {
387 ChannelOverflowStrategy::DropPacket => {
388 // skip, don't push latest packet to local consumer queue
389 }
390 ChannelOverflowStrategy::DropClient => {
391 // collect the 'lagged' client
392 dead_consumers.push(*entry.key());
393 }
394 ChannelOverflowStrategy::BackPressure => {
395 // collect all slow consumer
396 pending_consumers.push(*entry.key());
397 }
398 },
399 }
400 }
401
402 // blocking delivery pass for full rings only,
403 // parks until space frees up; woken by pop() or by the
404 // consumer disconnecting by ClosedError
405 for id in pending_consumers.drain(..) {
406 loop {
407 enum Step {
408 Pushed,
409 Remove,
410 Wait(Arc<Notify>),
411 }
412 let step: Step = 'lock: {
413 // get lock on the consumer ring, or remove the entry
414 let Some(mut entry) =
415 session.consumer_local_buffers.get_mut(&id)
416 else {
417 break 'lock Step::Remove;
418 };
419 // check if closed, or remove
420 let p = entry.value_mut();
421 if p.is_closed() {
422 break 'lock Step::Remove;
423 }
424 match p.push(data.clone()) {
425 Ok(()) => {
426 total_bytes.fetch_add(
427 data.len() as u64,
428 Ordering::Relaxed,
429 );
430 Step::Pushed
431 }
432 Err(FullError) => Step::Wait(p.space_notify()),
433 }
434 };
435 match step {
436 Step::Pushed => break,
437 Step::Remove => {
438 dead_consumers.push(id);
439 break;
440 }
441 // no lock held here, the guard was dropped at 'lock
442 Step::Wait(notify) => notify.notified().await,
443 }
444 }
445 }
446
447 // remove dead consumer queues
448 for id in dead_consumers.drain(..) {
449 session.consumer_local_buffers.remove(&id);
450 }
451 }
452
453 write_buffer.resize(config.fixed_packet_length as usize, 0);
454 }
455 Err(_) => {
456 error!(
457 "Producer (ip: {}) read error, closing connection for session_uuid: {}...",
458 client_addr,
459 &encode(join_header.uuid_or_token)[..12]
460 );
461 break;
462 }
463 }
464 }
465
466 info!(
467 "Producer (ip: {}) disconnected, removing session_uuid: {}...",
468 client_addr,
469 &encode(join_header.uuid_or_token)[..12]
470 );
471
472 // remove session and return (close stream) on disconnect or error
473 sessions.remove_session(join_header.uuid_or_token);
474 }
475
476 Role::Consumer => {
477 // check session exists, if not, return error
478 let session = match sessions.try_get_session(join_header.uuid_or_token) {
479 Some(s) => s,
480 None => {
481 warn!(
482 "Consumer (ip: {}) failed to join session_uuid: {}...",
483 client_addr,
484 &encode(join_header.uuid_or_token)[..12]
485 );
486
487 write_status_code(
488 &mut writer_h,
489 StatusCode::ErrSessionNotFound,
490 &transport_key,
491 &mut read_buffer,
492 )
493 .await?;
494 return Ok(());
495 }
496 };
497
498 info!(
499 "Consumer (ip: {}) joined session_uuid: {}...",
500 client_addr,
501 &encode(join_header.uuid_or_token)[..12]
502 );
503
504 // on success, send write_len to consumer
505 write_status_code(
506 &mut writer_h,
507 StatusCode::Success,
508 &transport_key,
509 &mut read_buffer,
510 )
511 .await?;
512 write_server_read_write_len(
513 &mut writer_h,
514 config.fixed_packet_length - (NONCE_LEN + TAG_LEN) as u64, // announce raw payload capacity (encryption overhead is internal)
515 &transport_key,
516 &mut read_buffer,
517 )
518 .await?;
519
520 // add new consumer to session, this adds local ring buffer for this consumer, and returns a consumer handle
521 let (consumer_id, mut consumer) =
522 session.add_consumer(config.channel_capacity as usize)?;
523
524 let mut peek = [0u8; 1]; // for checking conn alive
525 loop {
526 tokio::select! {
527 // block (async) until a packet arrives or peer drops;
528 // no busy waiting, woken exactly when the ring has data
529 popped = consumer.pop_async() => {
530 match popped {
531 Ok(data) => {
532 if writer_h.write_all(&data).await.is_err() {
533 let _ = writer_h.flush().await;
534 let _ = writer_h.shutdown().await;
535 error!(
536 "Consumer (ip: {}) write error, closing connection for session: {}...",
537 client_addr,
538 &encode(join_header.uuid_or_token)[..12]
539 );
540 break // if write fails (broken pipe and such issues)
541 }
542 }
543 Err(ClosedError) => {
544 let _ = writer_h.flush().await;
545 let _ = writer_h.shutdown().await;
546 info!(
547 "Consumer (ip: {}) session closed, closing connection for session: {}...",
548 client_addr,
549 &encode(join_header.uuid_or_token)[..12]
550 );
551 break // producer gone / session removed
552 },
553 }
554 }
555 result = read_h.read(&mut peek) => {
556 match result {
557 Ok(0) => break, // EOF check
558 Err(_) => {
559 let _ = writer_h.flush().await;
560 let _ = writer_h.shutdown().await;
561 info!(
562 "Consumer (ip: {}) peer read error, closing connection for session: {}...",
563 client_addr,
564 &encode(join_header.uuid_or_token)[..12]
565 );
566 break // if read fails or connection closed, break
567 },
568 _ => {}
569 }
570 }
571 }
572 }
573
574 info!(
575 "Consumer (ip: {}) disconnected, closing connection for session_uuid: {}...",
576 client_addr,
577 &encode(join_header.uuid_or_token)[..12]
578 );
579
580 // remove own queue from the session, its dead weight
581 if let Some(session) = sessions.try_get_session(join_header.uuid_or_token) {
582 session.consumer_local_buffers.remove(&consumer_id);
583 }
584 }
585
586 Role::Observer => {
587 // check token, if not present or invalid, return error
588 if let Some(token) = config.observer_token
589 && token != join_header.uuid_or_token
590 {
591 warn!(
592 "Observer (ip: {}) failed to authenticate with token: {}...",
593 client_addr,
594 &encode(join_header.uuid_or_token)[..12]
595 );
596
597 write_status_code(
598 &mut writer_h,
599 StatusCode::ErrInvalidToken,
600 &transport_key,
601 &mut read_buffer,
602 )
603 .await?;
604 return Ok(());
605 }
606
607 info!("Observer (ip: {}) token approved", client_addr,);
608 write_status_code(
609 &mut writer_h,
610 StatusCode::Success,
611 &transport_key,
612 &mut read_buffer,
613 )
614 .await?;
615 // send this anyway
616 write_server_read_write_len(
617 &mut writer_h,
618 config.fixed_packet_length - (NONCE_LEN + TAG_LEN) as u64,
619 &transport_key,
620 &mut read_buffer,
621 )
622 .await?;
623
624 // client sends 1 dumb byte, server replies with latest metrics
625 // client polls manually
626 let mut signal = [0u8; 1];
627 loop {
628 // instant break server shutting down, no wait
629 if is_shutting_down.load(Ordering::Relaxed) {
630 break;
631 }
632 if read_h.read_exact(&mut signal).await.is_err() {
633 break; // client closed / read error
634 }
635 let metrics = ServerMetrics {
636 uptime_hrs: get_uptime_hrs(),
637 total_sessions: total_sessions.load(Ordering::Relaxed),
638 active_sessions: sessions.map.len() as u64,
639 total_network_bandwidth: total_bytes.load(Ordering::Relaxed),
640 };
641 if write_server_metrics(
642 &mut writer_h,
643 &metrics,
644 &transport_key,
645 &mut read_buffer,
646 )
647 .await
648 .is_err()
649 {
650 break; // broken pipe
651 }
652 }
653 info!(
654 "Observer (ip: {}) disconnected, closing connection...",
655 client_addr,
656 );
657 return Ok(());
658 }
659 }
660
661 Ok(())
662 }
663
664 /// Returns the number of `active connections` to the server.
665 pub fn get_active_connections(&self) -> u64 {
666 self.active_connections.load(Ordering::Relaxed)
667 }
668
669 /// Returns the number of `active sessions` to the server.
670 pub fn get_active_sessions(&self) -> u64 {
671 self.session_map.map.len() as u64
672 }
673
674 /// Returns the total number of sessions ever created on the server.
675 pub fn get_total_sessions(&self) -> u64 {
676 self.total_sessions.load(Ordering::Relaxed)
677 }
678
679 /// Returns the observer token.
680 pub fn get_observer_token(&self) -> Option<[u8; 64]> {
681 self.config.observer_token
682 }
683
684 /// Returns the server uptime in seconds.
685 pub fn get_uptime_secs(&self) -> f64 {
686 if let Some(start_time) = START_TIME.get() {
687 let now = chrono::Local::now();
688 let duration = now.signed_duration_since(*start_time);
689 duration.as_seconds_f64()
690 } else {
691 0.0
692 }
693 }
694
695 /// Gracefully shuts down the server, stopping it from accepting `new connections` and waiting for `existing sessions` to finish.
696 pub async fn shutdown(&self) -> Result<()> {
697 self.shutdown_flag.store(true, Ordering::Relaxed);
698 Ok(())
699 }
700}
701
702#[allow(unused)]
703async fn handle_producer() -> Result<()> {
704 Ok(())
705}
706#[allow(unused)]
707async fn handle_consumer() -> Result<()> {
708 Ok(())
709}
710#[allow(unused)]
711async fn handle_observer() -> Result<()> {
712 Ok(())
713}