This is an automated email from the ASF dual-hosted git repository.
SteNicholas pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/celeborn.git
The following commit(s) were added to refs/heads/main by this push:
new f7b878e137 [CELEBORN-2417] Pass CelebornConf properties through the
C++ FFI
f7b878e137 is described below
commit f7b878e137c582718094f44c7f8494cc8bd662f3
Author: Yu Gan <[email protected]>
AuthorDate: Tue Aug 11 20:11:09 2026 +0800
[CELEBORN-2417] Pass CelebornConf properties through the C++ FFI
### What changes were proposed in this pull request?
Replace the per-option parameters of `celeborn_ffi_create_client` with a
generic `(keys, values, num_props)` property array, and update the Rust wrapper
and examples accordingly.
C ABI:
```c
-celeborn_ffi_handle* celeborn_ffi_create_client(
- const char* app_id, size_t app_id_len,
- int32_t push_buffer_max_size,
- const char* codec, size_t codec_len,
- char** err_out);
+celeborn_ffi_handle* celeborn_ffi_create_client(
+ const char* app_id, size_t app_id_len,
+ const char* const* keys,
+ const char* const* values,
+ size_t num_props,
+ char** err_out);
```
Each pair is fed to `CelebornConf::registerProperty`, which already
validates key names and value formats. Null `keys`/`values` and null entries at
any index are rejected at the boundary with a descriptive error rather than
segfaulting.
On the Rust side, `Config::push_buffer_max_size` and
`Config::shuffle_compression_codec` are replaced by `properties: Vec<(String,
String)>` with a `set_property()` builder applied in insertion order:
```rust
let mut config = Config::new(app_id);
config.set_property("celeborn.client.shuffle.compression.codec", "ZSTD");
```
### Why are the changes needed?
Only `celeborn.client.push.buffer.max.size` and
`celeborn.client.shuffle.compression.codec` were reachable through the FFI.
Every further setting a binding wants to expose costs a C ABI break plus a
matching change in each downstream language wrapper, so in practice the rest of
`CelebornConf` is unavailable to non-Java clients. Passing properties through
verbatim means a binding picks up new configuration keys with no ABI change and
no code change at all.
It also removes a duplicated source of truth. The Rust wrapper carried its
own codec allow-list (`NONE` / `LZ4` / `ZSTD`) that the C++ side already owns,
so the two could drift; validation is now left to `registerProperty`. The
Rust-side checks that remain are the ones that are genuinely client-side: empty
`app_id`, non-positive port, empty property key, and interior NUL bytes in a
property string.
### Does this PR resolve a correctness bug?
- [ ] Yes
### Does this PR introduce _any_ user-facing change?
- [x] Yes
This is a deliberate break of the `celeborn_ffi_create_client` signature
and of the `Config` struct in the `celeborn-client` crate. Neither has a
released consumer outside this repository — the C ABI and the Rust crate are
both new and unpublished — and the two in-tree examples under `rust/examples/`
are updated in the same change. No Celeborn configuration key, Java API or wire
protocol is affected.
### How was this patch tested?
Unit tests in `rust/celeborn-client/src/lib.rs`.
`validate_rejects_unknown_codec` is dropped along with the allow-list it
covered; the following are added or reworked:
- `validate_accepts_valid_args`
- `validate_rejects_empty_property_key`
- `set_property_accumulates_native_keys` — properties are recorded in
insertion order
- `to_c_strings_rejects_interior_nul`
`cargo test -p celeborn-client` passes 6/6. `cargo clippy` reports one
`too_many_arguments` warning on `push_data`, which is pre-existing on `main`
and untouched here; `cargo fmt --check` reports two diffs in `shutdown`/`Drop`,
also pre-existing and untouched.
Note that no CI workflow currently builds or tests the `rust/` crates, so
these tests do not run in CI today — the `Celeborn Cpp Integration Test`
workflow compiles the C++ side including `CelebornFfi.cc`, but nothing
exercises the binding. If reviewers want Rust coverage wired into CI, I am
happy to do it in a follow-up.
Closes #3795 from yugan95/CELEBORN-2417.
Authored-by: Yu Gan <[email protected]>
Signed-off-by: Nicholas Jiang <[email protected]>
---
cpp/celeborn/ffi/CelebornFfi.cc | 27 +++++----
cpp/celeborn/ffi/CelebornFfi.h | 12 +++-
rust/celeborn-client-sys/src/lib.rs | 10 +++-
rust/celeborn-client/src/lib.rs | 106 +++++++++++++++++++++++++++---------
rust/examples/data_sum_reader.rs | 2 +-
rust/examples/data_sum_writer.rs | 2 +-
6 files changed, 110 insertions(+), 49 deletions(-)
diff --git a/cpp/celeborn/ffi/CelebornFfi.cc b/cpp/celeborn/ffi/CelebornFfi.cc
index 10a00081aa..331306a8cc 100644
--- a/cpp/celeborn/ffi/CelebornFfi.cc
+++ b/cpp/celeborn/ffi/CelebornFfi.cc
@@ -110,16 +110,16 @@ void celeborn_ffi_free_buffer(uint8_t* data) {
celeborn_ffi_handle* celeborn_ffi_create_client(
const char* app_id,
size_t app_id_len,
- int32_t push_buffer_max_size,
- const char* codec,
- size_t codec_len,
+ const char* const* keys,
+ const char* const* values,
+ size_t num_props,
char** err_out) {
if (app_id == nullptr && app_id_len > 0) {
set_error(err_out, "null pointer for argument 'app_id'");
return nullptr;
}
- if (codec == nullptr && codec_len > 0) {
- set_error(err_out, "null pointer for argument 'codec'");
+ if (num_props > 0 && (keys == nullptr || values == nullptr)) {
+ set_error(err_out, "null pointer for argument 'keys' or 'values'");
return nullptr;
}
try {
@@ -129,15 +129,14 @@ celeborn_ffi_handle* celeborn_ffi_create_client(
}
impl->conf = std::make_shared<celeborn::conf::CelebornConf>();
- if (push_buffer_max_size > 0) {
- impl->conf->registerProperty(
- celeborn::conf::CelebornConf::kClientPushBufferMaxSize,
- std::to_string(push_buffer_max_size) + "b");
- }
- if (codec_len > 0) {
- impl->conf->registerProperty(
- celeborn::conf::CelebornConf::kShuffleCompressionCodec,
- std::string(codec, codec_len));
+ for (size_t i = 0; i < num_props; i++) {
+ if (keys[i] == nullptr || values[i] == nullptr) {
+ set_error(
+ err_out,
+ "null key or value at property index " + std::to_string(i));
+ return nullptr;
+ }
+ impl->conf->registerProperty(keys[i], values[i]);
}
impl->endpoint =
diff --git a/cpp/celeborn/ffi/CelebornFfi.h b/cpp/celeborn/ffi/CelebornFfi.h
index e686113baf..962f75cc15 100644
--- a/cpp/celeborn/ffi/CelebornFfi.h
+++ b/cpp/celeborn/ffi/CelebornFfi.h
@@ -53,13 +53,19 @@ void celeborn_ffi_free_error(char* err);
// Releases a buffer returned via celeborn_ffi_read_partition_full.
void celeborn_ffi_free_buffer(uint8_t* data);
+// Creates a client configured from `num_props` key/value pairs, each a
+// NUL-terminated string naming a CelebornConf property (for example
+// "celeborn.client.push.buffer.max.size"). Passing the properties through
+// verbatim means bindings pick up new configuration keys without an ABI
+// change; unknown keys are rejected by CelebornConf::registerProperty.
+//
// Returns NULL on failure; in that case *err_out is set.
celeborn_ffi_handle* celeborn_ffi_create_client(
const char* app_id,
size_t app_id_len,
- int32_t push_buffer_max_size,
- const char* codec,
- size_t codec_len,
+ const char* const* keys,
+ const char* const* values,
+ size_t num_props,
char** err_out);
celeborn_ffi_status celeborn_ffi_setup_lifecycle_manager(
diff --git a/rust/celeborn-client-sys/src/lib.rs
b/rust/celeborn-client-sys/src/lib.rs
index 811e67fa7b..9cedd0f617 100644
--- a/rust/celeborn-client-sys/src/lib.rs
+++ b/rust/celeborn-client-sys/src/lib.rs
@@ -53,12 +53,16 @@ extern "C" {
pub fn celeborn_ffi_free_error(err: *mut c_char);
pub fn celeborn_ffi_free_buffer(data: *mut u8);
+ /// Creates a client configured from `num_props` NUL-terminated
+ /// key/value pairs, each naming a `CelebornConf` property. Passing the
+ /// properties through verbatim means new configuration keys need no
+ /// change to this signature.
pub fn celeborn_ffi_create_client(
app_id: *const c_char,
app_id_len: usize,
- push_buffer_max_size: i32,
- codec: *const c_char,
- codec_len: usize,
+ keys: *const *const c_char,
+ values: *const *const c_char,
+ num_props: usize,
err_out: *mut *mut c_char,
) -> *mut celeborn_ffi_handle;
diff --git a/rust/celeborn-client/src/lib.rs b/rust/celeborn-client/src/lib.rs
index 6b73cee79d..b9bb4f69d7 100644
--- a/rust/celeborn-client/src/lib.rs
+++ b/rust/celeborn-client/src/lib.rs
@@ -16,6 +16,7 @@
//! Rust-friendly wrapper around `celeborn-client-sys` (raw C ABI bindings
//! to `libceleborn_client.{so,dylib}`).
+use std::ffi::CString;
use std::marker::PhantomData;
use std::os::raw::c_char;
use std::ptr;
@@ -46,20 +47,27 @@ unsafe fn ffi_error(err: *mut c_char) -> Error {
/// Configuration for connecting to a Celeborn LifecycleManager.
pub struct Config {
pub app_id: String,
- /// Max push buffer size in bytes. 0 means use cpp default (64kB).
- pub push_buffer_max_size: i32,
- /// Compression codec: "NONE", "LZ4", or "ZSTD".
- pub shuffle_compression_codec: String,
+ /// Native `CelebornConf` properties, forwarded verbatim to the C++
+ /// client, e.g. `("celeborn.client.push.buffer.max.size", "64k")`.
+ /// Anything the C++ client understands can be set here without a change
+ /// to this crate; unknown keys are rejected at connect time.
+ pub properties: Vec<(String, String)>,
}
impl Config {
pub fn new(app_id: String) -> Self {
Self {
app_id,
- push_buffer_max_size: 0,
- shuffle_compression_codec: "NONE".to_string(),
+ properties: Vec::new(),
}
}
+
+ /// Sets a native `CelebornConf` property. Properties are applied in
+ /// insertion order, so a later entry for the same key wins.
+ pub fn set_property(&mut self, key: impl Into<String>, value: impl
Into<String>) -> &mut Self {
+ self.properties.push((key.into(), value.into()));
+ self
+ }
}
/// A Rust-friendly Celeborn shuffle client backed by the C++ implementation.
@@ -118,28 +126,44 @@ fn validate_connect_args(config: &Config, lm_port: i32)
-> Result<()> {
if lm_port <= 0 {
return Err(Error::InvalidArg("lm_port must be > 0"));
}
- let valid_codecs = ["NONE", "LZ4", "ZSTD"];
- if !valid_codecs.contains(&config.shuffle_compression_codec.as_str()) {
- return Err(Error::InvalidArg(
- "shuffle_compression_codec must be NONE, LZ4, or ZSTD",
- ));
+ if config.properties.iter().any(|(key, _)| key.is_empty()) {
+ return Err(Error::InvalidArg("property key is empty"));
}
Ok(())
}
+/// Copies borrowed strings into NUL-terminated `CString`s for the C ABI.
+fn to_c_strings<'a>(items: impl Iterator<Item = &'a str>) ->
Result<Vec<CString>> {
+ items
+ .map(|item| {
+ CString::new(item)
+ .map_err(|_| Error::InvalidArg("property contains an interior
NUL byte"))
+ })
+ .collect()
+}
+
impl ShuffleClient {
/// Connect to a running LifecycleManager at `lm_host:lm_port`.
pub fn connect(config: Config, lm_host: &str, lm_port: i32) ->
Result<Self> {
validate_connect_args(&config, lm_port)?;
+ // The C++ side expects NUL-terminated strings, so the properties are
+ // copied into CStrings. `keys`/`values` own those buffers and must
+ // stay alive for the duration of the call below; `key_ptrs`/
+ // `value_ptrs` only borrow from them.
+ let keys = to_c_strings(config.properties.iter().map(|(key, _)|
key.as_str()))?;
+ let values = to_c_strings(config.properties.iter().map(|(_, value)|
value.as_str()))?;
+ let key_ptrs: Vec<*const c_char> = keys.iter().map(|key|
key.as_ptr()).collect();
+ let value_ptrs: Vec<*const c_char> = values.iter().map(|value|
value.as_ptr()).collect();
+
let mut err: *mut c_char = ptr::null_mut();
let handle = unsafe {
sys::celeborn_ffi_create_client(
config.app_id.as_ptr() as *const c_char,
config.app_id.len(),
- config.push_buffer_max_size,
- config.shuffle_compression_codec.as_ptr() as *const c_char,
- config.shuffle_compression_codec.len(),
+ key_ptrs.as_ptr(),
+ value_ptrs.as_ptr(),
+ key_ptrs.len(),
&mut err,
)
};
@@ -433,30 +457,27 @@ impl<'client> Drop for PartitionReader<'client> {
mod tests {
use super::*;
- fn config_with(app_id: &str, codec: &str) -> Config {
- let mut config = Config::new(app_id.to_string());
- config.shuffle_compression_codec = codec.to_string();
- config
+ fn config_with(app_id: &str) -> Config {
+ Config::new(app_id.to_string())
}
#[test]
- fn validate_accepts_supported_codecs() {
- for codec in ["NONE", "LZ4", "ZSTD"] {
- let config = config_with("app", codec);
- assert!(validate_connect_args(&config, 39099).is_ok());
- }
+ fn validate_accepts_valid_args() {
+ let mut config = config_with("app");
+ config.set_property("celeborn.client.shuffle.compression.codec",
"LZ4");
+ assert!(validate_connect_args(&config, 39099).is_ok());
}
#[test]
fn validate_rejects_empty_app_id() {
- let config = config_with("", "NONE");
+ let config = config_with("");
let err = validate_connect_args(&config, 39099).unwrap_err();
assert!(matches!(err, Error::InvalidArg("app_id is empty")));
}
#[test]
fn validate_rejects_non_positive_port() {
- let config = config_with("app", "NONE");
+ let config = config_with("app");
assert!(matches!(
validate_connect_args(&config, 0).unwrap_err(),
Error::InvalidArg("lm_port must be > 0")
@@ -468,10 +489,41 @@ mod tests {
}
#[test]
- fn validate_rejects_unknown_codec() {
- let config = config_with("app", "GZIP");
+ fn validate_rejects_empty_property_key() {
+ let mut config = config_with("app");
+ config.set_property("", "value");
assert!(matches!(
validate_connect_args(&config, 39099).unwrap_err(),
+ Error::InvalidArg("property key is empty")
+ ));
+ }
+
+ #[test]
+ fn set_property_accumulates_native_keys() {
+ let mut config = config_with("app");
+ config
+ .set_property("celeborn.client.push.buffer.max.size", "64k")
+ .set_property("celeborn.client.shuffle.compression.codec", "ZSTD");
+ assert_eq!(
+ config.properties,
+ vec![
+ (
+ "celeborn.client.push.buffer.max.size".to_string(),
+ "64k".to_string()
+ ),
+ (
+ "celeborn.client.shuffle.compression.codec".to_string(),
+ "ZSTD".to_string()
+ ),
+ ]
+ );
+ }
+
+ #[test]
+ fn to_c_strings_rejects_interior_nul() {
+ assert!(to_c_strings(["ok"].into_iter()).is_ok());
+ assert!(matches!(
+ to_c_strings(["ba\0d"].into_iter()).unwrap_err(),
Error::InvalidArg(_)
));
}
diff --git a/rust/examples/data_sum_reader.rs b/rust/examples/data_sum_reader.rs
index 18c34a26fd..d2a81650ca 100644
--- a/rust/examples/data_sum_reader.rs
+++ b/rust/examples/data_sum_reader.rs
@@ -62,7 +62,7 @@ fn main() {
);
let mut config = Config::new(app_id);
- config.shuffle_compression_codec = compress_codec;
+ config.set_property("celeborn.client.shuffle.compression.codec",
compress_codec);
let client =
Arc::new(ShuffleClient::connect(config, lm_host,
lm_port).expect("Failed to connect to LM"));
diff --git a/rust/examples/data_sum_writer.rs b/rust/examples/data_sum_writer.rs
index 5d7780bd50..a3213d642b 100644
--- a/rust/examples/data_sum_writer.rs
+++ b/rust/examples/data_sum_writer.rs
@@ -64,7 +64,7 @@ fn main() {
);
let mut config = Config::new(app_id);
- config.shuffle_compression_codec = compress_codec;
+ config.set_property("celeborn.client.shuffle.compression.codec",
compress_codec);
let client =
Arc::new(ShuffleClient::connect(config, lm_host,
lm_port).expect("Failed to connect to LM"));