1use std::pin::Pin;
9
10use async_trait::async_trait;
11use futures::Future;
12
13use super::backend::{CacheBackend, CacheEntry};
14use super::moka::key_footprint;
15use super::{CacheCodec, InternalCacheKey};
16use crate::Result;
17use crate::deepsize::Context;
18
19#[derive(Clone)]
20struct QuickEntry {
21 entry: CacheEntry,
22 size_bytes: usize,
23}
24
25#[derive(Clone)]
26struct EntryWeighter;
27
28impl quick_cache::Weighter<InternalCacheKey, QuickEntry> for EntryWeighter {
29 fn weight(&self, key: &InternalCacheKey, value: &QuickEntry) -> u64 {
30 key_footprint(key).saturating_add(value.size_bytes).max(1) as u64
32 }
33}
34
35pub struct QuickCacheBackend {
36 cache: quick_cache::sync::Cache<InternalCacheKey, QuickEntry, EntryWeighter>,
37 capacity: usize,
38}
39
40#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
42pub enum QuickCacheShardPolicy {
43 #[default]
45 Recommended,
46 Single,
51}
52
53impl std::fmt::Debug for QuickCacheBackend {
54 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55 f.debug_struct("QuickCacheBackend")
56 .field("entry_count", &self.cache.len())
57 .finish()
58 }
59}
60
61const MIN_SHARD_SHARE: usize = 4 << 30;
64
65pub fn recommended_cache_shards(capacity: usize) -> usize {
70 let available_parallelism = std::thread::available_parallelism()
71 .map(|n| n.get())
72 .unwrap_or(2);
73 recommended_cache_shards_for_parallelism(capacity, available_parallelism)
74}
75
76fn recommended_cache_shards_for_parallelism(
77 capacity: usize,
78 available_parallelism: usize,
79) -> usize {
80 let by_cpu = available_parallelism / 2;
81 let shards = (capacity / MIN_SHARD_SHARE).min(by_cpu).max(1);
82 let shards = if shards.is_power_of_two() {
83 shards
84 } else {
85 shards.next_power_of_two() / 2
86 };
87 shards.clamp(1, 1024)
88}
89
90const ESTIMATED_AVG_ENTRY_BYTES: usize = 64 << 10;
92
93impl QuickCacheBackend {
94 pub fn with_capacity(capacity: usize) -> Self {
98 Self::with_shard_policy(capacity, QuickCacheShardPolicy::Recommended)
99 }
100
101 pub fn with_shard_policy(capacity: usize, shard_policy: QuickCacheShardPolicy) -> Self {
119 let shards = match shard_policy {
120 QuickCacheShardPolicy::Recommended => recommended_cache_shards(capacity),
121 QuickCacheShardPolicy::Single => 1,
122 };
123 Self::with_shards(capacity, shards)
124 }
125
126 fn with_shards(capacity: usize, shards: usize) -> Self {
127 let estimated_items = (capacity / ESTIMATED_AVG_ENTRY_BYTES).clamp(shards * 32, 1_000_000);
130 let options = quick_cache::OptionsBuilder::new()
131 .estimated_items_capacity(estimated_items)
132 .weight_capacity(capacity as u64)
133 .shards(shards)
134 .build()
135 .expect("quick_cache options");
137 let cache = quick_cache::sync::Cache::with_options(
138 options,
139 EntryWeighter,
140 Default::default(),
141 Default::default(),
142 );
143 Self { cache, capacity }
144 }
145
146 #[cfg(test)]
147 fn num_shards(&self) -> usize {
148 self.cache.num_shards()
149 }
150
151 #[cfg(test)]
152 fn shard_index(&self, key: &InternalCacheKey) -> usize {
153 self.cache.shard_index(key)
154 }
155}
156
157#[async_trait]
158impl CacheBackend for QuickCacheBackend {
159 async fn get(&self, key: &InternalCacheKey, _codec: Option<CacheCodec>) -> Option<CacheEntry> {
160 self.cache.get(key).map(|v| v.entry)
161 }
162
163 async fn insert(
164 &self,
165 key: &InternalCacheKey,
166 entry: CacheEntry,
167 size_bytes: usize,
168 _codec: Option<CacheCodec>,
169 ) {
170 self.cache.insert(*key, QuickEntry { entry, size_bytes });
171 }
172
173 async fn get_or_insert<'a>(
174 &self,
175 key: &InternalCacheKey,
176 loader: Pin<Box<dyn Future<Output = Result<(CacheEntry, usize)>> + Send + 'a>>,
177 _codec: Option<CacheCodec>,
178 ) -> Result<(CacheEntry, bool)> {
179 match self.cache.get_value_or_guard_async(key).await {
180 Ok(value) => Ok((value.entry, true)),
181 Err(guard) => {
182 let (entry, size_bytes) = loader.await?;
183 let _ = guard.insert(QuickEntry {
184 entry: entry.clone(),
185 size_bytes,
186 });
187 Ok((entry, false))
188 }
189 }
190 }
191
192 async fn clear(&self) {
193 self.cache.clear();
194 }
195
196 async fn num_entries(&self) -> usize {
197 self.cache.len()
198 }
199
200 async fn size_bytes(&self) -> usize {
201 self.cache.weight() as usize
202 }
203
204 fn capacity_bytes(&self) -> Option<usize> {
205 Some(self.capacity)
206 }
207
208 fn approx_num_entries(&self) -> usize {
209 self.cache.len()
210 }
211
212 fn approx_size_bytes(&self) -> usize {
213 self.cache.weight() as usize
214 }
215
216 fn deep_size_of_entries(
217 &self,
218 context: &mut Context,
219 size_of_entry: &dyn Fn(&CacheEntry, &mut Context) -> Option<usize>,
220 ) -> Option<usize> {
221 Some(
222 self.cache
223 .iter()
224 .map(|(key, record)| {
225 key_footprint(&key)
226 + size_of_entry(&record.entry, context).unwrap_or(record.size_bytes)
227 })
228 .sum(),
229 )
230 }
231}
232
233#[cfg(test)]
234mod tests {
235 use std::marker::PhantomData;
236 use std::sync::Arc;
237 use std::sync::atomic::{AtomicUsize, Ordering};
238
239 use super::*;
240 use crate::cache::{CacheKey, LanceCache};
241
242 const TEST_CAPACITY: usize = 1_000;
243
244 struct TestKey<T: 'static> {
245 key: String,
246 _phantom: PhantomData<T>,
247 }
248
249 impl<T: 'static> TestKey<T> {
250 fn new(key: &str) -> Self {
251 Self {
252 key: key.to_string(),
253 _phantom: PhantomData,
254 }
255 }
256 }
257
258 impl<T: 'static> CacheKey for TestKey<T> {
259 type ValueType = T;
260 fn key(&self) -> std::borrow::Cow<'_, str> {
261 std::borrow::Cow::Borrowed(&self.key)
262 }
263 fn type_name() -> &'static str {
264 std::any::type_name::<T>()
265 }
266 }
267
268 #[test]
269 fn entry_weight_includes_fixed_key() {
270 let key = InternalCacheKey::from_bytes([0; 16]);
271 let entry = QuickEntry {
272 entry: Arc::new(()),
273 size_bytes: 7,
274 };
275 assert_eq!(
276 quick_cache::Weighter::weight(&EntryWeighter, &key, &entry),
277 23
278 );
279 }
280
281 #[test]
282 fn capacity_bytes_reports_configured_capacity() {
283 assert_eq!(
284 QuickCacheBackend::with_capacity(1 << 20).capacity_bytes(),
285 Some(1 << 20)
286 );
287 }
288
289 fn keys_in_shard(
290 cache: &QuickCacheBackend,
291 shard_index: usize,
292 count: usize,
293 ) -> Vec<InternalCacheKey> {
294 let mut keys = Vec::with_capacity(count);
295 for value in 0_u128.. {
296 let key = InternalCacheKey::from_bytes(value.to_le_bytes());
297 if cache.shard_index(&key) == shard_index {
298 keys.push(key);
299 if keys.len() == count {
300 return keys;
301 }
302 }
303 }
304 unreachable!("the key space must contain enough keys for every shard")
305 }
306
307 async fn load_declared_value(
308 cache: &QuickCacheBackend,
309 key: &InternalCacheKey,
310 value: usize,
311 size_bytes: usize,
312 loads: &AtomicUsize,
313 ) -> CacheEntry {
314 let (entry, _) = cache
315 .get_or_insert(
316 key,
317 Box::pin(async {
318 loads.fetch_add(1, Ordering::SeqCst);
319 Ok((Arc::new(value) as CacheEntry, size_bytes))
320 }),
321 None,
322 )
323 .await
324 .unwrap();
325 assert_eq!(*entry.downcast_ref::<usize>().unwrap(), value);
326 entry
327 }
328
329 #[test]
330 fn recommended_shards_cover_large_capacity_boundaries() {
331 assert_eq!(recommended_cache_shards_for_parallelism(8 << 30, 4), 2);
332 assert_eq!(
333 recommended_cache_shards_for_parallelism((8 << 30) - 1, 4),
334 1
335 );
336 assert_eq!(recommended_cache_shards_for_parallelism(16 << 30, 8), 4);
337 assert_eq!(
338 recommended_cache_shards_for_parallelism((16 << 30) - 1, 8),
339 2
340 );
341 }
342
343 #[tokio::test]
344 async fn single_shard_avoids_fragmentation_for_unequal_entries() {
345 let sizes = [168, 188, 256];
348 let sharded = QuickCacheBackend::with_shards(TEST_CAPACITY, 2);
349 assert_eq!(sharded.num_shards(), 2);
350 let sharded_keys = keys_in_shard(&sharded, 0, sizes.len());
351 let sharded_loads = AtomicUsize::new(0);
352 for _ in 0..3 {
353 for (value, (key, size_bytes)) in sharded_keys.iter().zip(sizes).enumerate() {
354 load_declared_value(&sharded, key, value, size_bytes, &sharded_loads).await;
355 }
356 }
357 assert!(sharded_loads.load(Ordering::SeqCst) > sizes.len());
358 assert!(sharded.num_entries().await < sizes.len());
359 assert!(sharded.size_bytes().await <= TEST_CAPACITY);
360
361 let single =
362 QuickCacheBackend::with_shard_policy(TEST_CAPACITY, QuickCacheShardPolicy::Single);
363 let single_loads = AtomicUsize::new(0);
364 for _ in 0..3 {
365 for (value, (key, size_bytes)) in sharded_keys.iter().zip(sizes).enumerate() {
366 load_declared_value(&single, key, value, size_bytes, &single_loads).await;
367 }
368 }
369 assert_eq!(single_loads.load(Ordering::SeqCst), sizes.len());
370 assert_eq!(single.num_entries().await, sizes.len());
371 assert_eq!(single.size_bytes().await, 660);
372 }
373
374 #[tokio::test]
375 async fn direct_insert_obeys_the_same_shard_budget() {
376 let sizes = [168, 188, 256];
377 for (shards, expected_entries) in [(2, 2), (1, 3)] {
378 let cache = QuickCacheBackend::with_shards(TEST_CAPACITY, shards);
379 let keys = keys_in_shard(&cache, 0, sizes.len());
380 for (value, (key, size_bytes)) in keys.iter().zip(sizes).enumerate() {
381 cache.insert(key, Arc::new(value), size_bytes, None).await;
382 }
383 assert_eq!(cache.num_entries().await, expected_entries);
384 assert!(cache.size_bytes().await <= TEST_CAPACITY);
385 }
386 }
387
388 #[tokio::test]
389 async fn single_shard_preserves_capacity_and_hot_admission_limits() {
390 let cache = QuickCacheBackend::with_shards(TEST_CAPACITY, 1);
391 let keys = keys_in_shard(&cache, 0, 4);
392
393 cache.insert(&keys[0], Arc::new(0_usize), 954, None).await;
396 assert!(cache.get(&keys[0], None).await.is_some());
397 cache.clear().await;
398 cache.insert(&keys[1], Arc::new(1_usize), 955, None).await;
399 assert!(cache.get(&keys[1], None).await.is_none());
400
401 let loads = AtomicUsize::new(0);
404 for _ in 0..2 {
405 for (value, key) in keys[1..].iter().enumerate() {
406 load_declared_value(&cache, key, value, 384, &loads).await;
407 }
408 }
409 assert!(loads.load(Ordering::SeqCst) > 3);
410 assert!(cache.size_bytes().await <= TEST_CAPACITY);
411 }
412
413 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
414 async fn concurrent_misses_share_a_load_and_retry_failure() {
415 let cache = Arc::new(QuickCacheBackend::with_shards(4096, 1));
416 let key = InternalCacheKey::from_bytes([42; 16]);
417 let loads = Arc::new(AtomicUsize::new(0));
418 let release = Arc::new(tokio::sync::Notify::new());
419 let (started_tx, started_rx) = tokio::sync::oneshot::channel();
420
421 let owner = {
422 let cache = cache.clone();
423 let loads = loads.clone();
424 let release = release.clone();
425 tokio::spawn(async move {
426 cache
427 .get_or_insert(
428 &key,
429 Box::pin(async move {
430 loads.fetch_add(1, Ordering::SeqCst);
431 let _ = started_tx.send(());
432 release.notified().await;
433 Err(crate::Error::timeout("test loader failed"))
434 }),
435 None,
436 )
437 .await
438 })
439 };
440 started_rx.await.unwrap();
441
442 let contender = {
443 let cache = cache.clone();
444 let loads = loads.clone();
445 tokio::spawn(async move {
446 cache
447 .get_or_insert(
448 &key,
449 Box::pin(async move {
450 loads.fetch_add(1, Ordering::SeqCst);
451 Ok((Arc::new(7_usize) as CacheEntry, 8))
452 }),
453 None,
454 )
455 .await
456 })
457 };
458 tokio::task::yield_now().await;
459 assert_eq!(loads.load(Ordering::SeqCst), 1);
460 release.notify_one();
461 assert!(owner.await.unwrap().is_err());
462 let (value, is_hit) = contender.await.unwrap().unwrap();
463 assert_eq!(*value.downcast_ref::<usize>().unwrap(), 7);
464 assert!(!is_hit);
465 assert_eq!(loads.load(Ordering::SeqCst), 2);
466 }
467
468 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
469 async fn cancelled_loader_releases_concurrent_miss() {
470 let cache = Arc::new(QuickCacheBackend::with_shards(4096, 1));
471 let key = InternalCacheKey::from_bytes([24; 16]);
472 let loads = Arc::new(AtomicUsize::new(0));
473 let (started_tx, started_rx) = tokio::sync::oneshot::channel();
474
475 let owner = {
476 let cache = cache.clone();
477 let loads = loads.clone();
478 tokio::spawn(async move {
479 cache
480 .get_or_insert(
481 &key,
482 Box::pin(async move {
483 loads.fetch_add(1, Ordering::SeqCst);
484 let _ = started_tx.send(());
485 std::future::pending::<()>().await;
486 Ok((Arc::new(1_usize) as CacheEntry, 8))
487 }),
488 None,
489 )
490 .await
491 })
492 };
493 started_rx.await.unwrap();
494
495 let contender = {
496 let cache = cache.clone();
497 let loads = loads.clone();
498 tokio::spawn(async move {
499 cache
500 .get_or_insert(
501 &key,
502 Box::pin(async move {
503 loads.fetch_add(1, Ordering::SeqCst);
504 Ok((Arc::new(2_usize) as CacheEntry, 8))
505 }),
506 None,
507 )
508 .await
509 })
510 };
511 tokio::task::yield_now().await;
512 assert_eq!(loads.load(Ordering::SeqCst), 1);
513
514 owner.abort();
515 assert!(owner.await.unwrap_err().is_cancelled());
516 let (value, is_hit) = tokio::time::timeout(std::time::Duration::from_secs(1), contender)
517 .await
518 .expect("contender remained blocked after loader cancellation")
519 .unwrap()
520 .unwrap();
521 assert_eq!(*value.downcast_ref::<usize>().unwrap(), 2);
522 assert!(!is_hit);
523 assert_eq!(loads.load(Ordering::SeqCst), 2);
524 }
525
526 #[tokio::test]
527 async fn test_quick_backend_roundtrip_singleflight_and_eviction() {
528 const CAPACITY: usize = 1 << 20;
532 let item = Arc::new(vec![1u8, 2, 3]);
533 let cache = LanceCache::with_backend(Arc::new(QuickCacheBackend::with_capacity(CAPACITY)));
534
535 cache
537 .insert_with_key(&TestKey::<Vec<u8>>::new("a"), item.clone())
538 .await;
539 assert_eq!(
540 cache
541 .get_with_key(&TestKey::<Vec<u8>>::new("a"))
542 .await
543 .as_deref(),
544 Some(&vec![1u8, 2, 3])
545 );
546 assert_eq!(cache.approx_size(), 1);
547 assert!(cache.size_bytes().await > 0);
548
549 let loads = Arc::new(AtomicUsize::new(0));
551 for _ in 0..2 {
552 let loads = loads.clone();
553 let value = cache
554 .get_or_insert_with_key(TestKey::<Vec<u8>>::new("b"), || async move {
555 loads.fetch_add(1, Ordering::SeqCst);
556 Ok(vec![7u8])
557 })
558 .await
559 .unwrap();
560 assert_eq!(value.as_ref(), &vec![7u8]);
561 }
562 assert_eq!(loads.load(Ordering::SeqCst), 1);
563
564 for i in 0..256 {
567 cache
568 .insert_with_key(
569 &TestKey::<Vec<u8>>::new(&format!("fill-{i}")),
570 Arc::new(vec![0u8; 16 << 10]),
571 )
572 .await;
573 }
574 assert!(cache.size_bytes().await <= CAPACITY);
575 assert!(cache.size().await < 258);
576
577 cache.clear().await;
578 assert_eq!(cache.size().await, 0);
579 }
580
581 #[tokio::test]
582 async fn test_quick_backend_tiny_capacity() {
583 const CAPACITY: usize = 64 << 10;
586 let cache = LanceCache::with_backend(Arc::new(QuickCacheBackend::with_capacity(CAPACITY)));
587 for i in 0..64 {
588 cache
589 .insert_with_key(
590 &TestKey::<Vec<u8>>::new(&format!("k-{i}")),
591 Arc::new(vec![0u8; 4 << 10]),
592 )
593 .await;
594 }
595 assert!(cache.size_bytes().await <= CAPACITY);
596 assert!(cache.size().await >= 1);
597 let hit = cache
598 .get_with_key(&TestKey::<Vec<u8>>::new("k-63"))
599 .await
600 .is_some()
601 || cache
602 .get_with_key(&TestKey::<Vec<u8>>::new("k-62"))
603 .await
604 .is_some();
605 assert!(hit, "recently inserted entries should be resident");
606 }
607}