Skip to content

Commit 433581a

Browse files
authored
[rust] Allow configuring remote log download directory (#4603)
* [rust] Allow configuring remote log download directory Add a configurable parent for remote log downloads, matching the Java client capability while preserving the Rust OS temporary-directory default. Each scanner owns an isolated temporary subdirectory and retains the existing cleanup lifecycle. Expose the option in Python and C++ and document it in both API trees. Cover directory creation, isolation, cleanup, invalid paths, Unix permission errors, and binding configuration. * [elixir] Expose the remote log download directory option --------- Co-authored-by: liuzhuo <liuzhuo@xiaohongshu.com>
1 parent 15ab4c0 commit 433581a

20 files changed

Lines changed: 388 additions & 6 deletions

File tree

‎fluss-rust/README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,7 @@ cargo build --example example-table --release
262262
| `scanner_log_fetch_max_bytes` | 16 MB | Max bytes per fetch request |
263263
| `scanner_log_fetch_wait_max_time_ms` | 500 ms | Max wait time for fetch |
264264
| `scanner_remote_log_read_concurrency` | 4 | Concurrency for remote log reads |
265+
| `scanner_io_tmp_dir` | OS temp dir/fluss | Parent for isolated remote log download directories |
265266
| `connect_timeout_ms` | 15 s | Connection timeout |
266267
| `security_sasl_username` / `security_sasl_password` | — | SASL PLAIN authentication |
267268

‎fluss-rust/bindings/cpp/include/fluss.hpp‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1645,6 +1645,8 @@ struct Configuration {
16451645
size_t remote_file_download_thread_num{3};
16461646
// Remote log read concurrency within one file (streaming read path)
16471647
size_t scanner_remote_log_read_concurrency{4};
1648+
// Parent directory for downloaded remote logs; empty uses the OS temporary directory plus `/fluss`
1649+
std::string scanner_io_tmp_dir;
16481650
// Maximum number of records returned in a single call to Poll() for LogScanner
16491651
size_t scanner_log_max_poll_records{500};
16501652
// Maximum bytes per fetch response for LogScanner (16 MB)

‎fluss-rust/bindings/cpp/src/ffi_converter.hpp‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,7 @@ inline ffi::FfiConfig to_ffi_config(const Configuration& config) {
223223
ffi_config.scanner_remote_log_prefetch_num = config.scanner_remote_log_prefetch_num;
224224
ffi_config.remote_file_download_thread_num = config.remote_file_download_thread_num;
225225
ffi_config.scanner_remote_log_read_concurrency = config.scanner_remote_log_read_concurrency;
226+
ffi_config.scanner_io_tmp_dir = rust::String(config.scanner_io_tmp_dir);
226227
ffi_config.scanner_log_max_poll_records = config.scanner_log_max_poll_records;
227228
ffi_config.scanner_log_fetch_max_bytes = config.scanner_log_fetch_max_bytes;
228229
ffi_config.scanner_log_fetch_min_bytes = config.scanner_log_fetch_min_bytes;

‎fluss-rust/bindings/cpp/src/lib.rs‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ mod ffi {
7474
scanner_remote_log_prefetch_num: usize,
7575
remote_file_download_thread_num: usize,
7676
scanner_remote_log_read_concurrency: usize,
77+
scanner_io_tmp_dir: String,
7778
scanner_log_max_poll_records: usize,
7879
scanner_log_fetch_max_bytes: i32,
7980
scanner_log_fetch_min_bytes: i32,
@@ -1193,6 +1194,8 @@ fn new_connection(config: &ffi::FfiConfig) -> ffi::FfiPtrResult {
11931194
scanner_remote_log_prefetch_num: config.scanner_remote_log_prefetch_num,
11941195
remote_file_download_thread_num: config.remote_file_download_thread_num,
11951196
scanner_remote_log_read_concurrency: config.scanner_remote_log_read_concurrency,
1197+
scanner_io_tmp_dir: (!config.scanner_io_tmp_dir.is_empty())
1198+
.then(|| config.scanner_io_tmp_dir.to_string()),
11961199
scanner_log_max_poll_records: config.scanner_log_max_poll_records,
11971200
scanner_log_fetch_max_bytes: config.scanner_log_fetch_max_bytes,
11981201
scanner_log_fetch_min_bytes: config.scanner_log_fetch_min_bytes,

‎fluss-rust/bindings/cpp/test/test_ffi_converter.cpp‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,15 @@ TEST(FfiConverterTest, KvBackpressureConfiguration) {
6262
EXPECT_TRUE(fluss::ErrorCode::IsRetriable(72));
6363
}
6464

65+
TEST(FfiConverterTest, RemoteLogDirectoryConfiguration) {
66+
fluss::Configuration config;
67+
EXPECT_TRUE(config.scanner_io_tmp_dir.empty());
68+
EXPECT_TRUE(fluss::utils::to_ffi_config(config).scanner_io_tmp_dir.empty());
69+
config.scanner_io_tmp_dir = "/tmp/fluss logs";
70+
auto ffi_config = fluss::utils::to_ffi_config(config);
71+
EXPECT_EQ(std::string(ffi_config.scanner_io_tmp_dir), "/tmp/fluss logs");
72+
}
73+
6574
TEST(FfiConverterTest, WriterRetryBackoffConfiguration) {
6675
fluss::Configuration config;
6776
EXPECT_EQ(config.writer_retry_backoff_ms, 100u);

‎fluss-rust/bindings/elixir/lib/fluss/config.ex‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ defmodule Fluss.Config do
4242
lookup_max_retries: nil,
4343
lookup_queue_size: nil,
4444
remote_file_download_thread_num: nil,
45+
scanner_io_tmp_dir: nil,
4546
scanner_log_fetch_max_bytes: nil,
4647
scanner_log_fetch_max_bytes_for_bucket: nil,
4748
scanner_log_fetch_min_bytes: nil,
@@ -78,6 +79,7 @@ defmodule Fluss.Config do
7879
lookup_max_retries: non_neg_integer() | nil,
7980
lookup_queue_size: non_neg_integer() | nil,
8081
remote_file_download_thread_num: non_neg_integer() | nil,
82+
scanner_io_tmp_dir: String.t() | nil,
8183
scanner_log_fetch_max_bytes: non_neg_integer() | nil,
8284
scanner_log_fetch_max_bytes_for_bucket: non_neg_integer() | nil,
8385
scanner_log_fetch_min_bytes: non_neg_integer() | nil,
@@ -149,6 +151,11 @@ defmodule Fluss.Config do
149151
when is_non_neg_integer(threads),
150152
do: %{config | remote_file_download_thread_num: threads}
151153

154+
@doc "Sets the parent directory for remote log downloads."
155+
@spec set_scanner_io_tmp_dir(t(), String.t()) :: t()
156+
def set_scanner_io_tmp_dir(%__MODULE__{} = config, path) when is_binary(path),
157+
do: %{config | scanner_io_tmp_dir: path}
158+
152159
@spec set_scanner_log_fetch_max_bytes(t(), non_neg_integer()) :: t()
153160
def set_scanner_log_fetch_max_bytes(%__MODULE__{} = config, max_bytes)
154161
when is_non_neg_integer(max_bytes),

‎fluss-rust/bindings/elixir/native/fluss_nif/src/config.rs‎

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ pub struct NifConfig {
3838
pub lookup_max_retries: Option<i32>,
3939
pub lookup_queue_size: Option<u64>,
4040
pub remote_file_download_thread_num: Option<u64>,
41+
pub scanner_io_tmp_dir: Option<String>,
4142
pub scanner_log_fetch_max_bytes: Option<i32>,
4243
pub scanner_log_fetch_max_bytes_for_bucket: Option<i32>,
4344
pub scanner_log_fetch_min_bytes: Option<i32>,
@@ -70,6 +71,7 @@ impl NifConfig {
7071
pub fn into_core(self) -> Config {
7172
let mut config = Config {
7273
bootstrap_servers: self.bootstrap_servers,
74+
scanner_io_tmp_dir: self.scanner_io_tmp_dir,
7375
..Config::default()
7476
};
7577
if let Some(timeout) = self.connect_timeout_ms {
@@ -177,3 +179,64 @@ impl NifConfig {
177179
config
178180
}
179181
}
182+
183+
#[cfg(test)]
184+
mod tests {
185+
use super::*;
186+
187+
fn default_config() -> NifConfig {
188+
NifConfig {
189+
bootstrap_servers: "localhost:9123".to_string(),
190+
connect_timeout_ms: None,
191+
lookup_batch_timeout_ms: None,
192+
lookup_max_batch_size: None,
193+
lookup_max_inflight_requests: None,
194+
lookup_max_retries: None,
195+
lookup_queue_size: None,
196+
remote_file_download_thread_num: None,
197+
scanner_io_tmp_dir: None,
198+
scanner_log_fetch_max_bytes: None,
199+
scanner_log_fetch_max_bytes_for_bucket: None,
200+
scanner_log_fetch_min_bytes: None,
201+
scanner_log_fetch_wait_max_time_ms: None,
202+
scanner_log_max_poll_records: None,
203+
scanner_remote_log_prefetch_num: None,
204+
scanner_remote_log_read_concurrency: None,
205+
security_protocol: None,
206+
security_sasl_mechanism: None,
207+
security_sasl_password: None,
208+
security_sasl_username: None,
209+
writer_acks: None,
210+
writer_batch_size: None,
211+
writer_batch_timeout_ms: None,
212+
writer_bucket_no_key_assigner: None,
213+
writer_buffer_memory_size: None,
214+
writer_buffer_wait_timeout_ms: None,
215+
writer_kv_backpressure_max_throttle_ms: None,
216+
writer_dynamic_batch_size_enabled: None,
217+
writer_dynamic_batch_size_min: None,
218+
writer_enable_idempotence: None,
219+
writer_max_inflight_requests_per_bucket: None,
220+
writer_request_max_size: None,
221+
writer_retries: None,
222+
writer_retry_backoff_ms: None,
223+
writer_retry_max_backoff_ms: None,
224+
}
225+
}
226+
227+
#[test]
228+
fn scanner_io_tmp_dir_is_preserved_in_core_config() {
229+
for directory in [
230+
None,
231+
Some("/tmp/fluss logs"),
232+
Some("relative/日志"),
233+
Some(""),
234+
] {
235+
let mut config = default_config();
236+
config.scanner_io_tmp_dir = directory.map(str::to_string);
237+
let core = config.into_core();
238+
assert_eq!(core.scanner_io_tmp_dir.as_deref(), directory);
239+
assert_eq!(core.bootstrap_servers, "localhost:9123");
240+
}
241+
}
242+
}

‎fluss-rust/bindings/elixir/test/config_test.exs‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,30 @@ defmodule Fluss.ConfigTest do
7979
assert config.remote_file_download_thread_num == 4
8080
end
8181

82+
test "scanner_io_tmp_dir defaults to nil" do
83+
assert Fluss.Config.new("localhost:9123").scanner_io_tmp_dir == nil
84+
assert Fluss.Config.default().scanner_io_tmp_dir == nil
85+
end
86+
87+
test "set_scanner_io_tmp_dir/2 preserves the configured path" do
88+
for path <- ["/tmp/fluss logs", "relative/日志", ""] do
89+
config =
90+
Fluss.Config.new("localhost:9123")
91+
|> Fluss.Config.set_scanner_io_tmp_dir(path)
92+
93+
assert config.scanner_io_tmp_dir == path
94+
assert config.bootstrap_servers == "localhost:9123"
95+
end
96+
end
97+
98+
test "set_scanner_io_tmp_dir/2 rejects non-string values" do
99+
for path <- [nil, 123, [:tmp]] do
100+
assert_raise FunctionClauseError, fn ->
101+
Fluss.Config.set_scanner_io_tmp_dir(Fluss.Config.default(), path)
102+
end
103+
end
104+
end
105+
82106
test "set_scanner_log_fetch_max_bytes/2 sets the fetch max bytes" do
83107
config =
84108
Fluss.Config.new("localhost:9123")

‎fluss-rust/bindings/python/fluss/__init__.pyi‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,10 @@ class Config:
202202
@scanner_remote_log_read_concurrency.setter
203203
def scanner_remote_log_read_concurrency(self, num: int) -> None: ...
204204
@property
205+
def scanner_io_tmp_dir(self) -> Optional[str]: ...
206+
@scanner_io_tmp_dir.setter
207+
def scanner_io_tmp_dir(self, path: Optional[str]) -> None: ...
208+
@property
205209
def scanner_log_max_poll_records(self) -> int: ...
206210
@scanner_log_max_poll_records.setter
207211
def scanner_log_max_poll_records(self, num: int) -> None: ...

‎fluss-rust/bindings/python/src/config.rs‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,9 @@ impl Config {
108108
))
109109
})?;
110110
}
111+
"scanner.io.tmpdir" => {
112+
config.scanner_io_tmp_dir = Some(value);
113+
}
111114
"scanner.log.max-poll-records" => {
112115
config.scanner_log_max_poll_records =
113116
value.parse::<usize>().map_err(|e| {
@@ -375,6 +378,18 @@ impl Config {
375378
self.inner.scanner_remote_log_read_concurrency = num;
376379
}
377380

381+
/// Parent directory for remote log downloads.
382+
/// None uses the OS temporary directory plus `/fluss`.
383+
#[getter]
384+
fn scanner_io_tmp_dir(&self) -> Option<String> {
385+
self.inner.scanner_io_tmp_dir.clone()
386+
}
387+
388+
#[setter]
389+
fn set_scanner_io_tmp_dir(&mut self, path: Option<String>) {
390+
self.inner.scanner_io_tmp_dir = path;
391+
}
392+
378393
/// Get the scanner log max poll records
379394
#[getter]
380395
fn scanner_log_max_poll_records(&self) -> usize {

0 commit comments

Comments
 (0)