1use async_trait::async_trait;
7use pingora::upstreams::peer::HttpPeer;
8use rand::seq::IndexedRandom;
9use std::collections::HashMap;
10use std::net::ToSocketAddrs;
11use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
12use std::sync::Arc;
13use std::time::Duration;
14use tokio::sync::RwLock;
15use tracing::{debug, error, info, trace, warn};
16
17use zentinel_common::{
18 errors::{ZentinelError, ZentinelResult},
19 types::{CircuitBreakerConfig, LoadBalancingAlgorithm},
20 CircuitBreaker, UpstreamId,
21};
22use zentinel_config::UpstreamConfig;
23
24#[derive(Debug, Clone)]
33pub struct UpstreamTarget {
34 pub address: String,
36 pub port: u16,
38 pub weight: u32,
40}
41
42impl UpstreamTarget {
43 pub fn new(address: impl Into<String>, port: u16, weight: u32) -> Self {
45 Self {
46 address: address.into(),
47 port,
48 weight,
49 }
50 }
51
52 pub fn from_address(addr: &str) -> Option<Self> {
54 let parts: Vec<&str> = addr.rsplitn(2, ':').collect();
55 if parts.len() == 2 {
56 let port = parts[0].parse().ok()?;
57 let address = parts[1].to_string();
58 Some(Self {
59 address,
60 port,
61 weight: 100,
62 })
63 } else {
64 None
65 }
66 }
67
68 pub fn from_config(config: &zentinel_config::UpstreamTarget) -> Option<Self> {
70 Self::from_address(&config.address).map(|mut t| {
71 t.weight = config.weight;
72 t
73 })
74 }
75
76 pub fn full_address(&self) -> String {
78 format!("{}:{}", self.address, self.port)
79 }
80}
81
82pub mod adaptive;
88pub mod consistent_hash;
89pub mod drain;
90pub mod health;
91pub mod inference_health;
92pub mod least_tokens;
93pub mod locality;
94pub mod maglev;
95pub mod p2c;
96pub mod peak_ewma;
97pub mod sticky_session;
98pub mod subset;
99pub mod weighted_least_conn;
100
101pub use adaptive::{AdaptiveBalancer, AdaptiveConfig};
103pub use consistent_hash::{ConsistentHashBalancer, ConsistentHashConfig};
104pub use health::{ActiveHealthChecker, HealthCheckRunner};
105pub use inference_health::InferenceHealthCheck;
106pub use least_tokens::{
107 LeastTokensQueuedBalancer, LeastTokensQueuedConfig, LeastTokensQueuedTargetStats,
108};
109pub use locality::{LocalityAwareBalancer, LocalityAwareConfig};
110pub use maglev::{MaglevBalancer, MaglevConfig};
111pub use p2c::{P2cBalancer, P2cConfig};
112pub use peak_ewma::{PeakEwmaBalancer, PeakEwmaConfig};
113pub use sticky_session::{StickySessionBalancer, StickySessionRuntimeConfig};
114pub use subset::{SubsetBalancer, SubsetConfig};
115pub use weighted_least_conn::{WeightedLeastConnBalancer, WeightedLeastConnConfig};
116
117#[derive(Debug, Clone)]
119pub struct RequestContext {
120 pub client_ip: Option<std::net::SocketAddr>,
121 pub headers: HashMap<String, String>,
122 pub path: String,
123 pub method: String,
124}
125
126#[async_trait]
128pub trait LoadBalancer: Send + Sync {
129 async fn select(&self, context: Option<&RequestContext>) -> ZentinelResult<TargetSelection>;
131
132 async fn report_health(&self, address: &str, healthy: bool);
134
135 async fn healthy_targets(&self) -> Vec<String>;
137
138 async fn release(&self, _selection: &TargetSelection) {
140 }
142
143 async fn report_result(
145 &self,
146 _selection: &TargetSelection,
147 _success: bool,
148 _latency: Option<Duration>,
149 ) {
150 }
152
153 async fn report_result_with_latency(
160 &self,
161 address: &str,
162 success: bool,
163 _latency: Option<Duration>,
164 ) {
165 self.report_health(address, success).await;
167 }
168}
169
170#[derive(Debug, Clone)]
172pub struct TargetSelection {
173 pub address: String,
175 pub weight: u32,
177 pub metadata: HashMap<String, String>,
179}
180
181pub struct UpstreamPool {
183 id: UpstreamId,
185 targets: Vec<UpstreamTarget>,
187 load_balancer: Arc<dyn LoadBalancer>,
189 pool_config: ConnectionPoolConfig,
191 http_version: HttpVersionOptions,
193 tls_enabled: bool,
195 tls_sni: Option<String>,
197 tls_config: Option<zentinel_config::UpstreamTlsConfig>,
199 circuit_breakers: Arc<RwLock<HashMap<String, CircuitBreaker>>>,
201 stats: Arc<PoolStats>,
203}
204
205pub struct ConnectionPoolConfig {
214 pub max_connections: usize,
216 pub max_idle: usize,
218 pub idle_timeout: Duration,
220 pub max_lifetime: Option<Duration>,
222 pub connection_timeout: Duration,
224 pub read_timeout: Duration,
226 pub write_timeout: Duration,
228}
229
230pub struct HttpVersionOptions {
232 pub min_version: u8,
234 pub max_version: u8,
236 pub h2_ping_interval: Duration,
238 pub max_h2_streams: usize,
240}
241
242impl ConnectionPoolConfig {
243 pub fn from_config(
245 pool_config: &zentinel_config::ConnectionPoolConfig,
246 timeouts: &zentinel_config::UpstreamTimeouts,
247 ) -> Self {
248 Self {
249 max_connections: pool_config.max_connections,
250 max_idle: pool_config.max_idle,
251 idle_timeout: Duration::from_secs(pool_config.idle_timeout_secs),
252 max_lifetime: pool_config.max_lifetime_secs.map(Duration::from_secs),
253 connection_timeout: Duration::from_secs(timeouts.connect_secs),
254 read_timeout: Duration::from_secs(timeouts.read_secs),
255 write_timeout: Duration::from_secs(timeouts.write_secs),
256 }
257 }
258}
259
260#[derive(Default)]
264pub struct PoolStats {
265 pub requests: AtomicU64,
267 pub successes: AtomicU64,
269 pub failures: AtomicU64,
271 pub retries: AtomicU64,
273 pub circuit_breaker_trips: AtomicU64,
275 pub active_requests: AtomicU64,
277}
278
279#[derive(Debug, Clone)]
281pub struct ShadowTarget {
282 pub scheme: String,
284 pub host: String,
286 pub port: u16,
288 pub sni: Option<String>,
290}
291
292impl ShadowTarget {
293 pub fn build_url(&self, path: &str) -> String {
295 let port_suffix = match (self.scheme.as_str(), self.port) {
296 ("http", 80) | ("https", 443) => String::new(),
297 _ => format!(":{}", self.port),
298 };
299 format!("{}://{}{}{}", self.scheme, self.host, port_suffix, path)
300 }
301}
302
303#[derive(Debug, Clone)]
305pub struct PoolConfigSnapshot {
306 pub max_connections: usize,
308 pub max_idle: usize,
310 pub idle_timeout_secs: u64,
312 pub max_lifetime_secs: Option<u64>,
314 pub connection_timeout_secs: u64,
316 pub read_timeout_secs: u64,
318 pub write_timeout_secs: u64,
320}
321
322struct RoundRobinBalancer {
324 targets: Vec<UpstreamTarget>,
325 current: AtomicUsize,
326 health_status: Arc<RwLock<HashMap<String, bool>>>,
327}
328
329impl RoundRobinBalancer {
330 fn new(targets: Vec<UpstreamTarget>) -> Self {
331 let mut health_status = HashMap::new();
332 for target in &targets {
333 health_status.insert(target.full_address(), true);
334 }
335
336 Self {
337 targets,
338 current: AtomicUsize::new(0),
339 health_status: Arc::new(RwLock::new(health_status)),
340 }
341 }
342}
343
344#[async_trait]
345impl LoadBalancer for RoundRobinBalancer {
346 async fn select(&self, _context: Option<&RequestContext>) -> ZentinelResult<TargetSelection> {
347 trace!(
348 total_targets = self.targets.len(),
349 algorithm = "round_robin",
350 "Selecting upstream target"
351 );
352
353 let health = self.health_status.read().await;
354 let healthy_targets: Vec<_> = self
355 .targets
356 .iter()
357 .filter(|t| *health.get(&t.full_address()).unwrap_or(&true))
358 .collect();
359
360 if healthy_targets.is_empty() {
361 warn!(
362 total_targets = self.targets.len(),
363 algorithm = "round_robin",
364 "No healthy upstream targets available"
365 );
366 return Err(ZentinelError::NoHealthyUpstream);
367 }
368
369 let index = self.current.fetch_add(1, Ordering::Relaxed) % healthy_targets.len();
370 let target = healthy_targets[index];
371
372 trace!(
373 selected_target = %target.full_address(),
374 healthy_count = healthy_targets.len(),
375 index = index,
376 algorithm = "round_robin",
377 "Selected target via round robin"
378 );
379
380 Ok(TargetSelection {
381 address: target.full_address(),
382 weight: target.weight,
383 metadata: HashMap::new(),
384 })
385 }
386
387 async fn report_health(&self, address: &str, healthy: bool) {
388 trace!(
389 target = %address,
390 healthy = healthy,
391 algorithm = "round_robin",
392 "Updating target health status"
393 );
394 self.health_status
395 .write()
396 .await
397 .insert(address.to_string(), healthy);
398 }
399
400 async fn healthy_targets(&self) -> Vec<String> {
401 self.health_status
402 .read()
403 .await
404 .iter()
405 .filter_map(|(addr, &healthy)| if healthy { Some(addr.clone()) } else { None })
406 .collect()
407 }
408}
409
410struct RandomBalancer {
412 targets: Vec<UpstreamTarget>,
413 health_status: Arc<RwLock<HashMap<String, bool>>>,
414}
415
416impl RandomBalancer {
417 fn new(targets: Vec<UpstreamTarget>) -> Self {
418 let mut health_status = HashMap::new();
419 for target in &targets {
420 health_status.insert(target.full_address(), true);
421 }
422
423 Self {
424 targets,
425 health_status: Arc::new(RwLock::new(health_status)),
426 }
427 }
428}
429
430#[async_trait]
431impl LoadBalancer for RandomBalancer {
432 async fn select(&self, _context: Option<&RequestContext>) -> ZentinelResult<TargetSelection> {
433 use rand::seq::SliceRandom;
434
435 trace!(
436 total_targets = self.targets.len(),
437 algorithm = "random",
438 "Selecting upstream target"
439 );
440
441 let health = self.health_status.read().await;
442 let healthy_targets: Vec<_> = self
443 .targets
444 .iter()
445 .filter(|t| *health.get(&t.full_address()).unwrap_or(&true))
446 .collect();
447
448 if healthy_targets.is_empty() {
449 warn!(
450 total_targets = self.targets.len(),
451 algorithm = "random",
452 "No healthy upstream targets available"
453 );
454 return Err(ZentinelError::NoHealthyUpstream);
455 }
456
457 let mut rng = rand::rng();
458 let target = healthy_targets
459 .choose(&mut rng)
460 .ok_or(ZentinelError::NoHealthyUpstream)?;
461
462 trace!(
463 selected_target = %target.full_address(),
464 healthy_count = healthy_targets.len(),
465 algorithm = "random",
466 "Selected target via random selection"
467 );
468
469 Ok(TargetSelection {
470 address: target.full_address(),
471 weight: target.weight,
472 metadata: HashMap::new(),
473 })
474 }
475
476 async fn report_health(&self, address: &str, healthy: bool) {
477 trace!(
478 target = %address,
479 healthy = healthy,
480 algorithm = "random",
481 "Updating target health status"
482 );
483 self.health_status
484 .write()
485 .await
486 .insert(address.to_string(), healthy);
487 }
488
489 async fn healthy_targets(&self) -> Vec<String> {
490 self.health_status
491 .read()
492 .await
493 .iter()
494 .filter_map(|(addr, &healthy)| if healthy { Some(addr.clone()) } else { None })
495 .collect()
496 }
497}
498
499struct LeastConnectionsBalancer {
501 targets: Vec<UpstreamTarget>,
502 connections: Arc<RwLock<HashMap<String, usize>>>,
503 health_status: Arc<RwLock<HashMap<String, bool>>>,
504}
505
506impl LeastConnectionsBalancer {
507 fn new(targets: Vec<UpstreamTarget>) -> Self {
508 let mut health_status = HashMap::new();
509 let mut connections = HashMap::new();
510
511 for target in &targets {
512 let addr = target.full_address();
513 health_status.insert(addr.clone(), true);
514 connections.insert(addr, 0);
515 }
516
517 Self {
518 targets,
519 connections: Arc::new(RwLock::new(connections)),
520 health_status: Arc::new(RwLock::new(health_status)),
521 }
522 }
523}
524
525#[async_trait]
526impl LoadBalancer for LeastConnectionsBalancer {
527 async fn select(&self, _context: Option<&RequestContext>) -> ZentinelResult<TargetSelection> {
528 trace!(
529 total_targets = self.targets.len(),
530 algorithm = "least_connections",
531 "Selecting upstream target"
532 );
533
534 let health = self.health_status.read().await;
535 let conns = self.connections.read().await;
536
537 let mut best_target = None;
538 let mut min_connections = usize::MAX;
539
540 for target in &self.targets {
541 let addr = target.full_address();
542 if !*health.get(&addr).unwrap_or(&true) {
543 trace!(
544 target = %addr,
545 algorithm = "least_connections",
546 "Skipping unhealthy target"
547 );
548 continue;
549 }
550
551 let conn_count = *conns.get(&addr).unwrap_or(&0);
552 trace!(
553 target = %addr,
554 connections = conn_count,
555 "Evaluating target connection count"
556 );
557 if conn_count < min_connections {
558 min_connections = conn_count;
559 best_target = Some(target);
560 }
561 }
562
563 match best_target {
564 Some(target) => {
565 trace!(
566 selected_target = %target.full_address(),
567 connections = min_connections,
568 algorithm = "least_connections",
569 "Selected target with fewest connections"
570 );
571 Ok(TargetSelection {
572 address: target.full_address(),
573 weight: target.weight,
574 metadata: HashMap::new(),
575 })
576 }
577 None => {
578 warn!(
579 total_targets = self.targets.len(),
580 algorithm = "least_connections",
581 "No healthy upstream targets available"
582 );
583 Err(ZentinelError::NoHealthyUpstream)
584 }
585 }
586 }
587
588 async fn report_health(&self, address: &str, healthy: bool) {
589 trace!(
590 target = %address,
591 healthy = healthy,
592 algorithm = "least_connections",
593 "Updating target health status"
594 );
595 self.health_status
596 .write()
597 .await
598 .insert(address.to_string(), healthy);
599 }
600
601 async fn healthy_targets(&self) -> Vec<String> {
602 self.health_status
603 .read()
604 .await
605 .iter()
606 .filter_map(|(addr, &healthy)| if healthy { Some(addr.clone()) } else { None })
607 .collect()
608 }
609}
610
611struct WeightedBalancer {
613 targets: Vec<UpstreamTarget>,
614 weights: Vec<u32>,
615 current_index: AtomicUsize,
616 health_status: Arc<RwLock<HashMap<String, bool>>>,
617}
618
619#[async_trait]
620impl LoadBalancer for WeightedBalancer {
621 async fn select(&self, _context: Option<&RequestContext>) -> ZentinelResult<TargetSelection> {
622 trace!(
623 total_targets = self.targets.len(),
624 algorithm = "weighted",
625 "Selecting upstream target"
626 );
627
628 let health = self.health_status.read().await;
629 let healthy: Vec<_> = self
630 .targets
631 .iter()
632 .enumerate()
633 .filter(|(_, t)| *health.get(&t.full_address()).unwrap_or(&true))
634 .map(|(i, _)| i)
635 .collect();
636
637 if healthy.is_empty() {
638 warn!(
639 total_targets = self.targets.len(),
640 algorithm = "weighted",
641 "No healthy upstream targets available"
642 );
643 return Err(ZentinelError::NoHealthyUpstream);
644 }
645
646 let total_weight: u32 = healthy
649 .iter()
650 .map(|&i| self.weights.get(i).copied().unwrap_or(1))
651 .sum();
652
653 if total_weight == 0 {
654 return Err(ZentinelError::NoHealthyUpstream);
655 }
656
657 let slot = (self.current_index.fetch_add(1, Ordering::Relaxed) as u32) % total_weight;
658 let mut cumulative = 0u32;
659 let mut target_idx = healthy[0];
660 for &i in &healthy {
661 let w = self.weights.get(i).copied().unwrap_or(1);
662 cumulative += w;
663 if slot < cumulative {
664 target_idx = i;
665 break;
666 }
667 }
668
669 let target = &self.targets[target_idx];
670 let weight = self.weights.get(target_idx).copied().unwrap_or(1);
671
672 trace!(
673 selected_target = %target.full_address(),
674 weight = weight,
675 healthy_count = healthy.len(),
676 algorithm = "weighted",
677 "Selected target via weighted round robin"
678 );
679
680 Ok(TargetSelection {
681 address: target.full_address(),
682 weight,
683 metadata: HashMap::new(),
684 })
685 }
686
687 async fn report_health(&self, address: &str, healthy: bool) {
688 trace!(
689 target = %address,
690 healthy = healthy,
691 algorithm = "weighted",
692 "Updating target health status"
693 );
694 self.health_status
695 .write()
696 .await
697 .insert(address.to_string(), healthy);
698 }
699
700 async fn healthy_targets(&self) -> Vec<String> {
701 self.health_status
702 .read()
703 .await
704 .iter()
705 .filter_map(|(addr, &healthy)| if healthy { Some(addr.clone()) } else { None })
706 .collect()
707 }
708}
709
710struct IpHashBalancer {
712 targets: Vec<UpstreamTarget>,
713 health_status: Arc<RwLock<HashMap<String, bool>>>,
714}
715
716#[async_trait]
717impl LoadBalancer for IpHashBalancer {
718 async fn select(&self, context: Option<&RequestContext>) -> ZentinelResult<TargetSelection> {
719 trace!(
720 total_targets = self.targets.len(),
721 algorithm = "ip_hash",
722 "Selecting upstream target"
723 );
724
725 let health = self.health_status.read().await;
726 let healthy_targets: Vec<_> = self
727 .targets
728 .iter()
729 .filter(|t| *health.get(&t.full_address()).unwrap_or(&true))
730 .collect();
731
732 if healthy_targets.is_empty() {
733 warn!(
734 total_targets = self.targets.len(),
735 algorithm = "ip_hash",
736 "No healthy upstream targets available"
737 );
738 return Err(ZentinelError::NoHealthyUpstream);
739 }
740
741 let (hash, client_ip_str) = if let Some(ctx) = context {
743 if let Some(ip) = &ctx.client_ip {
744 use std::hash::{Hash, Hasher};
745 let mut hasher = std::collections::hash_map::DefaultHasher::new();
746 ip.hash(&mut hasher);
747 (hasher.finish(), Some(ip.to_string()))
748 } else {
749 (0, None)
750 }
751 } else {
752 (0, None)
753 };
754
755 let idx = (hash as usize) % healthy_targets.len();
756 let target = healthy_targets[idx];
757
758 trace!(
759 selected_target = %target.full_address(),
760 client_ip = client_ip_str.as_deref().unwrap_or("unknown"),
761 hash = hash,
762 index = idx,
763 healthy_count = healthy_targets.len(),
764 algorithm = "ip_hash",
765 "Selected target via IP hash"
766 );
767
768 Ok(TargetSelection {
769 address: target.full_address(),
770 weight: target.weight,
771 metadata: HashMap::new(),
772 })
773 }
774
775 async fn report_health(&self, address: &str, healthy: bool) {
776 trace!(
777 target = %address,
778 healthy = healthy,
779 algorithm = "ip_hash",
780 "Updating target health status"
781 );
782 self.health_status
783 .write()
784 .await
785 .insert(address.to_string(), healthy);
786 }
787
788 async fn healthy_targets(&self) -> Vec<String> {
789 self.health_status
790 .read()
791 .await
792 .iter()
793 .filter_map(|(addr, &healthy)| if healthy { Some(addr.clone()) } else { None })
794 .collect()
795 }
796}
797
798impl UpstreamPool {
799 pub async fn new(config: UpstreamConfig) -> ZentinelResult<Self> {
801 let id = UpstreamId::new(&config.id);
802
803 info!(
804 upstream_id = %config.id,
805 target_count = config.targets.len(),
806 algorithm = ?config.load_balancing,
807 "Creating upstream pool"
808 );
809
810 let targets: Vec<UpstreamTarget> = config
812 .targets
813 .iter()
814 .filter_map(UpstreamTarget::from_config)
815 .collect();
816
817 if targets.is_empty() {
818 error!(
819 upstream_id = %config.id,
820 "No valid upstream targets configured"
821 );
822 return Err(ZentinelError::Config {
823 message: "No valid upstream targets".to_string(),
824 source: None,
825 });
826 }
827
828 for target in &targets {
829 debug!(
830 upstream_id = %config.id,
831 target = %target.full_address(),
832 weight = target.weight,
833 "Registered upstream target"
834 );
835 }
836
837 debug!(
839 upstream_id = %config.id,
840 algorithm = ?config.load_balancing,
841 "Creating load balancer"
842 );
843 let load_balancer = Self::create_load_balancer(&config.load_balancing, &targets, &config)?;
844
845 debug!(
847 upstream_id = %config.id,
848 max_connections = config.connection_pool.max_connections,
849 max_idle = config.connection_pool.max_idle,
850 idle_timeout_secs = config.connection_pool.idle_timeout_secs,
851 connect_timeout_secs = config.timeouts.connect_secs,
852 read_timeout_secs = config.timeouts.read_secs,
853 write_timeout_secs = config.timeouts.write_secs,
854 "Creating connection pool configuration"
855 );
856 let pool_config =
857 ConnectionPoolConfig::from_config(&config.connection_pool, &config.timeouts);
858
859 let http_version = HttpVersionOptions {
861 min_version: config.http_version.min_version,
862 max_version: config.http_version.max_version,
863 h2_ping_interval: if config.http_version.h2_ping_interval_secs > 0 {
864 Duration::from_secs(config.http_version.h2_ping_interval_secs)
865 } else {
866 Duration::ZERO
867 },
868 max_h2_streams: config.http_version.max_h2_streams,
869 };
870
871 let tls_enabled = config.tls.is_some();
873 let tls_sni = config.tls.as_ref().and_then(|t| t.sni.clone());
874 let tls_config = config.tls.clone();
875
876 if let Some(ref tls) = tls_config {
878 if tls.client_cert.is_some() {
879 info!(
880 upstream_id = %config.id,
881 "mTLS enabled for upstream (client certificate configured)"
882 );
883 }
884 }
885
886 if http_version.max_version >= 2 && tls_enabled {
887 info!(
888 upstream_id = %config.id,
889 "HTTP/2 enabled for upstream (via ALPN)"
890 );
891 }
892
893 let cb_config = config.circuit_breaker.unwrap_or_default();
898
899 let mut circuit_breakers = HashMap::new();
900 for target in &targets {
901 trace!(
902 upstream_id = %config.id,
903 target = %target.full_address(),
904 "Initializing circuit breaker for target, configuration {:?}",
905 cb_config
906 );
907
908 circuit_breakers.insert(target.full_address(), CircuitBreaker::new(cb_config));
909 }
910
911 let pool = Self {
912 id: id.clone(),
913 targets,
914 load_balancer,
915 pool_config,
916 http_version,
917 tls_enabled,
918 tls_sni,
919 tls_config,
920 circuit_breakers: Arc::new(RwLock::new(circuit_breakers)),
921 stats: Arc::new(PoolStats::default()),
922 };
923
924 info!(
925 upstream_id = %id,
926 target_count = pool.targets.len(),
927 "Upstream pool created successfully"
928 );
929
930 Ok(pool)
931 }
932
933 fn create_load_balancer(
935 algorithm: &LoadBalancingAlgorithm,
936 targets: &[UpstreamTarget],
937 config: &UpstreamConfig,
938 ) -> ZentinelResult<Arc<dyn LoadBalancer>> {
939 let balancer: Arc<dyn LoadBalancer> = match algorithm {
940 LoadBalancingAlgorithm::RoundRobin => {
941 Arc::new(RoundRobinBalancer::new(targets.to_vec()))
942 }
943 LoadBalancingAlgorithm::LeastConnections => {
944 Arc::new(LeastConnectionsBalancer::new(targets.to_vec()))
945 }
946 LoadBalancingAlgorithm::Weighted => {
947 let weights: Vec<u32> = targets.iter().map(|t| t.weight).collect();
948 Arc::new(WeightedBalancer {
949 targets: targets.to_vec(),
950 weights,
951 current_index: AtomicUsize::new(0),
952 health_status: Arc::new(RwLock::new(HashMap::new())),
953 })
954 }
955 LoadBalancingAlgorithm::IpHash => Arc::new(IpHashBalancer {
956 targets: targets.to_vec(),
957 health_status: Arc::new(RwLock::new(HashMap::new())),
958 }),
959 LoadBalancingAlgorithm::Random => Arc::new(RandomBalancer::new(targets.to_vec())),
960 LoadBalancingAlgorithm::ConsistentHash => Arc::new(ConsistentHashBalancer::new(
961 targets.to_vec(),
962 ConsistentHashConfig::default(),
963 )),
964 LoadBalancingAlgorithm::PowerOfTwoChoices => {
965 Arc::new(P2cBalancer::new(targets.to_vec(), P2cConfig::default()))
966 }
967 LoadBalancingAlgorithm::Adaptive => Arc::new(AdaptiveBalancer::new(
968 targets.to_vec(),
969 AdaptiveConfig::default(),
970 )),
971 LoadBalancingAlgorithm::LeastTokensQueued => Arc::new(LeastTokensQueuedBalancer::new(
972 targets.to_vec(),
973 LeastTokensQueuedConfig::default(),
974 )),
975 LoadBalancingAlgorithm::Maglev => Arc::new(MaglevBalancer::new(
976 targets.to_vec(),
977 MaglevConfig::default(),
978 )),
979 LoadBalancingAlgorithm::LocalityAware => Arc::new(LocalityAwareBalancer::new(
980 targets.to_vec(),
981 LocalityAwareConfig::default(),
982 )),
983 LoadBalancingAlgorithm::PeakEwma => Arc::new(PeakEwmaBalancer::new(
984 targets.to_vec(),
985 PeakEwmaConfig::default(),
986 )),
987 LoadBalancingAlgorithm::DeterministicSubset => Arc::new(SubsetBalancer::new(
988 targets.to_vec(),
989 SubsetConfig::default(),
990 )),
991 LoadBalancingAlgorithm::WeightedLeastConnections => {
992 Arc::new(WeightedLeastConnBalancer::new(
993 targets.to_vec(),
994 WeightedLeastConnConfig::default(),
995 ))
996 }
997 LoadBalancingAlgorithm::Sticky => {
998 let sticky_config = config.sticky_session.as_ref().ok_or_else(|| {
1000 ZentinelError::Config {
1001 message: format!(
1002 "Upstream '{}' uses Sticky algorithm but no sticky_session config provided",
1003 config.id
1004 ),
1005 source: None,
1006 }
1007 })?;
1008
1009 let runtime_config = StickySessionRuntimeConfig::from_config(sticky_config);
1011
1012 let fallback = Self::create_load_balancer_inner(&sticky_config.fallback, targets)?;
1014
1015 info!(
1016 upstream_id = %config.id,
1017 cookie_name = %runtime_config.cookie_name,
1018 cookie_ttl_secs = runtime_config.cookie_ttl_secs,
1019 fallback_algorithm = ?sticky_config.fallback,
1020 "Creating sticky session balancer"
1021 );
1022
1023 Arc::new(StickySessionBalancer::new(
1024 targets.to_vec(),
1025 runtime_config,
1026 fallback,
1027 ))
1028 }
1029 };
1030 Ok(balancer)
1031 }
1032
1033 fn create_load_balancer_inner(
1035 algorithm: &LoadBalancingAlgorithm,
1036 targets: &[UpstreamTarget],
1037 ) -> ZentinelResult<Arc<dyn LoadBalancer>> {
1038 let balancer: Arc<dyn LoadBalancer> = match algorithm {
1039 LoadBalancingAlgorithm::RoundRobin => {
1040 Arc::new(RoundRobinBalancer::new(targets.to_vec()))
1041 }
1042 LoadBalancingAlgorithm::LeastConnections => {
1043 Arc::new(LeastConnectionsBalancer::new(targets.to_vec()))
1044 }
1045 LoadBalancingAlgorithm::Weighted => {
1046 let weights: Vec<u32> = targets.iter().map(|t| t.weight).collect();
1047 Arc::new(WeightedBalancer {
1048 targets: targets.to_vec(),
1049 weights,
1050 current_index: AtomicUsize::new(0),
1051 health_status: Arc::new(RwLock::new(HashMap::new())),
1052 })
1053 }
1054 LoadBalancingAlgorithm::IpHash => Arc::new(IpHashBalancer {
1055 targets: targets.to_vec(),
1056 health_status: Arc::new(RwLock::new(HashMap::new())),
1057 }),
1058 LoadBalancingAlgorithm::Random => Arc::new(RandomBalancer::new(targets.to_vec())),
1059 LoadBalancingAlgorithm::ConsistentHash => Arc::new(ConsistentHashBalancer::new(
1060 targets.to_vec(),
1061 ConsistentHashConfig::default(),
1062 )),
1063 LoadBalancingAlgorithm::PowerOfTwoChoices => {
1064 Arc::new(P2cBalancer::new(targets.to_vec(), P2cConfig::default()))
1065 }
1066 LoadBalancingAlgorithm::Adaptive => Arc::new(AdaptiveBalancer::new(
1067 targets.to_vec(),
1068 AdaptiveConfig::default(),
1069 )),
1070 LoadBalancingAlgorithm::LeastTokensQueued => Arc::new(LeastTokensQueuedBalancer::new(
1071 targets.to_vec(),
1072 LeastTokensQueuedConfig::default(),
1073 )),
1074 LoadBalancingAlgorithm::Maglev => Arc::new(MaglevBalancer::new(
1075 targets.to_vec(),
1076 MaglevConfig::default(),
1077 )),
1078 LoadBalancingAlgorithm::LocalityAware => Arc::new(LocalityAwareBalancer::new(
1079 targets.to_vec(),
1080 LocalityAwareConfig::default(),
1081 )),
1082 LoadBalancingAlgorithm::PeakEwma => Arc::new(PeakEwmaBalancer::new(
1083 targets.to_vec(),
1084 PeakEwmaConfig::default(),
1085 )),
1086 LoadBalancingAlgorithm::DeterministicSubset => Arc::new(SubsetBalancer::new(
1087 targets.to_vec(),
1088 SubsetConfig::default(),
1089 )),
1090 LoadBalancingAlgorithm::WeightedLeastConnections => {
1091 Arc::new(WeightedLeastConnBalancer::new(
1092 targets.to_vec(),
1093 WeightedLeastConnConfig::default(),
1094 ))
1095 }
1096 LoadBalancingAlgorithm::Sticky => {
1097 return Err(ZentinelError::Config {
1099 message: "Sticky algorithm cannot be used as fallback for sticky sessions"
1100 .to_string(),
1101 source: None,
1102 });
1103 }
1104 };
1105 Ok(balancer)
1106 }
1107
1108 pub async fn select_peer_with_metadata(
1114 &self,
1115 context: Option<&RequestContext>,
1116 ) -> ZentinelResult<(HttpPeer, HashMap<String, String>)> {
1117 let request_num = self.stats.requests.fetch_add(1, Ordering::Relaxed) + 1;
1118
1119 trace!(
1120 upstream_id = %self.id,
1121 request_num = request_num,
1122 target_count = self.targets.len(),
1123 "Starting peer selection with metadata"
1124 );
1125
1126 let mut attempts = 0;
1127 let max_attempts = self.targets.len() * 2;
1128
1129 while attempts < max_attempts {
1130 attempts += 1;
1131
1132 trace!(
1133 upstream_id = %self.id,
1134 attempt = attempts,
1135 max_attempts = max_attempts,
1136 "Attempting to select peer"
1137 );
1138
1139 let selection = match self.load_balancer.select(context).await {
1140 Ok(s) => s,
1141 Err(e) => {
1142 warn!(
1143 upstream_id = %self.id,
1144 attempt = attempts,
1145 error = %e,
1146 "Load balancer selection failed"
1147 );
1148 continue;
1149 }
1150 };
1151
1152 trace!(
1153 upstream_id = %self.id,
1154 target = %selection.address,
1155 attempt = attempts,
1156 "Load balancer selected target"
1157 );
1158
1159 let breakers = self.circuit_breakers.read().await;
1161 if let Some(breaker) = breakers.get(&selection.address) {
1162 if !breaker.is_closed() {
1163 debug!(
1164 upstream_id = %self.id,
1165 target = %selection.address,
1166 attempt = attempts,
1167 "Circuit breaker is open, skipping target"
1168 );
1169 self.stats
1170 .circuit_breaker_trips
1171 .fetch_add(1, Ordering::Relaxed);
1172 continue;
1173 }
1174 }
1175
1176 trace!(
1178 upstream_id = %self.id,
1179 target = %selection.address,
1180 "Creating peer for upstream (Pingora handles connection reuse)"
1181 );
1182 let peer = self.create_peer(&selection)?;
1183
1184 debug!(
1185 upstream_id = %self.id,
1186 target = %selection.address,
1187 attempt = attempts,
1188 metadata_keys = ?selection.metadata.keys().collect::<Vec<_>>(),
1189 "Selected upstream peer with metadata"
1190 );
1191
1192 self.stats.successes.fetch_add(1, Ordering::Relaxed);
1193 return Ok((peer, selection.metadata));
1194 }
1195
1196 self.stats.failures.fetch_add(1, Ordering::Relaxed);
1197 error!(
1198 upstream_id = %self.id,
1199 attempts = attempts,
1200 max_attempts = max_attempts,
1201 "Failed to select upstream after max attempts"
1202 );
1203 Err(ZentinelError::upstream(
1204 self.id.to_string(),
1205 "Failed to select upstream after max attempts",
1206 ))
1207 }
1208
1209 pub async fn select_peer(&self, context: Option<&RequestContext>) -> ZentinelResult<HttpPeer> {
1211 self.select_peer_with_metadata(context)
1213 .await
1214 .map(|(peer, _)| peer)
1215 }
1216
1217 fn create_peer(&self, selection: &TargetSelection) -> ZentinelResult<HttpPeer> {
1223 let sni_hostname = self.tls_sni.clone().unwrap_or_else(|| {
1225 selection
1227 .address
1228 .split(':')
1229 .next()
1230 .unwrap_or(&selection.address)
1231 .to_string()
1232 });
1233
1234 let resolved_address = selection
1237 .address
1238 .to_socket_addrs()
1239 .map_err(|e| {
1240 error!(
1241 upstream = %self.id,
1242 address = %selection.address,
1243 error = %e,
1244 "Failed to resolve upstream address"
1245 );
1246 ZentinelError::Upstream {
1247 upstream: self.id.to_string(),
1248 message: format!("DNS resolution failed for {}: {}", selection.address, e),
1249 retryable: true,
1250 source: None,
1251 }
1252 })?
1253 .next()
1254 .ok_or_else(|| {
1255 error!(
1256 upstream = %self.id,
1257 address = %selection.address,
1258 "No addresses returned from DNS resolution"
1259 );
1260 ZentinelError::Upstream {
1261 upstream: self.id.to_string(),
1262 message: format!("No addresses for {}", selection.address),
1263 retryable: true,
1264 source: None,
1265 }
1266 })?;
1267
1268 let mut peer = HttpPeer::new(resolved_address, self.tls_enabled, sni_hostname.clone());
1270
1271 peer.options.idle_timeout = Some(self.pool_config.idle_timeout);
1275
1276 peer.options.connection_timeout = Some(self.pool_config.connection_timeout);
1278 peer.options.total_connection_timeout = Some(Duration::from_secs(10));
1279
1280 peer.options.read_timeout = Some(self.pool_config.read_timeout);
1282 peer.options.write_timeout = Some(self.pool_config.write_timeout);
1283
1284 peer.options.tcp_keepalive = Some(pingora::protocols::TcpKeepalive {
1286 idle: Duration::from_secs(60),
1287 interval: Duration::from_secs(10),
1288 count: 3,
1289 #[cfg(target_os = "linux")]
1291 user_timeout: Duration::from_secs(60),
1292 });
1293
1294 if self.tls_enabled {
1296 let alpn = match (self.http_version.min_version, self.http_version.max_version) {
1298 (2, _) => {
1299 pingora::upstreams::peer::ALPN::H2
1301 }
1302 (1, 2) | (_, 2) => {
1303 pingora::upstreams::peer::ALPN::H2H1
1305 }
1306 _ => {
1307 pingora::upstreams::peer::ALPN::H1
1309 }
1310 };
1311 peer.options.alpn = alpn;
1312
1313 if let Some(ref tls_config) = self.tls_config {
1315 if tls_config.insecure_skip_verify {
1317 peer.options.verify_cert = false;
1318 peer.options.verify_hostname = false;
1319 warn!(
1320 upstream_id = %self.id,
1321 target = %selection.address,
1322 "TLS certificate verification DISABLED (insecure_skip_verify=true)"
1323 );
1324 }
1325
1326 if let Some(ref sni) = tls_config.sni {
1328 peer.options.alternative_cn = Some(sni.clone());
1329 trace!(
1330 upstream_id = %self.id,
1331 target = %selection.address,
1332 alternative_cn = %sni,
1333 "Set alternative CN for TLS verification"
1334 );
1335 }
1336
1337 if let (Some(cert_path), Some(key_path)) =
1339 (&tls_config.client_cert, &tls_config.client_key)
1340 {
1341 match crate::tls::load_client_cert_key(cert_path, key_path) {
1342 Ok(cert_key) => {
1343 peer.client_cert_key = Some(cert_key);
1344 info!(
1345 upstream_id = %self.id,
1346 target = %selection.address,
1347 cert_path = ?cert_path,
1348 "mTLS client certificate configured"
1349 );
1350 }
1351 Err(e) => {
1352 error!(
1353 upstream_id = %self.id,
1354 target = %selection.address,
1355 error = %e,
1356 "Failed to load mTLS client certificate"
1357 );
1358 return Err(ZentinelError::Tls {
1359 message: format!("Failed to load client certificate: {}", e),
1360 source: None,
1361 });
1362 }
1363 }
1364 }
1365 }
1366
1367 trace!(
1368 upstream_id = %self.id,
1369 target = %selection.address,
1370 alpn = ?peer.options.alpn,
1371 min_version = self.http_version.min_version,
1372 max_version = self.http_version.max_version,
1373 verify_cert = peer.options.verify_cert,
1374 verify_hostname = peer.options.verify_hostname,
1375 "Configured ALPN and TLS options for HTTP version negotiation"
1376 );
1377 }
1378
1379 if self.http_version.max_version >= 2 {
1381 if !self.http_version.h2_ping_interval.is_zero() {
1383 peer.options.h2_ping_interval = Some(self.http_version.h2_ping_interval);
1384 trace!(
1385 upstream_id = %self.id,
1386 target = %selection.address,
1387 h2_ping_interval_secs = self.http_version.h2_ping_interval.as_secs(),
1388 "Configured H2 ping interval"
1389 );
1390 }
1391 }
1392
1393 trace!(
1394 upstream_id = %self.id,
1395 target = %selection.address,
1396 tls = self.tls_enabled,
1397 sni = %sni_hostname,
1398 idle_timeout_secs = self.pool_config.idle_timeout.as_secs(),
1399 http_max_version = self.http_version.max_version,
1400 "Created peer with Pingora connection pooling enabled"
1401 );
1402
1403 Ok(peer)
1404 }
1405
1406 pub async fn report_result(&self, target: &str, success: bool) {
1413 trace!(
1414 upstream_id = %self.id,
1415 target = %target,
1416 success = success,
1417 "Reporting connection result"
1418 );
1419
1420 if success {
1421 if let Some(breaker) = self.circuit_breakers.read().await.get(target) {
1422 breaker.record_success();
1423 trace!(
1424 upstream_id = %self.id,
1425 target = %target,
1426 "Recorded success in circuit breaker"
1427 );
1428 }
1429 self.load_balancer.report_health(target, true).await;
1430 } else {
1431 let breaker_opened =
1432 if let Some(breaker) = self.circuit_breakers.read().await.get(target) {
1433 let opened = breaker.record_failure();
1434 debug!(
1435 upstream_id = %self.id,
1436 target = %target,
1437 circuit_breaker_opened = opened,
1438 "Recorded failure in circuit breaker"
1439 );
1440 opened
1441 } else {
1442 false
1443 };
1444
1445 self.stats.failures.fetch_add(1, Ordering::Relaxed);
1454 warn!(
1455 upstream_id = %self.id,
1456 target = %target,
1457 circuit_breaker_opened = breaker_opened,
1458 "Connection failure reported for target"
1459 );
1460 }
1461 }
1462
1463 pub async fn report_result_with_latency(
1471 &self,
1472 target: &str,
1473 success: bool,
1474 latency: Option<Duration>,
1475 ) {
1476 trace!(
1477 upstream_id = %self.id,
1478 target = %target,
1479 success = success,
1480 latency_ms = latency.map(|l| l.as_millis() as u64),
1481 "Reporting result with latency for adaptive LB"
1482 );
1483
1484 if success {
1486 if let Some(breaker) = self.circuit_breakers.read().await.get(target) {
1487 breaker.record_success();
1488 }
1489 self.load_balancer
1491 .report_result_with_latency(target, true, latency)
1492 .await;
1493 } else {
1494 if let Some(breaker) = self.circuit_breakers.read().await.get(target) {
1500 breaker.record_failure();
1501 }
1502 self.stats.failures.fetch_add(1, Ordering::Relaxed);
1503 }
1504 }
1505
1506 pub fn stats(&self) -> &PoolStats {
1508 &self.stats
1509 }
1510
1511 pub fn id(&self) -> &UpstreamId {
1513 &self.id
1514 }
1515
1516 pub fn target_count(&self) -> usize {
1518 self.targets.len()
1519 }
1520
1521 pub fn pool_config(&self) -> PoolConfigSnapshot {
1523 PoolConfigSnapshot {
1524 max_connections: self.pool_config.max_connections,
1525 max_idle: self.pool_config.max_idle,
1526 idle_timeout_secs: self.pool_config.idle_timeout.as_secs(),
1527 max_lifetime_secs: self.pool_config.max_lifetime.map(|d| d.as_secs()),
1528 connection_timeout_secs: self.pool_config.connection_timeout.as_secs(),
1529 read_timeout_secs: self.pool_config.read_timeout.as_secs(),
1530 write_timeout_secs: self.pool_config.write_timeout.as_secs(),
1531 }
1532 }
1533
1534 pub async fn has_healthy_targets(&self) -> bool {
1538 let healthy = self.load_balancer.healthy_targets().await;
1539 !healthy.is_empty()
1540 }
1541
1542 pub async fn select_shadow_target(
1547 &self,
1548 context: Option<&RequestContext>,
1549 ) -> ZentinelResult<ShadowTarget> {
1550 let selection = self.load_balancer.select(context).await?;
1552
1553 let breakers = self.circuit_breakers.read().await;
1555 if let Some(breaker) = breakers.get(&selection.address) {
1556 if !breaker.is_closed() {
1557 return Err(ZentinelError::upstream(
1558 self.id.to_string(),
1559 "Circuit breaker is open for shadow target",
1560 ));
1561 }
1562 }
1563
1564 let (host, port) = if selection.address.contains(':') {
1566 let parts: Vec<&str> = selection.address.rsplitn(2, ':').collect();
1567 if parts.len() == 2 {
1568 (
1569 parts[1].to_string(),
1570 parts[0]
1571 .parse::<u16>()
1572 .unwrap_or(if self.tls_enabled { 443 } else { 80 }),
1573 )
1574 } else {
1575 (
1576 selection.address.clone(),
1577 if self.tls_enabled { 443 } else { 80 },
1578 )
1579 }
1580 } else {
1581 (
1582 selection.address.clone(),
1583 if self.tls_enabled { 443 } else { 80 },
1584 )
1585 };
1586
1587 Ok(ShadowTarget {
1588 scheme: if self.tls_enabled { "https" } else { "http" }.to_string(),
1589 host,
1590 port,
1591 sni: self.tls_sni.clone(),
1592 })
1593 }
1594
1595 pub fn is_tls_enabled(&self) -> bool {
1597 self.tls_enabled
1598 }
1599
1600 pub fn active_request_count(&self) -> u64 {
1605 self.stats.active_requests.load(Ordering::Relaxed)
1606 }
1607
1608 pub fn increment_active(&self) {
1611 self.stats.active_requests.fetch_add(1, Ordering::Relaxed);
1612 }
1613
1614 pub fn decrement_active(&self) {
1617 let prev = self.stats.active_requests.fetch_sub(1, Ordering::Relaxed);
1618 if prev == 0 {
1619 self.stats.active_requests.fetch_add(1, Ordering::Relaxed);
1620 warn!("Attempted to decrement active request count below zero");
1621 }
1622 }
1623
1624 pub async fn shutdown(&self) {
1628 info!(
1629 upstream_id = %self.id,
1630 target_count = self.targets.len(),
1631 total_requests = self.stats.requests.load(Ordering::Relaxed),
1632 total_successes = self.stats.successes.load(Ordering::Relaxed),
1633 total_failures = self.stats.failures.load(Ordering::Relaxed),
1634 "Shutting down upstream pool"
1635 );
1636 debug!(upstream_id = %self.id, "Upstream pool shutdown complete");
1638 }
1639}