blob: c014ea14b9def33b72df09de61a4650e4d5c281e [file]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
//! [`PlanRunner`] for running [`OutputPlan`]s.
use crate::csv::*;
use crate::generate::{generate_in_chunks, Source};
use crate::output_plan::{OutputLocation, OutputPlan};
use crate::parquet::generate_parquet;
use crate::s3_writer::S3Writer;
use crate::tbl::*;
use crate::{OutputFormat, Table, WriterSink};
use log::{debug, info};
use spatialbench::generators::{
BuildingGenerator, CustomerGenerator, DriverGenerator, TripGenerator, VehicleGenerator,
};
use spatialbench_arrow::{
BuildingArrow, CustomerArrow, DriverArrow, RecordBatchIterator, TripArrow, VehicleArrow,
};
use std::io;
use std::io::BufWriter;
use std::sync::Arc;
use tokio::task::{JoinError, JoinSet};
/// Runs multiple [`OutputPlan`]s in parallel, managing the number of threads
/// used to run them.
#[derive(Debug)]
pub struct PlanRunner {
plans: Vec<OutputPlan>,
num_threads: usize,
}
impl PlanRunner {
/// Create a new [`PlanRunner`] with the given plans and number of threads.
pub fn new(plans: Vec<OutputPlan>, num_threads: usize) -> Self {
Self { plans, num_threads }
}
/// Run all the plans in the runner.
pub async fn run(self) -> Result<(), io::Error> {
debug!(
"Running {} plans with {} threads...",
self.plans.len(),
self.num_threads
);
let Self {
mut plans,
num_threads,
} = self;
// Sort the plans by the number of parts so the largest are first
plans.sort_unstable_by(|a, b| {
let a_cnt = a.chunk_count();
let b_cnt = b.chunk_count();
a_cnt.cmp(&b_cnt)
});
// Do the actual work in parallel, using a worker queue
let mut worker_queue = WorkerQueue::new(num_threads);
while let Some(plan) = plans.pop() {
worker_queue.schedule_plan(plan).await?;
}
worker_queue.join_all().await
}
}
/// Manages worker tasks, limiting the number of total outstanding threads
/// to some fixed number
///
/// The runner executes each plan with a number of threads equal to the
/// number of parts in the plan, but no more than the total number of
/// threads specified when creating the runner. If a plan does not need all
/// the threads, the remaining threads are used to run other plans.
///
/// This is important to keep all cores busy for smaller tables that may not
/// have sufficient parts to keep all threads busy (see [`GenerationPlan`]
/// for more details), but not schedule more tasks than we have threads for.
///
/// Scheduling too many tasks requires more memory and leads to context
/// switching overhead, which can slow down the generation process.
///
/// [`GenerationPlan`]: crate::plan::GenerationPlan
struct WorkerQueue {
join_set: JoinSet<io::Result<usize>>,
/// Current number of threads available to commit
available_threads: usize,
}
impl WorkerQueue {
pub fn new(max_threads: usize) -> Self {
assert!(max_threads > 0);
Self {
join_set: JoinSet::new(),
available_threads: max_threads,
}
}
/// Spawns a task to run the plan with as many threads as possible
/// without exceeding the maximum number of threads.
///
/// If there are no threads available, it will wait for one to finish
/// before spawning the new task.
///
/// Note this algorithm does not guarantee that all threads are always busy,
/// but it should be good enough for most cases. For best thread utilization
/// spawn the largest plans first.
pub async fn schedule_plan(&mut self, plan: OutputPlan) -> io::Result<()> {
debug!("scheduling plan {plan}");
loop {
if self.available_threads == 0 {
debug!("no threads left, wait for one to finish");
let Some(result) = self.join_set.join_next().await else {
return Err(io::Error::other(
"Internal Error No more tasks to wait for, but had no threads",
));
};
self.available_threads += task_result(result)?;
continue; // look for threads again
}
// Check for any other jobs done so we can reuse their threads
if let Some(result) = self.join_set.try_join_next() {
self.available_threads += task_result(result)?;
continue;
}
debug_assert!(
self.available_threads > 0,
"should have at least one thread to continue"
);
// figure out how many threads to allocate to this plan. Each plan
// can use up to `part_count` threads.
let chunk_count = plan.chunk_count();
let num_plan_threads = self.available_threads.min(chunk_count);
// run the plan in a separate task, which returns the number of threads it used
debug!("Spawning plan {plan} with {num_plan_threads} threads");
self.join_set
.spawn(async move { run_plan(plan, num_plan_threads).await });
self.available_threads -= num_plan_threads;
return Ok(());
}
}
// Wait for all tasks to finish
pub async fn join_all(mut self) -> io::Result<()> {
debug!("Waiting for tasks to finish...");
while let Some(result) = self.join_set.join_next().await {
task_result(result)?;
}
debug!("Tasks finished.");
Ok(())
}
}
/// unwraps the result of a task and converts it to an `io::Result<T>`.
fn task_result<T>(result: Result<io::Result<T>, JoinError>) -> io::Result<T> {
result.map_err(|e| io::Error::other(format!("Task Panic: {e}")))?
}
/// Run a single [`OutputPlan`]
async fn run_plan(plan: OutputPlan, num_threads: usize) -> io::Result<usize> {
match plan.table() {
Table::Building => run_building_plan(plan, num_threads).await,
Table::Vehicle => run_vehicle_plan(plan, num_threads).await,
Table::Driver => run_driver_plan(plan, num_threads).await,
Table::Customer => run_customer_plan(plan, num_threads).await,
Table::Trip => run_trip_plan(plan, num_threads).await,
Table::Zone => todo!("Zone table is not supported in PlanRunner"),
}
}
/// Writes a CSV/TSV output from the sources
async fn write_file<I>(plan: OutputPlan, num_threads: usize, sources: I) -> Result<(), io::Error>
where
I: Iterator<Item: Source> + 'static,
{
// Since generate_in_chunks already buffers, there is no need to buffer
// again (aka don't use BufWriter here)
match plan.output_location() {
OutputLocation::Stdout => {
let sink = WriterSink::new(io::stdout());
generate_in_chunks(sink, sources, num_threads).await?;
Ok(())
}
OutputLocation::File(path) => {
// if the output already exists, skip running
if path.exists() {
info!("{} already exists, skipping generation", path.display());
return Ok(());
}
// write to a temp file and then rename to avoid partial files
let temp_path = path.with_extension("inprogress");
let file = std::fs::File::create(&temp_path).map_err(|err| {
io::Error::other(format!("Failed to create {temp_path:?}: {err}"))
})?;
let sink = WriterSink::new(file);
generate_in_chunks(sink, sources, num_threads).await?;
// rename the temp file to the final path
std::fs::rename(&temp_path, path).map_err(|e| {
io::Error::other(format!(
"Failed to rename {temp_path:?} to {path:?} file: {e}"
))
})?;
Ok(())
}
OutputLocation::S3 { uri, path, client } => {
info!("Writing to S3: {}", uri);
let s3_writer = S3Writer::with_client(Arc::clone(client), path);
let sink = WriterSink::new(s3_writer);
let sink = generate_in_chunks(sink, sources, num_threads).await?;
sink.into_inner().finish().await?;
Ok(())
}
}
}
/// Generates an output parquet file from the sources
async fn write_parquet<I>(plan: OutputPlan, num_threads: usize, sources: I) -> Result<(), io::Error>
where
I: Iterator<Item: RecordBatchIterator> + 'static,
{
match plan.output_location() {
OutputLocation::Stdout => {
let writer = BufWriter::with_capacity(crate::plan::PARQUET_BUFFER_SIZE, io::stdout());
generate_parquet(writer, sources, num_threads, plan.parquet_compression()).await
}
OutputLocation::File(path) => {
// if the output already exists, skip running
if path.exists() {
info!("{} already exists, skipping generation", path.display());
return Ok(());
}
// write to a temp file and then rename to avoid partial files
let temp_path = path.with_extension("inprogress");
let file = std::fs::File::create(&temp_path).map_err(|err| {
io::Error::other(format!("Failed to create {temp_path:?}: {err}"))
})?;
let writer = BufWriter::with_capacity(crate::plan::PARQUET_BUFFER_SIZE, file);
generate_parquet(writer, sources, num_threads, plan.parquet_compression()).await?;
// rename the temp file to the final path
std::fs::rename(&temp_path, path).map_err(|e| {
io::Error::other(format!(
"Failed to rename {temp_path:?} to {path:?} file: {e}"
))
})?;
Ok(())
}
OutputLocation::S3 { uri, path, client } => {
info!("Writing parquet to S3: {}", uri);
let s3_writer = S3Writer::with_client(Arc::clone(client), path);
generate_parquet(s3_writer, sources, num_threads, plan.parquet_compression()).await
}
}
}
/// macro to create a function for generating a part of a particular able
///
/// Arguments:
/// $FUN_NAME: name of the function to create
/// $GENERATOR: The generator type to use
/// $TBL_SOURCE: The [`Source`] type to use for TBL format
/// $CSV_SOURCE: The [`Source`] type to use for CSV format
/// $PARQUET_SOURCE: The [`RecordBatchIterator`] type to use for Parquet format
macro_rules! define_run {
($FUN_NAME:ident, $GENERATOR:ident, $TBL_SOURCE:ty, $CSV_SOURCE:ty, $PARQUET_SOURCE:ty) => {
async fn $FUN_NAME(plan: OutputPlan, num_threads: usize) -> io::Result<usize> {
use crate::GenerationPlan;
let scale_factor = plan.scale_factor();
info!("Writing {plan} using {num_threads} threads");
/// These interior functions are used to tell the compiler that the lifetime is 'static
/// (when these were closures, the compiler could not figure out the lifetime) and
/// resulted in errors like this:
/// let _ = join_set.spawn(async move {
/// | _____________________^
/// 96 | | run_plan(plan, num_plan_threads).await
/// 97 | | });
/// | |______________^ implementation of `FnOnce` is not general enough
fn tbl_sources(
generation_plan: &GenerationPlan,
scale_factor: f64,
) -> impl Iterator<Item: Source> + 'static {
generation_plan
.clone()
.into_iter()
.map(move |(part, num_parts)| $GENERATOR::new(scale_factor, part, num_parts))
.map(<$TBL_SOURCE>::new)
}
fn csv_sources(
generation_plan: &GenerationPlan,
scale_factor: f64,
) -> impl Iterator<Item: Source> + 'static {
generation_plan
.clone()
.into_iter()
.map(move |(part, num_parts)| $GENERATOR::new(scale_factor, part, num_parts))
.map(<$CSV_SOURCE>::new)
}
fn parquet_sources(
generation_plan: &GenerationPlan,
scale_factor: f64,
) -> impl Iterator<Item: RecordBatchIterator> + 'static {
generation_plan
.clone()
.into_iter()
.map(move |(part, num_parts)| $GENERATOR::new(scale_factor, part, num_parts))
.map(<$PARQUET_SOURCE>::new)
}
// Dispatch to the appropriate output format
match plan.output_format() {
OutputFormat::Tbl => {
let gens = tbl_sources(plan.generation_plan(), scale_factor);
write_file(plan, num_threads, gens).await?
}
OutputFormat::Csv => {
let gens = csv_sources(plan.generation_plan(), scale_factor);
write_file(plan, num_threads, gens).await?
}
OutputFormat::Parquet => {
let gens = parquet_sources(plan.generation_plan(), scale_factor);
write_parquet(plan, num_threads, gens).await?
}
};
Ok(num_threads)
}
};
}
define_run!(
run_trip_plan,
TripGenerator,
TripTblSource,
TripCsvSource,
TripArrow
);
define_run!(
run_building_plan,
BuildingGenerator,
BuildingTblSource,
BuildingCsvSource,
BuildingArrow
);
define_run!(
run_vehicle_plan,
VehicleGenerator,
VehicleTblSource,
VehicleCsvSource,
VehicleArrow
);
define_run!(
run_driver_plan,
DriverGenerator,
DriverTblSource,
DriverCsvSource,
DriverArrow
);
define_run!(
run_customer_plan,
CustomerGenerator,
CustomerTblSource,
CustomerCsvSource,
CustomerArrow
);