1use std::hash::{BuildHasher, RandomState};
2use std::time::{Duration, Instant};
3
4use ntex_rt::Id;
5use ntex_util::HashMap;
6
7use crate::Worker;
8
9#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)]
11#[non_exhaustive]
12pub enum LoadBalancing {
13 RoundRobin,
18 LeastLoaded,
25 #[default]
42 IncomingCpu,
43}
44
45const REFRESH: Duration = Duration::from_millis(10);
47const TAU: Duration = Duration::from_millis(250);
49const BLOCKED: Duration = Duration::from_millis(100);
51const UTIL_SLACK: f64 = 0.1;
53const LOCAL_UTIL_SLACK: f64 = 0.2;
56const LOCAL_LOAD_SLACK: usize = 16;
59
60#[derive(Copy, Clone, Debug, Default)]
61pub(crate) struct Sample {
63 busy: Option<Duration>,
65 blocked: bool,
67}
68
69impl Sample {
70 fn of<T>(wrk: &Worker<T>, now: Instant) -> Sample {
71 let arb = wrk.arbiter();
72 let hnd = arb.handle();
73 let blocked = hnd.current_turn().is_some_and(|t| t >= BLOCKED)
74 || arb.latency().is_some_and(|l| {
75 l.stalled
76 || l.last_sample
77 .is_some_and(|t| now.saturating_duration_since(t) >= l.interval + BLOCKED)
78 });
79 Sample {
80 busy: hnd.busy_duration(),
81 blocked,
82 }
83 }
84}
85
86#[derive(Debug, Default)]
87struct State {
88 busy: Option<Duration>,
89 at: Option<Instant>,
90 util: f64,
91 blocked: bool,
92}
93
94impl State {
95 fn update(&mut self, sample: Sample, now: Instant) {
96 if let (Some(busy), Some(prev), Some(at)) = (sample.busy, self.busy, self.at) {
97 let dt = now.saturating_duration_since(at);
98 if !dt.is_zero() {
99 let used = busy.saturating_sub(prev).as_secs_f64() / dt.as_secs_f64();
100 let alpha = dt.as_secs_f64() / (TAU + dt).as_secs_f64();
101 self.util += alpha * (used.min(1.0) - self.util);
102 }
103 }
104 self.busy = sample.busy;
105 self.at = Some(now);
106 self.blocked = sample.blocked;
107 }
108}
109
110#[derive(Debug)]
111pub(crate) struct Balancer {
115 mode: LoadBalancing,
116 next: usize,
117 states: Vec<State>,
118 ready: Vec<usize>,
120 refreshed: Option<Instant>,
121 rng: u64,
122 local: HashMap<usize, Vec<usize>>,
125 local_stale: bool,
127 parked: HashMap<String, (Id, State)>,
129}
130
131impl Balancer {
132 pub(crate) fn new(mode: LoadBalancing) -> Self {
133 Self {
134 mode,
135 next: 0,
136 states: Vec::new(),
137 ready: Vec::new(),
138 refreshed: None,
139 rng: RandomState::new().hash_one(0u64) | 1,
140 local: HashMap::default(),
141 local_stale: true,
142 parked: HashMap::default(),
143 }
144 }
145
146 #[cfg(test)]
147 pub(crate) fn insert(&mut self, idx: usize) {
149 self.insert_state(idx, State::default());
150 }
151
152 pub(crate) fn unpark(&mut self, idx: usize, name: &str, arbiter: Id) {
157 let state = match self.parked.remove(name) {
158 Some((id, state)) if id == arbiter => state,
159 _ => State::default(),
160 };
161 self.insert_state(idx, state);
162 }
163
164 fn insert_state(&mut self, idx: usize, state: State) {
165 self.states.insert(idx, state);
166 self.refreshed = None;
167 self.local_stale = true;
168 }
169
170 pub(crate) fn reset(&mut self, idx: usize) {
172 self.states[idx] = State::default();
173 self.refreshed = None;
174 self.local_stale = true;
175 }
176
177 pub(crate) fn remove(&mut self, idx: usize) {
179 self.states.remove(idx);
180 self.refreshed = None;
181 self.local_stale = true;
182 }
183
184 pub(crate) fn park(&mut self, idx: usize, name: &str, arbiter: Id) {
187 let state = self.states.remove(idx);
188 self.parked.insert(name.to_owned(), (arbiter, state));
189 self.refreshed = None;
190 self.local_stale = true;
191 }
192
193 pub(crate) fn uses_incoming_cpu<T>(&mut self, workers: &[Worker<T>]) -> bool {
197 if self.mode != LoadBalancing::IncomingCpu {
198 return false;
199 }
200 if self.local_stale {
201 self.local_stale = false;
202 self.map_cpus(workers.iter().map(Worker::cpus));
203 }
204 !self.local.is_empty()
205 }
206
207 pub(crate) fn select<T>(
212 &mut self,
213 workers: &[Worker<T>],
214 cpu: Option<usize>,
215 ) -> (usize, Option<bool>) {
216 match self.mode {
217 LoadBalancing::RoundRobin => (self.round_robin(workers.len()), None),
218 LoadBalancing::LeastLoaded | LoadBalancing::IncomingCpu => {
219 let now = Instant::now();
220 self.refresh(now, |idx| Sample::of(&workers[idx], now));
221 let load = |idx: usize| workers[idx].load();
222 let idx = self.least_loaded(load);
223 match cpu {
224 Some(cpu) if self.uses_incoming_cpu(workers) => {
225 let local = self.local_worker(cpu, idx, load);
226 (local.unwrap_or(idx), Some(local.is_some()))
227 }
228 _ => (idx, None),
229 }
230 }
231 }
232 }
233
234 fn round_robin(&mut self, len: usize) -> usize {
236 if self.next >= len {
237 self.next = len - 1;
238 }
239 (0..len)
241 .map(|i| (self.next + i) % len)
242 .find(|&i| !self.is_blocked(i))
243 .unwrap_or(self.next)
244 }
245
246 pub(crate) fn sample<T>(&mut self, workers: &[Worker<T>]) -> bool {
250 let now = Instant::now();
251 self.refresh(now, |idx| Sample::of(&workers[idx], now));
252 !self.ready.is_empty() && self.ready.len() < self.states.len()
253 }
254
255 pub(crate) fn is_blocked(&self, idx: usize) -> bool {
257 self.states.get(idx).is_some_and(|st| st.blocked)
258 }
259
260 pub(crate) fn sent(&mut self, idx: usize, len: usize) {
262 self.next = (idx + 1) % len;
263 }
264
265 fn refresh(&mut self, now: Instant, sample: impl Fn(usize) -> Sample) {
266 if self
267 .refreshed
268 .is_some_and(|t| now.saturating_duration_since(t) < REFRESH)
269 {
270 return;
271 }
272 self.refreshed = Some(now);
273 self.ready.clear();
274 for (idx, st) in self.states.iter_mut().enumerate() {
275 st.update(sample(idx), now);
276 if !st.blocked {
277 self.ready.push(idx);
278 }
279 }
280 }
281
282 fn least_loaded(&mut self, load: impl Fn(usize) -> usize) -> usize {
284 let all = self.ready.is_empty();
285 let len = if all { self.states.len() } else { self.ready.len() };
286 if len < 2 {
287 return if all { 0 } else { self.ready[0] };
288 }
289
290 let a = self.random(len);
291 let mut b = self.random(len - 1);
292 if b >= a {
293 b += 1;
294 }
295 let (a, b) = if all {
296 (a, b)
297 } else {
298 (self.ready[a], self.ready[b])
299 };
300
301 let (ua, ub) = (self.states[a].util, self.states[b].util);
302 if (ua - ub).abs() > UTIL_SLACK {
303 if ua < ub { a } else { b }
304 } else if load(b) < load(a) {
305 b
306 } else {
307 a
308 }
309 }
310
311 fn map_cpus<'a>(&mut self, cpus: impl Iterator<Item = &'a [usize]>) {
314 let cpus: Vec<_> = cpus.collect();
315 self.local.clear();
316 for (idx, cpus) in cpus.iter().enumerate() {
317 if let Some(&cpu) = cpus.first() {
318 self.local.entry(cpu).or_default().push(idx);
319 }
320 }
321 let mut siblings = HashMap::<usize, Vec<usize>>::default();
322 for (idx, cpus) in cpus.iter().enumerate() {
323 for cpu in cpus.iter().skip(1) {
324 if !self.local.contains_key(cpu) {
325 siblings.entry(*cpu).or_default().push(idx);
326 }
327 }
328 }
329 self.local.extend(siblings);
330 }
331
332 fn local_worker(
335 &self,
336 cpu: usize,
337 other: usize,
338 load: impl Fn(usize) -> usize,
339 ) -> Option<usize> {
340 let util = |idx: usize| self.states[idx].util;
341 let best = self
342 .local
343 .get(&cpu)?
344 .iter()
345 .copied()
346 .filter(|&idx| !self.states[idx].blocked)
347 .min_by(|&a, &b| {
348 util(a)
349 .total_cmp(&util(b))
350 .then_with(|| load(a).cmp(&load(b)))
351 })?;
352 let max_load = load(other) + load(other) / 2 + LOCAL_LOAD_SLACK;
353 (best == other || (util(best) <= util(other) + LOCAL_UTIL_SLACK && load(best) <= max_load))
354 .then_some(best)
355 }
356
357 #[allow(clippy::cast_possible_truncation)]
358 fn random(&mut self, n: usize) -> usize {
360 let mut x = self.rng;
362 x ^= x << 13;
363 x ^= x >> 7;
364 x ^= x << 17;
365 self.rng = x;
366 (x % n as u64) as usize
367 }
368}
369
370#[cfg(test)]
371mod tests {
372 use super::*;
373
374 const MS: Duration = Duration::from_millis(1);
375
376 fn balancer(n: usize) -> Balancer {
377 let mut b = Balancer::new(LoadBalancing::LeastLoaded);
378 for i in 0..n {
379 b.insert(i);
380 }
381 b
382 }
383
384 fn sample(busy_ms: u32, blocked: bool) -> Sample {
385 Sample {
386 busy: Some(MS * busy_ms),
387 blocked,
388 }
389 }
390
391 #[test]
392 fn utilization_average() {
393 let t = Instant::now();
394 let mut st = State::default();
395 st.update(sample(0, false), t);
396 assert!(st.util.abs() < f64::EPSILON);
397
398 st.update(sample(250, false), t + MS * 250);
400 assert!((st.util - 0.5).abs() < 1e-9, "{}", st.util);
401
402 st.update(sample(250, false), t + MS * 500);
404 assert!((st.util - 0.25).abs() < 1e-9, "{}", st.util);
405
406 st.update(Sample::default(), t + MS * 600);
408 assert!((st.util - 0.25).abs() < 1e-9, "{}", st.util);
409 st.update(sample(0, false), t + MS * 700);
410 st.update(sample(0, false), t + MS * 700);
411 assert!((st.util - 0.25).abs() < 1e-9, "{}", st.util);
412 }
413
414 #[test]
415 fn refresh_interval() {
416 let t = Instant::now();
417 let mut b = balancer(2);
418 b.refresh(t, |_| sample(0, false));
419 assert_eq!(b.ready, [0, 1]);
420
421 b.refresh(t + MS, |_| sample(0, true));
423 assert_eq!(b.ready, [0, 1]);
424 b.refresh(t + REFRESH, |i| sample(0, i == 0));
425 assert_eq!(b.ready, [1]);
426
427 b.insert(2);
429 b.refresh(t + REFRESH, |_| sample(0, false));
430 assert_eq!(b.ready, [0, 1, 2]);
431 }
432
433 #[test]
434 fn skips_blocked_workers() {
435 let t = Instant::now();
436 let mut b = balancer(3);
437 b.refresh(t, |i| sample(0, i != 2));
438 for _ in 0..100 {
439 assert_eq!(b.least_loaded(|_| 0), 2);
440 }
441
442 b.refresh(t + REFRESH, |_| sample(0, true));
444 let mut seen = [false; 3];
445 for _ in 0..100 {
446 seen[b.least_loaded(|_| 0)] = true;
447 }
448 assert_eq!(seen, [true; 3]);
449 }
450
451 #[test]
452 fn round_robin_skips_blocked_workers() {
453 let t = Instant::now();
454 let mut b = balancer(3);
455 b.refresh(t, |i| sample(0, i == 1));
456 let mut seen = Vec::new();
457 for _ in 0..4 {
458 let idx = b.round_robin(3);
459 b.sent(idx, 3);
460 seen.push(idx);
461 }
462 assert_eq!(seen, [0, 2, 0, 2]);
463
464 b.refresh(t + REFRESH, |_| sample(0, true));
466 assert_eq!(b.round_robin(3), 0);
467 }
468
469 #[test]
470 fn prefers_less_utilized_workers() {
471 let t = Instant::now();
472 let mut b = balancer(2);
473 b.refresh(t, |_| sample(0, false));
474 b.refresh(t + MS * 100, |i| {
475 sample(if i == 0 { 100 } else { 10 }, false)
476 });
477 assert!(b.states[0].util > b.states[1].util + UTIL_SLACK);
478 for _ in 0..100 {
479 assert_eq!(b.least_loaded(|i| if i == 0 { 0 } else { 100 }), 1);
481 }
482 }
483
484 #[test]
485 fn compares_load_of_similar_workers() {
486 let t = Instant::now();
487 let mut b = balancer(4);
488 b.refresh(t, |_| sample(0, false));
489 b.refresh(t + MS * 100, |i| sample([10, 11, 12, 13][i], false));
490 let mut seen = [0; 4];
491 for _ in 0..1000 {
492 seen[b.least_loaded(|i| [5, 1, 2, 3][i])] += 1;
493 }
494 assert_eq!(seen[0], 0);
496 assert!(
497 seen[1] > seen[2] && seen[2] > seen[3] && seen[3] > 0,
498 "{seen:?}"
499 );
500 }
501
502 #[test]
503 fn single_worker() {
504 let mut b = balancer(1);
505 b.refresh(Instant::now(), |_| sample(0, true));
506 assert_eq!(b.least_loaded(|_| 0), 0);
507 }
508
509 #[ntex::test]
510 async fn keeps_state_of_unavailable_workers() {
511 let (a1, a2) = (ntex_rt::Arbiter::new(), ntex_rt::Arbiter::new());
512 let t = Instant::now();
513 let mut b = balancer(2);
514 b.refresh(t, |_| sample(0, false));
515 b.refresh(t + MS * 100, |i| {
516 sample(if i == 0 { 100 } else { 10 }, false)
517 });
518 let util = b.states[0].util;
519 assert!(util > b.states[1].util + UTIL_SLACK);
520
521 b.park(0, "w0", a1.id());
523 b.unpark(0, "w0", a1.id());
524 assert!((b.states[0].util - util).abs() < f64::EPSILON);
525 for _ in 0..100 {
526 assert_eq!(b.least_loaded(|i| [1, 100][i]), 1);
527 }
528
529 b.park(0, "w0", a1.id());
531 b.unpark(0, "w0", a2.id());
532 assert!(b.states[0].util.abs() < f64::EPSILON);
533 assert!(b.parked.is_empty());
534 a1.stop();
535 a2.stop();
536 }
537
538 #[test]
539 fn map_cpus() {
540 let mut b = balancer(5);
541 b.map_cpus([&[0, 1][..], &[1, 0], &[2, 3], &[4, 5], &[]].into_iter());
544 let mut local: Vec<_> = b.local.iter().map(|(c, w)| (*c, w.clone())).collect();
545 local.sort();
546 assert_eq!(
547 local,
548 [
549 (0, vec![0]),
550 (1, vec![1]),
551 (2, vec![2]),
552 (3, vec![2]),
553 (4, vec![3]),
554 (5, vec![3])
555 ]
556 );
557
558 b.map_cpus([&[0][..], &[0]].into_iter());
560 assert_eq!(b.local.get(&0), Some(&vec![0, 1]));
561 assert_eq!(b.local.len(), 1);
562 }
563
564 #[test]
565 fn local_worker() {
566 let t = Instant::now();
567 let mut b = balancer(4);
568 b.map_cpus([&[0][..], &[1], &[2], &[2]].into_iter());
569 b.refresh(t, |_| sample(0, false));
570 b.refresh(t + MS * 100, |i| sample([10, 0, 100, 90][i], false));
571 let load = |_| 10;
572
573 assert_eq!(b.local_worker(0, 1, load), Some(0));
575 assert_eq!(b.local_worker(2, 2, load), Some(3));
577 assert_eq!(b.local_worker(7, 1, load), None);
579 assert!(b.states[3].util > b.states[1].util + LOCAL_UTIL_SLACK);
581 assert_eq!(b.local_worker(2, 1, load), None);
582 b.map_cpus([&[0][..], &[1], &[2], &[3]].into_iter());
583 assert_eq!(b.local_worker(2, 1, load), None);
584 assert_eq!(b.local_worker(2, 2, load), Some(2));
585
586 let load = |i: usize| [100, 10, 0, 0][i];
588 assert_eq!(b.local_worker(0, 1, load), None);
589 let load = |i: usize| [100, 56, 0, 0][i];
590 assert_eq!(b.local_worker(0, 1, load), Some(0));
591
592 b.refresh(t + MS * 200, |i| sample([10, 0, 100, 90][i], i == 0));
594 assert_eq!(b.local_worker(0, 1, |_| 0), None);
595 }
596
597 #[test]
598 fn random_range() {
599 let mut b = balancer(0);
600 let mut seen = [false; 5];
601 for _ in 0..1000 {
602 seen[b.random(5)] = true;
603 }
604 assert_eq!(seen, [true; 5]);
605 }
606}