Skip to main content

lance_core/cache/
quick.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright The Lance Authors
3
4//! [`CacheBackend`] backed by [quick_cache](https://crates.io/crates/quick_cache).
5//! A hit takes a shard read lock, clones the cached value, and marks it as
6//! accessed. There is no read-operation channel or inline eviction work.
7
8use super::PriorityEntries;
9use std::pin::Pin;
10use std::sync::Mutex;
11use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
12
13use async_trait::async_trait;
14use futures::Future;
15
16use super::backend::{CacheBackend, CacheEntry};
17use super::moka::key_footprint;
18use super::{CacheCodec, InternalCacheKey};
19use crate::Result;
20use crate::deepsize::Context;
21
22#[derive(Clone)]
23struct QuickEntry {
24    entry: CacheEntry,
25    size_bytes: usize,
26}
27
28#[derive(Clone)]
29struct EntryWeighter;
30
31impl quick_cache::Weighter<InternalCacheKey, QuickEntry> for EntryWeighter {
32    fn weight(&self, key: &InternalCacheKey, value: &QuickEntry) -> u64 {
33        // Same accounting as the moka backend.
34        key_footprint(key).saturating_add(value.size_bytes).max(1) as u64
35    }
36}
37
38pub struct QuickCacheBackend {
39    capacity: usize,
40    generation: AtomicU64,
41    priority_active: AtomicBool,
42    priority: Mutex<PriorityEntries<QuickEntry>>,
43    cache: quick_cache::sync::Cache<InternalCacheKey, QuickEntry, EntryWeighter>,
44}
45
46/// Controls how a [`QuickCacheBackend`] divides its weight budget.
47#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
48pub enum QuickCacheShardPolicy {
49    /// Choose a shard count from the cache capacity and available parallelism.
50    #[default]
51    Recommended,
52    /// Give every entry access to one shared weight budget.
53    ///
54    /// This avoids capacity fragmentation for a small number of large,
55    /// unequal entries. Concurrent hits can still take the shard read lock.
56    Single,
57}
58
59impl std::fmt::Debug for QuickCacheBackend {
60    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61        f.debug_struct("QuickCacheBackend")
62            .field("entry_count", &self.cache.len())
63            .finish()
64    }
65}
66
67/// Minimum weight budget (4 GiB) per shard: shards don't borrow capacity, and
68/// an entry heavier than ~its shard's budget is silently refused admission.
69const MIN_SHARD_SHARE: usize = 4 << 30;
70
71/// Recommended shard count: `min(cpus / 2, capacity / 4 GiB)`, power of two
72/// in `[1, 1024]`. The cpu term bounds lock contention; the capacity term
73/// keeps each shard's budget >= 4 GiB so large entries stay admissible.
74/// Rounded down because quick_cache rounds requests up.
75pub fn recommended_cache_shards(capacity: usize) -> usize {
76    let available_parallelism = std::thread::available_parallelism()
77        .map(|n| n.get())
78        .unwrap_or(2);
79    recommended_cache_shards_for_parallelism(capacity, available_parallelism)
80}
81
82fn recommended_cache_shards_for_parallelism(
83    capacity: usize,
84    available_parallelism: usize,
85) -> usize {
86    let by_cpu = available_parallelism / 2;
87    let shards = (capacity / MIN_SHARD_SHARE).min(by_cpu).max(1);
88    let shards = if shards.is_power_of_two() {
89        shards
90    } else {
91        shards.next_power_of_two() / 2
92    };
93    shards.clamp(1, 1024)
94}
95
96/// Assumed average entry size for pre-allocation sizing.
97const ESTIMATED_AVG_ENTRY_BYTES: usize = 64 << 10;
98
99impl QuickCacheBackend {
100    /// Create a backend holding up to `capacity` bytes of weighted entries
101    /// (weight = key footprint + declared size), sharded per
102    /// [`recommended_cache_shards`].
103    pub fn with_capacity(capacity: usize) -> Self {
104        Self::with_shard_policy(capacity, QuickCacheShardPolicy::Recommended)
105    }
106
107    /// Create a weighted cache with an explicit shard policy.
108    ///
109    /// `capacity` bounds the sum of key footprints and declared entry sizes.
110    /// Each shard has an independent share of that bound. In addition, the
111    /// default Quick admission policy rejects an unpinned entry heavier than
112    /// approximately 97% of one shard's share.
113    ///
114    /// # Example
115    ///
116    /// ```
117    /// use lance_core::cache::{QuickCacheBackend, QuickCacheShardPolicy};
118    ///
119    /// let cache = QuickCacheBackend::with_shard_policy(
120    ///     8 << 30,
121    ///     QuickCacheShardPolicy::Single,
122    /// );
123    /// ```
124    pub fn with_shard_policy(capacity: usize, shard_policy: QuickCacheShardPolicy) -> Self {
125        let shards = match shard_policy {
126            QuickCacheShardPolicy::Recommended => recommended_cache_shards(capacity),
127            QuickCacheShardPolicy::Single => 1,
128        };
129        Self::with_shards(capacity, shards)
130    }
131
132    fn with_shards(capacity: usize, shards: usize) -> Self {
133        // Floor protects the shard count from quick_cache's items-per-shard
134        // heuristic; ceiling bounds pre-allocation.
135        let estimated_items = (capacity / ESTIMATED_AVG_ENTRY_BYTES).clamp(shards * 32, 1_000_000);
136        let options = quick_cache::OptionsBuilder::new()
137            .estimated_items_capacity(estimated_items)
138            .weight_capacity(capacity as u64)
139            .shards(shards)
140            .build()
141            // Only errors when weight/item capacity is missing; both are set.
142            .expect("quick_cache options");
143        let cache = quick_cache::sync::Cache::with_options(
144            options,
145            EntryWeighter,
146            Default::default(),
147            Default::default(),
148        );
149        Self {
150            cache,
151            capacity,
152            generation: AtomicU64::new(0),
153            priority_active: AtomicBool::new(false),
154            priority: Mutex::new(PriorityEntries::default()),
155        }
156    }
157    fn admit_priority(
158        &self,
159        key: InternalCacheKey,
160        item: QuickEntry,
161        priority: u8,
162        generation: u64,
163    ) {
164        self.cache.remove(&key);
165        let dropped = {
166            let mut entries = self.priority.lock().unwrap_or_else(|e| e.into_inner());
167            if self.generation.load(Ordering::Acquire) != generation {
168                return;
169            }
170            let mut dropped = Vec::new();
171            if !self.priority_active.swap(true, Ordering::AcqRel) {
172                // Metadata and existing entries compete with signs, ahead of ex planes.
173                // Reserving the whole budget for planes would otherwise evict the IVF model.
174                let existing: Vec<_> = self.cache.iter().collect();
175                for (key, value) in existing {
176                    let size = key_footprint(&key).saturating_add(value.size_bytes);
177                    dropped.extend(entries.insert(key, value, size, 3, self.capacity));
178                }
179                // Evict resident values without invalidating single-flight
180                // placeholders: their loaders still belong to this generation.
181                self.cache.set_capacity(0);
182            }
183            let size = key_footprint(&key).saturating_add(item.size_bytes);
184            dropped.extend(entries.insert(
185                key,
186                item,
187                size,
188                if priority == 0 { 3 } else { priority },
189                self.capacity,
190            ));
191            dropped
192        };
193        drop(dropped);
194    }
195
196    #[cfg(test)]
197    fn num_shards(&self) -> usize {
198        self.cache.num_shards()
199    }
200
201    #[cfg(test)]
202    fn shard_index(&self, key: &InternalCacheKey) -> usize {
203        self.cache.shard_index(key)
204    }
205}
206
207#[async_trait]
208impl CacheBackend for QuickCacheBackend {
209    async fn get_resident(&self, key: &InternalCacheKey) -> Option<CacheEntry> {
210        self.priority
211            .lock()
212            .unwrap_or_else(|e| e.into_inner())
213            .get(key)
214            .or_else(|| self.cache.get(key))
215            .map(|r| r.entry)
216    }
217
218    /// Membership checks only: priority stamps and quick_cache's reference
219    /// bits stay unchanged, so a probed entry is evicted as if never probed.
220    async fn peek_resident(&self, key: &InternalCacheKey) -> bool {
221        self.priority
222            .lock()
223            .unwrap_or_else(|e| e.into_inner())
224            .contains(key)
225            || self.cache.contains_key(key)
226    }
227
228    async fn get(&self, key: &InternalCacheKey, codec: Option<CacheCodec>) -> Option<CacheEntry> {
229        if (self.priority_active.load(Ordering::Acquire)
230            || codec.is_some_and(|c| c.memory_priority() > 0))
231            && let Some(value) = self
232                .priority
233                .lock()
234                .unwrap_or_else(|e| e.into_inner())
235                .get(key)
236        {
237            return Some(value.entry);
238        }
239        self.cache.get(key).map(|v| v.entry)
240    }
241
242    async fn insert(
243        &self,
244        key: &InternalCacheKey,
245        entry: CacheEntry,
246        size_bytes: usize,
247        codec: Option<CacheCodec>,
248    ) {
249        let priority = codec.map(|c| c.memory_priority()).unwrap_or(0);
250        let item = QuickEntry { entry, size_bytes };
251        if priority > 0 || self.priority_active.load(Ordering::Acquire) {
252            self.admit_priority(
253                *key,
254                item,
255                priority,
256                self.generation.load(Ordering::Acquire),
257            );
258        } else {
259            self.cache.insert(*key, item);
260        }
261    }
262
263    async fn get_or_insert<'a>(
264        &self,
265        key: &InternalCacheKey,
266        loader: Pin<Box<dyn Future<Output = Result<(CacheEntry, usize)>> + Send + 'a>>,
267        codec: Option<CacheCodec>,
268    ) -> Result<(CacheEntry, bool)> {
269        let priority = codec.map(|c| c.memory_priority()).unwrap_or(0);
270        if (priority > 0 || self.priority_active.load(Ordering::Acquire))
271            && let Some(value) = self
272                .priority
273                .lock()
274                .unwrap_or_else(|e| e.into_inner())
275                .get(key)
276        {
277            return Ok((value.entry, true));
278        }
279        let generation = self.generation.load(Ordering::Acquire);
280        match self.cache.get_value_or_guard_async(key).await {
281            Ok(value) => Ok((value.entry, true)),
282            Err(guard) => {
283                let (entry, size_bytes) = loader.await?;
284                let item = QuickEntry {
285                    entry: entry.clone(),
286                    size_bytes,
287                };
288                if guard.insert(item.clone()).is_ok()
289                    && (priority > 0 || self.priority_active.load(Ordering::Acquire))
290                {
291                    self.admit_priority(*key, item, priority, generation);
292                }
293                Ok((entry, false))
294            }
295        }
296    }
297
298    async fn clear(&self) {
299        self.generation.fetch_add(1, Ordering::AcqRel);
300        let dropped = {
301            let mut entries = self.priority.lock().unwrap_or_else(|e| e.into_inner());
302            let dropped = entries.clear();
303            self.priority_active.store(false, Ordering::Release);
304            self.cache.set_capacity(self.capacity as u64);
305            dropped
306        };
307        drop(dropped);
308        self.cache.clear();
309    }
310
311    async fn num_entries(&self) -> usize {
312        self.cache.len()
313            + self
314                .priority
315                .lock()
316                .unwrap_or_else(|e| e.into_inner())
317                .len()
318    }
319
320    async fn size_bytes(&self) -> usize {
321        self.cache.weight() as usize
322            + self
323                .priority
324                .lock()
325                .unwrap_or_else(|e| e.into_inner())
326                .bytes()
327    }
328
329    fn capacity_bytes(&self) -> Option<usize> {
330        Some(self.capacity)
331    }
332
333    fn approx_num_entries(&self) -> usize {
334        self.cache.len()
335            + self
336                .priority
337                .lock()
338                .unwrap_or_else(|e| e.into_inner())
339                .len()
340    }
341
342    fn approx_size_bytes(&self) -> usize {
343        self.cache.weight() as usize
344            + self
345                .priority
346                .lock()
347                .unwrap_or_else(|e| e.into_inner())
348                .bytes()
349    }
350
351    fn deep_size_of_entries(
352        &self,
353        context: &mut Context,
354        size_of_entry: &dyn Fn(&CacheEntry, &mut Context) -> Option<usize>,
355    ) -> Option<usize> {
356        let prioritized: usize = self
357            .priority
358            .lock()
359            .unwrap_or_else(|e| e.into_inner())
360            .snapshot()
361            .into_iter()
362            .map(|(key, _, value)| {
363                key_footprint(&key)
364                    + size_of_entry(&value.entry, context).unwrap_or(value.size_bytes)
365            })
366            .sum();
367        Some(
368            prioritized
369                + self
370                    .cache
371                    .iter()
372                    .map(|(key, record)| {
373                        key_footprint(&key)
374                            + size_of_entry(&record.entry, context).unwrap_or(record.size_bytes)
375                    })
376                    .sum::<usize>(),
377        )
378    }
379}
380
381#[cfg(test)]
382mod tests {
383    use std::marker::PhantomData;
384    use std::sync::Arc;
385    use std::sync::atomic::{AtomicUsize, Ordering};
386
387    use super::*;
388    use crate::cache::{CacheKey, CacheTier, LanceCache};
389
390    const TEST_CAPACITY: usize = 1_000;
391
392    struct TestKey<T: 'static> {
393        key: String,
394        _phantom: PhantomData<T>,
395    }
396
397    impl<T: 'static> TestKey<T> {
398        fn new(key: &str) -> Self {
399            Self {
400                key: key.to_string(),
401                _phantom: PhantomData,
402            }
403        }
404    }
405
406    impl<T: 'static> CacheKey for TestKey<T> {
407        type ValueType = T;
408        fn key(&self) -> std::borrow::Cow<'_, str> {
409            std::borrow::Cow::Borrowed(&self.key)
410        }
411        fn type_name() -> &'static str {
412            std::any::type_name::<T>()
413        }
414    }
415
416    #[tokio::test]
417    async fn priority_transition_preserves_inflight_loads() {
418        let cache = Arc::new(QuickCacheBackend::with_capacity(1024));
419        let key = |id| InternalCacheKey::from_bytes([id; 16]);
420        let codec = CacheCodec::new("test.priority", 1, |_, _| Ok(()), |_| Ok(Arc::new(())))
421            .with_memory_priority(3);
422        let (started_tx, started_rx) = tokio::sync::oneshot::channel();
423        let (release_tx, release_rx) = tokio::sync::oneshot::channel();
424        let loading_cache = cache.clone();
425        let loading = tokio::spawn(async move {
426            loading_cache
427                .get_or_insert(
428                    &key(1),
429                    Box::pin(async move {
430                        started_tx.send(()).unwrap();
431                        release_rx.await.unwrap();
432                        Ok((Arc::new(1u64) as CacheEntry, 32))
433                    }),
434                    Some(codec),
435                )
436                .await
437                .unwrap()
438        });
439        started_rx.await.unwrap();
440        cache.insert(&key(2), Arc::new(2u64), 32, Some(codec)).await;
441        release_tx.send(()).unwrap();
442        loading.await.unwrap();
443        assert!(cache.get_resident(&key(1)).await.is_some());
444        assert!(cache.get_resident(&key(2)).await.is_some());
445    }
446
447    #[rstest::rstest]
448    #[case::recommended(QuickCacheShardPolicy::Recommended)]
449    #[case::single(QuickCacheShardPolicy::Single)]
450    #[tokio::test]
451    async fn priority_budget_retains_metadata_and_singleflight_entries(
452        #[case] shard_policy: QuickCacheShardPolicy,
453    ) {
454        let cache = QuickCacheBackend::with_shard_policy(160, shard_policy);
455        let key = |id| InternalCacheKey::from_bytes([id; 16]);
456        let codec = CacheCodec::new("test.priority", 1, |_, _| Ok(()), |_| Ok(Arc::new(())));
457        cache.insert(&key(0), Arc::new(0u64), 32, None).await;
458        for (id, priority) in [(1, 3), (2, 2), (3, 1)] {
459            cache
460                .insert(
461                    &key(id),
462                    Arc::new(id),
463                    32,
464                    Some(codec.with_memory_priority(priority)),
465                )
466                .await;
467        }
468        assert!(cache.get_resident(&key(0)).await.is_some());
469        assert!(cache.get_resident(&key(1)).await.is_some());
470        assert!(cache.get_resident(&key(2)).await.is_some());
471        assert!(cache.get_resident(&key(3)).await.is_none());
472        // Priority admission disables Quick's ordinary budget, but callers
473        // must still see the configured budget for the active priority tier.
474        assert_eq!(cache.capacity_bytes(), Some(160));
475        let (_, hit) = cache
476            .get_or_insert(
477                &key(4),
478                Box::pin(async { Ok((Arc::new(4u64) as CacheEntry, 32)) }),
479                None,
480            )
481            .await
482            .unwrap();
483        assert!(!hit);
484        let (_, hit) = cache
485            .get_or_insert(
486                &key(4),
487                Box::pin(async { panic!("resident entry reloaded") }),
488                None,
489            )
490            .await
491            .unwrap();
492        assert!(hit);
493        assert!(cache.get_resident(&key(2)).await.is_none());
494        assert!(cache.size_bytes().await <= 160);
495        cache.clear().await;
496        assert_eq!(cache.num_entries().await, 0);
497        assert_eq!(cache.capacity_bytes(), Some(160));
498        cache.insert(&key(0), Arc::new(0u64), 32, None).await;
499        assert!(cache.get_resident(&key(0)).await.is_some());
500    }
501
502    /// Fill a single-shard cache so that `victim` is its only cold entry,
503    /// then peek at its tier or get it, and insert one more entry.
504    async fn cold_victim_after(touch_with_get: bool) -> QuickCacheBackend {
505        const ENTRY_BYTES: usize = 100;
506        const HOT_ENTRIES: u8 = 9;
507        let key = |id| InternalCacheKey::from_bytes([id; 16]);
508        let entry_weight = key_footprint(&key(0)) + ENTRY_BYTES;
509        let capacity = entry_weight * (usize::from(HOT_ENTRIES) + 1);
510        let cache = QuickCacheBackend::with_capacity(capacity);
511        for id in 0..HOT_ENTRIES {
512            cache
513                .insert(&key(id), Arc::new(id), ENTRY_BYTES, None)
514                .await;
515        }
516        let victim = key(HOT_ENTRIES);
517        cache
518            .insert(&victim, Arc::new(0u8), ENTRY_BYTES, None)
519            .await;
520        if touch_with_get {
521            assert!(cache.get_resident(&victim).await.is_some());
522        } else {
523            assert_eq!(cache.peek_tier(&victim).await, CacheTier::Resident);
524        }
525        cache
526            .insert(&key(HOT_ENTRIES + 1), Arc::new(0u8), ENTRY_BYTES, None)
527            .await;
528        cache
529    }
530
531    #[tokio::test]
532    async fn tier_peek_leaves_eviction_order_unchanged() {
533        let victim = InternalCacheKey::from_bytes([9; 16]);
534        let peeked = cold_victim_after(false).await;
535        assert_eq!(peeked.peek_tier(&victim).await, CacheTier::Absent);
536        assert!(!peeked.cache.contains_key(&victim));
537
538        // A real access sets the reference bit, so the same victim survives.
539        let touched = cold_victim_after(true).await;
540        assert_eq!(touched.peek_tier(&victim).await, CacheTier::Resident);
541    }
542
543    /// The priority tier evicts its oldest stamp first; a tier peek leaves
544    /// the stamp alone where a get refreshes it.
545    #[tokio::test]
546    async fn tier_peek_leaves_priority_order_unchanged() {
547        let key = |id| InternalCacheKey::from_bytes([id; 16]);
548        let codec = CacheCodec::new("test.priority", 1, |_, _| Ok(()), |_| Ok(Arc::new(())))
549            .with_memory_priority(1);
550        for touch_with_get in [false, true] {
551            // Room for three 48-byte entries (32 bytes plus the 16-byte key).
552            let cache = QuickCacheBackend::with_capacity(160);
553            for id in 1..=3 {
554                cache
555                    .insert(&key(id), Arc::new(u64::from(id)), 32, Some(codec))
556                    .await;
557            }
558            if touch_with_get {
559                assert!(cache.get_resident(&key(1)).await.is_some());
560            } else {
561                assert_eq!(cache.peek_tier(&key(1)).await, CacheTier::Resident);
562            }
563            cache.insert(&key(4), Arc::new(4u64), 32, Some(codec)).await;
564            let (survivor, evicted) = if touch_with_get {
565                (key(1), key(2))
566            } else {
567                (key(2), key(1))
568            };
569            assert_eq!(
570                cache.peek_tier(&evicted).await,
571                CacheTier::Absent,
572                "get={touch_with_get}"
573            );
574            assert_eq!(
575                cache.peek_tier(&survivor).await,
576                CacheTier::Resident,
577                "get={touch_with_get}"
578            );
579        }
580    }
581
582    #[test]
583    fn entry_weight_includes_fixed_key() {
584        let key = InternalCacheKey::from_bytes([0; 16]);
585        let entry = QuickEntry {
586            entry: Arc::new(()),
587            size_bytes: 7,
588        };
589        assert_eq!(
590            quick_cache::Weighter::weight(&EntryWeighter, &key, &entry),
591            23
592        );
593    }
594
595    #[test]
596    fn capacity_bytes_reports_configured_capacity() {
597        assert_eq!(
598            QuickCacheBackend::with_capacity(1 << 20).capacity_bytes(),
599            Some(1 << 20)
600        );
601    }
602
603    fn keys_in_shard(
604        cache: &QuickCacheBackend,
605        shard_index: usize,
606        count: usize,
607    ) -> Vec<InternalCacheKey> {
608        let mut keys = Vec::with_capacity(count);
609        for value in 0_u128.. {
610            let key = InternalCacheKey::from_bytes(value.to_le_bytes());
611            if cache.shard_index(&key) == shard_index {
612                keys.push(key);
613                if keys.len() == count {
614                    return keys;
615                }
616            }
617        }
618        unreachable!("the key space must contain enough keys for every shard")
619    }
620
621    async fn load_declared_value(
622        cache: &QuickCacheBackend,
623        key: &InternalCacheKey,
624        value: usize,
625        size_bytes: usize,
626        loads: &AtomicUsize,
627    ) -> CacheEntry {
628        let (entry, _) = cache
629            .get_or_insert(
630                key,
631                Box::pin(async {
632                    loads.fetch_add(1, Ordering::SeqCst);
633                    Ok((Arc::new(value) as CacheEntry, size_bytes))
634                }),
635                None,
636            )
637            .await
638            .unwrap();
639        assert_eq!(*entry.downcast_ref::<usize>().unwrap(), value);
640        entry
641    }
642
643    #[test]
644    fn recommended_shards_cover_large_capacity_boundaries() {
645        assert_eq!(recommended_cache_shards_for_parallelism(8 << 30, 4), 2);
646        assert_eq!(
647            recommended_cache_shards_for_parallelism((8 << 30) - 1, 4),
648            1
649        );
650        assert_eq!(recommended_cache_shards_for_parallelism(16 << 30, 8), 4);
651        assert_eq!(
652            recommended_cache_shards_for_parallelism((16 << 30) - 1, 8),
653            2
654        );
655    }
656
657    #[tokio::test]
658    async fn single_shard_avoids_fragmentation_for_unequal_entries() {
659        // Declared entry weights, including the 16-byte key, total 660. They
660        // fit the whole cache but not one 500-byte share of a two-shard cache.
661        let sizes = [168, 188, 256];
662        let sharded = QuickCacheBackend::with_shards(TEST_CAPACITY, 2);
663        assert_eq!(sharded.num_shards(), 2);
664        let sharded_keys = keys_in_shard(&sharded, 0, sizes.len());
665        let sharded_loads = AtomicUsize::new(0);
666        for _ in 0..3 {
667            for (value, (key, size_bytes)) in sharded_keys.iter().zip(sizes).enumerate() {
668                load_declared_value(&sharded, key, value, size_bytes, &sharded_loads).await;
669            }
670        }
671        assert!(sharded_loads.load(Ordering::SeqCst) > sizes.len());
672        assert!(sharded.num_entries().await < sizes.len());
673        assert!(sharded.size_bytes().await <= TEST_CAPACITY);
674
675        let single =
676            QuickCacheBackend::with_shard_policy(TEST_CAPACITY, QuickCacheShardPolicy::Single);
677        let single_loads = AtomicUsize::new(0);
678        for _ in 0..3 {
679            for (value, (key, size_bytes)) in sharded_keys.iter().zip(sizes).enumerate() {
680                load_declared_value(&single, key, value, size_bytes, &single_loads).await;
681            }
682        }
683        assert_eq!(single_loads.load(Ordering::SeqCst), sizes.len());
684        assert_eq!(single.num_entries().await, sizes.len());
685        assert_eq!(single.size_bytes().await, 660);
686    }
687
688    #[tokio::test]
689    async fn direct_insert_obeys_the_same_shard_budget() {
690        let sizes = [168, 188, 256];
691        for (shards, expected_entries) in [(2, 2), (1, 3)] {
692            let cache = QuickCacheBackend::with_shards(TEST_CAPACITY, shards);
693            let keys = keys_in_shard(&cache, 0, sizes.len());
694            for (value, (key, size_bytes)) in keys.iter().zip(sizes).enumerate() {
695                cache.insert(key, Arc::new(value), size_bytes, None).await;
696            }
697            assert_eq!(cache.num_entries().await, expected_entries);
698            assert!(cache.size_bytes().await <= TEST_CAPACITY);
699        }
700    }
701
702    #[tokio::test]
703    async fn single_shard_preserves_capacity_and_hot_admission_limits() {
704        let cache = QuickCacheBackend::with_shards(TEST_CAPACITY, 1);
705        let keys = keys_in_shard(&cache, 0, 4);
706
707        // The default hot target is 97% of capacity. Weight includes the key,
708        // so a declared size of 954 is admitted at weight 970, while 955 is not.
709        cache.insert(&keys[0], Arc::new(0_usize), 954, None).await;
710        assert!(cache.get(&keys[0], None).await.is_some());
711        cache.clear().await;
712        cache.insert(&keys[1], Arc::new(1_usize), 955, None).await;
713        assert!(cache.get(&keys[1], None).await.is_none());
714
715        // Individually admissible entries whose working set exceeds the total
716        // budget must still reload and remain bounded.
717        let loads = AtomicUsize::new(0);
718        for _ in 0..2 {
719            for (value, key) in keys[1..].iter().enumerate() {
720                load_declared_value(&cache, key, value, 384, &loads).await;
721            }
722        }
723        assert!(loads.load(Ordering::SeqCst) > 3);
724        assert!(cache.size_bytes().await <= TEST_CAPACITY);
725    }
726
727    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
728    async fn concurrent_misses_share_a_load_and_retry_failure() {
729        let cache = Arc::new(QuickCacheBackend::with_shards(4096, 1));
730        let key = InternalCacheKey::from_bytes([42; 16]);
731        let loads = Arc::new(AtomicUsize::new(0));
732        let release = Arc::new(tokio::sync::Notify::new());
733        let (started_tx, started_rx) = tokio::sync::oneshot::channel();
734
735        let owner = {
736            let cache = cache.clone();
737            let loads = loads.clone();
738            let release = release.clone();
739            tokio::spawn(async move {
740                cache
741                    .get_or_insert(
742                        &key,
743                        Box::pin(async move {
744                            loads.fetch_add(1, Ordering::SeqCst);
745                            let _ = started_tx.send(());
746                            release.notified().await;
747                            Err(crate::Error::timeout("test loader failed"))
748                        }),
749                        None,
750                    )
751                    .await
752            })
753        };
754        started_rx.await.unwrap();
755
756        let contender = {
757            let cache = cache.clone();
758            let loads = loads.clone();
759            tokio::spawn(async move {
760                cache
761                    .get_or_insert(
762                        &key,
763                        Box::pin(async move {
764                            loads.fetch_add(1, Ordering::SeqCst);
765                            Ok((Arc::new(7_usize) as CacheEntry, 8))
766                        }),
767                        None,
768                    )
769                    .await
770            })
771        };
772        tokio::task::yield_now().await;
773        assert_eq!(loads.load(Ordering::SeqCst), 1);
774        release.notify_one();
775        assert!(owner.await.unwrap().is_err());
776        let (value, is_hit) = contender.await.unwrap().unwrap();
777        assert_eq!(*value.downcast_ref::<usize>().unwrap(), 7);
778        assert!(!is_hit);
779        assert_eq!(loads.load(Ordering::SeqCst), 2);
780    }
781
782    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
783    async fn cancelled_loader_releases_concurrent_miss() {
784        let cache = Arc::new(QuickCacheBackend::with_shards(4096, 1));
785        let key = InternalCacheKey::from_bytes([24; 16]);
786        let loads = Arc::new(AtomicUsize::new(0));
787        let (started_tx, started_rx) = tokio::sync::oneshot::channel();
788
789        let owner = {
790            let cache = cache.clone();
791            let loads = loads.clone();
792            tokio::spawn(async move {
793                cache
794                    .get_or_insert(
795                        &key,
796                        Box::pin(async move {
797                            loads.fetch_add(1, Ordering::SeqCst);
798                            let _ = started_tx.send(());
799                            std::future::pending::<()>().await;
800                            Ok((Arc::new(1_usize) as CacheEntry, 8))
801                        }),
802                        None,
803                    )
804                    .await
805            })
806        };
807        started_rx.await.unwrap();
808
809        let contender = {
810            let cache = cache.clone();
811            let loads = loads.clone();
812            tokio::spawn(async move {
813                cache
814                    .get_or_insert(
815                        &key,
816                        Box::pin(async move {
817                            loads.fetch_add(1, Ordering::SeqCst);
818                            Ok((Arc::new(2_usize) as CacheEntry, 8))
819                        }),
820                        None,
821                    )
822                    .await
823            })
824        };
825        tokio::task::yield_now().await;
826        assert_eq!(loads.load(Ordering::SeqCst), 1);
827
828        owner.abort();
829        assert!(owner.await.unwrap_err().is_cancelled());
830        let (value, is_hit) = tokio::time::timeout(std::time::Duration::from_secs(1), contender)
831            .await
832            .expect("contender remained blocked after loader cancellation")
833            .unwrap()
834            .unwrap();
835        assert_eq!(*value.downcast_ref::<usize>().unwrap(), 2);
836        assert!(!is_hit);
837        assert_eq!(loads.load(Ordering::SeqCst), 2);
838    }
839
840    #[tokio::test]
841    async fn test_quick_backend_roundtrip_singleflight_and_eviction() {
842        // Capacity must be large relative to one entry: quick_cache shards
843        // its weight budget, and an entry heavier than its shard's share is
844        // not admitted at all.
845        const CAPACITY: usize = 1 << 20;
846        let item = Arc::new(vec![1u8, 2, 3]);
847        let cache = LanceCache::with_backend(Arc::new(QuickCacheBackend::with_capacity(CAPACITY)));
848
849        // insert + get roundtrip and weighted accounting
850        cache
851            .insert_with_key(&TestKey::<Vec<u8>>::new("a"), item.clone())
852            .await;
853        assert_eq!(
854            cache
855                .get_with_key(&TestKey::<Vec<u8>>::new("a"))
856                .await
857                .as_deref(),
858            Some(&vec![1u8, 2, 3])
859        );
860        assert_eq!(cache.approx_size(), 1);
861        assert!(cache.size_bytes().await > 0);
862
863        // get_or_insert runs the loader only on a miss
864        let loads = Arc::new(AtomicUsize::new(0));
865        for _ in 0..2 {
866            let loads = loads.clone();
867            let value = cache
868                .get_or_insert_with_key(TestKey::<Vec<u8>>::new("b"), || async move {
869                    loads.fetch_add(1, Ordering::SeqCst);
870                    Ok(vec![7u8])
871                })
872                .await
873                .unwrap();
874            assert_eq!(value.as_ref(), &vec![7u8]);
875        }
876        assert_eq!(loads.load(Ordering::SeqCst), 1);
877
878        // capacity is enforced: overfill with 4x capacity of 16KiB entries
879        // and confirm eviction kept the weighted size within budget
880        for i in 0..256 {
881            cache
882                .insert_with_key(
883                    &TestKey::<Vec<u8>>::new(&format!("fill-{i}")),
884                    Arc::new(vec![0u8; 16 << 10]),
885                )
886                .await;
887        }
888        assert!(cache.size_bytes().await <= CAPACITY);
889        assert!(cache.size().await < 258);
890
891        cache.clear().await;
892        assert_eq!(cache.size().await, 0);
893    }
894
895    #[tokio::test]
896    async fn test_quick_backend_tiny_capacity() {
897        // A tiny cache must not over-provision item metadata and must still
898        // admit and evict correctly within its weight budget.
899        const CAPACITY: usize = 64 << 10;
900        let cache = LanceCache::with_backend(Arc::new(QuickCacheBackend::with_capacity(CAPACITY)));
901        for i in 0..64 {
902            cache
903                .insert_with_key(
904                    &TestKey::<Vec<u8>>::new(&format!("k-{i}")),
905                    Arc::new(vec![0u8; 4 << 10]),
906                )
907                .await;
908        }
909        assert!(cache.size_bytes().await <= CAPACITY);
910        assert!(cache.size().await >= 1);
911        let hit = cache
912            .get_with_key(&TestKey::<Vec<u8>>::new("k-63"))
913            .await
914            .is_some()
915            || cache
916                .get_with_key(&TestKey::<Vec<u8>>::new("k-62"))
917                .await
918                .is_some();
919        assert!(hit, "recently inserted entries should be resident");
920    }
921}