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 91a8b21d [core] Avoid excess manifest range reads with sidecars (#990)
91a8b21d is described below
commit 91a8b21ddf976f49bd34abbcc5e6442a4761e143
Author: Jingsong Lee <[email protected]>
AuthorDate: Tue Sep 29 09:41:21 2026 +0800
[core] Avoid excess manifest range reads with sidecars (#990)
---
crates/paimon/src/spec/manifest_sidecar.rs | 149 ++++++++++++++++++++++-------
crates/paimon/src/table/table_scan.rs | 97 +++++++++++++++++--
2 files changed, 205 insertions(+), 41 deletions(-)
diff --git a/crates/paimon/src/spec/manifest_sidecar.rs
b/crates/paimon/src/spec/manifest_sidecar.rs
index c11a4a41..ecdeafe6 100644
--- a/crates/paimon/src/spec/manifest_sidecar.rs
+++ b/crates/paimon/src/spec/manifest_sidecar.rs
@@ -44,6 +44,8 @@ const MIN_BLOCK_BYTES: usize = 6;
const MAX_INT: u64 = i32::MAX as u64;
const MAX_LONG: u64 = i64::MAX as u64;
const BLOCK_READ_BUFFER_BYTES: u64 = 4 * 1024 * 1024;
+// Cap the extra bytes spent to replace several range requests with one full
read.
+const MAX_FULL_READ_AMPLIFICATION: usize = 4;
type RowBlockFilter<'a> = dyn Fn(i64, i64) -> bool + Sync + 'a;
type PartitionBlockFilter<'a> = dyn FnMut(&[u8]) -> bool + Send + 'a;
@@ -81,6 +83,7 @@ impl ManifestSidecarBlock {
pub struct ManifestSidecarSelection {
header: Bytes,
blocks: Vec<ManifestSidecarBlock>,
+ manifest_size: u64,
}
impl ManifestSidecarSelection {
@@ -91,6 +94,64 @@ impl ManifestSidecarSelection {
pub fn blocks(&self) -> &[ManifestSidecarBlock] {
&self.blocks
}
+
+ /// Use a full read when it saves requests without multiplying transferred
+ /// bytes by more than the number of range reads (capped at four).
+ fn should_read_full(&self) -> Result<bool> {
+ let range_reads = self.read_ranges()?.len();
+ if range_reads <= 1 {
+ return Ok(false);
+ }
+ let selected_bytes = self.blocks.iter().try_fold(0u64, |total, block| {
+ total.checked_add(block.length).ok_or_else(invalid_sidecar)
+ })?;
+ Ok(
+ u128::from(selected_bytes) *
range_reads.min(MAX_FULL_READ_AMPLIFICATION) as u128
+ >= u128::from(self.manifest_size),
+ )
+ }
+
+ pub(crate) async fn read_bytes(&self, file_io: &FileIO, manifest_path:
&str) -> Result<Bytes> {
+ if self.should_read_full()? {
+ file_io.new_input(manifest_path)?.read().await
+ } else {
+ ManifestSidecar::read_selected_bytes(file_io, manifest_path,
self).await
+ }
+ }
+
+ fn read_ranges(&self) -> Result<Vec<Range<u64>>> {
+ let mut ranges = Vec::new();
+ let mut block_index = 0usize;
+ while block_index < self.blocks.len() {
+ let first = self.blocks[block_index];
+ let mut span_end = first
+ .offset
+ .checked_add(first.length)
+ .ok_or_else(invalid_sidecar)?;
+ block_index += 1;
+ while block_index < self.blocks.len() {
+ let next = self.blocks[block_index];
+ let Some(next_end) = next.offset.checked_add(next.length) else
{
+ return Err(invalid_sidecar());
+ };
+ if next.offset != span_end
+ || next_end.saturating_sub(first.offset) >
BLOCK_READ_BUFFER_BYTES
+ {
+ break;
+ }
+ span_end = next_end;
+ block_index += 1;
+ }
+
+ let mut position = first.offset;
+ while position < span_end {
+ let end = span_end.min(position + BLOCK_READ_BUFFER_BYTES);
+ ranges.push(position..end);
+ position = end;
+ }
+ }
+ Ok(ranges)
+ }
}
#[derive(Debug)]
@@ -566,41 +627,10 @@ impl ManifestSidecar {
);
out.extend_from_slice(&selection.header);
- let mut block_index = 0usize;
- while block_index < selection.blocks.len() {
- let first = selection.blocks[block_index];
- let mut span_end = first
- .offset
- .checked_add(first.length)
- .ok_or_else(invalid_sidecar)?;
- block_index += 1;
- while block_index < selection.blocks.len() {
- let next = selection.blocks[block_index];
- let Some(next_end) = next.offset.checked_add(next.length) else
{
- return Err(invalid_sidecar());
- };
- if next.offset != span_end
- || next_end.saturating_sub(first.offset) >
BLOCK_READ_BUFFER_BYTES
- {
- break;
- }
- span_end = next_end;
- block_index += 1;
- }
-
- let mut position = first.offset;
- while position < span_end {
- let end = span_end.min(position + BLOCK_READ_BUFFER_BYTES);
- let bytes = reader
- .read(Range {
- start: position,
- end,
- })
- .await?;
- require(bytes.len() as u64 == end - position)?;
- out.extend_from_slice(&bytes);
- position = end;
- }
+ for range in selection.read_ranges()? {
+ let bytes = reader.read(range.clone()).await?;
+ require(bytes.len() as u64 == range.end - range.start)?;
+ out.extend_from_slice(&bytes);
}
Ok(Bytes::from(out))
}
@@ -846,6 +876,7 @@ fn select_with_filters(
Ok(ManifestSidecarSelection {
header: Bytes::copy_from_slice(header),
blocks: selected,
+ manifest_size,
})
}
@@ -1079,6 +1110,54 @@ mod tests {
(builder.serialize(size, 7).unwrap(), meta(size, 7))
}
+ #[test]
+ fn full_read_policy_accounts_for_bytes_and_range_requests() {
+ const MIB: u64 = 1024 * 1024;
+ let selection = |blocks: &[(u64, u64)]| ManifestSidecarSelection {
+ header: Bytes::new(),
+ blocks: blocks
+ .iter()
+ .map(|&(offset, length)| ManifestSidecarBlock {
+ offset,
+ length,
+ first_record: 0,
+ record_count: 1,
+ })
+ .collect(),
+ manifest_size: 8 * MIB,
+ };
+
+ assert!(!selection(&[]).should_read_full().unwrap());
+ // A contiguous selection larger than the 4 MiB read buffer still needs
+ // two GETs, so a full read saves a request.
+ let large = selection(&[(0, 6 * MIB)]);
+ assert_eq!(large.read_ranges().unwrap().len(), 2);
+ assert!(large.should_read_full().unwrap());
+
+ assert!(selection(&[(0, 2 * MIB), (4 * MIB, 2 * MIB)])
+ .should_read_full()
+ .unwrap());
+ assert!(!selection(&[(0, MIB), (4 * MIB, MIB)])
+ .should_read_full()
+ .unwrap());
+ assert!(selection(&[
+ (0, MIB / 2),
+ (2 * MIB, MIB / 2),
+ (4 * MIB, MIB / 2),
+ (6 * MIB, MIB / 2),
+ ])
+ .should_read_full()
+ .unwrap());
+ assert!(!selection(&[
+ (0, MIB / 8),
+ (2 * MIB, MIB / 8),
+ (4 * MIB, MIB / 8),
+ (6 * MIB, MIB / 8),
+ ])
+ .should_read_full()
+ .unwrap());
+ }
+
fn partition(p: i32, q: Option<&str>) -> Vec<u8> {
let mut builder = BinaryRowBuilder::new(2);
builder.write_int(0, p);
diff --git a/crates/paimon/src/table/table_scan.rs
b/crates/paimon/src/table/table_scan.rs
index f4edba05..e2a26032 100644
--- a/crates/paimon/src/table/table_scan.rs
+++ b/crates/paimon/src/table/table_scan.rs
@@ -189,7 +189,7 @@ async fn read_manifest_bytes_with_sidecar(
.await;
match selection.as_ref() {
- Some(selection) => ManifestSidecar::read_selected_bytes(file_io, path,
selection).await,
+ Some(selection) => selection.read_bytes(file_io, path).await,
None => file_io.new_input(path)?.read().await,
}
}
@@ -2746,11 +2746,11 @@ mod tests {
use super::{
data_evolution_row_range_groups, data_file_overlaps_row_range_index,
group_data_files_by_partition_bucket,
manifest_file_overlaps_row_range_index,
- prune_data_evolution_group_by_read_fields, retain_index_manifest_entry,
- retain_index_manifest_entry_for_scan, retain_manifest_buckets,
+ prune_data_evolution_group_by_read_fields,
read_manifest_bytes_with_sidecar,
+ retain_index_manifest_entry, retain_index_manifest_entry_for_scan,
retain_manifest_buckets,
retain_manifest_entry_row_ranges, retain_manifest_row_ranges,
scan_predicate_field_ids,
should_skip_level_zero_for_scan, split_row_ranges_for_files,
LimitPushdownAccumulator,
- PaimonTableScan, RowRangeIndex, TableScan,
+ ManifestSidecarPruning, PaimonTableScan, RowRangeIndex, TableScan,
};
use crate::catalog::Identifier;
use crate::io::FileIOBuilder;
@@ -2758,8 +2758,9 @@ mod tests {
stats::BinaryTableStats, ArrayType, BinaryRow, BinaryRowBuilder,
BucketFunctionType,
ColumnMove, CommitKind, DataField, DataFileMeta, DataType, Datum,
DeletionVectorMeta,
FileKind, GlobalIndexMeta, IndexFileMeta, IndexManifestEntry, IntType,
ManifestEntry,
- ManifestFileMeta, Predicate, PredicateBuilder, PredicateOperator,
Schema as PaimonSchema,
- SchemaChange, Snapshot, TableSchema, VarCharType,
+ ManifestFileMeta, ManifestSidecar, ManifestSidecarBuilder, Predicate,
PredicateBuilder,
+ PredicateOperator, Schema as PaimonSchema, SchemaChange, Snapshot,
TableSchema,
+ VarCharType,
};
use crate::table::bucket_filter::{compute_target_buckets,
extract_predicate_for_keys};
use crate::table::partition_filter::PartitionFilter;
@@ -3946,6 +3947,90 @@ mod tests {
assert_eq!(delta_files, vec!["a-new", "a-old"]);
}
+ #[tokio::test]
+ async fn
test_manifest_sidecar_read_uses_full_file_only_for_dense_selection() {
+ let file_io = FileIOBuilder::new("memory").build().unwrap();
+ let manifest_path = "memory:/adaptive_manifest_read/manifest-test";
+ let sidecar_path = ManifestSidecar::path(manifest_path);
+ let mut header = b"Obj\x01".to_vec();
+ header.resize(32, 0);
+ let mut builder = ManifestSidecarBuilder::new(header.clone(), true,
false);
+ let mut offset = header.len() as u64;
+ for (length, first_row_id) in [(200, 0), (50, 100), (200, 200)] {
+ builder.begin_block(offset, length, 1).unwrap();
+ builder
+ .add(Some(first_row_id), 1, None, None, None)
+ .unwrap();
+ builder.end_block().unwrap();
+ offset += length;
+ }
+ let sidecar = builder.serialize(offset, 3).unwrap();
+ let manifest = ManifestFileMeta::new(
+ "manifest-test".to_string(),
+ offset as i64,
+ 3,
+ 0,
+ BinaryTableStats::empty(),
+ 0,
+ )
+
.with_extra_files(Some(vec!["manifest-test.avro.sidecar".to_string()]));
+ let mut avro = header.clone();
+ avro.extend((0..450).map(|value| value as u8));
+ file_io
+ .new_output(manifest_path)
+ .unwrap()
+ .write(Bytes::from(avro.clone()))
+ .await
+ .unwrap();
+ file_io
+ .new_output(&sidecar_path)
+ .unwrap()
+ .write(Bytes::from(sidecar))
+ .await
+ .unwrap();
+
+ let dense = RowRangeIndex::create(vec![RowRange::new(0, 0),
RowRange::new(200, 200)]);
+ let dense_bytes = read_manifest_bytes_with_sidecar(
+ &file_io,
+ manifest_path,
+ &manifest,
+ true,
+ ManifestSidecarPruning {
+ row_range_index: Some(&dense),
+ partition_filter: None,
+ partition_arity: 0,
+ bucket_predicate: None,
+ bucket_key_fields: &[],
+ bucket_function_type: BucketFunctionType::Default,
+ },
+ )
+ .await
+ .unwrap();
+ assert_eq!(dense_bytes.as_ref(), avro);
+
+ let sparse = RowRangeIndex::create(vec![RowRange::new(0, 0)]);
+ let sparse_bytes = read_manifest_bytes_with_sidecar(
+ &file_io,
+ manifest_path,
+ &manifest,
+ true,
+ ManifestSidecarPruning {
+ row_range_index: Some(&sparse),
+ partition_filter: None,
+ partition_arity: 0,
+ bucket_predicate: None,
+ bucket_key_fields: &[],
+ bucket_function_type: BucketFunctionType::Default,
+ },
+ )
+ .await
+ .unwrap();
+ let mut expected = header;
+ let start = expected.len();
+ expected.extend_from_slice(&avro[start..start + 200]);
+ assert_eq!(sparse_bytes.as_ref(), expected);
+ }
+
#[tokio::test]
async fn test_manifest_sidecar_prunes_avro_blocks_before_entry_decode() {
const FILE_COUNT: usize = 1_000;