lance_core/cache/
quick.rs1use std::pin::Pin;
10
11use async_trait::async_trait;
12use futures::Future;
13
14use super::backend::{CacheBackend, CacheEntry};
15use super::moka::key_footprint;
16use super::{CacheCodec, InternalCacheKey};
17use crate::Result;
18use crate::deepsize::Context;
19
20#[derive(Clone)]
21struct QuickEntry {
22 entry: CacheEntry,
23 size_bytes: usize,
24}
25
26#[derive(Clone)]
27struct EntryWeighter;
28
29impl quick_cache::Weighter<InternalCacheKey, QuickEntry> for EntryWeighter {
30 fn weight(&self, key: &InternalCacheKey, value: &QuickEntry) -> u64 {
31 key_footprint(key).saturating_add(value.size_bytes).max(1) as u64
33 }
34}
35
36pub struct QuickCacheBackend {
37 cache: quick_cache::sync::Cache<InternalCacheKey, QuickEntry, EntryWeighter>,
38}
39
40impl std::fmt::Debug for QuickCacheBackend {
41 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
42 f.debug_struct("QuickCacheBackend")
43 .field("entry_count", &self.cache.len())
44 .finish()
45 }
46}
47
48const MIN_SHARD_SHARE: usize = 4 << 30;
51
52pub fn recommended_cache_shards(capacity: usize) -> usize {
57 let by_cpu = std::thread::available_parallelism()
58 .map(|n| n.get())
59 .unwrap_or(2)
60 / 2;
61 let shards = (capacity / MIN_SHARD_SHARE).min(by_cpu).max(1);
62 let shards = if shards.is_power_of_two() {
63 shards
64 } else {
65 shards.next_power_of_two() / 2
66 };
67 shards.clamp(1, 1024)
68}
69
70const ESTIMATED_AVG_ENTRY_BYTES: usize = 64 << 10;
72
73impl QuickCacheBackend {
74 pub fn with_capacity(capacity: usize) -> Self {
78 let shards = recommended_cache_shards(capacity);
79 let estimated_items = (capacity / ESTIMATED_AVG_ENTRY_BYTES).clamp(shards * 32, 1_000_000);
82 let options = quick_cache::OptionsBuilder::new()
83 .estimated_items_capacity(estimated_items)
84 .weight_capacity(capacity as u64)
85 .shards(shards)
86 .build()
87 .expect("quick_cache options");
89 let cache = quick_cache::sync::Cache::with_options(
90 options,
91 EntryWeighter,
92 Default::default(),
93 Default::default(),
94 );
95 Self { cache }
96 }
97}
98
99#[async_trait]
100impl CacheBackend for QuickCacheBackend {
101 async fn get(&self, key: &InternalCacheKey, _codec: Option<CacheCodec>) -> Option<CacheEntry> {
102 self.cache.get(key).map(|v| v.entry)
103 }
104
105 async fn insert(
106 &self,
107 key: &InternalCacheKey,
108 entry: CacheEntry,
109 size_bytes: usize,
110 _codec: Option<CacheCodec>,
111 ) {
112 self.cache.insert(*key, QuickEntry { entry, size_bytes });
113 }
114
115 async fn get_or_insert<'a>(
116 &self,
117 key: &InternalCacheKey,
118 loader: Pin<Box<dyn Future<Output = Result<(CacheEntry, usize)>> + Send + 'a>>,
119 _codec: Option<CacheCodec>,
120 ) -> Result<(CacheEntry, bool)> {
121 match self.cache.get_value_or_guard_async(key).await {
122 Ok(value) => Ok((value.entry, true)),
123 Err(guard) => {
124 let (entry, size_bytes) = loader.await?;
125 let _ = guard.insert(QuickEntry {
126 entry: entry.clone(),
127 size_bytes,
128 });
129 Ok((entry, false))
130 }
131 }
132 }
133
134 async fn clear(&self) {
135 self.cache.clear();
136 }
137
138 async fn num_entries(&self) -> usize {
139 self.cache.len()
140 }
141
142 async fn size_bytes(&self) -> usize {
143 self.cache.weight() as usize
144 }
145
146 fn approx_num_entries(&self) -> usize {
147 self.cache.len()
148 }
149
150 fn approx_size_bytes(&self) -> usize {
151 self.cache.weight() as usize
152 }
153
154 fn deep_size_of_entries(
155 &self,
156 context: &mut Context,
157 size_of_entry: &dyn Fn(&CacheEntry, &mut Context) -> Option<usize>,
158 ) -> Option<usize> {
159 Some(
160 self.cache
161 .iter()
162 .map(|(key, record)| {
163 key_footprint(&key)
164 + size_of_entry(&record.entry, context).unwrap_or(record.size_bytes)
165 })
166 .sum(),
167 )
168 }
169}
170
171#[cfg(test)]
172mod tests {
173 use std::marker::PhantomData;
174 use std::sync::Arc;
175 use std::sync::atomic::{AtomicUsize, Ordering};
176
177 use super::*;
178 use crate::cache::{CacheKey, LanceCache};
179
180 struct TestKey<T: 'static> {
181 key: String,
182 _phantom: PhantomData<T>,
183 }
184
185 impl<T: 'static> TestKey<T> {
186 fn new(key: &str) -> Self {
187 Self {
188 key: key.to_string(),
189 _phantom: PhantomData,
190 }
191 }
192 }
193
194 impl<T: 'static> CacheKey for TestKey<T> {
195 type ValueType = T;
196 fn key(&self) -> std::borrow::Cow<'_, str> {
197 std::borrow::Cow::Borrowed(&self.key)
198 }
199 fn type_name() -> &'static str {
200 std::any::type_name::<T>()
201 }
202 }
203
204 #[test]
205 fn entry_weight_includes_fixed_key() {
206 let key = InternalCacheKey::from_bytes([0; 16]);
207 let entry = QuickEntry {
208 entry: Arc::new(()),
209 size_bytes: 7,
210 };
211 assert_eq!(
212 quick_cache::Weighter::weight(&EntryWeighter, &key, &entry),
213 23
214 );
215 }
216
217 #[tokio::test]
218 async fn test_quick_backend_roundtrip_singleflight_and_eviction() {
219 const CAPACITY: usize = 1 << 20;
223 let item = Arc::new(vec![1u8, 2, 3]);
224 let cache = LanceCache::with_backend(Arc::new(QuickCacheBackend::with_capacity(CAPACITY)));
225
226 cache
228 .insert_with_key(&TestKey::<Vec<u8>>::new("a"), item.clone())
229 .await;
230 assert_eq!(
231 cache
232 .get_with_key(&TestKey::<Vec<u8>>::new("a"))
233 .await
234 .as_deref(),
235 Some(&vec![1u8, 2, 3])
236 );
237 assert_eq!(cache.approx_size(), 1);
238 assert!(cache.size_bytes().await > 0);
239
240 let loads = Arc::new(AtomicUsize::new(0));
242 for _ in 0..2 {
243 let loads = loads.clone();
244 let value = cache
245 .get_or_insert_with_key(TestKey::<Vec<u8>>::new("b"), || async move {
246 loads.fetch_add(1, Ordering::SeqCst);
247 Ok(vec![7u8])
248 })
249 .await
250 .unwrap();
251 assert_eq!(value.as_ref(), &vec![7u8]);
252 }
253 assert_eq!(loads.load(Ordering::SeqCst), 1);
254
255 for i in 0..256 {
258 cache
259 .insert_with_key(
260 &TestKey::<Vec<u8>>::new(&format!("fill-{i}")),
261 Arc::new(vec![0u8; 16 << 10]),
262 )
263 .await;
264 }
265 assert!(cache.size_bytes().await <= CAPACITY);
266 assert!(cache.size().await < 258);
267
268 cache.clear().await;
269 assert_eq!(cache.size().await, 0);
270 }
271
272 #[tokio::test]
273 async fn test_quick_backend_tiny_capacity() {
274 const CAPACITY: usize = 64 << 10;
277 let cache = LanceCache::with_backend(Arc::new(QuickCacheBackend::with_capacity(CAPACITY)));
278 for i in 0..64 {
279 cache
280 .insert_with_key(
281 &TestKey::<Vec<u8>>::new(&format!("k-{i}")),
282 Arc::new(vec![0u8; 4 << 10]),
283 )
284 .await;
285 }
286 assert!(cache.size_bytes().await <= CAPACITY);
287 assert!(cache.size().await >= 1);
288 let hit = cache
289 .get_with_key(&TestKey::<Vec<u8>>::new("k-63"))
290 .await
291 .is_some()
292 || cache
293 .get_with_key(&TestKey::<Vec<u8>>::new("k-62"))
294 .await
295 .is_some();
296 assert!(hit, "recently inserted entries should be resident");
297 }
298}