1use crate::metrics::*;
2use dashmap::DashMap;
3use kumo_prometheus::prometheus::{Histogram, IntCounter, IntGauge};
4use kumo_server_memory::subscribe_to_memory_status_changes_async;
5pub use linkme;
6use parking_lot::Mutex;
7pub use pastey as paste;
8use scopeguard::defer;
9use std::borrow::Borrow;
10use std::collections::HashMap;
11use std::fmt::Debug;
12use std::future::Future;
13use std::hash::Hash;
14use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
15use std::sync::{Arc, LazyLock, Weak};
16use tokio::sync::{OwnedSemaphorePermit, Semaphore};
17use tokio::time::{timeout, timeout_at, Duration, Instant};
18
19mod metrics;
20
21static CACHES: LazyLock<Mutex<Vec<Weak<dyn CachePurger + Send + Sync>>>> =
22 LazyLock::new(Mutex::default);
23
24struct Inner<K: Clone + Hash + Eq + Debug, V: Clone + Send + Sync + Debug> {
25 name: String,
26 tick: AtomicUsize,
27 capacity: AtomicUsize,
28 allow_stale_reads: AtomicBool,
29 cache: DashMap<K, Item<V>>,
30 lru_samples: AtomicUsize,
31 sema_timeout_milliseconds: AtomicUsize,
32 lookup_counter: IntCounter,
33 evict_counter: IntCounter,
34 expire_counter: IntCounter,
35 hit_counter: IntCounter,
36 miss_counter: IntCounter,
37 populate_counter: IntCounter,
38 populate_histogram: Histogram,
39 insert_counter: IntCounter,
40 stale_counter: IntCounter,
41 error_counter: IntCounter,
42 wait_gauge: IntGauge,
43 size_gauge: IntGauge,
44}
45
46impl<
47 K: Clone + Debug + Send + Sync + Hash + Eq + 'static,
48 V: Clone + Debug + Send + Sync + 'static,
49 > Inner<K, V>
50{
51 pub fn clear(&self) -> usize {
52 let num_entries = self.cache.len();
53
54 self.cache.retain(|_k, item| {
59 if let ItemState::Pending(sema) = &item.item {
60 sema.close();
62 }
63 false
64 });
65
66 self.size_gauge.set(self.cache.len() as i64);
67 num_entries
68 }
69
70 pub fn evict_some(&self, target: usize) -> usize {
89 let now = Instant::now();
90
91 let cache_size = self.cache.len();
94 let num_samples = self.lru_samples.load(Ordering::Relaxed).min(cache_size);
96
97 let mut expired_keys = vec![];
99 let mut samples = vec![];
101
102 {
117 let mut rng = rand::thread_rng();
118 let mut indices =
119 rand::seq::index::sample(&mut rng, cache_size, num_samples).into_vec();
120 indices.sort();
121 let mut iter = self.cache.iter();
122 let mut current_idx = 0;
123
124 fn advance_by(iter: &mut impl Iterator, skip_amount: usize) {
129 for _ in 0..skip_amount {
130 if iter.next().is_none() {
131 return;
132 }
133 }
134 }
135
136 for idx in indices {
137 let skip_amount = idx - current_idx;
141 advance_by(&mut iter, skip_amount);
142
143 match iter.next() {
144 Some(map_entry) => {
145 current_idx = idx + 1;
146 let item = map_entry.value();
147 match &item.item {
148 ItemState::Pending(_) | ItemState::Refreshing { .. } => {
149 }
151 ItemState::Present(_) | ItemState::Failed(_) => {
152 if now >= item.expiration {
153 expired_keys.push(map_entry.key().clone());
154 } else {
155 let last_tick = item.last_tick.load(Ordering::Relaxed);
156 samples.push((map_entry.key().clone(), last_tick));
157 }
158 }
159 }
160 }
161 None => {
162 break;
163 }
164 }
165 }
166 }
167
168 let mut num_removed = 0;
169 for key in expired_keys {
170 let removed = self
174 .cache
175 .remove_if(&key, |_k, entry| now >= entry.expiration)
176 .is_some();
177 if removed {
178 tracing::trace!("{} expired {key:?}", self.name);
179 num_removed += 1;
180 self.expire_counter.inc();
181 }
182 }
183
184 let target = target.min(samples.len() / 2).max(1);
191
192 if num_removed >= target {
194 self.size_gauge.set(self.cache.len() as i64);
195 tracing::trace!("{} expired {num_removed} of target {target}", self.name);
196 return num_removed;
197 }
198
199 samples.sort_by(|(_ka, tick_a), (_kb, tick_b)| tick_a.cmp(tick_b));
202
203 for (key, tick) in samples {
204 if self
208 .cache
209 .remove_if(&key, |_k, item| {
210 item.last_tick.load(Ordering::Relaxed) == tick
211 })
212 .is_some()
213 {
214 tracing::debug!("{} evicted {key:?}", self.name);
215 num_removed += 1;
216 self.evict_counter.inc();
217 self.size_gauge.set(self.cache.len() as i64);
218 if num_removed >= target {
219 return num_removed;
220 }
221 }
222 }
223
224 if num_removed == 0 {
225 tracing::debug!(
226 "{} did not find anything to evict, target was {target}",
227 self.name
228 );
229 }
230
231 tracing::trace!("{} removed {num_removed} of target {target}", self.name);
232
233 num_removed
234 }
235
236 pub fn maybe_evict(&self) -> usize {
239 let cache_size = self.cache.len();
240 let capacity = self.capacity.load(Ordering::Relaxed);
241 if cache_size > capacity {
242 self.evict_some(cache_size - capacity)
243 } else {
244 0
245 }
246 }
247}
248
249trait CachePurger {
250 fn name(&self) -> &str;
251 fn purge(&self) -> usize;
252 fn process_expirations(&self) -> usize;
253 fn update_capacity(&self, capacity: usize);
254}
255
256impl<
257 K: Clone + Debug + Send + Sync + Hash + Eq + 'static,
258 V: Clone + Debug + Send + Sync + 'static,
259 > CachePurger for Inner<K, V>
260{
261 fn name(&self) -> &str {
262 &self.name
263 }
264 fn purge(&self) -> usize {
265 self.clear()
266 }
267 fn process_expirations(&self) -> usize {
268 let now = Instant::now();
269 let mut expired_keys = vec![];
270 for map_entry in self.cache.iter() {
271 let item = map_entry.value();
272 match &item.item {
273 ItemState::Pending(_) | ItemState::Refreshing { .. } => {
274 }
276 ItemState::Present(_) | ItemState::Failed(_) => {
277 if now >= item.expiration {
278 expired_keys.push(map_entry.key().clone());
279 }
280 }
281 }
282 }
283
284 let mut num_removed = 0;
285 for key in expired_keys {
286 let removed = self
290 .cache
291 .remove_if(&key, |_k, entry| now >= entry.expiration)
292 .is_some();
293 if removed {
294 num_removed += 1;
295 self.expire_counter.inc();
296 self.size_gauge.set(self.cache.len() as i64);
297 }
298 }
299
300 num_removed + self.maybe_evict()
301 }
302
303 fn update_capacity(&self, capacity: usize) {
304 self.capacity.store(capacity, Ordering::Relaxed);
305 self.process_expirations();
310 }
311}
312
313fn all_caches() -> Vec<Arc<dyn CachePurger + Send + Sync>> {
314 let mut result = vec![];
315 let mut caches = CACHES.lock();
316 caches.retain(|entry| match entry.upgrade() {
317 Some(purger) => {
318 result.push(purger);
319 true
320 }
321 None => false,
322 });
323 result
324}
325
326pub fn purge_all_caches() {
327 let purgers = all_caches();
328
329 tracing::error!("purging {} caches", purgers.len());
330 for purger in purgers {
331 let name = purger.name();
332 let num_entries = purger.purge();
333 tracing::error!("cleared {num_entries} entries from cache {name}");
334 }
335}
336
337async fn prune_expired_caches() {
338 loop {
339 tokio::time::sleep(tokio::time::Duration::from_secs(30)).await;
340 let purgers = all_caches();
341
342 for p in purgers {
343 let n = p.process_expirations();
344 if n > 0 {
345 tracing::debug!("expired {n} entries from cache {}", p.name());
346 }
347 }
348 }
349}
350
351#[linkme::distributed_slice]
352pub static LRUTTL_VIVIFY: [fn() -> CacheDefinition];
353
354#[macro_export]
355macro_rules! optional_doc {
356 ($doc:expr) => {
357 Some($doc.trim())
358 };
359 ($($doc:expr)+) => {
360 Some(concat!($($doc,)+).trim())
361 };
362 () => {
363 None
364 };
365}
366
367#[macro_export]
374macro_rules! declare_cache {
375 (
376 $(#[doc = $doc:expr])*
377 $vis:vis
378 static $sym:ident:
379 LruCacheWithTtl<$key:ty, $value:ty>::new($name:expr, $capacity:expr);
380 ) => {
381 $(#[doc = $doc])*
382 $vis static $sym: ::std::sync::LazyLock<$crate::LruCacheWithTtl<$key, $value>> =
383 ::std::sync::LazyLock::new(
384 || $crate::LruCacheWithTtl::new($name, $capacity));
385
386 $crate::paste::paste! {
388 #[linkme::distributed_slice($crate::LRUTTL_VIVIFY)]
389 static [<VIVIFY_ $sym>]: fn() -> $crate::CacheDefinition = || {
390 ::std::sync::LazyLock::force(&$sym);
391 $crate::CacheDefinition {
392 name: $name,
393 capacity: $capacity,
394 doc: $crate::optional_doc!($($doc)*),
395 }
396 };
397 }
398 };
399}
400
401fn vivify() {
404 LazyLock::force(&PREDEFINED_CACHES);
405}
406
407fn vivify_impl() -> HashMap<&'static str, CacheDefinition> {
408 let mut map = HashMap::new();
409
410 for vivify_func in LRUTTL_VIVIFY {
411 let definition = vivify_func();
412 assert!(
413 !map.contains_key(definition.name),
414 "duplicate cache name {}",
415 definition.name
416 );
417 map.insert(definition.name, definition);
418 }
419
420 map
421}
422
423#[derive(serde::Serialize)]
424pub struct CacheDefinition {
425 pub name: &'static str,
426 pub capacity: usize,
427 pub doc: Option<&'static str>,
428}
429
430static PREDEFINED_CACHES: LazyLock<HashMap<&'static str, CacheDefinition>> =
431 LazyLock::new(vivify_impl);
432
433pub fn get_definitions() -> Vec<&'static CacheDefinition> {
434 let mut defs = PREDEFINED_CACHES.values().collect::<Vec<_>>();
435 defs.sort_by(|a, b| a.name.cmp(&b.name));
436 defs
437}
438
439pub fn is_name_available(name: &str) -> bool {
440 !PREDEFINED_CACHES.contains_key(name)
441}
442
443pub fn set_cache_capacity(name: &str, capacity: usize) -> bool {
445 if !PREDEFINED_CACHES.contains_key(name) {
446 return false;
447 }
448 let caches = all_caches();
449 match caches.iter().find(|p| p.name() == name) {
450 Some(p) => {
451 p.update_capacity(capacity);
452 true
453 }
454 None => false,
455 }
456}
457
458pub fn spawn_memory_monitor() {
459 vivify();
460 tokio::spawn(purge_caches_on_memory_shortage());
461 tokio::spawn(prune_expired_caches());
462}
463
464async fn purge_caches_on_memory_shortage() {
465 tracing::debug!("starting memory monitor");
466 let mut memory_status = subscribe_to_memory_status_changes_async().await;
467 while let Ok(()) = memory_status.changed().await {
468 if kumo_server_memory::get_headroom() == 0 {
469 purge_all_caches();
470
471 tokio::time::sleep(tokio::time::Duration::from_secs(30)).await;
475 }
476 }
477}
478
479#[derive(Debug, Clone)]
480enum ItemState<V>
481where
482 V: Send,
483 V: Sync,
484{
485 Present(V),
486 Pending(Arc<Semaphore>),
487 Failed(Arc<anyhow::Error>),
488 Refreshing {
489 stale_value: V,
490 pending: Arc<Semaphore>,
491 },
492}
493
494#[derive(Debug)]
495struct Item<V>
496where
497 V: Send,
498 V: Sync,
499{
500 item: ItemState<V>,
501 expiration: Instant,
502 last_tick: AtomicUsize,
503}
504
505impl<V: Clone + Send + Sync> Clone for Item<V> {
506 fn clone(&self) -> Self {
507 Self {
508 item: self.item.clone(),
509 expiration: self.expiration,
510 last_tick: self.last_tick.load(Ordering::Relaxed).into(),
511 }
512 }
513}
514
515#[derive(Debug)]
516pub struct ItemLookup<V: Debug> {
517 pub item: V,
519 pub is_fresh: bool,
522 pub expiration: Instant,
524}
525
526pub struct LruCacheWithTtl<K: Clone + Debug + Hash + Eq, V: Clone + Debug + Send + Sync> {
527 inner: Arc<Inner<K, V>>,
528}
529
530impl<K: Clone + Debug + Hash + Eq, V: Clone + Debug + Send + Sync> Clone for LruCacheWithTtl<K, V> {
531 fn clone(&self) -> Self {
532 Self {
533 inner: Arc::clone(&self.inner),
534 }
535 }
536}
537
538enum Acquisition<V: Debug> {
540 Resolved(Result<ItemLookup<V>, Arc<anyhow::Error>>),
543 Owner(OwnedSemaphorePermit),
547}
548
549#[derive(Debug)]
552pub struct PopulateAbandoned {
553 pub cache: String,
554 pub key: String,
555}
556
557impl std::fmt::Display for PopulateAbandoned {
558 fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
559 write!(
560 fmt,
561 "{} populate for {} exited without recording a result",
562 self.cache, self.key
563 )
564 }
565}
566
567impl std::error::Error for PopulateAbandoned {}
568
569impl<
570 K: Clone + Debug + Hash + Eq + Send + Sync + std::fmt::Debug + 'static,
571 V: Clone + Debug + Send + Sync + 'static,
572 > LruCacheWithTtl<K, V>
573{
574 pub fn new<S: Into<String>>(name: S, capacity: usize) -> Self {
575 let name = name.into();
576 let cache = DashMap::new();
577
578 let lookup_counter = CACHE_LOOKUP
579 .get_metric_with_label_values(&[&name])
580 .expect("failed to get counter");
581 let hit_counter = CACHE_HIT
582 .get_metric_with_label_values(&[&name])
583 .expect("failed to get counter");
584 let stale_counter = CACHE_STALE
585 .get_metric_with_label_values(&[&name])
586 .expect("failed to get counter");
587 let evict_counter = CACHE_EVICT
588 .get_metric_with_label_values(&[&name])
589 .expect("failed to get counter");
590 let expire_counter = CACHE_EXPIRE
591 .get_metric_with_label_values(&[&name])
592 .expect("failed to get counter");
593 let miss_counter = CACHE_MISS
594 .get_metric_with_label_values(&[&name])
595 .expect("failed to get counter");
596 let populate_counter = CACHE_POPULATED
597 .get_metric_with_label_values(&[&name])
598 .expect("failed to get counter");
599 let populate_histogram = CACHE_POPULATE_DURATION
600 .get_metric_with_label_values(&[&name])
601 .expect("failed to get histogram");
602 let insert_counter = CACHE_INSERT
603 .get_metric_with_label_values(&[&name])
604 .expect("failed to get counter");
605 let error_counter = CACHE_ERROR
606 .get_metric_with_label_values(&[&name])
607 .expect("failed to get counter");
608 let wait_gauge = CACHE_WAIT
609 .get_metric_with_label_values(&[&name])
610 .expect("failed to get counter");
611 let size_gauge = CACHE_SIZE
612 .get_metric_with_label_values(&[&name])
613 .expect("failed to get counter");
614
615 let inner = Arc::new(Inner {
616 name,
617 cache,
618 tick: AtomicUsize::new(0),
619 allow_stale_reads: AtomicBool::new(false),
620 capacity: AtomicUsize::new(capacity),
621 lru_samples: AtomicUsize::new(10),
622 sema_timeout_milliseconds: AtomicUsize::new(120_000),
623 lookup_counter,
624 evict_counter,
625 expire_counter,
626 hit_counter,
627 miss_counter,
628 populate_counter,
629 populate_histogram,
630 error_counter,
631 wait_gauge,
632 insert_counter,
633 stale_counter,
634 size_gauge,
635 });
636
637 {
641 let generic: Arc<dyn CachePurger + Send + Sync> = inner.clone();
642 CACHES.lock().push(Arc::downgrade(&generic));
643 tracing::debug!(
644 "registered cache {} with capacity {capacity}",
645 generic.name()
646 );
647 }
648
649 Self { inner }
650 }
651
652 fn allow_stale_reads(&self) -> bool {
653 self.inner.allow_stale_reads.load(Ordering::Relaxed)
654 }
655
656 pub fn set_allow_stale_reads(&self, value: bool) {
657 self.inner.allow_stale_reads.store(value, Ordering::Relaxed);
658 }
659
660 pub fn set_sema_timeout(&self, duration: Duration) {
661 self.inner
662 .sema_timeout_milliseconds
663 .store(duration.as_millis() as usize, Ordering::Relaxed);
664 }
665
666 pub fn get_sema_timeout(&self) -> Duration {
667 Duration::from_millis(self.inner.sema_timeout_milliseconds.load(Ordering::Relaxed) as u64)
668 }
669
670 pub fn clear(&self) -> usize {
671 self.inner.clear()
672 }
673
674 fn inc_tick(&self) -> usize {
675 self.inner.tick.fetch_add(1, Ordering::Relaxed) + 1
676 }
677
678 fn update_tick(&self, item: &Item<V>) {
679 let v = self.inc_tick();
680 item.last_tick.store(v, Ordering::Relaxed);
681 }
682
683 pub fn lookup<Q: ?Sized>(&self, name: &Q) -> Option<ItemLookup<V>>
684 where
685 K: Borrow<Q>,
686 Q: Hash + Eq,
687 {
688 self.inner.lookup_counter.inc();
689 match self.inner.cache.get_mut(name) {
690 None => {
691 self.inner.miss_counter.inc();
692 None
693 }
694 Some(entry) => {
695 match &entry.item {
696 ItemState::Present(item) => {
697 let now = Instant::now();
698 if now >= entry.expiration {
699 if self.allow_stale_reads() {
701 self.inner.miss_counter.inc();
708 return None;
709 }
710
711 drop(entry);
715 if self
716 .inner
717 .cache
718 .remove_if(name, |_k, entry| now >= entry.expiration)
719 .is_some()
720 {
721 self.inner.expire_counter.inc();
722 self.inner.size_gauge.set(self.inner.cache.len() as i64);
723 }
724 self.inner.miss_counter.inc();
725 return None;
726 }
727 self.inner.hit_counter.inc();
728 self.update_tick(&entry);
729 Some(ItemLookup {
730 item: item.clone(),
731 expiration: entry.expiration,
732 is_fresh: false,
733 })
734 }
735 ItemState::Refreshing { .. } | ItemState::Pending(_) | ItemState::Failed(_) => {
736 self.inner.miss_counter.inc();
737 None
738 }
739 }
740 }
741 }
742 }
743
744 pub fn get<Q: ?Sized>(&self, name: &Q) -> Option<V>
745 where
746 K: Borrow<Q>,
747 Q: Hash + Eq,
748 {
749 self.lookup(name).map(|lookup| lookup.item)
750 }
751
752 pub async fn insert(&self, name: K, item: V, expiration: Instant) -> V {
753 self.inner.cache.insert(
754 name,
755 Item {
756 item: ItemState::Present(item.clone()),
757 expiration,
758 last_tick: self.inc_tick().into(),
759 },
760 );
761
762 self.inner.insert_counter.inc();
763 self.inner.size_gauge.set(self.inner.cache.len() as i64);
764 self.inner.maybe_evict();
765
766 item
767 }
768
769 fn clone_item_state(
770 &self,
771 name: &K,
772 deadline: Instant,
773 timeout_duration: Duration,
774 ) -> (ItemState<V>, Instant) {
775 let mut is_new = false;
776 let mut entry = self.inner.cache.entry(name.clone()).or_insert_with(|| {
777 is_new = true;
778 Item {
779 item: ItemState::Pending(Arc::new(Semaphore::new(1))),
780 expiration: deadline,
781 last_tick: self.inc_tick().into(),
782 }
783 });
784
785 match &entry.value().item {
786 ItemState::Pending(sema) => {
787 if sema.is_closed() {
788 entry.value_mut().item = ItemState::Pending(Arc::new(Semaphore::new(1)));
789 } else {
790 let now = Instant::now();
791 if now >= entry.expiration {
792 tracing::warn!(
797 "{} semaphore for {name:?} remains open, \
798 but the lookup state has not been satisfied within the \
799 populate deadline. Assuming that something is stuck and \
800 making the now-active caller responsible for populating \
801 this entry",
802 self.inner.name
803 );
804 entry.value_mut().item = ItemState::Pending(Arc::new(Semaphore::new(1)));
805 entry.value_mut().expiration = now + timeout_duration;
806 }
807 }
808 }
809 ItemState::Refreshing {
810 stale_value,
811 pending,
812 } => {
813 if pending.is_closed() {
814 entry.value_mut().item = ItemState::Refreshing {
815 stale_value: stale_value.clone(),
816 pending: Arc::new(Semaphore::new(1)),
817 };
818 }
819 }
820 ItemState::Present(item) => {
821 let now = Instant::now();
822 if now >= entry.expiration {
823 let pending = Arc::new(Semaphore::new(1));
825 if self.allow_stale_reads() {
826 entry.value_mut().item = ItemState::Refreshing {
827 stale_value: item.clone(),
828 pending,
829 };
830 } else {
831 entry.value_mut().item = ItemState::Pending(pending);
832 }
833 }
834 }
835 ItemState::Failed(_) => {
836 let now = Instant::now();
837 if now >= entry.expiration {
838 entry.value_mut().item = ItemState::Pending(Arc::new(Semaphore::new(1)));
840 entry.value_mut().expiration = now + timeout_duration;
841 }
842 }
843 }
844
845 self.update_tick(&entry);
846 let item = entry.value();
847 let result = (item.item.clone(), entry.expiration);
848 drop(entry);
849
850 if is_new {
851 self.inner.size_gauge.set(self.inner.cache.len() as i64);
852 self.inner.maybe_evict();
853 }
854
855 result
856 }
857
858 async fn acquire(
862 &self,
863 name: &K,
864 deadline: Instant,
865 timeout_duration: Duration,
866 ) -> Acquisition<V> {
867 loop {
870 let (stale_value, sema) = match self.clone_item_state(name, deadline, timeout_duration)
871 {
872 (ItemState::Present(item), expiration) => {
873 return Acquisition::Resolved(Ok(ItemLookup {
874 item,
875 expiration,
876 is_fresh: false,
877 }));
878 }
879 (ItemState::Failed(error), _) => {
880 return Acquisition::Resolved(Err(error));
881 }
882 (
883 ItemState::Refreshing {
884 stale_value,
885 pending,
886 },
887 expiration,
888 ) => (Some((stale_value, expiration)), pending),
889 (ItemState::Pending(sema), _) => (None, sema),
890 };
891
892 let wait_result = {
893 self.inner.wait_gauge.inc();
894 defer! {
895 self.inner.wait_gauge.dec();
896 }
897
898 match timeout_at(deadline, sema.acquire_owned()).await {
902 Err(_) => {
903 if let Some((item, expiration)) = stale_value {
904 tracing::debug!(
905 "{} semaphore acquire for {name:?} timed out after \
906 {timeout_duration:?}, allowing stale value to satisfy the lookup",
907 self.inner.name
908 );
909 self.inner.stale_counter.inc();
910 return Acquisition::Resolved(Ok(ItemLookup {
911 item,
912 expiration,
913 is_fresh: false,
914 }));
915 }
916 tracing::debug!(
917 "{} semaphore acquire for {name:?} timed out after \
918 {timeout_duration:?}, returning error",
919 self.inner.name
920 );
921
922 self.inner.error_counter.inc();
923 return Acquisition::Resolved(Err(Arc::new(anyhow::anyhow!(
924 "{} lookup for {name:?} \
925 timed out after {timeout_duration:?} \
926 on semaphore acquire while waiting for cache to populate",
927 self.inner.name
928 ))));
929 }
930 Ok(r) => r,
931 }
932 };
933
934 let current_sema = match self.clone_item_state(name, deadline, timeout_duration) {
937 (ItemState::Present(item), expiration) => {
938 return Acquisition::Resolved(Ok(ItemLookup {
939 item,
940 expiration,
941 is_fresh: false,
942 }));
943 }
944 (ItemState::Failed(error), _) => {
945 self.inner.hit_counter.inc();
946 return Acquisition::Resolved(Err(error));
947 }
948 (
949 ItemState::Refreshing {
950 stale_value: _,
951 pending,
952 },
953 _,
954 ) => pending,
955 (ItemState::Pending(current_sema), _) => current_sema,
956 };
957
958 match wait_result {
960 Ok(permit) => {
961 if !Arc::ptr_eq(¤t_sema, permit.semaphore()) {
962 self.inner.error_counter.inc();
963 tracing::warn!(
964 "{} mismatched semaphores for {name:?}, \
965 will restart cache resolve.",
966 self.inner.name
967 );
968 permit.semaphore().close();
972 continue;
974 }
975
976 return Acquisition::Owner(permit);
979 }
980 Err(_) => {
981 self.inner.error_counter.inc();
982
983 tracing::debug!(
986 "{} lookup for {name:?} woke up semaphores \
987 but is still marked pending, \
988 will restart cache lookup",
989 self.inner.name
990 );
991 continue;
992 }
993 }
994 }
995 }
996
997 pub async fn get_or_try_insert<E: Into<anyhow::Error>, TTL: FnOnce(&V) -> Duration>(
1005 &self,
1006 name: &K,
1007 ttl_func: TTL,
1008 fut: impl Future<Output = Result<V, E>>,
1009 ) -> Result<ItemLookup<V>, Arc<anyhow::Error>> {
1010 if let Some(entry) = self.lookup(name) {
1012 return Ok(entry);
1013 }
1014
1015 let timeout_duration = Duration::from_millis(
1016 self.inner.sema_timeout_milliseconds.load(Ordering::Relaxed) as u64,
1017 );
1018 let start = Instant::now();
1019 let deadline = start + timeout_duration;
1020
1021 let permit = match self.acquire(name, deadline, timeout_duration).await {
1022 Acquisition::Resolved(result) => return result,
1023 Acquisition::Owner(permit) => permit,
1024 };
1025
1026 defer! {
1029 permit.semaphore().close();
1030 }
1031
1032 self.inner.populate_counter.inc();
1033 let mut ttl = Duration::from_secs(60);
1034 let populate_timer = self.inner.populate_histogram.start_timer();
1035 let future_result = fut.await;
1036 populate_timer.observe_duration();
1037 let now = Instant::now();
1038
1039 let (item_result, return_value) = match future_result {
1040 Ok(item) => {
1041 ttl = ttl_func(&item);
1042 (
1043 ItemState::Present(item.clone()),
1044 Ok(ItemLookup {
1045 item,
1046 expiration: now + ttl,
1047 is_fresh: true,
1048 }),
1049 )
1050 }
1051 Err(err) => {
1052 self.inner.error_counter.inc();
1053 let err = Arc::new(err.into());
1054 (ItemState::Failed(err.clone()), Err(err))
1055 }
1056 };
1057
1058 self.inner.cache.insert(
1060 name.clone(),
1061 Item {
1062 item: item_result,
1063 expiration: Instant::now() + ttl,
1064 last_tick: self.inc_tick().into(),
1065 },
1066 );
1067 self.inner.maybe_evict();
1068 return_value
1069 }
1070
1071 pub async fn get_or_try_insert_detached<E, TTL, MK, F>(
1081 &self,
1082 name: &K,
1083 ttl_func: TTL,
1084 make_fut: MK,
1085 populate_timeout: Duration,
1086 ) -> Result<ItemLookup<V>, Arc<anyhow::Error>>
1087 where
1088 E: Into<anyhow::Error> + Send + 'static,
1089 TTL: Send + 'static + FnOnce(&V) -> Duration,
1090 MK: FnOnce() -> F,
1091 F: Future<Output = Result<V, E>> + Send + 'static,
1092 {
1093 if let Some(entry) = self.lookup(name) {
1094 return Ok(entry);
1095 }
1096
1097 let timeout_duration = Duration::from_millis(
1098 self.inner.sema_timeout_milliseconds.load(Ordering::Relaxed) as u64,
1099 );
1100 let deadline = Instant::now() + timeout_duration;
1101
1102 {
1103 let permit = match self.acquire(name, deadline, timeout_duration).await {
1104 Acquisition::Resolved(result) => return result,
1105 Acquisition::Owner(permit) => permit,
1106 };
1107
1108 let this = self.clone();
1109 let key = name.clone();
1110 let fut = make_fut();
1111 let sema = permit.semaphore().clone();
1114
1115 self.set_pending_expiration(name, Instant::now() + populate_timeout);
1119
1120 kumo_server_runtime::spawn_on_main("lruttl-populate", async move {
1123 defer! {
1126 permit.semaphore().close();
1127 }
1128
1129 let our_sema = permit.semaphore();
1133
1134 this.inner.populate_counter.inc();
1135 let populate_timer = this.inner.populate_histogram.start_timer();
1136 let populate_result = timeout(populate_timeout, fut).await;
1137 populate_timer.observe_duration();
1138 let (item, ttl) = match populate_result {
1139 Ok(Ok(value)) => {
1140 let ttl = ttl_func(&value);
1141 (ItemState::Present(value), ttl)
1142 }
1143 Ok(Err(err)) => {
1144 this.inner.error_counter.inc();
1145 (
1149 ItemState::Failed(Arc::new(err.into())),
1150 Duration::from_secs(60),
1151 )
1152 }
1153 Err(_) => {
1154 this.inner.error_counter.inc();
1155 (
1156 ItemState::Failed(Arc::new(anyhow::anyhow!(
1157 "{} populate for {key:?} timed out after {populate_timeout:?}",
1158 this.inner.name
1159 ))),
1160 Duration::from_secs(60),
1161 )
1162 }
1163 };
1164
1165 if let Some(mut entry) = this.inner.cache.get_mut(&key) {
1169 let is_current = match &entry.item {
1170 ItemState::Pending(current)
1171 | ItemState::Refreshing {
1172 pending: current, ..
1173 } => Arc::ptr_eq(current, our_sema),
1174 ItemState::Present(_) | ItemState::Failed(_) => false,
1175 };
1176 if is_current {
1177 entry.item = item;
1178 entry.expiration = Instant::now() + ttl;
1179 entry.last_tick = this.inc_tick().into();
1180 }
1181 }
1182 this.inner.maybe_evict();
1183 })
1184 .map_err(|err| {
1185 sema.close();
1189 Arc::new(anyhow::anyhow!(
1190 "{} failed to spawn detached populate for {name:?}: {err:#}",
1191 self.inner.name
1192 ))
1193 })?;
1194 }
1195
1196 match self.acquire(name, deadline, timeout_duration).await {
1197 Acquisition::Resolved(result) => result,
1198 Acquisition::Owner(permit) => {
1201 permit.semaphore().close();
1204 self.inner.error_counter.inc();
1205 Err(Arc::new(anyhow::Error::new(PopulateAbandoned {
1206 cache: self.inner.name.clone(),
1207 key: format!("{name:?}"),
1208 })))
1209 }
1210 }
1211 }
1212
1213 fn set_pending_expiration(&self, name: &K, expiration: Instant) {
1217 if let Some(mut entry) = self.inner.cache.get_mut(name) {
1218 if matches!(
1219 entry.item,
1220 ItemState::Pending(_) | ItemState::Refreshing { .. }
1221 ) {
1222 entry.expiration = expiration;
1223 }
1224 }
1225 }
1226}
1227
1228#[cfg(test)]
1229mod test {
1230 use super::*;
1231 use test_log::test; #[test(tokio::test)]
1234 async fn test_capacity() {
1235 let cache = LruCacheWithTtl::new("test_capacity", 40);
1236
1237 let expiration = Instant::now() + Duration::from_secs(60);
1238 for i in 0..100 {
1239 cache.insert(i, i, expiration).await;
1240 }
1241
1242 assert_eq!(cache.inner.cache.len(), 40, "capacity is respected");
1243 }
1244
1245 #[test(tokio::test)]
1246 async fn test_expiration() {
1247 let cache = LruCacheWithTtl::new("test_expiration", 1);
1248
1249 tokio::time::pause();
1250 let expiration = Instant::now() + Duration::from_secs(1);
1251 cache.insert(0, 0, expiration).await;
1252
1253 cache.get(&0).expect("still in cache");
1254 tokio::time::advance(Duration::from_secs(2)).await;
1255 assert!(cache.get(&0).is_none(), "evicted due to ttl");
1256 }
1257
1258 #[test(tokio::test)]
1259 async fn test_zero_ttl_requeries() {
1260 let cache = LruCacheWithTtl::<u32, u32>::new("test_zero_ttl_requeries", 1);
1262
1263 let first = cache
1264 .get_or_try_insert(&0, |_| Duration::ZERO, async { Ok::<_, anyhow::Error>(1) })
1265 .await
1266 .unwrap();
1267 assert_eq!(first.item, 1);
1268 assert!(first.is_fresh);
1269
1270 let second = cache
1271 .get_or_try_insert(&0, |_| Duration::ZERO, async { Ok::<_, anyhow::Error>(2) })
1272 .await
1273 .unwrap();
1274 assert_eq!(second.item, 2, "the zero-ttl entry was not reused");
1275 assert!(second.is_fresh);
1276 }
1277
1278 #[test(tokio::test)]
1279 async fn test_over_capacity_slow_resolve() {
1280 let cache = Arc::new(LruCacheWithTtl::<String, u64>::new(
1281 "test_over_capacity_slow_resolve",
1282 1,
1283 ));
1284
1285 let mut foos = vec![];
1286 for idx in 0..2 {
1287 let cache = cache.clone();
1288 foos.push(tokio::spawn(async move {
1289 eprintln!("spawned task {idx} is running");
1290 cache
1291 .get_or_try_insert(&"foo".to_string(), |_| Duration::from_secs(86400), async {
1292 if idx == 0 {
1293 eprintln!("foo {idx} getter sleeping");
1294 tokio::time::sleep(Duration::from_secs(300)).await;
1295 }
1296 eprintln!("foo {idx} getter done");
1297 Ok::<_, anyhow::Error>(idx)
1298 })
1299 .await
1300 }));
1301 }
1302
1303 tokio::task::yield_now().await;
1304
1305 eprintln!("calling again with immediate getter");
1306 let result = cache
1307 .get_or_try_insert(&"bar".to_string(), |_| Duration::from_secs(60), async {
1308 eprintln!("bar immediate getter running");
1309 Ok::<_, anyhow::Error>(42)
1310 })
1311 .await
1312 .unwrap();
1313
1314 assert_eq!(result.item, 42);
1315 assert_eq!(cache.inner.cache.len(), 1);
1316
1317 eprintln!("aborting first one");
1318 foos.remove(0).abort();
1319
1320 eprintln!("try new key");
1321 let result = cache
1322 .get_or_try_insert(&"baz".to_string(), |_| Duration::from_secs(60), async {
1323 eprintln!("baz immediate getter running");
1324 Ok::<_, anyhow::Error>(32)
1325 })
1326 .await
1327 .unwrap();
1328 assert_eq!(result.item, 32);
1329 assert_eq!(cache.inner.cache.len(), 1);
1330
1331 eprintln!("waiting second one");
1332 assert_eq!(1, foos.pop().unwrap().await.unwrap().unwrap().item);
1333
1334 assert_eq!(cache.inner.cache.len(), 1);
1335 }
1336
1337 #[test(tokio::test)]
1338 async fn detached_populate_survives_caller_cancellation() {
1339 use tokio::sync::Notify;
1340
1341 let cache = Arc::new(LruCacheWithTtl::<String, u64>::new(
1342 "detached_survives_cancel",
1343 16,
1344 ));
1345 let key = "k".to_string();
1346 let started = Arc::new(Notify::new());
1347 let gate = Arc::new(Notify::new());
1348 let runs = Arc::new(AtomicUsize::new(0));
1349
1350 let caller = {
1351 let cache = cache.clone();
1352 let key = key.clone();
1353 let started = started.clone();
1354 let gate = gate.clone();
1355 let runs = runs.clone();
1356 tokio::spawn(async move {
1357 cache
1358 .get_or_try_insert_detached(
1359 &key,
1360 |_| Duration::from_secs(86400),
1361 move || {
1362 let started = started.clone();
1363 let gate = gate.clone();
1364 let runs = runs.clone();
1365 async move {
1366 runs.fetch_add(1, Ordering::SeqCst);
1367 started.notify_one();
1368 gate.notified().await;
1369 Ok::<_, anyhow::Error>(42u64)
1370 }
1371 },
1372 Duration::from_secs(300),
1373 )
1374 .await
1375 })
1376 };
1377
1378 started.notified().await;
1381 caller.abort();
1382 gate.notify_one();
1384
1385 loop {
1386 if let Some(value) = cache.get(&key) {
1387 assert_eq!(value, 42);
1388 break;
1389 }
1390 tokio::task::yield_now().await;
1391 }
1392 assert_eq!(runs.load(Ordering::SeqCst), 1);
1393 }
1394
1395 #[test(tokio::test)]
1396 async fn detached_second_caller_shares_populate() {
1397 use tokio::sync::Notify;
1398
1399 let cache = Arc::new(LruCacheWithTtl::<String, u64>::new(
1400 "detached_shared_populate",
1401 16,
1402 ));
1403 let key = "k".to_string();
1404 let gate = Arc::new(Notify::new());
1405 let runs = Arc::new(AtomicUsize::new(0));
1406
1407 let make_caller = || {
1408 let cache = cache.clone();
1409 let key = key.clone();
1410 let gate = gate.clone();
1411 let runs = runs.clone();
1412 tokio::spawn(async move {
1413 cache
1414 .get_or_try_insert_detached(
1415 &key,
1416 |_| Duration::from_secs(86400),
1417 move || {
1418 let gate = gate.clone();
1419 let runs = runs.clone();
1420 async move {
1421 runs.fetch_add(1, Ordering::SeqCst);
1422 gate.notified().await;
1423 Ok::<_, anyhow::Error>(7u64)
1424 }
1425 },
1426 Duration::from_secs(300),
1427 )
1428 .await
1429 })
1430 };
1431
1432 let first = make_caller();
1433 let second = make_caller();
1434
1435 while runs.load(Ordering::SeqCst) == 0 {
1437 tokio::task::yield_now().await;
1438 }
1439 gate.notify_one();
1440
1441 let first = first.await.unwrap().unwrap();
1442 let second = second.await.unwrap().unwrap();
1443 assert_eq!(first.item, 7);
1444 assert_eq!(second.item, 7);
1445 assert_eq!(
1446 runs.load(Ordering::SeqCst),
1447 1,
1448 "the two callers must share one populate"
1449 );
1450 }
1451
1452 #[test(tokio::test)]
1453 async fn detached_populate_timeout_yields_one_error() {
1454 let cache = Arc::new(LruCacheWithTtl::<String, u64>::new(
1455 "detached_populate_timeout",
1456 16,
1457 ));
1458 let key = "k".to_string();
1459 let runs = Arc::new(AtomicUsize::new(0));
1460
1461 let runs_in_fut = runs.clone();
1462 let result = cache
1463 .get_or_try_insert_detached(
1464 &key,
1465 |_| Duration::from_secs(86400),
1466 move || {
1467 let runs = runs_in_fut.clone();
1468 async move {
1469 runs.fetch_add(1, Ordering::SeqCst);
1470 std::future::pending::<()>().await;
1473 Ok::<_, anyhow::Error>(0u64)
1474 }
1475 },
1476 Duration::from_millis(50),
1477 )
1478 .await;
1479
1480 let err = result.unwrap_err();
1481 assert!(
1482 err.to_string().contains("timed out after"),
1483 "unexpected error: {err}"
1484 );
1485 assert_eq!(
1486 runs.load(Ordering::SeqCst),
1487 1,
1488 "a timed-out populate must leave a Failed entry, not spin up repeated populates"
1489 );
1490 }
1491}