Custom worker connections#

A ChannelResolver controls how connections to workers are opened. It turns a worker URL into a Worker gRPC client backed by a Tonic channel — used both by the coordinating context and by workers when they call other workers mid-query.

It is optional. A default implementation already connects to each URL, builds a client, and caches it for reuse on later requests to the same URL — enough for most setups. You only implement your own to customize the connection: wrap the client in tower layers (auth, retries, tracing), tune timeouts or message-size limits, or run it on a dedicated I/O runtime.

The trait has a single async method:

#[async_trait]
pub trait ChannelResolver {
    async fn get_worker_client_for_url(
        &self,
        url: &Url,
    ) -> Result<WorkerServiceClient<BoxCloneSyncChannel>, DataFusionError>;
}

Note

get_worker_client_for_url is called on every gRPC request. Reuse clients rather than reconnecting each time, or you’ll open a fresh connection per request. The easiest way is to build on DefaultChannelResolver (which caches channels), or to use the create_worker_client helper.

Providing your own#

The simplest custom resolver wraps DefaultChannelResolver, delegating to it for channel caching and only customizing what you need:

#[derive(Clone)]
struct CustomChannelResolver {
    inner: DefaultChannelResolver,
}

#[async_trait]
impl ChannelResolver for CustomChannelResolver {
    async fn get_worker_client_for_url(
        &self,
        url: &Url,
    ) -> Result<WorkerServiceClient<BoxCloneSyncChannel>, DataFusionError> {
        // Delegate to the default (cached channels), or build your own client
        // here — e.g. wrapped in tower layers, or on a custom runtime.
        self.inner.get_worker_client_for_url(url).await
    }
}

Build a single instance for your application’s lifetime (so clients are reused), and register it in two places — on the coordinating context that plans queries, and on every worker, since workers open connections to other workers too:

let channel_resolver = CustomChannelResolver {
    inner: DefaultChannelResolver::default(),
};

// On the coordinating context:
let state = SessionStateBuilder::new()
    .with_default_features()
    .with_distributed_worker_resolver(/* ... */)
    .with_distributed_planner()
    .with_distributed_channel_resolver(channel_resolver.clone())
    .build();

// On every worker, via its session builder:
let worker = Worker::from_session_builder(move |ctx: WorkerQueryContext| {
    let channel_resolver = channel_resolver.clone();
    async move {
        Ok(ctx
            .builder
            .with_distributed_channel_resolver(channel_resolver)
            .build())
    }
});