perpcity_sdk/transport/
config.rs1use std::time::Duration;
26
27use crate::errors::PerpCityError;
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
31pub enum Strategy {
32 RoundRobin,
34 #[default]
36 LatencyBased,
37 Hedged { fan_out: usize },
40}
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44pub struct CircuitBreakerConfig {
45 pub failure_threshold: u32,
47 pub recovery_timeout: Duration,
49 pub half_open_max_requests: u32,
51}
52
53impl Default for CircuitBreakerConfig {
54 fn default() -> Self {
55 Self {
56 failure_threshold: 3,
57 recovery_timeout: Duration::from_secs(30),
58 half_open_max_requests: 1,
59 }
60 }
61}
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65pub struct RetryConfig {
66 pub max_retries: u32,
68 pub base_delay: Duration,
70}
71
72impl Default for RetryConfig {
73 fn default() -> Self {
74 Self {
75 max_retries: 2,
76 base_delay: Duration::from_millis(100),
77 }
78 }
79}
80
81#[derive(Debug, Clone)]
83pub struct TransportConfig {
84 pub http_endpoints: Vec<String>,
86 pub ws_endpoint: Option<String>,
88 pub request_timeout: Duration,
90 pub strategy: Strategy,
92 pub circuit_breaker: CircuitBreakerConfig,
94 pub retry: RetryConfig,
96}
97
98impl TransportConfig {
99 pub fn builder() -> TransportConfigBuilder {
101 TransportConfigBuilder::default()
102 }
103}
104
105#[derive(Debug, Clone)]
107pub struct TransportConfigBuilder {
108 http_endpoints: Vec<String>,
109 ws_endpoint: Option<String>,
110 request_timeout: Duration,
111 strategy: Strategy,
112 circuit_breaker: CircuitBreakerConfig,
113 retry: RetryConfig,
114}
115
116impl Default for TransportConfigBuilder {
117 fn default() -> Self {
118 Self {
119 http_endpoints: Vec::new(),
120 ws_endpoint: None,
121 request_timeout: Duration::from_secs(5),
122 strategy: Strategy::default(),
123 circuit_breaker: CircuitBreakerConfig::default(),
124 retry: RetryConfig::default(),
125 }
126 }
127}
128
129impl TransportConfigBuilder {
130 pub fn endpoint(mut self, url: impl Into<String>) -> Self {
132 self.http_endpoints.push(url.into());
133 self
134 }
135
136 pub fn ws_endpoint(mut self, url: impl Into<String>) -> Self {
138 self.ws_endpoint = Some(url.into());
139 self
140 }
141
142 pub fn request_timeout(mut self, timeout: Duration) -> Self {
144 self.request_timeout = timeout;
145 self
146 }
147
148 pub fn strategy(mut self, strategy: Strategy) -> Self {
150 self.strategy = strategy;
151 self
152 }
153
154 pub fn circuit_breaker(mut self, config: CircuitBreakerConfig) -> Self {
156 self.circuit_breaker = config;
157 self
158 }
159
160 pub fn retry(mut self, config: RetryConfig) -> Self {
162 self.retry = config;
163 self
164 }
165
166 pub fn build(self) -> crate::Result<TransportConfig> {
170 if self.http_endpoints.is_empty() {
171 return Err(PerpCityError::InvalidConfig {
172 reason: "no HTTP endpoints configured".into(),
173 });
174 }
175 if let Strategy::Hedged { fan_out } = self.strategy
176 && fan_out < 2
177 {
178 return Err(PerpCityError::InvalidConfig {
179 reason: "hedged strategy requires fan_out >= 2".into(),
180 });
181 }
182 Ok(TransportConfig {
183 http_endpoints: self.http_endpoints,
184 ws_endpoint: self.ws_endpoint,
185 request_timeout: self.request_timeout,
186 strategy: self.strategy,
187 circuit_breaker: self.circuit_breaker,
188 retry: self.retry,
189 })
190 }
191}
192
193#[cfg(test)]
194mod tests {
195 use super::*;
196
197 #[test]
198 fn builder_defaults() {
199 let config = TransportConfig::builder()
200 .endpoint("https://rpc1.example.com")
201 .build()
202 .unwrap();
203 assert_eq!(config.http_endpoints.len(), 1);
204 assert!(config.ws_endpoint.is_none());
205 assert_eq!(config.request_timeout, Duration::from_secs(5));
206 assert_eq!(config.strategy, Strategy::LatencyBased);
207 assert_eq!(config.circuit_breaker.failure_threshold, 3);
208 assert_eq!(config.retry.max_retries, 2);
209 }
210
211 #[test]
212 fn builder_all_options() {
213 let config = TransportConfig::builder()
214 .endpoint("https://rpc1.example.com")
215 .endpoint("https://rpc2.example.com")
216 .ws_endpoint("wss://ws.example.com")
217 .request_timeout(Duration::from_millis(500))
218 .strategy(Strategy::Hedged { fan_out: 3 })
219 .circuit_breaker(CircuitBreakerConfig {
220 failure_threshold: 5,
221 recovery_timeout: Duration::from_secs(60),
222 half_open_max_requests: 2,
223 })
224 .retry(RetryConfig {
225 max_retries: 5,
226 base_delay: Duration::from_millis(50),
227 })
228 .build()
229 .unwrap();
230
231 assert_eq!(config.http_endpoints.len(), 2);
232 assert_eq!(config.ws_endpoint.as_deref(), Some("wss://ws.example.com"));
233 assert_eq!(config.request_timeout, Duration::from_millis(500));
234 assert!(matches!(config.strategy, Strategy::Hedged { fan_out: 3 }));
235 assert_eq!(config.circuit_breaker.failure_threshold, 5);
236 assert_eq!(config.retry.max_retries, 5);
237 }
238
239 #[test]
240 fn no_endpoints_errors() {
241 let result = TransportConfig::builder().build();
242 assert!(result.is_err());
243 }
244
245 #[test]
246 fn hedged_fan_out_one_errors() {
247 let result = TransportConfig::builder()
248 .endpoint("https://rpc1.example.com")
249 .strategy(Strategy::Hedged { fan_out: 1 })
250 .build();
251 assert!(result.is_err());
252 }
253}