Skip to content

Commit aa42e83

Browse files
authored
dynamic_modules: add packed-address getter for cluster LB member updates (#45626)
**Commit Message:** dynamic_modules: add packed-address getter for cluster LB member updates **Additional Description:** A dynamic module cluster LB that keeps its own address-to-host map fills it during on_host_membership_update via get_member_update_host_address, which returns each host's address as a formatted string. In clusters with millions of hosts this dominates the update cost. Envoy formats the sockaddr into a string, copies it across the ABI, and the module hashes and byte-compares it on every insert and lookup; yet the string is derived data, since Envoy already stores the address in parsed form. This adds get_member_update_host_packed_address, which reads the IP bytes and port straight from the sockaddr so a module can key its map by an integer instead. The getter is additive; existing getters and the on_host_membership_update contract are unchanged. Risk Level: Low Testing: Unit and Integration Tests Docs Changes: Release Notes: Platform Specific Features: --------- Signed-off-by: Basundhara Chakrabarty <basundhara17061996@gmail.com>
1 parent b54720e commit aa42e83

8 files changed

Lines changed: 427 additions & 0 deletions

File tree

source/extensions/clusters/dynamic_modules/abi_impl.cc

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
22

33
// This file provides host-side implementations for the cluster dynamic module ABI callbacks.
44

5+
#include <cstring>
6+
57
#include "source/common/common/assert.h"
8+
#include "source/common/common/safe_memcpy.h"
69
#include "source/common/common/thread.h"
710
#include "source/common/http/message_impl.h"
811
#include "source/common/router/string_accessor_impl.h"
@@ -1367,4 +1370,44 @@ envoy_dynamic_module_callback_cluster_lb_get_member_update_host(
13671370
return const_cast<Envoy::Upstream::Host*>((*hosts)[index].get());
13681371
}
13691372

1373+
bool envoy_dynamic_module_callback_cluster_lb_get_member_update_host_packed_address(
1374+
envoy_dynamic_module_type_cluster_lb_envoy_ptr lb_envoy_ptr, size_t index, bool is_added,
1375+
envoy_dynamic_module_type_packed_address* result) {
1376+
if (lb_envoy_ptr == nullptr || result == nullptr) {
1377+
return false;
1378+
}
1379+
const auto* hosts =
1380+
is_added ? getLb(lb_envoy_ptr)->hostsAdded() : getLb(lb_envoy_ptr)->hostsRemoved();
1381+
if (hosts == nullptr || index >= hosts->size()) {
1382+
return false;
1383+
}
1384+
// Null for a pipe (non-IP) address; the packed representation only covers IP addresses.
1385+
const auto* ip = (*hosts)[index]->address()->ip();
1386+
if (ip == nullptr) {
1387+
return false;
1388+
}
1389+
std::memset(result->address_bytes, 0, sizeof(result->address_bytes));
1390+
// Both accessors return the address in network byte order, read straight from the sockaddr; see
1391+
// Ipv4/Ipv6Instance in source/common/network/address_impl.{h,cc}.
1392+
switch (ip->version()) {
1393+
case Envoy::Network::Address::IpVersion::v4: {
1394+
result->family = 4;
1395+
const uint32_t v4 = ip->ipv4()->address();
1396+
Envoy::safeMemcpyUnsafeDst(result->address_bytes, &v4);
1397+
break;
1398+
}
1399+
case Envoy::Network::Address::IpVersion::v6: {
1400+
result->family = 6;
1401+
const absl::uint128 v6 = ip->ipv6()->address();
1402+
Envoy::safeMemcpyUnsafeDst(result->address_bytes, &v6);
1403+
break;
1404+
}
1405+
default:
1406+
IS_ENVOY_BUG("unexpected IP version in cluster LB packed address getter");
1407+
return false;
1408+
}
1409+
result->port = static_cast<uint16_t>(ip->port());
1410+
return true;
1411+
}
1412+
13701413
} // extern "C"

source/extensions/dynamic_modules/abi/abi.h

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10030,6 +10030,43 @@ envoy_dynamic_module_type_cluster_host_envoy_ptr
1003010030
envoy_dynamic_module_callback_cluster_lb_get_member_update_host(
1003110031
envoy_dynamic_module_type_cluster_lb_envoy_ptr lb_envoy_ptr, size_t index, bool is_added);
1003210032

10033+
/**
10034+
* envoy_dynamic_module_type_packed_address holds a host's IP address and port as packed integers,
10035+
* read directly from the host's parsed sockaddr without any string formatting. This is the output
10036+
* of envoy_dynamic_module_callback_cluster_lb_get_member_update_host_packed_address.
10037+
*
10038+
* @param address_bytes is the IP address in network byte order. An IPv4 address occupies the first
10039+
* four bytes; an IPv6 address occupies all sixteen. Bytes beyond the address are zeroed.
10040+
* @param port is the port in host byte order.
10041+
* @param family is 4 for an IPv4 address and 6 for an IPv6 address.
10042+
*/
10043+
typedef struct envoy_dynamic_module_type_packed_address {
10044+
uint8_t address_bytes[16];
10045+
uint16_t port;
10046+
uint8_t family;
10047+
} envoy_dynamic_module_type_packed_address;
10048+
10049+
/**
10050+
* envoy_dynamic_module_callback_cluster_lb_get_member_update_host_packed_address returns the
10051+
* address of an added or removed host during the on_cluster_lb_on_host_membership_update event hook
10052+
* as packed integers, read directly from the host's sockaddr with no string formatting or
10053+
* allocation. This is the packed-integer sibling of
10054+
* envoy_dynamic_module_callback_cluster_lb_get_member_update_host_address, intended for modules
10055+
* that key their own host maps by an integer rather than a string. It is only valid during
10056+
* envoy_dynamic_module_on_cluster_lb_on_host_membership_update.
10057+
*
10058+
* @param lb_envoy_ptr is the pointer to the Envoy cluster load balancer.
10059+
* @param index is the index of the host in the added or removed list.
10060+
* @param is_added is true to get an added host address, false to get a removed host address.
10061+
* @param result is the output buffer that receives the packed address. Its contents are unspecified
10062+
* when the callback returns false; callers must gate on the return value.
10063+
* @return true if the host was found and has an IP address, false on a null pointer, an
10064+
* out-of-bounds index, the callback not being active, or a non-IP (pipe) address.
10065+
*/
10066+
bool envoy_dynamic_module_callback_cluster_lb_get_member_update_host_packed_address(
10067+
envoy_dynamic_module_type_cluster_lb_envoy_ptr lb_envoy_ptr, size_t index, bool is_added,
10068+
envoy_dynamic_module_type_packed_address* result);
10069+
1003310070
// =============================================================================
1003410071
// =============================== Load Balancer ===============================
1003510072
// =============================================================================

source/extensions/dynamic_modules/abi_impl.cc

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -509,6 +509,15 @@ envoy_dynamic_module_callback_cluster_lb_get_member_update_host(
509509
return nullptr;
510510
}
511511

512+
__attribute__((weak)) bool
513+
envoy_dynamic_module_callback_cluster_lb_get_member_update_host_packed_address(
514+
envoy_dynamic_module_type_cluster_lb_envoy_ptr, size_t, bool,
515+
envoy_dynamic_module_type_packed_address*) {
516+
IS_ENVOY_BUG("envoy_dynamic_module_callback_cluster_lb_get_member_update_host_packed_address: "
517+
"not implemented in this context");
518+
return false;
519+
}
520+
512521
__attribute__((weak)) void envoy_dynamic_module_callback_cluster_pre_init_complete(
513522
envoy_dynamic_module_type_cluster_envoy_ptr) {
514523
IS_ENVOY_BUG("envoy_dynamic_module_callback_cluster_pre_init_complete: "

source/extensions/dynamic_modules/sdk/rust/src/cluster.rs

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,20 @@ pub enum HostSelectionResult {
124124
AsyncPending(Box<dyn AsyncHostSelectionHandle>),
125125
}
126126

127+
/// A host's IP address and port as packed integers.
128+
///
129+
/// This is the decoded form of the packed address returned by
130+
/// [`EnvoyClusterLoadBalancer::get_member_update_host_packed_address`]. The address bytes are in
131+
/// network byte order and the port is in host byte order, letting a module key its own host map by
132+
/// an integer rather than a formatted string.
133+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
134+
pub enum PackedAddress {
135+
/// An IPv4 address (4 bytes, network byte order) and port (host byte order).
136+
V4([u8; 4], u16),
137+
/// An IPv6 address (16 bytes, network byte order) and port (host byte order).
138+
V6([u8; 16], u16),
139+
}
140+
127141
/// A handle for canceling an in-progress asynchronous host selection.
128142
///
129143
/// When the stream is destroyed before async host selection completes (e.g., due to a timeout),
@@ -660,6 +674,23 @@ pub trait EnvoyClusterLoadBalancer: Send {
660674
index: usize,
661675
is_added: bool,
662676
) -> Option<abi::envoy_dynamic_module_type_cluster_host_envoy_ptr>;
677+
678+
/// Returns the address of an added or removed host during the
679+
/// [`ClusterLb::on_host_membership_update`] callback as packed integers.
680+
///
681+
/// Unlike [`EnvoyClusterLoadBalancer::get_member_update_host_address`], this reads the IP address
682+
/// and port directly from the host's sockaddr without formatting a string, so a module can key
683+
/// its own host map by an integer. It is only valid during the `on_host_membership_update`
684+
/// callback.
685+
///
686+
/// Set `is_added` to `true` to get an added host address, `false` for a removed host address.
687+
/// Returns `None` when the index is out of bounds, the callback is not active, or the host has a
688+
/// non-IP (pipe) address.
689+
fn get_member_update_host_packed_address(
690+
&self,
691+
index: usize,
692+
is_added: bool,
693+
) -> Option<PackedAddress>;
663694
}
664695

665696
/// Envoy-side scheduler that dispatches events to the main thread.
@@ -1496,6 +1527,38 @@ impl EnvoyClusterLoadBalancer for EnvoyClusterLoadBalancerImpl {
14961527
Some(host)
14971528
}
14981529
}
1530+
1531+
fn get_member_update_host_packed_address(
1532+
&self,
1533+
index: usize,
1534+
is_added: bool,
1535+
) -> Option<PackedAddress> {
1536+
let mut result = abi::envoy_dynamic_module_type_packed_address {
1537+
address_bytes: [0; 16],
1538+
port: 0,
1539+
family: 0,
1540+
};
1541+
let found = unsafe {
1542+
abi::envoy_dynamic_module_callback_cluster_lb_get_member_update_host_packed_address(
1543+
self.raw,
1544+
index,
1545+
is_added,
1546+
&mut result,
1547+
)
1548+
};
1549+
if !found {
1550+
return None;
1551+
}
1552+
match result.family {
1553+
4 => {
1554+
let mut v4 = [0u8; 4];
1555+
v4.copy_from_slice(&result.address_bytes[..4]);
1556+
Some(PackedAddress::V4(v4, result.port))
1557+
},
1558+
6 => Some(PackedAddress::V6(result.address_bytes, result.port)),
1559+
_ => None,
1560+
}
1561+
}
14991562
}
15001563

15011564
/// Implementation of [`EnvoyClusterMetrics`] that calls into the Envoy ABI.

source/extensions/dynamic_modules/sdk/rust/src/lib_test.rs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4259,6 +4259,16 @@ pub extern "C" fn envoy_dynamic_module_callback_cluster_lb_get_member_update_hos
42594259
std::ptr::null_mut()
42604260
}
42614261

4262+
#[no_mangle]
4263+
pub extern "C" fn envoy_dynamic_module_callback_cluster_lb_get_member_update_host_packed_address(
4264+
_lb_envoy_ptr: abi::envoy_dynamic_module_type_cluster_lb_envoy_ptr,
4265+
_index: usize,
4266+
_is_added: bool,
4267+
_result: *mut abi::envoy_dynamic_module_type_packed_address,
4268+
) -> bool {
4269+
false
4270+
}
4271+
42624272
#[no_mangle]
42634273
pub extern "C" fn envoy_dynamic_module_callback_cluster_lb_async_host_selection_complete(
42644274
_lb_envoy_ptr: abi::envoy_dynamic_module_type_cluster_lb_envoy_ptr,
@@ -5291,6 +5301,45 @@ fn test_cluster_lb_get_member_update_host_none() {
52915301
assert!(mock_lb.get_member_update_host(0, false).is_none());
52925302
}
52935303

5304+
#[test]
5305+
fn test_cluster_lb_get_member_update_host_packed_address() {
5306+
let mut mock_lb = cluster::MockEnvoyClusterLoadBalancer::new();
5307+
mock_lb
5308+
.expect_get_member_update_host_packed_address()
5309+
.withf(|index, is_added| *index == 0 && *is_added)
5310+
.returning(|_, _| Some(cluster::PackedAddress::V4([127, 0, 0, 1], 10001)));
5311+
mock_lb
5312+
.expect_get_member_update_host_packed_address()
5313+
.withf(|_, is_added| !*is_added)
5314+
.returning(|_, _| Some(cluster::PackedAddress::V6([1; 16], 10002)));
5315+
5316+
match mock_lb.get_member_update_host_packed_address(0, true) {
5317+
Some(cluster::PackedAddress::V4(addr, port)) => {
5318+
assert_eq!(addr, [127, 0, 0, 1]);
5319+
assert_eq!(port, 10001);
5320+
},
5321+
other => panic!("expected V4, got {other:?}"),
5322+
}
5323+
match mock_lb.get_member_update_host_packed_address(0, false) {
5324+
Some(cluster::PackedAddress::V6(addr, port)) => {
5325+
assert_eq!(addr, [1; 16]);
5326+
assert_eq!(port, 10002);
5327+
},
5328+
other => panic!("expected V6, got {other:?}"),
5329+
}
5330+
}
5331+
5332+
#[test]
5333+
fn test_cluster_lb_get_member_update_host_packed_address_none() {
5334+
let mut mock_lb = cluster::MockEnvoyClusterLoadBalancer::new();
5335+
mock_lb
5336+
.expect_get_member_update_host_packed_address()
5337+
.returning(|_, _| None);
5338+
assert!(mock_lb
5339+
.get_member_update_host_packed_address(0, false)
5340+
.is_none());
5341+
}
5342+
52945343
#[test]
52955344
fn test_cluster_lb_get_host_locality() {
52965345
let mut mock_lb = cluster::MockEnvoyClusterLoadBalancer::new();

test/extensions/clusters/dynamic_modules/cluster_test.cc

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3408,6 +3408,76 @@ TEST_F(DynamicModuleClusterTest, LbMemberUpdateHostPointer) {
34083408
envoy_dynamic_module_callback_cluster_lb_get_member_update_host(lb_ptr, 0, true));
34093409
}
34103410

3411+
// Test get_member_update_host_packed_address returns the packed IP/port during a membership update.
3412+
TEST_F(DynamicModuleClusterTest, LbMemberUpdateHostPackedAddress) {
3413+
auto result = createCluster(makeYamlConfig("cluster_no_op"));
3414+
ASSERT_TRUE(result.ok()) << result.status().message();
3415+
3416+
auto cluster = std::dynamic_pointer_cast<DynamicModuleCluster>(result->first);
3417+
auto handle = std::make_shared<DynamicModuleClusterHandle>(cluster);
3418+
auto lb_instance = std::make_unique<DynamicModuleLoadBalancer>(handle, cluster->prioritySet());
3419+
auto* lb_ptr = static_cast<void*>(lb_instance.get());
3420+
3421+
// Distinctive octets so every byte position is pinned, and the upper twelve bytes must stay
3422+
// zeroed (no leftover from a previous v6 read), exercising the memset.
3423+
auto v4_host = Upstream::makeTestHost(cluster->info(), "tcp://1.2.3.4:10001");
3424+
// Distinctive bytes at both ends and the interior so a reversed/byte-swapped copy is caught.
3425+
auto v6_host = Upstream::makeTestHost(cluster->info(), "tcp://[2001:db8:1122:3344::1]:10002");
3426+
auto pipe_host = Upstream::makeTestHost(cluster->info(), "unix:///tmp/dynamic_modules_test");
3427+
3428+
envoy_dynamic_module_type_packed_address addr{};
3429+
3430+
// Outside a membership update callback the borrowed vectors are null, so the getter fails.
3431+
EXPECT_FALSE(envoy_dynamic_module_callback_cluster_lb_get_member_update_host_packed_address(
3432+
lb_ptr, 0, true, &addr));
3433+
3434+
Upstream::HostVector added{v4_host, v6_host};
3435+
Upstream::HostVector removed{pipe_host};
3436+
DynamicModuleClusterTestPeer::setMemberUpdateHosts(*lb_instance, &added, &removed);
3437+
3438+
// IPv6 host first: all sixteen bytes hold the address in network byte order. Reading it before
3439+
// the IPv4 host leaves non-zero bytes in the buffer, so the IPv4 read below must zero them.
3440+
ASSERT_TRUE(envoy_dynamic_module_callback_cluster_lb_get_member_update_host_packed_address(
3441+
lb_ptr, 1, true, &addr));
3442+
EXPECT_EQ(6, addr.family);
3443+
EXPECT_EQ(10002, addr.port);
3444+
const uint8_t expected_v6[16] = {0x20, 0x01, 0x0d, 0xb8, 0x11, 0x22, 0x33, 0x44,
3445+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01};
3446+
for (size_t i = 0; i < 16; ++i) {
3447+
EXPECT_EQ(expected_v6[i], addr.address_bytes[i]) << "v6 byte " << i;
3448+
}
3449+
3450+
// IPv4 host: address in the first four bytes (network byte order), the upper twelve zeroed by the
3451+
// memset (which must clear the leftover IPv6 bytes above), port in host byte order.
3452+
ASSERT_TRUE(envoy_dynamic_module_callback_cluster_lb_get_member_update_host_packed_address(
3453+
lb_ptr, 0, true, &addr));
3454+
EXPECT_EQ(4, addr.family);
3455+
EXPECT_EQ(10001, addr.port);
3456+
const uint8_t expected_v4[16] = {1, 2, 3, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
3457+
for (size_t i = 0; i < 16; ++i) {
3458+
EXPECT_EQ(expected_v4[i], addr.address_bytes[i]) << "v4 byte " << i;
3459+
}
3460+
3461+
// A pipe (non-IP) address returns false.
3462+
EXPECT_FALSE(envoy_dynamic_module_callback_cluster_lb_get_member_update_host_packed_address(
3463+
lb_ptr, 0, false, &addr));
3464+
3465+
// Out-of-bounds index returns false.
3466+
EXPECT_FALSE(envoy_dynamic_module_callback_cluster_lb_get_member_update_host_packed_address(
3467+
lb_ptr, 2, true, &addr));
3468+
3469+
// Null load balancer and null result return false.
3470+
EXPECT_FALSE(envoy_dynamic_module_callback_cluster_lb_get_member_update_host_packed_address(
3471+
nullptr, 0, true, &addr));
3472+
EXPECT_FALSE(envoy_dynamic_module_callback_cluster_lb_get_member_update_host_packed_address(
3473+
lb_ptr, 0, true, nullptr));
3474+
3475+
// Once the callback ends the borrowed vectors are cleared and the getter fails again.
3476+
DynamicModuleClusterTestPeer::setMemberUpdateHosts(*lb_instance, nullptr, nullptr);
3477+
EXPECT_FALSE(envoy_dynamic_module_callback_cluster_lb_get_member_update_host_packed_address(
3478+
lb_ptr, 0, true, &addr));
3479+
}
3480+
34113481
// Test hosts-per-locality is correctly maintained with locality-aware hosts.
34123482
TEST_F(DynamicModuleClusterTest, HostsPerLocalityWithLocality) {
34133483
auto result = createCluster(makeYamlConfig("cluster_no_op"));

test/extensions/clusters/dynamic_modules/integration_test.cc

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,26 @@ TEST_P(DynamicModuleClusterIntegrationTest, WorkerLocalPrioritySetRebuild) {
179179
EXPECT_EQ("200", response->headers().getStatusValue());
180180
}
181181

182+
// Drives the packed member-update address getter end to end. Each worker load balancer reads the
183+
// added host's address both as a string and as packed integers and confirms they agree before
184+
// incrementing the counter, so this runs the real packing under both IPv4 and IPv6 params.
185+
TEST_P(DynamicModuleClusterIntegrationTest, MemberUpdatePackedAddress) {
186+
concurrency_ = 2;
187+
initializeWithDecCluster("member_update_packed_address");
188+
189+
// Each worker increments the counter once its packed address matches the formatted address.
190+
test_server_->waitForCounter("dynamicmodulescustom.packed_address_verified_total",
191+
testing::Ge(2));
192+
193+
codec_client_ = makeHttpConnection(makeClientConnection(lookupPort("http")));
194+
auto response =
195+
sendRequestAndWaitForResponse(default_request_headers_, 0, default_response_headers_, 0);
196+
197+
EXPECT_TRUE(upstream_request_->complete());
198+
EXPECT_TRUE(response->complete());
199+
EXPECT_EQ("200", response->headers().getStatusValue());
200+
}
201+
182202
// Verifies that the cluster lifecycle callbacks fire correctly during cluster
183203
// initialization.
184204
TEST_P(DynamicModuleClusterIntegrationTest, LifecycleCallbacks) {

0 commit comments

Comments
 (0)