Skip to content

grpc/client: Implement Channel builder#2675

Open
nathanielford wants to merge 24 commits into
grpc:masterfrom
nathanielford:implement/channel-builder
Open

grpc/client: Implement Channel builder#2675
nathanielford wants to merge 24 commits into
grpc:masterfrom
nathanielford:implement/channel-builder

Conversation

@nathanielford

Copy link
Copy Markdown
Contributor

Introduces a type-builder Channel builder for creating new Channels, using an idiomatic pattern for adding in configuration options.

Motivation

In order to flexibly and ergonomically provide for channel configuration on construction, the type builder is introduced. This allows users to construct channels in a manner similar to:

let channel = Channel::builder("https://api.darkcave.com:443")
    .credential(my_creds)
    .runtime(my_runtime)
    .override_authority("arrow.wumpus.api")
    // ... any other options.
    .build();  // No error is returned here

Solution

The solution uses a 'type builder' so that users can build up their channel configuration over various calls, and the compiler can catch type issues. This also allows us to encapsulate various ergonomics (such as default selection of runtime). This PR implements the critical values at this time and defers additional optional values not being used until a later point. It also leaves work around the internals of the PersistentChannel/ActiveChannel for a future PR.

Because the public API is being altered, this requires a version bump.

@nathanielford nathanielford force-pushed the implement/channel-builder branch 3 times, most recently from 1014b27 to 821aa43 Compare June 11, 2026 19:50
@nathanielford nathanielford force-pushed the implement/channel-builder branch from 821aa43 to c0e1055 Compare June 11, 2026 20:01
@nathanielford nathanielford requested a review from arjan-bal June 11, 2026 20:08
@nathanielford nathanielford marked this pull request as ready for review June 11, 2026 20:08
…nChannelCredential impl adjustments should handle the type issue when passing DynChannelCredentials internally
@arjan-bal arjan-bal self-assigned this Jun 17, 2026
Comment thread grpc/src/client/channel.rs Outdated
Comment thread grpc/src/client/channel.rs Outdated
Comment thread grpc/src/client/name_resolution/mod.rs Outdated
@arjan-bal

Copy link
Copy Markdown
Contributor

As discussed offline, I've raised #2694 with only the changes to the trait bounds for the credentials. I'll wait for Doug to review it.

@arjan-bal arjan-bal removed their assignment Jun 17, 2026
@arjan-bal

Copy link
Copy Markdown
Contributor

Update: #2703 has been merged to make the ChannelCredentials trait object safe. You should be able to use Arc<dyn ChannelCredentials> everywhere in builder.

@nathanielford nathanielford force-pushed the implement/channel-builder branch from a8e3dac to e197877 Compare July 6, 2026 18:41
@arjan-bal arjan-bal assigned arjan-bal and unassigned nathanielford Jul 7, 2026

@arjan-bal arjan-bal left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I have left some new comments, and some of the previous feedback still needs to be addressed. Please take a look.

Comment thread grpc/src/client/name_resolution/mod.rs Outdated
Comment thread grpc/src/client/channel.rs Outdated
type PresentCredentials = PresentOpt<Arc<dyn ChannelCredentials>>;
type PresentRuntime = PresentOpt<GrpcRuntime>;

pub trait IntoCredentialConfig {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We should not need the IntoCredentialConfig trait anymore, we can directly use Arc<dyn ChannelCredentials> everywhere we need to accept the credentials.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

To be clear, this exists so that passing both arced and non-arced creds are possibe:

let my_creds: ChannelCredentials = ...
let my_other_creds: ChannelCredentials = ...
let my_arced_creds: Arc<dyn ChannelCredentials> = Arc::new(my_other_creds)

let my_channel = Channel::builder("https://api.darkcave.com:443")
    .credential(my_creds)
    .build()
let my_other_channel = Channel::builder("https://api.darkcave.com:443")
    .credential(my_arced_creds)
    .build()

In both cases they are, within the builder, coerced into a CredentialConfig, which is held for the lifetime of the builder but is not a thing the user needs even know about.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Introducing this trait adds an unnecessary layer of indirection for users trying to identify the concrete types their structs must implement. I recommend avoiding this approach.

If we want to improve ergonomics later, we can have the credential constructors return Arc-wrapped credentials directly, or add a builder method that accepts unwrapped credentials. However, it feels a bit too early to introduce these abstractions.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

There is no indirection that the users are required to know here. The blanket implementations act as a pass-through type:

  • If the credentials are passed as an arc, the implementation for CredentialConfig for arcs will leave it as-is.
  • If the credentials are not passed as an arc, that impl will arc it. The credentials are saved to the struct as a PresentOpt<Arc<dyn ChannelCredentials>>.
  • If anything but an Arc<dyn ChannelCredentials> or a ChannelCredentials is passed, it throws a compiler error.

At this point, any type inspection will show that the builder is of type ChannelBuilder<PresentOpt<Arc<dyn ChannelCredentials>>, MissingOpt> (modulo that last MissingOpt being filled in already).

Comment thread grpc/src/client/channel.rs Outdated
@arjan-bal arjan-bal assigned nathanielford and unassigned arjan-bal Jul 7, 2026
@arjan-bal arjan-bal changed the title Implement Channel builder grpc/client: Implement Channel builder Jul 7, 2026
@nathanielford nathanielford force-pushed the implement/channel-builder branch from 4288b7f to 8c1e34b Compare July 7, 2026 15:02
Comment thread grpc/src/client/channel.rs Outdated
type PresentCredentials = PresentOpt<Arc<dyn ChannelCredentials>>;
type PresentRuntime = PresentOpt<GrpcRuntime>;

pub trait IntoCredentialConfig {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Introducing this trait adds an unnecessary layer of indirection for users trying to identify the concrete types their structs must implement. I recommend avoiding this approach.

If we want to improve ergonomics later, we can have the credential constructors return Arc-wrapped credentials directly, or add a builder method that accepts unwrapped credentials. However, it feels a bit too early to introduce these abstractions.

Comment on lines +182 to +188
// TODO(nathanielford) In follow-up implement the following optional values:
// - default_service_config
// - http_proxy_cfg
// - disable_health_checks
// - idle_timeout
// - enable_channelz
// - keepalive_cfg

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Since many of these features are not yet implemented, adding a TODO to configure them feels premature. I recommend omitting the comment, as it lacks a tracking issue and is likely to become stale.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Are we using tracking issues for any of this? I was told we weren't, really.

That said, I do intend to address all of these in near-future followups for optional values and for the ServiceConfig work (which is actively blocked on this). I'm not entirely sure the best way to track the state of work given the uncomfortable tension between a) avoiding TODOs, b) not using issues, c) avoiding huge CLs.

Comment thread grpc/src/client/channel.rs Outdated
Comment on lines +192 to +195
/// Impl for adding the (required) credentials to the builder.
// This is provided as a separate builder function to allow for the possibility
// of satisfying the credential/security configuration through different means
// in the future (via adding methods to this impl taking different args).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Rustdoc for impl blocks is usually unnecessary. Instead we should document the public methods and struct.

Comment thread grpc/src/client/channel.rs Outdated
// of satisfying the credential/security configuration through different means
// in the future (via adding methods to this impl taking different args).
impl<Runtime> ChannelBuilder<MissingOpt, Runtime> {
pub fn credentials(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Please add rustdoc for public methods. I would also recommend using documentation tests to demonstrate the usage.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

(Still working on this.)

Comment thread grpc/src/client/channel.rs Outdated
pub struct ChannelBuilder<C, R> {
// Required values.
target: String,
credentials: C,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

What is the benefit of using the typed builder pattern for credentials compared to passing them directly into the builder's constructor? From my understanding, typed builders help enforce parameter configuration order, but our credentials do not seem to have any interdependencies.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

There are two primary reasons we discussed in the design doc:

  • Type builders to accumulate options is pretty standard in the wider Rust ecosystem.
  • This gives us flexibility w/o cruft around what the options are.

The credentials() call is a good example of a place where there is enough uncertainty on how we want to provide credentials in the future that being able to provide paths for satisfying that requirement is advantageous. Right now, .credentials(my_creds) is what we provide, but we are free to add any number of functions that yield a ChannelBuilder<PresentOpt<Arc<dyn ChannelCredentials>>, T>`. As with the runtime, if we wanted to provide a 'default' option in the future, or one configured via env variables or whatever other method.

(To be clear, we probably won't with credentials, but there are some questions around how this interacts with various concerns that giving ourselves a clear pathway to handle this cleanly is nice. Given this and the fact we are doing it with the runtime means it makes sense to keep it consistent.)

There are other nice to haves (e.g. free ordering of parameters) but the above are the two key reasons we went this route.

Comment thread grpc/src/client/channel.rs Outdated
Comment thread grpc/src/client/channel.rs Outdated
// Required values.
target: String,
credentials: C,
runtime: R, // Can be defaulted w/Tokio runtime feature.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Similar to my question above, what is the benefit of using a typed builder for the runtime? A simpler alternative would be to introduce a feature-flag-protected constructor that defaults to the Tokio runtime when enabled:

impl Channel {
    #[cfg(feature = "_runtime-tokio")]
    pub fn builder(target: impl Into<String>) -> ChannelBuilder {}

    #[cfg(not(feature = "_runtime-tokio"))]
    pub fn builder(target: impl Into<String>, rt: Arc<dyn Runtime>) -> ChannelBuilder {}
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Idiomatically this isn't how most libraries do their builders. You land yourself in a world where a string of parameters creates craziness. Consider how the example above looks if you have two parameters with feature flag and feature flag-less pathways, and what happens to code that flips a flag. This also lets us handle optional parameters in the same fashion.

let resolver_opts = name_resolution::ResolverOptions {
authority: persistent_channel.authority.clone(),
// authority: persistent_channel.security_opts.authority.clone(),
authority: "ignored".to_string(), // TODO(nathanielford) currently, this option is always ignored.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This authority will get used by the xDS resolver, we should keep it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I would like to leave this for a follow up; the persistent_channel.security_opts.authority is the wrong type for ResolverOptions, but I don't want to make a decision about how to manage this right now since it's not directly related to the ChannelBuilder.

Comment thread grpc/src/client/channel.rs
Comment on lines +253 to +254
// TODO(nathanielford) Find a better place to set up default registries.
setup_registeries();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

What's wrong with registering plugins here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

It might be fine, and we might just leave it. However, this sets up the default registries for the entire library for all channels. It's a bit odd for this to be the responsibility of the channel builder, though it may be the first and only reasonable place for it to happen in actual usage.

@arjan-bal arjan-bal removed their assignment Jul 7, 2026
@nathanielford nathanielford force-pushed the implement/channel-builder branch from 664e2cc to fbcbbb1 Compare July 7, 2026 22:24
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