1use std::collections::{HashMap, VecDeque};
7use std::sync::Arc;
8use std::time::{Duration, Instant};
9
10use parking_lot::{Mutex, RwLock};
11use tracing::{debug, info, warn};
12
13use super::health::HealthState;
14use super::strategy::{FailoverStrategy, FallbackBehavior, StrategySelector};
15
16#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
18pub struct CaId(pub String);
19
20impl std::fmt::Display for CaId {
21 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
22 f.write_str(&self.0)
23 }
24}
25
26impl std::borrow::Borrow<str> for CaId {
27 fn borrow(&self) -> &str {
28 &self.0
29 }
30}
31
32impl std::borrow::Borrow<String> for CaId {
33 fn borrow(&self) -> &String {
34 &self.0
35 }
36}
37
38impl From<String> for CaId {
39 fn from(s: String) -> Self {
40 Self(s)
41 }
42}
43
44impl From<&str> for CaId {
45 fn from(s: &str) -> Self {
46 Self(s.to_owned())
47 }
48}
49
50#[derive(Debug, Clone)]
52pub struct CaStatus {
53 pub health: HealthState,
55 pub consecutive_failures: u32,
57 pub last_success: Option<Instant>,
59 pub circuit_open_since: Option<Instant>,
61 pub latency_ema_ms: f64,
63 pub last_latency: Option<Duration>,
65}
66
67impl CaStatus {
68 fn new() -> Self {
69 Self {
70 health: HealthState::Healthy,
71 consecutive_failures: 0,
72 last_success: None,
73 circuit_open_since: None,
74 latency_ema_ms: 0.0,
75 last_latency: None,
76 }
77 }
78}
79
80#[derive(Debug, Clone)]
82pub struct CaConnection {
83 pub id: CaId,
85 pub endpoint: String,
87 pub weight: u32,
89 pub priority: u32,
91}
92
93#[derive(Debug, Clone)]
95pub struct CircuitBreakerConfig {
96 pub failure_threshold: u32,
98 pub cooldown: Duration,
100}
101
102impl Default for CircuitBreakerConfig {
103 fn default() -> Self {
104 Self {
105 failure_threshold: 3,
106 cooldown: Duration::from_secs(60),
107 }
108 }
109}
110
111#[derive(Debug, Clone)]
113pub struct PoolConfig {
114 pub strategy: FailoverStrategy,
116 pub fallback: FallbackBehavior,
118 pub circuit_breaker: CircuitBreakerConfig,
120}
121
122#[derive(Debug, Clone)]
124pub struct QueueConfig {
125 pub max_queue_size: usize,
129 pub queue_ttl_secs: u64,
133 pub max_csr_size: usize,
137}
138
139impl Default for QueueConfig {
140 fn default() -> Self {
141 Self {
142 max_queue_size: 100,
143 queue_ttl_secs: 300,
144 max_csr_size: 65_536,
145 }
146 }
147}
148
149#[derive(Debug, Clone)]
151pub struct QueuedRequest {
152 pub csr_der: Vec<u8>,
154 pub queued_at: Instant,
156}
157
158#[derive(Debug)]
160pub enum EnqueueResult {
161 Queued {
164 retry_after_secs: u32,
166 queue_depth: usize,
168 },
169 QueueFull {
171 max_size: usize,
173 },
174}
175
176pub struct CaPool {
182 connections: Vec<CaConnection>,
184 statuses: RwLock<HashMap<CaId, CaStatus>>,
186 config: PoolConfig,
188 selector: StrategySelector,
190 retry_queue: Arc<Mutex<VecDeque<QueuedRequest>>>,
193 queue_config: QueueConfig,
195}
196
197impl CaPool {
198 pub fn new(connections: Vec<CaConnection>, config: PoolConfig) -> Self {
200 Self::with_queue_config(connections, config, QueueConfig::default())
201 }
202
203 pub fn with_queue_config(
205 connections: Vec<CaConnection>,
206 config: PoolConfig,
207 queue_config: QueueConfig,
208 ) -> Self {
209 let mut statuses = HashMap::new();
210 for conn in &connections {
211 statuses.insert(conn.id.clone(), CaStatus::new());
212 }
213 let selector = StrategySelector::new(config.strategy.clone());
214
215 Self {
216 connections,
217 statuses: RwLock::new(statuses),
218 config,
219 selector,
220 retry_queue: Arc::new(Mutex::new(VecDeque::new())),
221 queue_config,
222 }
223 }
224
225 pub fn select(&self) -> Option<CaConnection> {
230 self.select_allowed(
231 &self
232 .connections
233 .iter()
234 .map(|c| c.id.0.clone())
235 .collect::<Vec<_>>(),
236 )
237 }
238
239 pub fn select_allowed(&self, allowed: &[String]) -> Option<CaConnection> {
241 let statuses = self.statuses.read();
242 let healthy: Vec<&CaConnection> = self
243 .connections
244 .iter()
245 .filter(|c| {
246 allowed.contains(&c.id.0)
247 && statuses
248 .get(&c.id)
249 .map(|s| s.health.is_available())
250 .unwrap_or(false)
251 })
252 .collect();
253
254 if healthy.is_empty() {
255 warn!("no healthy CA backends available");
256 return match self.config.fallback {
257 FallbackBehavior::Reject => None,
258 FallbackBehavior::QueueAndRetry => {
259 debug!("queue-and-retry fallback active; caller should enqueue");
262 None
263 }
264 };
265 }
266
267 let snapshot: Vec<(&CaConnection, &CaStatus)> = healthy
268 .iter()
269 .filter_map(|c| statuses.get(&c.id).map(|s| (*c, s)))
270 .collect();
271
272 self.selector.select(&snapshot)
273 }
274
275 pub fn record_success(&self, id: &CaId, latency: Duration) {
277 let mut statuses = self.statuses.write();
278 if let Some(status) = statuses.get_mut(id) {
279 status.consecutive_failures = 0;
280 status.last_success = Some(Instant::now());
281 status.circuit_open_since = None;
282 status.last_latency = Some(latency);
283
284 let ms = latency.as_secs_f64() * 1000.0;
285 status.latency_ema_ms = status.latency_ema_ms * 0.7 + ms * 0.3;
287
288 if status.health != HealthState::Healthy {
289 info!(ca = %id, "CA recovered, marking healthy");
290 status.health = HealthState::Healthy;
291 }
292 }
293 }
294
295 pub fn record_failure(&self, id: &CaId) {
297 let mut statuses = self.statuses.write();
298 if let Some(status) = statuses.get_mut(id) {
299 status.consecutive_failures += 1;
300 debug!(
301 ca = %id,
302 failures = status.consecutive_failures,
303 "CA request failed"
304 );
305
306 if status.consecutive_failures >= self.config.circuit_breaker.failure_threshold {
307 if status.health != HealthState::Unavailable {
308 warn!(
309 ca = %id,
310 failures = status.consecutive_failures,
311 "circuit breaker tripped, marking CA unavailable"
312 );
313 status.health = HealthState::Unavailable;
314 status.circuit_open_since = Some(Instant::now());
315 }
316 } else if status.health == HealthState::Healthy {
317 status.health = HealthState::Degraded;
318 }
319 }
320 }
321
322 pub fn should_reprobe(&self, id: &CaId) -> bool {
324 let statuses = self.statuses.read();
325 statuses.get(id).is_some_and(|s| {
326 s.circuit_open_since
327 .is_some_and(|opened| opened.elapsed() >= self.config.circuit_breaker.cooldown)
328 })
329 }
330
331 pub fn set_health(&self, id: &CaId, state: HealthState) {
333 let mut statuses = self.statuses.write();
334 if let Some(status) = statuses.get_mut(id) {
335 let prev = status.health.clone();
336 status.health = state.clone();
337 if prev != state {
338 info!(ca = %id, from = ?prev, to = ?state, "CA health state transition");
339 }
340 }
341 }
342
343 pub fn status_snapshot(&self) -> HashMap<CaId, CaStatus> {
345 self.statuses.read().clone()
346 }
347
348 pub fn connections(&self) -> &[CaConnection] {
350 &self.connections
351 }
352
353 pub fn config(&self) -> &PoolConfig {
355 &self.config
356 }
357
358 pub fn is_queue_and_retry(&self) -> bool {
360 matches!(self.config.fallback, FallbackBehavior::QueueAndRetry)
361 }
362
363 pub fn enqueue_request(&self, csr_der: Vec<u8>) -> EnqueueResult {
369 if csr_der.len() > self.queue_config.max_csr_size {
371 warn!(
372 csr_size = csr_der.len(),
373 max_size = self.queue_config.max_csr_size,
374 "rejecting oversized CSR for queue"
375 );
376 return EnqueueResult::QueueFull {
377 max_size: self.queue_config.max_queue_size,
378 };
379 }
380
381 let mut queue = self.retry_queue.lock();
382
383 let ttl = Duration::from_secs(self.queue_config.queue_ttl_secs);
385 let before = queue.len();
386 queue.retain(|req| req.queued_at.elapsed() < ttl);
387 let expired = before - queue.len();
388 if expired > 0 {
389 debug!(
390 expired,
391 remaining = queue.len(),
392 "drained expired queued requests"
393 );
394 }
395
396 if queue.len() >= self.queue_config.max_queue_size {
398 warn!(
399 max_size = self.queue_config.max_queue_size,
400 "retry queue full, rejecting request"
401 );
402 return EnqueueResult::QueueFull {
403 max_size: self.queue_config.max_queue_size,
404 };
405 }
406
407 queue.push_back(QueuedRequest {
408 csr_der,
409 queued_at: Instant::now(),
410 });
411
412 let depth = queue.len();
413 info!(queue_depth = depth, "request enqueued for retry");
414
415 EnqueueResult::Queued {
416 retry_after_secs: 30,
417 queue_depth: depth,
418 }
419 }
420
421 pub fn drain_queue(&self) -> Vec<QueuedRequest> {
426 let mut queue = self.retry_queue.lock();
427 let ttl = Duration::from_secs(self.queue_config.queue_ttl_secs);
428
429 let mut live = Vec::new();
430 while let Some(req) = queue.pop_front() {
431 if req.queued_at.elapsed() < ttl {
432 live.push(req);
433 } else {
434 debug!("discarding expired queued request");
435 }
436 }
437
438 if !live.is_empty() {
439 info!(count = live.len(), "drained queued requests for retry");
440 }
441
442 live
443 }
444
445 pub fn queue_depth(&self) -> usize {
447 self.retry_queue.lock().len()
448 }
449
450 pub fn queue_config(&self) -> &QueueConfig {
452 &self.queue_config
453 }
454}