1use std::sync::{Arc, Mutex};
13
14use async_trait::async_trait;
15use bytes::Bytes;
16use mz_dyncfg::{Config, ConfigSet, ParameterScope};
17use mz_ore::bytes::SegmentedBytes;
18use mz_ore::cast::CastFrom;
19use mz_persist::location::{Blob, BlobMetadata, ExternalError};
20
21use crate::cfg::PersistConfig;
22use crate::internal::metrics::Metrics;
23
24#[derive(Debug)]
26pub struct BlobMemCache {
27 cfg: Arc<ConfigSet>,
29 num_workers: usize,
31 metrics: Arc<Metrics>,
32 cache: Mutex<lru::Lru<String, SegmentedBytes>>,
33 blob: Arc<dyn Blob>,
34}
35
36pub(crate) const BLOB_CACHE_MEM_LIMIT_BYTES: Config<usize> = Config::new(
37 "persist_blob_cache_mem_limit_bytes",
38 128 * 1024 * 1024,
40 "Capacity of in-mem blob cache in bytes (Materialize).",
41 ParameterScope::Environment,
42);
43
44pub(crate) const BLOB_CACHE_SCALE_WITH_THREADS: Config<bool> = Config::new(
45 "persist_blob_cache_scale_with_threads",
46 false,
47 "Whether or not the size of the in-mem blob cache scales with the number of threads in the current process (Materialize).",
48 ParameterScope::Environment,
49);
50
51pub(crate) const BLOB_CACHE_SCALE_FACTOR_BYTES: Config<usize> = Config::new(
52 "persist_blob_cache_scale_factor_bytes",
53 32 * 1024 * 1024,
55 "Scale factor for the in-mem blob cache, in bytes, if scaling with threads (Materialize).",
56 ParameterScope::Environment,
57);
58
59impl BlobMemCache {
60 pub fn new(cfg: &PersistConfig, metrics: Arc<Metrics>, blob: Arc<dyn Blob>) -> Arc<dyn Blob> {
61 let eviction_metrics = Arc::clone(&metrics);
62 let capacity_bytes =
63 BlobMemCache::get_capacity_bytes(&cfg.configs, cfg.isolated_runtime_worker_threads);
64 let cache = lru::Lru::new(capacity_bytes, move |_, _, _| {
65 eviction_metrics.blob_cache_mem.evictions.inc()
66 });
67 let blob = BlobMemCache {
68 cfg: Arc::clone(&cfg.configs),
69 num_workers: cfg.isolated_runtime_worker_threads,
70 metrics,
71 cache: Mutex::new(cache),
72 blob,
73 };
74 Arc::new(blob)
75 }
76
77 fn resize_and_update_size_metrics(&self, cache: &mut lru::Lru<String, SegmentedBytes>) {
78 let capacity_bytes = BlobMemCache::get_capacity_bytes(&self.cfg, self.num_workers);
79 cache.update_capacity(capacity_bytes);
80 self.metrics
81 .blob_cache_mem
82 .size_blobs
83 .set(u64::cast_from(cache.entry_count()));
84 self.metrics
85 .blob_cache_mem
86 .size_bytes
87 .set(u64::cast_from(cache.entry_weight()));
88 }
89
90 fn get_capacity_bytes(cfg: &Arc<ConfigSet>, num_workers: usize) -> usize {
91 let static_size = BLOB_CACHE_MEM_LIMIT_BYTES.get(cfg);
94
95 if BLOB_CACHE_SCALE_WITH_THREADS.get(cfg) {
96 let per_thread_const = BLOB_CACHE_SCALE_FACTOR_BYTES.get(cfg);
97 let dynamic_size = num_workers.saturating_mul(per_thread_const);
98 std::cmp::max(dynamic_size, static_size)
99 } else {
100 static_size
101 }
102 }
103}
104
105#[async_trait]
106impl Blob for BlobMemCache {
107 async fn get(&self, key: &str) -> Result<Option<SegmentedBytes>, ExternalError> {
108 if let Some((_, cached_value)) = self.cache.lock().expect("lock poisoned").get(key) {
116 self.metrics.blob_cache_mem.hits_blobs.inc();
117 self.metrics
118 .blob_cache_mem
119 .hits_bytes
120 .inc_by(u64::cast_from(cached_value.len()));
121 return Ok(Some(cached_value.clone()));
122 }
123
124 let res = self.blob.get(key).await?;
125 if let Some(blob) = res.as_ref() {
126 let mut cache = self.cache.lock().expect("lock poisoned");
130 if blob.len() <= cache.capacity() {
134 cache.insert(key.to_owned(), blob.clone(), blob.len());
135 self.resize_and_update_size_metrics(&mut cache);
136 }
137 }
138 Ok(res)
139 }
140
141 async fn list_keys_and_metadata(
142 &self,
143 key_prefix: &str,
144 f: &mut (dyn FnMut(BlobMetadata) + Send + Sync),
145 ) -> Result<(), ExternalError> {
146 self.blob.list_keys_and_metadata(key_prefix, f).await
147 }
148
149 async fn set(&self, key: &str, value: Bytes) -> Result<(), ExternalError> {
150 let () = self.blob.set(key, value.clone()).await?;
151 let weight = value.len();
152 let mut cache = self.cache.lock().expect("lock poisoned");
153 if weight <= cache.capacity() {
157 cache.insert(key.to_owned(), SegmentedBytes::from(value), weight);
158 self.resize_and_update_size_metrics(&mut cache);
159 }
160 Ok(())
161 }
162
163 async fn delete(&self, key: &str) -> Result<Option<usize>, ExternalError> {
164 let res = self.blob.delete(key).await;
165 let mut cache = self.cache.lock().expect("lock poisoned");
166 cache.remove(key);
167 self.resize_and_update_size_metrics(&mut cache);
168 res
169 }
170
171 async fn restore(&self, key: &str) -> Result<(), ExternalError> {
172 self.blob.restore(key).await
173 }
174}
175
176mod lru {
177 use std::borrow::Borrow;
178 use std::collections::BTreeMap;
179 use std::hash::Hash;
180
181 use mz_ore::collections::HashMap;
182
183 #[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
184 pub struct Weight(usize);
185
186 #[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord)]
187 pub struct Time(usize);
188
189 pub struct Lru<K, V> {
194 evict_fn: Box<dyn Fn(K, V, usize) + Send>,
195 capacity: Weight,
196
197 next_time: Time,
198 entries: HashMap<K, (V, Weight, Time)>,
199 by_time: BTreeMap<Time, K>,
200 total_weight: Weight,
201 }
202
203 impl<K: Hash + Eq + Clone, V> Lru<K, V> {
204 pub fn new<F>(capacity: usize, evict_fn: F) -> Self
210 where
211 F: Fn(K, V, usize) + Send + 'static,
212 {
213 Lru {
214 evict_fn: Box::new(evict_fn),
215 capacity: Weight(capacity),
216 next_time: Time::default(),
217 entries: HashMap::new(),
218 by_time: BTreeMap::new(),
219 total_weight: Weight(0),
220 }
221 }
222
223 pub fn capacity(&self) -> usize {
225 self.capacity.0
226 }
227
228 pub fn entry_count(&self) -> usize {
230 mz_ore::soft_assert_eq_no_log!(self.entries.len(), self.by_time.len());
231 self.entries.len()
232 }
233
234 pub fn entry_weight(&self) -> usize {
236 self.total_weight.0
237 }
238
239 pub fn update_capacity(&mut self, capacity: usize) {
242 self.capacity = Weight(capacity);
243 self.resize();
244 assert!(self.total_weight <= self.capacity);
245
246 #[cfg(test)]
249 self.validate();
250 }
251
252 pub fn get<Q>(&mut self, key: &Q) -> Option<(&K, &V)>
255 where
256 K: Borrow<Q>,
257 Q: Hash + Eq + ?Sized,
258 {
259 {
260 let (key, val, weight) = self.remove(key)?;
261 self.insert_not_exists(key, val, Weight(weight));
262 }
263 let (key, (val, _, _)) = self
264 .entries
265 .get_key_value(key)
266 .expect("internal lru invariant violated");
267
268 #[cfg(test)]
271 self.validate();
272
273 Some((key, val))
274 }
275
276 pub fn insert(&mut self, key: K, val: V, weight: usize) {
282 let _ = self.remove(&key);
283 self.insert_not_exists(key, val, Weight(weight));
284
285 #[cfg(test)]
288 self.validate();
289 }
290
291 pub fn remove<Q>(&mut self, k: &Q) -> Option<(K, V, usize)>
295 where
296 K: Borrow<Q>,
297 Q: Hash + Eq + ?Sized,
298 {
299 let (_, _, time) = self.entries.get(k)?;
300 let (key, val, weight) = self.remove_exists(time.clone());
301
302 #[cfg(test)]
305 self.validate();
306
307 Some((key, val, weight.0))
308 }
309
310 #[allow(dead_code)]
313 pub(crate) fn iter(&self) -> impl Iterator<Item = (&K, &V, usize)> {
314 self.by_time.iter().rev().map(|(_, key)| {
315 let (val, _, weight) = self
316 .entries
317 .get(key)
318 .expect("internal lru invariant violated");
319 (key, val, weight.0)
320 })
321 }
322
323 fn insert_not_exists(&mut self, key: K, val: V, weight: Weight) {
324 let time = self.next_time.clone();
325 self.next_time.0 += 1;
326
327 self.total_weight.0 = self
328 .total_weight
329 .0
330 .checked_add(weight.0)
331 .expect("weight overflow");
332 assert!(
333 self.entries
334 .insert(key.clone(), (val, weight, time.clone()))
335 .is_none()
336 );
337 assert!(self.by_time.insert(time, key).is_none());
338 self.resize();
339 }
340
341 fn remove_exists(&mut self, time: Time) -> (K, V, Weight) {
342 let key = self
343 .by_time
344 .remove(&time)
345 .expect("internal list invariant violated");
346 let (val, weight, _time) = self
347 .entries
348 .remove(&key)
349 .expect("internal list invariant violated");
350 self.total_weight.0 = self
351 .total_weight
352 .0
353 .checked_sub(weight.0)
354 .expect("internal lru invariant violated");
355
356 (key, val, weight)
357 }
358
359 fn resize(&mut self) {
360 while self.total_weight > self.capacity {
361 let (time, _) = self
362 .by_time
363 .first_key_value()
364 .expect("internal lru invariant violated");
365 let (key, val, weight) = self.remove_exists(time.clone());
366 (self.evict_fn)(key, val, weight.0);
367 }
368 }
369
370 #[cfg(test)]
375 pub(crate) fn validate(&self) {
376 assert!(self.total_weight <= self.capacity);
377
378 let mut count = 0;
379 let mut weight = 0;
380 for (time, key) in self.by_time.iter() {
381 let (_val, w, t) = self
382 .entries
383 .get(key)
384 .expect("internal lru invariant violated");
385 count += 1;
386 weight += w.0;
387 assert_eq!(time, t);
388 }
389 assert_eq!(count, self.by_time.len());
390 assert_eq!(weight, self.total_weight.0);
391 }
392 }
393
394 impl<K: std::fmt::Debug, V> std::fmt::Debug for Lru<K, V> {
395 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
396 let Lru {
397 evict_fn: _,
398 capacity,
399 next_time,
400 entries: _,
401 by_time,
402 total_weight,
403 } = self;
404 f.debug_struct("Lru")
405 .field("capacity", &capacity)
406 .field("total_weight", &total_weight)
407 .field("next_time", &next_time)
408 .field("by_time", &by_time)
409 .finish_non_exhaustive()
410 }
411 }
412}
413
414#[cfg(test)]
415mod tests {
416 use mz_ore::assert_none;
417 use proptest::arbitrary::any;
418 use proptest::proptest;
419 use proptest_derive::Arbitrary;
420
421 use super::lru::*;
422
423 #[derive(Debug, Arbitrary)]
424 enum LruOp {
425 Get { key: u8 },
426 Insert { key: u8, weight: u8 },
427 Remove { key: u8 },
428 }
429
430 fn prop_testcase(ops: Vec<LruOp>) {
431 let capacity = usize::from(u8::MAX / 2) * usize::from(u8::MAX / 2) / 2;
435 let mut cache = Lru::new(capacity, |_, _, _| {});
436 for op in ops {
437 match op {
438 LruOp::Get { key } => {
439 let _ = cache.get(&key);
440 }
441 LruOp::Insert { key, weight } => {
442 cache.insert(key, (), usize::from(weight));
443 }
444 LruOp::Remove { key } => {
445 let _ = cache.remove(&key);
446 }
447 }
448 cache.validate();
449 }
450 }
451
452 #[mz_ore::test]
453 #[cfg_attr(miri, ignore)] fn lru_cache_prop() {
455 proptest!(|(state in proptest::collection::vec(any::<LruOp>(), 0..100))| prop_testcase(state));
456 }
457
458 impl Lru<&'static str, ()> {
459 fn keys(&self) -> Vec<&'static str> {
460 self.iter().map(|(k, _, _)| *k).collect()
461 }
462 }
463
464 #[mz_ore::test]
465 #[cfg_attr(miri, ignore)]
466 fn lru_cache_usage() {
467 let mut cache = Lru::<&'static str, ()>::new(3, |_, _, _| {});
468
469 assert_eq!(cache.entry_count(), 0);
471 assert_eq!(cache.entry_weight(), 0);
472
473 cache.insert("a", (), 2);
475 assert_eq!(cache.entry_count(), 1);
476 assert_eq!(cache.entry_weight(), 2);
477 assert_eq!(cache.keys(), &["a"]);
478
479 cache.insert("b", (), 2);
481 assert_eq!(cache.entry_count(), 1);
482 assert_eq!(cache.entry_weight(), 2);
483 assert_eq!(cache.keys(), &["b"]);
484
485 cache.insert("c", (), 1);
487 assert_eq!(cache.entry_count(), 2);
488 assert_eq!(cache.entry_weight(), 3);
489 assert_eq!(cache.keys(), &["c", "b"]);
490
491 cache.insert("d", (), 1);
493 cache.insert("e", (), 1);
494 assert_eq!(cache.entry_count(), 3);
495 assert_eq!(cache.entry_weight(), 3);
496 assert_eq!(cache.keys(), &["e", "d", "c"]);
497
498 cache.get("e");
500 assert_eq!(cache.entry_count(), 3);
501 assert_eq!(cache.entry_weight(), 3);
502 assert_eq!(cache.keys(), &["e", "d", "c"]);
503
504 cache.get("c");
506 assert_eq!(cache.entry_count(), 3);
507 assert_eq!(cache.entry_weight(), 3);
508 assert_eq!(cache.keys(), &["c", "e", "d"]);
509
510 cache.get("e");
512 assert_eq!(cache.entry_count(), 3);
513 assert_eq!(cache.entry_weight(), 3);
514 assert_eq!(cache.keys(), &["e", "c", "d"]);
515
516 cache.get("f");
518 assert_eq!(cache.entry_count(), 3);
519 assert_eq!(cache.entry_weight(), 3);
520 assert_eq!(cache.keys(), &["e", "c", "d"]);
521
522 assert!(cache.remove("c").is_some());
524 assert_eq!(cache.entry_count(), 2);
525 assert_eq!(cache.entry_weight(), 2);
526 assert_eq!(cache.keys(), &["e", "d"]);
527
528 assert_none!(cache.remove("f"));
530 assert_eq!(cache.entry_count(), 2);
531 assert_eq!(cache.entry_weight(), 2);
532 assert_eq!(cache.keys(), &["e", "d"]);
533
534 cache.insert("f", (), 3);
536 assert_eq!(cache.entry_count(), 1);
537 assert_eq!(cache.entry_weight(), 3);
538 assert_eq!(cache.keys(), &["f"]);
539
540 cache.insert("g", (), 4);
543 assert_eq!(cache.entry_count(), 0);
544 assert_eq!(cache.entry_weight(), 0);
545
546 cache.insert("h", (), 2);
548 cache.insert("i", (), 1);
549 cache.update_capacity(4);
550 cache.insert("j", (), 1);
551 assert_eq!(cache.entry_count(), 3);
552 assert_eq!(cache.entry_weight(), 4);
553 assert_eq!(cache.keys(), &["j", "i", "h"]);
554
555 cache.update_capacity(2);
557 assert_eq!(cache.entry_count(), 2);
558 assert_eq!(cache.entry_weight(), 2);
559 assert_eq!(cache.keys(), &["j", "i"]);
560 }
561}