1use crate::balancer::BalancingStrategy;
8use crate::circuit_breaker::CircuitBreaker;
9use crate::error::{NetError, NetResult};
10use crate::pool::{ConnectionPool, ConnectionPoolBuilder, PoolConfig, PoolStats};
11use crate::proto::aql::aql_service_client::AqlServiceClient;
12use crate::proto::aql::{
13 BatchRequest, BatchResponse, HealthCheckRequest, HealthCheckResponse, QueryRequest,
14 QueryResponse,
15};
16use std::path::PathBuf;
17use std::sync::Arc;
18use std::time::Duration;
19use tonic::codec::CompressionEncoding;
20use tonic::transport::{Certificate, Channel, ClientTlsConfig, Identity};
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
24pub enum CompressionAlgorithm {
25 #[default]
27 Identity,
28 Gzip,
30}
31
32#[derive(Debug, Clone)]
34pub struct CompressionConfig {
35 pub enabled: bool,
37 pub algorithm: CompressionAlgorithm,
39}
40
41impl Default for CompressionConfig {
42 fn default() -> Self {
43 Self {
44 enabled: false,
45 algorithm: CompressionAlgorithm::Identity,
46 }
47 }
48}
49
50impl CompressionConfig {
51 pub fn gzip() -> Self {
53 Self {
54 enabled: true,
55 algorithm: CompressionAlgorithm::Gzip,
56 }
57 }
58
59 fn to_tonic_encoding(&self) -> Option<CompressionEncoding> {
61 if !self.enabled {
62 return None;
63 }
64 match self.algorithm {
65 CompressionAlgorithm::Identity => None,
66 CompressionAlgorithm::Gzip => Some(CompressionEncoding::Gzip),
67 }
68 }
69}
70
71#[derive(Debug, Clone, Default)]
77pub struct TlsClientConfig {
78 pub ca_cert_path: Option<PathBuf>,
80 pub client_cert_path: Option<PathBuf>,
82 pub client_key_path: Option<PathBuf>,
84 pub domain_name: Option<String>,
86 pub skip_verification: bool,
88}
89
90impl TlsClientConfig {
91 pub fn new() -> Self {
93 Self::default()
94 }
95
96 pub fn with_ca_cert(mut self, path: impl Into<PathBuf>) -> Self {
98 self.ca_cert_path = Some(path.into());
99 self
100 }
101
102 pub fn with_client_identity(
104 mut self,
105 cert_path: impl Into<PathBuf>,
106 key_path: impl Into<PathBuf>,
107 ) -> Self {
108 self.client_cert_path = Some(cert_path.into());
109 self.client_key_path = Some(key_path.into());
110 self
111 }
112
113 pub fn with_domain_name(mut self, domain: impl Into<String>) -> Self {
115 self.domain_name = Some(domain.into());
116 self
117 }
118
119 pub fn with_skip_verification(mut self, skip: bool) -> Self {
124 self.skip_verification = skip;
125 self
126 }
127
128 pub fn validate(&self) -> NetResult<()> {
133 match (&self.client_cert_path, &self.client_key_path) {
135 (Some(_), None) => {
136 return Err(NetError::TlsError(
137 "Client certificate specified without client key".to_string(),
138 ));
139 }
140 (None, Some(_)) => {
141 return Err(NetError::TlsError(
142 "Client key specified without client certificate".to_string(),
143 ));
144 }
145 _ => {}
146 }
147 Ok(())
148 }
149
150 pub fn build_tonic_tls_config(&self) -> NetResult<ClientTlsConfig> {
155 self.validate()?;
156
157 let mut tls_config = ClientTlsConfig::new();
158
159 if let Some(ref ca_path) = self.ca_cert_path {
161 let ca_pem = std::fs::read(ca_path).map_err(|e| {
162 NetError::TlsError(format!(
163 "Failed to read CA certificate file '{}': {}",
164 ca_path.display(),
165 e
166 ))
167 })?;
168 let ca_cert = Certificate::from_pem(ca_pem);
169 tls_config = tls_config.ca_certificate(ca_cert);
170 }
171
172 if let Some(ref cert_path) = self.client_cert_path {
174 let key_path = self.client_key_path.as_ref().ok_or_else(|| {
175 NetError::TlsError("Client key path missing for mTLS".to_string())
176 })?;
177
178 let cert_pem = std::fs::read(cert_path).map_err(|e| {
179 NetError::TlsError(format!(
180 "Failed to read client certificate file '{}': {}",
181 cert_path.display(),
182 e
183 ))
184 })?;
185 let key_pem = std::fs::read(key_path).map_err(|e| {
186 NetError::TlsError(format!(
187 "Failed to read client key file '{}': {}",
188 key_path.display(),
189 e
190 ))
191 })?;
192
193 let identity = Identity::from_pem(cert_pem, key_pem);
194 tls_config = tls_config.identity(identity);
195 }
196
197 if let Some(ref domain) = self.domain_name {
199 tls_config = tls_config.domain_name(domain.clone());
200 }
201
202 Ok(tls_config)
203 }
204}
205
206#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
208pub enum RetryPolicy {
209 Never,
211 OnError,
213 #[default]
215 OnTransient,
216}
217
218impl RetryPolicy {
219 pub fn should_retry(&self, error: &NetError) -> bool {
221 match self {
222 RetryPolicy::Never => false,
223 RetryPolicy::OnError => true,
224 RetryPolicy::OnTransient => error.is_retryable(),
225 }
226 }
227}
228
229#[derive(Debug, Clone)]
231pub struct RetryConfig {
232 pub max_retries: usize,
234 pub initial_backoff: Duration,
236 pub max_backoff: Duration,
238 pub backoff_multiplier: f64,
240 pub policy: RetryPolicy,
242}
243
244impl Default for RetryConfig {
245 fn default() -> Self {
246 Self {
247 max_retries: 3,
248 initial_backoff: Duration::from_millis(100),
249 max_backoff: Duration::from_secs(10),
250 backoff_multiplier: 2.0,
251 policy: RetryPolicy::OnTransient,
252 }
253 }
254}
255
256impl RetryConfig {
257 pub fn backoff_duration(&self, attempt: usize) -> Duration {
262 let base_ms =
263 self.initial_backoff.as_millis() as f64 * self.backoff_multiplier.powi(attempt as i32);
264 let capped_ms = base_ms.min(self.max_backoff.as_millis() as f64);
265
266 let jitter_factor = ((attempt as f64 * 0.618033988) % 1.0) * 0.25;
269 let jittered_ms = capped_ms * (1.0 + jitter_factor);
270 let final_ms = jittered_ms.min(self.max_backoff.as_millis() as f64);
271
272 Duration::from_millis(final_ms as u64)
273 }
274
275 pub fn no_retry() -> Self {
277 Self {
278 max_retries: 0,
279 policy: RetryPolicy::Never,
280 ..Default::default()
281 }
282 }
283}
284
285#[derive(Debug, Clone)]
287pub struct ClientConfig {
288 pub connect_timeout: Duration,
290 pub request_timeout: Duration,
292 pub keep_alive: bool,
294 pub keep_alive_interval: Duration,
296 pub pool: PoolConfig,
298 pub retry: RetryConfig,
300 pub compression: CompressionConfig,
302 pub tls: Option<TlsClientConfig>,
304}
305
306impl Default for ClientConfig {
307 fn default() -> Self {
308 Self {
309 connect_timeout: Duration::from_secs(10),
310 request_timeout: Duration::from_secs(30),
311 keep_alive: true,
312 keep_alive_interval: Duration::from_secs(60),
313 pool: PoolConfig::default(),
314 retry: RetryConfig::default(),
315 compression: CompressionConfig::default(),
316 tls: None,
317 }
318 }
319}
320
321pub struct AqlClient {
323 pool: Arc<ConnectionPool>,
324 config: ClientConfig,
325 circuit_breaker: Option<CircuitBreaker>,
326}
327
328impl AqlClient {
329 pub fn new() -> Self {
333 Self::with_config(ClientConfig::default())
335 .expect("Default ClientConfig should always be valid")
336 }
337
338 pub fn with_config(config: ClientConfig) -> NetResult<Self> {
345 let cb = if config.pool.enable_circuit_breaker {
346 Some(CircuitBreaker::new())
347 } else {
348 None
349 };
350
351 let pool = if let Some(ref tls_cfg) = config.tls {
352 let tonic_tls = tls_cfg.build_tonic_tls_config()?;
353 ConnectionPool::with_tls(config.pool.clone(), tonic_tls)
354 } else {
355 ConnectionPool::new(config.pool.clone())
356 };
357
358 Ok(Self {
359 pool: Arc::new(pool),
360 config,
361 circuit_breaker: cb,
362 })
363 }
364
365 pub fn builder() -> AqlClientBuilder {
367 AqlClientBuilder::new()
368 }
369
370 pub fn add_endpoint(&self, id: String, address: String) {
372 self.pool.add_endpoint(id, address);
373 }
374
375 pub fn add_endpoint_with_weight(&self, id: String, address: String, weight: u32) {
377 self.pool.add_endpoint_with_weight(id, address, weight);
378 }
379
380 pub fn remove_endpoint(&self, endpoint_id: &str) -> bool {
382 self.pool.remove_endpoint(endpoint_id)
383 }
384
385 pub async fn get_service_client(&self) -> NetResult<AqlServiceClient<Channel>> {
390 let conn = self.pool.get_connection().await?;
391 let mut client = AqlServiceClient::new(conn.channel().clone());
392
393 #[cfg(feature = "compression")]
394 if let Some(encoding) = self.config.compression.to_tonic_encoding() {
395 client = client.send_compressed(encoding);
396 client = client.accept_compressed(encoding);
397 }
398
399 Ok(client)
400 }
401
402 pub async fn execute_query(&self, request: QueryRequest) -> NetResult<QueryResponse> {
404 self.execute_with_retry(|mut client| {
405 let req = request.clone();
406 Box::pin(async move {
407 client
408 .execute_query(req)
409 .await
410 .map(|resp| resp.into_inner())
411 .map_err(NetError::from)
412 })
413 })
414 .await
415 }
416
417 pub async fn execute_batch(&self, request: BatchRequest) -> NetResult<BatchResponse> {
419 self.execute_with_retry(|mut client| {
420 let req = request.clone();
421 Box::pin(async move {
422 client
423 .execute_batch(req)
424 .await
425 .map(|resp| resp.into_inner())
426 .map_err(NetError::from)
427 })
428 })
429 .await
430 }
431
432 pub async fn health_check(&self, service: Option<String>) -> NetResult<HealthCheckResponse> {
434 self.execute_with_retry(|mut client| {
435 let svc = service.clone();
436 Box::pin(async move {
437 let request = HealthCheckRequest { service: svc };
438 client
439 .health_check(request)
440 .await
441 .map(|resp| resp.into_inner())
442 .map_err(NetError::from)
443 })
444 })
445 .await
446 }
447
448 pub async fn execute_with_retry<F, T>(&self, operation: F) -> NetResult<T>
453 where
454 F: Fn(
455 AqlServiceClient<Channel>,
456 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = NetResult<T>> + Send>>,
457 T: Send + 'static,
458 {
459 let retry_config = &self.config.retry;
460 let mut last_error: Option<NetError> = None;
461
462 for attempt in 0..=retry_config.max_retries {
463 if let Some(ref cb) = self.circuit_breaker {
465 cb.is_request_allowed()?;
466 }
467
468 let client = match self.get_service_client().await {
469 Ok(c) => c,
470 Err(e) => {
471 if let Some(ref cb) = self.circuit_breaker {
472 cb.record_failure();
473 }
474 if attempt < retry_config.max_retries && retry_config.policy.should_retry(&e) {
475 last_error = Some(e);
476 let backoff = retry_config.backoff_duration(attempt);
477 tokio::time::sleep(backoff).await;
478 continue;
479 }
480 return Err(e);
481 }
482 };
483
484 match operation(client).await {
485 Ok(result) => {
486 if let Some(ref cb) = self.circuit_breaker {
487 cb.record_success();
488 }
489 return Ok(result);
490 }
491 Err(e) => {
492 if let Some(ref cb) = self.circuit_breaker {
493 cb.record_failure();
494 }
495 if attempt < retry_config.max_retries && retry_config.policy.should_retry(&e) {
496 last_error = Some(e);
497 let backoff = retry_config.backoff_duration(attempt);
498 tokio::time::sleep(backoff).await;
499 continue;
500 }
501 return Err(e);
502 }
503 }
504 }
505
506 Err(last_error.unwrap_or_else(|| {
508 NetError::Unknown("Retry loop exhausted without producing a result".to_string())
509 }))
510 }
511
512 pub fn pool_stats(&self) -> PoolStats {
514 self.pool.stats()
515 }
516
517 pub fn circuit_breaker_stats(&self) -> Option<crate::circuit_breaker::CircuitBreakerStats> {
519 self.pool.circuit_breaker_stats()
520 }
521
522 pub fn retry_config(&self) -> &RetryConfig {
524 &self.config.retry
525 }
526
527 pub fn compression_config(&self) -> &CompressionConfig {
529 &self.config.compression
530 }
531
532 pub fn tls_config(&self) -> Option<&TlsClientConfig> {
534 self.config.tls.as_ref()
535 }
536
537 pub async fn drain(&self) -> NetResult<()> {
539 self.pool.drain().await
540 }
541
542 pub async fn shutdown(self) -> NetResult<()> {
544 Arc::try_unwrap(self.pool)
545 .map_err(|_| {
546 NetError::ServerInternal("Cannot shutdown: pool still has references".to_string())
547 })?
548 .shutdown()
549 .await
550 }
551}
552
553impl Default for AqlClient {
554 fn default() -> Self {
555 Self::new()
556 }
557}
558
559pub struct AqlClientBuilder {
561 config: ClientConfig,
562 pool_builder: ConnectionPoolBuilder,
563 circuit_breaker: Option<CircuitBreaker>,
564 tls_client_config: Option<TlsClientConfig>,
565}
566
567impl AqlClientBuilder {
568 pub fn new() -> Self {
570 Self {
571 config: ClientConfig::default(),
572 pool_builder: ConnectionPoolBuilder::new(),
573 circuit_breaker: None,
574 tls_client_config: None,
575 }
576 }
577
578 pub fn connect_timeout(mut self, timeout: Duration) -> Self {
580 self.config.connect_timeout = timeout;
581 self.pool_builder = self.pool_builder.connect_timeout(timeout);
582 self
583 }
584
585 pub fn request_timeout(mut self, timeout: Duration) -> Self {
587 self.config.request_timeout = timeout;
588 self
589 }
590
591 pub fn keep_alive(mut self, enabled: bool) -> Self {
593 self.config.keep_alive = enabled;
594 self
595 }
596
597 pub fn keep_alive_interval(mut self, interval: Duration) -> Self {
599 self.config.keep_alive_interval = interval;
600 self
601 }
602
603 pub fn min_pool_size(mut self, size: usize) -> Self {
605 self.config.pool.min_size = size;
606 self.pool_builder = self.pool_builder.min_size(size);
607 self
608 }
609
610 pub fn max_pool_size(mut self, size: usize) -> Self {
612 self.config.pool.max_size = size;
613 self.pool_builder = self.pool_builder.max_size(size);
614 self
615 }
616
617 pub fn idle_timeout(mut self, timeout: Duration) -> Self {
619 self.config.pool.idle_timeout = timeout;
620 self.pool_builder = self.pool_builder.idle_timeout(timeout);
621 self
622 }
623
624 pub fn max_lifetime(mut self, lifetime: Duration) -> Self {
626 self.config.pool.max_lifetime = lifetime;
627 self.pool_builder = self.pool_builder.max_lifetime(lifetime);
628 self
629 }
630
631 pub fn health_check_interval(mut self, interval: Duration) -> Self {
633 self.config.pool.health_check_interval = interval;
634 self.pool_builder = self.pool_builder.health_check_interval(interval);
635 self
636 }
637
638 pub fn balancing_strategy(mut self, strategy: BalancingStrategy) -> Self {
640 self.config.pool.balancing_strategy = strategy;
641 self.pool_builder = self.pool_builder.balancing_strategy(strategy);
642 self
643 }
644
645 pub fn circuit_breaker(mut self, enabled: bool) -> Self {
647 self.config.pool.enable_circuit_breaker = enabled;
648 self.pool_builder = self.pool_builder.circuit_breaker(enabled);
649 if enabled {
650 self.circuit_breaker = Some(CircuitBreaker::new());
651 } else {
652 self.circuit_breaker = None;
653 }
654 self
655 }
656
657 pub fn with_retry(mut self, retry_config: RetryConfig) -> Self {
659 self.config.retry = retry_config;
660 self
661 }
662
663 pub fn with_compression(mut self, compression_config: CompressionConfig) -> Self {
665 self.config.compression = compression_config;
666 self
667 }
668
669 pub fn with_timeout(mut self, timeout: Duration) -> Self {
671 self.config.request_timeout = timeout;
672 self
673 }
674
675 pub fn with_tls_config(mut self, tls_config: TlsClientConfig) -> Self {
680 self.tls_client_config = Some(tls_config);
681 self
682 }
683
684 pub fn with_ca_cert(mut self, ca_cert_path: impl Into<PathBuf>) -> Self {
689 let config = self
690 .tls_client_config
691 .take()
692 .unwrap_or_default()
693 .with_ca_cert(ca_cert_path);
694 self.tls_client_config = Some(config);
695 self
696 }
697
698 pub fn with_mtls(
703 mut self,
704 cert_path: impl Into<PathBuf>,
705 key_path: impl Into<PathBuf>,
706 ) -> Self {
707 let config = self
708 .tls_client_config
709 .take()
710 .unwrap_or_default()
711 .with_client_identity(cert_path, key_path);
712 self.tls_client_config = Some(config);
713 self
714 }
715
716 pub fn with_tls_domain(mut self, domain: impl Into<String>) -> Self {
721 let config = self
722 .tls_client_config
723 .take()
724 .unwrap_or_default()
725 .with_domain_name(domain);
726 self.tls_client_config = Some(config);
727 self
728 }
729
730 pub fn with_tls_skip_verification(mut self) -> Self {
735 let config = self
736 .tls_client_config
737 .take()
738 .unwrap_or_default()
739 .with_skip_verification(true);
740 self.tls_client_config = Some(config);
741 self
742 }
743
744 pub fn add_endpoint(mut self, id: String, address: String) -> Self {
746 self.pool_builder = self.pool_builder.add_endpoint(id, address);
747 self
748 }
749
750 pub fn add_endpoint_with_weight(mut self, id: String, address: String, weight: u32) -> Self {
752 self.pool_builder = self
753 .pool_builder
754 .add_endpoint_with_weight(id, address, weight);
755 self
756 }
757
758 pub fn build(self) -> NetResult<AqlClient> {
766 let pool_builder = if let Some(ref tls_cfg) = self.tls_client_config {
767 let tonic_tls = tls_cfg.build_tonic_tls_config()?;
768 self.pool_builder.tls_config(tonic_tls)
769 } else {
770 self.pool_builder
771 };
772
773 let pool = pool_builder.build();
774 let cb = if self.config.pool.enable_circuit_breaker {
775 self.circuit_breaker.or_else(|| Some(CircuitBreaker::new()))
776 } else {
777 None
778 };
779
780 let mut config = self.config;
781 config.tls = self.tls_client_config;
782
783 Ok(AqlClient {
784 pool: Arc::new(pool),
785 config,
786 circuit_breaker: cb,
787 })
788 }
789}
790
791impl Default for AqlClientBuilder {
792 fn default() -> Self {
793 Self::new()
794 }
795}
796
797#[cfg(test)]
798mod tests {
799 use super::*;
800
801 #[test]
802 fn test_client_config_default() {
803 let config = ClientConfig::default();
804 assert_eq!(config.connect_timeout, Duration::from_secs(10));
805 assert_eq!(config.request_timeout, Duration::from_secs(30));
806 assert!(config.keep_alive);
807 assert_eq!(config.retry.max_retries, 3);
809 assert_eq!(config.retry.initial_backoff, Duration::from_millis(100));
810 assert_eq!(config.retry.max_backoff, Duration::from_secs(10));
811 assert!((config.retry.backoff_multiplier - 2.0).abs() < f64::EPSILON);
812 assert!(!config.compression.enabled);
813 assert!(config.tls.is_none());
814 }
815
816 #[tokio::test]
817 async fn test_client_creation() {
818 let config = ClientConfig::default();
819 let _client = AqlClient::with_config(config).expect("default config should be valid");
820 }
821
822 #[tokio::test]
823 async fn test_client_builder() {
824 let client = AqlClient::builder()
825 .connect_timeout(Duration::from_secs(5))
826 .request_timeout(Duration::from_secs(15))
827 .min_pool_size(3)
828 .max_pool_size(15)
829 .balancing_strategy(BalancingStrategy::RoundRobin)
830 .add_endpoint("ep1".to_string(), "localhost:50051".to_string())
831 .add_endpoint("ep2".to_string(), "localhost:50052".to_string())
832 .build()
833 .expect("builder should succeed without TLS");
834
835 let stats = client.pool_stats();
836 assert_eq!(stats.active_connections, 0);
837 }
838
839 #[tokio::test]
840 async fn test_client_add_remove_endpoint() {
841 let client = AqlClient::new();
842
843 client.add_endpoint("ep1".to_string(), "localhost:50051".to_string());
844 client.add_endpoint("ep2".to_string(), "localhost:50052".to_string());
845
846 assert!(client.remove_endpoint("ep1"));
847 assert!(!client.remove_endpoint("ep3"));
848 }
849
850 #[tokio::test]
851 async fn test_client_pool_stats() {
852 let client = AqlClient::builder()
853 .add_endpoint("ep1".to_string(), "localhost:50051".to_string())
854 .build()
855 .expect("builder should succeed");
856
857 let stats = client.pool_stats();
858 assert_eq!(stats.total_connections, 0);
859 }
860
861 #[tokio::test]
862 async fn test_client_drain() {
863 let client = AqlClient::builder()
864 .add_endpoint("ep1".to_string(), "localhost:50051".to_string())
865 .build()
866 .expect("builder should succeed");
867
868 let result = client.drain().await;
869 assert!(result.is_ok());
870 }
871
872 #[test]
875 fn test_retry_config_defaults() {
876 let config = RetryConfig::default();
877 assert_eq!(config.max_retries, 3);
878 assert_eq!(config.initial_backoff, Duration::from_millis(100));
879 assert_eq!(config.max_backoff, Duration::from_secs(10));
880 assert!((config.backoff_multiplier - 2.0).abs() < f64::EPSILON);
881 assert_eq!(config.policy, RetryPolicy::OnTransient);
882 }
883
884 #[test]
885 fn test_retry_config_no_retry() {
886 let config = RetryConfig::no_retry();
887 assert_eq!(config.max_retries, 0);
888 assert_eq!(config.policy, RetryPolicy::Never);
889 }
890
891 #[test]
892 fn test_retry_config_custom() {
893 let config = RetryConfig {
894 max_retries: 5,
895 initial_backoff: Duration::from_millis(200),
896 max_backoff: Duration::from_secs(30),
897 backoff_multiplier: 3.0,
898 policy: RetryPolicy::OnError,
899 };
900 assert_eq!(config.max_retries, 5);
901 assert_eq!(config.initial_backoff, Duration::from_millis(200));
902 assert_eq!(config.max_backoff, Duration::from_secs(30));
903 assert!((config.backoff_multiplier - 3.0).abs() < f64::EPSILON);
904 assert_eq!(config.policy, RetryPolicy::OnError);
905 }
906
907 #[test]
910 fn test_backoff_duration_exponential_growth() {
911 let config = RetryConfig {
912 initial_backoff: Duration::from_millis(100),
913 backoff_multiplier: 2.0,
914 max_backoff: Duration::from_secs(60),
915 ..Default::default()
916 };
917
918 let d0 = config.backoff_duration(0);
920 let d1 = config.backoff_duration(1);
922 let d2 = config.backoff_duration(2);
924
925 assert!(d0.as_millis() >= 100, "d0 should be >= 100ms, got {d0:?}");
927 assert!(
928 d0.as_millis() <= 130,
929 "d0 should be <= 130ms (jitter), got {d0:?}"
930 );
931 assert!(d1.as_millis() >= 200, "d1 should be >= 200ms, got {d1:?}");
932 assert!(
933 d1.as_millis() <= 260,
934 "d1 should be <= 260ms (jitter), got {d1:?}"
935 );
936 assert!(d2.as_millis() >= 400, "d2 should be >= 400ms, got {d2:?}");
937 assert!(
938 d2.as_millis() <= 520,
939 "d2 should be <= 520ms (jitter), got {d2:?}"
940 );
941 }
942
943 #[test]
944 fn test_backoff_duration_capped_at_max() {
945 let config = RetryConfig {
946 initial_backoff: Duration::from_secs(1),
947 backoff_multiplier: 10.0,
948 max_backoff: Duration::from_secs(5),
949 ..Default::default()
950 };
951
952 let d0 = config.backoff_duration(0);
954 assert!(d0.as_millis() >= 1000);
955 assert!(d0.as_millis() <= 1300);
956
957 let d2 = config.backoff_duration(2);
959 assert!(
960 d2.as_millis() <= 5000,
961 "Should be capped at max_backoff, got {d2:?}"
962 );
963 }
964
965 #[test]
966 fn test_backoff_duration_with_multiplier_one() {
967 let config = RetryConfig {
968 initial_backoff: Duration::from_millis(500),
969 backoff_multiplier: 1.0,
970 max_backoff: Duration::from_secs(60),
971 ..Default::default()
972 };
973
974 let d0 = config.backoff_duration(0);
976 let d1 = config.backoff_duration(1);
977 let d2 = config.backoff_duration(2);
978
979 assert!(d0.as_millis() >= 500 && d0.as_millis() <= 650);
980 assert!(d1.as_millis() >= 500 && d1.as_millis() <= 650);
981 assert!(d2.as_millis() >= 500 && d2.as_millis() <= 650);
982 }
983
984 #[test]
987 fn test_retry_policy_never() {
988 let policy = RetryPolicy::Never;
989 assert!(!policy.should_retry(&NetError::Timeout("test".to_string())));
990 assert!(!policy.should_retry(&NetError::ServerUnavailable("test".to_string())));
991 assert!(!policy.should_retry(&NetError::InvalidRequest("test".to_string())));
992 }
993
994 #[test]
995 fn test_retry_policy_on_error() {
996 let policy = RetryPolicy::OnError;
997 assert!(policy.should_retry(&NetError::Timeout("test".to_string())));
998 assert!(policy.should_retry(&NetError::ServerUnavailable("test".to_string())));
999 assert!(policy.should_retry(&NetError::InvalidRequest("test".to_string())));
1000 assert!(policy.should_retry(&NetError::AuthFailed("test".to_string())));
1001 }
1002
1003 #[test]
1004 fn test_retry_policy_on_transient() {
1005 let policy = RetryPolicy::OnTransient;
1006
1007 assert!(policy.should_retry(&NetError::Timeout("test".to_string())));
1009 assert!(policy.should_retry(&NetError::ConnectionRefused("test".to_string())));
1010 assert!(policy.should_retry(&NetError::ConnectionReset("test".to_string())));
1011 assert!(policy.should_retry(&NetError::ServerUnavailable("test".to_string())));
1012 assert!(policy.should_retry(&NetError::ServerOverloaded("test".to_string())));
1013
1014 assert!(!policy.should_retry(&NetError::InvalidRequest("test".to_string())));
1016 assert!(!policy.should_retry(&NetError::AuthFailed("test".to_string())));
1017 assert!(!policy.should_retry(&NetError::InsufficientPermissions("test".to_string())));
1018 assert!(!policy.should_retry(&NetError::MalformedMessage("test".to_string())));
1019 assert!(!policy.should_retry(&NetError::ServerInternal("test".to_string())));
1020 }
1021
1022 #[test]
1023 fn test_retry_policy_default_is_on_transient() {
1024 let policy = RetryPolicy::default();
1025 assert_eq!(policy, RetryPolicy::OnTransient);
1026 }
1027
1028 #[test]
1031 fn test_compression_config_default() {
1032 let config = CompressionConfig::default();
1033 assert!(!config.enabled);
1034 assert_eq!(config.algorithm, CompressionAlgorithm::Identity);
1035 assert!(config.to_tonic_encoding().is_none());
1036 }
1037
1038 #[test]
1039 fn test_compression_config_gzip() {
1040 let config = CompressionConfig::gzip();
1041 assert!(config.enabled);
1042 assert_eq!(config.algorithm, CompressionAlgorithm::Gzip);
1043 assert!(config.to_tonic_encoding().is_some());
1044 }
1045
1046 #[test]
1047 fn test_compression_identity_returns_none() {
1048 let config = CompressionConfig {
1049 enabled: true,
1050 algorithm: CompressionAlgorithm::Identity,
1051 };
1052 assert!(config.to_tonic_encoding().is_none());
1054 }
1055
1056 #[test]
1057 fn test_compression_disabled_returns_none() {
1058 let config = CompressionConfig {
1059 enabled: false,
1060 algorithm: CompressionAlgorithm::Gzip,
1061 };
1062 assert!(config.to_tonic_encoding().is_none());
1063 }
1064
1065 #[tokio::test]
1068 async fn test_builder_with_retry() {
1069 let retry = RetryConfig {
1070 max_retries: 5,
1071 initial_backoff: Duration::from_millis(50),
1072 max_backoff: Duration::from_secs(5),
1073 backoff_multiplier: 1.5,
1074 policy: RetryPolicy::OnError,
1075 };
1076
1077 let client = AqlClient::builder()
1078 .with_retry(retry)
1079 .add_endpoint("ep1".to_string(), "localhost:50051".to_string())
1080 .build()
1081 .expect("builder should succeed");
1082
1083 assert_eq!(client.retry_config().max_retries, 5);
1084 assert_eq!(
1085 client.retry_config().initial_backoff,
1086 Duration::from_millis(50)
1087 );
1088 assert_eq!(client.retry_config().policy, RetryPolicy::OnError);
1089 }
1090
1091 #[tokio::test]
1092 async fn test_builder_with_compression() {
1093 let client = AqlClient::builder()
1094 .with_compression(CompressionConfig::gzip())
1095 .add_endpoint("ep1".to_string(), "localhost:50051".to_string())
1096 .build()
1097 .expect("builder should succeed");
1098
1099 assert!(client.compression_config().enabled);
1100 assert_eq!(
1101 client.compression_config().algorithm,
1102 CompressionAlgorithm::Gzip
1103 );
1104 }
1105
1106 #[tokio::test]
1107 async fn test_builder_with_timeout() {
1108 let client = AqlClient::builder()
1109 .with_timeout(Duration::from_secs(60))
1110 .add_endpoint("ep1".to_string(), "localhost:50051".to_string())
1111 .build()
1112 .expect("builder should succeed");
1113
1114 assert_eq!(client.config.request_timeout, Duration::from_secs(60));
1115 }
1116
1117 #[tokio::test]
1118 async fn test_builder_full_chain() {
1119 let client = AqlClient::builder()
1120 .connect_timeout(Duration::from_secs(5))
1121 .request_timeout(Duration::from_secs(15))
1122 .keep_alive(true)
1123 .keep_alive_interval(Duration::from_secs(30))
1124 .min_pool_size(2)
1125 .max_pool_size(20)
1126 .idle_timeout(Duration::from_secs(120))
1127 .max_lifetime(Duration::from_secs(600))
1128 .health_check_interval(Duration::from_secs(10))
1129 .balancing_strategy(BalancingStrategy::RoundRobin)
1130 .circuit_breaker(true)
1131 .with_retry(RetryConfig {
1132 max_retries: 5,
1133 policy: RetryPolicy::OnTransient,
1134 ..Default::default()
1135 })
1136 .with_compression(CompressionConfig::gzip())
1137 .with_timeout(Duration::from_secs(20))
1138 .add_endpoint("ep1".to_string(), "localhost:50051".to_string())
1139 .add_endpoint_with_weight("ep2".to_string(), "localhost:50052".to_string(), 3)
1140 .build()
1141 .expect("builder should succeed");
1142
1143 assert_eq!(client.config.connect_timeout, Duration::from_secs(5));
1144 assert_eq!(client.config.request_timeout, Duration::from_secs(20));
1146 assert!(client.config.keep_alive);
1147 assert_eq!(client.retry_config().max_retries, 5);
1148 assert!(client.compression_config().enabled);
1149 }
1150
1151 #[tokio::test]
1154 async fn test_circuit_breaker_blocks_retries() {
1155 use crate::circuit_breaker::CircuitBreakerConfig;
1156
1157 let cb_config = CircuitBreakerConfig {
1159 failure_threshold: 2,
1160 ..Default::default()
1161 };
1162
1163 let cb = CircuitBreaker::with_config(cb_config);
1164
1165 cb.is_request_allowed().ok();
1167 cb.record_failure();
1168 cb.is_request_allowed().ok();
1169 cb.record_failure();
1170
1171 assert_eq!(cb.state(), crate::circuit_breaker::CircuitState::Open);
1173
1174 assert!(cb.is_request_allowed().is_err());
1176 }
1177
1178 #[tokio::test]
1179 async fn test_circuit_breaker_enabled_in_builder() {
1180 let client = AqlClient::builder()
1181 .circuit_breaker(true)
1182 .add_endpoint("ep1".to_string(), "localhost:50051".to_string())
1183 .build()
1184 .expect("builder should succeed");
1185
1186 assert!(client.circuit_breaker.is_some());
1187 }
1188
1189 #[tokio::test]
1190 async fn test_circuit_breaker_disabled_in_builder() {
1191 let client = AqlClient::builder()
1192 .circuit_breaker(false)
1193 .add_endpoint("ep1".to_string(), "localhost:50051".to_string())
1194 .build()
1195 .expect("builder should succeed");
1196
1197 assert!(client.circuit_breaker.is_none());
1198 }
1199
1200 #[tokio::test]
1201 async fn test_default_client_has_circuit_breaker() {
1202 let client = AqlClient::new();
1204 assert!(client.circuit_breaker.is_some());
1205 }
1206
1207 #[test]
1210 fn test_compression_algorithm_default() {
1211 let algo = CompressionAlgorithm::default();
1212 assert_eq!(algo, CompressionAlgorithm::Identity);
1213 }
1214
1215 #[test]
1216 fn test_compression_algorithm_variants() {
1217 assert_ne!(CompressionAlgorithm::Gzip, CompressionAlgorithm::Identity);
1218 }
1219
1220 #[tokio::test]
1223 async fn test_builder_default() {
1224 let builder = AqlClientBuilder::default();
1225 let client = builder.build().expect("default builder should succeed");
1226 assert_eq!(client.config.connect_timeout, Duration::from_secs(10));
1227 assert_eq!(client.config.request_timeout, Duration::from_secs(30));
1228 }
1229
1230 #[tokio::test]
1231 async fn test_client_default() {
1232 let client = AqlClient::default();
1233 assert_eq!(client.config.connect_timeout, Duration::from_secs(10));
1234 }
1235
1236 #[test]
1239 fn test_tls_config_default() {
1240 let config = TlsClientConfig::default();
1241 assert!(config.ca_cert_path.is_none());
1242 assert!(config.client_cert_path.is_none());
1243 assert!(config.client_key_path.is_none());
1244 assert!(config.domain_name.is_none());
1245 assert!(!config.skip_verification);
1246 }
1247
1248 #[test]
1249 fn test_tls_config_with_ca_cert() {
1250 let config = TlsClientConfig::new().with_ca_cert("/tmp/ca.pem");
1251 assert_eq!(config.ca_cert_path, Some(PathBuf::from("/tmp/ca.pem")));
1252 assert!(config.client_cert_path.is_none());
1253 assert!(config.client_key_path.is_none());
1254 }
1255
1256 #[test]
1257 fn test_tls_config_with_mtls() {
1258 let config =
1259 TlsClientConfig::new().with_client_identity("/tmp/client.pem", "/tmp/client.key");
1260 assert!(config.ca_cert_path.is_none());
1261 assert_eq!(
1262 config.client_cert_path,
1263 Some(PathBuf::from("/tmp/client.pem"))
1264 );
1265 assert_eq!(
1266 config.client_key_path,
1267 Some(PathBuf::from("/tmp/client.key"))
1268 );
1269 }
1270
1271 #[test]
1272 fn test_tls_config_missing_key() {
1273 let config = TlsClientConfig {
1274 client_cert_path: Some(PathBuf::from("/tmp/client.pem")),
1275 client_key_path: None,
1276 ..Default::default()
1277 };
1278 let result = config.validate();
1279 assert!(result.is_err());
1280 let err = result.expect_err("should be TlsError");
1281 assert!(
1282 err.to_string().contains("without client key"),
1283 "Error should mention missing key: {}",
1284 err
1285 );
1286 }
1287
1288 #[test]
1289 fn test_tls_config_missing_cert() {
1290 let config = TlsClientConfig {
1291 client_cert_path: None,
1292 client_key_path: Some(PathBuf::from("/tmp/client.key")),
1293 ..Default::default()
1294 };
1295 let result = config.validate();
1296 assert!(result.is_err());
1297 let err = result.expect_err("should be TlsError");
1298 assert!(
1299 err.to_string().contains("without client certificate"),
1300 "Error should mention missing cert: {}",
1301 err
1302 );
1303 }
1304
1305 #[tokio::test]
1306 async fn test_builder_with_ca_cert() {
1307 let builder = AqlClient::builder()
1309 .with_ca_cert("/tmp/test-ca.pem")
1310 .add_endpoint("ep1".to_string(), "localhost:50051".to_string());
1311
1312 let tls_cfg = builder
1314 .tls_client_config
1315 .as_ref()
1316 .expect("TLS config should be set");
1317 assert_eq!(
1318 tls_cfg.ca_cert_path,
1319 Some(PathBuf::from("/tmp/test-ca.pem"))
1320 );
1321 }
1322
1323 #[tokio::test]
1324 async fn test_builder_with_mtls() {
1325 let builder = AqlClient::builder()
1326 .with_mtls("/tmp/client.pem", "/tmp/client.key")
1327 .add_endpoint("ep1".to_string(), "localhost:50051".to_string());
1328
1329 let tls_cfg = builder
1330 .tls_client_config
1331 .as_ref()
1332 .expect("TLS config should be set");
1333 assert_eq!(
1334 tls_cfg.client_cert_path,
1335 Some(PathBuf::from("/tmp/client.pem"))
1336 );
1337 assert_eq!(
1338 tls_cfg.client_key_path,
1339 Some(PathBuf::from("/tmp/client.key"))
1340 );
1341 }
1342
1343 #[tokio::test]
1344 async fn test_builder_with_full_config() {
1345 let tls_config = TlsClientConfig::new()
1346 .with_ca_cert("/tmp/ca.pem")
1347 .with_client_identity("/tmp/client.pem", "/tmp/client.key")
1348 .with_domain_name("example.com");
1349
1350 let builder = AqlClient::builder()
1351 .with_tls_config(tls_config)
1352 .add_endpoint("ep1".to_string(), "localhost:50051".to_string());
1353
1354 let tls_cfg = builder
1355 .tls_client_config
1356 .as_ref()
1357 .expect("TLS config should be set");
1358 assert_eq!(tls_cfg.ca_cert_path, Some(PathBuf::from("/tmp/ca.pem")));
1359 assert_eq!(
1360 tls_cfg.client_cert_path,
1361 Some(PathBuf::from("/tmp/client.pem"))
1362 );
1363 assert_eq!(
1364 tls_cfg.client_key_path,
1365 Some(PathBuf::from("/tmp/client.key"))
1366 );
1367 assert_eq!(tls_cfg.domain_name, Some("example.com".to_string()));
1368 }
1369
1370 #[test]
1371 fn test_tls_config_domain_name() {
1372 let config = TlsClientConfig::new().with_domain_name("my.server.com");
1373 assert_eq!(config.domain_name, Some("my.server.com".to_string()));
1374 }
1375
1376 #[test]
1377 fn test_tls_config_skip_verification() {
1378 let config = TlsClientConfig::new().with_skip_verification(true);
1379 assert!(config.skip_verification);
1380
1381 let config2 = TlsClientConfig::new().with_skip_verification(false);
1382 assert!(!config2.skip_verification);
1383 }
1384
1385 #[test]
1386 fn test_tls_integration_invalid_cert() {
1387 let tmp_dir = std::env::temp_dir();
1388 let fake_cert_path = tmp_dir.join("nonexistent_test_cert_amaters.pem");
1389
1390 let config = TlsClientConfig::new().with_ca_cert(&fake_cert_path);
1391
1392 assert!(config.validate().is_ok());
1394
1395 let result = config.build_tonic_tls_config();
1397 assert!(result.is_err());
1398 let err = result.expect_err("should fail for missing file");
1399 assert!(
1400 err.to_string().contains("Failed to read CA certificate"),
1401 "Error should mention CA cert read failure: {}",
1402 err
1403 );
1404 }
1405
1406 #[test]
1407 fn test_tls_config_validate_valid_configs() {
1408 assert!(TlsClientConfig::default().validate().is_ok());
1410
1411 assert!(
1413 TlsClientConfig::new()
1414 .with_ca_cert("/tmp/ca.pem")
1415 .validate()
1416 .is_ok()
1417 );
1418
1419 assert!(
1421 TlsClientConfig::new()
1422 .with_ca_cert("/tmp/ca.pem")
1423 .with_client_identity("/tmp/client.pem", "/tmp/client.key")
1424 .validate()
1425 .is_ok()
1426 );
1427 }
1428
1429 #[tokio::test]
1430 async fn test_builder_with_tls_domain() {
1431 let builder = AqlClient::builder()
1432 .with_tls_domain("custom.domain.io")
1433 .add_endpoint("ep1".to_string(), "localhost:50051".to_string());
1434
1435 let tls_cfg = builder
1436 .tls_client_config
1437 .as_ref()
1438 .expect("TLS config should be set");
1439 assert_eq!(tls_cfg.domain_name, Some("custom.domain.io".to_string()));
1440 }
1441
1442 #[tokio::test]
1443 async fn test_builder_with_tls_skip_verification() {
1444 let builder = AqlClient::builder()
1445 .with_tls_skip_verification()
1446 .add_endpoint("ep1".to_string(), "localhost:50051".to_string());
1447
1448 let tls_cfg = builder
1449 .tls_client_config
1450 .as_ref()
1451 .expect("TLS config should be set");
1452 assert!(tls_cfg.skip_verification);
1453 }
1454
1455 #[test]
1456 fn test_tls_config_chaining() {
1457 let config = TlsClientConfig::new()
1459 .with_ca_cert("/tmp/ca.pem")
1460 .with_client_identity("/tmp/cert.pem", "/tmp/key.pem")
1461 .with_domain_name("example.com")
1462 .with_skip_verification(false);
1463
1464 assert_eq!(config.ca_cert_path, Some(PathBuf::from("/tmp/ca.pem")));
1465 assert_eq!(
1466 config.client_cert_path,
1467 Some(PathBuf::from("/tmp/cert.pem"))
1468 );
1469 assert_eq!(config.client_key_path, Some(PathBuf::from("/tmp/key.pem")));
1470 assert_eq!(config.domain_name, Some("example.com".to_string()));
1471 assert!(!config.skip_verification);
1472 assert!(config.validate().is_ok());
1473 }
1474}