satex_load_balancer/
lib.rs1mod background;
7mod load_balancer;
8
9pub mod discovery;
10pub mod health_check;
11pub mod resolver;
12pub mod selector;
13
14pub use load_balancer::LoadBalancer;
15
16use crate::discovery::Discovery;
17use crate::health_check::HealthCheck;
18use crate::health_check::health::Health;
19use arc_swap::ArcSwap;
20use derivative::Derivative;
21use http::Extensions;
22use satex_core::Error;
23use std::collections::{BTreeSet, HashMap};
24use std::hash::{DefaultHasher, Hash, Hasher};
25use std::net::{AddrParseError, SocketAddr};
26use std::str::FromStr;
27use std::sync::Arc;
28use tokio::spawn;
29use tracing::{info, warn};
30
31#[derive(Derivative)]
33#[derivative(Clone, Hash, PartialEq, PartialOrd, Eq, Ord, Debug)]
34pub struct Backend {
35 pub addr: SocketAddr,
39
40 pub weight: usize,
44
45 #[derivative(PartialEq = "ignore")]
49 #[derivative(PartialOrd = "ignore")]
50 #[derivative(Hash = "ignore")]
51 #[derivative(Ord = "ignore")]
52 pub extension: Extensions,
53}
54
55impl Backend {
56 pub fn new(addr: impl Into<SocketAddr>) -> Self {
58 Self::new_with_weight(addr.into(), 1)
59 }
60
61 pub fn new_with_weight(addr: impl Into<SocketAddr>, weight: usize) -> Self {
63 Self {
64 addr: addr.into(),
65 weight,
66 extension: Extensions::new(),
67 }
68 }
69
70 pub(crate) fn key(&self) -> u64 {
72 let mut hasher = DefaultHasher::new();
73 self.hash(&mut hasher);
74 hasher.finish()
75 }
76}
77
78impl FromStr for Backend {
79 type Err = AddrParseError;
80
81 fn from_str(addr: &str) -> Result<Self, Self::Err> {
82 SocketAddr::from_str(addr).map(Backend::new)
83 }
84}
85
86impl<'a> TryFrom<(&'a str, usize)> for Backend {
87 type Error = AddrParseError;
88
89 fn try_from((addr, weight): (&'a str, usize)) -> Result<Self, Self::Error> {
90 let addr = SocketAddr::from_str(addr)?;
91 Ok(Backend::new_with_weight(addr, weight))
92 }
93}
94
95
96pub struct Backends {
97 discovery: Box<dyn Discovery + Send + Sync>,
98 health_check: Option<Arc<dyn HealthCheck + Send + Sync>>,
99 backends: ArcSwap<BTreeSet<Backend>>,
100 health: ArcSwap<HashMap<u64, Health>>,
101}
102
103impl Backends {
104 pub fn new(discovery: impl Discovery + Send + Sync + 'static) -> Self {
105 Self {
106 discovery: Box::new(discovery),
107 health_check: None,
108 backends: Default::default(),
109 health: Default::default(),
110 }
111 }
112
113 pub(crate) fn with_health_check(
114 self,
115 health_check: impl HealthCheck + Send + Sync + 'static,
116 ) -> Self {
117 Self {
118 health_check: Some(Arc::new(health_check)),
119 ..self
120 }
121 }
122
123 fn do_update<F>(
127 &self,
128 new_backends: BTreeSet<Backend>,
129 enablement: HashMap<u64, bool>,
130 callback: F,
131 ) where
132 F: Fn(Arc<BTreeSet<Backend>>),
133 {
134 if (**self.backends.load()) != new_backends {
135 let old_health = self.health.load();
136 let mut new_health = HashMap::with_capacity(new_backends.len());
137 for backend in new_backends.iter() {
138 let key = backend.key();
139 let health = old_health.get(&key).cloned().unwrap_or_default();
141
142 if let Some(enabled) = enablement.get(&key) {
144 health.enable(*enabled);
145 }
146 new_health.insert(key, health);
147 }
148
149 let new_backends = Arc::new(new_backends);
153 callback(new_backends.clone());
154 self.backends.store(new_backends);
155 self.health.store(Arc::new(new_health));
156 } else {
157 for (key, backend_enabled) in enablement.iter() {
159 if let Some(health) = self.health.load().get(key) {
162 health.enable(*backend_enabled);
163 }
164 }
165 }
166 }
167
168 pub fn ready(&self, backend: &Backend) -> bool {
176 self.health
177 .load()
178 .get(&backend.key())
179 .map_or(self.health_check.is_none(), |h| h.ready())
182 }
183
184 pub fn set_enable(&self, backend: &Backend, enabled: bool) {
190 if let Some(health) = self.health.load().get(&backend.key()) {
192 health.enable(enabled)
193 };
194 }
195
196 pub fn items(&self) -> Arc<BTreeSet<Backend>> {
198 self.backends.load_full()
199 }
200
201 pub async fn update<F>(&self, callback: F) -> Result<(), Error>
206 where
207 F: Fn(Arc<BTreeSet<Backend>>),
208 {
209 let (new_backends, enablement) = self.discovery.discover().await?;
210 self.do_update(new_backends, enablement, callback);
211 Ok(())
212 }
213
214 pub async fn run_health_check(&self, parallel: bool) {
218 use crate::health_check::HealthCheck;
219
220 async fn check_and_report(
221 backend: &Backend,
222 check: &Arc<dyn HealthCheck + Send + Sync>,
223 health_table: &HashMap<u64, Health>,
224 ) {
225 let errored = check.check(backend).await.err();
226 if let Some(health) = health_table.get(&backend.key()) {
227 let flipped =
228 health.observe_health(errored.is_none(), check.threshold(errored.is_none()));
229 if flipped {
230 check.status_change(backend, errored.is_none()).await;
231 if let Some(e) = errored {
232 warn!("{backend:?} becomes unhealthy, {e}");
233 } else {
234 info!("{backend:?} becomes healthy");
235 }
236 }
237 }
238 }
239
240 let Some(health_check) = self.health_check.as_ref() else {
241 return;
242 };
243
244 let backends = self.backends.load();
245 if parallel {
246 let health_table = self.health.load_full();
247 let jobs = backends.iter().map(|backend| {
248 let backend = backend.clone();
249 let check = health_check.clone();
250 let ht = health_table.clone();
251 spawn(async move {
252 check_and_report(&backend, &check, &ht).await;
253 })
254 });
255
256 futures::future::join_all(jobs).await;
257 } else {
258 for backend in backends.iter() {
259 check_and_report(backend, health_check, &self.health.load()).await;
260 }
261 }
262 }
263}