use crate::message::FrameBatch;
use crate::runtime::MailboxSender;
use crate::socket::connection_iface::ISocketConnection;
use crate::socket::events::{MonitorSender, clean_endpoint_uri};
use crate::socket::options::SocketOptions;
use crate::socket::types::SocketType;
use crate::socket::SocketEvent;
use fibre::mpsc::BoundedAsyncSender;
use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use std::time::Instant;
use tokio::task::JoinHandle;
#[derive(Debug)]
pub(crate) struct EndpointInfo {
pub mailbox: MailboxSender,
pub task_handle: Option<JoinHandle<()>>,
pub endpoint_type: EndpointType,
pub endpoint_uri: String,
pub pipe_ids: Option<(usize, usize)>, pub handle_id: usize,
pub target_endpoint_uri: Option<String>,
pub is_outbound_connection: bool,
pub peer_socket_type: Option<String>,
pub connection_iface: Arc<dyn ISocketConnection>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum EndpointType {
Listener,
Session, }
#[derive(Debug, Clone)]
pub(crate) struct ReconnectState {
pub current_attempts: u32,
pub next_attempt_at: Option<Instant>,
}
impl Default for ReconnectState {
fn default() -> Self {
Self {
current_attempts: 0,
next_attempt_at: None,
}
}
}
impl ReconnectState {
pub fn on_connection_success(&mut self) {
self.current_attempts = 0;
self.next_attempt_at = None;
}
pub fn on_connection_failure(
&mut self,
base_ivl: std::time::Duration,
max_ivl: std::time::Duration,
) -> std::time::Duration {
let multiplier = 2u32.saturating_pow(self.current_attempts.min(31));
let mut delay = base_ivl.saturating_mul(multiplier);
if max_ivl > std::time::Duration::ZERO {
delay = delay.min(max_ivl);
}
self.current_attempts = self.current_attempts.saturating_add(1);
self.next_attempt_at = Some(Instant::now() + delay);
delay
}
pub fn is_due(&self, now: Instant) -> bool {
match self.next_attempt_at {
Some(time) => now >= time,
None => false,
}
}
}
#[derive(Debug)]
pub(crate) struct CoreState {
pub(crate) handle: usize,
pub options: Arc<SocketOptions>,
pub socket_type: SocketType,
pub pipes_tx: HashMap<usize, BoundedAsyncSender<FrameBatch>>,
pub pipe_reader_task_handles: HashMap<usize, JoinHandle<()>>,
pub endpoints: HashMap<String, EndpointInfo>,
pub(crate) reconnect_states: HashMap<String, ReconnectState>,
pub pipe_read_id_to_endpoint_uri: HashMap<usize, String>,
#[cfg(feature = "inproc")]
pub(crate) bound_inproc_names: HashSet<String>,
pub(crate) monitor_tx: Option<MonitorSender>,
pub(crate) last_bound_endpoint: Option<String>,
}
impl CoreState {
pub(crate) fn new(handle: usize, socket_type: SocketType, options: SocketOptions) -> Self {
Self {
handle,
options: Arc::new(options), socket_type,
pipes_tx: HashMap::new(),
pipe_reader_task_handles: HashMap::new(),
endpoints: HashMap::new(),
reconnect_states: HashMap::new(),
pipe_read_id_to_endpoint_uri: HashMap::new(),
#[cfg(feature = "inproc")]
bound_inproc_names: HashSet::new(),
monitor_tx: None,
last_bound_endpoint: None,
}
}
pub(crate) fn get_pipe_sender(&self, pipe_write_id: usize) -> Option<BoundedAsyncSender<FrameBatch>> {
self.pipes_tx.get(&pipe_write_id).cloned()
}
#[allow(dead_code)]
pub(crate) fn get_reader_task_handle(&self, pipe_read_id: usize) -> Option<&JoinHandle<()>> {
self.pipe_reader_task_handles.get(&pipe_read_id)
}
pub(crate) fn remove_pipe_state(&mut self, pipe_write_id: usize, pipe_read_id: usize) -> bool {
let tx_removed = self.pipes_tx.remove(&pipe_write_id).is_some();
if tx_removed {
tracing::trace!(
core_handle = self.handle,
pipe_id = pipe_write_id,
"CoreState: Removed pipe sender"
);
}
let reader_removed = if let Some(handle) = self.pipe_reader_task_handles.remove(&pipe_read_id) {
tracing::trace!(
core_handle = self.handle,
pipe_id = pipe_read_id,
"CoreState: Aborting pipe reader task"
);
handle.abort();
true
} else {
false
};
if reader_removed {
tracing::trace!(
core_handle = self.handle,
pipe_id = pipe_read_id,
"CoreState: Removed pipe reader task handle"
);
}
let map_removed = self
.pipe_read_id_to_endpoint_uri
.remove(&pipe_read_id)
.is_some();
if map_removed {
tracing::trace!(
core_handle = self.handle,
pipe_id = pipe_read_id,
"CoreState: Removed pipe_read_id_to_endpoint_uri mapping"
);
}
tx_removed || reader_removed || map_removed
}
pub(crate) fn send_monitor_event(&self, event: SocketEvent) {
if let Some(ref tx) = self.monitor_tx {
let clean = |s: String| -> String { clean_endpoint_uri(&s).to_owned() };
let event = match event {
SocketEvent::Listening { endpoint } => SocketEvent::Listening {
endpoint: clean(endpoint),
},
SocketEvent::BindFailed {
endpoint,
error_msg,
} => SocketEvent::BindFailed {
endpoint: clean(endpoint),
error_msg,
},
SocketEvent::Accepted {
endpoint,
peer_addr,
} => SocketEvent::Accepted {
endpoint: clean(endpoint),
peer_addr: clean(peer_addr),
},
SocketEvent::AcceptFailed {
endpoint,
error_msg,
} => SocketEvent::AcceptFailed {
endpoint: clean(endpoint),
error_msg,
},
SocketEvent::Connected {
endpoint,
peer_addr,
} => SocketEvent::Connected {
endpoint: clean(endpoint),
peer_addr: clean(peer_addr),
},
SocketEvent::ConnectDelayed {
endpoint,
error_msg,
} => SocketEvent::ConnectDelayed {
endpoint: clean(endpoint),
error_msg,
},
SocketEvent::ConnectRetried { endpoint, interval } => SocketEvent::ConnectRetried {
endpoint: clean(endpoint),
interval,
},
SocketEvent::ConnectFailed {
endpoint,
error_msg,
} => SocketEvent::ConnectFailed {
endpoint: clean(endpoint),
error_msg,
},
SocketEvent::Closed { endpoint } => SocketEvent::Closed {
endpoint: clean(endpoint),
},
SocketEvent::Disconnected { endpoint } => SocketEvent::Disconnected {
endpoint: clean(endpoint),
},
SocketEvent::HandshakeFailed {
endpoint,
error_msg,
} => SocketEvent::HandshakeFailed {
endpoint: clean(endpoint),
error_msg,
},
SocketEvent::HandshakeSucceeded { endpoint } => SocketEvent::HandshakeSucceeded {
endpoint: clean(endpoint),
},
SocketEvent::ConnectionCongested { endpoint } => SocketEvent::ConnectionCongested {
endpoint: clean(endpoint),
},
SocketEvent::ConnectionUncongested { endpoint } => SocketEvent::ConnectionUncongested {
endpoint: clean(endpoint),
},
};
if tx.try_send(event).is_err() {
tracing::warn!(
socket_handle = self.handle,
"Failed to send event to monitor channel (full or closed)"
);
}
}
}
pub(crate) fn get_monitor_sender_clone(&self) -> Option<MonitorSender> {
self.monitor_tx.clone()
}
}
#[cfg(test)]
mod reconnect_tests {
use super::*;
use std::time::Duration;
#[test]
fn test_reconnect_backoff_doubling() {
let mut state = ReconnectState::default();
let base = Duration::from_millis(100);
let max = Duration::from_millis(1000);
let delay1 = state.on_connection_failure(base, max);
assert_eq!(delay1, Duration::from_millis(100));
let delay2 = state.on_connection_failure(base, max);
assert_eq!(delay2, Duration::from_millis(200));
let delay3 = state.on_connection_failure(base, max);
assert_eq!(delay3, Duration::from_millis(400));
}
#[test]
fn test_reconnect_backoff_ceiling() {
let mut state = ReconnectState::default();
let base = Duration::from_millis(100);
let max = Duration::from_millis(250);
let _ = state.on_connection_failure(base, max); let delay2 = state.on_connection_failure(base, max); let delay3 = state.on_connection_failure(base, max);
assert_eq!(delay2, Duration::from_millis(200));
assert_eq!(delay3, max);
}
#[test]
fn test_reconnect_overflow_prevention() {
let mut state = ReconnectState::default();
let base = Duration::from_millis(100);
let max = Duration::from_secs(60);
for _ in 0..100 {
let delay = state.on_connection_failure(base, max);
assert!(
delay <= max,
"Delay {:?} must not exceed max {:?}",
delay,
max
);
}
assert_eq!(state.current_attempts, 100);
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ShutdownPhase {
Running,
StoppingChildren,
Lingering,
CleaningPipes,
Finished,
}
#[derive(Debug)]
pub(crate) struct ShutdownCoordinator {
pub(crate) state: ShutdownPhase,
pub(crate) pending_child_actors: HashMap<usize, String>,
pub(crate) pending_connections_to_close: HashMap<usize, (String, Arc<dyn ISocketConnection>)>, #[cfg(feature = "inproc")]
pub(crate) inproc_connections_to_cleanup: Vec<(usize, usize, String)>, pub(crate) linger_deadline: Option<Instant>,
}
impl Default for ShutdownCoordinator {
fn default() -> Self {
Self {
state: ShutdownPhase::Running,
pending_child_actors: HashMap::new(),
pending_connections_to_close: HashMap::new(),
#[cfg(feature = "inproc")]
inproc_connections_to_cleanup: Vec::new(),
linger_deadline: None,
}
}
}