Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
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
38 changes: 29 additions & 9 deletions tonic-xds/src/xds/resource/cluster.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ use xds_client::{Error, Resource};

use super::security::{ClusterSecurityConfig, parse_transport_socket};

// Re-exported so other modules keep using the `cluster::LbPolicy` path.
pub(crate) use super::lb_policy::{LbPolicy, RingHashSettings};

/// Validated Cluster resource.
#[derive(Debug, Clone)]
pub(crate) struct ClusterResource {
Expand All @@ -22,13 +25,6 @@ pub(crate) struct ClusterResource {
pub security: Option<ClusterSecurityConfig>,
}

/// Load balancing policies.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum LbPolicy {
RoundRobin,
LeastRequest,
}

impl Resource for ClusterResource {
type Message = Cluster;

Expand Down Expand Up @@ -58,6 +54,9 @@ impl Resource for ClusterResource {
let lb_policy = match cluster::LbPolicy::try_from(message.lb_policy) {
Ok(cluster::LbPolicy::RoundRobin) => LbPolicy::RoundRobin,
Ok(cluster::LbPolicy::LeastRequest) => LbPolicy::LeastRequest,
Ok(cluster::LbPolicy::RingHash) => {
LbPolicy::RingHash(RingHashSettings::validate(message.lb_config)?)
}
_ => {
return Err(Error::Validation(format!(
"unsupported load balancing policy: {}",
Expand Down Expand Up @@ -143,9 +142,10 @@ mod tests {

#[test]
fn test_unsupported_lb_policy_is_rejected() {
// A policy we don't support (e.g. MAGLEV) is still NACKed.
let cluster = Cluster {
name: "rh-cluster".to_string(),
lb_policy: cluster::LbPolicy::RingHash as i32,
name: "mg-cluster".to_string(),
lb_policy: cluster::LbPolicy::Maglev as i32,
..Default::default()
};
let err = ClusterResource::validate(cluster).unwrap_err();
Expand All @@ -155,6 +155,26 @@ mod tests {
);
}

#[test]
fn test_ring_hash_lb_policy() {
// A RING_HASH cluster validates to LbPolicy::RingHash with the parsed
// (here defaulted) settings. Detailed ring_hash_lb_config validation is
// covered in the `lb_policy` module.
let cluster = Cluster {
name: "rh-cluster".to_string(),
lb_policy: cluster::LbPolicy::RingHash as i32,
..Default::default()
};
let validated = ClusterResource::validate(cluster).unwrap();
assert_eq!(
validated.lb_policy,
LbPolicy::RingHash(RingHashSettings {
min_ring_size: 1024,
max_ring_size: 4096,
})
);
}

#[test]
fn test_validate_empty_name() {
let cluster = make_cluster("");
Expand Down
177 changes: 177 additions & 0 deletions tonic-xds/src/xds/resource/lb_policy.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
//! Cluster load-balancing policy (CDS) and ring-hash config validation.

use envoy_types::pb::envoy::config::cluster::v3::cluster;
use xds_client::Error;

/// Load balancing policies.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum LbPolicy {
RoundRobin,
LeastRequest,
/// Ring-hash (gRFC A42), carrying the validated ring bounds.
RingHash(RingHashSettings),
}

/// Validated gRFC A42 ring-hash sizing parsed from `ring_hash_lb_config`.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct RingHashSettings {
pub min_ring_size: u64,
Comment thread
madhurishgupta marked this conversation as resolved.
Outdated
pub max_ring_size: u64,
}

/// gRFC A42 ring-hash sizing. `minimum_ring_size` defaults to 1024 and
/// `maximum_ring_size` to the local cap of 4096 when unset; both are clamped to
/// that cap, and any configured value above the 8M ceiling is rejected.
pub(crate) const RING_HASH_DEFAULT_MIN_SIZE: u64 = 1024;
pub(crate) const RING_HASH_SIZE_CAP: u64 = 4096;
pub(crate) const RING_HASH_SIZE_CEILING: u64 = 8 * 1024 * 1024;

impl RingHashSettings {
/// Validate a Cluster's `lb_config` oneof as `ring_hash_lb_config` (gRFC
/// A42).
///
/// Rejects a `hash_function` other than `XX_HASH`, any ring size above the
/// 8M ceiling, and `min_ring_size > max_ring_size`. Unset sizes take the
/// defaults (1024 / 4096); the resolved bounds are then clamped to the
/// local cap.
pub(crate) fn validate(lb_config: Option<cluster::LbConfig>) -> xds_client::Result<Self> {
use cluster::ring_hash_lb_config::HashFunction;
Comment thread
madhurishgupta marked this conversation as resolved.
Outdated

let (min_field, max_field, hash_function) = match lb_config {
Some(cluster::LbConfig::RingHashLbConfig(c)) => {
(c.minimum_ring_size, c.maximum_ring_size, c.hash_function)
}
// No ring_hash_lb_config → defaults (XX_HASH is the proto default).
_ => (None, None, HashFunction::XxHash as i32),
};

match HashFunction::try_from(hash_function) {
Ok(HashFunction::XxHash) => {}
Ok(other) => {
return Err(Error::Validation(format!(
"unsupported ring_hash hash function: {}",
other.as_str_name()
)));
}
Err(_) => {
return Err(Error::Validation(format!(
"unknown ring_hash hash function: {hash_function}"
)));
}
}

let min = min_field.map_or(RING_HASH_DEFAULT_MIN_SIZE, |v| v.value);
let max = max_field.map_or(RING_HASH_SIZE_CAP, |v| v.value);
Comment thread
madhurishgupta marked this conversation as resolved.
Outdated
if min > RING_HASH_SIZE_CEILING || max > RING_HASH_SIZE_CEILING {
return Err(Error::Validation(format!(
"ring_hash ring size exceeds the maximum of {RING_HASH_SIZE_CEILING} \
(min_ring_size={min}, max_ring_size={max})"
)));
}
// Checked on the resolved sizes before the cap is applied.
if min > max {
return Err(Error::Validation(format!(
"ring_hash min_ring_size ({min}) is greater than max_ring_size ({max})"
)));
}

Ok(RingHashSettings {
min_ring_size: min.min(RING_HASH_SIZE_CAP),
max_ring_size: max.min(RING_HASH_SIZE_CAP),
})
}
}

#[cfg(test)]
mod tests {
use super::*;
use envoy_types::pb::google::protobuf::UInt64Value;

fn ring_size(value: u64) -> UInt64Value {
UInt64Value { value }
}

fn lb_config(config: cluster::RingHashLbConfig) -> Option<cluster::LbConfig> {
Some(cluster::LbConfig::RingHashLbConfig(config))
}

#[test]
fn ring_hash_defaults() {
// No ring_hash_lb_config → min 1024, max defaults to the cap; XX_HASH
// is the default hash function.
let settings = RingHashSettings::validate(None).unwrap();
assert_eq!(
settings,
RingHashSettings {
min_ring_size: 1024,
max_ring_size: 4096,
}
);
}

#[test]
fn ring_hash_custom_sizes_within_cap() {
let settings = RingHashSettings::validate(lb_config(cluster::RingHashLbConfig {
minimum_ring_size: Some(ring_size(2048)),
maximum_ring_size: Some(ring_size(3000)),
..Default::default()
}))
.unwrap();
assert_eq!(
settings,
RingHashSettings {
min_ring_size: 2048,
max_ring_size: 3000,
}
);
}

#[test]
fn ring_hash_sizes_clamped_to_cap() {
// Sizes within the 8M ceiling but above the local cap clamp to 4096.
let settings = RingHashSettings::validate(lb_config(cluster::RingHashLbConfig {
minimum_ring_size: Some(ring_size(100_000)),
maximum_ring_size: Some(ring_size(100_000)),
..Default::default()
}))
.unwrap();
assert_eq!(
settings,
RingHashSettings {
min_ring_size: 4096,
max_ring_size: 4096,
}
);
}

#[test]
fn ring_hash_rejects_non_xx_hash() {
let err = RingHashSettings::validate(lb_config(cluster::RingHashLbConfig {
hash_function: cluster::ring_hash_lb_config::HashFunction::MurmurHash2 as i32,
..Default::default()
}))
.unwrap_err();
assert!(err.to_string().contains("hash function"));
}

#[test]
fn ring_hash_rejects_size_above_ceiling() {
let err = RingHashSettings::validate(lb_config(cluster::RingHashLbConfig {
maximum_ring_size: Some(ring_size(8 * 1024 * 1024 + 1)),
..Default::default()
}))
.unwrap_err();
assert!(err.to_string().contains("exceeds the maximum"));
}

#[test]
fn ring_hash_rejects_min_greater_than_max() {
let err = RingHashSettings::validate(lb_config(cluster::RingHashLbConfig {
minimum_ring_size: Some(ring_size(3000)),
maximum_ring_size: Some(ring_size(2000)),
..Default::default()
}))
.unwrap_err();
assert!(err.to_string().contains("greater than max_ring_size"));
}
}
1 change: 1 addition & 0 deletions tonic-xds/src/xds/resource/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
pub(crate) mod cluster;
pub(crate) mod endpoints;
pub(crate) mod hash_policy;
pub(crate) mod lb_policy;
pub(crate) mod listener;
pub(crate) mod outlier_detection;
pub(crate) mod route_config;
Expand Down
Loading