Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
4a29d46
Implement Channel Type Builder
nathanielford Jun 4, 2026
6739db6
Update ChannelBuilder to use IntoDynChannelCredentials trait.
nathanielford Jun 4, 2026
ef64d7c
Add optional method for channel authority override.
nathanielford Jun 4, 2026
b9bce2a
Fix imports in interop client.
nathanielford Jun 4, 2026
7f240f9
Update examples to use the new Channel builder.
nathanielford Jun 4, 2026
7365a7a
Cleanup
nathanielford Jun 4, 2026
d2649c6
Cleanup and version bump.
nathanielford Jun 4, 2026
44f7e27
Fix typing on credentials builder impl.
nathanielford Jun 11, 2026
c0e1055
Cleanup credential typing.
nathanielford Jun 11, 2026
65b7738
Add into implementation for CredentialConfig, which along with the Dy…
nathanielford Jun 16, 2026
4390322
Merge branch 'master' into implement/channel-builder
nathanielford Jun 16, 2026
45f16c0
Fix formatting
nathanielford Jun 17, 2026
8cf7952
Merge remote-tracking branch 'upstream/master' into implement/channel…
nathanielford Jun 17, 2026
c821541
Fix new example to use builder.
nathanielford Jun 17, 2026
2714d5b
Merge branch 'master' into implement/channel-builder
nathanielford Jul 6, 2026
e197877
Fix merge issues, use direct Arc<dyn ChannelCredentials>
nathanielford Jul 6, 2026
0758d58
Fix comment
nathanielford Jul 6, 2026
73c3ddf
Remove tests that should have been removed in the merge
nathanielford Jul 7, 2026
8c1e34b
Adding ZSF to MissingOpt to prevent external usage, removing pub mark…
nathanielford Jul 7, 2026
e98a9c9
Comment removal
nathanielford Jul 7, 2026
2232440
Comment fix.
nathanielford Jul 7, 2026
fbcbbb1
Polish updates
nathanielford Jul 7, 2026
308b1c7
Reversing course to a non-type builder
nathanielford Jul 8, 2026
e820a38
Merge branch 'master' into implement/channel-builder
nathanielford Jul 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion examples/src/grpc-gcp/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
let tls = RustlsChannelCredendials::new(ClientTlsConfig::new())?;
let channel_creds = CompositeChannelCredentials::new(tls, Arc::new(call_creds));

let channel = Channel::new(ENDPOINT, Arc::new(channel_creds), Default::default());
let channel = Channel::builder(ENDPOINT, Arc::new(channel_creds)).build();

let client = PublisherClient::new(channel);

Expand Down
7 changes: 3 additions & 4 deletions examples/src/grpc-helloworld/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ use std::sync::Arc;
use generated::helloworld::HelloRequest;
use generated::helloworld::greeter_client::GreeterClient;
use grpc::client::Channel;
use grpc::client::ChannelOptions;
use grpc::credentials::LocalChannelCredentials;
use protobuf::proto;

Expand All @@ -25,11 +24,11 @@ async fn main() {
};

// Create a new gRPC channel:
let channel = Channel::new(
let channel = Channel::builder(
"dns:///[::1]:50051",
Arc::new(LocalChannelCredentials::new()),
ChannelOptions::default(),
);
)
.build();
let client = GreeterClient::new(channel);

// Send the request and print the response:
Expand Down
8 changes: 4 additions & 4 deletions examples/src/grpc-routeguide/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ use std::env;
use std::sync::Arc;

use grpc::client::Channel;
use grpc::client::ChannelOptions;
use grpc::client::Invoke;
use grpc::credentials::LocalChannelCredentials;
use tokio::task;
Expand Down Expand Up @@ -180,11 +179,12 @@ async fn main() {
println!("Connecting to {address}...");

// Create a new gRPC channel:
let channel = Channel::new(
let channel = Channel::builder(
format!("dns:///{address}"),
Arc::new(LocalChannelCredentials::new()),
ChannelOptions::default(),
);
)
.build();

let client = RouteGuideClient::new(channel);

println!("*** SIMPLE RPC ***");
Expand Down
229 changes: 105 additions & 124 deletions grpc/src/client/channel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,17 +29,14 @@
use std::str::FromStr;
use std::sync::Arc;
use std::sync::Mutex;
use std::time::Duration;
use std::time::Instant;
use std::vec;

use serde_json::json;
use tokio::sync::mpsc;
use tokio::sync::watch;

use crate::StatusCodeError;
use crate::StatusError;
use crate::attributes::Attributes;
use crate::client::CallOptions;
use crate::client::ConnectivityState;
use crate::client::DynInvoke;
Expand Down Expand Up @@ -84,71 +81,9 @@
use crate::credentials::common::Authority;
use crate::rt;
use crate::rt::GrpcRuntime;
#[cfg(feature = "_runtime-tokio")]
use crate::rt::default_runtime;

/// Configuration options for [`Channel`]s.
#[non_exhaustive]
pub struct ChannelOptions {
pub(crate) transport_options: Attributes, // ?
pub(crate) channel_authority: Option<String>,
pub(crate) connection_backoff: Option<Todo>,
pub(crate) default_service_config: Option<String>,
pub(crate) disable_proxy: bool,
pub(crate) disable_service_config_lookup: bool,
pub(crate) disable_health_checks: bool,
pub(crate) max_retry_memory: u32, // ?
pub(crate) idle_timeout: Duration,
// TODO: pub transport_registry: Option<TransportRegistry>,
// TODO: pub name_resolver_registry: Option<ResolverRegistry>,
// TODO: pub lb_policy_registry: Option<LbPolicyRegistry>,

// Typically we allow settings at the channel level that impact all RPCs,
// but can also be set per-RPC. E.g.s:
//
// - interceptors
// - user-agent string override
// - max message sizes
// - max retry/hedged attempts
// - disable retry
//
// In gRPC-Go, we can express CallOptions as DialOptions, which is a nice
// pattern: https://pkg.go.dev/google.golang.org/grpc#WithDefaultCallOptions
//
// To do this in rust, all optional behavior for a request would need to be
// expressed through a trait that applies a mutation to a request. We'd
// apply all those mutations before the user's options so the user's options
// would override the defaults, or so the defaults would occur first.
pub(crate) default_request_extensions: Vec<Todo>, // ??
}

impl Default for ChannelOptions {
fn default() -> Self {
Self {
transport_options: Attributes::default(),
channel_authority: None,
connection_backoff: None,
default_service_config: None,
disable_proxy: false,
disable_service_config_lookup: false,
disable_health_checks: false,
max_retry_memory: 8 * 1024 * 1024, // 8MB -- ???
idle_timeout: Duration::from_secs(30 * 60),
default_request_extensions: vec![],
}
}
}

impl ChannelOptions {
/// Overrides the channel authority, which is used for credentials
/// handshaking and HTTP virtual hosting.
pub fn override_authority(self, authority: impl Into<String>) -> Self {
Self {
channel_authority: Some(authority.into()),
..self
}
}
}

/// A virtual, persistent connection to a gRPC service.
///
/// A `Channel` begins in an "idle" state and connects when the first RPC is
Expand All @@ -161,33 +96,29 @@
}

impl Channel {
/// Constructs a new gRPC channel. Channel creation cannot fail, but if the
/// target string is invalid, the returned channel will never connect, and
/// will fail all RPCs.
pub fn new(
/// Creates a new channel builder for the given target.
///# Example
///
/// ```
/// use std::sync::Arc;
/// use grpc::client::Channel;
/// use grpc::credentials::LocalChannelCredentials;
///
/// let channel = Channel::builder("dns:///localhost:123", Arc::new(LocalChannelCredentials::new()))
/// .build();
/// ```
#[cfg(feature = "_runtime-tokio")]
pub fn builder(
target: impl Into<String>,
credentials: Arc<dyn ChannelCredentials>,
options: ChannelOptions,
) -> Self {
pick_first::reg();
round_robin::reg();
dns::reg();
#[cfg(unix)]
name_resolution::unix::reg();
#[cfg(target_os = "linux")]
name_resolution::unix_abstract::reg();
#[cfg(feature = "_runtime-tokio")]
tonic_transport::reg();
Self {
inner: Arc::new(PersistentChannel::new(
target,
default_runtime(),
options,
credentials,
)),
) -> ChannelBuilder {
ChannelBuilder {
target: target.into(),
credentials: credentials,

Check warning on line 117 in grpc/src/client/channel.rs

View workflow job for this annotation

GitHub Actions / clippy

redundant field names in struct initialization
channel_authority: None,
runtime: default_runtime(),
}
}

// TODO: enter_idle(&self) and graceful_stop()?

/// Returns the current state of the channel. If `connect` is true and the
Expand Down Expand Up @@ -222,53 +153,88 @@
}
}

// A PersistentChannel represents the static configuration of a channel and an
// optional Arc of an ActiveChannel. An ActiveChannel exists whenever the
// PersistentChannel is not IDLE. Every channel is IDLE at creation, or after
// some configurable timeout elapses without any any RPC activity.
struct PersistentChannel {
target: Target,
resolver_builder: Arc<dyn ResolverBuilder>,
options: ChannelOptions,
active_channel: Mutex<Option<Arc<ActiveChannel>>>,
pub struct ChannelBuilder {
// Required values.
target: String,
credentials: Arc<dyn ChannelCredentials>,
runtime: GrpcRuntime,
security_opts: SecurityOpts,
authority: String,

// Optional values.
channel_authority: Option<String>,
// 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
Comment on lines +164 to +170

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.

}

impl PersistentChannel {
// Channels begin idle so `new()` does not automatically connect.
// ChannelOption contain only optional parameters.
fn new(
target: impl Into<String>,
runtime: GrpcRuntime,
options: ChannelOptions,
credentials: Arc<dyn ChannelCredentials>,
) -> Self {
// TODO(nathanielford): Return errors here instead of panicking.
let target = Target::from_str(&target.into()).unwrap();
impl ChannelBuilder {
/// Builds the channel with the provided configuration.
/// # Example
///
/// ```
/// use std::sync::Arc;
/// use grpc::client::Channel;
/// use grpc::credentials::LocalChannelCredentials;
///
/// let channel = Channel::builder("dns:///localhost:123", Arc::new(LocalChannelCredentials::new()))
/// .build();
/// ```
pub fn build(self) -> Channel {
// TODO(nathanielford) This construction is currently a rough-cut placeholder.
// The design of PersistentChannel and how and where it is initialized
// will be finalized with the 'Internal Channel Design' with
// consideration for:
// - error handling (inc. always-failing resolvers due to invalid targets))
// - testing (inc. credential and transport configuration)

// TODO(nathanielford) Find a better place to set up default registries.
setup_registeries();
Comment on lines +193 to +194

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.


let target = Target::from_str(self.target.as_str()).unwrap();
let resolver_builder = global_registry().get(target.scheme()).unwrap();
Comment thread
nathanielford marked this conversation as resolved.
let authority = options
// TODO(nathanielford): Return errors here instead of panicking.
let target = name_resolution::Target::from(target);

Check warning on line 199 in grpc/src/client/channel.rs

View workflow job for this annotation

GitHub Actions / clippy

useless conversion to the same type: `client::name_resolution::Target`
let authority = self
.channel_authority
.clone()
.unwrap_or_else(|| resolver_builder.default_authority(&target).to_owned());
let security_opts = SecurityOpts {
credentials,
credentials: self.credentials,
authority: Authority::from_host_port_str(&authority),
handshake_info: ClientHandshakeInfo::default(),
};

Self {
target,
resolver_builder,
active_channel: Mutex::default(),
options,
runtime,
security_opts,
authority,
Channel {
inner: Arc::new(PersistentChannel {
active_channel: Mutex::default(),
target,
security_opts,
runtime: self.runtime,
resolver_builder,
}),
}
}

pub fn channel_authority(mut self, authority: impl Into<String>) -> Self {
self.channel_authority = Some(authority.into());
self
}
}

struct PersistentChannel {
active_channel: Mutex<Option<Arc<ActiveChannel>>>,

// Configuration
target: Target,
security_opts: SecurityOpts,
runtime: GrpcRuntime,

// Inferred Configuration
resolver_builder: Arc<dyn ResolverBuilder>,
}

impl PersistentChannel {
/// Returns the current state of the channel. If there is no underlying active channel,
/// returns Idle. If `connect` is true, will create a new active channel iff none exists.
fn get_state(&self, connect: bool) -> ConnectivityState {
Expand Down Expand Up @@ -325,7 +291,8 @@

let work_scheduler = Arc::new(ResolverWorkScheduler { wqtx });
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.

work_scheduler,
runtime: runtime.clone(),
};
Expand Down Expand Up @@ -616,3 +583,17 @@
}
Authority::new(host_and_port.to_string(), None)
}

// Sets up the default registries for transports, name resolvers, and load
// balancers.
fn setup_registeries() {
pick_first::reg();
round_robin::reg();
dns::reg();
#[cfg(unix)]
name_resolution::unix::reg();
#[cfg(target_os = "linux")]
name_resolution::unix_abstract::reg();
#[cfg(feature = "_runtime-tokio")]
tonic_transport::reg();
}
19 changes: 7 additions & 12 deletions grpc/src/client/interceptor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -201,17 +201,15 @@ where
///
/// ```
/// use std::sync::Arc;
/// use grpc::client::{Channel, ChannelOptions};
/// use grpc::client::Channel;
/// use grpc::credentials::LocalChannelCredentials;
/// use grpc::client::stream_util::ResponseValidator;
/// use grpc::client::interceptor::InvokeExt;
///
/// // Create a channel:
/// let channel = Channel::new(
/// "dns:///localhost:123",
/// Arc::new(LocalChannelCredentials::new()),
/// ChannelOptions::default(),
/// );
/// let channel = Channel::builder("dns:///localhost:123", Arc::new(LocalChannelCredentials::new()))
/// .build();
///
/// // Create an interceptor:
/// let interceptor = ResponseValidator::new(true);
///
Expand Down Expand Up @@ -247,17 +245,14 @@ pub trait InvokeExt: Invoke + Sized {
///
/// ```
/// use std::sync::Arc;
/// use grpc::client::{Channel, ChannelOptions};
/// use grpc::client::Channel;
/// use grpc::credentials::LocalChannelCredentials;
/// use grpc::client::stream_util::ResponseValidator;
/// use grpc::client::interceptor::InvokeOnceExt;
///
/// // Create a channel:
/// let channel = Channel::new(
/// "dns:///localhost:123",
/// Arc::new(LocalChannelCredentials::new()),
/// ChannelOptions::default(),
/// );
/// let channel = Channel::builder("dns:///localhost:123", Arc::new(LocalChannelCredentials::new()))
/// .build();
/// // Create an interceptor:
/// let interceptor = ResponseValidator::new(true);
///
Expand Down
Loading
Loading