moka/sync/builder.rs
1use super::{Cache, SegmentedCache};
2use crate::{
3 common::{builder_utils, concurrent::Weigher, time::Clock, HousekeeperConfig},
4 notification::{EvictionListener, RemovalCause},
5 policy::{EvictionPolicy, ExpirationPolicy},
6 Expiry,
7};
8
9use std::{
10 collections::hash_map::RandomState,
11 hash::{BuildHasher, Hash},
12 marker::PhantomData,
13 sync::Arc,
14 time::Duration,
15};
16
17/// Builds a [`Cache`][cache-struct] or [`SegmentedCache`][seg-cache-struct]
18/// with various configuration knobs.
19///
20/// [cache-struct]: ./struct.Cache.html
21/// [seg-cache-struct]: ./struct.SegmentedCache.html
22///
23/// # Example: Expirations
24///
25/// ```rust
26/// use moka::sync::Cache;
27/// use std::time::Duration;
28///
29/// let cache = Cache::builder()
30/// // Max 10,000 entries
31/// .max_capacity(10_000)
32/// // Time to live (TTL): 30 minutes
33/// .time_to_live(Duration::from_secs(30 * 60))
34/// // Time to idle (TTI): 5 minutes
35/// .time_to_idle(Duration::from_secs( 5 * 60))
36/// // Create the cache.
37/// .build();
38///
39/// // This entry will expire after 5 minutes (TTI) if there is no get().
40/// cache.insert(0, "zero");
41///
42/// // This get() will extend the entry life for another 5 minutes.
43/// cache.get(&0);
44///
45/// // Even though we keep calling get(), the entry will expire
46/// // after 30 minutes (TTL) from the insert().
47/// ```
48///
49#[must_use]
50pub struct CacheBuilder<K, V, C> {
51 name: Option<String>,
52 max_capacity: Option<u64>,
53 initial_capacity: Option<usize>,
54 num_segments: Option<usize>,
55 weigher: Option<Weigher<K, V>>,
56 eviction_policy: EvictionPolicy,
57 eviction_listener: Option<EvictionListener<K, V>>,
58 expiration_policy: ExpirationPolicy<K, V>,
59 housekeeper_config: HousekeeperConfig,
60 invalidator_enabled: bool,
61 clock: Clock,
62 cache_type: PhantomData<C>,
63}
64
65impl<K, V> Default for CacheBuilder<K, V, Cache<K, V, RandomState>>
66where
67 K: Eq + Hash + Send + Sync + 'static,
68 V: Clone + Send + Sync + 'static,
69{
70 fn default() -> Self {
71 Self {
72 name: None,
73 max_capacity: None,
74 initial_capacity: None,
75 num_segments: None,
76 weigher: None,
77 eviction_listener: None,
78 eviction_policy: EvictionPolicy::default(),
79 expiration_policy: ExpirationPolicy::default(),
80 housekeeper_config: HousekeeperConfig::default(),
81 invalidator_enabled: false,
82 clock: Clock::default(),
83 cache_type: PhantomData,
84 }
85 }
86}
87
88impl<K, V> CacheBuilder<K, V, Cache<K, V, RandomState>>
89where
90 K: Eq + Hash + Send + Sync + 'static,
91 V: Clone + Send + Sync + 'static,
92{
93 /// Construct a new `CacheBuilder` that will be used to build a `Cache` or
94 /// `SegmentedCache` holding up to `max_capacity` entries.
95 pub fn new(max_capacity: u64) -> Self {
96 Self {
97 max_capacity: Some(max_capacity),
98 ..Default::default()
99 }
100 }
101
102 /// Sets the number of segments of the cache.
103 ///
104 /// # Panics
105 ///
106 /// Panics if `num_segments` is zero.
107 pub fn segments(
108 self,
109 num_segments: usize,
110 ) -> CacheBuilder<K, V, SegmentedCache<K, V, RandomState>> {
111 assert!(num_segments != 0);
112
113 CacheBuilder {
114 name: self.name,
115 max_capacity: self.max_capacity,
116 initial_capacity: self.initial_capacity,
117 num_segments: Some(num_segments),
118 weigher: self.weigher,
119 eviction_policy: self.eviction_policy,
120 eviction_listener: self.eviction_listener,
121 expiration_policy: self.expiration_policy,
122 housekeeper_config: self.housekeeper_config,
123 invalidator_enabled: self.invalidator_enabled,
124 clock: self.clock,
125 cache_type: PhantomData,
126 }
127 }
128
129 /// Builds a `Cache<K, V>`.
130 ///
131 /// If you want to build a `SegmentedCache<K, V>`, call `segments` method before
132 /// calling this method.
133 ///
134 /// # Panics
135 ///
136 /// Panics if configured with either `time_to_live` or `time_to_idle` higher than
137 /// 1000 years. This is done to protect against overflow when computing key
138 /// expiration.
139 pub fn build(self) -> Cache<K, V, RandomState> {
140 let build_hasher = RandomState::default();
141 let exp = &self.expiration_policy;
142 builder_utils::ensure_expirations_or_panic(exp.time_to_live(), exp.time_to_idle());
143 Cache::with_everything(
144 self.name,
145 self.max_capacity,
146 self.initial_capacity,
147 build_hasher,
148 self.weigher,
149 self.eviction_policy,
150 self.eviction_listener,
151 self.expiration_policy,
152 self.housekeeper_config,
153 self.invalidator_enabled,
154 self.clock,
155 )
156 }
157
158 /// Builds a `Cache<K, V, S>` with the given `hasher` of type `S`.
159 ///
160 /// # Examples
161 ///
162 /// This example uses AHash hasher from [AHash][ahash-crate] crate.
163 ///
164 /// [ahash-crate]: https://crates.io/crates/ahash
165 ///
166 /// ```rust
167 /// // Cargo.toml
168 /// // [dependencies]
169 /// // ahash = "0.8"
170 /// // moka = ...
171 ///
172 /// use moka::sync::Cache;
173 ///
174 /// // The type of this cache is: Cache<i32, String, ahash::RandomState>
175 /// let cache = Cache::builder()
176 /// .max_capacity(100)
177 /// .build_with_hasher(ahash::RandomState::default());
178 /// cache.insert(1, "one".to_string());
179 /// ```
180 ///
181 /// Note: If you need to add a type annotation to your cache, you must use the
182 /// form of `Cache<K, V, S>` instead of `Cache<K, V>`. That `S` is the type of
183 /// the build hasher, and its default is the `RandomState` from
184 /// `std::collections::hash_map` module . If you use a different build hasher,
185 /// you must specify `S` explicitly.
186 ///
187 /// Here is a good example:
188 ///
189 /// ```rust
190 /// # use moka::sync::Cache;
191 /// # let cache = Cache::builder()
192 /// # .build_with_hasher(ahash::RandomState::default());
193 /// struct Good {
194 /// // Specifying the type in Cache<K, V, S> format.
195 /// cache: Cache<i32, String, ahash::RandomState>,
196 /// }
197 ///
198 /// // Storing the cache from above example. This should compile.
199 /// Good { cache };
200 /// ```
201 ///
202 /// Here is a bad example. This struct cannot store the above cache because it
203 /// does not specify `S`:
204 ///
205 /// ```compile_fail
206 /// # use moka::sync::Cache;
207 /// # let cache = Cache::builder()
208 /// # .build_with_hasher(ahash::RandomState::default());
209 /// struct Bad {
210 /// // Specifying the type in Cache<K, V> format.
211 /// cache: Cache<i32, String>,
212 /// }
213 ///
214 /// // This should not compile.
215 /// Bad { cache };
216 /// // => error[E0308]: mismatched types
217 /// // expected struct `std::collections::hash_map::RandomState`,
218 /// // found struct `ahash::RandomState`
219 /// ```
220 ///
221 /// # Panics
222 ///
223 /// Panics if configured with either `time_to_live` or `time_to_idle` higher than
224 /// 1000 years. This is done to protect against overflow when computing key
225 /// expiration.
226 pub fn build_with_hasher<S>(self, hasher: S) -> Cache<K, V, S>
227 where
228 S: BuildHasher + Clone + Send + Sync + 'static,
229 {
230 let exp = &self.expiration_policy;
231 builder_utils::ensure_expirations_or_panic(exp.time_to_live(), exp.time_to_idle());
232 Cache::with_everything(
233 self.name,
234 self.max_capacity,
235 self.initial_capacity,
236 hasher,
237 self.weigher,
238 self.eviction_policy,
239 self.eviction_listener,
240 self.expiration_policy,
241 self.housekeeper_config,
242 self.invalidator_enabled,
243 self.clock,
244 )
245 }
246}
247
248impl<K, V> CacheBuilder<K, V, SegmentedCache<K, V, RandomState>>
249where
250 K: Eq + Hash + Send + Sync + 'static,
251 V: Clone + Send + Sync + 'static,
252{
253 /// Builds a `SegmentedCache<K, V>`.
254 ///
255 /// If you want to build a `Cache<K, V>`, do not call `segments` method before
256 /// calling this method.
257 ///
258 /// # Panics
259 ///
260 /// Panics if configured with either `time_to_live` or `time_to_idle` higher than
261 /// 1000 years. This is done to protect against overflow when computing key
262 /// expiration.
263 pub fn build(self) -> SegmentedCache<K, V, RandomState> {
264 let build_hasher = RandomState::default();
265 let exp = &self.expiration_policy;
266 builder_utils::ensure_expirations_or_panic(exp.time_to_live(), exp.time_to_idle());
267 SegmentedCache::with_everything(
268 self.name,
269 self.max_capacity,
270 self.initial_capacity,
271 self.num_segments.unwrap(),
272 build_hasher,
273 self.weigher,
274 self.eviction_policy,
275 self.eviction_listener,
276 self.expiration_policy,
277 self.housekeeper_config,
278 self.invalidator_enabled,
279 self.clock,
280 )
281 }
282
283 /// Builds a `SegmentedCache<K, V, S>` with the given `hasher`.
284 ///
285 ///
286 /// # Examples
287 ///
288 /// This example uses AHash hasher from [AHash][ahash-crate] crate.
289 ///
290 /// [ahash-crate]: https://crates.io/crates/ahash
291 ///
292 /// ```rust
293 /// // Cargo.toml
294 /// // [dependencies]
295 /// // ahash = "0.8"
296 /// // moka = ...
297 ///
298 /// use moka::sync::SegmentedCache;
299 ///
300 /// // The type of this cache is: SegmentedCache<i32, String, ahash::RandomState>
301 /// let cache = SegmentedCache::builder(4)
302 /// .max_capacity(100)
303 /// .build_with_hasher(ahash::RandomState::default());
304 /// cache.insert(1, "one".to_string());
305 /// ```
306 ///
307 /// Note: If you need to add a type annotation to your cache, you must use the
308 /// form of `SegmentedCache<K, V, S>` instead of `SegmentedCache<K, V>`. That `S`
309 /// is the type of the build hasher, whose default is the `RandomState` from
310 /// `std::collections::hash_map` module . If you use a different build hasher,
311 /// you must specify `S` explicitly.
312 ///
313 /// Here is a good example:
314 ///
315 /// ```rust
316 /// # use moka::sync::SegmentedCache;
317 /// # let cache = SegmentedCache::builder(4)
318 /// # .build_with_hasher(ahash::RandomState::default());
319 /// struct Good {
320 /// // Specifying the type in SegmentedCache<K, V, S> format.
321 /// cache: SegmentedCache<i32, String, ahash::RandomState>,
322 /// }
323 ///
324 /// // Storing the cache from above example. This should compile.
325 /// Good { cache };
326 /// ```
327 ///
328 /// Here is a bad example. This struct cannot store the above cache because it
329 /// does not specify `S`:
330 ///
331 /// ```compile_fail
332 /// # use moka::sync::SegmentedCache;
333 /// # let cache = SegmentedCache::builder(4)
334 /// # .build_with_hasher(ahash::RandomState::default());
335 /// struct Bad {
336 /// // Specifying the type in SegmentedCache<K, V> format.
337 /// cache: SegmentedCache<i32, String>,
338 /// }
339 ///
340 /// // This should not compile.
341 /// Bad { cache };
342 /// // => error[E0308]: mismatched types
343 /// // expected struct `std::collections::hash_map::RandomState`,
344 /// // found struct `ahash::RandomState`
345 /// ```
346 ///
347 /// # Panics
348 ///
349 /// Panics if configured with either `time_to_live` or `time_to_idle` higher than
350 /// 1000 years. This is done to protect against overflow when computing key
351 /// expiration.
352 pub fn build_with_hasher<S>(self, hasher: S) -> SegmentedCache<K, V, S>
353 where
354 S: BuildHasher + Clone + Send + Sync + 'static,
355 {
356 let exp = &self.expiration_policy;
357 builder_utils::ensure_expirations_or_panic(exp.time_to_live(), exp.time_to_idle());
358 SegmentedCache::with_everything(
359 self.name,
360 self.max_capacity,
361 self.initial_capacity,
362 self.num_segments.unwrap(),
363 hasher,
364 self.weigher,
365 self.eviction_policy,
366 self.eviction_listener,
367 self.expiration_policy,
368 self.housekeeper_config,
369 self.invalidator_enabled,
370 self.clock,
371 )
372 }
373}
374
375impl<K, V, C> CacheBuilder<K, V, C> {
376 /// Sets the name of the cache. Currently the name is used for identification
377 /// only in logging messages.
378 pub fn name(self, name: &str) -> Self {
379 Self {
380 name: Some(name.to_string()),
381 ..self
382 }
383 }
384
385 /// Sets the max capacity of the cache.
386 pub fn max_capacity(self, max_capacity: u64) -> Self {
387 Self {
388 max_capacity: Some(max_capacity),
389 ..self
390 }
391 }
392
393 /// Sets the initial capacity (number of entries) of the cache.
394 pub fn initial_capacity(self, number_of_entries: usize) -> Self {
395 Self {
396 initial_capacity: Some(number_of_entries),
397 ..self
398 }
399 }
400
401 /// Sets the eviction (and admission) policy of the cache.
402 ///
403 /// The default policy is TinyLFU. See [`EvictionPolicy`][eviction-policy] for
404 /// more details.
405 ///
406 /// [eviction-policy]: ../policy/struct.EvictionPolicy.html
407 pub fn eviction_policy(self, policy: EvictionPolicy) -> Self {
408 Self {
409 eviction_policy: policy,
410 ..self
411 }
412 }
413
414 /// Sets the weigher closure to the cache.
415 ///
416 /// The closure should take `&K` and `&V` as the arguments and returns a `u32`
417 /// representing the relative size of the entry.
418 pub fn weigher(self, weigher: impl Fn(&K, &V) -> u32 + Send + Sync + 'static) -> Self {
419 Self {
420 weigher: Some(Arc::new(weigher)),
421 ..self
422 }
423 }
424
425 /// Sets the eviction listener closure to the cache.
426 ///
427 /// The closure should take `Arc<K>`, `V` and [`RemovalCause`][removal-cause] as
428 /// the arguments.
429 ///
430 /// # Panics
431 ///
432 /// It is very important to make the listener closure not to panic. Otherwise,
433 /// the cache will stop calling the listener after a panic. This is an intended
434 /// behavior because the cache cannot know whether it is memory safe or not to
435 /// call the panicked listener again.
436 ///
437 /// [removal-cause]: ../notification/enum.RemovalCause.html
438 pub fn eviction_listener(
439 self,
440 listener: impl Fn(Arc<K>, V, RemovalCause) + Send + Sync + 'static,
441 ) -> Self {
442 Self {
443 eviction_listener: Some(Arc::new(listener)),
444 ..self
445 }
446 }
447
448 /// Sets the time to live of the cache.
449 ///
450 /// A cached entry will be expired after the specified duration past from
451 /// `insert`.
452 ///
453 /// # Panics
454 ///
455 /// `CacheBuilder::build*` methods will panic if the given `duration` is longer
456 /// than 1000 years. This is done to protect against overflow when computing key
457 /// expiration.
458 pub fn time_to_live(self, duration: Duration) -> Self {
459 let mut builder = self;
460 builder.expiration_policy.set_time_to_live(duration);
461 builder
462 }
463
464 /// Sets the time to idle of the cache.
465 ///
466 /// A cached entry will be expired after the specified duration past from `get`
467 /// or `insert`.
468 ///
469 /// # Panics
470 ///
471 /// `CacheBuilder::build*` methods will panic if the given `duration` is longer
472 /// than 1000 years. This is done to protect against overflow when computing key
473 /// expiration.
474 pub fn time_to_idle(self, duration: Duration) -> Self {
475 let mut builder = self;
476 builder.expiration_policy.set_time_to_idle(duration);
477 builder
478 }
479
480 /// Sets the given `expiry` to the cache.
481 ///
482 /// See [the example][per-entry-expiration-example] for per-entry expiration
483 /// policy in the `Cache` documentation.
484 ///
485 /// [per-entry-expiration-example]:
486 /// ./struct.Cache.html#per-entry-expiration-policy
487 pub fn expire_after(self, expiry: impl Expiry<K, V> + Send + Sync + 'static) -> Self {
488 let mut builder = self;
489 builder.expiration_policy.set_expiry(Arc::new(expiry));
490 builder
491 }
492
493 #[cfg(test)]
494 pub(crate) fn housekeeper_config(self, conf: HousekeeperConfig) -> Self {
495 Self {
496 housekeeper_config: conf,
497 ..self
498 }
499 }
500
501 #[cfg(test)]
502 pub(crate) fn clock(self, clock: Clock) -> Self {
503 Self { clock, ..self }
504 }
505
506 /// Enables support for [`Cache::invalidate_entries_if`][cache-invalidate-if]
507 /// method.
508 ///
509 /// The cache will maintain additional internal data structures to support
510 /// `invalidate_entries_if` method.
511 ///
512 /// [cache-invalidate-if]: ./struct.Cache.html#method.invalidate_entries_if
513 pub fn support_invalidation_closures(self) -> Self {
514 Self {
515 invalidator_enabled: true,
516 ..self
517 }
518 }
519}
520
521#[cfg(test)]
522mod tests {
523 use super::CacheBuilder;
524
525 use std::time::Duration;
526
527 #[test]
528 fn build_cache() {
529 // Cache<char, String>
530 let cache = CacheBuilder::new(100).build();
531 let policy = cache.policy();
532
533 assert_eq!(policy.max_capacity(), Some(100));
534 assert_eq!(policy.time_to_live(), None);
535 assert_eq!(policy.time_to_idle(), None);
536 assert_eq!(policy.num_segments(), 1);
537
538 cache.insert('a', "Alice");
539 assert_eq!(cache.get(&'a'), Some("Alice"));
540
541 let cache = CacheBuilder::new(100)
542 .time_to_live(Duration::from_secs(45 * 60))
543 .time_to_idle(Duration::from_secs(15 * 60))
544 .build();
545 let config = cache.policy();
546
547 assert_eq!(config.max_capacity(), Some(100));
548 assert_eq!(config.time_to_live(), Some(Duration::from_secs(45 * 60)));
549 assert_eq!(config.time_to_idle(), Some(Duration::from_secs(15 * 60)));
550 assert_eq!(config.num_segments(), 1);
551
552 cache.insert('a', "Alice");
553 assert_eq!(cache.get(&'a'), Some("Alice"));
554 }
555
556 #[test]
557 fn build_segmented_cache() {
558 // SegmentCache<char, String>
559 let cache = CacheBuilder::new(100).segments(15).build();
560 let policy = cache.policy();
561
562 assert_eq!(policy.max_capacity(), Some(100));
563 assert!(policy.time_to_live().is_none());
564 assert!(policy.time_to_idle().is_none());
565 assert_eq!(policy.num_segments(), 16_usize.next_power_of_two());
566
567 cache.insert('b', "Bob");
568 assert_eq!(cache.get(&'b'), Some("Bob"));
569
570 let listener = move |_key, _value, _cause| ();
571
572 let builder = CacheBuilder::new(400)
573 .time_to_live(Duration::from_secs(45 * 60))
574 .time_to_idle(Duration::from_secs(15 * 60))
575 .eviction_listener(listener)
576 .name("tracked_sessions")
577 // Call segments() at the end to check all field values in the current
578 // builder struct are copied to the new builder:
579 // https://github.com/moka-rs/moka/issues/207
580 .segments(24);
581
582 assert!(builder.eviction_listener.is_some());
583
584 let cache = builder.build();
585 let policy = cache.policy();
586
587 assert_eq!(policy.max_capacity(), Some(400));
588 assert_eq!(policy.time_to_live(), Some(Duration::from_secs(45 * 60)));
589 assert_eq!(policy.time_to_idle(), Some(Duration::from_secs(15 * 60)));
590 assert_eq!(policy.num_segments(), 24_usize.next_power_of_two());
591 assert_eq!(cache.name(), Some("tracked_sessions"));
592
593 cache.insert('b', "Bob");
594 assert_eq!(cache.get(&'b'), Some("Bob"));
595 }
596
597 #[test]
598 #[should_panic(expected = "time_to_live is longer than 1000 years")]
599 fn build_cache_too_long_ttl() {
600 let thousand_years_secs: u64 = 1000 * 365 * 24 * 3600;
601 let builder: CacheBuilder<char, String, _> = CacheBuilder::new(100);
602 let duration = Duration::from_secs(thousand_years_secs);
603 builder
604 .time_to_live(duration + Duration::from_secs(1))
605 .build();
606 }
607
608 #[test]
609 #[should_panic(expected = "time_to_idle is longer than 1000 years")]
610 fn build_cache_too_long_tti() {
611 let thousand_years_secs: u64 = 1000 * 365 * 24 * 3600;
612 let builder: CacheBuilder<char, String, _> = CacheBuilder::new(100);
613 let duration = Duration::from_secs(thousand_years_secs);
614 builder
615 .time_to_idle(duration + Duration::from_secs(1))
616 .build();
617 }
618}