Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Dynamic module clusters (``envoy.clusters.dynamic_modules``) can now use
Envoy's built-in load balancers. In addition to ``CLUSTER_PROVIDED``,
``lb_policy`` may be set to ``LEAST_REQUEST``, ``ROUND_ROBIN``, ``RANDOM``,
``RING_HASH``, or ``MAGLEV``; the module then supplies only host discovery and
Envoy performs host selection.
28 changes: 21 additions & 7 deletions source/extensions/clusters/dynamic_modules/cluster.cc
Original file line number Diff line number Diff line change
Expand Up @@ -903,12 +903,22 @@ DynamicModuleClusterFactory::createClusterWithConfig(
const envoy::extensions::clusters::dynamic_modules::v3::ClusterConfig& proto_config,
Upstream::ClusterFactoryContext& context) {

// Validate that CLUSTER_PROVIDED LB policy is used.
if (cluster.lb_policy() != envoy::config::cluster::v3::Cluster::CLUSTER_PROVIDED) {
// CLUSTER_PROVIDED uses the module's load balancer; the native policies use Envoy's factory
// load balancer, with the module supplying only host discovery.
const auto policy = cluster.lb_policy();
const bool is_module_lb = (policy == envoy::config::cluster::v3::Cluster::CLUSTER_PROVIDED);
const bool is_native_lb = (policy == envoy::config::cluster::v3::Cluster::LEAST_REQUEST ||
policy == envoy::config::cluster::v3::Cluster::ROUND_ROBIN ||
policy == envoy::config::cluster::v3::Cluster::RANDOM ||
policy == envoy::config::cluster::v3::Cluster::RING_HASH ||
policy == envoy::config::cluster::v3::Cluster::MAGLEV);

if (!is_module_lb && !is_native_lb) {
return absl::InvalidArgumentError(
fmt::format("cluster: LB policy {} is not valid for cluster type "
"'envoy.clusters.dynamic_modules'. Only 'CLUSTER_PROVIDED' is allowed.",
envoy::config::cluster::v3::Cluster::LbPolicy_Name(cluster.lb_policy())));
"'envoy.clusters.dynamic_modules'. Supported policies are CLUSTER_PROVIDED, "
"LEAST_REQUEST, ROUND_ROBIN, RANDOM, RING_HASH, and MAGLEV.",
envoy::config::cluster::v3::Cluster::LbPolicy_Name(policy)));
}

Server::Configuration::ServerFactoryContext& server_context = context.serverFactoryContext();
Expand Down Expand Up @@ -952,9 +962,13 @@ DynamicModuleClusterFactory::createClusterWithConfig(
cluster, std::move(config_or_error.value()), context, creation_status));
RETURN_IF_NOT_OK(creation_status);

// Create the thread-aware load balancer.
auto handle = std::make_shared<DynamicModuleClusterHandle>(new_cluster);
auto lb = std::make_unique<DynamicModuleThreadAwareLoadBalancer>(handle);
// Create the thread-aware load balancer only if the module provides LB (CLUSTER_PROVIDED).
// For native LB policies, return nullptr so the cluster manager builds the native factory LB.
Upstream::ThreadAwareLoadBalancerPtr lb;
if (cluster.lb_policy() == envoy::config::cluster::v3::Cluster::CLUSTER_PROVIDED) {
auto handle = std::make_shared<DynamicModuleClusterHandle>(new_cluster);
lb = std::make_unique<DynamicModuleThreadAwareLoadBalancer>(handle);
}

return std::make_pair(std::move(new_cluster), std::move(lb));
}
Expand Down
27 changes: 23 additions & 4 deletions source/extensions/dynamic_modules/sdk/rust/src/cluster.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,16 @@ pub trait Cluster: Send + Sync {
///
/// Each worker thread gets its own load balancer instance. The `envoy_lb`
/// provides thread-local access to the cluster's host set.
fn new_load_balancer(&self, envoy_lb: &dyn EnvoyClusterLoadBalancer) -> Box<dyn ClusterLb>;
///
/// Return `Some(lb)` if this cluster implements host selection, or `None` if the cluster
/// only provides host discovery and Envoy should use its native load balancer.
/// When returning `None`, Envoy will use the standard load balancer factory based on
/// `lb_policy` + `common_lb_config` (e.g., zone-aware or locality-weighted routing).
/// The module's `choose_host` hook will never be called if this returns `None`.
fn new_load_balancer(
&self,
envoy_lb: &dyn EnvoyClusterLoadBalancer,
) -> Option<Box<dyn ClusterLb>>;

/// Called on the main thread when a new event is scheduled via
/// [`EnvoyClusterScheduler::commit`] for this [`Cluster`].
Expand Down Expand Up @@ -2560,6 +2569,9 @@ pub unsafe extern "C" fn envoy_dynamic_module_on_cluster_destroy(
/// Wrapper that pairs a module-side load balancer with the Envoy-side LB pointer.
/// The `lb_envoy_ptr` is needed by [`ClusterLbContextRef::should_select_another_host`] to
/// resolve host pointers from the priority set.
///
/// If the module returns None from `new_load_balancer()`, this wrapper is not created and
/// a null pointer is returned, signaling to Envoy to use the native factory load balancer.
struct ClusterLbWrapper {
lb: Box<dyn ClusterLb>,
lb_envoy_ptr: abi::envoy_dynamic_module_type_cluster_lb_envoy_ptr,
Expand All @@ -2578,9 +2590,16 @@ pub unsafe extern "C" fn envoy_dynamic_module_on_cluster_lb_new(
let cluster = cluster_module_ptr as *const *const dyn Cluster;
let cluster = &**cluster;
let envoy_lb = EnvoyClusterLoadBalancerImpl::new(lb_envoy_ptr);
let lb = cluster.new_load_balancer(&envoy_lb);
let wrapper = Box::new(ClusterLbWrapper { lb, lb_envoy_ptr });
Box::into_raw(wrapper) as abi::envoy_dynamic_module_type_cluster_lb_module_ptr
match cluster.new_load_balancer(&envoy_lb) {
Some(lb) => {
let wrapper = Box::new(ClusterLbWrapper { lb, lb_envoy_ptr });
Box::into_raw(wrapper) as abi::envoy_dynamic_module_type_cluster_lb_module_ptr
},
None => {
// Module does not provide a load balancer; return null so Envoy uses native LB.
std::ptr::null()
},
}
}))
.unwrap_or_else(|panic| {
crate::log_ffi_panic("envoy_dynamic_module_on_cluster_lb_new", panic);
Expand Down
4 changes: 2 additions & 2 deletions source/extensions/dynamic_modules/sdk/rust/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1420,8 +1420,8 @@ pub static NEW_CLUSTER_CONFIG_FUNCTION: OnceLock<NewClusterConfigFunction> = Onc
/// envoy_cluster.pre_init_complete();
/// }
///
/// fn new_load_balancer(&self, _envoy_lb: &dyn EnvoyClusterLoadBalancer) -> Box<dyn ClusterLb> {
/// Box::new(MyClusterLb {})
/// fn new_load_balancer(&self, _envoy_lb: &dyn EnvoyClusterLoadBalancer) -> Option<Box<dyn ClusterLb>> {
/// Some(Box::new(MyClusterLb {}))
/// }
/// }
///
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6973,7 +6973,10 @@ fn test_cluster_callout_done_with_null_buffers_yields_none() {
struct TestCluster;
impl Cluster for TestCluster {
fn on_init(&mut self, _envoy_cluster: &dyn EnvoyCluster) {}
fn new_load_balancer(&self, _envoy_lb: &dyn EnvoyClusterLoadBalancer) -> Box<dyn ClusterLb> {
fn new_load_balancer(
&self,
_envoy_lb: &dyn EnvoyClusterLoadBalancer,
) -> Option<Box<dyn ClusterLb>> {
unimplemented!("not exercised by this test")
}

Expand Down
5 changes: 5 additions & 0 deletions test/extensions/clusters/dynamic_modules/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ envoy_cc_test(
"//source/extensions/clusters/dynamic_modules:cluster_lib",
"//source/extensions/dynamic_modules:abi_impl",
"//source/extensions/load_balancing_policies/cluster_provided:config",
"//source/extensions/load_balancing_policies/least_request:config",
"//source/extensions/load_balancing_policies/maglev:config",
"//source/extensions/load_balancing_policies/random:config",
"//source/extensions/load_balancing_policies/ring_hash:config",
"//source/extensions/load_balancing_policies/round_robin:config",
"//source/extensions/transport_sockets/raw_buffer:config",
"//test/common/upstream:utility_lib",
"//test/extensions/dynamic_modules:util",
Expand Down
119 changes: 116 additions & 3 deletions test/extensions/clusters/dynamic_modules/cluster_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -207,8 +207,52 @@ TEST_F(DynamicModuleClusterTest, CreationWithClusterConfig) {
EXPECT_NE(nullptr, result->second);
}

// Test that a non-CLUSTER_PROVIDED lb_policy is rejected.
TEST_F(DynamicModuleClusterTest, InvalidLbPolicy) {
// Test that CLUSTER_PROVIDED lb_policy returns a non-null thread-aware LB.
TEST_F(DynamicModuleClusterTest, ClusterProvidedLbPolicy) {
const std::string yaml = R"EOF(
name: test_cluster
connect_timeout: 0.25s
lb_policy: CLUSTER_PROVIDED
cluster_type:
name: envoy.clusters.dynamic_modules
typed_config:
"@type": type.googleapis.com/envoy.extensions.clusters.dynamic_modules.v3.ClusterConfig
dynamic_module_config:
name: cluster_no_op
cluster_name: test
)EOF";

auto result = createCluster(yaml);
ASSERT_TRUE(result.ok()) << result.status().message();
EXPECT_NE(nullptr, result->first);
// CLUSTER_PROVIDED should return a non-null thread-aware LB (module LB).
EXPECT_NE(nullptr, result->second);
}

// Test that LEAST_REQUEST lb_policy is accepted and returns nullptr thread-aware LB.
TEST_F(DynamicModuleClusterTest, LeastRequestLbPolicy) {
const std::string yaml = R"EOF(
name: test_cluster
connect_timeout: 0.25s
lb_policy: LEAST_REQUEST
cluster_type:
name: envoy.clusters.dynamic_modules
typed_config:
"@type": type.googleapis.com/envoy.extensions.clusters.dynamic_modules.v3.ClusterConfig
dynamic_module_config:
name: cluster_no_op
cluster_name: test
)EOF";

auto result = createCluster(yaml);
ASSERT_TRUE(result.ok()) << result.status().message();
EXPECT_NE(nullptr, result->first);
// Native LB policies should return nullptr thread-aware LB.
EXPECT_EQ(nullptr, result->second);
}

// Test that ROUND_ROBIN lb_policy is accepted and returns nullptr thread-aware LB.
TEST_F(DynamicModuleClusterTest, RoundRobinLbPolicy) {
const std::string yaml = R"EOF(
name: test_cluster
connect_timeout: 0.25s
Expand All @@ -223,7 +267,76 @@ lb_policy: ROUND_ROBIN
)EOF";

auto result = createCluster(yaml);
ASSERT_THAT(result, HasStatusMessage(testing::HasSubstr("CLUSTER_PROVIDED")));
ASSERT_TRUE(result.ok()) << result.status().message();
EXPECT_NE(nullptr, result->first);
// Native LB policies should return nullptr thread-aware LB.
EXPECT_EQ(nullptr, result->second);
}

// Test that RANDOM lb_policy is accepted and returns nullptr thread-aware LB.
TEST_F(DynamicModuleClusterTest, RandomLbPolicy) {
const std::string yaml = R"EOF(
name: test_cluster
connect_timeout: 0.25s
lb_policy: RANDOM
cluster_type:
name: envoy.clusters.dynamic_modules
typed_config:
"@type": type.googleapis.com/envoy.extensions.clusters.dynamic_modules.v3.ClusterConfig
dynamic_module_config:
name: cluster_no_op
cluster_name: test
)EOF";

auto result = createCluster(yaml);
ASSERT_TRUE(result.ok()) << result.status().message();
EXPECT_NE(nullptr, result->first);
// Native LB policies should return nullptr thread-aware LB.
EXPECT_EQ(nullptr, result->second);
}

// Test that RING_HASH lb_policy is accepted and returns nullptr thread-aware LB.
TEST_F(DynamicModuleClusterTest, RingHashLbPolicy) {
const std::string yaml = R"EOF(
name: test_cluster
connect_timeout: 0.25s
lb_policy: RING_HASH
cluster_type:
name: envoy.clusters.dynamic_modules
typed_config:
"@type": type.googleapis.com/envoy.extensions.clusters.dynamic_modules.v3.ClusterConfig
dynamic_module_config:
name: cluster_no_op
cluster_name: test
)EOF";

auto result = createCluster(yaml);
ASSERT_TRUE(result.ok()) << result.status().message();
EXPECT_NE(nullptr, result->first);
// Native LB policies should return nullptr thread-aware LB.
EXPECT_EQ(nullptr, result->second);
}

// Test that MAGLEV lb_policy is accepted and returns nullptr thread-aware LB.
TEST_F(DynamicModuleClusterTest, MaglevLbPolicy) {
const std::string yaml = R"EOF(
name: test_cluster
connect_timeout: 0.25s
lb_policy: MAGLEV
cluster_type:
name: envoy.clusters.dynamic_modules
typed_config:
"@type": type.googleapis.com/envoy.extensions.clusters.dynamic_modules.v3.ClusterConfig
dynamic_module_config:
name: cluster_no_op
cluster_name: test
)EOF";

auto result = createCluster(yaml);
ASSERT_TRUE(result.ok()) << result.status().message();
EXPECT_NE(nullptr, result->first);
// Native LB policies should return nullptr thread-aware LB.
EXPECT_EQ(nullptr, result->second);
}

// Test that a missing module fails gracefully.
Expand Down
65 changes: 65 additions & 0 deletions test/extensions/clusters/dynamic_modules/integration_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -510,6 +510,71 @@ TEST_P(DynamicModuleClusterDynamicMetadataIntegrationTest, SetsDynamicMetadataDu
EXPECT_EQ("1234 test_value", log);
}

// =============================================================================
// Native LB test: cluster with LEAST_REQUEST policy (native LB, not module LB).
// =============================================================================

class DynamicModuleClusterNativeLbIntegrationTest
: public testing::TestWithParam<Network::Address::IpVersion>,
public HttpIntegrationTest {
public:
DynamicModuleClusterNativeLbIntegrationTest()
: HttpIntegrationTest(Http::CodecType::HTTP1, GetParam()) {}

void initializeWithNativeLb() {
TestEnvironment::setEnvVar(
"ENVOY_DYNAMIC_MODULES_SEARCH_PATH",
TestEnvironment::runfilesPath("test/extensions/dynamic_modules/test_data/rust"), 1);

config_helper_.addConfigModifier([this](envoy::config::bootstrap::v3::Bootstrap& bootstrap) {
auto* cluster = bootstrap.mutable_static_resources()->mutable_clusters(0);
const std::string upstream_address = fake_upstreams_[0]->localAddress()->asString();

cluster->set_name("cluster_0");
// Use LEAST_REQUEST policy instead of CLUSTER_PROVIDED to test native LB.
cluster->set_lb_policy(envoy::config::cluster::v3::Cluster::LEAST_REQUEST);
cluster->clear_load_assignment();

envoy::extensions::clusters::dynamic_modules::v3::ClusterConfig dec_config;
dec_config.mutable_dynamic_module_config()->set_name("cluster_integration_test");
dec_config.set_cluster_name("native_lb_test");

Protobuf::StringValue config_proto;
config_proto.set_value(upstream_address);
std::ignore = dec_config.mutable_cluster_config()->PackFrom(config_proto);

cluster->mutable_cluster_type()->set_name("envoy.clusters.dynamic_modules");
std::ignore = cluster->mutable_cluster_type()->mutable_typed_config()->PackFrom(dec_config);
});

HttpIntegrationTest::initialize();
}
};

INSTANTIATE_TEST_SUITE_P(IpVersions, DynamicModuleClusterNativeLbIntegrationTest,
testing::ValuesIn(TestEnvironment::getIpVersionsForTest()),
TestUtility::ipTestParamsToString);

// A native lb_policy makes Envoy build its factory load balancer instead of the module's. The
// request routes to the upstream, and the module's choose_host must not run: the module provides
// a load balancer, but native_lb_requests stays 0 because Envoy never calls into it.
TEST_P(DynamicModuleClusterNativeLbIntegrationTest, NativeLbWithLeastRequestPolicy) {
initializeWithNativeLb();
codec_client_ = makeHttpConnection(makeClientConnection(lookupPort("http")));

auto response =
sendRequestAndWaitForResponse(default_request_headers_, 0, default_response_headers_, 0);

EXPECT_TRUE(upstream_request_->complete());
EXPECT_TRUE(response->complete());
EXPECT_EQ("200", response->headers().getStatusValue());

// Native LB selected the host; the module's choose_host was never called, so its
// native_lb_requests counter is unincremented (absent or 0).
auto choose_host = test_server_->counter("dynamicmodulescustom.native_lb_requests");
EXPECT_TRUE(choose_host == nullptr || choose_host->value() == 0);
}

} // namespace DynamicModules
} // namespace Clusters
} // namespace Extensions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,13 @@ impl Cluster for DynamicMetadataWriterCluster {
envoy_cluster.pre_init_complete();
}

fn new_load_balancer(&self, _envoy_lb: &dyn EnvoyClusterLoadBalancer) -> Box<dyn ClusterLb> {
Box::new(DynamicMetadataWriterLb {
fn new_load_balancer(
&self,
_envoy_lb: &dyn EnvoyClusterLoadBalancer,
) -> Option<Box<dyn ClusterLb>> {
Some(Box::new(DynamicMetadataWriterLb {
hosts: self.hosts.clone(),
})
}))
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,10 +128,13 @@ impl Cluster for FilterStateReaderCluster {
envoy_cluster.pre_init_complete();
}

fn new_load_balancer(&self, _envoy_lb: &dyn EnvoyClusterLoadBalancer) -> Box<dyn ClusterLb> {
Box::new(FilterStateReaderLb {
fn new_load_balancer(
&self,
_envoy_lb: &dyn EnvoyClusterLoadBalancer,
) -> Option<Box<dyn ClusterLb>> {
Some(Box::new(FilterStateReaderLb {
hosts: self.hosts.clone(),
})
}))
}
}

Expand Down Expand Up @@ -200,10 +203,13 @@ impl Cluster for FilterStateWriterCluster {
envoy_cluster.pre_init_complete();
}

fn new_load_balancer(&self, _envoy_lb: &dyn EnvoyClusterLoadBalancer) -> Box<dyn ClusterLb> {
Box::new(FilterStateWriterLb {
fn new_load_balancer(
&self,
_envoy_lb: &dyn EnvoyClusterLoadBalancer,
) -> Option<Box<dyn ClusterLb>> {
Some(Box::new(FilterStateWriterLb {
hosts: self.hosts.clone(),
})
}))
}
}

Expand Down
Loading
Loading