Skip to main content

tpchgen_cli/
generate.rs

1//! Parallel data generation: [`Source`] and [`Sink`] and [`generate_in_chunks`]
2//!
3//! These traits and function are used to generate data in parallel and write it to a sink
4//! in streaming fashion (chunks). This is useful for generating large datasets that don't fit in memory.
5
6use crate::progress::TableProgress;
7use futures::StreamExt;
8use log::debug;
9use std::collections::VecDeque;
10use std::io;
11use std::sync::{Arc, Mutex};
12use tokio::task::JoinSet;
13
14/// Something that knows how to generate data into a buffer
15///
16/// For example, this is implemented for the different generators in the tpchgen
17/// crate
18pub trait Source: Send {
19    /// generates the data for this generator into the buffer, returning the buffer.
20    fn create(self, buffer: Vec<u8>) -> Vec<u8>;
21
22    /// Create the first line for the output, into the buffer
23    ///
24    /// This will be called before the first call to [`Self::create`] and
25    /// exactly once across all [`Source`]es
26    fn header(&self, buffer: Vec<u8>) -> Vec<u8>;
27}
28
29/// Something that can write the contents of a buffer somewhere
30///
31/// For example, this is implemented for a file writer.
32pub trait Sink: Send {
33    /// Write all data from the buffer to the sink
34    fn sink(&mut self, buffer: &[u8]) -> Result<(), io::Error>;
35
36    /// Complete and flush any remaining data from the sink
37    fn flush(self) -> Result<(), io::Error>;
38}
39
40/// Generates data in parallel from a series of [`Source`] and writes to a [`Sink`]
41///
42/// Each [`Source`] is a data generator that generates data directly into an in
43/// memory buffer.
44///
45/// This function will run the [`Source`]es in parallel up to num_threads.
46/// Data is written to the [`Sink`] in the order of the [`Source`]es in
47/// the input iterator.
48///
49/// G: Generator
50/// I: Iterator<Item = G>
51/// S: Sink that writes buffers somewhere
52pub async fn generate_in_chunks<G, I, S>(
53    sink: S,
54    sources: I,
55    num_threads: usize,
56) -> Result<(), io::Error>
57where
58    G: Source + 'static,
59    I: Iterator<Item = G>,
60    S: Sink + 'static,
61{
62    let progress = Default::default();
63    generate_in_chunks_impl(sink, sources, num_threads, progress).await
64}
65
66pub(crate) async fn generate_in_chunks_with_progress<G, I, S>(
67    sink: S,
68    sources: I,
69    num_threads: usize,
70    progress: TableProgress,
71) -> Result<(), io::Error>
72where
73    G: Source + 'static,
74    I: Iterator<Item = G>,
75    S: Sink + 'static,
76{
77    generate_in_chunks_impl(sink, sources, num_threads, progress).await
78}
79
80async fn generate_in_chunks_impl<G, I, S>(
81    mut sink: S,
82    sources: I,
83    num_threads: usize,
84    progress: TableProgress,
85) -> Result<(), io::Error>
86where
87    G: Source + 'static,
88    I: Iterator<Item = G>,
89    S: Sink + 'static,
90{
91    let recycler = BufferRecycler::new();
92    let mut sources = sources.peekable();
93
94    // use all cores to make data
95    debug!("Using {num_threads} threads");
96
97    let Some(first) = sources.peek() else {
98        return Ok(()); // no sources
99    };
100    let header = first.header(Vec::new());
101
102    let sources_and_recyclers = sources.map(|generator| (generator, recycler.clone()));
103
104    // create a channel to communicate between the generator tasks and the writer task
105    let (tx, mut rx) = tokio::sync::mpsc::channel(num_threads);
106
107    // convert to an async stream to run on tokio
108    let mut stream = futures::stream::iter(sources_and_recyclers)
109        // each generator writes to a buffer
110        .map(async |(source, recycler)| {
111            let buffer = recycler.new_buffer(1024 * 1024 * 8);
112            // do the work in a task (on a different thread)
113            let mut join_set = JoinSet::new();
114            join_set.spawn(async move { source.create(buffer) });
115            // wait for the task to be done and return the result
116            join_set
117                .join_next()
118                .await
119                .expect("had one item")
120                .expect("join_next join is infallible unless task panics")
121        })
122        // run in parallel
123        .buffered(num_threads)
124        .map(async |buffer| {
125            // send the buffer to the writer task, in order.
126
127            // Note we ignore errors writing because if the write errors it
128            // means the channel is closed / the program is exiting so there
129            // is nothing listening to send errors
130            if let Err(e) = tx.send(buffer).await {
131                debug!("Error sending buffer to writer: {e}");
132            }
133        });
134
135    // The writer task runs in a blocking thread to avoid blocking the async
136    // runtime. It reads from the channel and writes to the sink (doing File IO)
137    let captured_recycler = recycler.clone();
138    let captured_progress = progress.clone();
139    let writer_task = tokio::task::spawn_blocking(move || {
140        // The header is not an output unit; only generated chunks from the channel advance progress.
141        sink.sink(&header)?;
142        while let Some(buffer) = rx.blocking_recv() {
143            sink.sink(&buffer)?;
144            captured_recycler.return_buffer(buffer);
145            captured_progress.increment_output_unit();
146        }
147        // No more input, flush the sink and return
148        sink.flush()
149    });
150
151    // drive the stream to completion
152    while let Some(write_task) = stream.next().await {
153        // break early if the writer stream is done (errored)
154        if writer_task.is_finished() {
155            debug!("writer task is done early, stopping writer");
156            break;
157        }
158        write_task.await; // sends the buffer to the writer task
159    }
160    drop(stream); // drop any stream references
161    drop(tx); // drop last tx reference to tell the writer it is done.
162
163    // wait for writer to finish
164    debug!("waiting for writer task to complete");
165    writer_task.await.expect("writer task panicked")
166}
167
168/// A simple buffer recycler to avoid allocating new buffers for each part
169///
170/// Clones share the same underlying recycler, so it is not thread safe
171#[derive(Debug, Clone)]
172struct BufferRecycler {
173    buffers: Arc<Mutex<VecDeque<Vec<u8>>>>,
174}
175
176impl BufferRecycler {
177    fn new() -> Self {
178        Self {
179            buffers: Arc::new(Mutex::new(VecDeque::new())),
180        }
181    }
182    /// return a new empty buffer, with size bytes capacity
183    fn new_buffer(&self, size: usize) -> Vec<u8> {
184        let mut buffers = self.buffers.lock().unwrap();
185        if let Some(mut buffer) = buffers.pop_front() {
186            buffer.clear();
187            if size > buffer.capacity() {
188                buffer.reserve(size - buffer.capacity());
189            }
190            buffer
191        } else {
192            Vec::with_capacity(size)
193        }
194    }
195
196    fn return_buffer(&self, buffer: Vec<u8>) {
197        let mut buffers = self.buffers.lock().unwrap();
198        buffers.push_back(buffer);
199    }
200}
201
202#[cfg(all(test, feature = "progress"))]
203mod tests {
204    use super::*;
205    use crate::progress::{ProgressTracker, RunProgress};
206    use crate::Table;
207    use std::sync::atomic::{AtomicU64, Ordering};
208
209    #[derive(Debug)]
210    struct CountingProgress {
211        increments: AtomicU64,
212    }
213
214    impl ProgressTracker for CountingProgress {
215        fn increment(&self, _table: Table, units: u64) {
216            self.increments.fetch_add(units, Ordering::Relaxed);
217        }
218    }
219
220    struct TestSource {
221        header: &'static [u8],
222        data: &'static [u8],
223    }
224
225    impl Source for TestSource {
226        fn header(&self, mut buffer: Vec<u8>) -> Vec<u8> {
227            buffer.extend_from_slice(self.header);
228            buffer
229        }
230
231        fn create(self, mut buffer: Vec<u8>) -> Vec<u8> {
232            buffer.extend_from_slice(self.data);
233            buffer
234        }
235    }
236
237    struct CapturingSink {
238        writes: Arc<Mutex<Vec<Vec<u8>>>>,
239    }
240
241    impl Sink for CapturingSink {
242        fn sink(&mut self, buffer: &[u8]) -> Result<(), io::Error> {
243            self.writes.lock().unwrap().push(buffer.to_vec());
244            Ok(())
245        }
246
247        fn flush(self) -> Result<(), io::Error> {
248            Ok(())
249        }
250    }
251
252    #[tokio::test]
253    async fn progress_counts_generated_chunks_not_header() {
254        let writes = Arc::new(Mutex::new(Vec::new()));
255        let tracker = Arc::new(CountingProgress {
256            increments: AtomicU64::new(0),
257        });
258        let progress: Arc<dyn ProgressTracker> = tracker.clone();
259        let progress = RunProgress::with_tracker(progress).for_table(Table::Region);
260
261        let sources = vec![
262            TestSource {
263                header: b"header\n",
264                data: b"row-1\n",
265            },
266            TestSource {
267                header: b"header\n",
268                data: b"row-2\n",
269            },
270        ];
271
272        generate_in_chunks_with_progress(
273            CapturingSink {
274                writes: Arc::clone(&writes),
275            },
276            sources.into_iter(),
277            1,
278            progress,
279        )
280        .await
281        .unwrap();
282
283        assert_eq!(tracker.increments.load(Ordering::Relaxed), 2);
284        assert_eq!(
285            *writes.lock().unwrap(),
286            vec![
287                b"header\n".to_vec(),
288                b"row-1\n".to_vec(),
289                b"row-2\n".to_vec()
290            ]
291        );
292    }
293}