1use std::sync::Arc;
8use std::time::{Duration, Instant};
9
10use thiserror::Error;
11use tracing::{debug, info, warn};
12
13use crate::ha::pool::{CaId, CaPool};
14
15#[derive(Debug, Error)]
17pub enum CaBackendError {
18 #[error("no healthy CA backend available")]
20 NoHealthyBackend,
21
22 #[error("request to CA {ca_id} timed out after {elapsed_ms}ms")]
24 Timeout { ca_id: String, elapsed_ms: u64 },
25
26 #[error("all {attempts} retry attempts exhausted")]
28 RetriesExhausted { attempts: u32 },
29
30 #[error("CA backend error from {ca_id}: {message}")]
32 BackendError { ca_id: String, message: String },
33}
34
35#[derive(Debug, Clone)]
37pub struct CaBackendPoolConfig {
38 pub request_timeout: Duration,
40 pub max_retries: u32,
42 pub keep_alive: bool,
44}
45
46impl Default for CaBackendPoolConfig {
47 fn default() -> Self {
48 Self {
49 request_timeout: Duration::from_secs(30),
50 max_retries: 2,
51 keep_alive: true,
52 }
53 }
54}
55
56pub struct CaBackendPool {
61 ha_pool: Arc<CaPool>,
63 config: CaBackendPoolConfig,
65}
66
67impl CaBackendPool {
68 pub fn new(ha_pool: Arc<CaPool>, config: CaBackendPoolConfig) -> Self {
70 Self { ha_pool, config }
71 }
72
73 pub async fn route_enrollment(
87 &self,
88 csr_der: &[u8],
89 profile: &str,
90 ) -> Result<Vec<u8>, CaBackendError> {
91 let mut attempts = 0u32;
92 let mut last_error = None;
93
94 while attempts <= self.config.max_retries {
95 let ca = self
96 .ha_pool
97 .select()
98 .ok_or(CaBackendError::NoHealthyBackend)?;
99
100 debug!(
101 ca_id = %ca.id,
102 attempt = attempts + 1,
103 profile = %profile,
104 "routing enrollment to CA"
105 );
106
107 let start = Instant::now();
108 match self
109 .send_to_ca(&ca.id, &ca.endpoint, csr_der, profile)
110 .await
111 {
112 Ok(cert_der) => {
113 let elapsed = start.elapsed();
114 self.ha_pool.record_success(&ca.id, elapsed);
115 info!(
116 ca_id = %ca.id,
117 elapsed_ms = elapsed.as_millis(),
118 "enrollment succeeded"
119 );
120 return Ok(cert_der);
121 }
122 Err(e) => {
123 let elapsed = start.elapsed();
124 warn!(
125 ca_id = %ca.id,
126 attempt = attempts + 1,
127 elapsed_ms = elapsed.as_millis(),
128 error = %e,
129 "enrollment attempt failed"
130 );
131 self.ha_pool.record_failure(&ca.id);
132 last_error = Some(e);
133 }
134 }
135
136 attempts += 1;
137 }
138
139 Err(last_error.unwrap_or(CaBackendError::RetriesExhausted {
140 attempts: self.config.max_retries + 1,
141 }))
142 }
143
144 async fn send_to_ca(
149 &self,
150 ca_id: &CaId,
151 endpoint: &str,
152 _csr_der: &[u8],
153 _profile: &str,
154 ) -> Result<Vec<u8>, CaBackendError> {
155 let result = tokio::time::timeout(self.config.request_timeout, async {
157 debug!(
160 ca_id = %ca_id,
161 endpoint = %endpoint,
162 "sending enrollment request (integration pending)"
163 );
164
165 Ok::<Vec<u8>, CaBackendError>(vec![0x30, 0x00])
167 })
168 .await;
169
170 match result {
171 Ok(inner) => inner,
172 Err(_) => Err(CaBackendError::Timeout {
173 ca_id: ca_id.to_string(),
174 elapsed_ms: self.config.request_timeout.as_millis() as u64,
175 }),
176 }
177 }
178
179 pub fn ha_pool(&self) -> &Arc<CaPool> {
181 &self.ha_pool
182 }
183}