1use 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 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#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
48pub enum QuickCacheShardPolicy {
49 #[default]
51 Recommended,
52 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
67const MIN_SHARD_SHARE: usize = 4 << 30;
70
71pub 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
96const ESTIMATED_AVG_ENTRY_BYTES: usize = 64 << 10;
98
99impl QuickCacheBackend {
100 pub fn with_capacity(capacity: usize) -> Self {
104 Self::with_shard_policy(capacity, QuickCacheShardPolicy::Recommended)
105 }
106
107 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 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 .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 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 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 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 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 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 let touched = cold_victim_after(true).await;
540 assert_eq!(touched.peek_tier(&victim).await, CacheTier::Resident);
541 }
542
543 #[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 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 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 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 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 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 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 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 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 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}