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

JingsongLi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/paimon-rust.git


The following commit(s) were added to refs/heads/main by this push:
     new d9718da6 [rust] Bound sparse BlobDescriptor range reads (#883)
d9718da6 is described below

commit d9718da6696aeade74805ff140f76c53dbc96566
Author: Jingsong Lee <[email protected]>
AuthorDate: Sun Sep 20 14:27:35 2026 +0800

    [rust] Bound sparse BlobDescriptor range reads (#883)
---
 crates/paimon/src/table/blob_resolver.rs | 171 +++++++++++++++++++++++--------
 1 file changed, 127 insertions(+), 44 deletions(-)

diff --git a/crates/paimon/src/table/blob_resolver.rs 
b/crates/paimon/src/table/blob_resolver.rs
index 4d41aa93..cdf2fc7d 100644
--- a/crates/paimon/src/table/blob_resolver.rs
+++ b/crates/paimon/src/table/blob_resolver.rs
@@ -30,6 +30,7 @@ use tokio::sync::{OwnedSemaphorePermit, Semaphore};
 
 const BLOB_RANGE_MERGE_GAP: u64 = 64 * 1024;
 const BLOB_RANGE_MERGE_MAX_SPAN: u64 = 8 * 1024 * 1024;
+const BLOB_RANGE_MERGE_MAX_AMPLIFICATION: u64 = 2;
 pub(crate) const BLOB_DESCRIPTOR_READ_CONCURRENCY: usize = 
DEFAULT_BLOB_READ_PARALLELISM;
 const BLOB_DESCRIPTOR_READ_BYTE_UNIT: u64 = 1024 * 1024;
 const BLOB_DESCRIPTOR_READ_MAX_IN_FLIGHT_BYTES: u64 = 64 * 1024 * 1024;
@@ -505,37 +506,8 @@ pub(crate) async fn resolve_blob_column(
         });
     }
 
-    for ResolvedMergedBlobRead { merged, data } in 
read_blob_groups(read_groups, limiter).await? {
-        for request in merged.requests {
-            let start = usize::try_from(request.offset - 
merged.start).map_err(|e| {
-                crate::Error::DataInvalid {
-                    message: format!(
-                        "BlobDescriptor slice offset exceeds usize: offset={}, 
merged_start={}",
-                        request.offset, merged.start
-                    ),
-                    source: Some(Box::new(e)),
-                }
-            })?;
-            let length =
-                usize::try_from(request.length).map_err(|e| 
crate::Error::DataInvalid {
-                    message: format!(
-                        "BlobDescriptor slice length exceeds usize: {}",
-                        request.length
-                    ),
-                    source: Some(Box::new(e)),
-                })?;
-            let end = start
-                .checked_add(length)
-                .filter(|end| *end <= data.len())
-                .ok_or_else(|| crate::Error::DataInvalid {
-                    message: format!(
-                        "BlobDescriptor slice exceeds read data: 
start={start}, length={length}, actual={}",
-                        data.len()
-                    ),
-                    source: None,
-                })?;
-            cells[request.row] = 
ResolvedBlobCell::Value(data.slice(start..end));
-        }
+    for resolved in read_blob_groups(read_groups, limiter).await? {
+        cells[resolved.row] = ResolvedBlobCell::Value(resolved.data);
     }
 
     let mut builder = LargeBinaryBuilder::with_capacity(col.len(), 
value_capacity);
@@ -573,10 +545,11 @@ struct MergedBlobRead {
     start: u64,
     end: u64,
     requests: Vec<BlobReadRequest>,
+    selected_bytes: u64,
 }
 
-struct ResolvedMergedBlobRead {
-    merged: MergedBlobRead,
+struct ResolvedBlobRead {
+    row: usize,
     data: Bytes,
 }
 
@@ -589,9 +562,9 @@ struct BlobReadGroup {
 async fn read_blob_groups(
     groups: Vec<BlobReadGroup>,
     limiter: BlobReadLimiter,
-) -> Result<Vec<ResolvedMergedBlobRead>> {
+) -> Result<Vec<ResolvedBlobRead>> {
     let blob_parallelism = limiter.parallelism();
-    let grouped_results: Vec<Vec<ResolvedMergedBlobRead>> =
+    let grouped_results: Vec<Vec<ResolvedBlobRead>> =
         stream::iter(groups)
             .map(|group| {
                 let limiter = limiter.clone();
@@ -610,7 +583,7 @@ async fn read_merged_blob_ranges(
     reader: Arc<dyn FileRead>,
     reads: Vec<MergedBlobRead>,
     limiter: BlobReadLimiter,
-) -> Result<Vec<ResolvedMergedBlobRead>> {
+) -> Result<Vec<ResolvedBlobRead>> {
     let blob_parallelism = limiter.parallelism();
     stream::iter(reads)
         .map(|merged| {
@@ -618,8 +591,10 @@ async fn read_merged_blob_ranges(
             let reader = reader.clone();
             let limiter = limiter.clone();
             async move {
+                let expected_len = merged.end - merged.start;
+                let copy_values = expected_len > merged.selected_bytes;
                 let _permits = limiter
-                    .acquire_read(merged.end - merged.start, &uri)
+                    .acquire_read(expected_len, &uri)
                     .await?;
                 let data = reader
                     .read(merged.start..merged.end)
@@ -631,7 +606,6 @@ async fn read_merged_blob_ranges(
                         ),
                         source: Some(Box::new(e)),
                     })?;
-                let expected_len = merged.end - merged.start;
                 let actual_len = data.len() as u64;
                 if actual_len != expected_len {
                     return Err(crate::Error::DataInvalid {
@@ -642,12 +616,55 @@ async fn read_merged_blob_ranges(
                         source: None,
                     });
                 }
-                Ok(ResolvedMergedBlobRead { merged, data })
+                merged
+                    .requests
+                    .into_iter()
+                    .map(|request| {
+                        let start = usize::try_from(request.offset - 
merged.start).map_err(|e| {
+                            crate::Error::DataInvalid {
+                                message: format!(
+                                    "BlobDescriptor slice offset exceeds 
usize: offset={}, merged_start={}",
+                                    request.offset, merged.start
+                                ),
+                                source: Some(Box::new(e)),
+                            }
+                        })?;
+                        let length = 
usize::try_from(request.length).map_err(|e| {
+                            crate::Error::DataInvalid {
+                                message: format!(
+                                    "BlobDescriptor slice length exceeds 
usize: {}",
+                                    request.length
+                                ),
+                                source: Some(Box::new(e)),
+                            }
+                        })?;
+                        let end = start
+                            .checked_add(length)
+                            .filter(|end| *end <= data.len())
+                            .ok_or_else(|| crate::Error::DataInvalid {
+                                message: format!(
+                                    "BlobDescriptor slice exceeds read data: 
start={start}, length={length}, actual={}",
+                                    data.len()
+                                ),
+                                source: None,
+                            })?;
+                        let value = if copy_values {
+                            Bytes::copy_from_slice(&data[start..end])
+                        } else {
+                            data.slice(start..end)
+                        };
+                        Ok(ResolvedBlobRead {
+                            row: request.row,
+                            data: value,
+                        })
+                    })
+                    .collect::<Result<Vec<_>>>()
             }
         })
         .buffer_unordered(blob_parallelism)
         .try_collect()
         .await
+        .map(|results: Vec<Vec<ResolvedBlobRead>>| 
results.into_iter().flatten().collect())
 }
 
 fn merge_blob_read_requests(mut requests: Vec<BlobReadRequest>) -> 
Vec<MergedBlobRead> {
@@ -660,25 +677,32 @@ fn merge_blob_read_requests(mut requests: 
Vec<BlobReadRequest>) -> Vec<MergedBlo
     let mut current = MergedBlobRead {
         start: requests[0].offset,
         end: requests[0].offset + requests[0].length,
+        selected_bytes: requests[0].length,
         requests: vec![requests[0].clone()],
     };
 
     for request in requests.into_iter().skip(1) {
         let request_end = request.offset + request.length;
+        let added_selected_bytes = 
request_end.saturating_sub(request.offset.max(current.end));
+        let selected_bytes = 
current.selected_bytes.saturating_add(added_selected_bytes);
         let close_enough = current
             .end
             .checked_add(BLOB_RANGE_MERGE_GAP)
             .is_some_and(|merge_limit| request.offset <= merge_limit);
         let merged_end = current.end.max(request_end);
         let merged_span = merged_end - current.start;
-        if close_enough && merged_span <= BLOB_RANGE_MERGE_MAX_SPAN {
+        let amplification_bounded =
+            merged_span <= 
selected_bytes.saturating_mul(BLOB_RANGE_MERGE_MAX_AMPLIFICATION);
+        if close_enough && merged_span <= BLOB_RANGE_MERGE_MAX_SPAN && 
amplification_bounded {
             current.end = merged_end;
+            current.selected_bytes = selected_bytes;
             current.requests.push(request);
         } else {
             merged.push(current);
             current = MergedBlobRead {
                 start: request.offset,
                 end: request_end,
+                selected_bytes: request.length,
                 requests: vec![request],
             };
         }
@@ -764,6 +788,7 @@ mod tests {
             .map(|row| MergedBlobRead {
                 start: row,
                 end: row + 1,
+                selected_bytes: 1,
                 requests: vec![BlobReadRequest {
                     row: row as usize,
                     offset: row,
@@ -793,6 +818,7 @@ mod tests {
             .map(|row| MergedBlobRead {
                 start: row,
                 end: row + 1,
+                selected_bytes: 1,
                 requests: vec![BlobReadRequest {
                     row: row as usize,
                     offset: row,
@@ -821,6 +847,7 @@ mod tests {
             MergedBlobRead {
                 start: 4,
                 end: 8,
+                selected_bytes: 4,
                 requests: vec![BlobReadRequest {
                     row: 0,
                     offset: 4,
@@ -830,6 +857,7 @@ mod tests {
             MergedBlobRead {
                 start: 0,
                 end: 4,
+                selected_bytes: 4,
                 requests: vec![BlobReadRequest {
                     row: 1,
                     offset: 0,
@@ -849,7 +877,7 @@ mod tests {
 
         let mut by_row = results
             .into_iter()
-            .map(|result| (result.merged.requests[0].row, result.data))
+            .map(|result| (result.row, result.data))
             .collect::<Vec<_>>();
         by_row.sort_by_key(|(row, _)| *row);
         assert_eq!(by_row[0], (0, Bytes::from_static(b"efgh")));
@@ -875,6 +903,7 @@ mod tests {
                 reads: vec![MergedBlobRead {
                     start: 0,
                     end: 1,
+                    selected_bytes: 1,
                     requests: vec![BlobReadRequest {
                         row,
                         offset: 0,
@@ -947,10 +976,42 @@ mod tests {
         .await
         .unwrap();
 
-        assert_eq!(results.len(), 1);
+        assert_eq!(results.len(), 3);
         assert_eq!(reader.ranges(), vec![0..8]);
     }
 
+    #[tokio::test]
+    async fn test_gapped_merged_ranges_copy_selected_values() {
+        let reader = TrackingFileRead::new(Bytes::from_static(b"abcd--efgh"));
+        let reads = merge_blob_read_requests(vec![
+            BlobReadRequest {
+                row: 0,
+                offset: 0,
+                length: 4,
+            },
+            BlobReadRequest {
+                row: 1,
+                offset: 6,
+                length: 4,
+            },
+        ]);
+
+        let mut results = read_merged_blob_ranges(
+            "memory:/blob.bin",
+            Arc::new(reader.clone()),
+            reads,
+            BlobReadLimiter::new(),
+        )
+        .await
+        .unwrap();
+        results.sort_unstable_by_key(|result| result.row);
+
+        assert_eq!(reader.ranges(), vec![0..10]);
+        assert_eq!(results[0].data, Bytes::from_static(b"abcd"));
+        assert_eq!(results[1].data, Bytes::from_static(b"efgh"));
+        assert!(results.iter().all(|result| result.data.is_unique()));
+    }
+
     #[tokio::test]
     async fn test_blob_range_read_rejects_short_data() {
         let error = read_merged_blob_ranges(
@@ -959,6 +1020,7 @@ mod tests {
             vec![MergedBlobRead {
                 start: 4,
                 end: 8,
+                selected_bytes: 4,
                 requests: vec![BlobReadRequest {
                     row: 0,
                     offset: 4,
@@ -979,7 +1041,7 @@ mod tests {
         let merged = merge_blob_read_requests(vec![
             BlobReadRequest {
                 row: 2,
-                offset: 120,
+                offset: 20,
                 length: 5,
             },
             BlobReadRequest {
@@ -1001,7 +1063,7 @@ mod tests {
 
         assert_eq!(merged.len(), 2);
         assert_eq!(merged[0].start, 0);
-        assert_eq!(merged[0].end, 125);
+        assert_eq!(merged[0].end, 25);
         assert_eq!(
             merged[0]
                 .requests
@@ -1014,6 +1076,27 @@ mod tests {
         assert_eq!(merged[1].end, BLOB_RANGE_MERGE_MAX_SPAN + 5);
     }
 
+    #[test]
+    fn test_merge_blob_read_requests_rejects_sparse_ranges() {
+        let value_length = 4 * 1024;
+        let stride = value_length + BLOB_RANGE_MERGE_GAP;
+        let requests = (0..128)
+            .map(|row| BlobReadRequest {
+                row,
+                offset: row as u64 * stride,
+                length: value_length,
+            })
+            .collect();
+
+        let merged = merge_blob_read_requests(requests);
+
+        assert_eq!(merged.len(), 128);
+        assert_eq!(
+            merged.iter().map(|read| read.end - read.start).sum::<u64>(),
+            128 * value_length
+        );
+    }
+
     fn java_v2_descriptor(uri: &str, offset: i64, length: i64) -> Vec<u8> {
         let mut bytes = Vec::new();
         bytes.push(2);

Reply via email to