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 29a6510e [rust] Coalesce inline BLOB reads with bounded amplification
(#880)
29a6510e is described below
commit 29a6510ecb319bad1a428ad4d9498501b572e349
Author: Jingsong Lee <[email protected]>
AuthorDate: Sun Sep 20 13:13:15 2026 +0800
[rust] Coalesce inline BLOB reads with bounded amplification (#880)
---
crates/paimon/src/arrow/format/blob.rs | 527 ++++++++++++++++++---
.../table/data_evolution_reader/blob_fallback.rs | 35 +-
2 files changed, 491 insertions(+), 71 deletions(-)
diff --git a/crates/paimon/src/arrow/format/blob.rs
b/crates/paimon/src/arrow/format/blob.rs
index 79f2a0ca..a08deb81 100644
--- a/crates/paimon/src/arrow/format/blob.rs
+++ b/crates/paimon/src/arrow/format/blob.rs
@@ -169,6 +169,10 @@ const BLOB_TRAILER_SIZE: u64 = 12;
const BLOB_ENTRY_OVERHEAD: u64 = BLOB_INLINE_HEADER_SIZE + BLOB_TRAILER_SIZE;
const DEFAULT_BATCH_SIZE: usize = 128;
pub(crate) const DEFAULT_BLOB_READ_PARALLELISM: usize = 8;
+const BLOB_RANGE_MERGE_GAP: u64 = 1024 * 1024;
+const BLOB_RANGE_MERGE_MAX_SPAN: u64 = 8 * 1024 * 1024;
+// Never fetch more than twice the unique selected entry bytes in a merged
span.
+const BLOB_RANGE_MERGE_MAX_AMPLIFICATION: u64 = 2;
const BLOB_ARRAY_MAGIC_NUMBER: i32 = 1094861634;
const BLOB_ARRAY_VERSION: u8 = 1;
const BLOB_ARRAY_HEADER_SIZE: u64 = 9;
@@ -679,18 +683,164 @@ async fn fetch_blob_values(
planned_reads: Vec<PlannedBlobRead>,
blob_parallelism: usize,
) -> crate::Result<Vec<BlobReadValue>> {
- futures::stream::iter(planned_reads.into_iter().map(|planned_read| async
move {
+ let mut values = Vec::with_capacity(planned_reads.len());
+ let mut entry_reads = Vec::new();
+ for (result_index, planned_read) in planned_reads.into_iter().enumerate() {
match planned_read {
- PlannedBlobRead::Null => Ok(BlobReadValue::Null),
- PlannedBlobRead::Placeholder => Ok(BlobReadValue::Placeholder),
- PlannedBlobRead::Entry(range) => read_blob_entry(reader, range)
- .await
- .map(BlobReadValue::Value),
+ PlannedBlobRead::Null => values.push(Some(BlobReadValue::Null)),
+ PlannedBlobRead::Placeholder =>
values.push(Some(BlobReadValue::Placeholder)),
+ PlannedBlobRead::Entry(range) => {
+ values.push(None);
+ entry_reads.push(BlobEntryRead {
+ result_index,
+ range,
+ });
+ }
}
- }))
- .buffered(blob_parallelism)
- .try_collect()
- .await
+ }
+
+ for resolved in read_merged_blob_entries(reader, entry_reads,
blob_parallelism).await? {
+ let value = decode_blob_entry(resolved.entry, resolved.range)?;
+ values[resolved.result_index] = Some(BlobReadValue::Value(value));
+ }
+ collect_blob_read_values(values)
+}
+
+#[derive(Debug)]
+struct BlobEntryRead {
+ result_index: usize,
+ range: Range<u64>,
+}
+
+#[derive(Debug)]
+struct MergedBlobEntryRead {
+ range: Range<u64>,
+ entries: Vec<BlobEntryRead>,
+ selected_bytes: u64,
+}
+
+struct ResolvedBlobEntryRead {
+ result_index: usize,
+ range: Range<u64>,
+ entry: Bytes,
+}
+
+async fn read_merged_blob_entries(
+ reader: &dyn FileRead,
+ entry_reads: Vec<BlobEntryRead>,
+ blob_parallelism: usize,
+) -> crate::Result<Vec<ResolvedBlobEntryRead>> {
+ let merged_reads = merge_blob_entry_reads(entry_reads);
+ let resolved: Vec<Vec<ResolvedBlobEntryRead>> =
+ futures::stream::iter(merged_reads.into_iter().map(|merged_read| async
move {
+ let expected_length = merged_read.range.end -
merged_read.range.start;
+ let copy_entries = expected_length > merged_read.selected_bytes;
+ let data = reader.read(merged_read.range.clone()).await?;
+ if data.len() as u64 != expected_length {
+ return Err(Error::DataInvalid {
+ message: format!(
+ "Short read for merged Blob range {:?}: expected
{expected_length} bytes, got {}",
+ merged_read.range,
+ data.len()
+ ),
+ source: None,
+ });
+ }
+
+ merged_read
+ .entries
+ .into_iter()
+ .map(|entry_read| {
+ let start = usize::try_from(entry_read.range.start -
merged_read.range.start)
+ .map_err(|e| Error::DataInvalid {
+ message: "Blob entry offset exceeds
usize".to_string(),
+ source: Some(Box::new(e)),
+ })?;
+ let end = usize::try_from(entry_read.range.end -
merged_read.range.start)
+ .map_err(|e| Error::DataInvalid {
+ message: "Blob entry offset exceeds
usize".to_string(),
+ source: Some(Box::new(e)),
+ })?;
+ // Bytes::slice keeps the complete merged response alive.
Copy entries from a
+ // gapped span so unselected bytes can be released as soon
as this read resolves.
+ let entry = if copy_entries {
+ Bytes::copy_from_slice(&data[start..end])
+ } else {
+ data.slice(start..end)
+ };
+ Ok(ResolvedBlobEntryRead {
+ result_index: entry_read.result_index,
+ range: entry_read.range,
+ entry,
+ })
+ })
+ .collect::<crate::Result<Vec<_>>>()
+ }))
+ .buffer_unordered(blob_parallelism)
+ .try_collect()
+ .await?;
+ Ok(resolved.into_iter().flatten().collect())
+}
+
+fn collect_blob_read_values(
+ values: Vec<Option<BlobReadValue>>,
+) -> crate::Result<Vec<BlobReadValue>> {
+ values
+ .into_iter()
+ .map(|value| {
+ value.ok_or_else(|| Error::UnexpectedError {
+ message: "Blob read did not produce a value".to_string(),
+ source: None,
+ })
+ })
+ .collect()
+}
+
+fn merge_blob_entry_reads(mut reads: Vec<BlobEntryRead>) ->
Vec<MergedBlobEntryRead> {
+ if reads.is_empty() {
+ return Vec::new();
+ }
+
+ reads.sort_unstable_by_key(|read| (read.range.start, read.range.end,
read.result_index));
+ let mut reads = reads.into_iter();
+ let first = reads.next().unwrap();
+ let mut current = MergedBlobEntryRead {
+ range: first.range.clone(),
+ selected_bytes: first.range.end - first.range.start,
+ entries: vec![first],
+ };
+ let mut merged = Vec::new();
+ for read in reads {
+ let merged_end = current.range.end.max(read.range.end);
+ let added_selected_bytes = read
+ .range
+ .end
+ .saturating_sub(read.range.start.max(current.range.end));
+ let selected_bytes =
current.selected_bytes.saturating_add(added_selected_bytes);
+ let merged_span = merged_end - current.range.start;
+ let close_enough = read
+ .range
+ .start
+ .checked_sub(current.range.end)
+ .map(|gap| gap <= BLOB_RANGE_MERGE_GAP)
+ .unwrap_or(true);
+ 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.range.end = merged_end;
+ current.selected_bytes = selected_bytes;
+ current.entries.push(read);
+ } else {
+ merged.push(current);
+ current = MergedBlobEntryRead {
+ range: read.range.clone(),
+ selected_bytes: read.range.end - read.range.start,
+ entries: vec![read],
+ };
+ }
+ }
+ merged.push(current);
+ merged
}
fn blob_entry_range(payload_range: &Range<u64>) -> Range<u64> {
@@ -698,8 +848,12 @@ fn blob_entry_range(payload_range: &Range<u64>) ->
Range<u64> {
}
async fn read_blob_entry(reader: &dyn FileRead, entry_range: Range<u64>) ->
crate::Result<Bytes> {
- let expected_entry_length = entry_range.end - entry_range.start;
let entry = reader.read(entry_range.clone()).await?;
+ decode_blob_entry(entry, entry_range)
+}
+
+fn decode_blob_entry(entry: Bytes, entry_range: Range<u64>) ->
crate::Result<Bytes> {
+ let expected_entry_length = entry_range.end - entry_range.start;
if entry.len() as u64 != expected_entry_length {
return Err(Error::DataInvalid {
message: format!(
@@ -753,6 +907,42 @@ async fn read_blob_entry(reader: &dyn FileRead,
entry_range: Range<u64>) -> crat
Ok(entry.slice(BLOB_INLINE_HEADER_SIZE as usize..length_offset))
}
+struct InMemoryBlobEntryReader {
+ entry_range: Range<u64>,
+ entry: Bytes,
+}
+
+#[async_trait]
+impl FileRead for InMemoryBlobEntryReader {
+ async fn read(&self, range: Range<u64>) -> crate::Result<Bytes> {
+ if range.start > range.end
+ || range.start < self.entry_range.start
+ || range.end > self.entry_range.end
+ {
+ return Err(Error::DataInvalid {
+ message: format!(
+ "Blob entry memory read {range:?} is outside {:?}",
+ self.entry_range
+ ),
+ source: None,
+ });
+ }
+ let start = usize::try_from(range.start -
self.entry_range.start).map_err(|e| {
+ Error::DataInvalid {
+ message: "Blob entry memory offset exceeds usize".to_string(),
+ source: Some(Box::new(e)),
+ }
+ })?;
+ let end = usize::try_from(range.end -
self.entry_range.start).map_err(|e| {
+ Error::DataInvalid {
+ message: "Blob entry memory offset exceeds usize".to_string(),
+ source: Some(Box::new(e)),
+ }
+ })?;
+ Ok(self.entry.slice(start..end))
+ }
+}
+
fn plan_blob_array_reads(
blob_index: &BlobFileIndex,
positions: &[usize],
@@ -786,17 +976,23 @@ async fn fetch_blob_array_values(
descriptor_mode: bool,
blob_parallelism: usize,
) -> crate::Result<Vec<BlobReadValue>> {
+ if !descriptor_mode {
+ return fetch_inline_nested_blob_values(
+ reader,
+ planned_reads,
+ blob_parallelism,
+ InlineNestedBlobKind::Array,
+ )
+ .await;
+ }
+
futures::stream::iter(planned_reads.into_iter().map(|planned_read| async
move {
match planned_read {
PlannedBlobArrayRead::Null => Ok(BlobReadValue::Null),
PlannedBlobArrayRead::Placeholder =>
Ok(BlobReadValue::Placeholder),
PlannedBlobArrayRead::Read(payload_range) => {
- if descriptor_mode {
- let metadata = read_blob_array_metadata(reader,
payload_range).await?;
- build_blob_array_descriptors(metadata, file_path)
- } else {
- read_inline_blob_array_entry(reader, payload_range).await
- }
+ let metadata = read_blob_array_metadata(reader,
payload_range).await?;
+ build_blob_array_descriptors(metadata, file_path)
}
}
}))
@@ -1089,6 +1285,19 @@ async fn fetch_blob_map_values(
key_type: &DataType,
blob_parallelism: usize,
) -> crate::Result<Vec<BlobReadValue>> {
+ if !descriptor_mode {
+ return fetch_inline_nested_blob_values(
+ reader,
+ planned_reads,
+ blob_parallelism,
+ InlineNestedBlobKind::Map {
+ file_path,
+ key_type,
+ },
+ )
+ .await;
+ }
+
futures::stream::iter(planned_reads.into_iter().map(|planned_read| async
move {
match planned_read {
PlannedBlobArrayRead::Null => Ok(BlobReadValue::Null),
@@ -1104,6 +1313,62 @@ async fn fetch_blob_map_values(
.await
}
+#[derive(Clone, Copy)]
+enum InlineNestedBlobKind<'a> {
+ Array,
+ Map {
+ file_path: &'a str,
+ key_type: &'a DataType,
+ },
+}
+
+async fn fetch_inline_nested_blob_values(
+ reader: &dyn FileRead,
+ planned_reads: Vec<PlannedBlobArrayRead>,
+ blob_parallelism: usize,
+ kind: InlineNestedBlobKind<'_>,
+) -> crate::Result<Vec<BlobReadValue>> {
+ let mut values = Vec::with_capacity(planned_reads.len());
+ let mut entry_reads = Vec::new();
+ for (result_index, planned_read) in planned_reads.into_iter().enumerate() {
+ match planned_read {
+ PlannedBlobArrayRead::Null =>
values.push(Some(BlobReadValue::Null)),
+ PlannedBlobArrayRead::Placeholder =>
values.push(Some(BlobReadValue::Placeholder)),
+ PlannedBlobArrayRead::Read(payload_range) => {
+ values.push(None);
+ entry_reads.push(BlobEntryRead {
+ result_index,
+ range: blob_entry_range(&payload_range),
+ });
+ }
+ }
+ }
+
+ for resolved in read_merged_blob_entries(reader, entry_reads,
blob_parallelism).await? {
+ let result_index = resolved.result_index;
+ let payload_range =
+ resolved.range.start + BLOB_INLINE_HEADER_SIZE..resolved.range.end
- BLOB_TRAILER_SIZE;
+ let memory_reader = InMemoryBlobEntryReader {
+ entry_range: resolved.range,
+ entry: resolved.entry,
+ };
+ let value = match kind {
+ InlineNestedBlobKind::Array => {
+ read_inline_blob_array_entry(&memory_reader,
payload_range).await?
+ }
+ InlineNestedBlobKind::Map {
+ file_path,
+ key_type,
+ } => {
+ read_blob_map_entry(&memory_reader, payload_range, file_path,
false, key_type)
+ .await?
+ }
+ };
+ values[result_index] = Some(value);
+ }
+ collect_blob_read_values(values)
+}
+
async fn read_blob_map_entry(
reader: &dyn FileRead,
payload_range: Range<u64>,
@@ -2160,6 +2425,47 @@ mod tests {
assert_eq!(empty.length(), 0);
}
+ #[tokio::test]
+ async fn test_inline_blob_map_reader_coalesces_adjacent_entries() {
+ let first = build_blob_map_payload(&[("first", Some(b"alpha"))]);
+ let second = build_blob_map_payload(&[("second", Some(b"beta"))]);
+ let file_bytes = blob_test_utils::build_blob_file_bytes(&[
+ Some(first.as_slice()),
+ Some(second.as_slice()),
+ ]);
+ let reader = TrackingFileRead::new(Bytes::from(file_bytes.clone()));
+
+ let batches = BlobFormatReader::new(String::new(), false)
+ .read_batch_stream(
+ Box::new(reader.clone()),
+ file_bytes.len() as u64,
+ &blob_map_read_fields(),
+ None,
+ None,
+ None,
+ )
+ .await
+ .unwrap()
+ .try_collect::<Vec<_>>()
+ .await
+ .unwrap();
+
+ assert_eq!(
+ collect_blob_map_values(&batches[0]),
+ vec![
+ Some(vec![("first".to_string(), Some(b"alpha".to_vec()))]),
+ Some(vec![("second".to_string(), Some(b"beta".to_vec()))]),
+ ]
+ );
+ let payload_end = BLOB_ENTRY_OVERHEAD * 2 + first.len() as u64 +
second.len() as u64;
+ let payload_reads = reader
+ .ranges()
+ .into_iter()
+ .filter(|range| range.start < payload_end && range.end > 0)
+ .collect::<Vec<_>>();
+ assert_eq!(payload_reads, vec![0..payload_end]);
+ }
+
#[tokio::test]
async fn test_blob_map_descriptor_read_skips_values() {
let file_path = "file:///tmp/map.blob";
@@ -2418,11 +2724,13 @@ mod tests {
range.start < element_data_range.end &&
element_data_range.start < range.end
})
.collect::<Vec<_>>();
- assert_eq!(overlapping_reads, vec![0..expected_entry_end]);
+ assert_eq!(overlapping_reads.len(), 1);
+ assert_eq!(overlapping_reads[0].start, 0);
+ assert!(overlapping_reads[0].end >= expected_entry_end);
}
#[tokio::test]
- async fn test_blob_array_reader_preserves_order_with_bounded_parallelism()
{
+ async fn test_blob_array_reader_preserves_order_after_coalescing() {
let payloads = (0_u8..12)
.map(|value| build_blob_array_payload(&[value], &[1]))
.collect::<Vec<_>>();
@@ -2455,48 +2763,34 @@ mod tests {
.map(|value| Some(vec![Some(vec![value])]))
.collect::<Vec<_>>()
);
- assert!(reader.max_in_flight() > 1);
- assert!(reader.max_in_flight() <= DEFAULT_BLOB_READ_PARALLELISM);
+ assert_eq!(reader.max_in_flight(), 1);
}
#[tokio::test]
async fn test_blob_reader_honors_configured_parallelism() {
- let payloads = (0_u8..12).map(|value| vec![value]).collect::<Vec<_>>();
- let rows = payloads
- .iter()
- .map(|payload| Some(payload.as_slice()))
- .collect::<Vec<_>>();
- let file_bytes = blob_test_utils::build_blob_file_bytes(&rows);
- let read_fields = vec![DataField::new(
- 0,
- "payload".to_string(),
- DataType::Blob(BlobType::new()),
- )];
- let reader = TrackingFileRead::new(Bytes::from(file_bytes.clone()));
+ let entry_length = BLOB_ENTRY_OVERHEAD + 1;
+ let stride = entry_length + BLOB_RANGE_MERGE_GAP + 1;
+ let mut bytes = vec![0; (3 * stride) as usize];
+ let mut planned_reads = Vec::new();
+ for value in 0_u8..3 {
+ let entry =
blob_test_utils::build_blob_file_bytes(&[Some(&[value])]);
+ let offset = u64::from(value) * stride;
+ bytes[offset as usize..(offset + entry_length) as usize]
+ .copy_from_slice(&entry[..entry_length as usize]);
+ planned_reads.push(PlannedBlobRead::Entry(offset..offset +
entry_length));
+ }
+ let reader = TrackingFileRead::new(Bytes::from(bytes));
- let batches = BlobFormatReader::new(String::new(), false)
- .with_blob_parallelism(2)
- .read_batch_stream(
- Box::new(reader.clone()),
- file_bytes.len() as u64,
- &read_fields,
- None,
- Some(12),
- None,
- )
- .await
- .unwrap()
- .try_collect::<Vec<_>>()
- .await
- .unwrap();
+ let values = fetch_blob_values(&reader, planned_reads,
2).await.unwrap();
+ let actual = values
+ .into_iter()
+ .map(|value| match value {
+ BlobReadValue::Value(value) => value[0],
+ other => panic!("Expected scalar BLOB value, got {other:?}"),
+ })
+ .collect::<Vec<_>>();
- assert_eq!(batches.len(), 1);
- assert_eq!(
- collect_binary_values(&batches[0]),
- (0_u8..12)
- .map(|value| Some(vec![value]))
- .collect::<Vec<_>>()
- );
+ assert_eq!(actual, vec![0, 1, 2]);
assert_eq!(reader.max_in_flight(), 2);
}
@@ -2533,7 +2827,7 @@ mod tests {
}
#[tokio::test]
- async fn test_blob_reader_reads_payloads_with_bounded_parallelism() {
+ async fn test_blob_reader_coalesces_adjacent_payload_reads() {
let read_fields = vec![DataField::new(
0,
"payload".to_string(),
@@ -2567,7 +2861,126 @@ mod tests {
Some(Vec::new()),
]
);
- assert!(reader.max_in_flight() > 1);
+ let payload_end = basic_blob_rows()
+ .into_iter()
+ .flatten()
+ .map(|value| BLOB_ENTRY_OVERHEAD + value.len() as u64)
+ .sum::<u64>();
+ let payload_reads = reader
+ .ranges()
+ .into_iter()
+ .filter(|range| range.start < payload_end && range.end > 0)
+ .collect::<Vec<_>>();
+ assert_eq!(payload_reads, vec![0..payload_end]);
+ }
+
+ #[tokio::test]
+ async fn test_blob_reader_limits_sparse_read_amplification() {
+ let selected = vec![b's'; 4 * 1024];
+ let skipped = vec![b'g'; 64 * 1024];
+ let rows = vec![
+ Some(selected.as_slice()),
+ Some(skipped.as_slice()),
+ Some(selected.as_slice()),
+ Some(skipped.as_slice()),
+ Some(selected.as_slice()),
+ ];
+ let file_bytes = blob_test_utils::build_blob_file_bytes(&rows);
+ let selected_entry_length = BLOB_ENTRY_OVERHEAD + selected.len() as
u64;
+ let skipped_entry_length = BLOB_ENTRY_OVERHEAD + skipped.len() as u64;
+ let selected_ranges = vec![
+ 0..selected_entry_length,
+ selected_entry_length + skipped_entry_length
+ ..2 * selected_entry_length + skipped_entry_length,
+ 2 * (selected_entry_length + skipped_entry_length)
+ ..3 * selected_entry_length + 2 * skipped_entry_length,
+ ];
+ let reader = TrackingFileRead::new(Bytes::from(file_bytes));
+ let planned_reads = selected_ranges
+ .iter()
+ .cloned()
+ .map(PlannedBlobRead::Entry)
+ .collect();
+
+ let values = fetch_blob_values(&reader, planned_reads,
1).await.unwrap();
+
+ assert_eq!(values.len(), 3);
+ assert!(values.into_iter().all(|value| matches!(
+ value,
+ BlobReadValue::Value(bytes) if bytes.as_ref() ==
selected.as_slice()
+ )));
+ let ranges = reader.ranges();
+ assert_eq!(ranges, selected_ranges);
+ let read_bytes = ranges
+ .iter()
+ .map(|range| range.end - range.start)
+ .sum::<u64>();
+ assert!(read_bytes <= 2 * selected_entry_length * 3);
+ }
+
+ #[test]
+ fn test_blob_range_merge_rejects_reported_sparse_layout() {
+ let entry_length = BLOB_ENTRY_OVERHEAD + 4 * 1024;
+ let stride = entry_length + BLOB_RANGE_MERGE_GAP;
+ let reads = (0..128)
+ .map(|result_index| {
+ let start = result_index as u64 * stride;
+ BlobEntryRead {
+ result_index,
+ range: start..start + entry_length,
+ }
+ })
+ .collect();
+
+ let merged = merge_blob_entry_reads(reads);
+
+ assert_eq!(merged.len(), 128);
+ assert_eq!(
+ merged
+ .iter()
+ .map(|read| read.range.end - read.range.start)
+ .sum::<u64>(),
+ 128 * entry_length
+ );
+ }
+
+ #[tokio::test]
+ async fn test_blob_reader_copies_selected_entries_from_gapped_span() {
+ let first = vec![b'a'; 4 * 1024];
+ let skipped = vec![b'g'; 1024];
+ let second = vec![b'b'; 4 * 1024];
+ let rows = vec![
+ Some(first.as_slice()),
+ Some(skipped.as_slice()),
+ Some(second.as_slice()),
+ ];
+ let file_bytes = blob_test_utils::build_blob_file_bytes(&rows);
+ let first_end = BLOB_ENTRY_OVERHEAD + first.len() as u64;
+ let second_start = first_end + BLOB_ENTRY_OVERHEAD + skipped.len() as
u64;
+ let second_end = second_start + BLOB_ENTRY_OVERHEAD + second.len() as
u64;
+ let reader = TrackingFileRead::new(Bytes::from(file_bytes));
+
+ let values = fetch_blob_values(
+ &reader,
+ vec![
+ PlannedBlobRead::Entry(0..first_end),
+ PlannedBlobRead::Entry(second_start..second_end),
+ ],
+ 1,
+ )
+ .await
+ .unwrap();
+
+ assert_eq!(reader.ranges(), vec![0..second_end]);
+ for value in values {
+ let BlobReadValue::Value(value) = value else {
+ panic!("Expected scalar BLOB value");
+ };
+ assert!(
+ value.is_unique(),
+ "selected values must not retain the merged span buffer"
+ );
+ }
}
#[test]
diff --git a/crates/paimon/src/table/data_evolution_reader/blob_fallback.rs
b/crates/paimon/src/table/data_evolution_reader/blob_fallback.rs
index adcab7d5..3000aed9 100644
--- a/crates/paimon/src/table/data_evolution_reader/blob_fallback.rs
+++ b/crates/paimon/src/table/data_evolution_reader/blob_fallback.rs
@@ -334,7 +334,7 @@ mod tests {
use arrow_array::{Array, LargeBinaryArray};
use bytes::Bytes;
use std::ops::Range;
- use std::sync::atomic::{AtomicUsize, Ordering};
+ use std::sync::Mutex;
#[test]
fn test_row_id_batch_cursor_honors_batch_size() {
@@ -356,13 +356,13 @@ mod tests {
#[derive(Clone)]
struct TrackingFileRead {
bytes: Bytes,
- reads: Arc<AtomicUsize>,
+ ranges: Arc<Mutex<Vec<Range<u64>>>>,
}
#[async_trait::async_trait]
impl FileRead for TrackingFileRead {
async fn read(&self, range: Range<u64>) -> crate::Result<Bytes> {
- self.reads.fetch_add(1, Ordering::SeqCst);
+ self.ranges.lock().unwrap().push(range.clone());
Ok(self.bytes.slice(range.start as usize..range.end as usize))
}
}
@@ -371,14 +371,14 @@ mod tests {
file_name: &str,
first_row_id: i64,
values: &[BlobFixtureValue<'_>],
- ) -> (LazyBlobFile, Arc<AtomicUsize>) {
+ ) -> (LazyBlobFile, Arc<Mutex<Vec<Range<u64>>>>) {
let bytes = Bytes::from(build_blob_file_bytes_with_values(values));
let file_size = bytes.len() as u64;
- let reads = Arc::new(AtomicUsize::new(0));
+ let ranges = Arc::new(Mutex::new(Vec::new()));
let reader = IndexedBlobReader::open(
Box::new(TrackingFileRead {
bytes,
- reads: reads.clone(),
+ ranges: ranges.clone(),
}),
file_size,
file_name.to_string(),
@@ -386,7 +386,7 @@ mod tests {
)
.await
.unwrap();
- reads.store(0, Ordering::SeqCst);
+ ranges.lock().unwrap().clear();
let last_row_id = first_row_id + i64::try_from(values.len()).unwrap()
- 1;
(
@@ -398,7 +398,7 @@ mod tests {
row_count: i64::try_from(values.len()).unwrap(),
reader: Some(reader),
},
- reads,
+ ranges,
)
}
@@ -406,9 +406,9 @@ mod tests {
async fn test_resolve_batch_skips_payloads_for_resolved_rows() {
use BlobFixtureValue::{Null, Placeholder, Value};
- let (latest, latest_reads) =
+ let (latest, latest_ranges) =
tracking_blob_file("latest.blob", 0, &[Value(b"new-0"), Null,
Placeholder]).await;
- let (older, older_reads) = tracking_blob_file(
+ let (older, older_ranges) = tracking_blob_file(
"older.blob",
0,
&[
@@ -419,7 +419,7 @@ mod tests {
],
)
.await;
- let (oldest, oldest_reads) = tracking_blob_file(
+ let (oldest, oldest_ranges) = tracking_blob_file(
"oldest.blob",
0,
&[
@@ -464,8 +464,15 @@ mod tests {
assert!(values.is_null(1));
assert_eq!(values.value(2), b"old-2");
assert_eq!(values.value(3), b"old-3");
- assert_eq!(latest_reads.load(Ordering::SeqCst), 1);
- assert_eq!(older_reads.load(Ordering::SeqCst), 2);
- assert_eq!(oldest_reads.load(Ordering::SeqCst), 0);
+ let blob_entry_overhead = 4 + 8 + 4;
+ let latest_entry_end = (blob_entry_overhead + b"new-0".len()) as u64;
+ assert_eq!(*latest_ranges.lock().unwrap(), vec![0..latest_entry_end]);
+
+ let older_entry_length = (blob_entry_overhead + b"old-0".len()) as u64;
+ assert_eq!(
+ *older_ranges.lock().unwrap(),
+ vec![2 * older_entry_length..4 * older_entry_length]
+ );
+ assert!(oldest_ranges.lock().unwrap().is_empty());
}
}