Skip to main content

yo_reactor/
reactor.rs

1//! The six stages, and the state that survives between two turns of them.
2
3use crate::budget::Budget;
4use std::collections::VecDeque;
5use std::sync::Arc;
6use std::sync::atomic::{AtomicBool, Ordering};
7use yo_common::Result;
8use yo_shard::Epochs;
9use yo_shard::spsc::Receiver;
10
11/// Commands taken out of the intake lanes in one turn.
12///
13/// `04` section 3 fixes this at 64 and gives the reason: the prefetch distance
14/// is the whole batch, and Y1 removes what usually keeps that window short,
15/// which is a lock held across it and other threads able to invalidate a line
16/// inside it. Valkey ships 16 and Redis 8.4 ships 16 because they have both.
17pub const BATCH_MAX: usize = 64;
18
19/// What the loop does after a command.
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum Flow {
22    /// Carry on with the rest of the batch.
23    Next,
24    /// End the batch here.
25    ///
26    /// For a command whose key set is not known until something earlier in the
27    /// same batch has finished: a `MULTI` body, a `WAIT`, a blocking form.
28    /// Prefetching those keys was not possible, so running them in this batch
29    /// would be running them cold, and whatever was drained behind this command
30    /// waits for the next turn instead of being thrown away.
31    Break,
32}
33
34/// The shard behind the loop.
35///
36/// One implementation per kind of shard, which for now means one: the string
37/// plane. The loop makes these calls in the order `04` section 2 lists and
38/// never in another order, so an implementation can rely on `prefetch` for a
39/// piece of work coming before `run` for it, on `flush` coming after every
40/// `run` in the batch, and on `maintain` coming last.
41pub trait Engine {
42    /// One command, however the layer above chose to represent it.
43    type Work;
44
45    /// The hash of the key this work touches, or `None` when it touches none.
46    ///
47    /// Called once per command, on the first walk. Whatever comes back is
48    /// handed to [`Engine::run`] on the second walk, so a command's key is
49    /// hashed once per batch rather than once per walk.
50    fn key_hash(&self, work: &Self::Work) -> Option<u64>;
51
52    /// Ask the cache for whatever `run` is about to load for this work.
53    ///
54    /// Usually one call to the index's own prefetch. It has to be cheap and it
55    /// has to read nothing, because it runs for all 64 commands before the
56    /// first one executes.
57    ///
58    /// The work comes with the hash because a hash on its own does not say
59    /// which structure to warm. Two commands in one batch can carry the same
60    /// key into different databases, and later into different types, so the
61    /// engine needs the command to know which index the bucket is in.
62    fn prefetch(&self, work: &Self::Work, hash: u64);
63
64    /// Execute one command.
65    ///
66    /// `hash` is what `key_hash` returned for this work, so a lookup takes the
67    /// hashed form rather than hashing the key a second time.
68    fn run(&mut self, work: Self::Work, hash: Option<u64>) -> Flow;
69
70    /// Write out the replies the batch produced.
71    ///
72    /// One `writev` per connection touched, never one per reply. aki's
73    /// `HGETALL` profile spent 69.7 percent of its time in write syscalls, and
74    /// this is the call that exists so that does not happen again.
75    fn flush(&mut self);
76
77    /// Hand the submission queue to the kernel.
78    ///
79    /// The first stage. One syscall when there is something queued, and none at
80    /// all under SQPoll. Does nothing by default, which is right for a shard
81    /// with no ring under it.
82    ///
83    /// # Errors
84    ///
85    /// Whatever the ring says. The turn stops there and reports it.
86    fn submit_io(&mut self) -> Result<()> {
87        Ok(())
88    }
89
90    /// Pick up completions that have arrived.
91    ///
92    /// Where a parked command finds out its write landed. Does nothing by
93    /// default.
94    ///
95    /// # Errors
96    ///
97    /// Whatever the ring says, including a failure an earlier submission left
98    /// behind.
99    fn drain_io(&mut self) -> Result<()> {
100        Ok(())
101    }
102
103    /// Spend up to `budget` on background work.
104    ///
105    /// Expiry sampling, eviction, the compaction handshake, partition
106    /// rebalance and tier demotion, in that order of priority, per `04` section
107    /// 6. Does nothing by default.
108    fn maintain(&mut self, budget: &mut Budget) {
109        let _ = budget;
110    }
111}
112
113/// What one turn of the loop did.
114#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
115pub struct Turn {
116    /// Commands executed.
117    pub commands: usize,
118    /// Whether a command ended the batch early.
119    pub broke: bool,
120    /// Commands drained but not executed, waiting for the next turn.
121    pub carried: usize,
122    /// Maintenance units spent.
123    pub maintained: u32,
124}
125
126impl Turn {
127    /// Whether the turn found nothing to do.
128    ///
129    /// Maintenance does not count. A shard sampling for expired keys with no
130    /// commands in front of it is idle, and a caller spinning on this is right
131    /// to back off.
132    #[must_use]
133    pub const fn is_idle(&self) -> bool {
134        self.commands == 0 && self.carried == 0
135    }
136}
137
138/// The loop, and what it keeps between turns.
139///
140/// Owns the engine outright. A shard is one thread, one engine and one of
141/// these, and none of the three is shared with anything.
142pub struct Reactor<E: Engine> {
143    engine: E,
144    id: usize,
145    epochs: Arc<Epochs>,
146    lanes: Vec<Receiver<E::Work>>,
147    /// Drained and not yet executed. Empty at the end of most turns, and
148    /// holding the tail of a broken batch otherwise.
149    pending: VecDeque<E::Work>,
150    /// The first walk's answers, positional against `pending`.
151    hashes: Vec<Option<u64>>,
152    /// Which lane the next drain starts from, so a busy lane cannot starve a
153    /// quiet one.
154    lane: usize,
155    budget: u32,
156    turns: u64,
157    commands: u64,
158    batches: u64,
159    full: u64,
160    breaks: u64,
161    idle: u64,
162}
163
164impl<E: Engine> Reactor<E> {
165    /// A reactor for shard `id`, taking work from `lanes`.
166    ///
167    /// One lane per submitter, which is one per network reactor and one per
168    /// embedded caller thread, and each is single producer single consumer, so
169    /// no queue here ever has two writers.
170    ///
171    /// # Panics
172    ///
173    /// If `id` is not a shard `epochs` has a slot for. That is a wiring mistake
174    /// at startup and there is nothing sensible to do with it later.
175    pub fn new(engine: E, id: usize, epochs: Arc<Epochs>, lanes: Vec<Receiver<E::Work>>) -> Self {
176        assert!(id < epochs.len(), "shard {id} has no epoch slot");
177        Reactor {
178            engine,
179            id,
180            epochs,
181            lanes,
182            pending: VecDeque::with_capacity(BATCH_MAX),
183            hashes: Vec::with_capacity(BATCH_MAX),
184            lane: 0,
185            budget: crate::MAINTENANCE_UNITS,
186            turns: 0,
187            commands: 0,
188            batches: 0,
189            full: 0,
190            breaks: 0,
191            idle: 0,
192        }
193    }
194
195    /// A reactor with no lanes, for the caller who is the shard.
196    ///
197    /// `15` section 7's embedded mode. There is no queue to cross and no thread
198    /// to hand to, so the loop stops being a loop and becomes
199    /// [`Reactor::execute`], which runs the same dispatch the server path runs.
200    /// Y23 asks for the same code rather than the same idea, and this is what
201    /// that means in practice.
202    pub fn inline(engine: E) -> Self {
203        Reactor::new(engine, 0, Epochs::new(1), Vec::new())
204    }
205
206    /// Change the maintenance allowance per turn.
207    ///
208    /// Zero means no maintenance at all, which is what a benchmark measuring
209    /// the command path alone wants and what nothing in production wants.
210    #[must_use]
211    pub fn with_maintenance(mut self, units: u32) -> Self {
212        self.budget = units;
213        self
214    }
215
216    /// The engine.
217    pub const fn engine(&self) -> &E {
218        &self.engine
219    }
220
221    /// The engine, for a caller that owns both ends of it.
222    pub const fn engine_mut(&mut self) -> &mut E {
223        &mut self.engine
224    }
225
226    /// The shard this reactor is.
227    #[must_use]
228    pub const fn id(&self) -> usize {
229        self.id
230    }
231
232    /// Turns taken.
233    #[must_use]
234    pub const fn turns(&self) -> u64 {
235        self.turns
236    }
237
238    /// Commands executed.
239    #[must_use]
240    pub const fn commands(&self) -> u64 {
241        self.commands
242    }
243
244    /// Batches drained.
245    #[must_use]
246    pub const fn batches(&self) -> u64 {
247        self.batches
248    }
249
250    /// Batches that came out full, which is the number that says whether the
251    /// batch size is doing anything.
252    ///
253    /// A shard whose batches are never full is latency bound and the prefetch
254    /// walk is buying it very little. One whose batches are always full is
255    /// throughput bound, and the window is either the right size or too small.
256    #[must_use]
257    pub const fn full_batches(&self) -> u64 {
258        self.full
259    }
260
261    /// Batches ended early by a command that could not be prefetched with the
262    /// rest of them.
263    #[must_use]
264    pub const fn breaks(&self) -> u64 {
265        self.breaks
266    }
267
268    /// Turns that found nothing to do.
269    #[must_use]
270    pub const fn idle_turns(&self) -> u64 {
271        self.idle
272    }
273
274    /// Commands drained and waiting, which is only ever the tail of a broken
275    /// batch.
276    #[must_use]
277    pub fn carried(&self) -> usize {
278        self.pending.len()
279    }
280
281    /// One turn of the six stages.
282    ///
283    /// # Errors
284    ///
285    /// From the ring, at either of the two stages that touch it. The turn stops
286    /// at the failure rather than carrying on with half a batch, and nothing is
287    /// lost: whatever was drained is still held for the next turn.
288    pub fn tick(&mut self) -> Result<Turn> {
289        self.turns += 1;
290
291        // 1. Submit. One syscall, or zero under SQPoll.
292        self.engine.submit_io()?;
293
294        // 2. Intake. Only when the last batch finished, because a broken
295        // batch's tail is already a batch, and drawing more in on top of it
296        // would push the window past 64.
297        if self.pending.is_empty() {
298            self.fill();
299            if !self.pending.is_empty() {
300                self.batches += 1;
301                if self.pending.len() == BATCH_MAX {
302                    self.full += 1;
303                }
304            }
305        }
306
307        let mut turn = Turn::default();
308        if self.pending.is_empty() {
309            // An idle turn skips the epoch and the flush. Skipping the epoch is
310            // safe rather than merely cheap: an even counter is what `all_past`
311            // reads as holding nothing, so a shard that never enters again does
312            // not hold reclamation up. Skipping the flush is safe because a
313            // turn with no commands in it touched no connection.
314            self.idle += 1;
315        } else {
316            // 3. Enter, once per batch and not once per command.
317            self.epochs.enter(self.id);
318
319            // 4a. The first walk: hash, and ask for the line.
320            self.hashes.clear();
321            for w in &self.pending {
322                let h = self.engine.key_hash(w);
323                if let Some(h) = h {
324                    self.engine.prefetch(w, h);
325                }
326                self.hashes.push(h);
327            }
328
329            // 4b. The second walk: execute what the first walk warmed.
330            let mut n = 0;
331            while let Some(w) = self.pending.pop_front() {
332                let h = self.hashes[n];
333                n += 1;
334                if self.engine.run(w, h) == Flow::Break {
335                    turn.broke = true;
336                    self.breaks += 1;
337                    break;
338                }
339            }
340
341            // 5. Leave, then write the replies out.
342            self.epochs.leave(self.id);
343            self.engine.flush();
344
345            self.commands += n as u64;
346            turn.commands = n;
347            turn.carried = self.pending.len();
348        }
349
350        // 6. Completions, then a bounded slice of background work.
351        self.engine.drain_io()?;
352        if self.budget > 0 {
353            let mut budget = Budget::new(self.budget);
354            self.engine.maintain(&mut budget);
355            turn.maintained = budget.spent();
356        }
357        Ok(turn)
358    }
359
360    /// Turns until `stop` is set and there is nothing left to run.
361    ///
362    /// The backoff is deliberately plain: spin for a while, then yield. A shard
363    /// that is expected to be busy runs on a pinned thread and never gets here,
364    /// and one that is not busy should give the core back rather than burn it.
365    ///
366    /// # Errors
367    ///
368    /// The first failure any turn reports. Whatever was drained stays drained,
369    /// so a caller that decides to carry on can call [`Reactor::tick`] again.
370    pub fn run_until(&mut self, stop: &AtomicBool) -> Result<()> {
371        const SPINS: u32 = 128;
372        let mut idle = 0u32;
373        loop {
374            if !self.tick()?.is_idle() {
375                idle = 0;
376                continue;
377            }
378            if stop.load(Ordering::Acquire) {
379                // One more turn, in case something raced in behind the flag.
380                if self.tick()?.is_idle() {
381                    return Ok(());
382                }
383                continue;
384            }
385            idle += 1;
386            if idle < SPINS {
387                std::hint::spin_loop();
388            } else {
389                std::thread::yield_now();
390                idle = 0;
391            }
392        }
393    }
394
395    /// Execute one command directly, with no queue in the way.
396    ///
397    /// The embedded path. The same `prefetch` and the same `run` the loop
398    /// calls, so a command has one implementation rather than an inline one and
399    /// a server one that drift apart.
400    ///
401    /// The epoch is entered and left around the call, which is two stores and a
402    /// fence. That is what a caller pays for being allowed to hold on to what a
403    /// command returned, and [`Reactor::execute_all`] is how to pay it once for
404    /// many commands instead of once each.
405    pub fn execute(&mut self, work: E::Work) -> Flow {
406        self.turns += 1;
407        self.commands += 1;
408        self.epochs.enter(self.id);
409        let hash = self.engine.key_hash(&work);
410        if let Some(h) = hash {
411            self.engine.prefetch(&work, h);
412        }
413        let flow = self.engine.run(work, hash);
414        self.epochs.leave(self.id);
415        flow
416    }
417
418    /// Execute a batch directly, in the same two walks the loop uses.
419    ///
420    /// Returns how many ran, which is short of what went in when one of them
421    /// broke the batch. The rest are dropped rather than queued, because an
422    /// inline caller is the one holding the work and a queue here would be a
423    /// second place it can live.
424    ///
425    /// The batch goes through the same buffer the loop drains into, so a caller
426    /// doing this in a hot loop allocates on the first call and never again.
427    pub fn execute_all<I>(&mut self, work: I) -> usize
428    where
429        I: IntoIterator<Item = E::Work>,
430    {
431        self.pending.extend(work);
432        if self.pending.is_empty() {
433            return 0;
434        }
435        self.turns += 1;
436        self.epochs.enter(self.id);
437
438        self.hashes.clear();
439        for w in &self.pending {
440            let h = self.engine.key_hash(w);
441            if let Some(h) = h {
442                self.engine.prefetch(w, h);
443            }
444            self.hashes.push(h);
445        }
446
447        let mut n = 0;
448        while let Some(w) = self.pending.pop_front() {
449            let h = self.hashes[n];
450            n += 1;
451            if self.engine.run(w, h) == Flow::Break {
452                self.breaks += 1;
453                break;
454            }
455        }
456        self.pending.clear();
457
458        self.epochs.leave(self.id);
459        self.commands += n as u64;
460        n
461    }
462
463    /// Draw up to [`BATCH_MAX`] commands, round robin from the lane after the
464    /// one the last drain finished on.
465    fn fill(&mut self) {
466        if self.lanes.is_empty() {
467            return;
468        }
469        let n = self.lanes.len();
470        let mut room = BATCH_MAX;
471        let mut at = self.lane;
472        loop {
473            let mut took = 0;
474            for _ in 0..n {
475                if room == 0 {
476                    self.lane = at;
477                    return;
478                }
479                if let Some(w) = self.lanes[at].pop() {
480                    self.pending.push_back(w);
481                    room -= 1;
482                    took += 1;
483                }
484                at = (at + 1) % n;
485            }
486            if took == 0 {
487                self.lane = at;
488                return;
489            }
490        }
491    }
492}
493
494impl<E: Engine> std::fmt::Debug for Reactor<E> {
495    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
496        f.debug_struct("Reactor")
497            .field("id", &self.id)
498            .field("lanes", &self.lanes.len())
499            .field("turns", &self.turns)
500            .field("commands", &self.commands)
501            .field("batches", &self.batches)
502            .field("full_batches", &self.full)
503            .field("breaks", &self.breaks)
504            .field("idle_turns", &self.idle)
505            .field("carried", &self.pending.len())
506            .finish()
507    }
508}
509
510#[cfg(test)]
511mod tests {
512    use super::*;
513    use std::cell::RefCell;
514    use yo_common::{Code, Error};
515    use yo_shard::spsc::{Sender, lane};
516
517    /// Every call the loop made, in the order it made it.
518    #[derive(Debug, Clone, PartialEq, Eq)]
519    enum Step {
520        Submit,
521        Prefetch(u64),
522        Run(u64),
523        Flush,
524        Drain,
525        Maintain,
526    }
527
528    /// The work value that stands for a command with no key, since a command
529    /// with no key is the one case the two walks treat differently.
530    const KEYLESS: u64 = u64::MAX;
531
532    /// An engine that records rather than does, which is the only way to assert
533    /// about an order.
534    ///
535    /// The steps sit behind a `RefCell` because `prefetch` is handed `&self`,
536    /// same as a real engine's is, and a test double is not a reason to reach
537    /// for unsafe.
538    struct Recorder {
539        steps: RefCell<Vec<Step>>,
540        break_on: Option<u64>,
541        fail_submit: bool,
542        fail_drain: bool,
543        maintenance_item: u32,
544    }
545
546    impl Recorder {
547        fn new() -> Recorder {
548            Recorder {
549                steps: RefCell::new(Vec::new()),
550                break_on: None,
551                fail_submit: false,
552                fail_drain: false,
553                maintenance_item: 0,
554            }
555        }
556
557        fn push(&self, step: Step) {
558            self.steps.borrow_mut().push(step);
559        }
560
561        fn steps(&self) -> Vec<Step> {
562            self.steps.borrow().clone()
563        }
564
565        fn runs(&self) -> Vec<u64> {
566            self.steps
567                .borrow()
568                .iter()
569                .filter_map(|s| match s {
570                    Step::Run(v) => Some(*v),
571                    _ => None,
572                })
573                .collect()
574        }
575
576        fn count(&self, want: &Step) -> usize {
577            self.steps.borrow().iter().filter(|s| *s == want).count()
578        }
579    }
580
581    impl Engine for Recorder {
582        type Work = u64;
583
584        fn key_hash(&self, work: &u64) -> Option<u64> {
585            if *work == KEYLESS { None } else { Some(*work) }
586        }
587
588        fn prefetch(&self, _work: &u64, hash: u64) {
589            self.push(Step::Prefetch(hash));
590        }
591
592        fn run(&mut self, work: u64, hash: Option<u64>) -> Flow {
593            assert_eq!(
594                hash,
595                if work == KEYLESS { None } else { Some(work) },
596                "the second walk gets the hash the first walk took"
597            );
598            self.push(Step::Run(work));
599            if self.break_on == Some(work) {
600                return Flow::Break;
601            }
602            Flow::Next
603        }
604
605        fn flush(&mut self) {
606            self.push(Step::Flush);
607        }
608
609        fn submit_io(&mut self) -> Result<()> {
610            self.push(Step::Submit);
611            if self.fail_submit {
612                return Err(Error::new(Code::Io, "submit said no"));
613            }
614            Ok(())
615        }
616
617        fn drain_io(&mut self) -> Result<()> {
618            self.push(Step::Drain);
619            if self.fail_drain {
620                return Err(Error::new(Code::Io, "drain said no"));
621            }
622            Ok(())
623        }
624
625        fn maintain(&mut self, budget: &mut Budget) {
626            self.push(Step::Maintain);
627            if self.maintenance_item == 0 {
628                return;
629            }
630            while budget.spend(self.maintenance_item) {}
631        }
632    }
633
634    /// A reactor on `lanes` lanes, with the sending ends and the epochs handed
635    /// back.
636    fn wired(lanes: usize) -> (Reactor<Recorder>, Vec<Sender<u64>>, Arc<Epochs>) {
637        let mut rxs = Vec::new();
638        let mut txs = Vec::new();
639        for _ in 0..lanes {
640            let (tx, rx) = lane(1024);
641            txs.push(tx);
642            rxs.push(rx);
643        }
644        let epochs = Epochs::new(1);
645        let r = Reactor::new(Recorder::new(), 0, Arc::clone(&epochs), rxs);
646        (r, txs, epochs)
647    }
648
649    /// Where the last prefetch and the first run landed, which is the whole
650    /// claim the two walk shape makes.
651    fn walk_boundary(steps: &[Step]) -> (usize, usize) {
652        let last_prefetch = steps
653            .iter()
654            .rposition(|s| matches!(s, Step::Prefetch(_)))
655            .expect("nothing was prefetched");
656        let first_run = steps
657            .iter()
658            .position(|s| matches!(s, Step::Run(_)))
659            .expect("nothing ran");
660        (last_prefetch, first_run)
661    }
662
663    #[test]
664    fn the_stages_run_in_the_order_the_spec_lists_them() {
665        let (mut r, tx, _e) = wired(1);
666        tx[0].push(7).unwrap();
667        let turn = r.tick().unwrap();
668        assert_eq!(turn.commands, 1);
669        assert_eq!(
670            r.engine().steps(),
671            vec![
672                Step::Submit,
673                Step::Prefetch(7),
674                Step::Run(7),
675                Step::Flush,
676                Step::Drain,
677                Step::Maintain,
678            ]
679        );
680    }
681
682    #[test]
683    fn every_command_is_prefetched_before_any_of_them_runs() {
684        let (mut r, tx, _e) = wired(1);
685        for i in 0..8 {
686            tx[0].push(i).unwrap();
687        }
688        r.tick().unwrap();
689        let (last_prefetch, first_run) = walk_boundary(&r.engine().steps());
690        assert!(
691            last_prefetch < first_run,
692            "the two walks overlapped, which makes the prefetch distance one"
693        );
694        assert_eq!(r.engine().runs(), (0..8).collect::<Vec<_>>());
695    }
696
697    #[test]
698    fn a_batch_stops_at_sixty_four() {
699        let (mut r, tx, _e) = wired(1);
700        for i in 0..200 {
701            tx[0].push(i).unwrap();
702        }
703        assert_eq!(r.tick().unwrap().commands, BATCH_MAX);
704        assert_eq!(r.full_batches(), 1);
705        assert_eq!(r.tick().unwrap().commands, BATCH_MAX);
706        assert_eq!(r.tick().unwrap().commands, BATCH_MAX);
707        assert_eq!(r.tick().unwrap().commands, 200 - 3 * BATCH_MAX);
708        assert_eq!(r.batches(), 4);
709        assert_eq!(r.full_batches(), 3);
710        assert_eq!(r.commands(), 200);
711        assert_eq!(r.engine().runs(), (0..200).collect::<Vec<_>>());
712    }
713
714    #[test]
715    fn a_break_leaves_the_rest_of_the_batch_for_the_next_turn() {
716        let (mut r, tx, _e) = wired(1);
717        for i in 0..10 {
718            tx[0].push(i).unwrap();
719        }
720        r.engine_mut().break_on = Some(3);
721        let turn = r.tick().unwrap();
722        assert!(turn.broke);
723        assert_eq!(turn.commands, 4, "the command that broke it still ran");
724        assert_eq!(turn.carried, 6);
725        assert_eq!(r.engine().count(&Step::Flush), 1, "the replies still went");
726
727        // The next turn takes the carried tail and nothing new, so the window
728        // never grows past one batch.
729        r.engine_mut().break_on = None;
730        let turn = r.tick().unwrap();
731        assert_eq!(turn.commands, 6);
732        assert_eq!(turn.carried, 0);
733        assert_eq!(r.engine().runs(), (0..10).collect::<Vec<_>>());
734        assert_eq!(r.breaks(), 1);
735        assert_eq!(r.batches(), 1, "a broken batch is one batch, not two");
736    }
737
738    #[test]
739    fn the_epoch_moves_once_per_batch_and_not_once_per_command() {
740        let (mut r, tx, epochs) = wired(1);
741        for i in 0..10 {
742            tx[0].push(i).unwrap();
743        }
744        let before = epochs.get(0);
745        r.tick().unwrap();
746        assert_eq!(
747            epochs.get(0),
748            before + 2,
749            "one enter and one leave for ten commands"
750        );
751        assert_eq!(r.commands(), 10);
752    }
753
754    #[test]
755    fn an_idle_turn_does_not_touch_the_epoch_or_the_replies() {
756        let (mut r, _tx, epochs) = wired(1);
757        let before = epochs.get(0);
758        let turn = r.tick().unwrap();
759        assert!(turn.is_idle());
760        assert_eq!(epochs.get(0), before, "an idle shard holds nothing");
761        assert_eq!(r.engine().count(&Step::Flush), 0);
762        assert_eq!(
763            r.engine().count(&Step::Drain),
764            1,
765            "completions still get picked up"
766        );
767        assert_eq!(
768            r.engine().count(&Step::Maintain),
769            1,
770            "and background work still runs"
771        );
772        assert_eq!(r.idle_turns(), 1);
773        assert_eq!(r.batches(), 0);
774    }
775
776    #[test]
777    fn work_comes_off_every_lane_rather_than_the_first_one() {
778        let (mut r, tx, _e) = wired(4);
779        for (i, t) in tx.iter().enumerate() {
780            for j in 0..4u64 {
781                t.push(i as u64 * 10 + j).unwrap();
782            }
783        }
784        let turn = r.tick().unwrap();
785        assert_eq!(turn.commands, 16);
786        let runs = r.engine().runs();
787        for lane in 0..4u64 {
788            let from_lane = runs.iter().filter(|v| **v / 10 == lane).count();
789            assert_eq!(from_lane, 4, "lane {lane} was skipped or drained twice");
790        }
791        assert_eq!(
792            runs[..4].iter().map(|v| v / 10).collect::<Vec<_>>(),
793            vec![0, 1, 2, 3],
794            "round robin, so the first four are one from each lane"
795        );
796    }
797
798    #[test]
799    fn a_lane_that_never_stops_cannot_starve_the_others() {
800        let (mut r, tx, _e) = wired(2);
801        // Lane 0 has more than a batch on its own. Lane 1 has one command and
802        // has to get out in the first turn all the same.
803        for i in 0..200 {
804            tx[0].push(i).unwrap();
805        }
806        tx[1].push(9_999).unwrap();
807        r.tick().unwrap();
808        assert!(
809            r.engine().runs().contains(&9_999),
810            "the quiet lane waited behind a whole batch of the busy one"
811        );
812    }
813
814    #[test]
815    fn maintenance_gets_a_budget_and_stops_when_it_is_spent() {
816        let (mut r, _tx, _e) = wired(1);
817        r.engine_mut().maintenance_item = 100;
818        let turn = r.tick().unwrap();
819        // The slice stops on the item that takes it past the end rather than
820        // before it, so the overshoot is one item and never more.
821        assert!(
822            (crate::MAINTENANCE_UNITS..crate::MAINTENANCE_UNITS + 100).contains(&turn.maintained),
823            "spent {}",
824            turn.maintained
825        );
826
827        let mut r = r.with_maintenance(0);
828        let before = r.engine().count(&Step::Maintain);
829        let turn = r.tick().unwrap();
830        assert_eq!(turn.maintained, 0);
831        assert_eq!(
832            r.engine().count(&Step::Maintain),
833            before,
834            "a zero budget is no slice at all rather than an empty one"
835        );
836    }
837
838    #[test]
839    fn a_failed_submit_stops_the_turn_before_the_batch() {
840        let (mut r, tx, _e) = wired(1);
841        tx[0].push(1).unwrap();
842        r.engine_mut().fail_submit = true;
843        assert_eq!(r.tick().unwrap_err().code(), Code::Io);
844        assert_eq!(r.engine().runs(), Vec::<u64>::new());
845        assert_eq!(r.carried(), 0, "nothing was drained, so nothing is held");
846
847        // And the work is still in the lane afterwards.
848        r.engine_mut().fail_submit = false;
849        assert_eq!(r.tick().unwrap().commands, 1);
850    }
851
852    #[test]
853    fn a_failed_drain_still_ran_the_batch_and_flushed_it() {
854        let (mut r, tx, _e) = wired(1);
855        tx[0].push(1).unwrap();
856        r.engine_mut().fail_drain = true;
857        assert_eq!(r.tick().unwrap_err().code(), Code::Io);
858        assert_eq!(r.engine().runs(), vec![1]);
859        assert_eq!(r.engine().count(&Step::Flush), 1);
860    }
861
862    #[test]
863    fn a_command_with_no_key_is_run_without_a_hash() {
864        let (mut r, tx, _e) = wired(1);
865        tx[0].push(KEYLESS).unwrap();
866        let turn = r.tick().unwrap();
867        assert_eq!(turn.commands, 1);
868        assert_eq!(
869            r.engine().count(&Step::Prefetch(KEYLESS)),
870            0,
871            "there is nothing to warm for a command with no key"
872        );
873    }
874
875    #[test]
876    fn inline_execution_takes_the_same_walk_as_the_loop() {
877        let mut r = Reactor::inline(Recorder::new());
878        assert_eq!(r.execute(9), Flow::Next);
879        assert_eq!(
880            r.engine().steps(),
881            vec![Step::Prefetch(9), Step::Run(9)],
882            "no submit, no flush and no maintenance, and the same two calls"
883        );
884        assert_eq!(r.commands(), 1);
885    }
886
887    #[test]
888    fn inline_batches_pay_for_the_epoch_once() {
889        let mut r = Reactor::inline(Recorder::new());
890        assert_eq!(r.execute_all(0..20), 20);
891        assert_eq!(r.engine().runs(), (0..20).collect::<Vec<_>>());
892        let (last_prefetch, first_run) = walk_boundary(&r.engine().steps());
893        assert!(last_prefetch < first_run, "inline gets the two walks too");
894    }
895
896    #[test]
897    fn an_inline_batch_stops_at_a_break() {
898        let mut r = Reactor::inline(Recorder::new());
899        r.engine_mut().break_on = Some(2);
900        assert_eq!(r.execute_all(0..10), 3);
901        assert_eq!(r.engine().runs(), vec![0, 1, 2]);
902        assert_eq!(r.carried(), 0, "inline holds nothing over to next time");
903
904        // And the next batch is not the last one's leftovers.
905        r.engine_mut().break_on = None;
906        assert_eq!(r.execute_all(100..103), 3);
907        assert_eq!(r.engine().runs(), vec![0, 1, 2, 100, 101, 102]);
908    }
909
910    #[test]
911    fn an_empty_inline_batch_is_not_a_turn() {
912        let mut r = Reactor::inline(Recorder::new());
913        assert_eq!(r.execute_all(Vec::new()), 0);
914        assert_eq!(r.turns(), 0);
915        assert!(r.engine().steps().is_empty());
916    }
917
918    #[test]
919    fn run_until_returns_when_the_flag_is_set_and_the_lanes_are_dry() {
920        let (mut r, tx, _e) = wired(1);
921        for i in 0..300 {
922            tx[0].push(i).unwrap();
923        }
924        let stop = AtomicBool::new(true);
925        r.run_until(&stop).unwrap();
926        assert_eq!(r.commands(), 300, "the flag does not drop queued work");
927        assert!(r.turns() >= 5);
928    }
929
930    #[test]
931    fn a_reactor_says_what_it_has_been_doing() {
932        let (mut r, tx, _e) = wired(1);
933        tx[0].push(1).unwrap();
934        r.tick().unwrap();
935        let said = format!("{r:?}");
936        assert!(said.contains("commands: 1"), "{said}");
937        assert!(said.contains("lanes: 1"), "{said}");
938    }
939}