Skip to content

1503: feat: (remote) shuffle reader cleanup - #6

Open
martin-augment wants to merge 1 commit into
mainfrom
pr-1503-2026-03-13-06-05-06
Open

1503: feat: (remote) shuffle reader cleanup#6
martin-augment wants to merge 1 commit into
mainfrom
pr-1503-2026-03-13-06-05-06

Conversation

@martin-augment

Copy link
Copy Markdown
Owner

1503: To review by AI

@coderabbitai

coderabbitai Bot commented Mar 13, 2026

Copy link
Copy Markdown

Walkthrough

This pull request refactors the Ballista client configuration and partition fetching mechanism. The BallistaClient struct now stores host and port as public fields initialized during construction, and the fetch_partition method signature is updated to remove explicit host and port parameters, instead using the stored values. Configuration management is restructured by renaming gRPC-related accessors from default_grpc_client_* to grpc_client_*, introducing a new BALLISTA_CLIENT_USE_TLS configuration key, and migrating TLS handling from a dedicated BallistaUseTls struct to configuration-based access via BallistaConfig. Multiple modules are updated to utilize these new signatures and configuration accessors.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch pr-1503-2026-03-13-06-05-06
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist

Copy link
Copy Markdown

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request refactors and cleans up the remote shuffle reader client logic within Ballista. The primary goal is to centralize configuration management and simplify the API for fetching partitions. By moving connection details into the BallistaClient and consolidating gRPC client settings, the codebase becomes more consistent and easier to maintain. Additionally, it introduces a new configuration option for enabling TLS connections, enhancing security.

Highlights

  • BallistaClient Refactoring: The BallistaClient now internally stores the host and port, simplifying method signatures by removing redundant host and port parameters from fetch_partition calls.
  • Centralized Configuration for Shuffle Reader: Shuffle reader settings, including maximum concurrent requests, force remote read, and prefer Flight protocol, are now directly retrieved from the BallistaConfig object, streamlining configuration access.
  • gRPC Client Configuration Method Renaming: All default_grpc_client_* methods in BallistaConfig have been renamed to grpc_client_* for consistency and clarity.
  • TLS Configuration Option: A new configuration option, BALLISTA_CLIENT_USE_TLS, has been introduced to control whether client, scheduler, and executor connections use TLS.
  • Simplified Partition Fetching Logic: The PartitionReader trait and PartitionReaderEnum have been removed, leading to a more direct and simplified implementation for fetching local and remote partitions.
Changelog
  • ballista/core/src/client.rs
    • Updated BallistaClient struct to include host and port fields.
    • Modified BallistaClient::try_new to store host and port internally.
    • Simplified BallistaClient::fetch_partition method signature by removing host and port parameters.
  • ballista/core/src/config.rs
    • Removed blank lines between related constant definitions.
    • Added BALLISTA_CLIENT_USE_TLS constant for TLS configuration.
    • Renamed default_grpc_client_max_message_size to grpc_client_max_message_size.
    • Renamed default_grpc_client_connect_timeout_seconds to grpc_client_connect_timeout_seconds.
    • Renamed default_grpc_client_timeout_seconds to grpc_client_timeout_seconds.
    • Renamed default_grpc_client_tcp_keepalive_seconds to grpc_client_tcp_keepalive_seconds.
    • Renamed default_grpc_client_http2_keepalive_interval_seconds to grpc_client_http2_keepalive_interval_seconds.
    • Added client_use_tls getter method to BallistaConfig.
    • Added set_with_prefix method to BallistaConfig for setting configuration values.
    • Updated default_config test to use the new grpc_client_max_message_size method.
  • ballista/core/src/execution_plans/distributed_query.rs
    • Updated calls to config.default_grpc_client_max_message_size() to config.grpc_client_max_message_size().
    • Removed host and port parameters from fetch_partition calls.
  • ballista/core/src/execution_plans/shuffle_reader.rs
    • Reordered use statements for better organization.
    • Simplified ShuffleReaderExec::execute by directly using BallistaConfig for shuffle reader settings.
    • Modified send_fetch_partitions function signature to accept &BallistaConfig.
    • Changed local partition fetching to use spawn_blocking for potentially blocking I/O.
    • Removed the PartitionReader trait and PartitionReaderEnum.
    • Introduced a new_ballista_client helper function.
    • Modified fetch_partition_remote to accept &BallistaConfig and use prefer_flight from the config.
    • Converted fetch_partition_local to a synchronous function.
    • Removed fetch_partition_object_store.
    • Updated test cases to align with the new BallistaConfig usage in send_fetch_partitions.
  • ballista/core/src/extension.rs
    • Updated imports to include BALLISTA_CLIENT_USE_TLS.
    • Modified ballista_grpc_client_max_message_size to call grpc_client_max_message_size on BallistaConfig.
    • Updated with_ballista_use_tls and ballista_use_tls to interact directly with the BallistaConfig extension.
    • Removed the BallistaUseTls wrapper struct.
  • ballista/core/src/utils.rs
    • Updated GrpcClientConfig::from implementation to use the new grpc_client_* method names from BallistaConfig.
    • Updated test cases to reflect the renamed grpc_client_* methods.
  • examples/examples/standalone-substrait.rs
    • Removed host and port parameters from fetch_partition calls.
Activity
  • The pull request was created by martin-augment.
  • The author requested a review by AI.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request provides a good cleanup of the shuffle reader and related configuration handling. The BallistaClient is now more stateful, storing its host and port, which simplifies the fetch_partition API. Configuration handling is also improved by centralizing settings within BallistaConfig and removing separate wrapper structs, leading to more consistent code. The shuffle reader logic itself has been refactored for clarity, for example by removing the PartitionReader trait abstraction.

I have one suggestion regarding a performance optimization in the shuffle reader to avoid expensive cloning of BallistaConfig in a loop.


spawned_tasks.push(SpawnedTask::spawn({
// TODO: make BallistaConfig cheaper to clone
let config = config.clone();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

As noted in the TODO on the preceding line, cloning BallistaConfig here for every remote partition can be expensive due to the inner HashMap. This can be optimized by wrapping BallistaConfig in an Arc and cloning the Arc instead, which is a cheap reference count increment.

You could change send_fetch_partitions to accept an Arc<BallistaConfig>:

fn send_fetch_partitions(
    partition_locations: Vec<PartitionLocation>,
    config: Arc<BallistaConfig>,
    customize_endpoint: Option<Arc<BallistaConfigGrpcEndpoint>>,
) -> AbortableReceiverStream { /* ... */ }

Then, update the call sites to wrap the config in an Arc.

In ShuffleReaderExec::execute:

let config = Arc::new(context.session_config().ballista_config());
let response_receiver =
    send_fetch_partitions(partition_locations, config, customize_endpoint);

In tests::test_send_fetch_partitions:

let response_receiver = send_fetch_partitions(partition_locations, Arc::new(config), None);

This would make the clone inside the spawned task a cheap Arc clone, addressing the performance concern.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

DataFusionError::Internal("Received empty partition id".to_owned())
})?;
let host = metadata.host.as_str();
let port = metadata.port as u16;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action contains proxy address instead of executor address

Medium Severity

When a flight proxy is configured, get_client_host_port returns the proxy's address (scheduler or external proxy), not the executor's. The old code explicitly passed metadata.host and metadata.port to fetch_partition for the Action::FetchPartition payload, keeping the executor's real address. Now BallistaClient stores the connection target (client_host/client_port) as self.host/self.port, and fetch_partition uses those in the action. This means the Action::FetchPartition host/port fields contain the proxy address instead of the executor's actual address in proxy scenarios.

Additional Locations (1)
Fix in Cursor Fix in Web

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

value:useful; category:bug; feedback: The Bugbot AI reviewer is correct! The changes in the Pull Request propose to use the flight proxy for some functionalities and still use direct connection to the executor for others. In this particular case the connection should be direct/sticky, because a specific executor is responsible for a given partition.

@claude

claude Bot commented Mar 13, 2026

Copy link
Copy Markdown

PR Review: feat: (remote) shuffle reader cleanup

Overall this is a solid refactoring PR. A few items to address before merging.

ISSUES

  1. Potential behavioral change in distributed_query.rs (proxy scenario)

BallistaClient::try_new is called with client_host/client_port, which may be a proxy address when a flight proxy is configured. Previously the host/port passed to fetch_partition came from metadata.host/metadata.port -- the actual executor address. Now self.host/self.port stored in the client are the connection address, and these get embedded in Action::FetchPartition.

If the executor uses those fields to identify its own partitions, passing the proxy address instead of the executor address could be incorrect. Worth verifying this semantic difference is intentional or harmless. The shuffle reader path is unaffected since it connects directly to metadata.host/metadata.port.

  1. Commented-out code in config.rs (set_with_prefix)

The dead comment inside set_with_prefix should be removed. It also makes the method name misleading -- the method does not actually add any prefix. Renaming to set or set_config_value would be clearer.

  1. Doc comment style in config.rs

A blank line is missing between get_f64_setting and the doc comment for set_with_prefix. Rust convention also starts doc comments with a capital letter.

POSITIVES

  • spawn_blocking for local file reads: blocking I/O should not run on the async executor. Good catch.
  • Removing PartitionReaderEnum and PartitionReader trait: ObjectStoreRemote was a stub returning NotImplemented. Clean removal.
  • Removing BallistaUseTls: consolidating TLS config into BallistaConfig removes a redundant extension type and keeps settings in one place.
  • Method renaming: dropping the default_ prefix (e.g. default_grpc_client_connect_timeout_seconds to grpc_client_connect_timeout_seconds) improves readability.
  • send_fetch_partitions signature: collapsing six positional arguments into &BallistaConfig + customize_endpoint makes call sites much easier to follow.
  • BallistaClient storing host/port: eliminating the redundant parameters from fetch_partition is the right direction.

MINOR NIT

new_ballista_client is a thin wrapper around BallistaClient::try_new that only extracts two config values. Consider inlining it into fetch_partition_remote to reduce indirection, or keeping it as a seam for future connection pooling (the existing TODO comment suggests this is future work).

@augmentcode

augmentcode Bot commented Mar 13, 2026

Copy link
Copy Markdown
🤖 Augment PR Summary

Summary: Refactors remote shuffle partition reads and related client configuration to reduce duplicated plumbing and centralize settings.

Changes:

  • Refactors `ShuffleReaderExec` to pass a `BallistaConfig` through the remote/local partition fetch pipeline
  • Simplifies partition fetching by splitting local vs remote readers and introducing `new_ballista_client`
  • Extends `BallistaClient` to store `host`/`port` and removes per-call host/port parameters for `fetch_partition`
  • Adds `ballista.client.use_tls` to `BallistaConfig` and routes TLS settings through Ballista’s config extension
  • Renames several `default_grpc_client_*` accessors to `grpc_client_*` and updates call sites/tests

Technical Notes: Remote shuffle reads now consistently derive concurrency/message-size/TLS/transport preferences from BallistaConfig, and local reads are dispatched via a blocking task for file-open/stream setup.

🤖 Was this summary useful? React with 👍 or 👎

@augmentcode augmentcode Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review completed. 2 suggestions posted.

Fix All in Augment

Comment augment review to trigger a new review at any time.

path: path.to_owned(),
host: host.to_owned(),
port,
host: self.host.to_owned(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FetchPartition.host/port are used by the scheduler flight proxy (flight_proxy_service.rs) to connect onward to the executor; using self.host/self.port here looks problematic when the client is connected to a proxy/scheduler endpoint. In those cases the ticket may embed the proxy’s host/port and cause the proxy to forward back to itself rather than the executor.

Severity: high

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

value:useful; category:bug; feedback: The Augment AI reviewer is correct! The changes in the Pull Request propose to use the flight proxy for some functionalities and still use direct connection to the executor for others. In this particular case the connection should be direct/sticky, because a specific executor is responsible for a given partition.

&location.path,
host,
port,
flight_transport,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In FlightProxy::Local(true) / FlightProxy::External modes get_client_host_port makes the Flight client connect to the proxy/scheduler, but the FetchPartition ticket still needs the executor’s metadata.host/port for proxy routing. With the BallistaClient::fetch_partition signature change, this call path seems likely to produce tickets that point back at the proxy/scheduler (potential forwarding loop/failure).

Severity: high

Other Locations
  • examples/examples/standalone-substrait.rs:419

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

value:useful; category:bug; feedback: The Augment AI reviewer is correct! The changes in the Pull Request propose to use the flight proxy for some functionalities and still use direct connection to the executor for others. In this particular case the connection should be direct/sticky, because a specific executor is responsible for a given partition.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
ballista/core/src/execution_plans/shuffle_reader.rs (1)

440-456: Config cloning per remote partition may be expensive at scale.

The config.clone() at line 442 occurs for each remote partition. For queries with many partitions, this could add overhead. The existing TODO acknowledges this.

Consider extracting the needed config values (max_message_size, use_tls, prefer_flight) into a small struct before the loop to avoid repeated cloning:

struct RemoteFetchConfig {
    max_message_size: usize,
    use_tls: bool,
    prefer_flight: bool,
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ballista/core/src/execution_plans/shuffle_reader.rs` around lines 440 - 456,
The loop currently calls config.clone() for every spawned task which is
expensive; extract only the needed fields (e.g., max_message_size, use_tls,
prefer_flight) into a small Copy/Clone struct (e.g., RemoteFetchConfig) outside
the loop and capture that in the SpawnedTask::spawn closure instead of cloning
the full BallistaConfig; update the async block passed to SpawnedTask::spawn to
use the new RemoteFetchConfig when calling fetch_partition_remote (or pass it
through to a modified fetch_partition_remote signature) and remove the
per-iteration config.clone() call so only the small struct is moved into each
task.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@ballista/core/src/config.rs`:
- Around line 430-446: The set_with_prefix function currently only checks key
existence (use Self::valid_entries and entries.contains_key) but does not
validate that value parses to the declared type, which later causes panics in
callers like get_usize_setting that unwrap parsed values; update set_with_prefix
to fetch the entry from valid_entries (e.g., entries.get(key)), attempt to parse
the provided value into the entry's declared type using the same
parsing/validation logic used by with_settings(), and only insert into
self.settings if parsing succeeds; on parse failure return a config_err! with a
clear message rather than inserting the invalid string. Ensure you reference and
reuse the same parsing helper/path used by with_settings() to keep behavior
consistent.

---

Nitpick comments:
In `@ballista/core/src/execution_plans/shuffle_reader.rs`:
- Around line 440-456: The loop currently calls config.clone() for every spawned
task which is expensive; extract only the needed fields (e.g., max_message_size,
use_tls, prefer_flight) into a small Copy/Clone struct (e.g., RemoteFetchConfig)
outside the loop and capture that in the SpawnedTask::spawn closure instead of
cloning the full BallistaConfig; update the async block passed to
SpawnedTask::spawn to use the new RemoteFetchConfig when calling
fetch_partition_remote (or pass it through to a modified fetch_partition_remote
signature) and remove the per-iteration config.clone() call so only the small
struct is moved into each task.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: af13e352-29ea-407b-b25f-f9f7b2380187

📥 Commits

Reviewing files that changed from the base of the PR and between 9e8e030 and 1307392.

📒 Files selected for processing (7)
  • ballista/core/src/client.rs
  • ballista/core/src/config.rs
  • ballista/core/src/execution_plans/distributed_query.rs
  • ballista/core/src/execution_plans/shuffle_reader.rs
  • ballista/core/src/extension.rs
  • ballista/core/src/utils.rs
  • examples/examples/standalone-substrait.rs
💤 Files with no reviewable changes (1)
  • examples/examples/standalone-substrait.rs

Comment on lines +430 to +446
/// sets the configuration value where key starts with ballista
/// prefix.
pub fn set_with_prefix(
&mut self,
key: &str,
value: &str,
) -> datafusion::error::Result<()> {
let entries = Self::valid_entries();
//let k = format!("{}.{key}", BallistaConfig::PREFIX);

if entries.contains_key(key) {
self.settings.insert(key.to_string(), value.to_string());
Ok(())
} else {
config_err!("configuration key `{}` does not exist", key)
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

set_with_prefix does not validate value types.

Unlike with_settings() which validates that values can be parsed as their declared types, set_with_prefix only validates key existence. Setting an invalid value (e.g., "not_a_number" for a UInt64 config) will cause a panic when the value is later accessed via get_usize_setting() which calls .unwrap().

Consider adding type validation:

🛡️ Proposed fix to add value validation
 pub fn set_with_prefix(
     &mut self,
     key: &str,
     value: &str,
 ) -> datafusion::error::Result<()> {
     let entries = Self::valid_entries();
 
     if entries.contains_key(key) {
+        let entry = entries.get(key).unwrap();
+        Self::parse_value(value, entry.data_type.clone())
+            .map_err(|e| datafusion::error::DataFusionError::Configuration(
+                format!("Invalid value '{value}' for key '{key}': {e}")
+            ))?;
         self.settings.insert(key.to_string(), value.to_string());
         Ok(())
     } else {
         config_err!("configuration key `{}` does not exist", key)
     }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ballista/core/src/config.rs` around lines 430 - 446, The set_with_prefix
function currently only checks key existence (use Self::valid_entries and
entries.contains_key) but does not validate that value parses to the declared
type, which later causes panics in callers like get_usize_setting that unwrap
parsed values; update set_with_prefix to fetch the entry from valid_entries
(e.g., entries.get(key)), attempt to parse the provided value into the entry's
declared type using the same parsing/validation logic used by with_settings(),
and only insert into self.settings if parsing succeeds; on parse failure return
a config_err! with a clear message rather than inserting the invalid string.
Ensure you reference and reuse the same parsing helper/path used by
with_settings() to keep behavior consistent.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

value:useful; category:bug; feedback: The CodeRabbit AI reviewer is correct! The new method does not validate the provided values and this may lead to problems later when these values need to be used. Prevents storing invalid values.

@martin-augment

Copy link
Copy Markdown
Owner Author

440-456: Config cloning per remote partition may be expensive at scale.

The config.clone() at line 442 occurs for each remote partition. For queries with many partitions, this could add overhead. The existing TODO acknowledges this.

Consider extracting the needed config values (max_message_size, use_tls, prefer_flight) into a small struct before the loop to avoid repeated cloning:

struct RemoteFetchConfig {
    max_message_size: usize,
    use_tls: bool,
    prefer_flight: bool,
}

value:good-to-have; category:bug; feedback: The CodeRabbit AI is correct! Introducing RemoteFetchConfig will minimize the size of the copied/cloned fields. Another option is to use std::sync::Arc that uses reference counting.

@martin-augment

Copy link
Copy Markdown
Owner Author
  1. Potential behavioral change in distributed_query.rs (proxy scenario)

BallistaClient::try_new is called with client_host/client_port, which may be a proxy address when a flight proxy is configured. Previously the host/port passed to fetch_partition came from metadata.host/metadata.port -- the actual executor address. Now self.host/self.port stored in the client are the connection address, and these get embedded in Action::FetchPartition.

If the executor uses those fields to identify its own partitions, passing the proxy address instead of the executor address could be incorrect. Worth verifying this semantic difference is intentional or harmless. The shuffle reader path is unaffected since it connects directly to metadata.host/metadata.port.

value:useful; category:bug; feedback: The Claude AI reviewer is correct! The changes in the Pull Request propose to use the flight proxy for some functionalities and still use direct connection to the executor for others. In this particular case the connection should be direct/sticky, because a specific executor is responsible for a given partition.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants