use bytes::Bytes;
use rand::Rng;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use tokio::sync::{broadcast, RwLock};
use tokio::task::JoinHandle;
use tracing::{debug, info, warn};
use crate::channels::{OutputFanout, OutputStream};
use crate::errors::{Error, Result};
use crate::session::{CloseReason, Session, SessionConfig, SessionManager};
use crate::shutdown::ShutdownSignal;
#[derive(Debug, Clone)]
pub struct ReconnectConfig {
pub max_attempts: u32,
pub initial_delay: Duration,
pub max_delay: Duration,
pub session: SessionConfig,
}
impl Default for ReconnectConfig {
fn default() -> Self {
Self {
max_attempts: 10,
initial_delay: Duration::from_secs(1),
max_delay: Duration::from_secs(60),
session: SessionConfig::default(),
}
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum ReconnectEvent {
Disconnected {
reason: CloseReason,
},
Reconnecting {
attempt: u32,
delay: Duration,
},
Reconnected {
session_id: String,
attempts: u32,
},
GaveUp {
attempts: u32,
reason: String,
},
}
struct Inner {
manager: SessionManager,
config: ReconnectConfig,
current: RwLock<Option<Arc<Session>>>,
output: Arc<OutputFanout>,
events: broadcast::Sender<ReconnectEvent>,
shutdown: ShutdownSignal,
generation: Mutex<u64>,
}
#[derive(Clone)]
pub struct ReconnectingSession {
inner: Arc<Inner>,
supervisor: Arc<JoinHandle<()>>,
}
impl ReconnectingSession {
pub async fn connect(target: impl Into<String>, config: ReconnectConfig) -> Result<Self> {
let manager = SessionManager::new().await?;
Self::connect_with(target, config, manager).await
}
pub async fn connect_with(
target: impl Into<String>,
mut config: ReconnectConfig,
manager: SessionManager,
) -> Result<Self> {
config.session.target = target.into();
let (events, _) = broadcast::channel(64);
let inner = Arc::new(Inner {
manager,
config,
current: RwLock::new(None),
output: Arc::new(OutputFanout::new()),
events,
shutdown: ShutdownSignal::new(),
generation: Mutex::new(0),
});
let session = Arc::new(
inner
.manager
.start_session(inner.config.session.clone())
.await?,
);
inner.adopt(session).await;
let supervisor = tokio::spawn(supervise(Arc::clone(&inner)));
Ok(Self {
inner,
supervisor: Arc::new(supervisor),
})
}
pub fn target(&self) -> &str {
&self.inner.config.session.target
}
pub async fn current(&self) -> Option<Arc<Session>> {
self.inner.current.read().await.clone()
}
pub fn output(&self) -> OutputStream {
self.inner
.output
.subscribe(self.inner.config.session.output_buffer)
}
pub fn events(&self) -> broadcast::Receiver<ReconnectEvent> {
self.inner.events.subscribe()
}
pub fn generation(&self) -> u64 {
*self
.inner
.generation
.lock()
.unwrap_or_else(|e| e.into_inner())
}
pub async fn is_ready(&self) -> bool {
matches!(self.current().await, Some(s) if s.is_ready() && !s.is_closed())
}
pub async fn send(&self, data: impl Into<Bytes>) -> Result<()> {
let data = data.into();
let deadline = tokio::time::Instant::now() + self.inner.config.session.ready_timeout;
loop {
if let Some(session) = self.current().await {
if !session.is_closed() {
match session.send(data.clone()).await {
Ok(()) => return Ok(()),
Err(Error::SessionClosed(_)) => {}
Err(e) => return Err(e),
}
}
}
if self.inner.shutdown.is_shutdown() {
return Err(Error::SessionClosed(
"the reconnecting session has stopped".into(),
));
}
if tokio::time::Instant::now() >= deadline {
return Err(Error::Timeout(self.inner.config.session.ready_timeout));
}
tokio::time::sleep(Duration::from_millis(100)).await;
}
}
pub async fn terminate(&self) -> Result<()> {
self.inner.shutdown.shutdown();
let session = self.inner.current.write().await.take();
let result = match session {
Some(session) => session.terminate().await,
None => Ok(()),
};
self.inner.output.close();
result
}
}
impl Drop for ReconnectingSession {
fn drop(&mut self) {
if Arc::strong_count(&self.supervisor) == 1 {
self.inner.shutdown.shutdown();
self.supervisor.abort();
}
}
}
impl std::fmt::Debug for ReconnectingSession {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ReconnectingSession")
.field("target", &self.target())
.field("generation", &self.generation())
.finish()
}
}
impl Inner {
async fn adopt(&self, session: Arc<Session>) {
*self.current.write().await = Some(Arc::clone(&session));
*self.generation.lock().unwrap_or_else(|e| e.into_inner()) += 1;
tokio::spawn(pump_output(session, Arc::clone(&self.output)));
}
fn emit(&self, event: ReconnectEvent) {
let _ = self.events.send(event);
}
}
async fn pump_output(session: Arc<Session>, output: Arc<OutputFanout>) {
use futures_util::StreamExt;
let mut stream = session.output();
while let Some(chunk) = stream.next().await {
output.send(chunk);
}
if stream.lagged() {
warn!("output consumer fell behind during a reconnecting session");
}
}
async fn supervise(inner: Arc<Inner>) {
loop {
let session = {
let guard = inner.current.read().await;
match guard.as_ref() {
Some(session) => Arc::clone(session),
None => return,
}
};
tokio::select! {
biased;
() = inner.shutdown.cancelled() => return,
() = session.closed() => {}
}
let reason = session
.close_reason()
.unwrap_or(CloseReason::Transport("connection lost".into()));
inner.emit(ReconnectEvent::Disconnected {
reason: reason.clone(),
});
if !reason.is_recoverable() {
info!(%reason, "session ended for a reason a reconnect cannot fix");
inner.emit(ReconnectEvent::GaveUp {
attempts: 0,
reason: reason.to_string(),
});
break;
}
match reconnect(&inner).await {
Ok(()) => continue,
Err(e) => {
inner.emit(ReconnectEvent::GaveUp {
attempts: inner.config.max_attempts,
reason: e.to_string(),
});
break;
}
}
}
inner.output.close();
*inner.current.write().await = None;
debug!("reconnect supervisor finished");
}
async fn reconnect(inner: &Arc<Inner>) -> Result<()> {
let mut attempt = 0u32;
let mut ceiling = inner.config.initial_delay;
loop {
attempt += 1;
if inner.config.max_attempts > 0 && attempt > inner.config.max_attempts {
return Err(Error::transport(format!(
"gave up reconnecting to {} after {} attempts",
inner.config.session.target, inner.config.max_attempts
)));
}
let delay = jitter(ceiling);
inner.emit(ReconnectEvent::Reconnecting { attempt, delay });
info!(attempt, ?delay, target = %inner.config.session.target, "reconnecting");
tokio::select! {
biased;
() = inner.shutdown.cancelled() => {
return Err(Error::SessionClosed("shutdown requested while reconnecting".into()));
}
_ = tokio::time::sleep(delay) => {}
}
match inner
.manager
.start_session(inner.config.session.clone())
.await
{
Ok(session) => {
let session = Arc::new(session);
let session_id = session.id().to_owned();
inner.adopt(session).await;
info!(%session_id, attempt, "reconnected");
inner.emit(ReconnectEvent::Reconnected {
session_id,
attempts: attempt,
});
return Ok(());
}
Err(e) if !e.is_retriable() => {
warn!(error = %e, "reconnect failed permanently");
return Err(e);
}
Err(e) => {
warn!(attempt, error = %e, "reconnect attempt failed");
ceiling = (ceiling * 2).min(inner.config.max_delay);
}
}
}
}
fn jitter(ceiling: Duration) -> Duration {
let millis = ceiling.as_millis().min(u128::from(u64::MAX)) as u64;
if millis == 0 {
return Duration::ZERO;
}
Duration::from_millis(rand::thread_rng().gen_range(0..=millis))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn defaults_bound_both_attempts_and_delay() {
let config = ReconnectConfig::default();
assert_eq!(config.max_attempts, 10);
assert_eq!(config.initial_delay, Duration::from_secs(1));
assert_eq!(config.max_delay, Duration::from_secs(60));
}
#[test]
fn jitter_stays_within_the_ceiling() {
let ceiling = Duration::from_millis(1000);
let samples: Vec<Duration> = (0..500).map(|_| jitter(ceiling)).collect();
assert!(
samples.iter().all(|d| *d <= ceiling),
"jitter exceeded the ceiling"
);
assert!(
samples.iter().any(|d| *d < ceiling / 2),
"jitter should reach the low half of the range"
);
assert!(
samples.iter().any(|d| *d > ceiling / 2),
"jitter should reach the high half of the range"
);
}
#[test]
fn jitter_of_zero_is_zero() {
assert_eq!(jitter(Duration::ZERO), Duration::ZERO);
}
#[test]
fn only_recoverable_closures_trigger_a_reconnect() {
assert!(CloseReason::Transport("reset".into()).is_recoverable());
assert!(CloseReason::PeerUnresponsive {
idle: Duration::from_secs(120)
}
.is_recoverable());
assert!(CloseReason::DeliveryFailed {
sequence: 1,
attempts: 3000
}
.is_recoverable());
assert!(!CloseReason::Terminated.is_recoverable());
assert!(!CloseReason::AgentClosed {
exit_code: Some(0),
detail: None
}
.is_recoverable());
assert!(!CloseReason::Protocol("bad digest".into()).is_recoverable());
}
#[test]
fn events_are_cloneable_for_broadcast() {
let event = ReconnectEvent::Reconnected {
session_id: "s-1".into(),
attempts: 3,
};
let debug = format!("{:?}", event.clone());
assert!(debug.contains("s-1"), "{debug}");
}
}