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

milenkovicm pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/datafusion-ballista.git


The following commit(s) were added to refs/heads/main by this push:
     new 59cd7528 feat: Feat force shuffle reader to read all files using arrow 
flight client (#1310)
59cd7528 is described below

commit 59cd7528415d57145609f8b98f8e4d0bf722fd87
Author: Marko Milenković <[email protected]>
AuthorDate: Wed Sep 10 16:52:15 2025 +0100

    feat: Feat force shuffle reader to read all files using arrow flight client 
(#1310)
    
    * add ability to force shuffle read to fetch all files
    like they are remote.
    
    new configuration option has been exposed:
    `ballista.shuffle.force_remote_read=false`
    
    * minor test setup issue
---
 ballista/client/tests/common/mod.rs                | 12 +++-
 ballista/client/tests/context_checks.rs            | 58 ++++++++++++++++
 ballista/core/src/config.rs                        | 17 +++++
 .../core/src/execution_plans/shuffle_reader.rs     | 77 ++++++++++++++++++++--
 ballista/core/src/extension.rs                     | 40 ++++++++++-
 5 files changed, 194 insertions(+), 10 deletions(-)

diff --git a/ballista/client/tests/common/mod.rs 
b/ballista/client/tests/common/mod.rs
index 800c21ea..85cb3fbc 100644
--- a/ballista/client/tests/common/mod.rs
+++ b/ballista/client/tests/common/mod.rs
@@ -236,13 +236,21 @@ pub async fn remote_context() -> SessionContext {
 
 #[allow(dead_code)]
 pub async fn standalone_context_with_state() -> SessionContext {
-    let state = SessionStateBuilder::new().with_default_features().build();
+    let config = SessionConfig::new_with_ballista();
+    let state = SessionStateBuilder::new()
+        .with_config(config)
+        .with_default_features()
+        .build();
     SessionContext::standalone_with_state(state).await.unwrap()
 }
 
 #[allow(dead_code)]
 pub async fn remote_context_with_state() -> SessionContext {
-    let state = SessionStateBuilder::new().with_default_features().build();
+    let config = SessionConfig::new_with_ballista();
+    let state = SessionStateBuilder::new()
+        .with_config(config)
+        .with_default_features()
+        .build();
     let (host, port) = setup_test_cluster_with_state(state.clone()).await;
     SessionContext::remote_with_state(&format!("df://{host}:{port}"), state)
         .await
diff --git a/ballista/client/tests/context_checks.rs 
b/ballista/client/tests/context_checks.rs
index a936f143..a4392f77 100644
--- a/ballista/client/tests/context_checks.rs
+++ b/ballista/client/tests/context_checks.rs
@@ -814,4 +814,62 @@ mod supported {
 
         Ok(())
     }
+
+    #[rstest]
+    #[case::standalone(standalone_context())]
+    #[case::remote(remote_context())]
+    #[tokio::test]
+    async fn should_force_local_read(
+        #[future(awt)]
+        #[case]
+        ctx: SessionContext,
+        test_data: String,
+    ) -> datafusion::error::Result<()> {
+        ctx.register_parquet(
+            "test",
+            &format!("{test_data}/alltypes_plain.parquet"),
+            Default::default(),
+        )
+        .await?;
+
+        ctx.sql("SET ballista.shuffle.force_remote_read = true")
+            .await?
+            .show()
+            .await?;
+
+        let result = ctx
+            .sql("select name, value from information_schema.df_settings where 
name like 'ballista.shuffle.force_remote_read' order by name limit 1")
+            .await?
+            .collect()
+            .await?;
+
+        let expected = [
+            "+------------------------------------+-------+",
+            "| name                               | value |",
+            "+------------------------------------+-------+",
+            "| ballista.shuffle.force_remote_read | true  |",
+            "+------------------------------------+-------+",
+        ];
+
+        assert_batches_eq!(expected, &result);
+
+        let expected = [
+            "+------------+----------+",
+            "| string_col | count(*) |",
+            "+------------+----------+",
+            "| 30         | 1        |",
+            "| 31         | 2        |",
+            "+------------+----------+",
+        ];
+
+        let result = ctx
+            .sql("select string_col, count(*) from test where id > 4 group by 
string_col order by string_col")
+            .await?
+            .collect()
+            .await?;
+
+        assert_batches_eq!(expected, &result);
+
+        Ok(())
+    }
 }
diff --git a/ballista/core/src/config.rs b/ballista/core/src/config.rs
index b5b0ea23..823d1d16 100644
--- a/ballista/core/src/config.rs
+++ b/ballista/core/src/config.rs
@@ -34,6 +34,8 @@ pub const BALLISTA_GRPC_CLIENT_MAX_MESSAGE_SIZE: &str =
     "ballista.grpc_client_max_message_size";
 pub const BALLISTA_SHUFFLE_READER_MAX_REQUESTS: &str =
     "ballista.shuffle.max_concurrent_read_requests";
+pub const BALLISTA_SHUFFLE_READER_FORCE_REMOTE_READ: &str =
+    "ballista.shuffle.force_remote_read";
 
 pub type ParseResult<T> = result::Result<T, String>;
 use std::sync::LazyLock;
@@ -54,6 +56,11 @@ static CONFIG_ENTRIES: LazyLock<HashMap<String, 
ConfigEntry>> = LazyLock::new(||
                          "Maximum concurrent requests shuffle reader can 
process".to_string(),
                          DataType::UInt64,
                          Some((64).to_string())),
+        ConfigEntry::new(BALLISTA_SHUFFLE_READER_FORCE_REMOTE_READ.to_string(),
+                         "Forces the shuffle reader to always read partitions 
via the Arrow Flight client, even when partitions are local to the 
node.".to_string(),
+                         DataType::Boolean,
+                         Some((false).to_string())),
+
     ];
     entries
         .into_iter()
@@ -175,6 +182,16 @@ impl BallistaConfig {
         self.get_usize_setting(BALLISTA_SHUFFLE_READER_MAX_REQUESTS)
     }
 
+    /// Forces the shuffle reader to always read partitions via the Arrow 
Flight client,
+    /// even when partitions are local to the node.
+    ///
+    /// Enabling forced remote read may significantly reduce performance,
+    /// as all partition reads will go through the Arrow Flight client even 
for local data.
+    /// Use only when necessary, like in tests
+    pub fn shuffle_reader_force_remote_read(&self) -> bool {
+        self.get_bool_setting(BALLISTA_SHUFFLE_READER_FORCE_REMOTE_READ)
+    }
+
     fn get_usize_setting(&self, key: &str) -> usize {
         if let Some(v) = self.settings.get(key) {
             // infallible because we validate all configs in the constructor
diff --git a/ballista/core/src/execution_plans/shuffle_reader.rs 
b/ballista/core/src/execution_plans/shuffle_reader.rs
index b0c0f04f..53624d17 100644
--- a/ballista/core/src/execution_plans/shuffle_reader.rs
+++ b/ballista/core/src/execution_plans/shuffle_reader.rs
@@ -160,6 +160,14 @@ impl ExecutionPlan for ShuffleReaderExec {
         let max_request_num =
             config.ballista_shuffle_reader_maximum_concurrent_requests();
         let max_message_size = config.ballista_grpc_client_max_message_size();
+        let force_remote_read = 
config.ballista_shuffle_reader_force_remote_read();
+
+        if force_remote_read {
+            debug!(
+            "All shuffle partitions will be read as remote partitions! To 
disable this behavior set: `{}=false`",
+            crate::config::BALLISTA_SHUFFLE_READER_FORCE_REMOTE_READ
+        );
+        }
 
         log::debug!(
             "ShuffleReaderExec::execute({task_id}) max_request_num: 
{max_request_num}, max_message_size: {max_message_size}"
@@ -180,8 +188,12 @@ impl ExecutionPlan for ShuffleReaderExec {
             .collect();
         // Shuffle partitions for evenly send fetching partition requests to 
avoid hot executors within multiple tasks
         partition_locations.shuffle(&mut rng());
-        let response_receiver =
-            send_fetch_partitions(partition_locations, max_request_num, 
max_message_size);
+        let response_receiver = send_fetch_partitions(
+            partition_locations,
+            max_request_num,
+            max_message_size,
+            force_remote_read,
+        );
 
         let result = RecordBatchStreamAdapter::new(
             Arc::new(self.schema.as_ref().clone()),
@@ -352,18 +364,35 @@ impl Stream for AbortableReceiverStream {
             .map_err(|e| ArrowError::ExternalError(Box::new(e)))
     }
 }
+/// Splits the provided partition locations into local and remote partitions.
+/// Local partitions are read directly from local Arrow IPC files,
+/// while remote partitions are fetched using the Arrow Flight client.
+/// If `force_remote_read` is true, all partitions are treated as remote.
+fn local_remote_read_split(
+    partition_locations: Vec<PartitionLocation>,
+    force_remote_read: bool,
+) -> (Vec<PartitionLocation>, Vec<PartitionLocation>) {
+    if !force_remote_read {
+        partition_locations
+            .into_iter()
+            .partition(check_is_local_location)
+    } else {
+        (vec![], partition_locations)
+    }
+}
 
 fn send_fetch_partitions(
     partition_locations: Vec<PartitionLocation>,
     max_request_num: usize,
     max_message_size: usize,
+    force_remote_read: bool,
 ) -> AbortableReceiverStream {
     let (response_sender, response_receiver) = mpsc::channel(max_request_num);
     let semaphore = Arc::new(Semaphore::new(max_request_num));
     let mut spawned_tasks: Vec<SpawnedTask<()>> = vec![];
-    let (local_locations, remote_locations): (Vec<_>, Vec<_>) = 
partition_locations
-        .into_iter()
-        .partition(check_is_local_location);
+
+    let (local_locations, remote_locations): (Vec<_>, Vec<_>) =
+        local_remote_read_split(partition_locations, force_remote_read);
 
     debug!(
         "local shuffle file counts:{}, remote shuffle file count:{}.",
@@ -854,6 +883,36 @@ mod tests {
         }
     }
 
+    // tests if force remote read configuration option will
+    // qualify all partitions as remote
+    #[tokio::test]
+    async fn test_remote_local_read() {
+        let schema = get_test_partition_schema();
+        let data_array = Int32Array::from(vec![1]);
+        let batch =
+            RecordBatch::try_new(Arc::new(schema.clone()), 
vec![Arc::new(data_array)])
+                .unwrap();
+        let tmp_dir = tempdir().unwrap();
+        let file_path = tmp_dir.path().join("shuffle_data");
+        let file = File::create(&file_path).unwrap();
+        let mut writer = StreamWriter::try_new(file, &schema).unwrap();
+        writer.write(&batch).unwrap();
+        writer.finish().unwrap();
+
+        let partition_locations =
+            get_test_partition_locations(1, 
file_path.to_str().unwrap().to_string());
+
+        let (local, remote) = 
local_remote_read_split(partition_locations.clone(), false);
+
+        assert!(!local.is_empty());
+        assert!(remote.is_empty());
+
+        let (local, remote) = local_remote_read_split(partition_locations, 
true);
+
+        assert!(local.is_empty());
+        assert!(!remote.is_empty());
+    }
+
     async fn test_send_fetch_partitions(max_request_num: usize, partition_num: 
usize) {
         let schema = get_test_partition_schema();
         let data_array = Int32Array::from(vec![1]);
@@ -872,8 +931,12 @@ mod tests {
             file_path.to_str().unwrap().to_string(),
         );
 
-        let response_receiver =
-            send_fetch_partitions(partition_locations, max_request_num, 4 * 
1024 * 1024);
+        let response_receiver = send_fetch_partitions(
+            partition_locations,
+            max_request_num,
+            4 * 1024 * 1024,
+            false,
+        );
 
         let stream = RecordBatchStreamAdapter::new(
             Arc::new(schema),
diff --git a/ballista/core/src/extension.rs b/ballista/core/src/extension.rs
index f1c0c200..5c4ba944 100644
--- a/ballista/core/src/extension.rs
+++ b/ballista/core/src/extension.rs
@@ -17,7 +17,8 @@
 
 use crate::config::{
     BallistaConfig, BALLISTA_GRPC_CLIENT_MAX_MESSAGE_SIZE, BALLISTA_JOB_NAME,
-    BALLISTA_SHUFFLE_READER_MAX_REQUESTS, BALLISTA_STANDALONE_PARALLELISM,
+    BALLISTA_SHUFFLE_READER_FORCE_REMOTE_READ, 
BALLISTA_SHUFFLE_READER_MAX_REQUESTS,
+    BALLISTA_STANDALONE_PARALLELISM,
 };
 use crate::planner::BallistaQueryPlanner;
 use crate::serde::protobuf::KeyValuePair;
@@ -95,6 +96,21 @@ pub trait SessionConfigExt {
 
     /// Returns parallelism of standalone cluster
     fn ballista_standalone_parallelism(&self) -> usize;
+
+    /// Forces the shuffle reader to always read partitions via the Arrow 
Flight client,
+    /// even when partitions are local to the node.
+    fn ballista_shuffle_reader_force_remote_read(&self) -> bool;
+    /// Forces the shuffle reader to always read partitions via the Arrow 
Flight client,
+    /// even when partitions are local to the node.
+    ///
+    /// Enabling forced remote read may significantly reduce performance,
+    /// as all partition reads will go through the Arrow Flight client even 
for local data.
+    /// Use only when necessary, like in tests
+    fn with_ballista_shuffle_reader_force_remote_read(
+        self,
+        force_remote_read: bool,
+    ) -> Self;
+
     /// Sets parallelism of standalone cluster
     ///
     /// This option to be used to configure standalone session context
@@ -319,6 +335,28 @@ impl SessionConfigExt for SessionConfig {
                 .set_usize(BALLISTA_SHUFFLE_READER_MAX_REQUESTS, max_requests)
         }
     }
+
+    fn ballista_shuffle_reader_force_remote_read(&self) -> bool {
+        self.options()
+            .extensions
+            .get::<BallistaConfig>()
+            .map(|c| c.shuffle_reader_force_remote_read())
+            .unwrap_or_else(|| {
+                BallistaConfig::default().shuffle_reader_force_remote_read()
+            })
+    }
+
+    fn with_ballista_shuffle_reader_force_remote_read(
+        self,
+        force_remote_read: bool,
+    ) -> Self {
+        if self.options().extensions.get::<BallistaConfig>().is_some() {
+            self.set_bool(BALLISTA_SHUFFLE_READER_FORCE_REMOTE_READ, 
force_remote_read)
+        } else {
+            self.with_option_extension(BallistaConfig::default())
+                .set_bool(BALLISTA_SHUFFLE_READER_FORCE_REMOTE_READ, 
force_remote_read)
+        }
+    }
 }
 
 impl SessionConfigHelperExt for SessionConfig {


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to