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}