moka/future/builder.rs
1use super::{Cache, FutureExt};
2use crate::{
3 common::{builder_utils, concurrent::Weigher, time::Clock, HousekeeperConfig},
4 notification::{AsyncEvictionListener, ListenerFuture, 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] with various configuration knobs.
18///
19/// [cache-struct]: ./struct.Cache.html
20///
21/// # Example: Expirations
22///
23/// ```rust
24/// // Cargo.toml
25/// //
26/// // [dependencies]
27/// // moka = { version = "0.12", features = ["future"] }
28/// // tokio = { version = "1", features = ["rt-multi-thread", "macros" ] }
29/// // futures = "0.3"
30///
31/// use moka::future::Cache;
32/// use std::time::Duration;
33///
34/// #[tokio::main]
35/// async fn main() {
36/// let cache = Cache::builder()
37/// // Max 10,000 entries
38/// .max_capacity(10_000)
39/// // Time to live (TTL): 30 minutes
40/// .time_to_live(Duration::from_secs(30 * 60))
41/// // Time to idle (TTI): 5 minutes
42/// .time_to_idle(Duration::from_secs( 5 * 60))
43/// // Create the cache.
44/// .build();
45///
46/// // This entry will expire after 5 minutes (TTI) if there is no get().
47/// cache.insert(0, "zero").await;
48///
49/// // This get() will extend the entry life for another 5 minutes.
50/// cache.get(&0);
51///
52/// // Even though we keep calling get(), the entry will expire
53/// // after 30 minutes (TTL) from the insert().
54/// }
55/// ```
56///
57#[must_use]
58pub struct CacheBuilder<K, V, C> {
59 name: Option<String>,
60 max_capacity: Option<u64>,
61 initial_capacity: Option<usize>,
62 weigher: Option<Weigher<K, V>>,
63 eviction_policy: EvictionPolicy,
64 eviction_listener: Option<AsyncEvictionListener<K, V>>,
65 expiration_policy: ExpirationPolicy<K, V>,
66 housekeeper_config: HousekeeperConfig,
67 invalidator_enabled: bool,
68 clock: Clock,
69 cache_type: PhantomData<C>,
70}
71
72impl<K, V> Default for CacheBuilder<K, V, Cache<K, V, RandomState>>
73where
74 K: Eq + Hash + Send + Sync + 'static,
75 V: Clone + Send + Sync + 'static,
76{
77 fn default() -> Self {
78 Self {
79 name: None,
80 max_capacity: None,
81 initial_capacity: None,
82 weigher: None,
83 eviction_policy: EvictionPolicy::default(),
84 eviction_listener: None,
85 expiration_policy: ExpirationPolicy::default(),
86 housekeeper_config: HousekeeperConfig::default(),
87 invalidator_enabled: false,
88 clock: Clock::default(),
89 cache_type: PhantomData,
90 }
91 }
92}
93
94impl<K, V> CacheBuilder<K, V, Cache<K, V, RandomState>>
95where
96 K: Eq + Hash + Send + Sync + 'static,
97 V: Clone + Send + Sync + 'static,
98{
99 /// Construct a new `CacheBuilder` that will be used to build a `Cache` holding
100 /// up to `max_capacity` entries.
101 pub fn new(max_capacity: u64) -> Self {
102 Self {
103 max_capacity: Some(max_capacity),
104 ..Self::default()
105 }
106 }
107
108 /// Builds a `Cache<K, V>`.
109 ///
110 /// # Panics
111 ///
112 /// Panics if configured with either `time_to_live` or `time_to_idle` higher than
113 /// 1000 years. This is done to protect against overflow when computing key
114 /// expiration.
115 pub fn build(self) -> Cache<K, V, RandomState> {
116 let build_hasher = RandomState::default();
117 let exp = &self.expiration_policy;
118 builder_utils::ensure_expirations_or_panic(exp.time_to_live(), exp.time_to_idle());
119 Cache::with_everything(
120 self.name,
121 self.max_capacity,
122 self.initial_capacity,
123 build_hasher,
124 self.weigher,
125 self.eviction_policy,
126 self.eviction_listener,
127 self.expiration_policy,
128 self.housekeeper_config,
129 self.invalidator_enabled,
130 self.clock,
131 )
132 }
133
134 /// Builds a `Cache<K, V, S>` with the given `hasher` of type `S`.
135 ///
136 /// # Examples
137 ///
138 /// This example uses AHash hasher from [AHash][ahash-crate] crate.
139 ///
140 /// [ahash-crate]: https://crates.io/crates/ahash
141 ///
142 /// ```rust
143 /// // Cargo.toml
144 /// // [dependencies]
145 /// // ahash = "0.8"
146 /// // moka = { version = ..., features = ["future"] }
147 /// // tokio = { version = "1", features = ["rt-multi-thread", "macros" ] }
148 ///
149 /// use moka::future::Cache;
150 ///
151 /// #[tokio::main]
152 /// async fn main() {
153 /// // The type of this cache is: Cache<i32, String, ahash::RandomState>
154 /// let cache = Cache::builder()
155 /// .max_capacity(100)
156 /// .build_with_hasher(ahash::RandomState::default());
157 /// cache.insert(1, "one".to_string()).await;
158 /// }
159 /// ```
160 ///
161 /// Note: If you need to add a type annotation to your cache, you must use the
162 /// form of `Cache<K, V, S>` instead of `Cache<K, V>`. That `S` is the type of
163 /// the build hasher, and its default is the `RandomState` from
164 /// `std::collections::hash_map` module . If you use a different build hasher,
165 /// you must specify `S` explicitly.
166 ///
167 /// Here is a good example:
168 ///
169 /// ```rust
170 /// # use moka::future::Cache;
171 /// # #[tokio::main]
172 /// # async fn main() {
173 /// # let cache = Cache::builder()
174 /// # .build_with_hasher(ahash::RandomState::default());
175 /// struct Good {
176 /// // Specifying the type in Cache<K, V, S> format.
177 /// cache: Cache<i32, String, ahash::RandomState>,
178 /// }
179 ///
180 /// // Storing the cache from above example. This should compile.
181 /// Good { cache };
182 /// # }
183 /// ```
184 ///
185 /// Here is a bad example. This struct cannot store the above cache because it
186 /// does not specify `S`:
187 ///
188 /// ```compile_fail
189 /// # use moka::future::Cache;
190 /// # #[tokio::main]
191 /// # async fn main() {
192 /// # let cache = Cache::builder()
193 /// # .build_with_hasher(ahash::RandomState::default());
194 /// struct Bad {
195 /// // Specifying the type in Cache<K, V> format.
196 /// cache: Cache<i32, String>,
197 /// }
198 ///
199 /// // This should not compile.
200 /// Bad { cache };
201 /// // => error[E0308]: mismatched types
202 /// // expected struct `std::collections::hash_map::RandomState`,
203 /// // found struct `ahash::RandomState`
204 /// # }
205 /// ```
206 ///
207 /// # Panics
208 ///
209 /// Panics if configured with either `time_to_live` or `time_to_idle` higher than
210 /// 1000 years. This is done to protect against overflow when computing key
211 /// expiration.
212 pub fn build_with_hasher<S>(self, hasher: S) -> Cache<K, V, S>
213 where
214 S: BuildHasher + Clone + Send + Sync + 'static,
215 {
216 let exp = &self.expiration_policy;
217 builder_utils::ensure_expirations_or_panic(exp.time_to_live(), exp.time_to_idle());
218 Cache::with_everything(
219 self.name,
220 self.max_capacity,
221 self.initial_capacity,
222 hasher,
223 self.weigher,
224 self.eviction_policy,
225 self.eviction_listener,
226 self.expiration_policy,
227 self.housekeeper_config,
228 self.invalidator_enabled,
229 self.clock,
230 )
231 }
232}
233
234impl<K, V, C> CacheBuilder<K, V, C> {
235 /// Sets the name of the cache. Currently the name is used for identification
236 /// only in logging messages.
237 pub fn name(self, name: &str) -> Self {
238 Self {
239 name: Some(name.to_string()),
240 ..self
241 }
242 }
243
244 /// Sets the max capacity of the cache.
245 pub fn max_capacity(self, max_capacity: u64) -> Self {
246 Self {
247 max_capacity: Some(max_capacity),
248 ..self
249 }
250 }
251
252 /// Sets the initial capacity (number of entries) of the cache.
253 pub fn initial_capacity(self, number_of_entries: usize) -> Self {
254 Self {
255 initial_capacity: Some(number_of_entries),
256 ..self
257 }
258 }
259
260 /// Sets the eviction (and admission) policy of the cache.
261 ///
262 /// The default policy is TinyLFU. See [`EvictionPolicy`][eviction-policy] for
263 /// more details.
264 ///
265 /// [eviction-policy]: ../policy/struct.EvictionPolicy.html
266 pub fn eviction_policy(self, policy: EvictionPolicy) -> Self {
267 Self {
268 eviction_policy: policy,
269 ..self
270 }
271 }
272
273 /// Sets the weigher closure to the cache.
274 ///
275 /// The closure should take `&K` and `&V` as the arguments and returns a `u32`
276 /// representing the relative size of the entry.
277 pub fn weigher(self, weigher: impl Fn(&K, &V) -> u32 + Send + Sync + 'static) -> Self {
278 Self {
279 weigher: Some(Arc::new(weigher)),
280 ..self
281 }
282 }
283
284 /// Sets the eviction listener closure to the cache. The closure should take
285 /// `Arc<K>`, `V` and [`RemovalCause`][removal-cause] as the arguments.
286 ///
287 /// See [this example][example] for a usage of eviction listener.
288 ///
289 /// # Sync or Async Eviction Listener
290 ///
291 /// The closure can be either synchronous or asynchronous, and `CacheBuilder`
292 /// provides two methods for setting the eviction listener closure:
293 ///
294 /// - If you do not need to `.await` anything in the eviction listener, use this
295 /// `eviction_listener` method.
296 /// - If you need to `.await` something in the eviction listener, use
297 /// [`async_eviction_listener`](#method.async_eviction_listener) method
298 /// instead.
299 ///
300 /// # Panics
301 ///
302 /// It is very important to make the listener closure not to panic. Otherwise,
303 /// the cache will stop calling the listener after a panic. This is an intended
304 /// behavior because the cache cannot know whether it is memory safe or not to
305 /// call the panicked listener again.
306 ///
307 /// [removal-cause]: ../notification/enum.RemovalCause.html
308 /// [example]: ./struct.Cache.html#per-entry-expiration-policy
309 pub fn eviction_listener<F>(self, listener: F) -> Self
310 where
311 F: Fn(Arc<K>, V, RemovalCause) + Send + Sync + 'static,
312 {
313 let async_listener = move |k, v, c| {
314 {
315 listener(k, v, c);
316 std::future::ready(())
317 }
318 .boxed()
319 };
320
321 self.async_eviction_listener(async_listener)
322 }
323
324 /// Sets the eviction listener closure to the cache. The closure should take
325 /// `Arc<K>`, `V` and [`RemovalCause`][removal-cause] as the arguments, and
326 /// return a [`ListenerFuture`][listener-future].
327 ///
328 /// See [this example][example] for a usage of asynchronous eviction listener.
329 ///
330 /// # Sync or Async Eviction Listener
331 ///
332 /// The closure can be either synchronous or asynchronous, and `CacheBuilder`
333 /// provides two methods for setting the eviction listener closure:
334 ///
335 /// - If you do not need to `.await` anything in the eviction listener, use
336 /// [`eviction_listener`](#method.eviction_listener) method instead.
337 /// - If you need to `.await` something in the eviction listener, use
338 /// this method.
339 ///
340 /// # Panics
341 ///
342 /// It is very important to make the listener closure not to panic. Otherwise,
343 /// the cache will stop calling the listener after a panic. This is an intended
344 /// behavior because the cache cannot know whether it is memory safe or not to
345 /// call the panicked listener again.
346 ///
347 /// [removal-cause]: ../notification/enum.RemovalCause.html
348 /// [listener-future]: ../notification/type.ListenerFuture.html
349 /// [example]: ./struct.Cache.html#example-eviction-listener
350 pub fn async_eviction_listener<F>(self, listener: F) -> Self
351 where
352 F: Fn(Arc<K>, V, RemovalCause) -> ListenerFuture + Send + Sync + 'static,
353 {
354 Self {
355 eviction_listener: Some(Box::new(listener)),
356 ..self
357 }
358 }
359
360 /// Sets the time to live of the cache.
361 ///
362 /// A cached entry will be expired after the specified duration past from
363 /// `insert`.
364 ///
365 /// # Panics
366 ///
367 /// `CacheBuilder::build*` methods will panic if the given `duration` is longer
368 /// than 1000 years. This is done to protect against overflow when computing key
369 /// expiration.
370 pub fn time_to_live(self, duration: Duration) -> Self {
371 let mut builder = self;
372 builder.expiration_policy.set_time_to_live(duration);
373 builder
374 }
375
376 /// Sets the time to idle of the cache.
377 ///
378 /// A cached entry will be expired after the specified duration past from `get`
379 /// or `insert`.
380 ///
381 /// # Panics
382 ///
383 /// `CacheBuilder::build*` methods will panic if the given `duration` is longer
384 /// than 1000 years. This is done to protect against overflow when computing key
385 /// expiration.
386 pub fn time_to_idle(self, duration: Duration) -> Self {
387 let mut builder = self;
388 builder.expiration_policy.set_time_to_idle(duration);
389 builder
390 }
391
392 /// Sets the given `expiry` to the cache.
393 ///
394 /// See [the example][per-entry-expiration-example] for per-entry expiration
395 /// policy in the `Cache` documentation.
396 ///
397 /// [per-entry-expiration-example]:
398 /// ./struct.Cache.html#per-entry-expiration-policy
399 pub fn expire_after(self, expiry: impl Expiry<K, V> + Send + Sync + 'static) -> Self {
400 let mut builder = self;
401 builder.expiration_policy.set_expiry(Arc::new(expiry));
402 builder
403 }
404
405 #[cfg(test)]
406 pub(crate) fn housekeeper_config(self, conf: HousekeeperConfig) -> Self {
407 Self {
408 housekeeper_config: conf,
409 ..self
410 }
411 }
412
413 #[cfg(test)]
414 pub(crate) fn clock(self, clock: Clock) -> Self {
415 Self { clock, ..self }
416 }
417
418 /// Enables support for [`Cache::invalidate_entries_if`][cache-invalidate-if]
419 /// method.
420 ///
421 /// The cache will maintain additional internal data structures to support
422 /// `invalidate_entries_if` method.
423 ///
424 /// [cache-invalidate-if]: ./struct.Cache.html#method.invalidate_entries_if
425 pub fn support_invalidation_closures(self) -> Self {
426 Self {
427 invalidator_enabled: true,
428 ..self
429 }
430 }
431}
432
433#[cfg(test)]
434mod tests {
435 use super::CacheBuilder;
436
437 use std::time::Duration;
438
439 #[tokio::test]
440 async fn build_cache() {
441 // Cache<char, String>
442 let cache = CacheBuilder::new(100).build();
443 let policy = cache.policy();
444
445 assert_eq!(policy.max_capacity(), Some(100));
446 assert_eq!(policy.time_to_live(), None);
447 assert_eq!(policy.time_to_idle(), None);
448 assert_eq!(policy.num_segments(), 1);
449
450 cache.insert('a', "Alice").await;
451 assert_eq!(cache.get(&'a').await, Some("Alice"));
452
453 let cache = CacheBuilder::new(100)
454 .time_to_live(Duration::from_secs(45 * 60))
455 .time_to_idle(Duration::from_secs(15 * 60))
456 .build();
457 let policy = cache.policy();
458
459 assert_eq!(policy.max_capacity(), Some(100));
460 assert_eq!(policy.time_to_live(), Some(Duration::from_secs(45 * 60)));
461 assert_eq!(policy.time_to_idle(), Some(Duration::from_secs(15 * 60)));
462 assert_eq!(policy.num_segments(), 1);
463
464 cache.insert('a', "Alice").await;
465 assert_eq!(cache.get(&'a').await, Some("Alice"));
466 }
467
468 #[tokio::test]
469 #[should_panic(expected = "time_to_live is longer than 1000 years")]
470 async fn build_cache_too_long_ttl() {
471 let thousand_years_secs: u64 = 1000 * 365 * 24 * 3600;
472 let builder: CacheBuilder<char, String, _> = CacheBuilder::new(100);
473 let duration = Duration::from_secs(thousand_years_secs);
474 builder
475 .time_to_live(duration + Duration::from_secs(1))
476 .build();
477 }
478
479 #[tokio::test]
480 #[should_panic(expected = "time_to_idle is longer than 1000 years")]
481 async fn build_cache_too_long_tti() {
482 let thousand_years_secs: u64 = 1000 * 365 * 24 * 3600;
483 let builder: CacheBuilder<char, String, _> = CacheBuilder::new(100);
484 let duration = Duration::from_secs(thousand_years_secs);
485 builder
486 .time_to_idle(duration + Duration::from_secs(1))
487 .build();
488 }
489}