Skip to main content

mito2/cache/
range_cache.rs

1// Copyright 2023 Greptime Team
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Coordinates range-result cache loads and SST retirement. No I/O or decoding
16//! runs under the coordinator lock; loaders use the shared query runtime.
17
18mod 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/// Separates active scans with different query memory reservations.
48/// Completed results are reusable across scopes. The default scope is for
49/// sources without a query-specific memory pool.
50#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
51pub(crate) struct RangeCacheScope(usize);
52
53impl RangeCacheScope {
54    /// Uses the identity of the pool retained by the source scan.
55    pub(crate) fn for_memory_pool(pool: &Arc<dyn MemoryPool>) -> Self {
56        Self(Arc::as_ptr(pool) as *const () as usize)
57    }
58}
59
60/// Reports cache activity to the query that owns the reader or publication.
61pub(crate) trait RangeCacheMetricsReport: Send + Sync {
62    /// Records a completed-result hit for this reader.
63    fn inc_range_cache_hit(&self);
64    /// Records a reader without a deliverable completed result.
65    fn inc_range_cache_miss(&self);
66    /// Records bytes published by the reader that finishes the shared scan.
67    fn inc_range_cache_size(&self, bytes: usize);
68}
69
70/// Compacted batches with their original stream boundaries and memory weight.
71pub(crate) struct RangeScanCacheValue {
72    cached_batches: Vec<CachedBatchSlice>,
73    estimated_batch_size: usize,
74    /// Precomputed size of all compacted batches.
75    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
116/// Stores completed range results and coordinates reader-owned shared scans.
117pub(crate) struct RangeResultCache {
118    cache: SyncCache<RangeScanCacheKey, Arc<Entry>>,
119    state: Mutex<State>,
120    /// Eviction callbacks and the last scan owner can run while `state` is locked.
121    /// Queue their cleanup instead of reentering the coordinator lock.
122    cleanup_tx: mpsc::Sender<RangeCacheCleanup>,
123    cleanup_rx: Mutex<mpsc::Receiver<RangeCacheCleanup>>,
124    /// Covers loads and completed deliveries, separately from cache residency
125    /// and the temporary concat-memory limiter. Never wait for this budget.
126    retention_memory: Arc<RangeResultMemoryLimiter>,
127    /// Bounds temporary allocations while compacting a completed result.
128    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    /// Replays a cached result or subscribes to one reader-owned streaming scan.
143    #[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            // An oversized range has closed admission. This caller has not emitted
197            // any rows, so its original ordinary stream remains a safe fallback.
198            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            // Keep the weak registry entry alive until this reader finishes.
209            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                            // Finalization owns the registration until publication, so
219                            // a new caller can still join during background compaction.
220                            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    /// Creates independent retention and compaction budgets with the cache capacity.
231    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    /// Compacts a completed scan, then publishes it under the retirement fence.
265    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        // A hit without delivery budget must bypass, not start another load.
316        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    /// For test/benchmark producers that do not hold live SST handles.
328    #[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    /// Called at logical retirement, before the last snapshot releases the SST.
360    /// Existing readers may finish, but their results must not refill this cache.
361    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    /// Looks up a retained result or registers one reader-owned streaming scan.
386    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    /// Publishes only the current, successful scan of live files. Existing readers
438    /// may still finish an invalidated scan, but it cannot refill the cache.
439    fn complete(&self, load: &RangeLoad, value: Arc<RangeScanCacheValue>) -> bool {
440        // Streaming readers already own their buffer reservations. Cache-only
441        // residency needs no delivery reservation; cache hits acquire one later.
442        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/// Cached result for one range scan.
467#[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
479/// Keeps completed results charged while subscribers or replay streams own them.
480/// Cache entries retain only a weak reference: after the last reader leaves,
481/// the resident value is covered solely by the cache's capacity accounting.
482struct RangeCacheDelivery {
483    value: Arc<RangeScanCacheValue>,
484    _permit: OwnedSemaphorePermit,
485}
486
487impl RangeCacheDelivery {
488    /// Shares a decoded result together with its memory reservation.
489    fn new(value: Arc<RangeScanCacheValue>, permit: OwnedSemaphorePermit) -> Arc<Self> {
490        Arc::new(Self {
491            value,
492            _permit: permit,
493        })
494    }
495
496    /// Borrows the result without detaching it from the delivery reservation.
497    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        // Serialize cache-hit reservations so readers of the same result share
517        // one charge, including when the last previous reader just left.
518        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
534/// Readers own the scan; the coordinator only retains a weak registration.
535/// Query-pool identity isolates active reservations, not completed cache values.
536struct 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    /// File IDs are globally unique. Index by physical file, not query region:
575    /// a copied region can read an SST whose original region differs from its own.
576    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                // Eviction callbacks may arrive after the same key is refilled.
589                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
600/// Returns a stream that replays cached record batches.
601fn cached_flat_range_stream(delivery: Arc<RangeCacheDelivery>) -> BoxedRecordBatchStream {
602    Box::pin(try_stream! {
603        // Borrow through the delivery guard so it survives every yield. Batches
604        // handed to downstream operators are then owned by the query, not by
605        // this completed-result backlog.
606        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        // Arrow's singleton concat aliases its input; a second empty batch lets
687        // the concat kernel copy sliced data without adding a replay boundary.
688        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        // Publication must not reserve the active source's buffer twice.
806        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        // Last-owner release can happen while invalidation holds this lock.
859        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        // Reproduce release arriving after another caller has replaced the dead Weak.
873        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        // Neither an existing scan nor a new request through the old
973        // index handle may refill a range after the current handle is retired.
974        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}