1use std::time::Duration;
31
32use tower_mcp::router::RouterResponse;
33use tower_resilience::retry::{RetryBudgetBuilder, RetryLayer};
34
35use crate::config::RetryConfig;
36
37fn is_retriable_error(code: i32) -> bool {
39 code == -32603 || (-32099..=-32000).contains(&code)
42}
43
44fn is_retriable_response(resp: &RouterResponse) -> bool {
46 match &resp.inner {
47 Err(err) => is_retriable_error(err.code),
48 Ok(_) => false,
49 }
50}
51
52pub fn build_retry_layer(
57 config: &RetryConfig,
58 backend_name: &str,
59) -> RetryLayer<tower_mcp::router::RouterRequest, RouterResponse, std::convert::Infallible> {
60 let mut builder = RetryLayer::builder()
61 .max_attempts((config.max_retries + 1) as usize)
63 .exponential_backoff(Duration::from_millis(config.initial_backoff_ms))
64 .retry_on_response(is_retriable_response)
65 .name(format!("retry-{backend_name}"));
66
67 if let Some(percent) = config.budget_percent {
69 let min_per_sec = config.min_retries_per_sec as f64;
76 let max_tokens = ((percent / 100.0) * 1000.0).max(min_per_sec * 10.0) as usize;
77
78 let budget = RetryBudgetBuilder::new()
79 .token_bucket()
80 .tokens_per_second(min_per_sec)
81 .max_tokens(max_tokens.max(1))
82 .initial_tokens(max_tokens.max(1))
83 .build()
84 .expect("nonnegative retry refill and nonzero capacity with matching initial tokens");
85
86 builder = builder.budget(budget);
87 }
88
89 builder.build()
90}
91
92#[cfg(test)]
93mod tests {
94 use super::*;
95 use crate::config::RetryConfig;
96 use crate::test_util::{ErrorMockService, MockService, call_service};
97 use tower::Layer;
98 use tower_mcp_types::protocol::McpRequest;
99
100 fn make_config(max_retries: u32) -> RetryConfig {
101 RetryConfig {
102 max_retries,
103 initial_backoff_ms: 1, max_backoff_ms: 10,
105 budget_percent: None,
106 min_retries_per_sec: 10,
107 }
108 }
109
110 fn make_config_with_budget(max_retries: u32, budget_percent: f64) -> RetryConfig {
111 RetryConfig {
112 max_retries,
113 initial_backoff_ms: 1,
114 max_backoff_ms: 10,
115 budget_percent: Some(budget_percent),
116 min_retries_per_sec: 0,
117 }
118 }
119
120 #[tokio::test]
121 async fn test_retries_internal_error() {
122 let svc = ErrorMockService;
124 let layer = build_retry_layer(&make_config(3), "test");
125 let mut svc = layer.layer(svc);
126
127 let resp = call_service(&mut svc, McpRequest::ListTools(Default::default())).await;
128 assert!(resp.inner.is_err());
130 }
131
132 #[tokio::test]
133 async fn test_does_not_retry_success() {
134 let svc = MockService::with_tools(&["tool1"]);
135 let layer = build_retry_layer(&make_config(3), "test");
136 let mut svc = layer.layer(svc);
137
138 let resp = call_service(&mut svc, McpRequest::ListTools(Default::default())).await;
139 assert!(resp.inner.is_ok());
140 }
141
142 #[tokio::test]
143 async fn test_response_predicate_matches_transient_errors() {
144 use tower_mcp::protocol::RequestId;
146 use tower_mcp_types::JsonRpcError;
147
148 let transient = RouterResponse {
149 id: RequestId::Number(1),
150 inner: Err(JsonRpcError {
151 code: -32603,
152 message: "internal error".to_string(),
153 data: None,
154 }),
155 };
156 assert!(is_retriable_response(&transient));
157
158 let server_err = RouterResponse {
159 id: RequestId::Number(1),
160 inner: Err(JsonRpcError {
161 code: -32000,
162 message: "server error".to_string(),
163 data: None,
164 }),
165 };
166 assert!(is_retriable_response(&server_err));
167
168 let client_err = RouterResponse {
170 id: RequestId::Number(1),
171 inner: Err(JsonRpcError {
172 code: -32601,
173 message: "method not found".to_string(),
174 data: None,
175 }),
176 };
177 assert!(!is_retriable_response(&client_err));
178
179 let success = RouterResponse {
181 id: RequestId::Number(1),
182 inner: Ok(tower_mcp_types::protocol::McpResponse::ListTools(
183 tower_mcp_types::protocol::ListToolsResult {
184 tools: vec![],
185 next_cursor: None,
186 ttl_ms: None,
187 cache_scope: None,
188 meta: None,
189 },
190 )),
191 };
192 assert!(!is_retriable_response(&success));
193 }
194
195 #[tokio::test]
196 async fn test_budget_limits_retries() {
197 let config = make_config_with_budget(10, 1.0); let layer = build_retry_layer(&config, "test");
200
201 let svc = ErrorMockService;
202 let mut svc = layer.layer(svc);
203
204 let resp = call_service(&mut svc, McpRequest::ListTools(Default::default())).await;
206 assert!(resp.inner.is_err());
207 }
208
209 #[tokio::test]
210 async fn test_no_budget_allows_all_retries() {
211 let config = make_config(2); let _layer = build_retry_layer(&config, "test");
213 assert!(config.budget_percent.is_none());
215 }
216}