use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::Duration;
use forge_sandbox::{ResourceDispatcher, ToolDispatcher};
use serde_json::Value;
use tokio::sync::{Mutex, Notify, RwLock};
use crate::{McpClient, TransportConfig};
pub struct ReconnectingClient {
name: String,
transport_config: TransportConfig,
inner: RwLock<Arc<McpClient>>,
reconnecting: AtomicBool,
reconnect_done: Notify,
max_backoff: Duration,
current_backoff: Mutex<Duration>,
}
impl ReconnectingClient {
pub fn new(
name: String,
transport_config: TransportConfig,
client: Arc<McpClient>,
max_backoff: Duration,
) -> Self {
Self {
name,
transport_config,
inner: RwLock::new(client),
reconnecting: AtomicBool::new(false),
reconnect_done: Notify::new(),
max_backoff,
current_backoff: Mutex::new(Duration::from_secs(1)),
}
}
async fn try_reconnect(&self) -> bool {
if self
.reconnecting
.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
.is_err()
{
tracing::debug!(server = %self.name, "waiting for concurrent reconnection to complete");
let wait_result =
tokio::time::timeout(Duration::from_secs(30), self.reconnect_done.notified()).await;
if wait_result.is_err() {
tracing::warn!(
server = %self.name,
"timed out waiting for concurrent reconnection"
);
}
return !self.reconnecting.load(Ordering::SeqCst);
}
let backoff = {
let guard = self.current_backoff.lock().await;
*guard
};
tracing::info!(
server = %self.name,
backoff_ms = backoff.as_millis(),
"attempting reconnection after backoff"
);
tokio::time::sleep(backoff).await;
let success = match McpClient::connect(self.name.clone(), &self.transport_config).await {
Ok(new_client) => {
tracing::info!(server = %self.name, "reconnection successful");
{
let mut inner = self.inner.write().await;
*inner = Arc::new(new_client);
}
{
let mut guard = self.current_backoff.lock().await;
*guard = Duration::from_secs(1);
}
true
}
Err(e) => {
tracing::warn!(
server = %self.name,
error = %e,
"reconnection failed"
);
{
let mut guard = self.current_backoff.lock().await;
*guard = (*guard * 2).min(self.max_backoff);
}
false
}
};
self.reconnecting.store(false, Ordering::SeqCst);
self.reconnect_done.notify_waiters();
success
}
async fn current_client(&self) -> Arc<McpClient> {
self.inner.read().await.clone()
}
}
#[async_trait::async_trait]
impl ToolDispatcher for ReconnectingClient {
async fn call_tool(
&self,
server: &str,
tool: &str,
args: Value,
) -> Result<Value, forge_error::DispatchError> {
let client = self.current_client().await;
let result = client.call_tool(server, tool, args.clone()).await;
match result {
Err(forge_error::DispatchError::TransportDead { .. }) => {
tracing::warn!(
server = %self.name,
tool = %tool,
"transport dead, attempting reconnection"
);
if self.try_reconnect().await {
let new_client = self.current_client().await;
new_client.call_tool(server, tool, args).await
} else {
Err(forge_error::DispatchError::TransportDead {
server: self.name.clone(),
reason: "reconnection failed".into(),
})
}
}
other => other,
}
}
}
#[async_trait::async_trait]
impl ResourceDispatcher for ReconnectingClient {
async fn read_resource(
&self,
server: &str,
uri: &str,
) -> Result<Value, forge_error::DispatchError> {
let client = self.current_client().await;
let result = ResourceDispatcher::read_resource(client.as_ref(), server, uri).await;
match result {
Err(forge_error::DispatchError::TransportDead { .. }) => {
tracing::warn!(
server = %self.name,
uri = %uri,
"transport dead, attempting reconnection"
);
if self.try_reconnect().await {
let new_client = self.current_client().await;
ResourceDispatcher::read_resource(new_client.as_ref(), server, uri).await
} else {
Err(forge_error::DispatchError::TransportDead {
server: self.name.clone(),
reason: "reconnection failed".into(),
})
}
}
other => other,
}
}
}