This is an automated email from the ASF dual-hosted git repository.

nju_yaho pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/arrow-ballista.git


The following commit(s) were added to refs/heads/master by this push:
     new 913f6759 Make fetch shuffle partition data in parallel (#256)
913f6759 is described below

commit 913f675955adaf6335002ba9a5c56beef5cce56e
Author: yahoNanJing <[email protected]>
AuthorDate: Tue Sep 27 22:43:34 2022 +0800

    Make fetch shuffle partition data in parallel (#256)
    
    * Make fetch shuffle partition data in parallel
    
    * Add unit test
    
    * Fast abort shuffle reader futures when error occurs
    
    Co-authored-by: yangzhong <[email protected]>
---
 ballista/rust/core/Cargo.toml                      |   3 +
 .../core/src/execution_plans/shuffle_reader.rs     | 211 ++++++++++++++++++---
 ballista/rust/executor/src/flight_service.rs       |   9 +-
 3 files changed, 196 insertions(+), 27 deletions(-)

diff --git a/ballista/rust/core/Cargo.toml b/ballista/rust/core/Cargo.toml
index b484047e..e7fea396 100644
--- a/ballista/rust/core/Cargo.toml
+++ b/ballista/rust/core/Cargo.toml
@@ -50,6 +50,7 @@ datafusion-proto = "12.0.0"
 futures = "0.3"
 hashbrown = "0.12"
 
+itertools = "0.10"
 libloading = "0.7.3"
 log = "0.4"
 object_store = "0.5.0"
@@ -59,9 +60,11 @@ parking_lot = "0.12"
 parse_arg = "0.1.3"
 prost = "0.11"
 prost-types = "0.11"
+rand = "0.8"
 serde = { version = "1", features = ["derive"] }
 sqlparser = "0.23"
 tokio = "1.0"
+tokio-stream = { version = "0.1", features = ["net"] }
 tonic = "0.8"
 url = "2.2"
 uuid = { version = "1.0", features = ["v4"] }
diff --git a/ballista/rust/core/src/execution_plans/shuffle_reader.rs 
b/ballista/rust/core/src/execution_plans/shuffle_reader.rs
index 0c153d3e..d0d9a28b 100644
--- a/ballista/rust/core/src/execution_plans/shuffle_reader.rs
+++ b/ballista/rust/core/src/execution_plans/shuffle_reader.rs
@@ -16,8 +16,10 @@
 // under the License.
 
 use std::any::Any;
+use std::collections::HashMap;
 use std::sync::Arc;
 
+#[cfg(not(test))]
 use crate::client::BallistaClient;
 use crate::serde::scheduler::{PartitionLocation, PartitionStats};
 
@@ -25,18 +27,22 @@ use datafusion::arrow::datatypes::SchemaRef;
 
 use datafusion::error::{DataFusionError, Result};
 use datafusion::physical_plan::expressions::PhysicalSortExpr;
-use datafusion::physical_plan::metrics::{
-    ExecutionPlanMetricsSet, MetricBuilder, MetricsSet,
-};
+use datafusion::physical_plan::metrics::{ExecutionPlanMetricsSet, MetricsSet};
 use datafusion::physical_plan::{
     DisplayFormatType, ExecutionPlan, Partitioning, SendableRecordBatchStream, 
Statistics,
 };
-use futures::{StreamExt, TryStreamExt};
+use futures::{Stream, StreamExt, TryStreamExt};
 
-use datafusion::arrow::error::ArrowError;
 use datafusion::execution::context::TaskContext;
+use datafusion::physical_plan::common::AbortOnDropMany;
 use datafusion::physical_plan::stream::RecordBatchStreamAdapter;
-use log::debug;
+use itertools::Itertools;
+use log::{error, info};
+use rand::prelude::SliceRandom;
+use rand::thread_rng;
+use tokio::sync::{mpsc, Semaphore};
+use tokio::task::JoinHandle;
+use tokio_stream::wrappers::ReceiverStream;
 
 /// ShuffleReaderExec reads partitions that have already been materialized by 
a ShuffleWriterExec
 /// being executed by an executor
@@ -102,29 +108,35 @@ impl ExecutionPlan for ShuffleReaderExec {
     fn execute(
         &self,
         partition: usize,
-        _context: Arc<TaskContext>,
+        context: Arc<TaskContext>,
     ) -> Result<SendableRecordBatchStream> {
-        debug!("ShuffleReaderExec::execute({})", partition);
-
-        let fetch_time =
-            MetricBuilder::new(&self.metrics).subset_time("fetch_time", 
partition);
-
-        let locations = self.partition[partition].clone();
-        let stream = locations.into_iter().map(move |p| {
-            let fetch_time = fetch_time.clone();
-            futures::stream::once(async move {
-                let timer = fetch_time.timer();
-                let r = fetch_partition(&p).await;
-                timer.done();
-
-                r.map_err(|e| ArrowError::ExternalError(Box::new(e)))
-            })
-            .try_flatten()
-        });
+        let task_id = context.task_id().unwrap_or_else(|| 
partition.to_string());
+        info!("ShuffleReaderExec::execute({})", task_id);
+
+        // TODO make the maximum size configurable, or make it depends on 
global memory control
+        let max_request_num = 50usize;
+        let mut partition_locations = HashMap::new();
+        for p in &self.partition[partition] {
+            partition_locations
+                .entry(p.executor_meta.id.clone())
+                .or_insert_with(Vec::new)
+                .push(p.clone());
+        }
+        // Sort partitions for evenly send fetching partition requests to 
avoid hot executors within one task
+        let mut partition_locations: Vec<PartitionLocation> = 
partition_locations
+            .into_values()
+            .flat_map(|ps| ps.into_iter().enumerate())
+            .sorted_by(|(p1_idx, _), (p2_idx, _)| Ord::cmp(p1_idx, p2_idx))
+            .map(|(_, p)| p)
+            .collect();
+        // Shuffle partitions for evenly send fetching partition requests to 
avoid hot executors within multiple tasks
+        partition_locations.shuffle(&mut thread_rng());
+        let response_receiver =
+            send_fetch_partitions(partition_locations, max_request_num);
 
         let result = RecordBatchStreamAdapter::new(
             Arc::new(self.schema.as_ref().clone()),
-            futures::stream::iter(stream).flatten(),
+            response_receiver.try_flatten(),
         );
         Ok(Box::pin(result))
     }
@@ -178,6 +190,67 @@ fn stats_for_partitions(
     )
 }
 
+/// Adapter for a tokio ReceiverStream that implements the 
SendableRecordBatchStream interface
+struct AbortableReceiverStream {
+    inner: ReceiverStream<Result<SendableRecordBatchStream>>,
+
+    #[allow(dead_code)]
+    drop_helper: AbortOnDropMany<()>,
+}
+
+impl AbortableReceiverStream {
+    /// Construct a new SendableRecordBatchReceiverStream which will send 
batches of the specified schema from inner
+    pub fn create(
+        rx: tokio::sync::mpsc::Receiver<Result<SendableRecordBatchStream>>,
+        join_handles: Vec<JoinHandle<()>>,
+    ) -> AbortableReceiverStream {
+        let inner = ReceiverStream::new(rx);
+        Self {
+            inner,
+            drop_helper: AbortOnDropMany(join_handles),
+        }
+    }
+}
+
+impl Stream for AbortableReceiverStream {
+    type Item = Result<SendableRecordBatchStream>;
+
+    fn poll_next(
+        mut self: std::pin::Pin<&mut Self>,
+        cx: &mut std::task::Context<'_>,
+    ) -> std::task::Poll<Option<Self::Item>> {
+        self.inner.poll_next_unpin(cx)
+    }
+}
+
+fn send_fetch_partitions(
+    partition_locations: Vec<PartitionLocation>,
+    max_request_num: usize,
+) -> AbortableReceiverStream {
+    let (response_sender, response_receiver) = mpsc::channel(max_request_num);
+    let semaphore = Arc::new(Semaphore::new(max_request_num));
+    let mut join_handles = vec![];
+    for p in partition_locations.into_iter() {
+        let semaphore = semaphore.clone();
+        let response_sender = response_sender.clone();
+        let join_handle = tokio::spawn(async move {
+            // Block if exceeds max request number
+            let permit = semaphore.acquire_owned().await.unwrap();
+            let r = fetch_partition(&p).await;
+            // Block if the channel buffer is full
+            if let Err(e) = response_sender.send(r).await {
+                error!("Fail to send response event to the channel due to {}", 
e);
+            }
+            // Increase semaphore by dropping existing permits.
+            drop(permit);
+        });
+        join_handles.push(join_handle);
+    }
+
+    AbortableReceiverStream::create(response_receiver, join_handles)
+}
+
+#[cfg(not(test))]
 async fn fetch_partition(
     location: &PartitionLocation,
 ) -> Result<SendableRecordBatchStream> {
@@ -203,9 +276,22 @@ async fn fetch_partition(
         .map_err(|e| DataFusionError::Execution(format!("{:?}", e)))
 }
 
+#[cfg(test)]
+async fn fetch_partition(
+    location: &PartitionLocation,
+) -> Result<SendableRecordBatchStream> {
+    tests::fetch_test_partition(location)
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;
+    use crate::serde::scheduler::{ExecutorMetadata, ExecutorSpecification, 
PartitionId};
+    use datafusion::arrow::array::Int32Array;
+    use datafusion::arrow::datatypes::{DataType, Field, Schema};
+    use datafusion::arrow::record_batch::RecordBatch;
+    use datafusion::physical_plan::common;
+    use datafusion::physical_plan::stream::RecordBatchReceiverStream;
 
     #[tokio::test]
     async fn test_stats_for_partitions_empty() {
@@ -274,4 +360,79 @@ mod tests {
 
         assert_eq!(result, exptected);
     }
+
+    #[tokio::test]
+    async fn test_send_fetch_partitions_1() {
+        test_send_fetch_partitions(1, 10).await;
+    }
+
+    #[tokio::test]
+    async fn test_send_fetch_partitions_n() {
+        test_send_fetch_partitions(4, 10).await;
+    }
+
+    async fn test_send_fetch_partitions(max_request_num: usize, partition_num: 
usize) {
+        let schema = get_test_partition_schema();
+        let partition_locations = get_test_partition_locations(partition_num);
+        let response_receiver =
+            send_fetch_partitions(partition_locations, max_request_num);
+
+        let stream = RecordBatchStreamAdapter::new(
+            Arc::new(schema),
+            response_receiver.try_flatten(),
+        );
+
+        let result = common::collect(Box::pin(stream)).await.unwrap();
+        assert_eq!(partition_num, result.len());
+    }
+
+    fn get_test_partition_locations(n: usize) -> Vec<PartitionLocation> {
+        (0..n)
+            .into_iter()
+            .map(|partition_id| PartitionLocation {
+                partition_id: PartitionId {
+                    job_id: "job".to_string(),
+                    stage_id: 1,
+                    partition_id,
+                },
+                executor_meta: ExecutorMetadata {
+                    id: format!("exec{}", partition_id),
+                    host: "localhost".to_string(),
+                    port: 50051,
+                    grpc_port: 50052,
+                    specification: ExecutorSpecification { task_slots: 12 },
+                },
+                partition_stats: Default::default(),
+                path: format!("/tmp/job/1/{}", partition_id),
+            })
+            .collect()
+    }
+
+    pub(crate) fn fetch_test_partition(
+        location: &PartitionLocation,
+    ) -> Result<SendableRecordBatchStream> {
+        let id_array = 
Int32Array::from(vec![location.partition_id.partition_id as i32]);
+        let schema = Arc::new(get_test_partition_schema());
+
+        let batch = RecordBatch::try_new(schema.clone(), 
vec![Arc::new(id_array)])?;
+
+        let (tx, rx) = tokio::sync::mpsc::channel(2);
+
+        // task simply sends data in order but in a separate
+        // thread (to ensure the batches are not available without the
+        // DelayedStream yielding).
+        let join_handle = tokio::task::spawn(async move {
+            println!("Sending batch via delayed stream");
+            if let Err(e) = tx.send(Ok(batch)).await {
+                println!("ERROR batch via delayed stream: {}", e);
+            }
+        });
+
+        // returned stream simply reads off the rx stream
+        Ok(RecordBatchReceiverStream::create(&schema, rx, join_handle))
+    }
+
+    fn get_test_partition_schema() -> Schema {
+        Schema::new(vec![Field::new("id", DataType::Int32, false)])
+    }
 }
diff --git a/ballista/rust/executor/src/flight_service.rs 
b/ballista/rust/executor/src/flight_service.rs
index 82c4f0ae..fde4f045 100644
--- a/ballista/rust/executor/src/flight_service.rs
+++ b/ballista/rust/executor/src/flight_service.rs
@@ -104,10 +104,11 @@ impl FlightService for BallistaFlightService {
 
                 let (tx, rx): (FlightDataSender, FlightDataReceiver) = 
channel(2);
 
+                let file_path = path.to_owned();
                 // Arrow IPC reader does not implement Sync + Send so we need 
to use a channel
                 // to communicate
                 task::spawn(async move {
-                    if let Err(e) = stream_flight_data(reader, tx).await {
+                    if let Err(e) = stream_flight_data(file_path, reader, 
tx).await {
                         warn!("Error streaming results: {:?}", e);
                     }
                 });
@@ -220,6 +221,7 @@ fn create_flight_iter(
 }
 
 async fn stream_flight_data<T>(
+    file_path: String,
     reader: FileReader<T>,
     tx: FlightDataSender,
 ) -> Result<(), Status>
@@ -242,7 +244,10 @@ where
             send_response(&tx, batch).await?;
         }
     }
-    debug!("FetchPartition streamed {} rows", row_count);
+    debug!(
+        "FetchPartition streamed {} rows for file {}",
+        row_count, file_path
+    );
     Ok(())
 }
 

Reply via email to