1use super::{CacheEntryKey, CacheEntryKeyRef, EvictionManager};
25#[cfg(test)]
26use crate::key::CompactCacheKey;
27
28use async_trait::async_trait;
29use pingora_core::server::ShutdownWatch;
30use pingora_error::{ErrorType::*, OrErr, Result};
31use pingora_lru::{
32 async_lru::{hash_key, AsyncEvictionCallback, AsyncLru},
33 persistence,
34};
35use serde::de::SeqAccess;
36
37use std::sync::Arc;
38use std::time::SystemTime;
39
40pub struct Manager<const N: usize> {
51 lru: Arc<AsyncLru<CacheEntryKey, N>>,
52}
53
54impl<const N: usize> Manager<N> {
55 pub fn from_lru(lru: Arc<AsyncLru<CacheEntryKey, N>>) -> Self {
57 Manager { lru }
58 }
59}
60
61pub struct ManagerBuilder<C, const N: usize> {
75 weight_limit: usize,
76 eviction_cb: Arc<C>,
77 shutdown: ShutdownWatch,
78 runtime: tokio::runtime::Handle,
79 capacity: usize,
80 len_watermark: Option<usize>,
81 num_eviction_workers: usize,
82}
83
84impl<C, const N: usize> ManagerBuilder<C, N>
85where
86 C: AsyncEvictionCallback<CacheEntryKey>,
87{
88 pub fn capacity(mut self, capacity: usize) -> Self {
90 self.capacity = capacity;
91 self
92 }
93
94 pub fn len_watermark(mut self, watermark: usize) -> Self {
96 self.len_watermark = Some(watermark);
97 self
98 }
99
100 pub fn num_eviction_workers(mut self, n: usize) -> Self {
103 self.num_eviction_workers = n;
104 self
105 }
106
107 pub fn build(self) -> Manager<N> {
111 let mut lru_builder = AsyncLru::builder(
112 self.weight_limit,
113 self.eviction_cb,
114 self.shutdown,
115 self.runtime,
116 )
117 .capacity(self.capacity)
118 .num_eviction_workers(self.num_eviction_workers);
119 if let Some(w) = self.len_watermark {
120 lru_builder = lru_builder.len_watermark(w);
121 }
122 Manager {
123 lru: Arc::new(lru_builder.build()),
124 }
125 }
126}
127
128impl<const N: usize> Manager<N> {
129 pub fn builder<C>(
140 weight_limit: usize,
141 eviction_cb: Arc<C>,
142 shutdown: ShutdownWatch,
143 runtime: tokio::runtime::Handle,
144 ) -> ManagerBuilder<C, N>
145 where
146 C: AsyncEvictionCallback<CacheEntryKey>,
147 {
148 ManagerBuilder {
149 weight_limit,
150 eviction_cb,
151 shutdown,
152 runtime,
153 capacity: 0,
154 len_watermark: None,
155 num_eviction_workers: N.max(1),
156 }
157 }
158
159 pub fn shards(&self) -> usize {
161 self.lru.shards()
162 }
163
164 pub async fn shard_weight(&self, shard: usize) -> Option<usize> {
167 self.lru.shard_weight(shard).await
168 }
169
170 pub fn shard_len(&self, shard: usize) -> usize {
173 self.lru.shard_len(shard)
174 }
175
176 pub fn get_shard_for_key(&self, key: &CacheEntryKey) -> usize {
179 (hash_key(key) % N as u64) as usize
180 }
181
182 pub async fn peek_lru(&self, shard: usize) -> Option<CacheEntryKey> {
186 self.lru.peek_lru(shard).await.map(|(key, _weight)| key)
187 }
188
189 pub fn peek_weight(&self, item: &CacheEntryKey) -> Option<usize> {
192 self.lru.peek_weight(item)
193 }
194
195 pub async fn serialize_shard(&self, shard: usize) -> Result<Vec<u8>> {
200 use rmp_serde::encode::Serializer;
201 use serde::ser::SerializeSeq;
202 use serde::ser::Serializer as _;
203
204 let items = self.lru.snapshot_shard(shard).await.unwrap_or_default();
205 let mut ser = Serializer::new(vec![]);
206 let mut seq = ser
207 .serialize_seq(Some(items.len()))
208 .or_err(InternalError, "fail to serialize node")?;
209 for item in &items {
210 seq.serialize_element(item)
211 .or_err(InternalError, "when serializing LRU element")?;
212 }
213 seq.end().or_err(InternalError, "when serializing LRU")?;
214 Ok(ser.into_inner())
215 }
216
217 fn deserialize_shard(buf: &[u8]) -> Result<Vec<(CacheEntryKey, usize)>> {
219 use rmp_serde::decode::Deserializer;
220 use serde::de::Deserializer as _;
221
222 let mut de = Deserializer::new(buf);
223 let visitor = CollectItems;
224 de.deserialize_seq(visitor)
225 .or_err(InternalError, "when deserializing async LRU")
226 }
227
228 fn load_shard(&self, items: Vec<(CacheEntryKey, usize)>) {
230 for (key, weight) in items {
231 self.lru.insert_tail(key, weight);
232 }
233 }
234}
235
236struct CollectItems;
237
238impl<'de> serde::de::Visitor<'de> for CollectItems {
239 type Value = Vec<(CacheEntryKey, usize)>;
240
241 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
242 formatter.write_str("array of (key, weight) tuples")
243 }
244
245 fn visit_seq<A>(self, mut seq: A) -> std::result::Result<Self::Value, A::Error>
246 where
247 A: SeqAccess<'de>,
248 {
249 let mut items = Vec::with_capacity(seq.size_hint().unwrap_or(0));
250 while let Some(item) = seq.next_element::<(CacheEntryKey, usize)>()? {
251 items.push(item);
252 }
253 Ok(items)
254 }
255}
256
257const FILE_NAME: &str = "lru.data";
258
259#[async_trait]
260impl<const N: usize> EvictionManager for Manager<N> {
261 fn total_size(&self) -> usize {
263 self.lru.weight()
264 }
265 fn total_items(&self) -> usize {
267 self.lru.len()
268 }
269 fn evicted_size(&self) -> usize {
271 self.lru.evicted_weight()
272 }
273 fn evicted_items(&self) -> usize {
275 self.lru.evicted_len()
276 }
277
278 fn admit(
283 &self,
284 item: CacheEntryKey,
285 size: usize,
286 _fresh_until: SystemTime,
287 ) -> Vec<CacheEntryKey> {
288 self.lru.admit(item, size);
289 vec![]
290 }
291
292 fn increment_weight(
297 &self,
298 item: &CacheEntryKey,
299 delta: usize,
300 max_weight: Option<usize>,
301 ) -> Vec<CacheEntryKey> {
302 self.lru.increment_weight(item, delta, max_weight);
303 vec![]
304 }
305
306 fn remove(&self, item: CacheEntryKeyRef<'_>) {
310 self.lru.remove_by_hash(hash_key(&item));
311 }
312
313 fn access(&self, item: &CacheEntryKey, size: usize, _fresh_until: SystemTime) -> bool {
318 if self.lru.promote(item) {
319 true
320 } else {
321 self.lru.admit(item.clone(), size);
322 false
323 }
324 }
325
326 fn peek(&self, item: &CacheEntryKey) -> bool {
329 self.lru.peek(item)
330 }
331
332 async fn save(&self, dir_path: &str) -> Result<()> {
337 persistence::save_shards_async(dir_path, FILE_NAME, N, |i| self.serialize_shard(i))
338 .await
339 .or_err(InternalError, "failed to save async LRU")?;
340 Ok(())
341 }
342
343 async fn load(&self, dir_path: &str) -> Result<()> {
348 persistence::load_shards(dir_path, FILE_NAME, N, |_i, data| {
349 Self::deserialize_shard(data).map(|items| self.load_shard(items))
350 })
351 .await;
352 Ok(())
353 }
354}
355
356#[cfg(test)]
357impl<const N: usize> Manager<N> {
358 fn increment_weight(
359 &self,
360 item: &CompactCacheKey,
361 delta: usize,
362 max_weight: Option<usize>,
363 ) -> Vec<CompactCacheKey> {
364 EvictionManager::increment_weight(
365 self,
366 &CacheEntryKey::key_only(item.clone()),
367 delta,
368 max_weight,
369 )
370 .into_iter()
371 .map(CacheEntryKey::into_key)
372 .collect()
373 }
374}
375
376#[cfg(test)]
377mod tests {
378 use super::*;
379 use crate::key::CacheKey;
380
381 #[tokio::test(flavor = "multi_thread")]
382 async fn increment_weight_admits_missing_key() {
383 let (_shutdown_tx, shutdown_rx) = tokio::sync::watch::channel(false);
384 let manager = Manager::<1>::builder(
385 100,
386 Arc::new(|_key: CacheEntryKey, _weight| async {}),
387 shutdown_rx,
388 tokio::runtime::Handle::current(),
389 )
390 .build();
391 let key = CacheKey::new("missing", "1").to_compact();
392
393 manager.increment_weight(&key, 7, Some(10));
394 manager.shard_weight(0).await;
395
396 assert_eq!(manager.peek_weight(&CacheEntryKey::key_only(key)), Some(7));
397 assert_eq!(manager.total_items(), 1);
398 }
399
400 #[tokio::test(flavor = "multi_thread")]
401 async fn remove_requires_complete_identity() {
402 let (_shutdown_tx, shutdown_rx) = tokio::sync::watch::channel(false);
403 let manager = Manager::<1>::builder(
404 100,
405 Arc::new(|_key: CacheEntryKey, _weight| async {}),
406 shutdown_rx,
407 tokio::runtime::Handle::current(),
408 )
409 .build();
410 let key = CacheKey::new("identified", "1").to_compact();
411 let id = crate::eviction::CacheEntryId::new(42);
412
413 let _ = EvictionManager::admit(
414 &manager,
415 CacheEntryKey::identified(key.clone(), id),
416 1,
417 SystemTime::now(),
418 );
419 manager.shard_weight(0).await;
421
422 EvictionManager::remove(&manager, CacheEntryKeyRef::from_entry_id(&key, None));
423 manager.shard_weight(0).await;
424 assert_eq!(manager.total_size(), 1);
425
426 EvictionManager::remove(&manager, CacheEntryKeyRef::from_entry_id(&key, Some(id)));
427 manager.shard_weight(0).await;
428 assert_eq!(manager.total_size(), 0);
429 }
430
431 #[tokio::test(flavor = "multi_thread")]
432 async fn save_returns_error_when_all_shards_fail() {
433 let dir = std::env::temp_dir().join(format!(
434 "pingora-async-lru-total-save-failure-{}-{}",
435 std::process::id(),
436 rand::random::<u32>()
437 ));
438 std::fs::create_dir_all(&dir).unwrap();
439 for shard in 0..2 {
440 std::fs::create_dir(dir.join(format!("{FILE_NAME}.{shard}"))).unwrap();
442 }
443
444 let (_shutdown_tx, shutdown_rx) = tokio::sync::watch::channel(false);
445 let manager = Manager::<2>::builder(
446 100,
447 Arc::new(|_key: CacheEntryKey, _weight| async {}),
448 shutdown_rx,
449 tokio::runtime::Handle::current(),
450 )
451 .build();
452
453 let error = manager.save(dir.to_str().unwrap()).await.unwrap_err();
454 assert!(
455 error.to_string().contains("failed to save async LRU"),
456 "unexpected error: {error}"
457 );
458
459 std::fs::remove_dir_all(dir).unwrap();
460 }
461}