Skip to main content

ntex_server/
balance.rs

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/// How the server distributes items between workers.
10#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)]
11#[non_exhaustive]
12pub enum LoadBalancing {
13    /// Items are sent to the workers in turn.
14    ///
15    /// Workers whose runtime is blocked by a task are skipped while another
16    /// worker is available.
17    RoundRobin,
18    /// Each item is sent to the less loaded of two randomly chosen workers.
19    ///
20    /// A worker's load is its runtime utilization, averaged over the last
21    /// ~250 ms and not counting the time the runtime is blocked by a task.
22    /// Workers whose utilization differs by less than 10% are compared
23    /// by their open connections plus queued items. Workers whose runtime
24    /// is blocked by a task are skipped while another worker is available.
25    LeastLoaded,
26    /// Each connection is sent to the worker placed on the CPU that receives
27    /// its network packets, unless that worker is busier than other workers.
28    ///
29    /// The worker on the incoming CPU, or on an SMT sibling of it if no worker
30    /// is placed on that CPU, is used if its utilization exceeds the
31    /// [`LeastLoaded`](Self::LeastLoaded) choice by at most 20% and its load by
32    /// at most half plus 16. Otherwise, and for items without a known
33    /// incoming CPU, it works like [`LeastLoaded`](Self::LeastLoaded).
34    ///
35    /// The incoming CPU is the receive-side scaling (RSS) CPU of the
36    /// connection on Windows, known for connections over network adapters
37    /// with RSS, and the CPU that processed the connection's last packet
38    /// (`SO_INCOMING_CPU`) on Linux and Android. Requires worker affinity, see
39    /// [`Affinity`](crate::Affinity).
40    ///
41    /// This is the default.
42    #[default]
43    IncomingCpu,
44}
45
46/// Minimum interval between worker load samples.
47const REFRESH: Duration = Duration::from_millis(10);
48/// Time constant of the utilization average.
49const TAU: Duration = Duration::from_millis(250);
50/// A runtime turn longer than this marks the worker as blocked.
51const BLOCKED: Duration = Duration::from_millis(100);
52/// Utilization difference below which workers are compared by load.
53const UTIL_SLACK: f64 = 0.1;
54/// Utilization by which the worker on the incoming CPU may exceed the
55/// least loaded choice.
56const LOCAL_UTIL_SLACK: f64 = 0.2;
57/// Load by which the worker on the incoming CPU may exceed the least loaded
58/// choice, on top of half of that choice's load.
59const LOCAL_LOAD_SLACK: usize = 16;
60
61#[derive(Copy, Clone, Debug, Default)]
62/// Worker runtime sample.
63pub(crate) struct Sample {
64    /// Total time the runtime has been busy, including the current turn.
65    busy: Option<Duration>,
66    /// How long the current runtime turn has been running.
67    turn: Duration,
68    /// The runtime is blocked by a task.
69    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        // the busy time is taken at the start of the current turn, a turn
102        // counts once it ends
103        let busy = sample.busy.map(|b| b.saturating_sub(sample.turn));
104        let now = now.checked_sub(sample.turn).unwrap_or(now);
105        // a blocked worker is skipped, the time it is blocked is not load
106        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)]
124/// Selects the worker for the next item.
125///
126/// Keeps a load state per worker, in the order of the manager's worker list.
127pub(crate) struct Balancer {
128    mode: LoadBalancing,
129    next: usize,
130    states: Vec<State>,
131    /// Indexes of the workers that are not blocked.
132    ready: Vec<usize>,
133    refreshed: Option<Instant>,
134    rng: u64,
135    /// Indexes of the workers placed on each CPU, or on an SMT sibling of a
136    /// CPU without its own worker.
137    local: HashMap<usize, Vec<usize>>,
138    /// `local` must be rebuilt.
139    local_stale: bool,
140    /// Load state of unavailable workers by name, with their arbiter.
141    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    /// A worker is inserted at `idx`.
161    pub(crate) fn insert(&mut self, idx: usize) {
162        self.insert_state(idx, State::default());
163    }
164
165    /// A worker that became available again is inserted at `idx`.
166    ///
167    /// Its load state is kept from when it became unavailable, unless the
168    /// worker was restarted on a new arbiter since then.
169    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    /// The worker at `idx` is replaced.
184    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    /// The worker at `idx` is removed.
191    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    /// The worker at `idx` became unavailable and is removed, its load state
198    /// is kept for [`unpark`](Self::unpark).
199    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    /// Returns `true` if [`select`](Self::select) uses the incoming CPU of
207    /// items, i.e. the mode is [`LoadBalancing::IncomingCpu`] and some
208    /// workers are placed on CPUs.
209    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    /// Returns the index of the worker for the next item, and whether it was
221    /// chosen for the incoming CPU of the item if that CPU is set.
222    ///
223    /// `workers` must not be empty.
224    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    /// Next worker in turn that is not blocked, `len` workers.
248    fn round_robin(&mut self, len: usize) -> usize {
249        if self.next >= len {
250            self.next = len - 1;
251        }
252        // blocked flags are refreshed by `sample`
253        (0..len)
254            .map(|i| (self.next + i) % len)
255            .find(|&i| !self.is_blocked(i))
256            .unwrap_or(self.next)
257    }
258
259    /// Refreshes the load state of the workers.
260    ///
261    /// Returns `true` if some workers are blocked and others are not.
262    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    /// Returns `true` if the worker at `idx` was blocked when last sampled.
269    pub(crate) fn is_blocked(&self, idx: usize) -> bool {
270        self.states.get(idx).is_some_and(|st| st.blocked)
271    }
272
273    /// The item was sent to the worker at `idx`.
274    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    /// Power of two choices among the workers that are not blocked.
296    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    /// Maps CPUs to the workers placed on them, `cpus` yields the CPU of
325    /// each worker followed by its SMT siblings.
326    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    /// Returns the least loaded worker for the incoming `cpu`, unless it is
346    /// busier than `other`, the least loaded choice.
347    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    /// Returns a random number in `0..n`.
372    fn random(&mut self, n: usize) -> usize {
373        // xorshift64
374        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    /// Sample taken `turn_ms` into the current runtime turn.
402    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        // fully busy for one time constant
418        st.update(sample(250, false), t + MS * 250);
419        assert!((st.util - 0.5).abs() < 1e-9, "{}", st.util);
420
421        // idle
422        st.update(sample(250, false), t + MS * 500);
423        assert!((st.util - 0.25).abs() < 1e-9, "{}", st.util);
424
425        // busy time is not available, or there is no time between samples
426        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    /// STATEMENT: The time a worker's runtime is blocked does not count
434    /// towards its utilization, and a long turn counts once it ends.
435    ///
436    /// HOW:
437    /// - Bring the utilization to 0.5, then sample a turn that starts at
438    ///   250 ms, at 50 ms into it, when it is blocked at 150 ms, and 10 ms
439    ///   after it ended at 300 ms; the utilization stays 0.5.
440    /// - Sample an idle period and confirm the utilization decays from 0.5.
441    /// - Sample a 50 ms turn that does not block, while it runs and after it
442    ///   ended, and confirm it counts as fully busy once it ends.
443    #[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        // idle for one time constant
461        st.update(sample(550, false), t + MS * 810);
462        assert!((st.util - 0.25).abs() < 1e-9, "{}", st.util);
463
464        // a turn from 810 ms to 860 ms
465        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        // samples are taken at most every `REFRESH`
484        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        // worker list changes force a refresh
490        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        // all workers are blocked
505        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        // all workers are blocked
527        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            // utilization wins over load
542            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        // the most loaded worker never wins a comparison
557        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        // the busy worker becomes unavailable and available again
584        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        // a restarted worker starts over
592        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        // workers 0 and 1 on the two CPUs of core (0, 1), workers 2 and 3
604        // on CPU 2 and 4 of cores (2, 3) and (4, 5), worker 4 not placed
605        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        // several workers on one CPU
621        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        // the worker on the incoming CPU is preferred
636        assert_eq!(b.local_worker(0, 1, load), Some(0));
637        // the least utilized of the workers on the CPU
638        assert_eq!(b.local_worker(2, 2, load), Some(3));
639        // no worker on the CPU
640        assert_eq!(b.local_worker(7, 1, load), None);
641        // utilization exceeds the other choice by more than the slack
642        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        // load exceeds the other choice by more than half plus the slack
649        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        // blocked worker
655        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}