Skip to main content

git_policy_cli/foundation/
parallel_runner.rs

1//! What:
2//!  Runs independent items on a bounded number of threads and reports what they say and what they return in input order,
3//!  as if they had run one after another.
4//! Why:
5//!  Items that share nothing (one worktree each, one directory each) need not wait for each other, but a person reads one
6//!  stream of lines. The calling thread is the only consumer: workers hand it lines and results over a channel, it prints the
7//!  lines of the first unfinished item as they arrive (so progress stays live), holds the lines of later items until every item
8//!  before them is complete, and then prints them in order. No lock is taken: the channel and one atomic counter that hands out
9//!  the lanes are the whole of the sharing, and every item has exactly one owner.
10//!
11//! In TS you'd write (pseudocode):
12//! ```ts
13//! await runOrdered({ items, lanes, workers, work: (index, item, say) => convert(item, say), onEvent });
14//! ```
15
16use std::sync::atomic::{AtomicUsize, Ordering};
17use std::sync::mpsc::{Sender, channel};
18
19/// What:
20///  What the consumer is told, always in input order.
21/// Why:
22///   An item begins when every item before it is complete, says its lines, and ends with its result.
23///
24/// In TS you'd write (pseudocode):
25/// ```ts
26/// type Event<R> = { begin: number } | { line: [number, string] } | { end: [number, R] };
27/// ```
28#[derive(Debug, Eq, PartialEq)]
29pub enum Event<'line, R> {
30    /// The item at this index is the first unfinished one; its lines follow.
31    Begin(usize),
32    /// A line the item at this index said.
33    Line(usize, &'line str),
34    /// The item at this index is complete, with its result.
35    End(usize, R),
36}
37
38/// What a worker hands the consumer.
39enum Message<R> {
40    /// A line an item said.
41    Line(usize, String),
42    /// An item is complete.
43    Done(usize, R),
44}
45
46/// What:
47///  Everything one ordered run needs.
48/// Why:
49///   One value names the inputs, so the call reads as a list of facts.
50///
51/// In TS you'd write (pseudocode):
52/// ```ts
53/// type OrderedRun<T, R> = { items: T[]; lanes: number[][]; workers: number; work: (index, item, say) => R; onEvent: (event) => void };
54/// ```
55pub struct OrderedRun<'run, T, R> {
56    /// The items, in the order their output is reported.
57    pub items: &'run [T],
58    /// The indices of the items, grouped: the items of a lane run one after another on one worker, lanes run side by side.
59    ///  Every index appears in exactly one lane. Items that must not overlap (the same worktree named twice) share a lane.
60    pub lanes: &'run [Vec<usize>],
61    /// The most threads that run at once; at least one, and never more than there are lanes.
62    pub workers: usize,
63    /// What one item does, given its index, itself and a function that says one line.
64    pub work: &'run (dyn Fn(usize, &T, &mut dyn FnMut(&str)) -> R + Sync),
65    /// The consumer, called on the calling thread only.
66    pub on_event: &'run mut dyn FnMut(Event<'_, R>),
67}
68
69/// What the calling thread has reported so far.
70struct Cursor<R> {
71    /// The first item that is not complete.
72    head: usize,
73    /// Whether `Begin` was reported for `head`.
74    begun: bool,
75    /// The lines of items that are not the head yet, by index.
76    held_lines: Vec<Vec<String>>,
77    /// The results of items that are complete but not reported, by index.
78    finished: Vec<Option<R>>,
79}
80
81impl<R> Cursor<R> {
82    /// Report everything that is now reportable: the head begins, its held lines follow, and a complete head ends and the
83    ///  next item becomes the head.
84    fn advance(&mut self, on_event: &mut dyn FnMut(Event<'_, R>)) {
85        loop {
86            if self.head >= self.finished.len() {
87                return;
88            }
89            if !self.begun {
90                on_event(Event::Begin(self.head));
91                self.begun = true;
92                for text in std::mem::take(&mut self.held_lines[self.head]) {
93                    on_event(Event::Line(self.head, text.as_str()));
94                }
95            }
96            match self.finished[self.head].take() {
97                Some(result) => {
98                    on_event(Event::End(self.head, result));
99                    self.head += 1;
100                    self.begun = false;
101                }
102                None => return,
103            }
104        }
105    }
106}
107
108/// What a worker does: take lanes until none is left, run the items of each lane in order, and send every line and result.
109fn work_lanes<T, R>(run: (&[T], &[Vec<usize>], &AtomicUsize, &(dyn Fn(usize, &T, &mut dyn FnMut(&str)) -> R + Sync)), sender: &Sender<Message<R>>) {
110    let (items, lanes, next_lane, work) = run;
111    loop {
112        let lane_index: usize = next_lane.fetch_add(1, Ordering::Relaxed);
113        let Some(lane) = lanes.get(lane_index) else { return };
114        for &index in lane {
115            let Some(item) = items.get(index) else { continue };
116            let result: R = work(index, item, &mut |text: &str| {
117                // The consumer is gone only when the calling thread panicked, and that panic is reported by the scope.
118                let _sent = sender.send(Message::Line(index, text.to_string()));
119            });
120            let _sent = sender.send(Message::Done(index, result));
121        }
122    }
123}
124
125/// What:
126///  Run every item and report in input order.
127/// Why:
128///   See the module description. A panic in a worker ends the run: the channel closes when every worker is gone, the
129///  consumer stops waiting, and the scope raises the worker's panic on the calling thread.
130///
131/// In TS you'd write (pseudocode):
132/// ```ts
133/// await runOrdered({ items, lanes, workers, work, onEvent });
134/// ```
135///
136/// # Panics
137/// When a lane names an index that is not an item, or the lanes do not cover every item exactly once.
138pub fn run_ordered<T: Sync, R: Send>(run: OrderedRun<'_, T, R>) {
139    let OrderedRun { items, lanes, workers, work, on_event } = run;
140    let mut covered: Vec<bool> = vec![false; items.len()];
141    for &index in lanes.iter().flatten() {
142        let slot: &mut bool = covered.get_mut(index).unwrap_or_else(|| { panic!("a lane names item {index}, but there are {} items", items.len()); });
143        assert!(!*slot, "item {index} is in two lanes");
144        *slot = true;
145    }
146    assert!(covered.iter().all(|seen: &bool| { return *seen; }), "a lane set must cover every item");
147    let next_lane: AtomicUsize = AtomicUsize::new(0);
148    let (sender, receiver) = channel::<Message<R>>();
149    let mut cursor: Cursor<R> = Cursor { head: 0, begun: false, held_lines: items.iter().map(|_item: &T| { return Vec::new(); }).collect(), finished: items.iter().map(|_item: &T| { return None; }).collect() };
150    std::thread::scope(|scope| {
151        for _worker in 0..workers.max(1).min(lanes.len().max(1)) {
152            let worker_sender: Sender<Message<R>> = sender.clone();
153            let next: &AtomicUsize = &next_lane;
154            scope.spawn(move || { work_lanes((items, lanes, next, work), &worker_sender); });
155        }
156        drop(sender);
157        cursor.advance(on_event);
158        while let Ok(message) = receiver.recv() {
159            match message {
160                Message::Line(index, text) => {
161                    if index == cursor.head && cursor.begun {
162                        on_event(Event::Line(index, text.as_str()));
163                    } else {
164                        cursor.held_lines[index].push(text);
165                    }
166                }
167                Message::Done(index, result) => cursor.finished[index] = Some(result),
168            }
169            cursor.advance(on_event);
170        }
171    });
172}
173
174/// Ordered run controls stay out of the release executable.
175#[cfg(test)]
176#[path = "parallel_runner_tests.rs"]
177mod tests;