Skip to content

Commit b1cff83

Browse files
committed
Introduce WorkerTransport + WorkerConnection traits
Promote the previously concrete `WorkerConnection` struct to a pair of traits so embedded executors can plug their own non-Flight transport into `WorkerConnectionPool` without forking the per-operator code paths. - `WorkerConnection`: per-task connection that demultiplexes the underlying transport into one record-batch stream per partition. - `WorkerTransport`: factory that opens a `WorkerConnection` for a given input stage / partition range / target task. The default implementation (`FlightWorkerTransport` / `FlightWorkerConnection`) is the existing Arrow-Flight gRPC code, moved verbatim behind the trait. The pool's slot type changes to `Box<dyn WorkerConnection + Send + Sync>` and `get_or_init_worker_connection` now consults the transport registered on the `TaskContext` via a new `get_distributed_worker_transport` accessor; callers who haven't registered one fall back to a process-wide `FlightWorkerTransport`. Custom transports plug in through `DistributedExt::with_distributed_worker_transport`, mirroring the existing channel- and worker-resolver hooks. Drive-bys: drop the unused `on_metadata` callback from `stream_partition` (every existing caller passed `|_meta| {}`); rename the inner struct to `FlightWorkerConnection` and `WorkerConnection::init` to `FlightWorkerConnection::open`; tighten the connection-slot type into a `ConnectionSlot` alias for clippy. All 190 lib tests + 39 integration tests pass; clippy + cargo fmt clean.
1 parent 69e6813 commit b1cff83

11 files changed

Lines changed: 313 additions & 61 deletions

File tree

src/distributed_ext.rs

Lines changed: 77 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,16 @@ use crate::config_extension_ext::{
22
set_distributed_option_extension, set_distributed_option_extension_from_headers,
33
};
44
use crate::distributed_planner::set_distributed_task_estimator;
5-
use crate::networking::{set_distributed_channel_resolver, set_distributed_worker_resolver};
5+
use crate::networking::{
6+
set_distributed_channel_resolver, set_distributed_worker_resolver,
7+
set_distributed_worker_transport,
8+
};
69
use crate::passthrough_headers::set_passthrough_headers;
710
use crate::protobuf::{set_distributed_user_codec, set_distributed_user_codec_arc};
811
use crate::work_unit_feed::set_distributed_work_unit_feed;
912
use crate::{
1013
ChannelResolver, DistributedConfig, TaskEstimator, WorkUnitFeed, WorkUnitFeedProvider,
11-
WorkerResolver,
14+
WorkerResolver, WorkerTransport,
1215
};
1316
use arrow_ipc::CompressionType;
1417
use datafusion::common::DataFusionError;
@@ -284,6 +287,52 @@ pub trait DistributedExt: Sized {
284287
resolver: T,
285288
);
286289

290+
/// Overrides the [WorkerTransport] used by [crate::worker::WorkerConnectionPool] when opening
291+
/// connections to remote workers. The default transport is the Arrow-Flight gRPC implementation
292+
/// shipped in this crate; embedded executors can plug in their own (e.g. shared-memory queues)
293+
/// without forking the per-operator code paths.
294+
///
295+
/// Example:
296+
///
297+
/// ```ignore
298+
/// # use std::ops::Range;
299+
/// # use std::sync::Arc;
300+
/// # use datafusion::common::Result;
301+
/// # use datafusion::execution::{SessionStateBuilder, TaskContext};
302+
/// # use datafusion::physical_expr_common::metrics::ExecutionPlanMetricsSet;
303+
/// # use datafusion_distributed::{
304+
/// # DistributedExt, Stage, WorkerConnection, WorkerPartitionStream, WorkerTransport,
305+
/// # };
306+
///
307+
/// struct MyTransport;
308+
/// impl WorkerTransport for MyTransport {
309+
/// fn open(
310+
/// &self,
311+
/// _input_stage: &Stage,
312+
/// _target_partitions: Range<usize>,
313+
/// _target_task: usize,
314+
/// _ctx: &Arc<TaskContext>,
315+
/// _metrics: &ExecutionPlanMetricsSet,
316+
/// ) -> Result<Box<dyn WorkerConnection + Send + Sync>> {
317+
/// todo!()
318+
/// }
319+
/// }
320+
///
321+
/// let state = SessionStateBuilder::new()
322+
/// .with_distributed_worker_transport(MyTransport)
323+
/// .build();
324+
/// ```
325+
fn with_distributed_worker_transport<T: WorkerTransport + Send + Sync + 'static>(
326+
self,
327+
transport: T,
328+
) -> Self;
329+
330+
/// Same as [DistributedExt::with_distributed_worker_transport] but with an in-place mutation.
331+
fn set_distributed_worker_transport<T: WorkerTransport + Send + Sync + 'static>(
332+
&mut self,
333+
transport: T,
334+
);
335+
287336
/// Adds a distributed task count estimator. [TaskEstimator]s are executed on each node
288337
/// sequentially until one returns an estimation on the number of tasks that should be
289338
/// used for the stage containing that node.
@@ -619,6 +668,13 @@ impl DistributedExt for SessionConfig {
619668
set_distributed_channel_resolver(self, resolver);
620669
}
621670

671+
fn set_distributed_worker_transport<T: WorkerTransport + Send + Sync + 'static>(
672+
&mut self,
673+
transport: T,
674+
) {
675+
set_distributed_worker_transport(self, transport);
676+
}
677+
622678
fn set_distributed_task_estimator<T: TaskEstimator + Send + Sync + 'static>(
623679
&mut self,
624680
estimator: T,
@@ -756,6 +812,10 @@ impl DistributedExt for SessionConfig {
756812
#[expr($;self)]
757813
fn with_distributed_channel_resolver<T: ChannelResolver + Send + Sync + 'static>(mut self, resolver: T) -> Self;
758814

815+
#[call(set_distributed_worker_transport)]
816+
#[expr($;self)]
817+
fn with_distributed_worker_transport<T: WorkerTransport + Send + Sync + 'static>(mut self, transport: T) -> Self;
818+
759819
#[call(set_distributed_task_estimator)]
760820
#[expr($;self)]
761821
fn with_distributed_task_estimator<T: TaskEstimator + Send + Sync + 'static>(mut self, estimator: T) -> Self;
@@ -849,6 +909,11 @@ impl DistributedExt for SessionStateBuilder {
849909
#[expr($;self)]
850910
fn with_distributed_channel_resolver<T: ChannelResolver + Send + Sync + 'static>(mut self, resolver: T) -> Self;
851911

912+
fn set_distributed_worker_transport<T: WorkerTransport + Send + Sync + 'static>(&mut self, transport: T);
913+
#[call(set_distributed_worker_transport)]
914+
#[expr($;self)]
915+
fn with_distributed_worker_transport<T: WorkerTransport + Send + Sync + 'static>(mut self, transport: T) -> Self;
916+
852917
fn set_distributed_task_estimator<T: TaskEstimator + Send + Sync + 'static>(&mut self, estimator: T);
853918
#[call(set_distributed_task_estimator)]
854919
#[expr($;self)]
@@ -960,6 +1025,11 @@ impl DistributedExt for SessionState {
9601025
#[expr($;self)]
9611026
fn with_distributed_channel_resolver<T: ChannelResolver + Send + Sync + 'static>(mut self, resolver: T) -> Self;
9621027

1028+
fn set_distributed_worker_transport<T: WorkerTransport + Send + Sync + 'static>(&mut self, transport: T);
1029+
#[call(set_distributed_worker_transport)]
1030+
#[expr($;self)]
1031+
fn with_distributed_worker_transport<T: WorkerTransport + Send + Sync + 'static>(mut self, transport: T) -> Self;
1032+
9631033
fn set_distributed_task_estimator<T: TaskEstimator + Send + Sync + 'static>(&mut self, estimator: T);
9641034
#[call(set_distributed_task_estimator)]
9651035
#[expr($;self)]
@@ -1071,6 +1141,11 @@ impl DistributedExt for SessionContext {
10711141
#[expr($;self)]
10721142
fn with_distributed_channel_resolver<T: ChannelResolver + Send + Sync + 'static>(self, resolver: T) -> Self;
10731143

1144+
fn set_distributed_worker_transport<T: WorkerTransport + Send + Sync + 'static>(&mut self, transport: T);
1145+
#[call(set_distributed_worker_transport)]
1146+
#[expr($;self)]
1147+
fn with_distributed_worker_transport<T: WorkerTransport + Send + Sync + 'static>(self, transport: T) -> Self;
1148+
10741149
fn set_distributed_task_estimator<T: TaskEstimator + Send + Sync + 'static>(&mut self, estimator: T);
10751150
#[call(set_distributed_task_estimator)]
10761151
#[expr($;self)]

src/distributed_planner/distributed_config.rs

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
use crate::TaskEstimator;
22
use crate::distributed_planner::task_estimator::CombinedTaskEstimator;
3-
use crate::networking::{ChannelResolverExtension, WorkerResolverExtension};
3+
use crate::networking::{
4+
ChannelResolverExtension, WorkerResolverExtension, WorkerTransportExtension,
5+
};
46
use crate::work_unit_feed::WorkUnitFeedRegistry;
57
use datafusion::common::utils::get_available_parallelism;
68
use datafusion::common::{DataFusionError, extensions_options, not_impl_err, plan_err};
@@ -74,6 +76,10 @@ extensions_options! {
7476
/// [WorkerResolver] implementation that tells the distributed planner information about
7577
/// the available workers ready to execute distributed tasks.
7678
pub(crate) __private_worker_resolver: WorkerResolverExtension, default = WorkerResolverExtension::not_implemented()
79+
/// Optional [crate::WorkerTransport] override used by [crate::worker::WorkerConnectionPool]
80+
/// when opening connections to remote workers. When unset, callers fall back to a process-
81+
/// wide [crate::FlightWorkerTransport].
82+
pub(crate) __private_worker_transport: WorkerTransportExtension, default = WorkerTransportExtension::default()
7783
/// [WorkUnitFeedRegistry] that contains a set of getters that, applied to each node in a
7884
/// plan, will return the [crate::WorkUnitFeed]s present in all nodes.
7985
pub(crate) __private_work_unit_feed_registry: WorkUnitFeedRegistry, default = WorkUnitFeedRegistry::default()
@@ -171,6 +177,22 @@ impl Debug for WorkerResolverExtension {
171177
}
172178
}
173179

180+
impl ConfigField for WorkerTransportExtension {
181+
fn visit<V: Visit>(&self, _: &mut V, _: &str, _: &'static str) {
182+
// nothing to do.
183+
}
184+
185+
fn set(&mut self, _: &str, _: &str) -> datafusion::common::Result<()> {
186+
not_impl_err!("Not implemented")
187+
}
188+
}
189+
190+
impl Debug for WorkerTransportExtension {
191+
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
192+
write!(f, "WorkerTransportExtension")
193+
}
194+
}
195+
174196
impl ConfigField for CombinedTaskEstimator {
175197
fn visit<V: Visit>(&self, _: &mut V, _: &str, _: &'static str) {
176198
//nothing to do.

src/execution_plans/network_broadcast.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -251,7 +251,7 @@ impl ExecutionPlan for NetworkBroadcastExec {
251251
&context,
252252
)?;
253253

254-
let stream = worker_connection.stream_partition(off + partition, |_meta| {})?;
254+
let stream = worker_connection.stream_partition(off + partition)?;
255255
streams.push(stream);
256256
}
257257

src/execution_plans/network_coalesce.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,7 @@ impl ExecutionPlan for NetworkCoalesceExec {
243243
&context,
244244
)?;
245245

246-
let stream = worker_connection.stream_partition(target_partition, |_meta| {})?;
246+
let stream = worker_connection.stream_partition(target_partition)?;
247247

248248
Ok(Box::pin(RecordBatchStreamAdapter::new(
249249
self.schema(),

src/execution_plans/network_shuffle.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,7 @@ impl ExecutionPlan for NetworkShuffleExec {
237237
&context,
238238
)?;
239239

240-
let stream = worker_connection.stream_partition(off + partition, |_meta| {})?;
240+
let stream = worker_connection.stream_partition(off + partition)?;
241241
streams.push(stream);
242242
}
243243

src/lib.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ pub use metrics::{
3737
pub use networking::{
3838
BoxCloneSyncChannel, ChannelResolver, DefaultChannelResolver, WorkerResolver,
3939
create_worker_client, get_distributed_channel_resolver, get_distributed_worker_resolver,
40+
get_distributed_worker_transport,
4041
};
4142
pub use stage::{
4243
DistributedTaskContext, Stage, display_plan_ascii, display_plan_graphviz, explain_analyze,
@@ -48,8 +49,9 @@ pub use worker::generated::worker::worker_service_client::WorkerServiceClient;
4849
pub use worker::generated::worker::worker_service_server::WorkerServiceServer;
4950
pub use worker::generated::worker::{GetWorkerInfoRequest, GetWorkerInfoResponse, TaskKey};
5051
pub use worker::{
51-
DefaultSessionBuilder, MappedWorkerSessionBuilder, MappedWorkerSessionBuilderExt, TaskData,
52-
Worker, WorkerQueryContext, WorkerSessionBuilder,
52+
DefaultSessionBuilder, FlightWorkerTransport, MappedWorkerSessionBuilder,
53+
MappedWorkerSessionBuilderExt, TaskData, Worker, WorkerConnection, WorkerPartitionStream,
54+
WorkerQueryContext, WorkerSessionBuilder, WorkerTransport,
5355
};
5456

5557
pub use observability::{

src/networking/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
mod channel_resolver;
22
mod worker_resolver;
3+
mod worker_transport;
34

45
pub use channel_resolver::{
56
BoxCloneSyncChannel, ChannelResolver, DefaultChannelResolver, create_worker_client,
@@ -9,3 +10,6 @@ pub(crate) use channel_resolver::{ChannelResolverExtension, set_distributed_chan
910

1011
pub use worker_resolver::{WorkerResolver, get_distributed_worker_resolver};
1112
pub(crate) use worker_resolver::{WorkerResolverExtension, set_distributed_worker_resolver};
13+
14+
pub use worker_transport::get_distributed_worker_transport;
15+
pub(crate) use worker_transport::{WorkerTransportExtension, set_distributed_worker_transport};

src/networking/worker_transport.rs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
use crate::DistributedConfig;
2+
use crate::config_extension_ext::set_distributed_option_extension;
3+
use crate::worker::{FlightWorkerTransport, WorkerTransport};
4+
use datafusion::execution::TaskContext;
5+
use datafusion::prelude::SessionConfig;
6+
use std::sync::{Arc, LazyLock};
7+
8+
pub(crate) fn set_distributed_worker_transport(
9+
cfg: &mut SessionConfig,
10+
transport: impl WorkerTransport + Send + Sync + 'static,
11+
) {
12+
let opts = cfg.options_mut();
13+
let extension = WorkerTransportExtension(Some(Arc::new(transport)));
14+
if let Some(distributed_cfg) = opts.extensions.get_mut::<DistributedConfig>() {
15+
distributed_cfg.__private_worker_transport = extension;
16+
} else {
17+
set_distributed_option_extension(
18+
cfg,
19+
DistributedConfig {
20+
__private_worker_transport: extension,
21+
..Default::default()
22+
},
23+
)
24+
}
25+
}
26+
27+
// The default Flight transport carries no per-runtime state (it consults the channel resolver each
28+
// time), so a single process-wide instance is sufficient for callers that have not registered
29+
// their own.
30+
static DEFAULT_WORKER_TRANSPORT: LazyLock<Arc<dyn WorkerTransport + Send + Sync>> =
31+
LazyLock::new(|| Arc::new(FlightWorkerTransport));
32+
33+
/// Returns the [WorkerTransport] registered on the session config attached to `task_ctx`, or a
34+
/// process-wide [FlightWorkerTransport] if none has been set. This is what
35+
/// [crate::worker::WorkerConnectionPool] consults at execute time when opening connections to
36+
/// remote workers.
37+
pub fn get_distributed_worker_transport(
38+
task_ctx: &TaskContext,
39+
) -> Arc<dyn WorkerTransport + Send + Sync> {
40+
let opts = task_ctx.session_config().options();
41+
if let Some(distributed_cfg) = opts.extensions.get::<DistributedConfig>()
42+
&& let Some(t) = &distributed_cfg.__private_worker_transport.0
43+
{
44+
return Arc::clone(t);
45+
}
46+
Arc::clone(&DEFAULT_WORKER_TRANSPORT)
47+
}
48+
49+
#[derive(Clone, Default)]
50+
pub(crate) struct WorkerTransportExtension(
51+
pub(crate) Option<Arc<dyn WorkerTransport + Send + Sync>>,
52+
);

src/worker/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,13 @@ mod single_write_multi_read;
66
mod spawn_select_all;
77
#[cfg(any(test, feature = "integration"))]
88
pub(crate) mod test_utils;
9+
pub(crate) mod transport;
910
mod worker_connection_pool;
1011
mod worker_service;
1112

1213
pub(crate) use single_write_multi_read::SingleWriteMultiRead;
14+
pub use transport::{WorkerConnection, WorkerPartitionStream, WorkerTransport};
15+
pub use worker_connection_pool::FlightWorkerTransport;
1316
pub(crate) use worker_connection_pool::{LocalWorkerContext, WorkerConnectionPool};
1417

1518
pub use session_builder::{

src/worker/transport.rs

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
use crate::stage::RemoteStage;
2+
use datafusion::arrow::array::RecordBatch;
3+
use datafusion::common::Result;
4+
use datafusion::execution::TaskContext;
5+
use datafusion::physical_expr_common::metrics::ExecutionPlanMetricsSet;
6+
use futures::Stream;
7+
use std::ops::Range;
8+
use std::pin::Pin;
9+
use std::sync::Arc;
10+
11+
/// A schema-less stream of record batches produced by a single partition of a [WorkerConnection].
12+
///
13+
/// Operators wrap this with [datafusion::physical_plan::stream::RecordBatchStreamAdapter] when they
14+
/// need a schema-bearing [datafusion::execution::SendableRecordBatchStream]. Keeping the transport
15+
/// free of schema responsibilities makes alternative implementations (e.g. shared-memory queues
16+
/// backing embedded executors) easier to write.
17+
pub type WorkerPartitionStream = Pin<Box<dyn Stream<Item = Result<RecordBatch>> + Send + 'static>>;
18+
19+
/// A live connection to a single worker that demultiplexes the underlying transport into one
20+
/// [WorkerPartitionStream] per partition.
21+
///
22+
/// One connection handles every partition in the `target_partition_range` requested at open time,
23+
/// so the implementation can reuse a single underlying network/IPC stream and fan messages out to
24+
/// per-partition queues. Each partition can be streamed exactly once.
25+
pub trait WorkerConnection {
26+
/// Returns the stream of record batches for `partition`. Calling this twice for the same
27+
/// partition is an error.
28+
fn stream_partition(&self, partition: usize) -> Result<WorkerPartitionStream>;
29+
}
30+
31+
/// Factory that opens a [WorkerConnection] to a single worker task.
32+
///
33+
/// The default implementation is the Arrow-Flight gRPC transport baked into this crate. Custom
34+
/// transports (e.g. shared-memory queues for an embedded execution context) plug in via
35+
/// [crate::DistributedExt::with_distributed_worker_transport].
36+
pub trait WorkerTransport {
37+
/// Opens a connection to the worker hosting `target_task` of `input_stage` covering the
38+
/// partitions in `target_partitions`. The returned [WorkerConnection] takes ownership of any
39+
/// background resources (gRPC streams, demux tasks, cancellation tokens, ...) and is expected
40+
/// to clean them up on drop.
41+
fn open(
42+
&self,
43+
input_stage: &RemoteStage,
44+
target_partitions: Range<usize>,
45+
target_task: usize,
46+
ctx: &Arc<TaskContext>,
47+
metrics: &ExecutionPlanMetricsSet,
48+
) -> Result<Box<dyn WorkerConnection + Send + Sync>>;
49+
}
50+
51+
impl WorkerTransport for Arc<dyn WorkerTransport + Send + Sync> {
52+
fn open(
53+
&self,
54+
input_stage: &RemoteStage,
55+
target_partitions: Range<usize>,
56+
target_task: usize,
57+
ctx: &Arc<TaskContext>,
58+
metrics: &ExecutionPlanMetricsSet,
59+
) -> Result<Box<dyn WorkerConnection + Send + Sync>> {
60+
self.as_ref()
61+
.open(input_stage, target_partitions, target_task, ctx, metrics)
62+
}
63+
}

0 commit comments

Comments
 (0)