1use std::ops::Range;
18
19use bytes::{Bytes, BytesMut};
20use common_recordbatch::QueryMemoryTracker;
21use datatypes::arrow::record_batch::RecordBatch;
22use futures::StreamExt;
23use futures::stream::BoxStream;
24use object_store::ObjectStore;
25use parquet::DecodeResult;
26use parquet::arrow::ProjectionMask;
27use parquet::arrow::arrow_reader::{ArrowReaderMetadata, RowSelection};
28use parquet::arrow::push_decoder::ParquetPushDecoderBuilder;
29use snafu::{OptionExt, ResultExt, ensure};
30
31use crate::cache::file_cache::{FileType, IndexKey};
32use crate::cache::{CacheStrategy, PageRangePart};
33use crate::error::{OpenDalSnafu, ReadParquetSnafu, Result, UnexpectedSnafu};
34use crate::metrics::{READ_STAGE_ELAPSED, READ_STAGE_FETCH_PAGES};
35use crate::sst::file::RegionFileId;
36use crate::sst::parquet::helper::fetch_byte_ranges;
37use crate::sst::parquet::row_group::{ParquetFetchMetrics, compute_total_range_size};
38
39pub struct SstParquetRangeFetcher {
45 region_file_id: RegionFileId,
47 file_path: String,
49 object_store: ObjectStore,
51 cache_strategy: CacheStrategy,
53 row_group_idx: usize,
55 fetch_metrics: Option<ParquetFetchMetrics>,
57 memory_tracker: Option<QueryMemoryTracker>,
59}
60
61impl SstParquetRangeFetcher {
62 pub fn new(
64 region_file_id: RegionFileId,
65 file_path: String,
66 object_store: ObjectStore,
67 cache_strategy: CacheStrategy,
68 row_group_idx: usize,
69 fetch_metrics: Option<ParquetFetchMetrics>,
70 ) -> Self {
71 Self {
72 region_file_id,
73 file_path,
74 object_store,
75 cache_strategy,
76 row_group_idx,
77 fetch_metrics,
78 memory_tracker: None,
79 }
80 }
81
82 pub(crate) fn with_memory_tracker(mut self, tracker: Option<QueryMemoryTracker>) -> Self {
84 self.memory_tracker = tracker;
85 self
86 }
87
88 async fn fetch_bytes_with_cache(&self, ranges: Vec<Range<u64>>) -> Result<Vec<Bytes>> {
90 let fetch_start = self
91 .fetch_metrics
92 .as_ref()
93 .map(|_| std::time::Instant::now());
94 let _timer = READ_STAGE_FETCH_PAGES.start_timer();
95
96 let mut page_lookup = self.cache_strategy.get_page_ranges(
97 self.region_file_id.file_id(),
98 self.row_group_idx,
99 &ranges,
100 );
101 if let Some(lookup) = &page_lookup
102 && lookup.cached_bytes > 0
103 && let Some(metrics) = &self.fetch_metrics
104 {
105 let mut metrics_data = metrics.data.lock().unwrap();
106 metrics_data.page_cache_hit += 1;
107 metrics_data.pages_to_fetch_mem += lookup.cached_range_count;
108 metrics_data.page_size_to_fetch_mem += lookup.cached_bytes;
109 metrics_data.page_size_needed += lookup.cached_bytes;
110 }
111
112 if page_lookup
114 .as_ref()
115 .map(|lookup| lookup.is_fully_cached())
116 .unwrap_or(false)
117 {
118 let lookup = page_lookup.take().unwrap();
119 if let Some(metrics) = &self.fetch_metrics
120 && let Some(start) = fetch_start
121 {
122 metrics.data.lock().unwrap().total_fetch_elapsed += start.elapsed();
123 }
124 return assemble_ranges(&ranges, lookup.cached_parts, &[]);
125 }
126
127 let missing_ranges = page_lookup
128 .as_ref()
129 .map(|lookup| lookup.missing_ranges.clone())
130 .unwrap_or_else(|| ranges.clone());
131
132 let (_, unaligned_size) = compute_total_range_size(&missing_ranges);
134
135 let key = IndexKey::new(
137 self.region_file_id.region_id(),
138 self.region_file_id.file_id(),
139 FileType::Parquet,
140 );
141 let fetch_write_cache_start = self
142 .fetch_metrics
143 .as_ref()
144 .map(|_| std::time::Instant::now());
145 let write_cache_result = match self.cache_strategy.write_cache() {
146 Some(cache) => cache.file_cache().read_ranges(key, &missing_ranges).await,
147 None => None,
148 };
149
150 let fetched_pages = match write_cache_result {
151 Some(data) => {
152 if let Some(metrics) = &self.fetch_metrics {
153 let elapsed = fetch_write_cache_start
154 .map(|start| start.elapsed())
155 .unwrap_or_default();
156 let range_size_needed: u64 =
157 missing_ranges.iter().map(|r| r.end - r.start).sum();
158 let mut metrics_data = metrics.data.lock().unwrap();
159 metrics_data.write_cache_fetch_elapsed += elapsed;
160 metrics_data.write_cache_hit += 1;
161 metrics_data.pages_to_fetch_write_cache += missing_ranges.len();
162 metrics_data.page_size_to_fetch_write_cache += unaligned_size;
163 metrics_data.page_size_needed += range_size_needed;
164 }
165 data
166 }
167 None => {
168 let _timer = READ_STAGE_ELAPSED
170 .with_label_values(&["cache_miss_read"])
171 .start_timer();
172
173 let start = self
174 .fetch_metrics
175 .as_ref()
176 .map(|_| std::time::Instant::now());
177 let data =
178 fetch_byte_ranges(&self.file_path, self.object_store.clone(), &missing_ranges)
179 .await
180 .context(OpenDalSnafu)?;
181
182 if let Some(metrics) = &self.fetch_metrics {
183 let elapsed = start.map(|start| start.elapsed()).unwrap_or_default();
184 let range_size_needed: u64 =
185 missing_ranges.iter().map(|r| r.end - r.start).sum();
186 let mut metrics_data = metrics.data.lock().unwrap();
187 metrics_data.store_fetch_elapsed += elapsed;
188 metrics_data.cache_miss += 1;
189 metrics_data.pages_to_fetch_store += missing_ranges.len();
190 metrics_data.page_size_to_fetch_store += unaligned_size;
191 metrics_data.page_size_needed += range_size_needed;
192 }
193 data
194 }
195 };
196 ensure!(
197 fetched_pages.len() == missing_ranges.len(),
198 UnexpectedSnafu {
199 reason: format!(
200 "Invalid parquet range fetch: {} missing ranges but {} fetched byte ranges",
201 missing_ranges.len(),
202 fetched_pages.len()
203 ),
204 }
205 );
206
207 self.cache_strategy.put_page_ranges(
208 self.region_file_id.file_id(),
209 self.row_group_idx,
210 &missing_ranges,
211 &fetched_pages,
212 );
213
214 if let (Some(metrics), Some(start)) = (&self.fetch_metrics, fetch_start) {
215 metrics.data.lock().unwrap().total_fetch_elapsed += start.elapsed();
216 }
217
218 if let Some(lookup) = page_lookup {
219 let fetched_parts = missing_ranges
220 .into_iter()
221 .zip(fetched_pages)
222 .map(|(range, bytes)| PageRangePart { range, bytes })
223 .collect::<Vec<_>>();
224 return assemble_ranges(&ranges, lookup.cached_parts, &fetched_parts);
225 }
226
227 Ok(fetched_pages)
228 }
229}
230
231fn assemble_ranges(
232 ranges: &[Range<u64>],
233 cached_parts: Vec<Vec<PageRangePart>>,
234 fetched_parts: &[PageRangePart],
235) -> Result<Vec<Bytes>> {
236 ensure!(
237 ranges.len() == cached_parts.len(),
238 UnexpectedSnafu {
239 reason: format!(
240 "Invalid parquet range assembly: {} requested ranges but {} cached part groups",
241 ranges.len(),
242 cached_parts.len()
243 ),
244 }
245 );
246
247 ranges
248 .iter()
249 .zip(cached_parts)
250 .map(|(range, mut parts)| {
251 parts.extend(
252 fetched_parts
253 .iter()
254 .filter_map(|part| overlapping_part(range, part)),
255 );
256 assemble_range(range, parts)
257 })
258 .collect()
259}
260
261fn overlapping_part(range: &Range<u64>, part: &PageRangePart) -> Option<PageRangePart> {
262 let start = range.start.max(part.range.start);
263 let end = range.end.min(part.range.end);
264 if start >= end {
265 return None;
266 }
267
268 let slice_start = (start - part.range.start) as usize;
269 let slice_end = (end - part.range.start) as usize;
270 Some(PageRangePart {
271 range: start..end,
272 bytes: part.bytes.slice(slice_start..slice_end),
273 })
274}
275
276fn assemble_range(range: &Range<u64>, mut parts: Vec<PageRangePart>) -> Result<Bytes> {
277 if range.start >= range.end {
278 return Ok(Bytes::new());
279 }
280
281 parts.sort_unstable_by_key(|part| part.range.start);
282 if parts.len() == 1 && parts[0].range == *range {
283 return Ok(parts.pop().unwrap().bytes);
284 }
285
286 let mut cursor = range.start;
287 let mut output = BytesMut::with_capacity((range.end - range.start) as usize);
288 for part in parts {
289 ensure!(
290 part.range.start <= cursor,
291 UnexpectedSnafu {
292 reason: format!(
293 "Missing cached parquet bytes for range {}..{}, next part starts at {}",
294 range.start, range.end, part.range.start
295 ),
296 }
297 );
298 if part.range.end <= cursor {
299 continue;
300 }
301
302 let slice_start = (cursor - part.range.start) as usize;
303 let slice_end = (part.range.end.min(range.end) - part.range.start) as usize;
304 output.extend_from_slice(&part.bytes.slice(slice_start..slice_end));
305 cursor = part.range.end.min(range.end);
306 if cursor >= range.end {
307 break;
308 }
309 }
310
311 ensure!(
312 cursor == range.end,
313 UnexpectedSnafu {
314 reason: format!(
315 "Missing cached parquet bytes for range {}..{}, assembled through {}",
316 range.start, range.end, cursor
317 ),
318 }
319 );
320
321 Ok(output.freeze())
322}
323
324pub fn build_sst_parquet_record_batch_stream(
326 arrow_metadata: ArrowReaderMetadata,
327 row_group_idx: usize,
328 row_selection: Option<RowSelection>,
329 projection: ProjectionMask,
330 fetcher: SstParquetRangeFetcher,
331 file_path: String,
332 batch_size: usize,
333) -> Result<BoxStream<'static, Result<RecordBatch>>> {
334 let uncompressed_bytes = if fetcher.memory_tracker.is_some() {
335 let row_group = arrow_metadata.metadata().row_group(row_group_idx);
336 row_group
337 .columns()
338 .iter()
339 .enumerate()
340 .filter(|(index, _)| projection.leaf_included(*index))
341 .try_fold(0usize, |total, (_, column)| {
342 usize::try_from(column.uncompressed_size())
343 .ok()
344 .and_then(|size| total.checked_add(size))
345 })
346 .context(UnexpectedSnafu {
347 reason: "Invalid projected row-group uncompressed size",
348 })?
349 } else {
350 0
351 };
352 let mut builder = ParquetPushDecoderBuilder::new_with_metadata(arrow_metadata)
353 .with_row_groups(vec![row_group_idx])
354 .with_projection(projection)
355 .with_batch_size(batch_size);
356
357 if let Some(selection) = row_selection {
358 builder = builder.with_row_selection(selection);
359 }
360
361 let mut decoder = builder
362 .build()
363 .context(ReadParquetSnafu { path: &file_path })?;
364
365 Ok(async_stream::try_stream! {
366 let _reservation = if let Some(tracker) = &fetcher.memory_tracker {
367 Some(tracker.reserve(uncompressed_bytes)
368 .context(crate::error::RecordBatchSnafu)?)
369 } else {
370 None
371 };
372 loop {
373 match decoder.try_decode().context(ReadParquetSnafu { path: &file_path })? {
374 DecodeResult::NeedsData(ranges) => {
375 let data = fetcher.fetch_bytes_with_cache(ranges.clone()).await?;
376 decoder.push_ranges(ranges, data)
377 .context(ReadParquetSnafu { path: &file_path })?;
378 }
379 DecodeResult::Data(batch) => yield batch,
380 DecodeResult::Finished => break,
381 }
382 }
383 }
384 .boxed())
385}
386
387#[cfg(test)]
388mod tests {
389 use super::*;
390
391 #[tokio::test]
392 async fn test_decoder_memory_projection_lifetime_and_error() {
393 use std::sync::Arc;
394
395 use datatypes::arrow::array::{Int64Array, StringArray};
396 use datatypes::arrow::datatypes::{DataType, Field, Schema};
397 use object_store::services::Memory;
398 use parquet::arrow::ArrowWriter;
399 use parquet::arrow::arrow_reader::ArrowReaderOptions;
400 use parquet::file::properties::WriterProperties;
401
402 use crate::sst::parquet::metadata::MetadataLoader;
403 use crate::sst::parquet::reader::MetadataCacheMetrics;
404
405 let schema = Arc::new(Schema::new(vec![
406 Field::new("value", DataType::Int64, false),
407 Field::new("payload", DataType::Utf8, false),
408 ]));
409 let batch = RecordBatch::try_new(
410 schema.clone(),
411 vec![
412 Arc::new(Int64Array::from_iter_values(0..8)),
413 Arc::new(StringArray::from_iter_values(
414 (0..8).map(|i| format!("{i}:{}", "x".repeat(512))),
415 )),
416 ],
417 )
418 .unwrap();
419 let mut bytes = Vec::new();
420 let props = WriterProperties::builder()
421 .set_max_row_group_row_count(Some(4))
422 .build();
423 let mut writer = ArrowWriter::try_new(&mut bytes, schema, Some(props)).unwrap();
424 writer.write(&batch).unwrap();
425 writer.close().unwrap();
426 let store = ObjectStore::new(Memory::default()).unwrap();
427 let file_size = bytes.len() as u64;
428 store.write("memory.parquet", bytes).await.unwrap();
429 let metadata = MetadataLoader::new(store.clone(), "memory.parquet", file_size)
430 .load(&mut MetadataCacheMetrics::default())
431 .await
432 .unwrap();
433 let sizes: Vec<_> = metadata
434 .row_groups()
435 .iter()
436 .map(|rg| rg.column(0).uncompressed_size() as usize)
437 .collect();
438 let aligned = |bytes: usize| bytes.div_ceil(1024) * 1024;
439 let limit = aligned(*sizes.iter().max().unwrap());
440 assert!(metadata.row_group(0).total_byte_size() as usize > limit);
441 let projection = ProjectionMask::leaves(metadata.file_metadata().schema_descr(), [0]);
442 let metadata =
443 ArrowReaderMetadata::try_new(Arc::new(metadata), ArrowReaderOptions::new()).unwrap();
444 let tracker = QueryMemoryTracker::builder(limit).build();
445 let cache = CacheStrategy::EnableAll(Arc::new(
446 crate::cache::CacheManager::builder()
447 .page_cache_size(1024 * 1024)
448 .build(),
449 ));
450 let region_file_id = RegionFileId::new(
451 store_api::storage::RegionId::new(1, 1),
452 store_api::storage::FileId::random(),
453 );
454 let build = |row_group_idx, path: &str, selection| {
455 let fetcher = SstParquetRangeFetcher::new(
456 region_file_id,
457 path.into(),
458 store.clone(),
459 if path == "memory.parquet" {
460 cache.clone()
461 } else {
462 CacheStrategy::Disabled
463 },
464 row_group_idx,
465 None,
466 )
467 .with_memory_tracker(Some(tracker.clone()));
468 build_sst_parquet_record_batch_stream(
469 metadata.clone(),
470 row_group_idx,
471 selection,
472 projection.clone(),
473 fetcher,
474 path.into(),
475 1,
476 )
477 .unwrap()
478 };
479 drop(build(0, "memory.parquet", None));
481 assert_eq!(0, tracker.current());
482 for (row_group_idx, size) in sizes.iter().enumerate() {
483 let mut first = build(row_group_idx, "memory.parquet", None);
484 assert_eq!(1, first.next().await.unwrap().unwrap().num_rows());
485 assert_eq!(aligned(*size), tracker.current());
486 let mut competing = build(row_group_idx, "memory.parquet", None);
487 assert!(competing.next().await.unwrap().is_err());
488 assert_eq!(aligned(*size), tracker.current());
489 let mut skipped = build(
491 row_group_idx,
492 "memory.parquet",
493 Some(RowSelection::from(vec![
494 parquet::arrow::arrow_reader::RowSelector::skip(4),
495 ])),
496 );
497 assert!(skipped.next().await.unwrap().is_err());
498 assert_eq!(aligned(*size), tracker.current());
499 while first.next().await.transpose().unwrap().is_some() {}
500 assert_eq!(0, tracker.current());
502 }
503 store.delete("memory.parquet").await.unwrap();
505 let mut cancelled = build(0, "memory.parquet", None);
506 cancelled.next().await.unwrap().unwrap();
507 drop(cancelled);
508 assert_eq!(0, tracker.current());
509 let mut failed = build(0, "missing.parquet", None);
510 assert!(failed.next().await.unwrap().is_err());
511 drop(failed);
512 assert_eq!(0, tracker.current());
513 let mut skipped = build(
514 0,
515 "memory.parquet",
516 Some(RowSelection::from(vec![
517 parquet::arrow::arrow_reader::RowSelector::skip(4),
518 ])),
519 );
520 assert!(skipped.next().await.is_none());
521 assert_eq!(0, tracker.current());
522 }
523
524 #[test]
525 fn test_assemble_range_from_cached_subrange_and_fetched_tail() {
526 let cached_parts = vec![vec![PageRangePart {
527 range: 400..500,
528 bytes: Bytes::from(vec![1; 100]),
529 }]];
530 let fetched_parts = vec![PageRangePart {
531 range: 500..600,
532 bytes: Bytes::from(vec![2; 100]),
533 }];
534
535 let requested = 400..600;
536 let output = assemble_ranges(
537 std::slice::from_ref(&requested),
538 cached_parts,
539 &fetched_parts,
540 )
541 .unwrap();
542 assert_eq!(1, output.len());
543 assert_eq!(vec![1; 100].as_slice(), &output[0][..100]);
544 assert_eq!(vec![2; 100].as_slice(), &output[0][100..]);
545 }
546
547 #[test]
548 fn test_assemble_range_returns_single_covering_part_without_copy() {
549 let bytes = Bytes::from_static(b"abcdef");
550 let cached_parts = vec![vec![PageRangePart {
551 range: 10..16,
552 bytes: bytes.clone(),
553 }]];
554
555 let requested = 10..16;
556 let output = assemble_ranges(std::slice::from_ref(&requested), cached_parts, &[]).unwrap();
557 assert_eq!(bytes, output[0]);
558 }
559
560 #[test]
561 fn test_assemble_range_clamps_overlapping_part_to_requested_end() {
562 let parts = vec![PageRangePart {
563 range: 0..10,
564 bytes: Bytes::from_static(b"0123456789"),
565 }];
566
567 let output = assemble_range(&(2..5), parts).unwrap();
568 assert_eq!(Bytes::from_static(b"234"), output);
569 }
570}