1mod shared_range;
19
20use std::collections::HashMap;
21use std::future::Future;
22use std::mem;
23use std::sync::atomic::{AtomicBool, Ordering};
24use std::sync::{Arc, Mutex, Weak, mpsc};
25
26use async_stream::try_stream;
27use common_cache::SyncCache;
28use common_telemetry::warn;
29use datafusion::execution::memory_pool::MemoryPool;
30use datatypes::arrow::compute::concat_batches;
31use datatypes::arrow::record_batch::RecordBatch;
32use snafu::{IntoError, ResultExt};
33use store_api::storage::FileId;
34use tokio::sync::{OnceCell, OwnedSemaphorePermit};
35
36use crate::cache::range_cache::shared_range::{RangeRead, SharedRange};
37use crate::cache::{RANGE_RESULT_TYPE, RangeResultMemoryLimiter, range_result_cache_weight};
38use crate::error::{ComputeArrowSnafu, Result};
39use crate::metrics::{CACHE_BYTES, CACHE_EVICTION, RANGE_CACHE_WORKER_EVENTS};
40use crate::read::BoxedRecordBatchStream;
41use crate::read::range_cache::RangeScanCacheKey;
42use crate::sst::file::FileHandle;
43use crate::sst::parquet::DEFAULT_READ_BATCH_SIZE;
44
45pub(crate) const RANGE_CACHE_COMPACT_THRESHOLD_BYTES: usize = 8 * 1024 * 1024;
46
47#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
51pub(crate) struct RangeCacheScope(usize);
52
53impl RangeCacheScope {
54 pub(crate) fn for_memory_pool(pool: &Arc<dyn MemoryPool>) -> Self {
56 Self(Arc::as_ptr(pool) as *const () as usize)
57 }
58}
59
60pub(crate) trait RangeCacheMetricsReport: Send + Sync {
62 fn inc_range_cache_hit(&self);
64 fn inc_range_cache_miss(&self);
66 fn inc_range_cache_size(&self, bytes: usize);
68}
69
70pub(crate) struct RangeScanCacheValue {
72 cached_batches: Vec<CachedBatchSlice>,
73 estimated_batch_size: usize,
74 estimated_batches_size: usize,
76}
77
78impl RangeScanCacheValue {
79 #[cfg(test)]
80 pub(crate) fn empty() -> Self {
81 Self::new(Vec::new(), 0)
82 }
83
84 #[cfg(test)]
85 pub(crate) fn cached_batches(&self) -> impl Iterator<Item = (&RecordBatch, &[usize])> {
86 self.cached_batches
87 .iter()
88 .map(|batch| (&batch.batch, batch.slice_lengths.as_slice()))
89 }
90
91 #[cfg(test)]
92 pub(crate) fn estimated_batches_size(&self) -> usize {
93 self.estimated_batches_size
94 }
95
96 fn new(cached_batches: Vec<CachedBatchSlice>, estimated_batches_size: usize) -> Self {
97 Self {
98 cached_batches,
99 estimated_batch_size: DEFAULT_READ_BATCH_SIZE,
100 estimated_batches_size,
101 }
102 }
103
104 pub(crate) fn estimated_size(&self) -> usize {
105 mem::size_of::<Self>()
106 + self.cached_batches.capacity() * mem::size_of::<CachedBatchSlice>()
107 + self
108 .cached_batches
109 .iter()
110 .map(CachedBatchSlice::metadata_size)
111 .sum::<usize>()
112 + self.estimated_batches_size
113 }
114}
115
116pub(crate) struct RangeResultCache {
118 cache: SyncCache<RangeScanCacheKey, Arc<Entry>>,
119 state: Mutex<State>,
120 cleanup_tx: mpsc::Sender<RangeCacheCleanup>,
123 cleanup_rx: Mutex<mpsc::Receiver<RangeCacheCleanup>>,
124 retention_memory: Arc<RangeResultMemoryLimiter>,
127 concat_memory: Arc<RangeResultMemoryLimiter>,
129}
130
131impl RangeResultCache {
132 #[cfg(test)]
133 pub(crate) fn retention_memory(&self) -> &Arc<RangeResultMemoryLimiter> {
134 &self.retention_memory
135 }
136
137 #[cfg(test)]
138 pub(crate) fn concat_memory(&self) -> &Arc<RangeResultMemoryLimiter> {
139 &self.concat_memory
140 }
141
142 #[tracing::instrument(name = "range_result_lookup", skip_all)]
144 pub(crate) async fn read<F, Fut>(
145 self: &Arc<Self>,
146 key: RangeScanCacheKey,
147 part_metrics: Arc<dyn RangeCacheMetricsReport>,
148 files: Vec<FileHandle>,
149 scope: RangeCacheScope,
150 build: F,
151 ) -> Result<(BoxedRecordBatchStream, usize)>
152 where
153 F: FnOnce() -> Fut,
154 Fut: Future<Output = Result<(BoxedRecordBatchStream, usize)>>,
155 {
156 let cache = self;
157 let load = match cache.get_or_register(key, files, scope) {
158 RangeCacheLookup::Hit(value) => {
159 part_metrics.inc_range_cache_hit();
160 let batch_size = value.value().estimated_batch_size;
161 return Ok((cached_flat_range_stream(value), batch_size));
162 }
163 RangeCacheLookup::Loading(load) => load,
164 RangeCacheLookup::Bypass => {
165 part_metrics.inc_range_cache_miss();
166 return build().await;
167 }
168 };
169 part_metrics.inc_range_cache_miss();
170 let cache = cache.clone();
171 let limiter = cache.retention_memory.clone();
172 let mut build = Some(build);
173 let mut owner_cursor = None;
174 let (shared, batch_size) = load
175 .shared_range
176 .get_or_try_init(|| async {
177 let builder = build.take().ok_or_else(|| {
178 crate::error::UnexpectedSnafu {
179 reason: "range scan builder already consumed",
180 }
181 .build()
182 })?;
183 let (source, batch_size) = Box::pin(builder()).await?;
184 let shared = SharedRange::new(source, limiter.clone());
185 owner_cursor = shared.subscribe().await;
186 Ok::<_, crate::error::Error>((shared, batch_size))
187 })
188 .await?;
189 let shared = shared.clone();
190 let batch_size = *batch_size;
191 let cursor = match owner_cursor {
192 Some(cursor) => Some(cursor),
193 None => shared.subscribe().await,
194 };
195 let Some(mut cursor) = cursor else {
196 let builder = build.ok_or_else(|| {
199 crate::error::UnexpectedSnafu {
200 reason: "range scan owner lost its initial cursor",
201 }
202 .build()
203 })?;
204 return builder().await;
205 };
206 let metrics = part_metrics;
207 let stream = Box::pin(try_stream! {
208 let load = load;
210 loop {
211 match shared.read(&mut cursor).await {
212 RangeRead::Batch(batch) => yield batch,
213 RangeRead::Failed(error) => {
214 Err(crate::error::LoadRangeResultSnafu.into_error(error))?;
215 }
216 RangeRead::Finished => {
217 if let Some(batches) = shared.take_complete_batches().await {
218 common_runtime::spawn_query(cache.finish_scan(load, batches, batch_size, metrics));
221 }
222 break;
223 }
224 }
225 }
226 });
227 Ok((stream, batch_size))
228 }
229
230 pub(crate) fn new(capacity: u64) -> Arc<Self> {
232 let (tx, rx) = mpsc::channel();
233 let eviction_tx = tx.clone();
234 let cache = SyncCache::new(
235 RANGE_RESULT_TYPE,
236 capacity,
237 |k: &RangeScanCacheKey, v: &Arc<Entry>| range_result_cache_weight(k, &v.value),
238 move |key, entry: Arc<Entry>, cause| {
239 CACHE_BYTES
240 .with_label_values(&[RANGE_RESULT_TYPE])
241 .sub(range_result_cache_weight(&key, &entry.value).into());
242 CACHE_EVICTION
243 .with_label_values(&[RANGE_RESULT_TYPE, cause.as_str()])
244 .inc();
245 let _ = eviction_tx.send(RangeCacheCleanup::Evicted {
246 key,
247 generation: entry.generation,
248 });
249 },
250 );
251 Arc::new(Self {
252 cache,
253 state: Mutex::new(State::default()),
254 cleanup_tx: tx,
255 cleanup_rx: Mutex::new(rx),
256 retention_memory: Arc::new(RangeResultMemoryLimiter::new(capacity as usize, 1024)),
257 concat_memory: Arc::new(RangeResultMemoryLimiter::new(
258 capacity as usize,
259 crate::cache::RANGE_RESULT_CONCAT_MEMORY_PERMIT.as_bytes() as usize,
260 )),
261 })
262 }
263
264 async fn finish_scan(
266 self: Arc<Self>,
267 load: Arc<RangeLoad>,
268 batches: Vec<CachedBatchSlice>,
269 batch_size: usize,
270 metrics: Arc<dyn RangeCacheMetricsReport>,
271 ) {
272 let bytes = batches
273 .iter()
274 .map(|cached| cached.batch.get_array_memory_size())
275 .sum();
276 let value = RangeScanCacheValue::new(batches, bytes);
277 let mut compacted = CacheConcatState::default();
278 if let Err(error) = compacted.compact_range(&value, &self.concat_memory).await {
279 warn!(error; "Failed to compact shared range result");
280 return;
281 }
282 drop(value);
283 let mut value = compacted.finish();
284 value.estimated_batch_size = batch_size;
285 let bytes = load.key.0.estimated_size() + value.estimated_size();
286 if self.complete(&load, Arc::new(value)) {
287 metrics.inc_range_cache_size(bytes);
288 }
289 }
290
291 fn drain_cleanup(&self, state: &mut State) {
292 for cleanup in self.cleanup_rx.lock().unwrap().try_iter() {
293 match cleanup {
294 RangeCacheCleanup::Evicted { key, generation } => state.forget(&key, generation),
295 RangeCacheCleanup::Released { key, generation } => {
296 if state
297 .flights
298 .get(&key)
299 .is_some_and(|entry| entry.generation == generation)
300 {
301 state.flights.remove(&key);
302 }
303 }
304 }
305 }
306 }
307
308 #[cfg(test)]
309 pub(crate) fn get(&self, key: &RangeScanCacheKey) -> Option<Arc<RangeScanCacheValue>> {
310 self.cache.get(key).map(|entry| entry.value.clone())
311 }
312
313 fn get_for_delivery(&self, key: &RangeScanCacheKey) -> Option<RangeCacheLookup> {
314 let entry = self.cache.get(key)?;
315 Some(match entry.deliver(&self.retention_memory) {
317 Some(delivery) => RangeCacheLookup::Hit(delivery),
318 None => {
319 RANGE_CACHE_WORKER_EVENTS
320 .with_label_values(&["bypass_memory"])
321 .inc();
322 RangeCacheLookup::Bypass
323 }
324 })
325 }
326
327 #[cfg(test)]
329 pub(crate) fn insert(&self, key: RangeScanCacheKey, value: Arc<RangeScanCacheValue>) {
330 let mut state = self.state.lock().unwrap();
331 self.drain_cleanup(&mut state);
332 self.publish(&mut state, key, value);
333 }
334
335 fn publish(&self, state: &mut State, key: RangeScanCacheKey, value: Arc<RangeScanCacheValue>) {
336 let generation = state.next_generation();
337 let indexed_key = Arc::new(key.clone());
338 for (file, _) in &key.row_groups {
339 state
340 .files
341 .entry(*file)
342 .or_default()
343 .insert(indexed_key.clone(), generation);
344 }
345 CACHE_BYTES
346 .with_label_values(&[RANGE_RESULT_TYPE])
347 .add(range_result_cache_weight(&key, &value).into());
348 self.cache.insert(
349 key,
350 Arc::new(Entry {
351 generation,
352 value,
353 delivery: Mutex::new(Weak::new()),
354 }),
355 );
356 self.drain_cleanup(state);
357 }
358
359 pub(crate) fn invalidate_file(&self, file: FileId) {
362 RANGE_CACHE_WORKER_EVENTS
363 .with_label_values(&["invalidate"])
364 .inc();
365 let mut state = self.state.lock().unwrap();
366 self.drain_cleanup(&mut state);
367 if let Some(keys) = state.files.remove(&file) {
368 for (key, generation) in keys {
369 self.cache.invalidate(key.as_ref());
370 state.forget(&key, generation);
371 }
372 }
373 state.flights.retain(|(key, _), flight| {
374 let Some(flight) = flight.load.upgrade() else {
375 return false;
376 };
377 if key.row_groups.iter().any(|(id, _)| *id == file) {
378 flight.invalidated.store(true, Ordering::Relaxed);
379 }
380 true
381 });
382 self.drain_cleanup(&mut state);
383 }
384
385 fn get_or_register(
387 self: &Arc<Self>,
388 key: RangeScanCacheKey,
389 files: Vec<FileHandle>,
390 scope: RangeCacheScope,
391 ) -> RangeCacheLookup {
392 let mut state = self.state.lock().unwrap();
393 self.drain_cleanup(&mut state);
394 if files.iter().any(FileHandle::is_deleted) {
395 RANGE_CACHE_WORKER_EVENTS
396 .with_label_values(&["bypass_retired"])
397 .inc();
398 return RangeCacheLookup::Bypass;
399 }
400 if let Some(lookup) =
401 crate::cache::update_hit_miss(self.get_for_delivery(&key), RANGE_RESULT_TYPE)
402 {
403 return lookup;
404 }
405 let scoped_key = (key, scope);
406 if let Some(load) = state
407 .flights
408 .get(&scoped_key)
409 .and_then(|entry| entry.load.upgrade())
410 {
411 RANGE_CACHE_WORKER_EVENTS
412 .with_label_values(&["coalesced"])
413 .inc();
414 return RangeCacheLookup::Loading(load);
415 }
416 let load = Arc::new(RangeLoad {
417 shared_range: OnceCell::new(),
418 key: scoped_key.clone(),
419 generation: state.next_generation(),
420 files,
421 invalidated: AtomicBool::new(false),
422 cleanup_tx: self.cleanup_tx.clone(),
423 });
424 state.flights.insert(
425 scoped_key,
426 RangeRegistration {
427 generation: load.generation,
428 load: Arc::downgrade(&load),
429 },
430 );
431 RANGE_CACHE_WORKER_EVENTS
432 .with_label_values(&["started"])
433 .inc();
434 RangeCacheLookup::Loading(load)
435 }
436
437 fn complete(&self, load: &RangeLoad, value: Arc<RangeScanCacheValue>) -> bool {
440 let mut state = self.state.lock().unwrap();
443 self.drain_cleanup(&mut state);
444 let current = state
445 .flights
446 .get(&load.key)
447 .and_then(|entry| entry.load.upgrade());
448 if !current.is_some_and(|current| current.generation == load.generation) {
449 return false;
450 }
451 if load.invalidated.load(Ordering::Relaxed) || load.files.iter().any(FileHandle::is_deleted)
452 {
453 RANGE_CACHE_WORKER_EVENTS
454 .with_label_values(&["late_fill_skipped"])
455 .inc();
456 return false;
457 }
458 self.publish(&mut state, load.key.0.clone(), value);
459 RANGE_CACHE_WORKER_EVENTS
460 .with_label_values(&["published"])
461 .inc();
462 true
463 }
464}
465
466#[derive(Debug, Clone)]
468struct CachedBatchSlice {
469 batch: RecordBatch,
470 slice_lengths: Vec<usize>,
471}
472
473impl CachedBatchSlice {
474 fn metadata_size(&self) -> usize {
475 self.slice_lengths.capacity() * mem::size_of::<usize>()
476 }
477}
478
479struct RangeCacheDelivery {
483 value: Arc<RangeScanCacheValue>,
484 _permit: OwnedSemaphorePermit,
485}
486
487impl RangeCacheDelivery {
488 fn new(value: Arc<RangeScanCacheValue>, permit: OwnedSemaphorePermit) -> Arc<Self> {
490 Arc::new(Self {
491 value,
492 _permit: permit,
493 })
494 }
495
496 fn value(&self) -> &RangeScanCacheValue {
498 &self.value
499 }
500}
501
502enum RangeCacheLookup {
503 Hit(Arc<RangeCacheDelivery>),
504 Loading(Arc<RangeLoad>),
505 Bypass,
506}
507
508struct Entry {
509 generation: u64,
510 value: Arc<RangeScanCacheValue>,
511 delivery: Mutex<Weak<RangeCacheDelivery>>,
512}
513
514impl Entry {
515 fn deliver(&self, memory: &RangeResultMemoryLimiter) -> Option<Arc<RangeCacheDelivery>> {
516 let mut shared_delivery = self
519 .delivery
520 .lock()
521 .unwrap_or_else(std::sync::PoisonError::into_inner);
522 if let Some(delivery) = shared_delivery.upgrade() {
523 return Some(delivery);
524 }
525 let permit = memory.try_acquire(self.value.estimated_size())?;
526 let delivery = RangeCacheDelivery::new(self.value.clone(), permit);
527 *shared_delivery = Arc::downgrade(&delivery);
528 Some(delivery)
529 }
530}
531
532type FlightKey = (RangeScanCacheKey, RangeCacheScope);
533
534struct RangeLoad {
537 shared_range: OnceCell<(Arc<SharedRange>, usize)>,
538 key: FlightKey,
539 generation: u64,
540 files: Vec<FileHandle>,
541 invalidated: AtomicBool,
542 cleanup_tx: mpsc::Sender<RangeCacheCleanup>,
543}
544
545impl Drop for RangeLoad {
546 fn drop(&mut self) {
547 let _ = self.cleanup_tx.send(RangeCacheCleanup::Released {
548 key: self.key.clone(),
549 generation: self.generation,
550 });
551 }
552}
553
554struct RangeRegistration {
555 generation: u64,
556 load: Weak<RangeLoad>,
557}
558
559enum RangeCacheCleanup {
560 Evicted {
561 key: Arc<RangeScanCacheKey>,
562 generation: u64,
563 },
564 Released {
565 key: FlightKey,
566 generation: u64,
567 },
568}
569
570#[derive(Default)]
571struct State {
572 generation: u64,
573 flights: HashMap<FlightKey, RangeRegistration>,
574 files: HashMap<FileId, HashMap<Arc<RangeScanCacheKey>, u64>>,
577}
578
579impl State {
580 fn next_generation(&mut self) -> u64 {
581 self.generation += 1;
582 self.generation
583 }
584
585 fn forget(&mut self, key: &RangeScanCacheKey, generation: u64) {
586 for (file, _) in &key.row_groups {
587 if let Some(keys) = self.files.get_mut(file) {
588 if keys.get(key) == Some(&generation) {
590 keys.remove(key);
591 }
592 if keys.is_empty() {
593 self.files.remove(file);
594 }
595 }
596 }
597 }
598}
599
600fn cached_flat_range_stream(delivery: Arc<RangeCacheDelivery>) -> BoxedRecordBatchStream {
602 Box::pin(try_stream! {
603 for cached_batch in &delivery.value().cached_batches {
607 let mut offset = 0;
608 for &len in &cached_batch.slice_lengths {
609 yield cached_batch.batch.slice(offset, len);
610 offset += len;
611 }
612 }
613 })
614}
615
616#[derive(Default)]
617struct CacheConcatState {
618 cached_batches: Vec<CachedBatchSlice>,
619 estimated_size: usize,
620}
621
622impl CacheConcatState {
623 async fn compact(
624 &mut self,
625 batches: Vec<RecordBatch>,
626 limiter: &crate::cache::RangeResultMemoryLimiter,
627 ) -> Result<()> {
628 if batches.is_empty() {
629 return Ok(());
630 }
631
632 let input_size = batches
633 .iter()
634 .map(RecordBatch::get_array_memory_size)
635 .sum::<usize>();
636 let _permit = limiter.acquire(input_size).await?;
637
638 let compacted = compact_record_batches(batches)?;
639 self.estimated_size += compacted.batch.get_array_memory_size();
640 self.cached_batches.push(compacted);
641 Ok(())
642 }
643
644 async fn compact_range(
645 &mut self,
646 value: &RangeScanCacheValue,
647 limiter: &crate::cache::RangeResultMemoryLimiter,
648 ) -> Result<()> {
649 let mut pending = Vec::new();
650 let mut rows = 0;
651 let mut bytes = 0;
652 for cached in &value.cached_batches {
653 if cached.slice_lengths.len() > 1 {
654 self.compact(mem::take(&mut pending), limiter).await?;
655 self.estimated_size += cached.batch.get_array_memory_size();
656 self.cached_batches.push(cached.clone());
657 rows = 0;
658 bytes = 0;
659 continue;
660 }
661 rows += cached.batch.num_rows();
662 bytes += cached.batch.get_array_memory_size();
663 pending.push(cached.batch.clone());
664 if should_compact_batches(pending.len(), rows, bytes) {
665 self.compact(mem::take(&mut pending), limiter).await?;
666 rows = 0;
667 bytes = 0;
668 }
669 }
670 self.compact(pending, limiter).await
671 }
672
673 fn finish(self) -> RangeScanCacheValue {
674 RangeScanCacheValue::new(self.cached_batches, self.estimated_size)
675 }
676}
677
678fn should_compact_batches(batches: usize, rows: usize, bytes: usize) -> bool {
679 batches > 1 && (rows > DEFAULT_READ_BATCH_SIZE || bytes > RANGE_CACHE_COMPACT_THRESHOLD_BYTES)
680}
681
682fn compact_record_batches(mut batches: Vec<RecordBatch>) -> Result<CachedBatchSlice> {
683 debug_assert!(!batches.is_empty());
684 let slice_lengths = batches.iter().map(RecordBatch::num_rows).collect();
685 if batches.len() == 1 {
686 batches.push(batches[0].slice(0, 0));
689 }
690 let batch = concat_batches(&batches[0].schema(), &batches).context(ComputeArrowSnafu)?;
691 Ok(CachedBatchSlice {
692 batch,
693 slice_lengths,
694 })
695}
696
697#[cfg(test)]
698mod tests {
699 use super::*;
700 use crate::cache::test_util::range_cache_key;
701 use crate::sst::file::FileMeta;
702 use crate::sst::file_purger::NoopFilePurger;
703
704 fn empty_result() -> Arc<RangeScanCacheValue> {
705 Arc::new(RangeScanCacheValue::new(vec![], 0))
706 }
707
708 fn loading(lookup: RangeCacheLookup) -> Arc<RangeLoad> {
709 let RangeCacheLookup::Loading(load) = lookup else {
710 panic!("expected active scan")
711 };
712 load
713 }
714
715 #[test]
716 fn test_retirement_removes_all_dependent_keys_but_preserves_other_files() {
717 let worker = RangeResultCache::new(1024 * 1024);
718 let a = FileId::random();
719 let b = FileId::random();
720 let combined = range_cache_key(&[a, b, a]);
721 let mut copied = range_cache_key(&[a]);
722 copied.region_id = store_api::storage::RegionId::new(2, 1);
723 let unaffected = range_cache_key(&[b]);
724 for key in [&combined, &copied, &unaffected] {
725 worker.insert(key.clone(), empty_result());
726 }
727 worker.invalidate_file(a);
728 worker.invalidate_file(a);
729 assert!(worker.get(&combined).is_none());
730 assert!(worker.get(&copied).is_none());
731 assert!(worker.get(&unaffected).is_some());
732 worker.invalidate_file(b);
733 assert!(worker.get(&unaffected).is_none());
734 assert!(worker.state.lock().unwrap().files.is_empty());
735 }
736
737 #[test]
738 fn test_delayed_replacement_eviction_cannot_hide_a_new_entry_from_invalidation() {
739 let worker = RangeResultCache::new(1024 * 1024);
740 let file = FileId::random();
741 let key = range_cache_key(&[file]);
742 worker.insert(key.clone(), empty_result());
743 worker.cache.run_pending_tasks();
744 let replacement = empty_result();
745 worker.insert(key.clone(), replacement.clone());
746 worker.cache.run_pending_tasks();
747 assert!(Arc::ptr_eq(&worker.get(&key).unwrap(), &replacement));
748 worker.invalidate_file(file);
749 assert!(worker.get(&key).is_none());
750 }
751
752 #[test]
753 fn test_cache_hits_share_delivery_budget_until_the_last_reader_leaves() {
754 let worker = RangeResultCache::new(1024);
755 let file = FileId::random();
756 let key = range_cache_key(&[file]);
757 worker.insert(key.clone(), empty_result());
758 assert_eq!(worker.retention_memory.available_permits(), 1);
759
760 let blocker = worker.retention_memory.try_acquire(1024).unwrap();
761 assert!(matches!(
762 worker.get_or_register(key.clone(), vec![], RangeCacheScope(0)),
763 RangeCacheLookup::Bypass
764 ));
765 assert!(worker.get(&key).is_some());
766 drop(blocker);
767
768 let RangeCacheLookup::Hit(first) =
769 worker.get_or_register(key.clone(), vec![], RangeCacheScope(0))
770 else {
771 panic!("expected an accounted cache hit");
772 };
773 let RangeCacheLookup::Hit(second) =
774 worker.get_or_register(key.clone(), vec![], RangeCacheScope(0))
775 else {
776 panic!("readers of the same value must share its reservation");
777 };
778 assert!(Arc::ptr_eq(&first, &second));
779 assert_eq!(worker.retention_memory.available_permits(), 0);
780
781 worker.invalidate_file(file);
782 worker.cache.run_pending_tasks();
783 assert!(worker.get(&key).is_none());
784 drop(first);
785 assert_eq!(worker.retention_memory.available_permits(), 0);
786 drop(second);
787 assert_eq!(worker.retention_memory.available_permits(), 1);
788 }
789
790 #[test]
791 fn test_streaming_registration_is_scoped_and_reader_owned() {
792 let worker = RangeResultCache::new(1024 * 1024);
793 let key = range_cache_key(&[FileId::random()]);
794 let first = loading(worker.get_or_register(key.clone(), vec![], RangeCacheScope(1)));
795 let second = loading(worker.get_or_register(key.clone(), vec![], RangeCacheScope(1)));
796 assert!(Arc::ptr_eq(&first, &second));
797 let other_pool = loading(worker.get_or_register(key.clone(), vec![], RangeCacheScope(2)));
798 assert!(!Arc::ptr_eq(&first, &other_pool));
799 let weak = Arc::downgrade(&first);
800 drop(first);
801 assert!(weak.upgrade().is_some());
802 drop(second);
803 assert!(weak.upgrade().is_none());
804 let retry = loading(worker.get_or_register(key.clone(), vec![], RangeCacheScope(1)));
805 let source_permit = worker.retention_memory.try_acquire(1024 * 1024).unwrap();
807 assert!(worker.complete(&retry, empty_result()));
808 assert!(worker.get(&key).is_some());
809 assert!(matches!(
810 worker.get_or_register(key.clone(), vec![], RangeCacheScope(3)),
811 RangeCacheLookup::Bypass
812 ));
813 drop(source_permit);
814 assert!(matches!(
815 worker.get_or_register(key, vec![], RangeCacheScope(3)),
816 RangeCacheLookup::Hit(_)
817 ));
818 }
819
820 #[test]
821 fn test_retirement_blocks_late_publication_and_old_snapshot_registration() {
822 for mark_handle in [false, true] {
823 let worker = RangeResultCache::new(1024 * 1024);
824 let file = FileHandle::new(FileMeta::default(), Arc::new(NoopFilePurger));
825 let key = range_cache_key(&[file.file_id().file_id()]);
826 let load = loading(worker.get_or_register(
827 key.clone(),
828 vec![file.clone()],
829 RangeCacheScope(0),
830 ));
831 if mark_handle {
832 file.mark_deleted();
833 } else {
834 worker.invalidate_file(file.file_id().file_id());
835 }
836 worker.complete(&load, empty_result());
837 assert!(worker.get(&key).is_none());
838 if mark_handle {
839 assert!(matches!(
840 worker.get_or_register(key, vec![file], RangeCacheScope(0)),
841 RangeCacheLookup::Bypass
842 ));
843 }
844 }
845 }
846
847 #[test]
848 fn test_scan_release_cleans_only_the_last_owner_under_the_state_lock() {
849 let cache = RangeResultCache::new(1024 * 1024);
850 let key = range_cache_key(&[FileId::random()]);
851 let first = loading(cache.get_or_register(key.clone(), vec![], RangeCacheScope(0)));
852 let second = loading(cache.get_or_register(key.clone(), vec![], RangeCacheScope(0)));
853 let weak = Arc::downgrade(&first);
854 let mut state = cache.state.lock().unwrap();
855 drop(first);
856 cache.drain_cleanup(&mut state);
857 assert_eq!(state.flights.len(), 1);
858 drop(second);
860 assert!(weak.upgrade().is_none());
861 cache.drain_cleanup(&mut state);
862 assert!(state.flights.is_empty());
863 }
864
865 #[test]
866 fn test_delayed_scan_release_keeps_a_replacement_registration() {
867 let cache = RangeResultCache::new(1024 * 1024);
868 let key = range_cache_key(&[FileId::random()]);
869 let old = loading(cache.get_or_register(key.clone(), vec![], RangeCacheScope(0)));
870 drop(old);
871 let delayed = cache.cleanup_rx.lock().unwrap().try_recv().unwrap();
872 let new = loading(cache.get_or_register(key.clone(), vec![], RangeCacheScope(0)));
874 cache.cleanup_tx.send(delayed).unwrap();
875 let shared = loading(cache.get_or_register(key, vec![], RangeCacheScope(0)));
876 assert!(Arc::ptr_eq(&new, &shared));
877 drop(new);
878 drop(shared);
879 let mut state = cache.state.lock().unwrap();
880 cache.drain_cleanup(&mut state);
881 assert!(state.flights.is_empty());
882 }
883
884 #[rstest::rstest]
885 fn test_distinct_active_scans_share_loads_and_prune_abandoned_registrations(
886 #[values(512, 2048, 8192)] count: usize,
887 ) {
888 let worker = RangeResultCache::new(1024 * 1024);
889 let keys: Vec<_> = (0..count)
890 .map(|i| range_cache_key(&[FileId::from_bytes((i as u128).to_le_bytes())]))
891 .collect();
892 let loads: Vec<_> = keys
893 .iter()
894 .map(|key| loading(worker.get_or_register(key.clone(), vec![], RangeCacheScope(0))))
895 .collect();
896 for (key, load) in keys.iter().zip(&loads) {
897 let shared = loading(worker.get_or_register(key.clone(), vec![], RangeCacheScope(0)));
898 assert!(Arc::ptr_eq(load, &shared));
899 }
900 let abandoned: Vec<_> = loads.iter().map(Arc::downgrade).collect();
901 drop(loads);
902 assert!(abandoned.iter().all(|load| load.upgrade().is_none()));
903 let next = loading(worker.get_or_register(keys[0].clone(), vec![], RangeCacheScope(0)));
904 assert_eq!(worker.state.lock().unwrap().flights.len(), 1);
905 let shared = loading(worker.get_or_register(keys[0].clone(), vec![], RangeCacheScope(0)));
906 assert!(Arc::ptr_eq(&next, &shared));
907 }
908
909 #[rstest::rstest]
910 #[tokio::test]
911 async fn test_index_replacement_retirement_invalidates_and_blocks_old_reader_fills(
912 #[values(false, true)] gc_enabled: bool,
913 ) {
914 use common_base::readable_size::ReadableSize;
915 use store_api::region_engine::RegionEngine;
916 use store_api::region_request::RegionRequest;
917 use store_api::storage::RegionId;
918
919 use crate::config::MitoConfig;
920 use crate::gc::GcConfig;
921 use crate::sst::file::IndexType;
922 use crate::sst::version::SstVersion;
923 use crate::test_util::{CreateRequestBuilder, TestEnv};
924
925 let mut env = TestEnv::new().await;
926 let engine = env
927 .create_engine(MitoConfig {
928 range_result_cache_size: ReadableSize::mb(1),
929 gc: GcConfig {
930 enable: gc_enabled,
931 ..Default::default()
932 },
933 ..Default::default()
934 })
935 .await;
936 let region_id = RegionId::new(1, 1);
937 engine
938 .handle_request(
939 region_id,
940 RegionRequest::Create(CreateRequestBuilder::new().build()),
941 )
942 .await
943 .unwrap();
944 let region = engine.get_region(region_id).unwrap();
945 let cache = engine.cache_manager();
946 let worker = cache.range_result_cache.as_ref().unwrap();
947 let meta = FileMeta {
948 region_id,
949 file_id: FileId::random(),
950 ..Default::default()
951 };
952 let mut version = SstVersion::new(crate::test_util::memtable_util::metadata_for_test());
953 version.add_files(region.file_purger.clone(), [meta.clone()].into_iter());
954 let old_reader = version.levels()[0].files[&meta.file_id].clone();
955 let key = range_cache_key(&[meta.file_id]);
956 worker.insert(key.clone(), empty_result());
957 assert!(worker.get(&key).is_some());
958 let loading_key = key.clone().for_cross_range_merge();
959 let pending = loading(worker.get_or_register(
960 loading_key.clone(),
961 vec![old_reader.clone()],
962 RangeCacheScope(0),
963 ));
964 let updated = FileMeta {
965 index_version: 1,
966 available_indexes: smallvec::smallvec![IndexType::InvertedIndex],
967 ..meta
968 };
969 version.add_files(region.file_purger.clone(), [updated.clone()].into_iter());
970 version.remove_files([updated].into_iter());
971
972 assert!(old_reader.is_deleted());
975 assert!(worker.get(&key).is_none());
976 assert!(matches!(
977 worker.get_or_register(key, vec![old_reader], RangeCacheScope(0)),
978 RangeCacheLookup::Bypass
979 ));
980 assert!(!worker.complete(&pending, empty_result()));
981 assert!(worker.get(&loading_key).is_none());
982 }
983}