1use crate::csv::*;
4use crate::generate::generate_in_chunks_with_progress;
5use crate::generate::Source;
6use crate::output_plan::{OutputLocation, OutputPlan};
7use crate::parquet::generate_parquet_with_progress;
8#[cfg(feature = "progress")]
9use crate::progress::ProgressTracker;
10use crate::progress::RunProgress;
11use crate::tbl::*;
12use crate::tbl::{LineItemTblSource, NationTblSource, RegionTblSource};
13use crate::{OutputFormat, Table, WriterSink};
14use log::{debug, info};
15use std::io;
16use std::io::BufWriter;
17#[cfg(feature = "progress")]
18use std::sync::Arc;
19use tokio::task::{JoinError, JoinSet};
20use tpchgen::generators::{
21 CustomerGenerator, LineItemGenerator, NationGenerator, OrderGenerator, PartGenerator,
22 PartSuppGenerator, RegionGenerator, SupplierGenerator,
23};
24use tpchgen_arrow::{
25 CustomerArrow, LineItemArrow, NationArrow, OrderArrow, PartArrow, PartSuppArrow,
26 RecordBatchIterator, RegionArrow, SupplierArrow,
27};
28
29#[derive(Debug)]
32pub struct PlanRunner {
33 plans: Vec<OutputPlan>,
34 num_threads: usize,
35 progress: RunProgress,
36}
37
38impl PlanRunner {
39 pub fn new(plans: Vec<OutputPlan>, num_threads: usize) -> Self {
42 Self {
43 plans,
44 num_threads,
45 progress: RunProgress::default(),
46 }
47 }
48
49 #[cfg(feature = "progress")]
57 pub fn with_progress_tracker(mut self, tracker: Arc<dyn ProgressTracker>) -> Self {
58 self.progress = RunProgress::with_tracker(tracker);
59 self
60 }
61
62 pub async fn run(self) -> Result<(), io::Error> {
64 debug!(
65 "Running {} plans with {} threads...",
66 self.plans.len(),
67 self.num_threads
68 );
69 let Self {
70 mut plans,
71 num_threads,
72 progress,
73 } = self;
74
75 plans.sort_unstable_by(|a, b| {
77 let a_cnt = a.chunk_count();
78 let b_cnt = b.chunk_count();
79 a_cnt.cmp(&b_cnt)
80 });
81
82 progress.register_totals(&plans);
85
86 let mut worker_queue = WorkerQueue::new(num_threads, progress.clone());
88 while let Some(plan) = plans.pop() {
89 worker_queue.schedule_plan(plan).await?;
90 }
91 worker_queue.join_all().await?;
92 progress.finish();
93 Ok(())
94 }
95}
96
97struct WorkerQueue {
114 join_set: JoinSet<io::Result<usize>>,
115 available_threads: usize,
117 progress: RunProgress,
118}
119
120impl WorkerQueue {
121 pub fn new(max_threads: usize, progress: RunProgress) -> Self {
122 assert!(max_threads > 0);
123 Self {
124 join_set: JoinSet::new(),
125 available_threads: max_threads,
126 progress,
127 }
128 }
129
130 pub async fn schedule_plan(&mut self, plan: OutputPlan) -> io::Result<()> {
140 debug!("scheduling plan {plan}");
141 loop {
142 if self.available_threads == 0 {
143 debug!("no threads left, wait for one to finish");
144 let Some(result) = self.join_set.join_next().await else {
145 return Err(io::Error::other(
146 "Internal Error No more tasks to wait for, but had no threads",
147 ));
148 };
149 self.available_threads += task_result(result)?;
150 continue; }
152
153 if let Some(result) = self.join_set.try_join_next() {
155 self.available_threads += task_result(result)?;
156 continue;
157 }
158
159 debug_assert!(
160 self.available_threads > 0,
161 "should have at least one thread to continue"
162 );
163
164 let chunk_count = plan.chunk_count();
167
168 let num_plan_threads = self.available_threads.min(chunk_count);
169
170 debug!("Spawning plan {plan} with {num_plan_threads} threads");
172
173 let progress = self.progress.clone();
174 self.join_set
175 .spawn(async move { run_plan(plan, num_plan_threads, progress).await });
176 self.available_threads -= num_plan_threads;
177 return Ok(());
178 }
179 }
180
181 pub async fn join_all(mut self) -> io::Result<()> {
183 debug!("Waiting for tasks to finish...");
184 while let Some(result) = self.join_set.join_next().await {
185 task_result(result)?;
186 }
187 debug!("Tasks finished.");
188 Ok(())
189 }
190}
191
192fn task_result<T>(result: Result<io::Result<T>, JoinError>) -> io::Result<T> {
194 result.map_err(|e| io::Error::other(format!("Task Panic: {e}")))?
195}
196
197async fn run_plan(
199 plan: OutputPlan,
200 num_threads: usize,
201 progress: RunProgress,
202) -> io::Result<usize> {
203 match plan.table() {
204 Table::Nation => run_nation_plan(plan, num_threads, progress).await,
205 Table::Region => run_region_plan(plan, num_threads, progress).await,
206 Table::Part => run_part_plan(plan, num_threads, progress).await,
207 Table::Supplier => run_supplier_plan(plan, num_threads, progress).await,
208 Table::Partsupp => run_partsupp_plan(plan, num_threads, progress).await,
209 Table::Customer => run_customer_plan(plan, num_threads, progress).await,
210 Table::Orders => run_orders_plan(plan, num_threads, progress).await,
211 Table::Lineitem => run_lineitem_plan(plan, num_threads, progress).await,
212 }
213}
214
215fn maybe_skip_existing(path: &std::path::Path, plan: &OutputPlan, progress: &RunProgress) -> bool {
219 if !path.exists() {
220 return false;
221 }
222 log::warn!("{} already exists, skipping generation", path.display());
223 progress.increment_for_existing(plan);
224 true
225}
226
227async fn write_file<I>(
229 plan: OutputPlan,
230 num_threads: usize,
231 sources: I,
232 progress: RunProgress,
233) -> Result<(), io::Error>
234where
235 I: Iterator<Item: Source> + 'static,
236{
237 let table = plan.table();
238 let table_progress = progress.for_table(table);
239 match plan.output_location() {
242 OutputLocation::Stdout => {
243 let sink = WriterSink::new(io::stdout());
244 generate_in_chunks_with_progress(sink, sources, num_threads, table_progress).await
245 }
246 OutputLocation::File(path) => {
247 if maybe_skip_existing(path, &plan, &progress) {
248 return Ok(());
249 }
250 let temp_path = path.with_extension("inprogress");
252 let file = std::fs::File::create(&temp_path).map_err(|err| {
253 io::Error::other(format!("Failed to create {temp_path:?}: {err}"))
254 })?;
255 let sink = WriterSink::new(file);
256 generate_in_chunks_with_progress(sink, sources, num_threads, table_progress).await?;
257 std::fs::rename(&temp_path, path).map_err(|e| {
259 io::Error::other(format!(
260 "Failed to rename {temp_path:?} to {path:?} file: {e}"
261 ))
262 })?;
263 Ok(())
264 }
265 }
266}
267
268async fn write_parquet<I>(
270 plan: OutputPlan,
271 num_threads: usize,
272 sources: I,
273 progress: RunProgress,
274) -> Result<(), io::Error>
275where
276 I: Iterator<Item: RecordBatchIterator> + 'static,
277{
278 let table = plan.table();
279 let table_progress = progress.for_table(table);
280 match plan.output_location() {
281 OutputLocation::Stdout => {
282 let writer = BufWriter::with_capacity(32 * 1024 * 1024, io::stdout()); generate_parquet_with_progress(
284 writer,
285 sources,
286 num_threads,
287 plan.parquet_compression(),
288 table_progress,
289 )
290 .await
291 }
292 OutputLocation::File(path) => {
293 if maybe_skip_existing(path, &plan, &progress) {
294 return Ok(());
295 }
296 let temp_path = path.with_extension("inprogress");
298 let file = std::fs::File::create(&temp_path).map_err(|err| {
299 io::Error::other(format!("Failed to create {temp_path:?}: {err}"))
300 })?;
301 let writer = BufWriter::with_capacity(32 * 1024 * 1024, file); generate_parquet_with_progress(
303 writer,
304 sources,
305 num_threads,
306 plan.parquet_compression(),
307 table_progress,
308 )
309 .await?;
310 std::fs::rename(&temp_path, path).map_err(|e| {
312 io::Error::other(format!(
313 "Failed to rename {temp_path:?} to {path:?} file: {e}"
314 ))
315 })?;
316 Ok(())
317 }
318 }
319}
320
321macro_rules! define_run {
330 ($FUN_NAME:ident, $GENERATOR:ident, $TBL_SOURCE:ty, $CSV_SOURCE:ty, $PARQUET_SOURCE:ty) => {
331 async fn $FUN_NAME(
332 plan: OutputPlan,
333 num_threads: usize,
334 progress: RunProgress,
335 ) -> io::Result<usize> {
336 use crate::GenerationPlan;
337 let scale_factor = plan.scale_factor();
338 info!("Writing {plan} using {num_threads} threads");
339
340 fn tbl_sources(
349 generation_plan: &GenerationPlan,
350 scale_factor: f64,
351 ) -> impl Iterator<Item: Source> + 'static {
352 generation_plan
353 .clone()
354 .into_iter()
355 .map(move |(part, num_parts)| $GENERATOR::new(scale_factor, part, num_parts))
356 .map(<$TBL_SOURCE>::new)
357 }
358
359 fn csv_sources(
360 generation_plan: &GenerationPlan,
361 scale_factor: f64,
362 delimiter: char,
363 ) -> impl Iterator<Item: Source> + 'static {
364 generation_plan
365 .clone()
366 .into_iter()
367 .map(move |(part, num_parts)| $GENERATOR::new(scale_factor, part, num_parts))
368 .map(move |gen| <$CSV_SOURCE>::new(gen, delimiter))
369 }
370
371 fn parquet_sources(
372 generation_plan: &GenerationPlan,
373 scale_factor: f64,
374 ) -> impl Iterator<Item: RecordBatchIterator> + 'static {
375 generation_plan
376 .clone()
377 .into_iter()
378 .map(move |(part, num_parts)| $GENERATOR::new(scale_factor, part, num_parts))
379 .map(<$PARQUET_SOURCE>::new)
380 }
381
382 match plan.output_format() {
384 OutputFormat::Tbl => {
385 let gens = tbl_sources(plan.generation_plan(), scale_factor);
386 write_file(plan, num_threads, gens, progress).await?
387 }
388 OutputFormat::Csv => {
389 let delimiter = plan.csv_delimiter();
390 let gens = csv_sources(plan.generation_plan(), scale_factor, delimiter);
391 write_file(plan, num_threads, gens, progress).await?
392 }
393 OutputFormat::Parquet => {
394 let gens = parquet_sources(plan.generation_plan(), scale_factor);
395 write_parquet(plan, num_threads, gens, progress).await?
396 }
397 };
398 Ok(num_threads)
399 }
400 };
401}
402
403define_run!(
404 run_lineitem_plan,
405 LineItemGenerator,
406 LineItemTblSource,
407 LineItemCsvSource,
408 LineItemArrow
409);
410
411define_run!(
412 run_nation_plan,
413 NationGenerator,
414 NationTblSource,
415 NationCsvSource,
416 NationArrow
417);
418
419define_run!(
420 run_region_plan,
421 RegionGenerator,
422 RegionTblSource,
423 RegionCsvSource,
424 RegionArrow
425);
426
427define_run!(
428 run_part_plan,
429 PartGenerator,
430 PartTblSource,
431 PartCsvSource,
432 PartArrow
433);
434
435define_run!(
436 run_supplier_plan,
437 SupplierGenerator,
438 SupplierTblSource,
439 SupplierCsvSource,
440 SupplierArrow
441);
442define_run!(
443 run_partsupp_plan,
444 PartSuppGenerator,
445 PartSuppTblSource,
446 PartSuppCsvSource,
447 PartSuppArrow
448);
449
450define_run!(
451 run_customer_plan,
452 CustomerGenerator,
453 CustomerTblSource,
454 CustomerCsvSource,
455 CustomerArrow
456);
457
458define_run!(
459 run_orders_plan,
460 OrderGenerator,
461 OrderTblSource,
462 OrderCsvSource,
463 OrderArrow
464);
465
466#[cfg(all(test, feature = "progress"))]
467mod tests {
468 use super::*;
469 use crate::progress::ProgressTracker;
470 use crate::{Compression, GenerationPlan, DEFAULT_PARQUET_ROW_GROUP_BYTES};
471 use std::sync::{
472 atomic::{AtomicU64, Ordering},
473 Arc,
474 };
475
476 #[derive(Debug)]
477 struct CountingProgress {
478 increments: AtomicU64,
479 }
480
481 impl ProgressTracker for CountingProgress {
482 fn increment(&self, _table: Table, units: u64) {
483 self.increments.fetch_add(units, Ordering::Relaxed);
484 }
485 }
486
487 #[test]
488 fn skip_existing_advances_progress_by_full_plan() {
489 let output_dir = tempfile::tempdir().unwrap();
490 let output_path = output_dir.path().join("lineitem.tbl");
491 std::fs::write(&output_path, b"already here").unwrap();
492
493 let generation_plan = GenerationPlan::try_new(
494 Table::Lineitem,
495 OutputFormat::Tbl,
496 1.0,
497 Some(1),
498 Some(4),
499 DEFAULT_PARQUET_ROW_GROUP_BYTES,
500 )
501 .unwrap();
502 let plan = OutputPlan::new(
503 Table::Lineitem,
504 1.0,
505 OutputFormat::Tbl,
506 Compression::SNAPPY,
507 OutputLocation::File(output_path.clone()),
508 generation_plan,
509 ',',
510 );
511 let expected_units = plan.chunk_count() as u64;
512 assert!(expected_units > 1);
513
514 let tracker = Arc::new(CountingProgress {
515 increments: AtomicU64::new(0),
516 });
517 let progress: Arc<dyn ProgressTracker> = tracker.clone();
518 let progress = RunProgress::with_tracker(progress);
519
520 assert!(maybe_skip_existing(&output_path, &plan, &progress));
521 assert_eq!(tracker.increments.load(Ordering::Relaxed), expected_units);
522 }
523}