1use std::future::Future;
18use std::mem;
19use std::sync::Arc;
20
21use async_stream::try_stream;
22use common_telemetry::warn;
23use common_time::Timestamp;
24use common_time::range::TimestampRange;
25use common_time::timestamp::TimeUnit;
26use datafusion_expr::expr::Expr;
27use datafusion_expr::{Between, BinaryExpr, Operator};
28use datatypes::arrow::compute::concat_batches;
29use datatypes::arrow::record_batch::RecordBatch;
30use datatypes::prelude::ConcreteDataType;
31use datatypes::value::scalar_value_to_timestamp;
32use futures::TryStreamExt;
33use snafu::ResultExt;
34use store_api::region_engine::PartitionRange;
35use store_api::storage::{FileId, RegionId, SequenceRange, TimeSeriesRowSelector};
36use table::predicate::is_string_timestamp_literal;
37use tokio::sync::{mpsc, oneshot};
38
39use crate::cache::CacheStrategy;
40use crate::cache::range_cache::{RangeCacheDelivery, RangeCacheLookup, RangeCacheWorker};
41use crate::error::{ComputeArrowSnafu, Result};
42use crate::read::BoxedRecordBatchStream;
43use crate::read::read_columns::ReadColumns;
44use crate::read::scan_region::StreamContext;
45use crate::read::scan_util::PartitionMetrics;
46use crate::read::series_reader::SeriesRange;
47use crate::region::options::MergeMode;
48use crate::sst::parquet::DEFAULT_READ_BATCH_SIZE;
49
50const RANGE_CACHE_COMPACT_THRESHOLD_BYTES: usize = 8 * 1024 * 1024;
51
52#[derive(Debug, Clone, PartialEq, Eq, Hash)]
59pub(crate) struct ScanRequestFingerprint {
60 inner: Arc<SharedScanRequestFingerprint>,
62 time_filters: Option<Arc<Vec<String>>>,
64 series_row_selector: Option<TimeSeriesRowSelector>,
65 append_mode: bool,
66 filter_deleted: bool,
67 merge_mode: MergeMode,
68 sequence_range: Option<SequenceRange>,
72 stage: RangeScanStage,
73 partition_expr_version: u64,
76}
77
78#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
79enum RangeScanStage {
80 Data,
81 DataForCrossRangeMerge,
84 CandidateSeries,
85 SeriesData(SeriesRange),
86}
87
88#[derive(Debug)]
89pub(crate) struct ScanRequestFingerprintBuilder {
90 pub(crate) read_columns: ReadColumns,
91 pub(crate) read_column_types: Vec<Option<ConcreteDataType>>,
92 pub(crate) filters: Vec<String>,
93 pub(crate) time_filters: Vec<String>,
94 pub(crate) series_row_selector: Option<TimeSeriesRowSelector>,
95 pub(crate) append_mode: bool,
96 pub(crate) filter_deleted: bool,
97 pub(crate) merge_mode: MergeMode,
98 pub(crate) sequence_range: Option<SequenceRange>,
99 pub(crate) partition_expr_version: u64,
100}
101
102impl ScanRequestFingerprintBuilder {
103 pub(crate) fn build(self) -> ScanRequestFingerprint {
104 let Self {
105 read_columns,
106 read_column_types,
107 filters,
108 time_filters,
109 series_row_selector,
110 append_mode,
111 filter_deleted,
112 merge_mode,
113 sequence_range,
114 partition_expr_version,
115 } = self;
116
117 ScanRequestFingerprint {
118 inner: Arc::new(SharedScanRequestFingerprint {
119 read_columns,
120 read_column_types,
121 filters,
122 }),
123 time_filters: (!time_filters.is_empty()).then(|| Arc::new(time_filters)),
124 series_row_selector,
125 append_mode,
126 filter_deleted,
127 merge_mode,
128 sequence_range,
129 stage: RangeScanStage::Data,
130 partition_expr_version,
131 }
132 }
133}
134
135#[derive(Debug, PartialEq, Eq, Hash)]
137struct SharedScanRequestFingerprint {
138 read_columns: ReadColumns,
140 read_column_types: Vec<Option<ConcreteDataType>>,
143 filters: Vec<String>,
145}
146
147impl ScanRequestFingerprint {
148 #[cfg(test)]
149 pub(crate) fn read_columns(&self) -> &ReadColumns {
150 &self.inner.read_columns
151 }
152
153 #[cfg(test)]
154 pub(crate) fn read_column_types(&self) -> &[Option<ConcreteDataType>] {
155 &self.inner.read_column_types
156 }
157
158 #[cfg(test)]
159 pub(crate) fn filters(&self) -> &[String] {
160 &self.inner.filters
161 }
162
163 #[cfg(test)]
164 pub(crate) fn time_filters(&self) -> &[String] {
165 self.time_filters
166 .as_deref()
167 .map(Vec::as_slice)
168 .unwrap_or(&[])
169 }
170
171 pub(crate) fn without_time_filters(&self) -> Self {
172 Self {
173 inner: Arc::clone(&self.inner),
174 time_filters: None,
175 series_row_selector: self.series_row_selector,
176 append_mode: self.append_mode,
177 filter_deleted: self.filter_deleted,
178 merge_mode: self.merge_mode,
179 sequence_range: self.sequence_range,
180 stage: self.stage,
181 partition_expr_version: self.partition_expr_version,
182 }
183 }
184
185 fn for_candidate_series(&self) -> Self {
186 Self {
187 inner: Arc::clone(&self.inner),
188 time_filters: self.time_filters.clone(),
189 series_row_selector: self.series_row_selector,
190 append_mode: self.append_mode,
191 filter_deleted: self.filter_deleted,
192 merge_mode: self.merge_mode,
193 sequence_range: self.sequence_range,
194 stage: RangeScanStage::CandidateSeries,
195 partition_expr_version: self.partition_expr_version,
196 }
197 }
198
199 fn for_series_data(&self, range: SeriesRange) -> Self {
200 Self {
201 inner: Arc::clone(&self.inner),
202 time_filters: self.time_filters.clone(),
203 series_row_selector: self.series_row_selector,
204 append_mode: self.append_mode,
205 filter_deleted: self.filter_deleted,
206 merge_mode: self.merge_mode,
207 sequence_range: self.sequence_range,
208 stage: RangeScanStage::SeriesData(range),
209 partition_expr_version: self.partition_expr_version,
210 }
211 }
212
213 pub(crate) fn estimated_size(&self) -> usize {
214 mem::size_of::<SharedScanRequestFingerprint>()
215 + self.inner.read_columns.estimated_size()
216 + self.inner.read_column_types.capacity() * mem::size_of::<Option<ConcreteDataType>>()
217 + self.inner.filters.capacity() * mem::size_of::<String>()
218 + self
219 .inner
220 .filters
221 .iter()
222 .map(|filter| filter.capacity())
223 .sum::<usize>()
224 + self.time_filters.as_ref().map_or(0, |filters| {
225 mem::size_of::<Vec<String>>()
226 + filters.capacity() * mem::size_of::<String>()
227 + filters
228 .iter()
229 .map(|filter| filter.capacity())
230 .sum::<usize>()
231 })
232 }
233}
234
235#[derive(Debug, Clone, PartialEq, Eq, Hash)]
237pub(crate) struct RangeScanCacheKey {
238 pub(crate) region_id: RegionId,
239 pub(crate) row_groups: Vec<(FileId, i64)>,
241 pub(crate) scan: ScanRequestFingerprint,
242}
243
244impl RangeScanCacheKey {
245 pub(crate) fn for_cross_range_merge(mut self) -> Self {
246 debug_assert_eq!(self.scan.stage, RangeScanStage::Data);
247 self.scan.stage = RangeScanStage::DataForCrossRangeMerge;
248 self
249 }
250
251 pub(crate) fn estimated_size(&self) -> usize {
252 mem::size_of::<Self>()
253 + self.row_groups.capacity() * mem::size_of::<(FileId, i64)>()
254 + self.scan.estimated_size()
255 }
256}
257
258#[derive(Debug)]
260pub(crate) struct CachedBatchSlice {
261 batch: RecordBatch,
262 slice_lengths: Vec<usize>,
263}
264
265impl CachedBatchSlice {
266 fn metadata_size(&self) -> usize {
267 self.slice_lengths.capacity() * mem::size_of::<usize>()
268 }
269}
270
271pub(crate) struct RangeScanCacheValue {
272 cached_batches: Vec<CachedBatchSlice>,
273 estimated_batches_size: usize,
275}
276
277impl RangeScanCacheValue {
278 pub(crate) fn new(
279 cached_batches: Vec<CachedBatchSlice>,
280 estimated_batches_size: usize,
281 ) -> Self {
282 Self {
283 cached_batches,
284 estimated_batches_size,
285 }
286 }
287
288 pub(crate) fn estimated_size(&self) -> usize {
289 mem::size_of::<Self>()
290 + self.cached_batches.capacity() * mem::size_of::<CachedBatchSlice>()
291 + self
292 .cached_batches
293 .iter()
294 .map(CachedBatchSlice::metadata_size)
295 .sum::<usize>()
296 + self.estimated_batches_size
297 }
298}
299
300pub(crate) struct PartitionRangeRowGroups {
302 pub(crate) row_groups: Vec<(FileId, i64)>,
304 pub(crate) only_file_sources: bool,
305}
306
307pub(crate) fn collect_partition_range_row_groups(
309 stream_ctx: &StreamContext,
310 part_range: &PartitionRange,
311) -> PartitionRangeRowGroups {
312 let range_meta = &stream_ctx.ranges[part_range.identifier];
313 let mut row_groups = Vec::new();
314 let mut only_file_sources = true;
315
316 for index in &range_meta.row_group_indices {
317 if stream_ctx.is_file_range_index(*index) {
318 let file_id = stream_ctx.input.file_from_index(*index).file_id().file_id();
319 row_groups.push((file_id, index.row_group_index));
320 } else {
321 only_file_sources = false;
322 }
323 }
324
325 row_groups.sort_unstable_by(|a, b| a.0.as_bytes().cmp(b.0.as_bytes()).then(a.1.cmp(&b.1)));
326
327 PartitionRangeRowGroups {
328 row_groups,
329 only_file_sources,
330 }
331}
332
333pub(crate) fn implied_time_range_from_exprs(
351 ts_col_name: &str,
352 ts_col_unit: TimeUnit,
353 exprs: &[&Expr],
354) -> Option<TimestampRange> {
355 if exprs.is_empty() {
356 return None;
357 }
358
359 let mut acc = TimestampRange::min_to_max();
360 for expr in exprs {
361 let r = implied_time_range_from_expr(ts_col_name, ts_col_unit, expr)?;
362 acc = acc.and(&r);
363 }
364 Some(acc)
365}
366
367fn implied_time_range_from_expr(
368 ts_col_name: &str,
369 ts_col_unit: TimeUnit,
370 expr: &Expr,
371) -> Option<TimestampRange> {
372 match expr {
373 Expr::BinaryExpr(BinaryExpr { left, op, right }) => match op {
374 Operator::And => {
375 let l = implied_time_range_from_expr(ts_col_name, ts_col_unit, left)?;
376 let r = implied_time_range_from_expr(ts_col_name, ts_col_unit, right)?;
377 Some(l.and(&r))
378 }
379 Operator::Eq | Operator::Lt | Operator::LtEq | Operator::Gt | Operator::GtEq => {
380 implied_from_cmp(ts_col_name, ts_col_unit, left, *op, right)
381 }
382 _ => None,
386 },
387 Expr::Between(Between {
388 expr,
389 negated,
390 low,
391 high,
392 }) => {
393 if *negated {
394 return None;
395 }
396 implied_from_between(ts_col_name, ts_col_unit, expr, low, high)
397 }
398 _ => None,
400 }
401}
402
403fn match_ts_column_literal<'a>(
404 ts_col_name: &str,
405 left: &'a Expr,
406 right: &'a Expr,
407) -> Option<(Timestamp, bool)> {
408 let (col, scalar, reverse) = match (left, right) {
409 (Expr::Column(c), Expr::Literal(s, _)) => (c, s, false),
410 (Expr::Literal(s, _), Expr::Column(c)) => (c, s, true),
411 _ => return None,
412 };
413 if col.name != ts_col_name {
414 return None;
415 }
416 if is_string_timestamp_literal(scalar) {
419 return None;
420 }
421 scalar_value_to_timestamp(scalar, None).map(|t| (t, reverse))
422}
423
424fn implied_from_cmp(
425 ts_col_name: &str,
426 ts_col_unit: TimeUnit,
427 left: &Expr,
428 op: Operator,
429 right: &Expr,
430) -> Option<TimestampRange> {
431 let (ts, reverse) = match_ts_column_literal(ts_col_name, left, right)?;
432 let op = if reverse {
434 match op {
435 Operator::Lt => Operator::Gt,
436 Operator::LtEq => Operator::GtEq,
437 Operator::Gt => Operator::Lt,
438 Operator::GtEq => Operator::LtEq,
439 Operator::Eq => Operator::Eq,
440 _ => return None,
441 }
442 } else {
443 op
444 };
445
446 match op {
447 Operator::GtEq => {
448 let b = ts.convert_to_ceil(ts_col_unit)?;
450 Some(TimestampRange::from_start(b))
451 }
452 Operator::Gt => {
453 let v = ts.convert_to(ts_col_unit)?.value().checked_add(1)?;
455 Some(TimestampRange::from_start(Timestamp::new(v, ts_col_unit)))
456 }
457 Operator::LtEq => {
458 let b = ts.convert_to(ts_col_unit)?;
460 Some(TimestampRange::until_end(b, true))
461 }
462 Operator::Lt => {
463 let b = ts.convert_to_ceil(ts_col_unit)?;
466 Some(TimestampRange::until_end(b, false))
467 }
468 Operator::Eq => {
469 let f = ts.convert_to(ts_col_unit)?;
471 let c = ts.convert_to_ceil(ts_col_unit)?;
472 if f.value() != c.value() {
473 return None;
474 }
475 Some(TimestampRange::single(f))
476 }
477 _ => None,
478 }
479}
480
481fn implied_from_between(
482 ts_col_name: &str,
483 ts_col_unit: TimeUnit,
484 expr: &Expr,
485 low: &Expr,
486 high: &Expr,
487) -> Option<TimestampRange> {
488 let Expr::Column(c) = expr else {
489 return None;
490 };
491 if c.name != ts_col_name {
492 return None;
493 }
494 let (low_s, high_s) = match (low, high) {
495 (Expr::Literal(l, _), Expr::Literal(h, _)) => (l, h),
496 _ => return None,
497 };
498 if is_string_timestamp_literal(low_s) || is_string_timestamp_literal(high_s) {
499 return None;
500 }
501 let low_ts = scalar_value_to_timestamp(low_s, None)?;
502 let high_ts = scalar_value_to_timestamp(high_s, None)?;
503 let lo = low_ts.convert_to_ceil(ts_col_unit)?;
505 let hi = high_ts.convert_to(ts_col_unit)?;
506 Some(TimestampRange::new_inclusive(Some(lo), Some(hi)))
507}
508
509pub(crate) fn build_range_cache_key(
511 stream_ctx: &StreamContext,
512 part_range: &PartitionRange,
513) -> Option<RangeScanCacheKey> {
514 build_range_cache_key_inner(stream_ctx, part_range, None)
515}
516
517pub(crate) fn build_candidate_range_cache_key(
519 stream_ctx: &StreamContext,
520 part_range: &PartitionRange,
521) -> Option<RangeScanCacheKey> {
522 build_range_cache_key_inner(
523 stream_ctx,
524 part_range,
525 Some(RangeScanStage::CandidateSeries),
526 )
527}
528
529pub(crate) fn build_series_range_cache_key(
531 stream_ctx: &StreamContext,
532 part_range: &PartitionRange,
533 range: SeriesRange,
534) -> Option<RangeScanCacheKey> {
535 build_range_cache_key_inner(
536 stream_ctx,
537 part_range,
538 Some(RangeScanStage::SeriesData(range)),
539 )
540}
541
542fn build_range_cache_key_inner(
543 stream_ctx: &StreamContext,
544 part_range: &PartitionRange,
545 stage: Option<RangeScanStage>,
546) -> Option<RangeScanCacheKey> {
547 if !stream_ctx.input.cache_strategy.has_range_result_cache() {
548 return None;
549 }
550
551 let fingerprint = stream_ctx.input.scan_fingerprint()?;
552
553 let has_dyn_filters = stream_ctx
555 .input
556 .predicate_group()
557 .predicate_without_region()
558 .is_some_and(|p| !p.dyn_filters().is_empty());
559 if has_dyn_filters {
560 return None;
561 }
562
563 let rg = collect_partition_range_row_groups(stream_ctx, part_range);
564 if !rg.only_file_sources || rg.row_groups.is_empty() {
565 return None;
566 }
567
568 let range_meta = &stream_ctx.ranges[part_range.identifier];
575 let (file_min, file_max) = range_meta.time_range;
576 let covers = match stream_ctx.input.implied_time_range() {
577 Some(implied) if !implied.is_empty() => {
580 implied.contains(&file_min) && implied.contains(&file_max)
586 }
587 _ => false,
588 };
589 let scan = if covers {
590 fingerprint.without_time_filters()
591 } else {
592 fingerprint.clone()
593 };
594 let scan = match stage {
595 Some(RangeScanStage::CandidateSeries) => scan.for_candidate_series(),
596 Some(RangeScanStage::SeriesData(assignment)) => scan.for_series_data(assignment),
597 Some(RangeScanStage::DataForCrossRangeMerge) => {
598 let mut scan = scan;
599 scan.stage = RangeScanStage::DataForCrossRangeMerge;
600 scan
601 }
602 Some(RangeScanStage::Data) | None => scan,
603 };
604
605 Some(RangeScanCacheKey {
606 region_id: stream_ctx.input.region_metadata().region_id,
607 row_groups: rg.row_groups,
608 scan,
609 })
610}
611
612pub(crate) fn cached_flat_range_stream(
614 delivery: Arc<RangeCacheDelivery>,
615) -> BoxedRecordBatchStream {
616 Box::pin(try_stream! {
617 for cached_batch in &delivery.value().cached_batches {
621 let mut offset = 0;
622 for &len in &cached_batch.slice_lengths {
623 yield cached_batch.batch.slice(offset, len);
624 offset += len;
625 }
626 }
627 })
628}
629
630pub(crate) struct CachedRangeReader<'a> {
634 pub(crate) stream_ctx: &'a StreamContext,
635 pub(crate) part_range: &'a PartitionRange,
636 pub(crate) key: Option<RangeScanCacheKey>,
637 pub(crate) metrics: &'a PartitionMetrics,
638}
639
640impl CachedRangeReader<'_> {
641 pub(crate) async fn build<F, Fut>(self, build: F) -> Result<(BoxedRecordBatchStream, usize)>
642 where
643 F: FnOnce() -> Fut + Clone + Send + 'static,
644 Fut: Future<Output = Result<(BoxedRecordBatchStream, usize)>> + Send + 'static,
645 {
646 let Self {
647 stream_ctx,
648 part_range,
649 key,
650 metrics,
651 } = self;
652 let strategy = &stream_ctx.input.cache_strategy;
653 let (Some(key), Some(worker)) = (key, strategy.range_cache_worker()) else {
654 return build().await;
655 };
656 let files = stream_ctx.ranges[part_range.identifier]
657 .row_group_indices
658 .iter()
659 .map(|index| stream_ctx.input.file_from_index(*index).clone())
660 .collect();
661 let loader = build.clone();
662 let load_worker = worker.clone();
663 let load_strategy = strategy.clone();
664 let load_metrics = metrics.clone();
665 let key_size = key.estimated_size();
666 match worker.get_or_load(key, files, async move {
667 let result = async {
668 let (stream, _) = loader().await?;
669 collect_range_result(stream, &load_worker, &load_strategy).await
670 }
671 .await;
672 match result {
673 Ok(Some(value)) => {
674 load_metrics.inc_range_cache_size(key_size + value.value().estimated_size());
675 Some(value)
676 }
677 Ok(None) => None,
678 Err(error) => {
679 warn!(error; "Failed to load range cache; falling back to an uncached scan");
680 None
681 }
682 }
683 }) {
684 RangeCacheLookup::Hit(value) => {
685 metrics.inc_range_cache_hit();
686 Ok((cached_flat_range_stream(value), DEFAULT_READ_BATCH_SIZE))
687 }
688 RangeCacheLookup::Loading(receiver) => {
689 metrics.inc_range_cache_miss();
690 Ok((
691 shared_range_stream(receiver, build),
692 DEFAULT_READ_BATCH_SIZE,
693 ))
694 }
695 RangeCacheLookup::Bypass => {
696 metrics.inc_range_cache_miss();
697 build().await
698 }
699 }
700 }
701}
702
703fn shared_range_stream<F, Fut>(
705 mut receiver: tokio::sync::watch::Receiver<Option<Arc<RangeCacheDelivery>>>,
706 build: F,
707) -> BoxedRecordBatchStream
708where
709 F: FnOnce() -> Fut + Send + 'static,
710 Fut: Future<Output = Result<(BoxedRecordBatchStream, usize)>> + Send + 'static,
711{
712 Box::pin(try_stream! {
713 let cached = receiver.wait_for(|cached| cached.is_some()).await
714 .ok().and_then(|cached| cached.clone());
715 drop(receiver);
716 let mut stream = if let Some(cached) = cached {
717 cached_flat_range_stream(cached)
718 } else {
719 build().await?.0
722 };
723 while let Some(batch) = stream.try_next().await? {
724 yield batch;
725 }
726 })
727}
728
729async fn collect_range_result(
734 mut stream: BoxedRecordBatchStream,
735 worker: &RangeCacheWorker,
736 strategy: &CacheStrategy,
737) -> Result<Option<Arc<RangeCacheDelivery>>> {
738 let mut permit: Option<tokio::sync::OwnedSemaphorePermit> = None;
739 let mut buffer = CacheBatchBuffer::new(strategy);
740 while let Some(batch) = stream.try_next().await? {
741 let Some(next_permit) = worker
742 .load_memory
743 .try_acquire(batch.get_array_memory_size())
744 else {
745 return Ok(None);
746 };
747 if let Some(permit) = &mut permit {
748 permit.merge(next_permit);
749 } else {
750 permit = Some(next_permit);
751 }
752 buffer.push(batch)?;
753 }
754 let Some(value) = buffer.finish().await.transpose()? else {
755 return Ok(None);
756 };
757 let Some(permit) = worker
758 .load_memory
759 .try_resize(permit, value.estimated_size())
760 else {
761 return Ok(None);
762 };
763 Ok(Some(RangeCacheDelivery::new(value, permit)))
764}
765
766enum CacheConcatCommand {
767 Compact(Vec<RecordBatch>),
768 Finish {
769 pending: Vec<RecordBatch>,
770 result_tx: oneshot::Sender<Result<Arc<RangeScanCacheValue>>>,
771 },
772}
773
774#[derive(Default)]
775struct CacheConcatState {
776 cached_batches: Vec<CachedBatchSlice>,
777 estimated_size: usize,
778}
779
780impl CacheConcatState {
781 async fn compact(
782 &mut self,
783 batches: Vec<RecordBatch>,
784 limiter: &crate::cache::RangeResultMemoryLimiter,
785 ) -> Result<()> {
786 if batches.is_empty() {
787 return Ok(());
788 }
789
790 let input_size = batches
791 .iter()
792 .map(RecordBatch::get_array_memory_size)
793 .sum::<usize>();
794 let _permit = limiter.acquire(input_size).await?;
795
796 let compacted = compact_record_batches(batches)?;
797 self.estimated_size += compacted.batch.get_array_memory_size();
798 self.cached_batches.push(compacted);
799 Ok(())
800 }
801
802 fn finish(self) -> RangeScanCacheValue {
803 RangeScanCacheValue::new(self.cached_batches, self.estimated_size)
804 }
805}
806
807fn compact_record_batches(batches: Vec<RecordBatch>) -> Result<CachedBatchSlice> {
808 debug_assert!(!batches.is_empty());
809
810 let slice_lengths = batches.iter().map(RecordBatch::num_rows).collect();
811 build_cached_batch_slice(batches, slice_lengths)
812}
813
814fn build_cached_batch_slice(
815 batches: Vec<RecordBatch>,
816 slice_lengths: Vec<usize>,
817) -> Result<CachedBatchSlice> {
818 let batch = if batches.len() == 1 {
819 batches.into_iter().next().unwrap()
820 } else {
821 let schema = batches[0].schema();
822 concat_batches(&schema, &batches).context(ComputeArrowSnafu)?
823 };
824
825 Ok(CachedBatchSlice {
826 batch,
827 slice_lengths,
828 })
829}
830
831async fn run_cache_concat_task(
832 mut rx: mpsc::UnboundedReceiver<CacheConcatCommand>,
833 limiter: Arc<crate::cache::RangeResultMemoryLimiter>,
834 skip_threshold_bytes: usize,
835) {
836 let mut state = CacheConcatState::default();
837
838 while let Some(cmd) = rx.recv().await {
839 match cmd {
840 CacheConcatCommand::Compact(batches) => {
841 if let Err(err) = state.compact(batches, &limiter).await {
842 warn!(err; "Failed to compact range cache batches");
843 return;
844 }
845 if state.estimated_size > skip_threshold_bytes {
848 return;
849 }
850 }
851 CacheConcatCommand::Finish { pending, result_tx } => {
852 let compact_result = state
853 .compact(pending, &limiter)
854 .await
855 .map(|()| state.finish());
856 let result = match compact_result {
857 Ok(v) => {
858 let value = Arc::new(v);
859 Ok(value)
860 }
861 Err(e) => {
862 warn!(e; "Failed to finalize range cache batches");
863
864 Err(e)
865 }
866 };
867
868 let _ = result_tx.send(result);
869
870 break;
871 }
872 }
873 }
874}
875
876struct CacheBatchBuffer {
877 buffered_batches: Vec<RecordBatch>,
878 buffered_rows: usize,
879 buffered_size: usize,
880 sender: Option<mpsc::UnboundedSender<CacheConcatCommand>>,
881 concat_task: Option<common_runtime::JoinHandle<()>>,
883}
884
885impl CacheBatchBuffer {
886 fn new(cache_strategy: &CacheStrategy) -> Self {
887 let (sender, concat_task) = cache_strategy
888 .range_result_memory_limiter()
889 .map(|limiter| {
890 let skip_threshold_bytes = cache_strategy.range_result_cache_size().unwrap_or(0);
891 let (tx, rx) = mpsc::unbounded_channel();
892 let task = common_runtime::spawn_query(run_cache_concat_task(
893 rx,
894 limiter.clone(),
895 skip_threshold_bytes,
896 ));
897 (tx, task)
898 })
899 .unzip();
900
901 Self {
902 buffered_batches: Vec::new(),
903 buffered_rows: 0,
904 buffered_size: 0,
905 sender,
906 concat_task,
907 }
908 }
909
910 fn push(&mut self, batch: RecordBatch) -> Result<()> {
911 if self.sender.is_none() {
912 return Ok(());
913 }
914
915 self.buffered_rows += batch.num_rows();
916 self.buffered_size += batch.get_array_memory_size();
917 self.buffered_batches.push(batch);
918
919 if self.buffered_batches.len() > 1
920 && (self.buffered_rows > DEFAULT_READ_BATCH_SIZE
921 || self.buffered_size > RANGE_CACHE_COMPACT_THRESHOLD_BYTES)
922 {
923 self.notify_compact();
924 }
925
926 Ok(())
927 }
928
929 fn notify_compact(&mut self) {
930 if self.buffered_batches.is_empty() || self.sender.is_none() {
931 return;
932 }
933
934 let batches = mem::take(&mut self.buffered_batches);
935 self.buffered_rows = 0;
936 self.buffered_size = 0;
937
938 let Some(sender) = &self.sender else {
939 return;
940 };
941 if sender.send(CacheConcatCommand::Compact(batches)).is_err() {
942 self.sender = None;
943 }
944 }
945
946 async fn finish(mut self) -> Option<Result<Arc<RangeScanCacheValue>>> {
947 let sender = self.sender.take()?;
948 let (result_tx, result_rx) = oneshot::channel();
949 sender
950 .send(CacheConcatCommand::Finish {
951 pending: mem::take(&mut self.buffered_batches),
952 result_tx,
953 })
954 .ok()?;
955 let result = result_rx.await.ok();
958 self.concat_task = None;
959 result
960 }
961}
962
963impl Drop for CacheBatchBuffer {
964 fn drop(&mut self) {
965 if let Some(task) = &self.concat_task {
967 task.abort();
968 }
969 }
970}
971
972#[cfg(feature = "test")]
974pub(crate) fn cache_flat_range_stream(
975 mut stream: BoxedRecordBatchStream,
976 cache_strategy: CacheStrategy,
977 key: RangeScanCacheKey,
978 part_metrics: PartitionMetrics,
979) -> BoxedRecordBatchStream {
980 Box::pin(try_stream! {
981 let mut buffer = CacheBatchBuffer::new(&cache_strategy);
982 while let Some(batch) = stream.try_next().await? {
983 buffer.push(batch.clone())?;
984 yield batch;
985 }
986
987 if let Some(Ok(value)) = buffer.finish().await {
988 part_metrics.inc_range_cache_size(key.estimated_size() + value.estimated_size());
989 cache_strategy.put_range_result(key, value);
990 }
991 })
992}
993
994#[cfg(feature = "test")]
999pub fn bench_cache_flat_range_stream(
1000 stream: BoxedRecordBatchStream,
1001 cache_size_bytes: u64,
1002 region_id: RegionId,
1003) -> BoxedRecordBatchStream {
1004 use std::time::Instant;
1005
1006 use datafusion::physical_plan::metrics::ExecutionPlanMetricsSet;
1007
1008 use crate::region::options::MergeMode;
1009
1010 let cache_manager = Arc::new(
1011 crate::cache::CacheManager::builder()
1012 .range_result_cache_size(cache_size_bytes)
1013 .build(),
1014 );
1015 let cache_strategy = CacheStrategy::EnableAll(cache_manager);
1016
1017 let fingerprint = ScanRequestFingerprintBuilder {
1018 read_columns: ReadColumns::new(std::iter::empty()),
1019 read_column_types: vec![],
1020 filters: vec![],
1021 time_filters: vec![],
1022 series_row_selector: None,
1023 append_mode: false,
1024 filter_deleted: false,
1025 merge_mode: MergeMode::LastRow,
1026 sequence_range: None,
1027 partition_expr_version: 0,
1028 }
1029 .build();
1030
1031 let key = RangeScanCacheKey {
1032 region_id,
1033 row_groups: vec![],
1034 scan: fingerprint,
1035 };
1036
1037 let metrics_set = ExecutionPlanMetricsSet::new();
1038 let part_metrics =
1039 PartitionMetrics::new(region_id, 0, "bench", Instant::now(), false, &metrics_set);
1040
1041 cache_flat_range_stream(stream, cache_strategy, key, part_metrics)
1042}
1043
1044#[cfg(test)]
1045mod tests {
1046 use std::sync::Arc;
1047 use std::time::Instant;
1048
1049 use common_time::Timestamp;
1050 use common_time::range::TimestampRange;
1051 use common_time::timestamp::TimeUnit;
1052 use datafusion::physical_plan::metrics::ExecutionPlanMetricsSet;
1053 use datafusion_common::ScalarValue;
1054 use datafusion_expr::{Expr, col, lit};
1055 use smallvec::smallvec;
1056 use store_api::storage::{FileId, RegionId};
1057 use tokio::sync::watch;
1058
1059 use super::*;
1060 use crate::cache::CacheManager;
1061 use crate::cache::test_util::range_cache_key;
1062 use crate::read::flat_projection::FlatProjectionMapper;
1063 use crate::read::range::{RangeMeta, RowGroupIndex, SourceIndex};
1064 use crate::read::scan_region::{PredicateGroup, ScanInput};
1065 use crate::sst::file::FileTimeRange;
1066 use crate::test_util::memtable_util::metadata_with_primary_key;
1067 use crate::test_util::scheduler_util::SchedulerEnv;
1068 use crate::test_util::sst_util::sst_file_handle_with_file_id;
1069
1070 fn test_cache_strategy() -> CacheStrategy {
1071 CacheStrategy::EnableAll(Arc::new(
1072 CacheManager::builder()
1073 .range_result_cache_size(1024 * 1024)
1074 .build(),
1075 ))
1076 }
1077
1078 fn test_scan_fingerprint(
1079 filters: Vec<String>,
1080 time_filters: Vec<String>,
1081 series_row_selector: Option<TimeSeriesRowSelector>,
1082 filter_deleted: bool,
1083 partition_expr_version: u64,
1084 ) -> ScanRequestFingerprint {
1085 let read_columns = ReadColumns::new([1, 2]);
1086 ScanRequestFingerprintBuilder {
1087 read_columns,
1088 read_column_types: vec![None, None],
1089 filters,
1090 time_filters,
1091 series_row_selector,
1092 append_mode: false,
1093 filter_deleted,
1094 merge_mode: MergeMode::LastRow,
1095 sequence_range: None,
1096 partition_expr_version,
1097 }
1098 .build()
1099 }
1100
1101 fn test_cache_context(strategy: &CacheStrategy) -> (RangeScanCacheKey, PartitionMetrics) {
1102 let region_id = RegionId::new(1, 1);
1103 let key = RangeScanCacheKey {
1104 region_id,
1105 row_groups: vec![],
1106 scan: test_scan_fingerprint(vec![], vec![], None, false, 0),
1107 };
1108
1109 let metrics_set = ExecutionPlanMetricsSet::new();
1110 let part_metrics =
1111 PartitionMetrics::new(region_id, 0, "test", Instant::now(), false, &metrics_set);
1112
1113 assert!(strategy.get_range_result(&key).is_none());
1114 (key, part_metrics)
1115 }
1116
1117 async fn new_stream_context(
1118 filters: Vec<Expr>,
1119 query_time_range: Option<TimestampRange>,
1120 partition_time_range: FileTimeRange,
1121 ) -> (StreamContext, PartitionRange) {
1122 let env = SchedulerEnv::new().await;
1123 let metadata = Arc::new(metadata_with_primary_key(vec![0, 1], false));
1124 let mapper = FlatProjectionMapper::new(&metadata, [0, 2, 3]).unwrap();
1125 let predicate = PredicateGroup::new(metadata.as_ref(), &filters).unwrap();
1126 let file_id = FileId::random();
1127 let file = sst_file_handle_with_file_id(
1128 file_id,
1129 partition_time_range.0.value(),
1130 partition_time_range.1.value(),
1131 );
1132 let input = ScanInput::builder(env.access_layer.clone(), mapper)
1133 .with_predicate(predicate)
1134 .with_time_range(query_time_range)
1135 .with_files(vec![file])
1136 .with_cache(test_cache_strategy())
1137 .build();
1138 let range_meta = RangeMeta {
1139 time_range: partition_time_range,
1140 indices: smallvec![SourceIndex {
1141 index: 0,
1142 num_row_groups: 1,
1143 }],
1144 row_group_indices: smallvec![RowGroupIndex {
1145 index: 0,
1146 row_group_index: 0,
1147 }],
1148 num_rows: 10,
1149 };
1150 let partition_range = range_meta.new_partition_range(0);
1151 let stream_ctx = StreamContext {
1152 input,
1153 ranges: vec![range_meta],
1154 query_start: Instant::now(),
1155 };
1156
1157 (stream_ctx, partition_range)
1158 }
1159
1160 #[rstest::rstest]
1161 #[case(1024 * 1024, true)]
1162 #[case(1, false)]
1163 #[tokio::test]
1164 async fn shared_range_read_preserves_batches_or_falls_back_on_budget_exhaustion(
1165 #[case] capacity: u64,
1166 #[case] cached: bool,
1167 ) {
1168 use std::sync::atomic::{AtomicUsize, Ordering};
1169
1170 let (mut ctx, range) =
1171 new_stream_context(vec![col("k0").eq(lit("a"))], None, FileTimeRange::default()).await;
1172 ctx.input.cache_strategy = CacheStrategy::EnableAll(Arc::new(
1173 CacheManager::builder()
1174 .range_result_cache_size(capacity)
1175 .build(),
1176 ));
1177 let key = build_range_cache_key(&ctx, &range).unwrap();
1178 let (_, metrics) = test_cache_context(&ctx.input.cache_strategy);
1179 let calls = Arc::new(AtomicUsize::new(0));
1180 let expected = vec![make_batch(&[1, 2]), make_batch(&[3, 4, 5])];
1181 let batches = expected.clone();
1182 let count = calls.clone();
1183 let (stream, _) = CachedRangeReader {
1184 stream_ctx: &ctx,
1185 part_range: &range,
1186 key: Some(key.clone()),
1187 metrics: &metrics,
1188 }
1189 .build(move || async move {
1190 count.fetch_add(1, Ordering::Relaxed);
1191 Ok((
1192 Box::pin(futures::stream::iter(batches.into_iter().map(Ok)))
1193 as BoxedRecordBatchStream,
1194 DEFAULT_READ_BATCH_SIZE,
1195 ))
1196 })
1197 .await
1198 .unwrap();
1199 let actual: Vec<_> = stream.try_collect().await.unwrap();
1200 assert_eq!(actual, expected);
1201 assert_eq!(calls.load(Ordering::Relaxed), if cached { 1 } else { 2 });
1202 assert_eq!(
1203 ctx.input.cache_strategy.get_range_result(&key).is_some(),
1204 cached
1205 );
1206 assert_eq!(
1207 ctx.input
1208 .cache_strategy
1209 .range_cache_worker()
1210 .unwrap()
1211 .load_memory
1212 .available_permits(),
1213 (capacity as usize).div_ceil(1024)
1214 );
1215 }
1216
1217 fn start_shared_load(
1218 strategy: &CacheStrategy,
1219 key: RangeScanCacheKey,
1220 batches: Vec<RecordBatch>,
1221 ) -> watch::Receiver<Option<Arc<RangeCacheDelivery>>> {
1222 let worker = strategy.range_cache_worker().unwrap();
1223 let loader_worker = worker.clone();
1224 let loader_strategy = strategy.clone();
1225 let lookup = worker.get_or_load(key, vec![], async move {
1226 collect_range_result(
1227 Box::pin(futures::stream::iter(batches.into_iter().map(Ok))),
1228 &loader_worker,
1229 &loader_strategy,
1230 )
1231 .await
1232 .unwrap()
1233 });
1234 let RangeCacheLookup::Loading(receiver) = lookup else {
1235 panic!("expected a shared load");
1236 };
1237 receiver
1238 }
1239
1240 async fn wait_for_shared_load(
1241 receiver: &mut watch::Receiver<Option<Arc<RangeCacheDelivery>>>,
1242 ) -> Option<Arc<RangeCacheDelivery>> {
1243 tokio::time::timeout(std::time::Duration::from_secs(5), async {
1244 receiver
1245 .wait_for(|value| value.is_some())
1246 .await
1247 .ok()
1248 .and_then(|value| value.clone())
1249 })
1250 .await
1251 .expect("a shared load must finish or fall back without waiting for consumers")
1252 }
1253
1254 #[tokio::test]
1255 async fn completed_load_keeps_memory_until_all_subscribers_release() {
1256 let strategy = test_cache_strategy();
1257 let worker = strategy.range_cache_worker().unwrap();
1258 let capacity = worker.load_memory.available_permits();
1259 let (key, _) = test_cache_context(&strategy);
1260 let mut first = start_shared_load(&strategy, key.clone(), vec![make_batch(&[1, 2, 3])]);
1261 let second = first.clone();
1262 drop(wait_for_shared_load(&mut first).await.unwrap());
1263
1264 assert!(worker.load_memory.available_permits() < capacity);
1267 drop(first);
1268 assert!(worker.load_memory.available_permits() < capacity);
1269 drop(second);
1270 tokio::time::timeout(std::time::Duration::from_secs(5), async {
1271 while worker.load_memory.available_permits() != capacity {
1272 tokio::task::yield_now().await;
1273 }
1274 })
1275 .await
1276 .expect("the last subscriber must release the delivery reservation");
1277 assert!(
1278 worker.get(&key).is_some(),
1279 "the cache still owns the result"
1280 );
1281 }
1282
1283 #[rstest::rstest]
1284 #[tokio::test]
1285 async fn successive_loads_with_slow_consumers_exhaust_delivery_budget(
1286 #[values(false, true)] invalidate: bool,
1287 ) {
1288 let strategy = CacheStrategy::EnableAll(Arc::new(
1289 CacheManager::builder()
1290 .range_result_cache_size(8192)
1291 .build(),
1292 ));
1293 let worker = strategy.range_cache_worker().unwrap();
1294 let capacity = worker.load_memory.available_permits();
1295 let mut held_streams = Vec::new();
1296 let mut fallbacks = 0;
1297
1298 for _ in 0..40 {
1301 let file = FileId::random();
1302 let key = range_cache_key(&[file]);
1303 let expected = vec![make_large_binary_batch(1, 2048)];
1304 let mut receiver = start_shared_load(&strategy, key, expected.clone());
1305 let shared = wait_for_shared_load(&mut receiver).await.is_some();
1306 if invalidate {
1307 worker.invalidate_file(file);
1308 }
1309 let fallback_batches = expected.clone();
1310 let stream = shared_range_stream(receiver, move || async move {
1311 Ok((
1312 Box::pin(futures::stream::iter(fallback_batches.into_iter().map(Ok)))
1313 as BoxedRecordBatchStream,
1314 DEFAULT_READ_BATCH_SIZE,
1315 ))
1316 });
1317 if shared {
1318 held_streams.push(stream);
1319 } else {
1320 fallbacks += 1;
1321 let actual: Vec<_> =
1322 tokio::time::timeout(std::time::Duration::from_secs(5), stream.try_collect())
1323 .await
1324 .expect("fallback must not wait for an unpolled range")
1325 .unwrap();
1326 assert_eq!(actual, expected);
1327 }
1328 }
1329
1330 assert!(!held_streams.is_empty());
1331 assert!(held_streams.len() < 32);
1332 assert!(
1333 fallbacks > 0,
1334 "completed results must not escape the budget"
1335 );
1336 assert!(worker.load_memory.available_permits() < capacity);
1337
1338 for stream in &mut held_streams {
1341 assert!(stream.try_next().await.unwrap().is_some());
1342 }
1343 assert!(worker.load_memory.available_permits() < capacity);
1344 drop(held_streams);
1345 tokio::time::timeout(std::time::Duration::from_secs(5), async {
1346 while worker.load_memory.available_permits() != capacity {
1347 tokio::task::yield_now().await;
1348 }
1349 })
1350 .await
1351 .expect("dropping slow readers must unblock new shared loads");
1352
1353 let mut retry = start_shared_load(
1354 &strategy,
1355 range_cache_key(&[FileId::random()]),
1356 vec![make_large_binary_batch(1, 2048)],
1357 );
1358 assert!(wait_for_shared_load(&mut retry).await.is_some());
1359 }
1360
1361 #[tokio::test]
1362 async fn cancelling_a_partial_load_releases_its_reservation() {
1363 let strategy = test_cache_strategy();
1364 let worker = strategy.range_cache_worker().unwrap().clone();
1365 let capacity = worker.load_memory.available_permits();
1366 let (key, _) = test_cache_context(&strategy);
1367 let loader_worker = worker.clone();
1368 let lookup = worker.get_or_load(key.clone(), vec![], async move {
1369 let stream = Box::pin(try_stream! {
1370 yield make_batch(&[1, 2, 3]);
1371 std::future::pending::<()>().await;
1372 });
1373 collect_range_result(stream, &loader_worker, &strategy)
1374 .await
1375 .unwrap()
1376 });
1377 let RangeCacheLookup::Loading(receiver) = lookup else {
1378 panic!("expected a shared load");
1379 };
1380 tokio::time::timeout(std::time::Duration::from_secs(5), async {
1381 while worker.load_memory.available_permits() == capacity {
1382 tokio::task::yield_now().await;
1383 }
1384 })
1385 .await
1386 .expect("the partial result must acquire its input reservation");
1387 drop(receiver);
1388 tokio::time::timeout(std::time::Duration::from_secs(5), async {
1389 while worker.load_memory.available_permits() != capacity {
1390 tokio::task::yield_now().await;
1391 }
1392 })
1393 .await
1394 .expect("cancellation must release the partial result reservation");
1395 assert!(worker.get(&key).is_none());
1396 }
1397
1398 #[tokio::test]
1399 async fn follower_can_finish_without_polling_the_first_range_stream() {
1400 use std::sync::atomic::{AtomicUsize, Ordering};
1401
1402 let (ctx, range) =
1403 new_stream_context(vec![col("k0").eq(lit("a"))], None, FileTimeRange::default()).await;
1404 let key = build_range_cache_key(&ctx, &range).unwrap();
1405 let (_, metrics) = test_cache_context(&ctx.input.cache_strategy);
1406 let gate = Arc::new(tokio::sync::Semaphore::new(0));
1407 let calls = Arc::new(AtomicUsize::new(0));
1408 let count = calls.clone();
1409 let loader_gate = gate.clone();
1410 let build = move || async move {
1411 count.fetch_add(1, Ordering::Relaxed);
1412 let _permit = loader_gate.acquire().await.unwrap();
1413 Ok((
1414 Box::pin(futures::stream::iter([Ok(make_batch(&[1, 2, 3]))]))
1415 as BoxedRecordBatchStream,
1416 DEFAULT_READ_BATCH_SIZE,
1417 ))
1418 };
1419 let (first, _) = CachedRangeReader {
1420 stream_ctx: &ctx,
1421 part_range: &range,
1422 key: Some(key.clone()),
1423 metrics: &metrics,
1424 }
1425 .build(build.clone())
1426 .await
1427 .unwrap();
1428 let (second, _) = CachedRangeReader {
1429 stream_ctx: &ctx,
1430 part_range: &range,
1431 key: Some(key),
1432 metrics: &metrics,
1433 }
1434 .build(build)
1435 .await
1436 .unwrap();
1437 gate.add_permits(1);
1438 let second_batches: Vec<_> =
1439 tokio::time::timeout(std::time::Duration::from_secs(5), second.try_collect())
1440 .await
1441 .expect("a follower must not depend on polling the first query")
1442 .unwrap();
1443 let first_batches: Vec<_> = first.try_collect().await.unwrap();
1444 assert_eq!(first_batches, second_batches);
1445 assert_eq!(second_batches, vec![make_batch(&[1, 2, 3])]);
1446 assert_eq!(calls.load(Ordering::Relaxed), 1);
1447 }
1448
1449 fn ts_lit(val: i64) -> Expr {
1451 lit(ScalarValue::TimestampMillisecond(Some(val), None))
1452 }
1453
1454 fn normalized_exprs(exprs: impl IntoIterator<Item = Expr>) -> Vec<String> {
1455 let mut exprs = exprs
1456 .into_iter()
1457 .map(|expr| expr.to_string())
1458 .collect::<Vec<_>>();
1459 exprs.sort_unstable();
1460 exprs
1461 }
1462
1463 async fn assert_range_cache_filters(
1464 filters: Vec<Expr>,
1465 query_time_range: Option<TimestampRange>,
1466 partition_time_range: FileTimeRange,
1467 expected_filters: Vec<Expr>,
1468 expected_time_filters: Vec<Expr>,
1469 ) {
1470 let (stream_ctx, part_range) =
1471 new_stream_context(filters, query_time_range, partition_time_range).await;
1472
1473 let key = build_range_cache_key(&stream_ctx, &part_range).unwrap();
1474
1475 assert_eq!(
1476 key.scan.filters(),
1477 normalized_exprs(expected_filters).as_slice()
1478 );
1479 assert_eq!(
1480 key.scan.time_filters(),
1481 normalized_exprs(expected_time_filters).as_slice()
1482 );
1483 }
1484
1485 #[tokio::test]
1486 async fn range_cache_time_filter_key_cases() {
1487 let partition = (
1488 Timestamp::new_millisecond(1000),
1489 Timestamp::new_millisecond(2000),
1490 );
1491
1492 struct Case {
1493 filters: Vec<Expr>,
1494 query_time_range: Option<TimestampRange>,
1495 expected_filters: Vec<Expr>,
1496 expected_time_filters: Vec<Expr>,
1497 }
1498
1499 for case in [
1503 Case {
1504 filters: vec![
1505 col("ts").gt_eq(ts_lit(1000)),
1506 col("ts").lt(ts_lit(2001)),
1507 col("ts").is_not_null(),
1508 col("k0").eq(lit("foo")),
1509 ],
1510 query_time_range: TimestampRange::with_unit(1000, 2002, TimeUnit::Millisecond),
1511 expected_filters: vec![col("k0").eq(lit("foo")), col("ts").is_not_null()],
1512 expected_time_filters: vec![],
1513 },
1514 Case {
1515 filters: vec![
1516 col("ts").gt_eq(ts_lit(500)),
1517 col("ts").lt(ts_lit(3000)),
1518 col("k0").eq(lit("foo")),
1519 ],
1520 query_time_range: TimestampRange::with_unit(500, 3000, TimeUnit::Millisecond),
1521 expected_filters: vec![col("k0").eq(lit("foo"))],
1522 expected_time_filters: vec![],
1523 },
1524 Case {
1525 filters: vec![
1526 col("ts").gt_eq(ts_lit(1000)),
1527 col("ts").lt_eq(ts_lit(2000)),
1528 col("k0").eq(lit("foo")),
1529 ],
1530 query_time_range: TimestampRange::with_unit(1000, 2001, TimeUnit::Millisecond),
1531 expected_filters: vec![col("k0").eq(lit("foo"))],
1532 expected_time_filters: vec![],
1533 },
1534 Case {
1535 filters: vec![
1536 col("ts").between(ts_lit(1000), ts_lit(2000)),
1537 col("k0").eq(lit("foo")),
1538 ],
1539 query_time_range: TimestampRange::with_unit(1000, 2001, TimeUnit::Millisecond),
1540 expected_filters: vec![col("k0").eq(lit("foo"))],
1541 expected_time_filters: vec![],
1542 },
1543 Case {
1544 filters: vec![col("ts").gt_eq(ts_lit(1200)), col("k0").eq(lit("foo"))],
1545 query_time_range: TimestampRange::with_unit(1200, 2001, TimeUnit::Millisecond),
1546 expected_filters: vec![col("k0").eq(lit("foo"))],
1547 expected_time_filters: vec![col("ts").gt_eq(ts_lit(1200))],
1548 },
1549 Case {
1550 filters: vec![
1551 col("ts").gt_eq(ts_lit(1500)),
1552 col("ts").is_not_null(),
1553 col("k0").eq(lit("foo")),
1554 ],
1555 query_time_range: None,
1556 expected_filters: vec![col("k0").eq(lit("foo")), col("ts").is_not_null()],
1557 expected_time_filters: vec![col("ts").gt_eq(ts_lit(1500))],
1558 },
1559 ] {
1560 assert_range_cache_filters(
1561 case.filters,
1562 case.query_time_range,
1563 partition,
1564 case.expected_filters,
1565 case.expected_time_filters,
1566 )
1567 .await;
1568 }
1569 }
1570
1571 #[tokio::test]
1572 async fn two_distinct_queries_share_cache_key_when_both_cover() {
1573 let partition_range = (
1574 Timestamp::new_millisecond(1000),
1575 Timestamp::new_millisecond(2000),
1576 );
1577
1578 let (ctx_a, part_a) = new_stream_context(
1579 vec![
1580 col("ts").gt_eq(ts_lit(500)),
1581 col("ts").lt(ts_lit(3000)),
1582 col("k0").eq(lit("foo")),
1583 ],
1584 TimestampRange::with_unit(500, 3000, TimeUnit::Millisecond),
1585 partition_range,
1586 )
1587 .await;
1588 let (ctx_b, part_b) = new_stream_context(
1589 vec![
1590 col("ts").gt_eq(ts_lit(100)),
1591 col("ts").lt(ts_lit(5000)),
1592 col("k0").eq(lit("foo")),
1593 ],
1594 TimestampRange::with_unit(100, 5000, TimeUnit::Millisecond),
1595 partition_range,
1596 )
1597 .await;
1598
1599 let key_a = build_range_cache_key(&ctx_a, &part_a).unwrap();
1600 let key_b = build_range_cache_key(&ctx_b, &part_b).unwrap();
1601 assert_eq!(key_a.scan, key_b.scan);
1602 assert!(key_a.scan.time_filters().is_empty());
1603 }
1604
1605 #[tokio::test]
1606 async fn scan_stages_have_separate_cache_keys() {
1607 let partition_range = (
1608 Timestamp::new_millisecond(1000),
1609 Timestamp::new_millisecond(2000),
1610 );
1611 let (ctx, part_range) =
1612 new_stream_context(vec![col("k0").eq(lit("foo"))], None, partition_range).await;
1613
1614 let data_key = build_range_cache_key(&ctx, &part_range).unwrap();
1615 let candidate_key = build_candidate_range_cache_key(&ctx, &part_range).unwrap();
1616 let range_0 = SeriesRange::new(0, 2).unwrap();
1617 let range_1 = SeriesRange::new(1, 2).unwrap();
1618 let series_key_0 = build_series_range_cache_key(&ctx, &part_range, range_0).unwrap();
1619 let series_key_1 = build_series_range_cache_key(&ctx, &part_range, range_1).unwrap();
1620
1621 let merge_key = data_key.clone().for_cross_range_merge();
1622 assert_ne!(merge_key, data_key);
1623 assert_ne!(merge_key.scan, candidate_key.scan);
1624 assert_ne!(merge_key.scan, series_key_0.scan);
1625 assert_eq!(merge_key.row_groups, data_key.row_groups);
1626 assert_eq!(merge_key, data_key.clone().for_cross_range_merge());
1627 assert_ne!(data_key.scan, candidate_key.scan);
1628 assert_ne!(data_key.scan, series_key_0.scan);
1629 assert_ne!(candidate_key.scan, series_key_0.scan);
1630 assert_ne!(series_key_0.scan, series_key_1.scan);
1631 assert_eq!(data_key.row_groups, candidate_key.row_groups);
1632 assert_eq!(data_key.row_groups, series_key_0.row_groups);
1633 }
1634
1635 #[tokio::test]
1636 async fn disables_optimization_on_or_clause() {
1637 let partition_range = (
1638 Timestamp::new_millisecond(1000),
1639 Timestamp::new_millisecond(2000),
1640 );
1641
1642 let or_a = col("ts").gt_eq(ts_lit(1000)).or(col("ts").lt(ts_lit(500)));
1643 let or_b = col("ts").gt_eq(ts_lit(900)).or(col("ts").lt(ts_lit(400)));
1644
1645 let (ctx_a, part_a) = new_stream_context(
1646 vec![or_a.clone(), col("k0").eq(lit("foo"))],
1647 None,
1648 partition_range,
1649 )
1650 .await;
1651 let (ctx_b, part_b) = new_stream_context(
1652 vec![or_b.clone(), col("k0").eq(lit("foo"))],
1653 None,
1654 partition_range,
1655 )
1656 .await;
1657
1658 assert!(ctx_a.input.implied_time_range().is_none());
1659 let key_a = build_range_cache_key(&ctx_a, &part_a).unwrap();
1660 let key_b = build_range_cache_key(&ctx_b, &part_b).unwrap();
1661 assert_ne!(key_a.scan, key_b.scan);
1662 assert_eq!(
1663 key_a.scan.time_filters(),
1664 normalized_exprs([or_a]).as_slice()
1665 );
1666 }
1667
1668 #[tokio::test]
1669 async fn empty_implied_range_does_not_panic_on_non_ms_file_range() {
1670 let partition = (
1675 Timestamp::new_millisecond(1000),
1676 Timestamp::new_millisecond(2000),
1677 );
1678
1679 let (mut ctx, part_range) = new_stream_context(
1680 vec![
1681 col("ts").gt_eq(ts_lit(1500)),
1682 col("ts").lt(ts_lit(1500)),
1683 col("k0").eq(lit("foo")),
1684 ],
1685 TimestampRange::with_unit(1500, 3000, TimeUnit::Millisecond),
1686 partition,
1687 )
1688 .await;
1689
1690 assert!(ctx.input.implied_time_range().unwrap().is_empty());
1691 ctx.ranges[0].time_range = (
1692 Timestamp::new(1_000_000_000, TimeUnit::Nanosecond),
1693 Timestamp::new(2_000_000_000, TimeUnit::Nanosecond),
1694 );
1695
1696 let key = build_range_cache_key(&ctx, &part_range).unwrap();
1697 assert!(!key.scan.time_filters().is_empty());
1699 }
1700
1701 #[tokio::test]
1702 async fn mixed_unit_file_range_is_compared_by_instant() {
1703 let partition = (
1708 Timestamp::new_millisecond(1000),
1709 Timestamp::new_millisecond(2000),
1710 );
1711
1712 let (mut ctx, part_range) = new_stream_context(
1716 vec![
1717 col("ts").gt_eq(ts_lit(0)),
1718 col("ts").lt(ts_lit(3000)),
1719 col("k0").eq(lit("foo")),
1720 ],
1721 TimestampRange::with_unit(0, 3000, TimeUnit::Millisecond),
1722 partition,
1723 )
1724 .await;
1725 ctx.ranges[0].time_range = (
1726 Timestamp::new_microsecond(1_000_000),
1727 Timestamp::new_microsecond(2_000_000),
1728 );
1729 let key = build_range_cache_key(&ctx, &part_range).unwrap();
1730 assert!(key.scan.time_filters().is_empty());
1731
1732 let (mut ctx, part_range) = new_stream_context(
1735 vec![
1736 col("ts").gt_eq(ts_lit(1500)),
1737 col("ts").lt(ts_lit(3000)),
1738 col("k0").eq(lit("foo")),
1739 ],
1740 TimestampRange::with_unit(1500, 3000, TimeUnit::Millisecond),
1741 partition,
1742 )
1743 .await;
1744 ctx.ranges[0].time_range = (
1745 Timestamp::new_microsecond(1_000_000),
1746 Timestamp::new_microsecond(2_000_000),
1747 );
1748 let key = build_range_cache_key(&ctx, &part_range).unwrap();
1749 assert!(!key.scan.time_filters().is_empty());
1750 }
1751
1752 fn ms_ts(v: i64) -> Timestamp {
1753 Timestamp::new_millisecond(v)
1754 }
1755
1756 fn implied_ms(expr: Expr) -> Option<TimestampRange> {
1757 implied_time_range_from_exprs("ts", TimeUnit::Millisecond, &[&expr])
1758 }
1759
1760 #[test]
1761 fn implied_time_range_supported_exprs() {
1762 for (expr, expected) in [
1763 (
1764 col("ts").gt_eq(ts_lit(1000)),
1765 Some(TimestampRange::from_start(ms_ts(1000))),
1766 ),
1767 (
1768 col("ts").gt(ts_lit(1000)),
1769 Some(TimestampRange::from_start(ms_ts(1001))),
1770 ),
1771 (
1772 col("ts").lt_eq(ts_lit(2000)),
1773 Some(TimestampRange::until_end(ms_ts(2000), true)),
1774 ),
1775 (
1776 col("ts").lt(ts_lit(2000)),
1777 Some(TimestampRange::until_end(ms_ts(2000), false)),
1778 ),
1779 (
1780 col("ts").eq(ts_lit(1500)),
1781 Some(TimestampRange::single(ms_ts(1500))),
1782 ),
1783 (
1784 ts_lit(1000).lt_eq(col("ts")),
1785 Some(TimestampRange::from_start(ms_ts(1000))),
1786 ),
1787 (
1788 col("ts").between(ts_lit(1000), ts_lit(2000)),
1789 Some(TimestampRange::new_inclusive(
1790 Some(ms_ts(1000)),
1791 Some(ms_ts(2000)),
1792 )),
1793 ),
1794 (
1795 col("ts")
1796 .gt_eq(ts_lit(1000))
1797 .and(col("ts").lt(ts_lit(2000))),
1798 TimestampRange::with_unit(1000, 2000, TimeUnit::Millisecond),
1799 ),
1800 (
1801 col("ts")
1802 .gt_eq(ts_lit(1000))
1803 .and(col("ts").lt(ts_lit(5000)))
1804 .and(col("ts").lt_eq(ts_lit(3000))),
1805 TimestampRange::with_unit(1000, 3001, TimeUnit::Millisecond),
1806 ),
1807 ] {
1808 assert_eq!(implied_ms(expr), expected);
1809 }
1810
1811 assert_eq!(
1812 implied_time_range_from_exprs("ts", TimeUnit::Millisecond, &[]),
1813 None
1814 );
1815 }
1816
1817 #[test]
1818 fn implied_time_range_unsupported_exprs() {
1819 let not_between = Expr::Between(Between {
1820 expr: Box::new(col("ts")),
1821 negated: true,
1822 low: Box::new(ts_lit(1000)),
1823 high: Box::new(ts_lit(2000)),
1824 });
1825
1826 for expr in [
1827 not_between,
1828 col("ts").gt_eq(ts_lit(1000)).or(col("ts").lt(ts_lit(500))),
1829 Expr::Not(Box::new(col("ts").gt_eq(ts_lit(1000)))),
1830 col("ts").in_list(vec![ts_lit(1000), ts_lit(2000)], false),
1831 col("ts").gt_eq(col("other")),
1832 col("other_ts").gt_eq(ts_lit(1000)),
1833 ] {
1834 assert!(implied_ms(expr).is_none());
1835 }
1836 }
1837
1838 #[test]
1839 fn implied_time_range_unit_conversion() {
1840 let second_1 = lit(ScalarValue::TimestampSecond(Some(1), None));
1841 let ns_1500 = lit(ScalarValue::TimestampNanosecond(Some(1_500_000_000), None));
1842 let ns_1500_5 = lit(ScalarValue::TimestampNanosecond(Some(1_500_500_000), None));
1843
1844 for (expr, expected) in [
1845 (
1846 col("ts").gt_eq(second_1.clone()),
1847 Some(TimestampRange::from_start(ms_ts(1000))),
1848 ),
1849 (
1850 col("ts").lt_eq(second_1),
1851 Some(TimestampRange::until_end(ms_ts(1000), true)),
1852 ),
1853 (
1854 col("ts").eq(ns_1500),
1855 Some(TimestampRange::single(ms_ts(1500))),
1856 ),
1857 (col("ts").eq(ns_1500_5.clone()), None),
1858 (
1859 col("ts").gt_eq(ns_1500_5.clone()),
1860 Some(TimestampRange::from_start(ms_ts(1501))),
1861 ),
1862 (
1863 col("ts").lt_eq(ns_1500_5.clone()),
1864 Some(TimestampRange::until_end(ms_ts(1500), true)),
1865 ),
1866 (
1867 col("ts").gt(ns_1500_5.clone()),
1868 Some(TimestampRange::from_start(ms_ts(1501))),
1869 ),
1870 (
1871 col("ts").lt(ns_1500_5),
1872 Some(TimestampRange::until_end(ms_ts(1501), false)),
1873 ),
1874 ] {
1875 assert_eq!(implied_ms(expr), expected);
1876 }
1877 }
1878
1879 #[test]
1880 fn selector_after_merge_changes_fingerprint() {
1881 let ordinary = test_scan_fingerprint(
1882 vec!["k0 = 'foo'".to_string()],
1883 vec![],
1884 Some(TimeSeriesRowSelector::LastRow { after_merge: false }),
1885 true,
1886 0,
1887 );
1888 let after_merge = test_scan_fingerprint(
1889 vec!["k0 = 'foo'".to_string()],
1890 vec![],
1891 Some(TimeSeriesRowSelector::LastRow { after_merge: true }),
1892 true,
1893 0,
1894 );
1895
1896 assert_ne!(ordinary, after_merge);
1897 }
1898
1899 #[test]
1900 fn true_selector_after_merge_is_preserved_by_fingerprint_transforms() {
1901 let normalized =
1902 test_scan_fingerprint(vec!["k0 = 'foo'".to_string()], vec![], None, true, 0);
1903
1904 assert!(normalized.time_filters().is_empty());
1905
1906 let fingerprint = test_scan_fingerprint(
1907 vec!["k0 = 'foo'".to_string()],
1908 vec!["ts >= 1000".to_string()],
1909 Some(TimeSeriesRowSelector::LastRow { after_merge: true }),
1910 true,
1911 7,
1912 );
1913
1914 let reset = fingerprint.without_time_filters();
1915 let candidate = fingerprint.for_candidate_series();
1916 let series_data = fingerprint.for_series_data(SeriesRange::new(0, 1).unwrap());
1917
1918 assert_eq!(reset.read_columns(), fingerprint.read_columns());
1919 assert_eq!(reset.read_column_types(), fingerprint.read_column_types());
1920 assert_eq!(reset.filters(), fingerprint.filters());
1921 assert!(reset.time_filters().is_empty());
1922 assert_eq!(reset.series_row_selector, fingerprint.series_row_selector);
1923 assert_eq!(
1924 fingerprint.series_row_selector,
1925 Some(TimeSeriesRowSelector::LastRow { after_merge: true })
1926 );
1927 assert_eq!(
1928 candidate.series_row_selector,
1929 fingerprint.series_row_selector
1930 );
1931 assert_eq!(
1932 series_data.series_row_selector,
1933 fingerprint.series_row_selector
1934 );
1935 assert_eq!(reset.append_mode, fingerprint.append_mode);
1936 assert_eq!(reset.filter_deleted, fingerprint.filter_deleted);
1937 assert_eq!(reset.merge_mode, fingerprint.merge_mode);
1938 assert_eq!(
1939 reset.partition_expr_version,
1940 fingerprint.partition_expr_version
1941 );
1942 }
1943
1944 fn test_schema() -> Arc<datatypes::arrow::datatypes::Schema> {
1945 use datatypes::arrow::datatypes::{DataType as ArrowDataType, Field, Schema};
1946
1947 Arc::new(Schema::new(vec![Field::new(
1948 "value",
1949 ArrowDataType::Int64,
1950 false,
1951 )]))
1952 }
1953
1954 fn make_batch(values: &[i64]) -> RecordBatch {
1955 use datatypes::arrow::array::Int64Array;
1956
1957 RecordBatch::try_new(
1958 test_schema(),
1959 vec![Arc::new(Int64Array::from(values.to_vec()))],
1960 )
1961 .unwrap()
1962 }
1963
1964 fn make_large_binary_batch(rows: usize, bytes_per_row: usize) -> RecordBatch {
1965 use datatypes::arrow::array::BinaryArray;
1966 use datatypes::arrow::datatypes::{DataType as ArrowDataType, Field, Schema};
1967
1968 let schema = Arc::new(Schema::new(vec![Field::new(
1969 "value",
1970 ArrowDataType::Binary,
1971 false,
1972 )]));
1973 let payload = vec![b'x'; bytes_per_row];
1974 let values = (0..rows).map(|_| payload.as_slice()).collect::<Vec<_>>();
1975
1976 RecordBatch::try_new(schema, vec![Arc::new(BinaryArray::from_vec(values))]).unwrap()
1977 }
1978
1979 #[test]
1980 fn compact_record_batches_keeps_original_boundaries() {
1981 let batches = vec![make_batch(&[1, 2]), make_batch(&[3]), make_batch(&[4, 5])];
1982
1983 let compacted = compact_record_batches(batches).unwrap();
1984
1985 assert_eq!(compacted.batch.num_rows(), 5);
1986 assert_eq!(compacted.slice_lengths, vec![2, 1, 2]);
1987 }
1988
1989 #[tokio::test]
1990 async fn cached_flat_range_stream_replays_original_batches() {
1991 let value = Arc::new(RangeScanCacheValue::new(
1992 vec![CachedBatchSlice {
1993 batch: make_batch(&[1, 2, 3]),
1994 slice_lengths: vec![2, 1],
1995 }],
1996 make_batch(&[1, 2, 3]).get_array_memory_size(),
1997 ));
1998
1999 let worker = RangeCacheWorker::new(1024 * 1024);
2000 let permit = worker
2001 .load_memory
2002 .try_acquire(value.estimated_size())
2003 .unwrap();
2004 let delivery = RangeCacheDelivery::new(value, permit);
2005 let replayed = cached_flat_range_stream(delivery)
2006 .try_collect::<Vec<_>>()
2007 .await
2008 .unwrap();
2009
2010 assert_eq!(replayed.len(), 2);
2011 assert_eq!(replayed[0].num_rows(), 2);
2012 assert_eq!(replayed[1].num_rows(), 1);
2013 }
2014
2015 #[tokio::test]
2016 async fn dropped_cache_buffer_releases_batches_while_waiting_for_memory() {
2017 let strategy = test_cache_strategy();
2018 let limiter = strategy.range_result_memory_limiter().unwrap();
2019 let _permit = limiter
2021 .acquire(limiter.available_permits() * limiter.permit_bytes())
2022 .await
2023 .unwrap();
2024 let batch = make_batch(&vec![1; DEFAULT_READ_BATCH_SIZE / 2 + 1]);
2025 let weak = Arc::downgrade(batch.column(0));
2026 let mut buffer = CacheBatchBuffer::new(&strategy);
2027 buffer.push(batch.clone()).unwrap();
2028 buffer.push(batch).unwrap();
2029 assert!(buffer.buffered_batches.is_empty());
2031
2032 tokio::time::timeout(std::time::Duration::from_secs(5), async {
2035 while limiter.waited_acquires() == 0 {
2036 tokio::task::yield_now().await;
2037 }
2038 })
2039 .await
2040 .expect("concat task must wait for a memory permit");
2041 drop(buffer);
2042
2043 tokio::time::timeout(std::time::Duration::from_secs(5), async {
2044 while weak.upgrade().is_some() {
2045 tokio::task::yield_now().await;
2046 }
2047 })
2048 .await
2049 .expect("cancelled cache work must release input without waiting for a permit");
2050 }
2051
2052 #[tokio::test]
2053 async fn cache_batch_buffer_finishes_pending_batches() {
2054 let strategy = test_cache_strategy();
2055 let batch = make_batch(&[1, 2, 3]);
2056 let expected_size = batch.get_array_memory_size();
2057
2058 let mut buffer = CacheBatchBuffer::new(&strategy);
2059 buffer.push(batch).unwrap();
2060
2061 let value = buffer.finish().await.unwrap().unwrap();
2062 assert_eq!(value.cached_batches.len(), 1);
2063 assert_eq!(value.cached_batches[0].slice_lengths, vec![3]);
2064 assert_eq!(value.estimated_batches_size, expected_size);
2065 }
2066
2067 #[tokio::test]
2068 async fn cache_batch_buffer_compacts_when_rows_exceed_default_batch_size() {
2069 let strategy = test_cache_strategy();
2070 let batch = make_batch(&vec![1; DEFAULT_READ_BATCH_SIZE / 2 + 1]);
2071
2072 let mut buffer = CacheBatchBuffer::new(&strategy);
2073 buffer.push(batch.clone()).unwrap();
2074 buffer.push(batch).unwrap();
2075
2076 assert_eq!(buffer.buffered_rows, 0);
2077 assert!(buffer.buffered_batches.is_empty());
2078
2079 let value = buffer.finish().await.unwrap().unwrap();
2080 assert_eq!(value.cached_batches.len(), 1);
2081 assert_eq!(
2082 value.cached_batches[0].slice_lengths,
2083 vec![
2084 DEFAULT_READ_BATCH_SIZE / 2 + 1,
2085 DEFAULT_READ_BATCH_SIZE / 2 + 1
2086 ]
2087 );
2088 }
2089
2090 #[tokio::test]
2091 async fn cache_batch_buffer_compacts_when_buffered_size_exceeds_threshold() {
2092 let large_batch = make_large_binary_batch(DEFAULT_READ_BATCH_SIZE, 4096);
2093 let strategy = CacheStrategy::EnableAll(Arc::new(
2094 CacheManager::builder()
2095 .range_result_cache_size((large_batch.get_array_memory_size() * 3) as u64)
2096 .build(),
2097 ));
2098
2099 let mut buffer = CacheBatchBuffer::new(&strategy);
2100 buffer.push(large_batch.clone()).unwrap();
2101
2102 assert_eq!(buffer.buffered_rows, large_batch.num_rows());
2103 assert_eq!(buffer.buffered_batches.len(), 1);
2104
2105 buffer.push(large_batch.clone()).unwrap();
2106
2107 assert_eq!(buffer.buffered_rows, 0);
2108 assert!(buffer.buffered_batches.is_empty());
2109
2110 let value = buffer.finish().await.unwrap().unwrap();
2111 assert_eq!(value.cached_batches.len(), 1);
2112 assert_eq!(
2113 value.cached_batches[0].slice_lengths,
2114 vec![large_batch.num_rows(), large_batch.num_rows()]
2115 );
2116 }
2117
2118 #[tokio::test]
2119 async fn cache_batch_buffer_skips_cache_when_compacted_size_exceeds_limit() {
2120 let large_batch = make_large_binary_batch(DEFAULT_READ_BATCH_SIZE / 2 + 1, 4096);
2121 let budget = (large_batch.get_array_memory_size() as u64) * 2 + 1;
2123 let strategy = CacheStrategy::EnableAll(Arc::new(
2124 CacheManager::builder()
2125 .range_result_cache_size(budget)
2126 .build(),
2127 ));
2128
2129 let mut buffer = CacheBatchBuffer::new(&strategy);
2130 for _ in 0..4 {
2131 buffer.push(large_batch.clone()).unwrap();
2132 }
2133 assert!(!matches!(buffer.finish().await, Some(Ok(_))));
2134 }
2135}