forge_client/
reconnect.rs1use std::sync::atomic::{AtomicBool, Ordering};
13use std::sync::Arc;
14use std::time::Duration;
15
16use forge_sandbox::{ResourceDispatcher, ToolDispatcher};
17use serde_json::Value;
18use tokio::sync::{Mutex, Notify, RwLock};
19
20use crate::{McpClient, TransportConfig};
21
22pub struct ReconnectingClient {
36 name: String,
37 transport_config: TransportConfig,
38 inner: RwLock<Arc<McpClient>>,
39 reconnecting: AtomicBool,
40 reconnect_done: Notify,
42 max_backoff: Duration,
43 current_backoff: Mutex<Duration>,
44}
45
46impl ReconnectingClient {
47 pub fn new(
49 name: String,
50 transport_config: TransportConfig,
51 client: Arc<McpClient>,
52 max_backoff: Duration,
53 ) -> Self {
54 Self {
55 name,
56 transport_config,
57 inner: RwLock::new(client),
58 reconnecting: AtomicBool::new(false),
59 reconnect_done: Notify::new(),
60 max_backoff,
61 current_backoff: Mutex::new(Duration::from_secs(1)),
62 }
63 }
64
65 async fn try_reconnect(&self) -> bool {
71 if self
73 .reconnecting
74 .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
75 .is_err()
76 {
77 tracing::debug!(server = %self.name, "waiting for concurrent reconnection to complete");
80 let wait_result =
81 tokio::time::timeout(Duration::from_secs(30), self.reconnect_done.notified()).await;
82 if wait_result.is_err() {
83 tracing::warn!(
84 server = %self.name,
85 "timed out waiting for concurrent reconnection"
86 );
87 }
88 return !self.reconnecting.load(Ordering::SeqCst);
91 }
92
93 let backoff = {
95 let guard = self.current_backoff.lock().await;
96 *guard
97 };
98 tracing::info!(
99 server = %self.name,
100 backoff_ms = backoff.as_millis(),
101 "attempting reconnection after backoff"
102 );
103 tokio::time::sleep(backoff).await;
104
105 let success = match McpClient::connect(self.name.clone(), &self.transport_config).await {
107 Ok(new_client) => {
108 tracing::info!(server = %self.name, "reconnection successful");
109 {
111 let mut inner = self.inner.write().await;
112 *inner = Arc::new(new_client);
113 }
114 {
116 let mut guard = self.current_backoff.lock().await;
117 *guard = Duration::from_secs(1);
118 }
119 true
120 }
121 Err(e) => {
122 tracing::warn!(
123 server = %self.name,
124 error = %e,
125 "reconnection failed"
126 );
127 {
129 let mut guard = self.current_backoff.lock().await;
130 *guard = (*guard * 2).min(self.max_backoff);
131 }
132 false
133 }
134 };
135
136 self.reconnecting.store(false, Ordering::SeqCst);
137 self.reconnect_done.notify_waiters();
139 success
140 }
141
142 async fn current_client(&self) -> Arc<McpClient> {
144 self.inner.read().await.clone()
145 }
146}
147
148#[async_trait::async_trait]
149impl ToolDispatcher for ReconnectingClient {
150 async fn call_tool(
151 &self,
152 server: &str,
153 tool: &str,
154 args: Value,
155 ) -> Result<Value, forge_error::DispatchError> {
156 let client = self.current_client().await;
157 let result = client.call_tool(server, tool, args.clone()).await;
158
159 match result {
160 Err(forge_error::DispatchError::TransportDead { .. }) => {
161 tracing::warn!(
162 server = %self.name,
163 tool = %tool,
164 "transport dead, attempting reconnection"
165 );
166 if self.try_reconnect().await {
167 let new_client = self.current_client().await;
169 new_client.call_tool(server, tool, args).await
170 } else {
171 Err(forge_error::DispatchError::TransportDead {
172 server: self.name.clone(),
173 reason: "reconnection failed".into(),
174 })
175 }
176 }
177 other => other,
178 }
179 }
180}
181
182#[async_trait::async_trait]
183impl ResourceDispatcher for ReconnectingClient {
184 async fn read_resource(
185 &self,
186 server: &str,
187 uri: &str,
188 ) -> Result<Value, forge_error::DispatchError> {
189 let client = self.current_client().await;
190 let result = ResourceDispatcher::read_resource(client.as_ref(), server, uri).await;
191
192 match result {
193 Err(forge_error::DispatchError::TransportDead { .. }) => {
194 tracing::warn!(
195 server = %self.name,
196 uri = %uri,
197 "transport dead, attempting reconnection"
198 );
199 if self.try_reconnect().await {
200 let new_client = self.current_client().await;
201 ResourceDispatcher::read_resource(new_client.as_ref(), server, uri).await
202 } else {
203 Err(forge_error::DispatchError::TransportDead {
204 server: self.name.clone(),
205 reason: "reconnection failed".into(),
206 })
207 }
208 }
209 other => other,
210 }
211 }
212}