1use std::path::Path;
2use std::time::Duration;
3
4use serde::de::DeserializeOwned;
5use serde_json::Value;
6
7use crate::errors::{KodeBridgeError, Result};
8use crate::http_client::{send_request, RequestBuilder, Response};
9use crate::metrics::global_metrics;
10use crate::pool::{ConnectionPool, PoolConfig, PooledConnection};
11use crate::retry::{RetryConfig, RetryExecutor};
12use crate::transport::{Endpoint, IpcStream};
13use bytes::Bytes;
14use http::Method;
15use std::str::FromStr as _;
16use tracing::{debug, trace};
17
18#[derive(Debug, Clone)]
20pub struct ClientConfig {
21 pub default_timeout: Duration,
23 pub pool_config: PoolConfig,
25 pub enable_pooling: bool,
27 pub max_retries: usize,
29 pub retry_delay: Duration,
30 pub max_concurrent_requests: usize,
32 pub max_requests_per_second: Option<f64>,
34}
35
36impl Default for ClientConfig {
37 fn default() -> Self {
38 Self {
39 default_timeout: Duration::from_secs(5), pool_config: PoolConfig::default(),
41 enable_pooling: true,
42 max_retries: 3, retry_delay: Duration::from_millis(25), max_concurrent_requests: 16, max_requests_per_second: Some(50.0), }
47 }
48}
49
50pub struct IpcHttpClient {
55 endpoint: Endpoint,
56 config: ClientConfig,
57 pool: Option<ConnectionPool>,
58 retry_executor: RetryExecutor,
59 put_retry_executor: RetryExecutor,
61}
62
63pub struct HttpRequestBuilder<'a> {
65 client: &'a IpcHttpClient,
66 method: Method,
67 path: String,
68 body: Option<RequestBody>,
69 timeout: Option<Duration>,
70 headers: Vec<(String, String)>,
71 put_optimized: bool,
73 expected_size: Option<usize>,
75}
76
77enum RequestBody {
78 Json(Value),
79 JsonBytes(Bytes),
80}
81
82#[derive(Debug)]
84pub struct HttpResponse {
85 inner: Response,
86}
87
88impl HttpResponse {
89 const fn new(response: Response) -> Self {
90 Self { inner: response }
91 }
92
93 pub const fn status(&self) -> u16 {
95 self.inner.status_code()
96 }
97
98 pub fn headers(&self) -> Value {
100 let headers_map: std::collections::HashMap<String, String> = self
101 .inner
102 .headers()
103 .iter()
104 .map(|(k, v)| (k.to_string(), v.to_str().unwrap_or("").to_string()))
105 .collect();
106 serde_json::to_value(headers_map).unwrap_or(Value::Null)
107 }
108
109 pub fn body(&self) -> Result<String> {
111 self.inner.text()
112 }
113
114 pub fn is_success(&self) -> bool {
116 self.inner.is_success()
117 }
118
119 pub fn is_client_error(&self) -> bool {
121 self.inner.is_client_error()
122 }
123
124 pub fn is_server_error(&self) -> bool {
126 self.inner.is_server_error()
127 }
128
129 pub fn content_length(&self) -> u64 {
131 self.inner.content_length().unwrap_or(0)
132 }
133
134 pub fn json<T>(&self) -> Result<T>
136 where
137 T: DeserializeOwned,
138 {
139 self.inner.json()
140 }
141
142 pub fn json_value(&self) -> Result<Value> {
144 self.inner.json_value()
145 }
146
147 pub fn into_inner(self) -> Response {
149 self.inner
150 }
151
152 pub fn to_legacy(&self) -> crate::response::LegacyResponse {
154 self.inner.to_legacy()
155 }
156}
157
158impl IpcHttpClient {
159 pub fn new<P>(path: P) -> Result<Self>
161 where
162 P: AsRef<Path>,
163 {
164 Self::with_config(path, ClientConfig::default())
165 }
166
167 pub fn with_config<P>(path: P, config: ClientConfig) -> Result<Self>
169 where
170 P: AsRef<Path>,
171 {
172 let endpoint = Endpoint::new(path)?;
173
174 let pool = if config.enable_pooling {
175 Some(ConnectionPool::new(endpoint.clone(), config.pool_config.clone()))
176 } else {
177 None
178 };
179
180 let retry_config = RetryConfig::for_network_operations()
182 .max_attempts(config.max_retries)
183 .base_delay(Duration::from_millis(config.pool_config.retry_delay_ms));
184
185 let retry_executor = RetryExecutor::new(retry_config);
186
187 let put_retry_config = RetryConfig::for_put_requests();
189 let put_retry_executor = RetryExecutor::new(put_retry_config);
190
191 Ok(Self {
192 endpoint,
193 config,
194 pool,
195 retry_executor,
196 put_retry_executor,
197 })
198 }
199
200 async fn create_direct_connection(&self) -> Result<IpcStream> {
202 let mut last_error = None;
203
204 for attempt in 0..self.config.max_retries {
205 if attempt > 0 {
206 tokio::time::sleep(self.config.retry_delay).await;
207 }
208
209 match IpcStream::connect(&self.endpoint).await {
210 Ok(stream) => {
211 debug!("Created direct connection on attempt {}", attempt + 1);
212 return Ok(stream);
213 }
214 Err(e) => {
215 trace!("Connection attempt {} failed: {}", attempt + 1, e);
216 last_error = Some(e);
217 }
218 }
219 }
220
221 Err(KodeBridgeError::connection(format!(
222 "Failed to create connection after {} attempts: {}",
223 self.config.max_retries,
224 last_error
225 .map(|e| e.to_string())
226 .unwrap_or_else(|| "Unknown error".to_string())
227 )))
228 }
229
230 async fn get_connection(&self) -> Result<Either<PooledConnection, IpcStream>> {
232 let metrics = global_metrics();
233
234 if let Some(ref pool) = self.pool {
235 match pool.get_connection().await {
236 Ok(conn) => {
237 metrics.connection_created(true); Ok(Either::Pool(conn))
239 }
240 Err(e) => {
241 metrics.connection_failed();
242 Err(e)
243 }
244 }
245 } else {
246 match self.create_direct_connection().await {
247 Ok(stream) => {
248 metrics.connection_created(false); Ok(Either::Direct(stream))
250 }
251 Err(e) => {
252 metrics.connection_failed();
253 Err(e)
254 }
255 }
256 }
257 }
258
259 pub async fn request(
261 &self,
262 method: &str,
263 path: &str,
264 body: Option<&serde_json::Value>,
265 ) -> crate::errors::AnyResult<crate::response::LegacyResponse> {
266 let response = self
267 .send_request_internal(method, path, body, self.config.default_timeout)
268 .await?;
269 Ok(response.to_legacy())
270 }
271
272 async fn send_request_internal(
274 &self,
275 method: &str,
276 path: &str,
277 body: Option<&Value>,
278 timeout: Duration,
279 ) -> Result<Response> {
280 let method =
281 Method::from_str(method).map_err(|e| KodeBridgeError::invalid_request(format!("Invalid method: {}", e)))?;
282
283 let mut builder = RequestBuilder::new(method.clone(), path.to_string());
284
285 if let Some(json_body) = body {
289 builder = builder.json(json_body)?;
290 }
291
292 let request = builder.build()?;
293
294 self.retry_executor
296 .execute_with_context(&format!("{} {}", method.as_str(), path), || async {
297 let result = tokio::time::timeout(timeout, async {
299 let mut connection = self.get_connection().await?;
300
301 match &mut connection {
302 Either::Pool(conn) => {
303 if let Some(stream) = conn.stream() {
304 let result = send_request(stream, request.clone()).await;
305 if result.is_err() {
306 conn.invalidate();
307 }
308 result
309 } else {
310 conn.invalidate();
311 Err(KodeBridgeError::connection("Pooled connection is invalid"))
312 }
313 }
314 Either::Direct(stream) => send_request(stream, request.clone()).await,
315 }
316 })
317 .await;
318
319 match result {
320 Ok(response) => response,
321 Err(_) => Err(KodeBridgeError::timeout(timeout.as_millis() as u64)),
322 }
323 })
324 .await
325 }
326
327 async fn send_request_with_optimization(
329 &self,
330 method: &str,
331 path: &str,
332 body: Option<&RequestBody>,
333 headers: &[(String, String)],
334 timeout: Duration,
335 is_put_optimized: bool,
336 expected_size: Option<usize>,
337 ) -> Result<Response> {
338 let method_enum =
339 Method::from_str(method).map_err(|e| KodeBridgeError::invalid_request(format!("Invalid method: {}", e)))?;
340
341 let mut builder = RequestBuilder::new(method_enum.clone(), path.to_string());
342
343 for (key, value) in headers {
345 builder = builder.header(key.as_str(), value.as_str());
346 }
347
348 if let Some(body) = body {
349 builder = match body {
350 RequestBody::Json(value) => builder.json(value)?,
351 RequestBody::JsonBytes(bytes) => builder.body_bytes(bytes.clone(), "application/json")?,
352 };
353 }
354
355 let request = builder.build()?;
356
357 let retry_context = if is_put_optimized {
359 format!("PUT_OPTIMIZED {}", path)
360 } else {
361 format!("{} {}", method, path)
362 };
363
364 let retry_executor = if is_put_optimized {
366 &self.put_retry_executor } else {
368 &self.retry_executor };
370
371 retry_executor
372 .execute_with_context(&retry_context, || async {
373 let result = tokio::time::timeout(timeout, async {
375 let mut connection = if is_put_optimized && expected_size.unwrap_or(0) > 10240 {
376 self.get_fresh_connection().await?
378 } else {
379 self.get_connection().await?
380 };
381
382 match &mut connection {
383 Either::Pool(conn) => {
384 if let Some(stream) = conn.stream() {
385 let result = send_request(stream, request.clone()).await;
386 if result.is_err() {
387 conn.invalidate();
388 }
389 result
390 } else {
391 conn.invalidate();
392 Err(KodeBridgeError::connection("Pooled connection is invalid"))
393 }
394 }
395 Either::Direct(stream) => send_request(stream, request.clone()).await,
396 }
397 })
398 .await;
399
400 match result {
401 Ok(response) => response,
402 Err(_) => Err(KodeBridgeError::timeout(timeout.as_millis() as u64)),
403 }
404 })
405 .await
406 }
407
408 async fn get_fresh_connection(&self) -> Result<Either<PooledConnection, IpcStream>> {
410 if let Some(ref pool) = self.pool {
412 match tokio::time::timeout(Duration::from_millis(20), pool.get_fresh_connection()).await {
413 Ok(Ok(conn)) => return Ok(Either::Pool(conn)),
414 Ok(Err(_)) | Err(_) => {
415 }
417 }
418 }
419
420 match tokio::time::timeout(Duration::from_millis(100), IpcStream::connect(&self.endpoint)).await {
422 Ok(Ok(stream)) => Ok(Either::Direct(stream)),
423 Ok(Err(_)) | Err(_) => {
424 if let Some(ref pool) = self.pool {
426 let conn = pool.get_connection().await?;
427 Ok(Either::Pool(conn))
428 } else {
429 Err(KodeBridgeError::connection("Failed to get fresh connection"))
430 }
431 }
432 }
433 }
434
435 pub fn get(&self, path: &str) -> HttpRequestBuilder<'_> {
437 HttpRequestBuilder::new(self, Method::GET, path)
438 }
439
440 pub fn post(&self, path: &str) -> HttpRequestBuilder<'_> {
442 HttpRequestBuilder::new(self, Method::POST, path)
443 }
444
445 pub fn put(&self, path: &str) -> HttpRequestBuilder<'_> {
447 HttpRequestBuilder::new(self, Method::PUT, path)
448 }
449
450 pub async fn put_batch(&self, requests: Vec<(String, Value)>) -> Result<Vec<HttpResponse>> {
452 let batch_size = requests.len();
453 if batch_size == 0 {
454 return Ok(Vec::new());
455 }
456
457 let concurrent_limit = std::cmp::min(self.config.max_concurrent_requests, batch_size);
458 let mut responses = Vec::with_capacity(batch_size);
459 let mut pending = Vec::with_capacity(concurrent_limit);
460
461 for (path, body) in requests {
462 let body = Bytes::from(body.to_string());
463 pending.push(self.put(&path).json_bytes(body).optimize_for_put().send());
464
465 if pending.len() == concurrent_limit {
466 let chunk_results = futures::future::join_all(std::mem::take(&mut pending)).await;
467 for result in chunk_results {
468 match result {
469 Ok(response) => responses.push(response),
470 Err(e) => return Err(e),
471 }
472 }
473 }
474 }
475
476 if !pending.is_empty() {
477 let chunk_results = futures::future::join_all(pending).await;
478 for result in chunk_results {
479 match result {
480 Ok(response) => responses.push(response),
481 Err(e) => return Err(e),
482 }
483 }
484 }
485
486 Ok(responses)
487 }
488
489 pub fn delete(&self, path: &str) -> HttpRequestBuilder<'_> {
491 HttpRequestBuilder::new(self, Method::DELETE, path)
492 }
493
494 pub fn patch(&self, path: &str) -> HttpRequestBuilder<'_> {
496 HttpRequestBuilder::new(self, Method::PATCH, path)
497 }
498
499 pub fn head(&self, path: &str) -> HttpRequestBuilder<'_> {
501 HttpRequestBuilder::new(self, Method::HEAD, path)
502 }
503
504 pub fn options(&self, path: &str) -> HttpRequestBuilder<'_> {
506 HttpRequestBuilder::new(self, Method::OPTIONS, path)
507 }
508
509 pub fn pool_stats(&self) -> Option<crate::pool::PoolStats> {
511 self.pool.as_ref().map(|p| p.stats())
512 }
513
514 pub fn close(&self) {
516 if let Some(ref pool) = self.pool {
517 pool.close();
518 }
519 }
520
521 pub async fn preheat_for_puts(&self, count: usize) {
523 if let Some(ref pool) = self.pool {
524 pool.preheat_for_puts(count).await;
525 }
526 }
527
528 fn calculate_smart_timeout(&self, method: &str, body_size: Option<usize>) -> Duration {
530 match method {
531 "PUT" | "POST" => {
532 match body_size {
533 Some(size) if size > 5 * 1024 * 1024 => Duration::from_secs(30), Some(size) if size > 1024 * 1024 => Duration::from_secs(15), Some(size) if size > 100 * 1024 => Duration::from_secs(8), Some(size) if size > 10 * 1024 => Duration::from_secs(4), _ => Duration::from_secs(2), }
539 }
540 _ => self.config.default_timeout, }
542 }
543}
544
545impl<'a> HttpRequestBuilder<'a> {
546 fn new(client: &'a IpcHttpClient, method: Method, path: &str) -> Self {
547 let is_put = method == Method::PUT;
548 Self {
549 client,
550 method,
551 path: path.to_string(),
552 body: None,
553 timeout: None,
554 headers: Vec::new(),
555 put_optimized: is_put, expected_size: None,
557 }
558 }
559
560 pub fn json_body(mut self, body: &Value) -> Self {
562 self.body = Some(RequestBody::Json(body.clone()));
563 self
564 }
565
566 pub fn json_bytes(mut self, body: Bytes) -> Self {
568 self.expected_size = Some(body.len());
569 self.body = Some(RequestBody::JsonBytes(body));
570 self
571 }
572
573 pub const fn timeout(mut self, timeout: Duration) -> Self {
575 self.timeout = Some(timeout);
576 self
577 }
578
579 pub const fn expected_size(mut self, size: usize) -> Self {
581 self.expected_size = Some(size);
582 self
583 }
584
585 pub const fn optimize_for_put(mut self) -> Self {
587 self.put_optimized = true;
588 self
589 }
590
591 pub fn header<K, V>(mut self, key: K, value: V) -> Self
593 where
594 K: Into<String>,
595 V: Into<String>,
596 {
597 self.headers.push((key.into(), value.into()));
598 self
599 }
600
601 pub async fn send(self) -> Result<HttpResponse> {
603 let metrics = global_metrics();
604 let tracker = metrics.request_start(self.method.as_str());
605
606 let timeout = if self.put_optimized {
608 self.timeout.unwrap_or_else(|| {
609 self.client
610 .calculate_smart_timeout(self.method.as_str(), self.expected_size)
611 })
612 } else {
613 self.timeout.unwrap_or(self.client.config.default_timeout)
614 };
615
616 match self
617 .client
618 .send_request_with_optimization(
619 self.method.as_str(),
620 &self.path,
621 self.body.as_ref(),
622 &self.headers,
623 timeout,
624 self.put_optimized,
625 self.expected_size,
626 )
627 .await
628 {
629 Ok(response) => {
630 tracker.success(response.status_code());
631 Ok(HttpResponse::new(response))
632 }
633 Err(e) => {
634 tracker.failure(&format!("{:?}", e));
635 Err(e)
636 }
637 }
638 }
639}
640
641enum Either<A, B> {
643 Pool(A),
644 Direct(B),
645}
646
647impl Drop for IpcHttpClient {
648 fn drop(&mut self) {
649 self.close();
650 }
651}