This is an automated email from the ASF dual-hosted git repository.

fresh-borzoni pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/fluss.git


The following commit(s) were added to refs/heads/main by this push:
     new dd653f0b0 [rust][tests] Add DynamicWriteBatchSizeEstimator integration 
tests (#3588)
dd653f0b0 is described below

commit dd653f0b0c28e9d036a06104944882d2950a71d2
Author: Prajwal Banakar <[email protected]>
AuthorDate: Thu Jul 9 22:22:22 2026 +0530

    [rust][tests] Add DynamicWriteBatchSizeEstimator integration tests (#3588)
    
    * [rust][tests] Add DynamicWriteBatchSizeEstimator integration tests
    
    * [rust][tests] Add DynamicWriteBatchSizeEstimator integration tests
    
    * Address review: verify estimator grows in shrink_then_grow test
---
 fluss-rust/crates/fluss/src/client/connection.rs   |  14 +
 .../crates/fluss/src/client/write/accumulator.rs   |   7 +-
 .../crates/fluss/src/client/write/writer_client.rs |   8 +
 .../fluss/tests/integration/dynamic_batch_size.rs  | 340 +++++++++++++++++++++
 fluss-rust/crates/fluss/tests/test_fluss.rs        |   1 +
 5 files changed, 368 insertions(+), 2 deletions(-)

diff --git a/fluss-rust/crates/fluss/src/client/connection.rs 
b/fluss-rust/crates/fluss/src/client/connection.rs
index c31104c46..71985cb1a 100644
--- a/fluss-rust/crates/fluss/src/client/connection.rs
+++ b/fluss-rust/crates/fluss/src/client/connection.rs
@@ -23,6 +23,9 @@ use crate::client::table::FlussTable;
 use crate::config::Config;
 use crate::error::{Error, FlussError, Result};
 use crate::metadata::TablePath;
+
+#[cfg(feature = "integration_tests")]
+use crate::metadata::PhysicalTablePath;
 use crate::rpc::RpcClient;
 use parking_lot::RwLock;
 use std::sync::Arc;
@@ -144,6 +147,17 @@ impl FlussConnection {
         Ok(new_client)
     }
 
+    #[cfg(feature = "integration_tests")]
+    pub fn estimated_batch_size_for_table(
+        &self,
+        table_path: &Arc<PhysicalTablePath>,
+    ) -> Option<usize> {
+        self.writer_client
+            .read()
+            .as_ref()
+            .and_then(|c| c.estimated_batch_size_for_table(table_path))
+    }
+
     /// Gets or creates a lookup client for batched lookup operations.
     pub fn get_or_create_lookup_client(&self) -> Result<Arc<LookupClient>> {
         // 1. Fast path: Attempt to acquire a read lock to check if the client 
already exists.
diff --git a/fluss-rust/crates/fluss/src/client/write/accumulator.rs 
b/fluss-rust/crates/fluss/src/client/write/accumulator.rs
index ed5d31a30..a39f3bfaf 100644
--- a/fluss-rust/crates/fluss/src/client/write/accumulator.rs
+++ b/fluss-rust/crates/fluss/src/client/write/accumulator.rs
@@ -741,8 +741,11 @@ impl RecordAccumulator {
         }
     }
 
-    #[cfg(test)]
-    fn estimated_batch_size(&self, table_path: &Arc<PhysicalTablePath>) -> 
Option<usize> {
+    #[cfg(any(test, feature = "integration_tests"))]
+    pub(crate) fn estimated_batch_size(
+        &self,
+        table_path: &Arc<PhysicalTablePath>,
+    ) -> Option<usize> {
         self.write_batches
             .get(table_path)?
             .dynamic_batch_size
diff --git a/fluss-rust/crates/fluss/src/client/write/writer_client.rs 
b/fluss-rust/crates/fluss/src/client/write/writer_client.rs
index 73d91c7c2..53e1fd487 100644
--- a/fluss-rust/crates/fluss/src/client/write/writer_client.rs
+++ b/fluss-rust/crates/fluss/src/client/write/writer_client.rs
@@ -221,6 +221,14 @@ impl WriterClient {
         Ok(())
     }
 
+    #[cfg(feature = "integration_tests")]
+    pub fn estimated_batch_size_for_table(
+        &self,
+        table_path: &Arc<PhysicalTablePath>,
+    ) -> Option<usize> {
+        self.accumulate.estimated_batch_size(table_path)
+    }
+
     pub fn create_bucket_assigner(
         table_info: &Arc<TableInfo>,
         table_path: Arc<PhysicalTablePath>,
diff --git a/fluss-rust/crates/fluss/tests/integration/dynamic_batch_size.rs 
b/fluss-rust/crates/fluss/tests/integration/dynamic_batch_size.rs
new file mode 100644
index 000000000..965f1005d
--- /dev/null
+++ b/fluss-rust/crates/fluss/tests/integration/dynamic_batch_size.rs
@@ -0,0 +1,340 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#[cfg(test)]
+mod dynamic_batch_size_test {
+    use crate::integration::utils::{create_table, get_shared_cluster, 
wait_for_table_ready};
+    use fluss::client::{EARLIEST_OFFSET, FlussConnection};
+    use fluss::config::Config;
+    use fluss::metadata::{DataTypes, PhysicalTablePath, Schema, 
TableDescriptor, TablePath};
+    use fluss::row::{DataGetters, Datum, GenericRow};
+    use std::sync::Arc;
+    use std::time::Duration;
+
+    fn make_config(
+        bootstrap_servers: &str,
+        batch_size: i32,
+        dynamic_enabled: bool,
+        dynamic_min: i32,
+    ) -> Config {
+        Config {
+            bootstrap_servers: bootstrap_servers.to_string(),
+            writer_acks: "all".to_string(),
+            writer_batch_size: batch_size,
+            writer_dynamic_batch_size_enabled: dynamic_enabled,
+            writer_dynamic_batch_size_min: dynamic_min,
+            ..Config::default()
+        }
+    }
+
+    fn log_table_descriptor() -> TableDescriptor {
+        TableDescriptor::builder()
+            .schema(
+                Schema::builder()
+                    .column("id", DataTypes::int())
+                    .column("payload", DataTypes::string())
+                    .build()
+                    .expect("Failed to build schema"),
+            )
+            .build()
+            .expect("Failed to build table descriptor")
+    }
+
+    fn make_row(id: i32, payload: &str) -> GenericRow<'static> {
+        let mut row = GenericRow::new(2);
+        row.set_field(0, Datum::Int32(id));
+        row.set_field(1, Datum::String(payload.to_string().into()));
+        row
+    }
+
+    async fn read_back_records(
+        connection: &FlussConnection,
+        table_path: &TablePath,
+        expected_count: usize,
+    ) -> Vec<(i32, String)> {
+        let table = connection
+            .get_table(table_path)
+            .await
+            .expect("Failed to get table for scan");
+        let log_scanner = table
+            .new_scan()
+            .create_log_scanner()
+            .expect("Failed to create log scanner");
+        log_scanner
+            .subscribe(0, EARLIEST_OFFSET)
+            .await
+            .expect("Failed to subscribe");
+
+        let mut collected: Vec<(i32, String)> = Vec::new();
+        let start = std::time::Instant::now();
+        while collected.len() < expected_count && start.elapsed() < 
Duration::from_secs(30) {
+            let records = log_scanner
+                .poll(Duration::from_millis(500))
+                .await
+                .expect("Failed to poll");
+            for rec in records {
+                let row = rec.row();
+                collected.push((
+                    row.get_int(0).unwrap(),
+                    row.get_string(1).unwrap().to_string(),
+                ));
+            }
+        }
+        collected
+    }
+
+    /// Writes many tiny rows (well below 50% of batch size per drain) so the 
estimator
+    /// shrinks the target toward min, then reads all records back to verify 
data integrity.
+    #[tokio::test]
+    async fn small_rows_shrink_batch_size() {
+        let cluster = get_shared_cluster();
+        let config = make_config(
+            cluster.plaintext_bootstrap_servers(),
+            256 * 1024, // max = 256 KB
+            true,
+            4 * 1024, // min = 4 KB
+        );
+        let connection = FlussConnection::new(config)
+            .await
+            .expect("Failed to connect");
+        let admin = connection.get_admin().expect("Failed to get admin");
+        let table_path = TablePath::new("fluss", "test_dynamic_small_rows");
+        create_table(&admin, &table_path, &log_table_descriptor()).await;
+        wait_for_table_ready(&admin, &table_path).await;
+
+        let table = connection
+            .get_table(&table_path)
+            .await
+            .expect("Failed to get table");
+        let writer = table
+            .new_append()
+            .expect("Failed to create append")
+            .create_writer()
+            .expect("Failed to create writer");
+
+        // Write many tiny rows — each well below 50% of the batch size so
+        // the estimator shrinks the target toward min after each drain.
+        let row_count = 200usize;
+        for i in 0..row_count {
+            let row = make_row(i as i32, "x");
+            writer.append(&row).expect("Failed to append row");
+        }
+        writer.flush().await.expect("Failed to flush");
+
+        // Verify the estimator actually shrunk toward min, not just that 
flush succeeded.
+        let physical_path = 
Arc::new(PhysicalTablePath::of(Arc::new(table_path.clone())));
+        let estimated = connection
+            .estimated_batch_size_for_table(&physical_path)
+            .expect("Estimator should exist after writes");
+        assert!(
+            estimated < 256 * 1024,
+            "Expected batch size to shrink below max (256 KB), got {estimated}"
+        );
+    }
+
+    /// Verifies estimator adapts correctly across a shrink-then-grow cycle:
+    /// first shrinks with tiny rows, then grows with large rows, and all
+    /// records written in both phases are read back correctly.
+    #[tokio::test]
+    async fn shrink_then_grow_batch_size() {
+        let cluster = get_shared_cluster();
+        let max_batch = 256 * 1024i32; // 256 KB
+        let config = make_config(
+            cluster.plaintext_bootstrap_servers(),
+            max_batch,
+            true,
+            4 * 1024, // min = 4 KB
+        );
+        let connection = FlussConnection::new(config)
+            .await
+            .expect("Failed to connect");
+        let admin = connection.get_admin().expect("Failed to get admin");
+        let table_path = TablePath::new("fluss", "test_dynamic_shrink_grow");
+        create_table(&admin, &table_path, &log_table_descriptor()).await;
+        wait_for_table_ready(&admin, &table_path).await;
+
+        let table = connection
+            .get_table(&table_path)
+            .await
+            .expect("Failed to get table");
+        let writer = table
+            .new_append()
+            .expect("Failed to create append")
+            .create_writer()
+            .expect("Failed to create writer");
+
+        // Phase 1: shrink — many tiny rows drive the target toward min.
+        let small_count = 100usize;
+        for i in 0..small_count {
+            let row = make_row(i as i32, "small");
+            writer.append(&row).expect("Failed to append small row");
+        }
+        writer.flush().await.expect("Failed to flush phase 1");
+
+        let physical_path = 
Arc::new(PhysicalTablePath::of(Arc::new(table_path.clone())));
+
+        let estimated_after_shrink = connection
+            .estimated_batch_size_for_table(&physical_path)
+            .expect("Estimator should exist after shrink phase");
+
+        assert!(
+            estimated_after_shrink < max_batch as usize,
+            "Expected batch size to shrink below max ({max_batch}), got 
{estimated_after_shrink}"
+        );
+
+        // Phase 2: grow — large rows (>80% of current target) drive the
+        // estimator back up toward max after it has been shrunk.
+        let large_payload = "A".repeat(200 * 1024); // ~200 KB
+        let large_count = 5usize;
+        for i in 0..large_count {
+            let row = make_row((small_count + i) as i32, &large_payload);
+            writer.append(&row).expect("Failed to append large row");
+        }
+        writer.flush().await.expect("Failed to flush phase 2");
+
+        let estimated_after_grow = connection
+            .estimated_batch_size_for_table(&physical_path)
+            .expect("Estimator should exist after grow phase");
+
+        assert!(
+            estimated_after_grow > estimated_after_shrink,
+            "Expected batch size to grow after large writes 
(before={estimated_after_shrink}, after={estimated_after_grow})"
+        );
+
+        // Read back all records from both phases to verify data integrity.
+        let total = small_count + large_count;
+        let records = read_back_records(&connection, &table_path, total).await;
+        assert_eq!(
+            records.len(),
+            total,
+            "Expected {total} records after shrink-then-grow cycle, got {}",
+            records.len()
+        );
+    }
+
+    /// With dynamic sizing disabled, the writer uses the static 
writer_batch_size
+    /// for every batch. Verifies all records are written and read back 
correctly.
+    #[tokio::test]
+    async fn disabled_keeps_static_batch_size() {
+        let cluster = get_shared_cluster();
+        let config = make_config(
+            cluster.plaintext_bootstrap_servers(),
+            256 * 1024,
+            false, // disabled
+            4 * 1024,
+        );
+        let connection = FlussConnection::new(config)
+            .await
+            .expect("Failed to connect");
+        let admin = connection.get_admin().expect("Failed to get admin");
+        let table_path = TablePath::new("fluss", "test_dynamic_disabled");
+        create_table(&admin, &table_path, &log_table_descriptor()).await;
+        wait_for_table_ready(&admin, &table_path).await;
+
+        let table = connection
+            .get_table(&table_path)
+            .await
+            .expect("Failed to get table");
+        let writer = table
+            .new_append()
+            .expect("Failed to create append")
+            .create_writer()
+            .expect("Failed to create writer");
+
+        let row_count = 100usize;
+        for i in 0..row_count {
+            let row = make_row(i as i32, "static");
+            writer.append(&row).expect("Failed to append row");
+        }
+        writer.flush().await.expect("Failed to flush");
+
+        let records = read_back_records(&connection, &table_path, 
row_count).await;
+        assert_eq!(
+            records.len(),
+            row_count,
+            "Expected {row_count} records with static batch size, got {}",
+            records.len()
+        );
+    }
+
+    /// Multiple concurrent tasks share a single connection (and thus a single
+    /// WriterClient and RecordAccumulator with a shared estimator). Verifies 
that
+    /// concurrent appends from different tasks don't corrupt estimator state 
and
+    /// all records land correctly.
+    #[tokio::test]
+    async fn concurrent_appends_share_estimator_without_corruption() {
+        let cluster = get_shared_cluster();
+        let config = make_config(
+            cluster.plaintext_bootstrap_servers(),
+            256 * 1024,
+            true,
+            4 * 1024,
+        );
+        let connection = FlussConnection::new(config)
+            .await
+            .expect("Failed to connect");
+        let admin = connection.get_admin().expect("Failed to get admin");
+        let table_path = TablePath::new("fluss", "test_dynamic_concurrent");
+        create_table(&admin, &table_path, &log_table_descriptor()).await;
+        wait_for_table_ready(&admin, &table_path).await;
+
+        let table = connection
+            .get_table(&table_path)
+            .await
+            .expect("Failed to get table");
+
+        // All tasks share the same AppendWriter (and thus the same
+        // RecordAccumulator / estimator) via Arc.
+        let writer = Arc::new(
+            table
+                .new_append()
+                .expect("Failed to create append")
+                .create_writer()
+                .expect("Failed to create writer"),
+        );
+
+        let tasks_count = 4usize;
+        let rows_per_task = 50usize;
+        let mut handles = Vec::new();
+
+        for task_id in 0..tasks_count {
+            let writer_clone = Arc::clone(&writer);
+            let handle = tokio::spawn(async move {
+                for i in 0..rows_per_task {
+                    let row = make_row((task_id * rows_per_task + i) as i32, 
"concurrent");
+                    writer_clone.append(&row).expect("Failed to append");
+                }
+            });
+            handles.push(handle);
+        }
+
+        for handle in handles {
+            handle.await.expect("Task panicked");
+        }
+        writer.flush().await.expect("Failed to flush");
+
+        let total = tasks_count * rows_per_task;
+        let records = read_back_records(&connection, &table_path, total).await;
+        assert_eq!(
+            records.len(),
+            total,
+            "Expected {total} records from concurrent appends, got {}",
+            records.len()
+        );
+    }
+}
diff --git a/fluss-rust/crates/fluss/tests/test_fluss.rs 
b/fluss-rust/crates/fluss/tests/test_fluss.rs
index eb1e40700..63c4f7855 100644
--- a/fluss-rust/crates/fluss/tests/test_fluss.rs
+++ b/fluss-rust/crates/fluss/tests/test_fluss.rs
@@ -31,5 +31,6 @@ mod integration {
 
     mod utils;
 
+    mod dynamic_batch_size;
     mod table_remote_scan;
 }

Reply via email to