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,
26 #[default]
43 IncomingCpu,
44}
45
46const REFRESH: Duration = Duration::from_millis(10);
48const TAU: Duration = Duration::from_millis(250);
50const BLOCKED: Duration = Duration::from_millis(100);
52const UTIL_SLACK: f64 = 0.1;
54const LOCAL_UTIL_SLACK: f64 = 0.2;
57const LOCAL_LOAD_SLACK: usize = 16;
60
61#[derive(Copy, Clone, Debug, Default)]
62pub(crate) struct Sample {
64 busy: Option<Duration>,
66 turn: Duration,
68 blocked: bool,
70}
71
72impl Sample {
73 fn of<T>(wrk: &Worker<T>, now: Instant) -> Sample {
74 let arb = wrk.arbiter();
75 let hnd = arb.handle();
76 let turn = hnd.current_turn().unwrap_or_default();
77 let blocked = turn >= BLOCKED
78 || arb.latency().is_some_and(|l| {
79 l.stalled
80 || l.last_sample
81 .is_some_and(|t| now.saturating_duration_since(t) >= l.interval + BLOCKED)
82 });
83 Sample {
84 busy: hnd.busy_duration(),
85 turn,
86 blocked,
87 }
88 }
89}
90
91#[derive(Debug, Default)]
92struct State {
93 busy: Option<Duration>,
94 at: Option<Instant>,
95 util: f64,
96 blocked: bool,
97}
98
99impl State {
100 fn update(&mut self, sample: Sample, now: Instant) {
101 let busy = sample.busy.map(|b| b.saturating_sub(sample.turn));
104 let now = now.checked_sub(sample.turn).unwrap_or(now);
105 if !self.blocked
107 && !sample.blocked
108 && let (Some(busy), Some(prev), Some(at)) = (busy, self.busy, self.at)
109 {
110 let dt = now.saturating_duration_since(at);
111 if !dt.is_zero() {
112 let used = busy.saturating_sub(prev).as_secs_f64() / dt.as_secs_f64();
113 let alpha = dt.as_secs_f64() / (TAU + dt).as_secs_f64();
114 self.util += alpha * (used.min(1.0) - self.util);
115 }
116 }
117 self.busy = busy;
118 self.at = Some(now);
119 self.blocked = sample.blocked;
120 }
121}
122
123#[derive(Debug)]
124pub(crate) struct Balancer {
128 mode: LoadBalancing,
129 next: usize,
130 states: Vec<State>,
131 ready: Vec<usize>,
133 refreshed: Option<Instant>,
134 rng: u64,
135 local: HashMap<usize, Vec<usize>>,
138 local_stale: bool,
140 parked: HashMap<String, (Id, State)>,
142}
143
144impl Balancer {
145 pub(crate) fn new(mode: LoadBalancing) -> Self {
146 Self {
147 mode,
148 next: 0,
149 states: Vec::new(),
150 ready: Vec::new(),
151 refreshed: None,
152 rng: RandomState::new().hash_one(0u64) | 1,
153 local: HashMap::default(),
154 local_stale: true,
155 parked: HashMap::default(),
156 }
157 }
158
159 #[cfg(test)]
160 pub(crate) fn insert(&mut self, idx: usize) {
162 self.insert_state(idx, State::default());
163 }
164
165 pub(crate) fn unpark(&mut self, idx: usize, name: &str, arbiter: Id) {
170 let state = match self.parked.remove(name) {
171 Some((id, state)) if id == arbiter => state,
172 _ => State::default(),
173 };
174 self.insert_state(idx, state);
175 }
176
177 fn insert_state(&mut self, idx: usize, state: State) {
178 self.states.insert(idx, state);
179 self.refreshed = None;
180 self.local_stale = true;
181 }
182
183 pub(crate) fn reset(&mut self, idx: usize) {
185 self.states[idx] = State::default();
186 self.refreshed = None;
187 self.local_stale = true;
188 }
189
190 pub(crate) fn remove(&mut self, idx: usize) {
192 self.states.remove(idx);
193 self.refreshed = None;
194 self.local_stale = true;
195 }
196
197 pub(crate) fn park(&mut self, idx: usize, name: &str, arbiter: Id) {
200 let state = self.states.remove(idx);
201 self.parked.insert(name.to_owned(), (arbiter, state));
202 self.refreshed = None;
203 self.local_stale = true;
204 }
205
206 pub(crate) fn uses_incoming_cpu<T>(&mut self, workers: &[Worker<T>]) -> bool {
210 if self.mode != LoadBalancing::IncomingCpu {
211 return false;
212 }
213 if self.local_stale {
214 self.local_stale = false;
215 self.map_cpus(workers.iter().map(Worker::cpus));
216 }
217 !self.local.is_empty()
218 }
219
220 pub(crate) fn select<T>(
225 &mut self,
226 workers: &[Worker<T>],
227 cpu: Option<usize>,
228 ) -> (usize, Option<bool>) {
229 match self.mode {
230 LoadBalancing::RoundRobin => (self.round_robin(workers.len()), None),
231 LoadBalancing::LeastLoaded | LoadBalancing::IncomingCpu => {
232 let now = Instant::now();
233 self.refresh(now, |idx| Sample::of(&workers[idx], now));
234 let load = |idx: usize| workers[idx].load();
235 let idx = self.least_loaded(load);
236 match cpu {
237 Some(cpu) if self.uses_incoming_cpu(workers) => {
238 let local = self.local_worker(cpu, idx, load);
239 (local.unwrap_or(idx), Some(local.is_some()))
240 }
241 _ => (idx, None),
242 }
243 }
244 }
245 }
246
247 fn round_robin(&mut self, len: usize) -> usize {
249 if self.next >= len {
250 self.next = len - 1;
251 }
252 (0..len)
254 .map(|i| (self.next + i) % len)
255 .find(|&i| !self.is_blocked(i))
256 .unwrap_or(self.next)
257 }
258
259 pub(crate) fn sample<T>(&mut self, workers: &[Worker<T>]) -> bool {
263 let now = Instant::now();
264 self.refresh(now, |idx| Sample::of(&workers[idx], now));
265 !self.ready.is_empty() && self.ready.len() < self.states.len()
266 }
267
268 pub(crate) fn is_blocked(&self, idx: usize) -> bool {
270 self.states.get(idx).is_some_and(|st| st.blocked)
271 }
272
273 pub(crate) fn sent(&mut self, idx: usize, len: usize) {
275 self.next = (idx + 1) % len;
276 }
277
278 fn refresh(&mut self, now: Instant, sample: impl Fn(usize) -> Sample) {
279 if self
280 .refreshed
281 .is_some_and(|t| now.saturating_duration_since(t) < REFRESH)
282 {
283 return;
284 }
285 self.refreshed = Some(now);
286 self.ready.clear();
287 for (idx, st) in self.states.iter_mut().enumerate() {
288 st.update(sample(idx), now);
289 if !st.blocked {
290 self.ready.push(idx);
291 }
292 }
293 }
294
295 fn least_loaded(&mut self, load: impl Fn(usize) -> usize) -> usize {
297 let all = self.ready.is_empty();
298 let len = if all { self.states.len() } else { self.ready.len() };
299 if len < 2 {
300 return if all { 0 } else { self.ready[0] };
301 }
302
303 let a = self.random(len);
304 let mut b = self.random(len - 1);
305 if b >= a {
306 b += 1;
307 }
308 let (a, b) = if all {
309 (a, b)
310 } else {
311 (self.ready[a], self.ready[b])
312 };
313
314 let (ua, ub) = (self.states[a].util, self.states[b].util);
315 if (ua - ub).abs() > UTIL_SLACK {
316 if ua < ub { a } else { b }
317 } else if load(b) < load(a) {
318 b
319 } else {
320 a
321 }
322 }
323
324 fn map_cpus<'a>(&mut self, cpus: impl Iterator<Item = &'a [usize]>) {
327 let cpus: Vec<_> = cpus.collect();
328 self.local.clear();
329 for (idx, cpus) in cpus.iter().enumerate() {
330 if let Some(&cpu) = cpus.first() {
331 self.local.entry(cpu).or_default().push(idx);
332 }
333 }
334 let mut siblings = HashMap::<usize, Vec<usize>>::default();
335 for (idx, cpus) in cpus.iter().enumerate() {
336 for cpu in cpus.iter().skip(1) {
337 if !self.local.contains_key(cpu) {
338 siblings.entry(*cpu).or_default().push(idx);
339 }
340 }
341 }
342 self.local.extend(siblings);
343 }
344
345 fn local_worker(
348 &self,
349 cpu: usize,
350 other: usize,
351 load: impl Fn(usize) -> usize,
352 ) -> Option<usize> {
353 let util = |idx: usize| self.states[idx].util;
354 let best = self
355 .local
356 .get(&cpu)?
357 .iter()
358 .copied()
359 .filter(|&idx| !self.states[idx].blocked)
360 .min_by(|&a, &b| {
361 util(a)
362 .total_cmp(&util(b))
363 .then_with(|| load(a).cmp(&load(b)))
364 })?;
365 let max_load = load(other) + load(other) / 2 + LOCAL_LOAD_SLACK;
366 (best == other || (util(best) <= util(other) + LOCAL_UTIL_SLACK && load(best) <= max_load))
367 .then_some(best)
368 }
369
370 #[allow(clippy::cast_possible_truncation)]
371 fn random(&mut self, n: usize) -> usize {
373 let mut x = self.rng;
375 x ^= x << 13;
376 x ^= x >> 7;
377 x ^= x << 17;
378 self.rng = x;
379 (x % n as u64) as usize
380 }
381}
382
383#[cfg(test)]
384mod tests {
385 use super::*;
386
387 const MS: Duration = Duration::from_millis(1);
388
389 fn balancer(n: usize) -> Balancer {
390 let mut b = Balancer::new(LoadBalancing::LeastLoaded);
391 for i in 0..n {
392 b.insert(i);
393 }
394 b
395 }
396
397 fn sample(busy_ms: u32, blocked: bool) -> Sample {
398 turn(busy_ms, 0, blocked)
399 }
400
401 fn turn(busy_ms: u32, turn_ms: u32, blocked: bool) -> Sample {
403 Sample {
404 busy: Some(MS * busy_ms),
405 turn: MS * turn_ms,
406 blocked,
407 }
408 }
409
410 #[test]
411 fn utilization_average() {
412 let t = Instant::now();
413 let mut st = State::default();
414 st.update(sample(0, false), t);
415 assert!(st.util.abs() < f64::EPSILON);
416
417 st.update(sample(250, false), t + MS * 250);
419 assert!((st.util - 0.5).abs() < 1e-9, "{}", st.util);
420
421 st.update(sample(250, false), t + MS * 500);
423 assert!((st.util - 0.25).abs() < 1e-9, "{}", st.util);
424
425 st.update(Sample::default(), t + MS * 600);
427 assert!((st.util - 0.25).abs() < 1e-9, "{}", st.util);
428 st.update(sample(0, false), t + MS * 700);
429 st.update(sample(0, false), t + MS * 700);
430 assert!((st.util - 0.25).abs() < 1e-9, "{}", st.util);
431 }
432
433 #[test]
444 fn blocked_time_is_not_utilization() {
445 let t = Instant::now();
446 let mut st = State::default();
447 st.update(sample(0, false), t);
448 st.update(sample(250, false), t + MS * 250);
449 assert!((st.util - 0.5).abs() < 1e-9, "{}", st.util);
450
451 st.update(turn(300, 50, false), t + MS * 300);
452 assert!((st.util - 0.5).abs() < 1e-9, "{}", st.util);
453 st.update(turn(400, 150, true), t + MS * 400);
454 assert!(st.blocked);
455 assert!((st.util - 0.5).abs() < 1e-9, "{}", st.util);
456 st.update(sample(550, false), t + MS * 560);
457 assert!(!st.blocked);
458 assert!((st.util - 0.5).abs() < 1e-9, "{}", st.util);
459
460 st.update(sample(550, false), t + MS * 810);
462 assert!((st.util - 0.25).abs() < 1e-9, "{}", st.util);
463
464 st.update(turn(570, 20, false), t + MS * 830);
466 assert!((st.util - 0.25).abs() < 1e-9, "{}", st.util);
467 st.update(sample(600, false), t + MS * 860);
468 let alpha = 50.0 / 300.0;
469 assert!(
470 (st.util - (0.25 + alpha * 0.75)).abs() < 1e-9,
471 "{}",
472 st.util
473 );
474 }
475
476 #[test]
477 fn refresh_interval() {
478 let t = Instant::now();
479 let mut b = balancer(2);
480 b.refresh(t, |_| sample(0, false));
481 assert_eq!(b.ready, [0, 1]);
482
483 b.refresh(t + MS, |_| sample(0, true));
485 assert_eq!(b.ready, [0, 1]);
486 b.refresh(t + REFRESH, |i| sample(0, i == 0));
487 assert_eq!(b.ready, [1]);
488
489 b.insert(2);
491 b.refresh(t + REFRESH, |_| sample(0, false));
492 assert_eq!(b.ready, [0, 1, 2]);
493 }
494
495 #[test]
496 fn skips_blocked_workers() {
497 let t = Instant::now();
498 let mut b = balancer(3);
499 b.refresh(t, |i| sample(0, i != 2));
500 for _ in 0..100 {
501 assert_eq!(b.least_loaded(|_| 0), 2);
502 }
503
504 b.refresh(t + REFRESH, |_| sample(0, true));
506 let mut seen = [false; 3];
507 for _ in 0..100 {
508 seen[b.least_loaded(|_| 0)] = true;
509 }
510 assert_eq!(seen, [true; 3]);
511 }
512
513 #[test]
514 fn round_robin_skips_blocked_workers() {
515 let t = Instant::now();
516 let mut b = balancer(3);
517 b.refresh(t, |i| sample(0, i == 1));
518 let mut seen = Vec::new();
519 for _ in 0..4 {
520 let idx = b.round_robin(3);
521 b.sent(idx, 3);
522 seen.push(idx);
523 }
524 assert_eq!(seen, [0, 2, 0, 2]);
525
526 b.refresh(t + REFRESH, |_| sample(0, true));
528 assert_eq!(b.round_robin(3), 0);
529 }
530
531 #[test]
532 fn prefers_less_utilized_workers() {
533 let t = Instant::now();
534 let mut b = balancer(2);
535 b.refresh(t, |_| sample(0, false));
536 b.refresh(t + MS * 100, |i| {
537 sample(if i == 0 { 100 } else { 10 }, false)
538 });
539 assert!(b.states[0].util > b.states[1].util + UTIL_SLACK);
540 for _ in 0..100 {
541 assert_eq!(b.least_loaded(|i| if i == 0 { 0 } else { 100 }), 1);
543 }
544 }
545
546 #[test]
547 fn compares_load_of_similar_workers() {
548 let t = Instant::now();
549 let mut b = balancer(4);
550 b.refresh(t, |_| sample(0, false));
551 b.refresh(t + MS * 100, |i| sample([10, 11, 12, 13][i], false));
552 let mut seen = [0; 4];
553 for _ in 0..1000 {
554 seen[b.least_loaded(|i| [5, 1, 2, 3][i])] += 1;
555 }
556 assert_eq!(seen[0], 0);
558 assert!(
559 seen[1] > seen[2] && seen[2] > seen[3] && seen[3] > 0,
560 "{seen:?}"
561 );
562 }
563
564 #[test]
565 fn single_worker() {
566 let mut b = balancer(1);
567 b.refresh(Instant::now(), |_| sample(0, true));
568 assert_eq!(b.least_loaded(|_| 0), 0);
569 }
570
571 #[ntex::test]
572 async fn keeps_state_of_unavailable_workers() {
573 let (a1, a2) = (ntex_rt::Arbiter::new(), ntex_rt::Arbiter::new());
574 let t = Instant::now();
575 let mut b = balancer(2);
576 b.refresh(t, |_| sample(0, false));
577 b.refresh(t + MS * 100, |i| {
578 sample(if i == 0 { 100 } else { 10 }, false)
579 });
580 let util = b.states[0].util;
581 assert!(util > b.states[1].util + UTIL_SLACK);
582
583 b.park(0, "w0", a1.id());
585 b.unpark(0, "w0", a1.id());
586 assert!((b.states[0].util - util).abs() < f64::EPSILON);
587 for _ in 0..100 {
588 assert_eq!(b.least_loaded(|i| [1, 100][i]), 1);
589 }
590
591 b.park(0, "w0", a1.id());
593 b.unpark(0, "w0", a2.id());
594 assert!(b.states[0].util.abs() < f64::EPSILON);
595 assert!(b.parked.is_empty());
596 a1.stop();
597 a2.stop();
598 }
599
600 #[test]
601 fn map_cpus() {
602 let mut b = balancer(5);
603 b.map_cpus([&[0, 1][..], &[1, 0], &[2, 3], &[4, 5], &[]].into_iter());
606 let mut local: Vec<_> = b.local.iter().map(|(c, w)| (*c, w.clone())).collect();
607 local.sort();
608 assert_eq!(
609 local,
610 [
611 (0, vec![0]),
612 (1, vec![1]),
613 (2, vec![2]),
614 (3, vec![2]),
615 (4, vec![3]),
616 (5, vec![3])
617 ]
618 );
619
620 b.map_cpus([&[0][..], &[0]].into_iter());
622 assert_eq!(b.local.get(&0), Some(&vec![0, 1]));
623 assert_eq!(b.local.len(), 1);
624 }
625
626 #[test]
627 fn local_worker() {
628 let t = Instant::now();
629 let mut b = balancer(4);
630 b.map_cpus([&[0][..], &[1], &[2], &[2]].into_iter());
631 b.refresh(t, |_| sample(0, false));
632 b.refresh(t + MS * 100, |i| sample([10, 0, 100, 90][i], false));
633 let load = |_| 10;
634
635 assert_eq!(b.local_worker(0, 1, load), Some(0));
637 assert_eq!(b.local_worker(2, 2, load), Some(3));
639 assert_eq!(b.local_worker(7, 1, load), None);
641 assert!(b.states[3].util > b.states[1].util + LOCAL_UTIL_SLACK);
643 assert_eq!(b.local_worker(2, 1, load), None);
644 b.map_cpus([&[0][..], &[1], &[2], &[3]].into_iter());
645 assert_eq!(b.local_worker(2, 1, load), None);
646 assert_eq!(b.local_worker(2, 2, load), Some(2));
647
648 let load = |i: usize| [100, 10, 0, 0][i];
650 assert_eq!(b.local_worker(0, 1, load), None);
651 let load = |i: usize| [100, 56, 0, 0][i];
652 assert_eq!(b.local_worker(0, 1, load), Some(0));
653
654 b.refresh(t + MS * 200, |i| sample([10, 0, 100, 90][i], i == 0));
656 assert_eq!(b.local_worker(0, 1, |_| 0), None);
657 }
658
659 #[test]
660 fn random_range() {
661 let mut b = balancer(0);
662 let mut seen = [false; 5];
663 for _ in 0..1000 {
664 seen[b.random(5)] = true;
665 }
666 assert_eq!(seen, [true; 5]);
667 }
668}