1use crate::balancer::{BalancingStrategy, EndpointId, LoadBalancer};
16use crate::circuit_breaker::CircuitBreaker;
17use crate::error::{NetError, NetResult};
18use parking_lot::RwLock;
19use std::collections::VecDeque;
20use std::sync::Arc;
21use std::sync::atomic::{AtomicU64, Ordering};
22use std::time::{Duration, Instant};
23use tokio::time;
24use tonic::transport::{Channel, ClientTlsConfig, Endpoint};
25
26#[derive(Debug, Clone)]
28pub struct PoolConfig {
29 pub min_size: usize,
31 pub max_size: usize,
33 pub idle_timeout: Duration,
35 pub max_lifetime: Duration,
37 pub connect_timeout: Duration,
39 pub health_check_interval: Duration,
41 pub balancing_strategy: BalancingStrategy,
43 pub enable_circuit_breaker: bool,
45}
46
47impl Default for PoolConfig {
48 fn default() -> Self {
49 Self {
50 min_size: 2,
51 max_size: 10,
52 idle_timeout: Duration::from_secs(300), max_lifetime: Duration::from_secs(1800), connect_timeout: Duration::from_secs(10),
55 health_check_interval: Duration::from_secs(30),
56 balancing_strategy: BalancingStrategy::LeastConnections,
57 enable_circuit_breaker: true,
58 }
59 }
60}
61
62#[derive(Debug, Clone)]
64pub struct HealthCheckConfig {
65 pub interval: Duration,
67 pub timeout: Duration,
69 pub unhealthy_threshold: u32,
71}
72
73impl Default for HealthCheckConfig {
74 fn default() -> Self {
75 Self {
76 interval: Duration::from_secs(30),
77 timeout: Duration::from_secs(5),
78 unhealthy_threshold: 3,
79 }
80 }
81}
82
83#[derive(Debug, Clone, Copy, PartialEq, Eq)]
85pub enum ConnectionHealth {
86 Healthy,
88 Degraded,
90 Unhealthy,
92}
93
94#[derive(Debug, Clone)]
96pub struct AdaptiveConfig {
97 pub min_size: usize,
99 pub max_size: usize,
101 pub scale_up_threshold: f64,
103 pub scale_down_threshold: f64,
105 pub scale_step: usize,
107 pub cooldown: Duration,
109}
110
111impl Default for AdaptiveConfig {
112 fn default() -> Self {
113 Self {
114 min_size: 2,
115 max_size: 20,
116 scale_up_threshold: 0.8,
117 scale_down_threshold: 0.2,
118 scale_step: 2,
119 cooldown: Duration::from_secs(60),
120 }
121 }
122}
123
124#[derive(Debug, Clone)]
126pub struct PoolMetrics {
127 pub total_connections: usize,
129 pub active_connections: usize,
131 pub idle_connections: usize,
133 pub total_checkouts: u64,
135 pub total_checkins: u64,
137 pub total_timeouts: u64,
139 pub total_health_check_failures: u64,
141 pub avg_checkout_duration_us: u64,
143 pub utilization: f64,
145}
146
147impl Default for PoolMetrics {
148 fn default() -> Self {
149 Self {
150 total_connections: 0,
151 active_connections: 0,
152 idle_connections: 0,
153 total_checkouts: 0,
154 total_checkins: 0,
155 total_timeouts: 0,
156 total_health_check_failures: 0,
157 avg_checkout_duration_us: 0,
158 utilization: 0.0,
159 }
160 }
161}
162
163#[derive(Debug, Clone, Default)]
165pub struct PoolStats {
166 pub total_connections: usize,
168 pub active_connections: usize,
170 pub idle_connections: usize,
172 pub failed_connections: u64,
174 pub total_created: u64,
176 pub total_closed: u64,
178 pub pool_exhausted_count: u64,
180 pub avg_wait_time_ms: u64,
182}
183
184#[derive(Debug)]
186struct ConnectionMeta {
187 channel: Channel,
189 endpoint_id: EndpointId,
191 created_at: Instant,
193 last_used: Instant,
195 health: ConnectionHealth,
197 health_check_failures: u32,
199 last_health_check: Option<Instant>,
201}
202
203impl ConnectionMeta {
204 fn new(channel: Channel, endpoint_id: EndpointId) -> Self {
206 let now = Instant::now();
207 Self {
208 channel,
209 endpoint_id,
210 created_at: now,
211 last_used: now,
212 health: ConnectionHealth::Healthy,
213 health_check_failures: 0,
214 last_health_check: None,
215 }
216 }
217
218 fn is_idle_expired(&self, idle_timeout: Duration) -> bool {
220 self.last_used.elapsed() > idle_timeout
221 }
222
223 fn is_lifetime_expired(&self, max_lifetime: Duration) -> bool {
225 self.created_at.elapsed() > max_lifetime
226 }
227
228 fn touch(&mut self) {
230 self.last_used = Instant::now();
231 }
232
233 fn record_health_success(&mut self) {
235 self.health_check_failures = 0;
236 self.health = ConnectionHealth::Healthy;
237 self.last_health_check = Some(Instant::now());
238 }
239
240 fn record_health_failure(&mut self, unhealthy_threshold: u32) -> ConnectionHealth {
242 self.health_check_failures += 1;
243 self.last_health_check = Some(Instant::now());
244
245 if self.health_check_failures >= unhealthy_threshold {
246 self.health = ConnectionHealth::Unhealthy;
247 } else if self.health_check_failures >= unhealthy_threshold.saturating_sub(1).max(1) {
248 self.health = ConnectionHealth::Degraded;
249 }
250 self.health
251 }
252}
253
254pub struct PooledConnection {
256 meta: Option<ConnectionMeta>,
257 pool: Arc<ConnectionPoolInner>,
258}
259
260impl PooledConnection {
261 pub fn channel(&self) -> &Channel {
263 &self.meta.as_ref().expect("connection should exist").channel
264 }
265
266 pub fn endpoint_id(&self) -> &str {
268 &self
269 .meta
270 .as_ref()
271 .expect("connection should exist")
272 .endpoint_id
273 }
274}
275
276impl Drop for PooledConnection {
277 fn drop(&mut self) {
278 if let Some(mut meta) = self.meta.take() {
279 meta.touch();
280 self.pool.return_connection(meta);
281 }
282 }
283}
284
285struct ConnectionPoolInner {
287 config: PoolConfig,
288 health_check_config: HealthCheckConfig,
289 adaptive_config: Option<AdaptiveConfig>,
290 tls_config: Option<ClientTlsConfig>,
292 idle_connections: RwLock<VecDeque<ConnectionMeta>>,
293 active_count: std::sync::Mutex<usize>,
294 stats: RwLock<PoolStats>,
295 load_balancer: LoadBalancer,
296 circuit_breaker: Option<CircuitBreaker>,
297 total_checkouts: AtomicU64,
299 total_checkins: AtomicU64,
300 total_timeouts: AtomicU64,
301 total_health_check_failures: AtomicU64,
302 checkout_duration_sum_us: AtomicU64,
303 checkout_count_for_avg: AtomicU64,
304 last_scale_time: RwLock<Option<Instant>>,
306 effective_max_size: std::sync::Mutex<usize>,
308}
309
310impl ConnectionPoolInner {
311 fn return_connection(&self, meta: ConnectionMeta) {
313 self.total_checkins.fetch_add(1, Ordering::Relaxed);
314
315 if meta.health == ConnectionHealth::Unhealthy {
317 self.stats.write().total_closed += 1;
318 let mut active = self
319 .active_count
320 .lock()
321 .expect("active count lock poisoned");
322 *active = active.saturating_sub(1);
323 return;
324 }
325
326 if meta.is_idle_expired(self.config.idle_timeout)
328 || meta.is_lifetime_expired(self.config.max_lifetime)
329 {
330 self.stats.write().total_closed += 1;
332 let mut active = self
333 .active_count
334 .lock()
335 .expect("active count lock poisoned");
336 *active = active.saturating_sub(1);
337 return;
338 }
339
340 self.idle_connections.write().push_back(meta);
342 let mut active = self
343 .active_count
344 .lock()
345 .expect("active count lock poisoned");
346 *active = active.saturating_sub(1);
347 }
348
349 fn get_stats(&self) -> PoolStats {
351 let mut stats = self.stats.read().clone();
352 let idle = self.idle_connections.read().len();
353 let active = *self
354 .active_count
355 .lock()
356 .expect("active count lock poisoned");
357 stats.total_connections = idle + active;
358 stats.active_connections = active;
359 stats.idle_connections = idle;
360 stats
361 }
362
363 fn utilization(&self) -> f64 {
365 let active = *self
366 .active_count
367 .lock()
368 .expect("active count lock poisoned");
369 let max_size = *self
370 .effective_max_size
371 .lock()
372 .expect("effective max size lock poisoned");
373 if max_size == 0 {
374 return 0.0;
375 }
376 active as f64 / max_size as f64
377 }
378
379 fn get_metrics(&self) -> PoolMetrics {
381 let idle = self.idle_connections.read().len();
382 let active = *self
383 .active_count
384 .lock()
385 .expect("active count lock poisoned");
386 let max_size = *self
387 .effective_max_size
388 .lock()
389 .expect("effective max size lock poisoned");
390
391 let total_checkouts = self.total_checkouts.load(Ordering::Relaxed);
392 let total_checkins = self.total_checkins.load(Ordering::Relaxed);
393 let total_timeouts = self.total_timeouts.load(Ordering::Relaxed);
394 let total_health_check_failures = self.total_health_check_failures.load(Ordering::Relaxed);
395
396 let checkout_count = self.checkout_count_for_avg.load(Ordering::Relaxed);
397 let checkout_sum = self.checkout_duration_sum_us.load(Ordering::Relaxed);
398 let avg_checkout_duration_us = checkout_sum.checked_div(checkout_count).unwrap_or(0);
399
400 let utilization = if max_size > 0 {
401 active as f64 / max_size as f64
402 } else {
403 0.0
404 };
405
406 PoolMetrics {
407 total_connections: idle + active,
408 active_connections: active,
409 idle_connections: idle,
410 total_checkouts,
411 total_checkins,
412 total_timeouts,
413 total_health_check_failures,
414 avg_checkout_duration_us,
415 utilization,
416 }
417 }
418}
419
420pub struct ConnectionPool {
422 inner: Arc<ConnectionPoolInner>,
423 shutdown_tx: tokio::sync::watch::Sender<bool>,
424}
425
426impl ConnectionPool {
427 pub fn new(config: PoolConfig) -> Self {
429 Self::with_health_and_adaptive(config, HealthCheckConfig::default(), None)
430 }
431
432 pub fn with_tls(config: PoolConfig, tls_config: ClientTlsConfig) -> Self {
434 Self::with_full_config(config, HealthCheckConfig::default(), None, Some(tls_config))
435 }
436
437 pub fn with_health_and_adaptive(
439 config: PoolConfig,
440 health_check_config: HealthCheckConfig,
441 adaptive_config: Option<AdaptiveConfig>,
442 ) -> Self {
443 Self::with_full_config(config, health_check_config, adaptive_config, None)
444 }
445
446 pub fn with_full_config(
448 config: PoolConfig,
449 health_check_config: HealthCheckConfig,
450 adaptive_config: Option<AdaptiveConfig>,
451 tls_config: Option<ClientTlsConfig>,
452 ) -> Self {
453 let load_balancer = LoadBalancer::new(config.balancing_strategy);
454 let circuit_breaker = if config.enable_circuit_breaker {
455 Some(CircuitBreaker::new())
456 } else {
457 None
458 };
459
460 let effective_max = config.max_size;
461
462 let inner = Arc::new(ConnectionPoolInner {
463 config: config.clone(),
464 health_check_config,
465 adaptive_config,
466 tls_config,
467 idle_connections: RwLock::new(VecDeque::new()),
468 active_count: std::sync::Mutex::new(0),
469 stats: RwLock::new(PoolStats::default()),
470 load_balancer,
471 circuit_breaker,
472 total_checkouts: AtomicU64::new(0),
473 total_checkins: AtomicU64::new(0),
474 total_timeouts: AtomicU64::new(0),
475 total_health_check_failures: AtomicU64::new(0),
476 checkout_duration_sum_us: AtomicU64::new(0),
477 checkout_count_for_avg: AtomicU64::new(0),
478 last_scale_time: RwLock::new(None),
479 effective_max_size: std::sync::Mutex::new(effective_max),
480 });
481
482 let (shutdown_tx, shutdown_rx) = tokio::sync::watch::channel(false);
483
484 let health_check_inner = Arc::clone(&inner);
486 tokio::spawn(async move {
487 Self::health_check_loop(health_check_inner, shutdown_rx).await;
488 });
489
490 Self { inner, shutdown_tx }
491 }
492
493 pub fn add_endpoint(&self, id: EndpointId, address: String) {
495 self.add_endpoint_with_weight(id, address, 1);
496 }
497
498 pub fn add_endpoint_with_weight(&self, id: EndpointId, address: String, weight: u32) {
500 let endpoint = crate::balancer::Endpoint::with_weight(id, address, weight);
501 self.inner.load_balancer.add_endpoint(endpoint);
502 }
503
504 pub fn remove_endpoint(&self, endpoint_id: &str) -> bool {
506 let removed = self.inner.load_balancer.remove_endpoint(endpoint_id);
508
509 if removed {
511 let mut idle = self.inner.idle_connections.write();
512 idle.retain(|conn| conn.endpoint_id != endpoint_id);
513 }
514
515 removed
516 }
517
518 pub async fn get_connection(&self) -> NetResult<PooledConnection> {
520 let start = Instant::now();
521
522 if let Some(ref cb) = self.inner.circuit_breaker {
524 cb.is_request_allowed()?;
525 }
526
527 let conn = self.try_get_healthy_idle_connection();
529 if let Some(meta) = conn {
530 self.record_checkout_duration(start);
531 return Ok(PooledConnection {
532 meta: Some(meta),
533 pool: Arc::clone(&self.inner),
534 });
535 }
536
537 let active = *self
539 .inner
540 .active_count
541 .lock()
542 .expect("active count lock poisoned");
543 let idle = self.inner.idle_connections.read().len();
544 let effective_max = *self
545 .inner
546 .effective_max_size
547 .lock()
548 .expect("effective max size lock poisoned");
549
550 if active + idle >= effective_max {
551 self.inner.stats.write().pool_exhausted_count += 1;
553
554 let timeout = Duration::from_secs(30);
556 let deadline = Instant::now() + timeout;
557
558 while Instant::now() < deadline {
559 let conn = self.try_get_healthy_idle_connection();
560 if let Some(meta) = conn {
561 let wait_time = start.elapsed().as_millis() as u64;
563 let mut stats = self.inner.stats.write();
564 stats.avg_wait_time_ms = (stats.avg_wait_time_ms + wait_time) / 2;
565
566 self.record_checkout_duration(start);
567 return Ok(PooledConnection {
568 meta: Some(meta),
569 pool: Arc::clone(&self.inner),
570 });
571 }
572
573 time::sleep(Duration::from_millis(100)).await;
575 }
576
577 self.inner.total_timeouts.fetch_add(1, Ordering::Relaxed);
578 return Err(NetError::ServerOverloaded(
579 "Connection pool exhausted".to_string(),
580 ));
581 }
582
583 let meta = self.create_connection().await?;
585 *self
586 .inner
587 .active_count
588 .lock()
589 .expect("active count lock poisoned") += 1;
590 self.inner.total_checkouts.fetch_add(1, Ordering::Relaxed);
591 self.record_checkout_duration(start);
592
593 Ok(PooledConnection {
594 meta: Some(meta),
595 pool: Arc::clone(&self.inner),
596 })
597 }
598
599 fn try_get_healthy_idle_connection(&self) -> Option<ConnectionMeta> {
601 let mut idle = self.inner.idle_connections.write();
602 let mut attempts = idle.len();
603
604 while attempts > 0 {
605 if let Some(mut meta) = idle.pop_front() {
606 if meta.health == ConnectionHealth::Unhealthy {
607 self.inner.stats.write().total_closed += 1;
609 attempts -= 1;
610 continue;
611 }
612 meta.touch();
613 *self
614 .inner
615 .active_count
616 .lock()
617 .expect("active count lock poisoned") += 1;
618 self.inner.total_checkouts.fetch_add(1, Ordering::Relaxed);
619 return Some(meta);
620 }
621 break;
622 }
623 None
624 }
625
626 fn record_checkout_duration(&self, start: Instant) {
628 let duration_us = start.elapsed().as_micros() as u64;
629 self.inner
630 .checkout_duration_sum_us
631 .fetch_add(duration_us, Ordering::Relaxed);
632 self.inner
633 .checkout_count_for_avg
634 .fetch_add(1, Ordering::Relaxed);
635 }
636
637 async fn create_connection(&self) -> NetResult<ConnectionMeta> {
639 let endpoint = self.inner.load_balancer.select_endpoint()?;
641
642 let scheme = if self.inner.tls_config.is_some() {
644 "https"
645 } else {
646 "http"
647 };
648 let mut ep = Endpoint::from_shared(format!("{}://{}", scheme, endpoint.address))
649 .map_err(|e| NetError::InvalidRequest(format!("Invalid endpoint: {}", e)))?
650 .connect_timeout(self.inner.config.connect_timeout)
651 .timeout(Duration::from_secs(30));
652
653 if let Some(ref tls_cfg) = self.inner.tls_config {
655 ep = ep
656 .tls_config(tls_cfg.clone())
657 .map_err(|e| NetError::TlsError(format!("Failed to apply TLS config: {}", e)))?;
658 }
659
660 let channel = ep.connect().await.map_err(|e| {
661 self.inner.stats.write().failed_connections += 1;
662 if let Some(ref cb) = self.inner.circuit_breaker {
663 cb.record_failure();
664 }
665 NetError::ConnectionRefused(format!("Failed to connect: {}", e))
666 })?;
667
668 if let Some(ref cb) = self.inner.circuit_breaker {
670 cb.record_success();
671 }
672
673 self.inner.stats.write().total_created += 1;
674
675 Ok(ConnectionMeta::new(channel, endpoint.id.clone()))
676 }
677
678 async fn health_check_loop(
680 inner: Arc<ConnectionPoolInner>,
681 mut shutdown_rx: tokio::sync::watch::Receiver<bool>,
682 ) {
683 let interval_duration = inner.health_check_config.interval;
684 let mut interval = time::interval(interval_duration);
685
686 loop {
687 tokio::select! {
688 _ = interval.tick() => {
689 Self::run_health_checks(&inner).await;
690 Self::evaluate_scaling(&inner);
691 }
692 _ = shutdown_rx.changed() => {
693 if *shutdown_rx.borrow() {
694 break;
695 }
696 }
697 }
698 }
699 }
700
701 fn check_connection_health(
704 meta: &mut ConnectionMeta,
705 config: &PoolConfig,
706 health_config: &HealthCheckConfig,
707 ) -> ConnectionHealth {
708 if meta.is_idle_expired(config.idle_timeout)
710 || meta.is_lifetime_expired(config.max_lifetime)
711 {
712 meta.health = ConnectionHealth::Unhealthy;
713 return ConnectionHealth::Unhealthy;
714 }
715
716 if let Some(last_check) = meta.last_health_check {
721 if last_check.elapsed() < health_config.interval {
722 return meta.health;
724 }
725 }
726
727 if meta.last_used.elapsed() < health_config.timeout {
729 meta.record_health_success();
730 return ConnectionHealth::Healthy;
731 }
732
733 meta.record_health_success();
739 ConnectionHealth::Healthy
740 }
741
742 async fn run_health_checks(inner: &Arc<ConnectionPoolInner>) {
744 let mut removed_count: u64 = 0;
745
746 {
747 let mut idle = inner.idle_connections.write();
748 let config = &inner.config;
749 let health_config = &inner.health_check_config;
750
751 idle.retain_mut(|conn| {
753 let health = Self::check_connection_health(conn, config, health_config);
754 match health {
755 ConnectionHealth::Unhealthy => {
756 removed_count += 1;
757 false
758 }
759 _ => {
760 if conn.is_idle_expired(config.idle_timeout)
762 || conn.is_lifetime_expired(config.max_lifetime)
763 {
764 removed_count += 1;
765 false
766 } else {
767 true
768 }
769 }
770 }
771 });
772 } if removed_count > 0 {
775 inner.stats.write().total_closed += removed_count;
776 inner
777 .total_health_check_failures
778 .fetch_add(removed_count, Ordering::Relaxed);
779 }
780 }
781
782 fn mark_connection_unhealthy(meta: &mut ConnectionMeta, threshold: u32) {
784 meta.record_health_failure(threshold);
785 }
786
787 fn evaluate_scaling(inner: &Arc<ConnectionPoolInner>) {
789 let adaptive = match &inner.adaptive_config {
790 Some(cfg) => cfg.clone(),
791 None => return,
792 };
793
794 {
796 let last_scale = inner.last_scale_time.read();
797 if let Some(t) = *last_scale {
798 if t.elapsed() < adaptive.cooldown {
799 return;
800 }
801 }
802 }
803
804 let utilization = inner.utilization();
805 let current_max = *inner
806 .effective_max_size
807 .lock()
808 .expect("effective max size lock poisoned");
809
810 if utilization >= adaptive.scale_up_threshold {
811 let new_max = (current_max + adaptive.scale_step).min(adaptive.max_size);
813 if new_max != current_max {
814 *inner
815 .effective_max_size
816 .lock()
817 .expect("effective max size lock poisoned") = new_max;
818 *inner.last_scale_time.write() = Some(Instant::now());
819 tracing::info!(
820 old_max = current_max,
821 new_max = new_max,
822 utilization = utilization,
823 "Pool scaled up"
824 );
825 }
826 } else if utilization <= adaptive.scale_down_threshold {
827 let new_max = current_max
829 .saturating_sub(adaptive.scale_step)
830 .max(adaptive.min_size);
831 if new_max != current_max {
832 *inner
833 .effective_max_size
834 .lock()
835 .expect("effective max size lock poisoned") = new_max;
836 *inner.last_scale_time.write() = Some(Instant::now());
837 tracing::info!(
838 old_max = current_max,
839 new_max = new_max,
840 utilization = utilization,
841 "Pool scaled down"
842 );
843
844 let mut idle = inner.idle_connections.write();
846 let active = *inner
847 .active_count
848 .lock()
849 .expect("active count lock poisoned");
850 while idle.len() + active > new_max {
851 if idle.pop_back().is_some() {
852 } else {
854 break;
855 }
856 }
857 }
858 }
859 }
860
861 pub fn utilization(&self) -> f64 {
863 self.inner.utilization()
864 }
865
866 pub fn scale_up(&self, count: usize) {
868 let adaptive = self.inner.adaptive_config.as_ref();
869 let ceiling = adaptive.map_or(self.inner.config.max_size, |a| a.max_size);
870
871 let mut max_size = self
872 .inner
873 .effective_max_size
874 .lock()
875 .expect("effective max size lock poisoned");
876 let new_max = (*max_size + count).min(ceiling);
877 *max_size = new_max;
878 *self.inner.last_scale_time.write() = Some(Instant::now());
879 }
880
881 pub fn scale_down(&self, count: usize) {
883 let adaptive = self.inner.adaptive_config.as_ref();
884 let floor = adaptive.map_or(self.inner.config.min_size, |a| a.min_size);
885
886 let mut max_size = self
887 .inner
888 .effective_max_size
889 .lock()
890 .expect("effective max size lock poisoned");
891 let new_max = max_size.saturating_sub(count).max(floor);
892 *max_size = new_max;
893 *self.inner.last_scale_time.write() = Some(Instant::now());
894
895 let mut idle = self.inner.idle_connections.write();
897 let active = *self
898 .inner
899 .active_count
900 .lock()
901 .expect("active count lock poisoned");
902 while idle.len() + active > new_max {
903 if idle.pop_back().is_none() {
904 break;
905 }
906 }
907 }
908
909 pub fn stats(&self) -> PoolStats {
911 self.inner.get_stats()
912 }
913
914 pub fn metrics(&self) -> PoolMetrics {
916 self.inner.get_metrics()
917 }
918
919 pub fn effective_max_size(&self) -> usize {
921 *self
922 .inner
923 .effective_max_size
924 .lock()
925 .expect("effective max size lock poisoned")
926 }
927
928 pub fn circuit_breaker_stats(&self) -> Option<crate::circuit_breaker::CircuitBreakerStats> {
930 self.inner.circuit_breaker.as_ref().map(|cb| cb.stats())
931 }
932
933 pub async fn shutdown(self) -> NetResult<()> {
935 self.shutdown_tx
937 .send(true)
938 .map_err(|_| NetError::ServerInternal("Failed to signal shutdown".to_string()))?;
939
940 time::sleep(Duration::from_millis(500)).await;
942
943 let mut idle = self.inner.idle_connections.write();
945 let count = idle.len();
946 idle.clear();
947
948 self.inner.stats.write().total_closed += count as u64;
949
950 Ok(())
951 }
952
953 pub async fn drain(&self) -> NetResult<()> {
955 let timeout = Duration::from_secs(30);
957 let deadline = Instant::now() + timeout;
958
959 while Instant::now() < deadline {
960 let active = *self
961 .inner
962 .active_count
963 .lock()
964 .expect("active count lock poisoned");
965 if active == 0 {
966 break;
967 }
968 time::sleep(Duration::from_millis(100)).await;
969 }
970
971 let active = *self
972 .inner
973 .active_count
974 .lock()
975 .expect("active count lock poisoned");
976 if active > 0 {
977 return Err(NetError::Timeout(format!(
978 "Drain timeout: {} active connections remaining",
979 active
980 )));
981 }
982
983 Ok(())
984 }
985}
986
987pub struct ConnectionPoolBuilder {
989 config: PoolConfig,
990 health_check_config: HealthCheckConfig,
991 adaptive_config: Option<AdaptiveConfig>,
992 tls_config: Option<ClientTlsConfig>,
993 endpoints: Vec<(EndpointId, String, u32)>,
994}
995
996impl ConnectionPoolBuilder {
997 pub fn new() -> Self {
999 Self {
1000 config: PoolConfig::default(),
1001 health_check_config: HealthCheckConfig::default(),
1002 adaptive_config: None,
1003 tls_config: None,
1004 endpoints: Vec::new(),
1005 }
1006 }
1007
1008 pub fn tls_config(mut self, tls_config: ClientTlsConfig) -> Self {
1010 self.tls_config = Some(tls_config);
1011 self
1012 }
1013
1014 pub fn min_size(mut self, size: usize) -> Self {
1016 self.config.min_size = size;
1017 self
1018 }
1019
1020 pub fn max_size(mut self, size: usize) -> Self {
1022 self.config.max_size = size;
1023 self
1024 }
1025
1026 pub fn idle_timeout(mut self, timeout: Duration) -> Self {
1028 self.config.idle_timeout = timeout;
1029 self
1030 }
1031
1032 pub fn max_lifetime(mut self, lifetime: Duration) -> Self {
1034 self.config.max_lifetime = lifetime;
1035 self
1036 }
1037
1038 pub fn connect_timeout(mut self, timeout: Duration) -> Self {
1040 self.config.connect_timeout = timeout;
1041 self
1042 }
1043
1044 pub fn health_check_interval(mut self, interval: Duration) -> Self {
1046 self.config.health_check_interval = interval;
1047 self
1048 }
1049
1050 pub fn balancing_strategy(mut self, strategy: BalancingStrategy) -> Self {
1052 self.config.balancing_strategy = strategy;
1053 self
1054 }
1055
1056 pub fn circuit_breaker(mut self, enabled: bool) -> Self {
1058 self.config.enable_circuit_breaker = enabled;
1059 self
1060 }
1061
1062 pub fn add_endpoint(mut self, id: EndpointId, address: String) -> Self {
1064 self.endpoints.push((id, address, 1));
1065 self
1066 }
1067
1068 pub fn add_endpoint_with_weight(
1070 mut self,
1071 id: EndpointId,
1072 address: String,
1073 weight: u32,
1074 ) -> Self {
1075 self.endpoints.push((id, address, weight));
1076 self
1077 }
1078
1079 pub fn health_check_config(mut self, config: HealthCheckConfig) -> Self {
1081 self.health_check_config = config;
1082 self
1083 }
1084
1085 pub fn health_check_timeout(mut self, timeout: Duration) -> Self {
1087 self.health_check_config.timeout = timeout;
1088 self
1089 }
1090
1091 pub fn unhealthy_threshold(mut self, threshold: u32) -> Self {
1093 self.health_check_config.unhealthy_threshold = threshold;
1094 self
1095 }
1096
1097 pub fn adaptive(mut self, config: AdaptiveConfig) -> Self {
1099 self.adaptive_config = Some(config);
1100 self
1101 }
1102
1103 pub fn adaptive_default(mut self) -> Self {
1105 self.adaptive_config = Some(AdaptiveConfig::default());
1106 self
1107 }
1108
1109 pub fn build(self) -> ConnectionPool {
1111 let pool = ConnectionPool::with_full_config(
1112 self.config,
1113 self.health_check_config,
1114 self.adaptive_config,
1115 self.tls_config,
1116 );
1117
1118 for (id, address, weight) in self.endpoints {
1119 pool.add_endpoint_with_weight(id, address, weight);
1120 }
1121
1122 pool
1123 }
1124}
1125
1126impl Default for ConnectionPoolBuilder {
1127 fn default() -> Self {
1128 Self::new()
1129 }
1130}
1131
1132#[cfg(test)]
1133mod tests {
1134 use super::*;
1135
1136 #[test]
1137 fn test_pool_config_default() {
1138 let config = PoolConfig::default();
1139 assert_eq!(config.min_size, 2);
1140 assert_eq!(config.max_size, 10);
1141 assert!(config.enable_circuit_breaker);
1142 }
1143
1144 #[test]
1145 fn test_health_check_config_defaults() {
1146 let config = HealthCheckConfig::default();
1147 assert_eq!(config.interval, Duration::from_secs(30));
1148 assert_eq!(config.timeout, Duration::from_secs(5));
1149 assert_eq!(config.unhealthy_threshold, 3);
1150 }
1151
1152 #[test]
1153 fn test_adaptive_config_defaults() {
1154 let config = AdaptiveConfig::default();
1155 assert_eq!(config.min_size, 2);
1156 assert_eq!(config.max_size, 20);
1157 assert!((config.scale_up_threshold - 0.8).abs() < f64::EPSILON);
1158 assert!((config.scale_down_threshold - 0.2).abs() < f64::EPSILON);
1159 assert_eq!(config.scale_step, 2);
1160 assert_eq!(config.cooldown, Duration::from_secs(60));
1161 }
1162
1163 #[test]
1164 fn test_connection_health_status() {
1165 assert_eq!(ConnectionHealth::Healthy, ConnectionHealth::Healthy);
1166 assert_ne!(ConnectionHealth::Healthy, ConnectionHealth::Degraded);
1167 assert_ne!(ConnectionHealth::Degraded, ConnectionHealth::Unhealthy);
1168 }
1169
1170 #[tokio::test]
1171 async fn test_connection_meta_expiry() {
1172 let endpoint = Endpoint::from_static("http://localhost:50051");
1174 if let Ok(channel) = endpoint.connect().await {
1175 let meta = ConnectionMeta::new(channel, "ep1".to_string());
1176
1177 assert!(!meta.is_idle_expired(Duration::from_secs(10)));
1178 assert!(!meta.is_lifetime_expired(Duration::from_secs(10)));
1179 assert_eq!(meta.health, ConnectionHealth::Healthy);
1180 assert_eq!(meta.health_check_failures, 0);
1181 }
1182 }
1184
1185 #[test]
1186 fn test_health_check_healthy_connection() {
1187 let endpoint = Endpoint::from_static("http://localhost:50051");
1190 let config = PoolConfig::default();
1193 let health_config = HealthCheckConfig::default();
1194
1195 assert_eq!(health_config.unhealthy_threshold, 3);
1197 assert_eq!(health_config.timeout, Duration::from_secs(5));
1198
1199 assert_eq!(ConnectionHealth::Healthy, ConnectionHealth::Healthy);
1201 }
1202
1203 #[tokio::test]
1204 async fn test_health_check_removes_unhealthy() {
1205 let pool = ConnectionPoolBuilder::new()
1208 .min_size(0)
1209 .max_size(10)
1210 .circuit_breaker(false)
1211 .add_endpoint("ep1".to_string(), "localhost:50051".to_string())
1212 .build();
1213
1214 let idle_count = pool.inner.idle_connections.read().len();
1217 assert_eq!(idle_count, 0);
1218
1219 let metrics = pool.metrics();
1221 assert_eq!(metrics.total_health_check_failures, 0);
1222 }
1223
1224 #[tokio::test]
1225 async fn test_adaptive_scale_up() {
1226 let adaptive = AdaptiveConfig {
1227 min_size: 2,
1228 max_size: 20,
1229 scale_up_threshold: 0.8,
1230 scale_down_threshold: 0.2,
1231 scale_step: 2,
1232 cooldown: Duration::from_millis(10), };
1234
1235 let pool = ConnectionPoolBuilder::new()
1236 .min_size(2)
1237 .max_size(5)
1238 .circuit_breaker(false)
1239 .adaptive(adaptive)
1240 .add_endpoint("ep1".to_string(), "localhost:50051".to_string())
1241 .build();
1242
1243 assert_eq!(pool.effective_max_size(), 5);
1245
1246 pool.scale_up(3);
1248 assert_eq!(pool.effective_max_size(), 8);
1249
1250 pool.scale_up(100);
1252 assert_eq!(pool.effective_max_size(), 20);
1253 }
1254
1255 #[tokio::test]
1256 async fn test_adaptive_scale_down() {
1257 let adaptive = AdaptiveConfig {
1258 min_size: 2,
1259 max_size: 20,
1260 scale_up_threshold: 0.8,
1261 scale_down_threshold: 0.2,
1262 scale_step: 2,
1263 cooldown: Duration::from_millis(10),
1264 };
1265
1266 let pool = ConnectionPoolBuilder::new()
1267 .min_size(2)
1268 .max_size(10)
1269 .circuit_breaker(false)
1270 .adaptive(adaptive)
1271 .add_endpoint("ep1".to_string(), "localhost:50051".to_string())
1272 .build();
1273
1274 assert_eq!(pool.effective_max_size(), 10);
1275
1276 pool.scale_down(3);
1278 assert_eq!(pool.effective_max_size(), 7);
1279
1280 pool.scale_down(100);
1282 assert_eq!(pool.effective_max_size(), 2);
1283 }
1284
1285 #[tokio::test]
1286 async fn test_pool_metrics_tracking() {
1287 let pool = ConnectionPoolBuilder::new()
1288 .min_size(0)
1289 .max_size(10)
1290 .circuit_breaker(false)
1291 .add_endpoint("ep1".to_string(), "localhost:50051".to_string())
1292 .build();
1293
1294 let metrics = pool.metrics();
1295 assert_eq!(metrics.total_connections, 0);
1296 assert_eq!(metrics.active_connections, 0);
1297 assert_eq!(metrics.idle_connections, 0);
1298 assert_eq!(metrics.total_checkouts, 0);
1299 assert_eq!(metrics.total_checkins, 0);
1300 assert_eq!(metrics.total_timeouts, 0);
1301 assert_eq!(metrics.total_health_check_failures, 0);
1302 assert_eq!(metrics.avg_checkout_duration_us, 0);
1303 assert!((metrics.utilization - 0.0).abs() < f64::EPSILON);
1304 }
1305
1306 #[tokio::test]
1307 async fn test_pool_utilization_calculation() {
1308 let pool = ConnectionPoolBuilder::new()
1309 .min_size(0)
1310 .max_size(10)
1311 .circuit_breaker(false)
1312 .add_endpoint("ep1".to_string(), "localhost:50051".to_string())
1313 .build();
1314
1315 let util = pool.utilization();
1317 assert!((util - 0.0).abs() < f64::EPSILON);
1318
1319 {
1321 let mut active = pool
1322 .inner
1323 .active_count
1324 .lock()
1325 .expect("active count lock poisoned");
1326 *active = 5;
1327 }
1328
1329 let util = pool.utilization();
1330 assert!((util - 0.5).abs() < f64::EPSILON);
1331
1332 {
1334 let mut active = pool
1335 .inner
1336 .active_count
1337 .lock()
1338 .expect("active count lock poisoned");
1339 *active = 0;
1340 }
1341 }
1342
1343 #[tokio::test]
1344 async fn test_adaptive_cooldown() {
1345 let adaptive = AdaptiveConfig {
1346 min_size: 2,
1347 max_size: 20,
1348 scale_up_threshold: 0.8,
1349 scale_down_threshold: 0.2,
1350 scale_step: 2,
1351 cooldown: Duration::from_secs(60), };
1353
1354 let pool = ConnectionPoolBuilder::new()
1355 .min_size(2)
1356 .max_size(10)
1357 .circuit_breaker(false)
1358 .adaptive(adaptive)
1359 .add_endpoint("ep1".to_string(), "localhost:50051".to_string())
1360 .build();
1361
1362 pool.scale_up(2);
1364 let first_max = pool.effective_max_size();
1365 assert_eq!(first_max, 12);
1366
1367 {
1369 let mut active = pool
1370 .inner
1371 .active_count
1372 .lock()
1373 .expect("active count lock poisoned");
1374 *active = 11; }
1376
1377 ConnectionPool::evaluate_scaling(&pool.inner);
1379 assert_eq!(pool.effective_max_size(), 12);
1380
1381 {
1383 let mut active = pool
1384 .inner
1385 .active_count
1386 .lock()
1387 .expect("active count lock poisoned");
1388 *active = 0;
1389 }
1390 }
1391
1392 #[tokio::test]
1393 async fn test_pool_builder() {
1394 let pool = ConnectionPoolBuilder::new()
1395 .min_size(5)
1396 .max_size(20)
1397 .idle_timeout(Duration::from_secs(600))
1398 .balancing_strategy(BalancingStrategy::RoundRobin)
1399 .health_check_timeout(Duration::from_secs(10))
1400 .unhealthy_threshold(5)
1401 .adaptive_default()
1402 .add_endpoint("ep1".to_string(), "localhost:50051".to_string())
1403 .add_endpoint("ep2".to_string(), "localhost:50052".to_string())
1404 .build();
1405
1406 let stats = pool.stats();
1407 assert_eq!(stats.active_connections, 0);
1408 assert_eq!(stats.idle_connections, 0);
1409
1410 assert_eq!(pool.effective_max_size(), 20);
1412 }
1413
1414 #[tokio::test]
1415 async fn test_pool_add_remove_endpoint() {
1416 let pool = ConnectionPool::new(PoolConfig::default());
1417
1418 pool.add_endpoint("ep1".to_string(), "localhost:50051".to_string());
1419 pool.add_endpoint("ep2".to_string(), "localhost:50052".to_string());
1420
1421 assert!(pool.remove_endpoint("ep1"));
1422 assert!(!pool.remove_endpoint("ep3"));
1423 }
1424
1425 #[tokio::test]
1426 async fn test_pool_stats() {
1427 let pool = ConnectionPool::new(PoolConfig::default());
1428 pool.add_endpoint("ep1".to_string(), "localhost:50051".to_string());
1429
1430 let stats = pool.stats();
1431 assert_eq!(stats.total_connections, 0);
1432 assert_eq!(stats.active_connections, 0);
1433 assert_eq!(stats.idle_connections, 0);
1434 }
1435
1436 #[tokio::test]
1437 async fn test_pool_shutdown() {
1438 let pool = ConnectionPool::new(PoolConfig::default());
1439 pool.add_endpoint("ep1".to_string(), "localhost:50051".to_string());
1440
1441 let result = pool.shutdown().await;
1443 assert!(result.is_ok());
1444 }
1445
1446 #[test]
1447 fn test_pool_metrics_default() {
1448 let metrics = PoolMetrics::default();
1449 assert_eq!(metrics.total_connections, 0);
1450 assert_eq!(metrics.total_checkouts, 0);
1451 assert_eq!(metrics.total_checkins, 0);
1452 assert_eq!(metrics.total_timeouts, 0);
1453 assert_eq!(metrics.total_health_check_failures, 0);
1454 }
1455
1456 #[test]
1457 fn test_connection_meta_health_recording() {
1458 let threshold: u32 = 3;
1461
1462 let mut failure_count: u32 = 0;
1464 let mut health = ConnectionHealth::Healthy;
1465
1466 for _ in 0..threshold {
1467 failure_count += 1;
1468 if failure_count >= threshold {
1469 health = ConnectionHealth::Unhealthy;
1470 } else if failure_count >= threshold.saturating_sub(1).max(1) {
1471 health = ConnectionHealth::Degraded;
1472 }
1473 }
1474
1475 assert_eq!(health, ConnectionHealth::Unhealthy);
1476 assert_eq!(failure_count, 3);
1477 }
1478
1479 #[tokio::test]
1480 async fn test_scale_respects_adaptive_bounds() {
1481 let adaptive = AdaptiveConfig {
1482 min_size: 5,
1483 max_size: 15,
1484 scale_up_threshold: 0.8,
1485 scale_down_threshold: 0.2,
1486 scale_step: 2,
1487 cooldown: Duration::from_millis(10),
1488 };
1489
1490 let pool = ConnectionPoolBuilder::new()
1491 .min_size(5)
1492 .max_size(10)
1493 .circuit_breaker(false)
1494 .adaptive(adaptive)
1495 .add_endpoint("ep1".to_string(), "localhost:50051".to_string())
1496 .build();
1497
1498 pool.scale_up(10);
1500 assert_eq!(pool.effective_max_size(), 15); pool.scale_down(20);
1504 assert_eq!(pool.effective_max_size(), 5); }
1506
1507 #[tokio::test]
1508 async fn test_metrics_after_operations() {
1509 let pool = ConnectionPoolBuilder::new()
1510 .min_size(0)
1511 .max_size(10)
1512 .circuit_breaker(false)
1513 .add_endpoint("ep1".to_string(), "localhost:50051".to_string())
1514 .build();
1515
1516 pool.inner.total_checkouts.fetch_add(5, Ordering::Relaxed);
1518 pool.inner.total_checkins.fetch_add(3, Ordering::Relaxed);
1519 pool.inner.total_timeouts.fetch_add(1, Ordering::Relaxed);
1520 pool.inner
1521 .total_health_check_failures
1522 .fetch_add(2, Ordering::Relaxed);
1523 pool.inner
1524 .checkout_duration_sum_us
1525 .fetch_add(5000, Ordering::Relaxed);
1526 pool.inner
1527 .checkout_count_for_avg
1528 .fetch_add(5, Ordering::Relaxed);
1529
1530 let metrics = pool.metrics();
1531 assert_eq!(metrics.total_checkouts, 5);
1532 assert_eq!(metrics.total_checkins, 3);
1533 assert_eq!(metrics.total_timeouts, 1);
1534 assert_eq!(metrics.total_health_check_failures, 2);
1535 assert_eq!(metrics.avg_checkout_duration_us, 1000); }
1537}