diff --git a/Cargo.toml b/Cargo.toml index b3795f6b..ce32046b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -120,19 +120,3 @@ rustc_version = "0.4.0" # Build the doc with some features enabled. features = ["future"] rustdoc-args = ["--cfg", "docsrs"] - -# ---------------------------------- -# RUSTSEC, etc. -# -# crossbeam-channel: -# - Workaround a bug in upstream related to TLS access on AArch64 Linux: -# - https://github.com/crossbeam-rs/crossbeam/pull/802 (Patched >= 0.5.4) -# - Addressed some stacked borrow violations found by Miri: -# - https://github.com/crossbeam-rs/crossbeam/blob/master/crossbeam-channel/CHANGELOG.md#version-052 (Patched >= 0.5.2) -# -# smallvec: -# - https://rustsec.org/advisories/RUSTSEC-2021-0003.html (Patched >= 1.6.1) -# -# Tokio: -# - https://rustsec.org/advisories/RUSTSEC-2021-0124.html (Patched >= 1.13.1) -# - https://rustsec.org/advisories/RUSTSEC-2021-0072.html (Patched >= 1.8.1) diff --git a/README.md b/README.md index 285fa601..ff3524be 100644 --- a/README.md +++ b/README.md @@ -135,10 +135,9 @@ The `unsync::Cache` and `dash::Cache` have been moved to a separate crate called - [Synchronous Cache](#example-synchronous-cache) - [Asynchronous Cache](#example-asynchronous-cache) - [Avoiding to clone the value at `get`](#avoiding-to-clone-the-value-at-get) -- Examples (Part 2) +- Example (Part 2) - [Size Aware Eviction](#example-size-aware-eviction) - - [Expiration Policies](#example-expiration-policies) -- [Hashing Algorithm](#hashing-algorithm) +- [Expiration Policies](#expiration-policies) - [Minimum Supported Rust Versions](#minimum-supported-rust-versions) - Troubleshooting - [Integer Overflow in Quanta Crate on Some x86_64 Machines](#integer-overflow-in-quanta-crate-on-some-x86_64-machines) @@ -392,67 +391,26 @@ fn main() { Note that weighted sizes are not used when making eviction selections. -## Example: Expiration Policies +## Expiration Policies Moka supports the following expiration policies: -- **Time to live**: A cached entry will be expired after the specified duration past - from `insert`. -- **Time to idle**: A cached entry will be expired after the specified duration past - from `get` or `insert`. +- **Cache-level expiration policies:** + - Cache-level policies are applied to all entries in the cache. + - **Time to live (TTL)**: A cached entry will be expired after the specified + duration past from `insert`. + - **Time to idle (TTI)**: A cached entry will be expired after the specified + duration past from `get` or `insert`. +- **Per-entry expiration policy:** + - The per-entry expiration lets you sets a different expiration time for each + entry. -To set them, use the `CacheBuilder`. +For details and examples of above policies, see the "Example: Time-based Expiration" +section ([`sync::Cache`][doc-sync-cache-expiration], +[`future::Cache`][doc-future-cache-expiration]) of the document. -```rust -use moka::sync::Cache; -use std::time::Duration; - -fn main() { - let cache = Cache::builder() - // Time to live (TTL): 30 minutes - .time_to_live(Duration::from_secs(30 * 60)) - // Time to idle (TTI): 5 minutes - .time_to_idle(Duration::from_secs( 5 * 60)) - // Create the cache. - .build(); - - // This entry will expire after 5 minutes (TTI) if there is no get(). - cache.insert(0, "zero"); - - // This get() will extend the entry life for another 5 minutes. - cache.get(&0); - - // Even though we keep calling get(), the entry will expire - // after 30 minutes (TTL) from the insert(). -} -``` - -### A note on expiration policies - -The cache builders will panic if configured with either `time_to_live` or `time to idle` -longer than 1000 years. This is done to protect against overflow when computing key -expiration. - - -## Hashing Algorithm - -By default, a cache uses a hashing algorithm selected to provide resistance against -HashDoS attacks. - -The default hashing algorithm is the one used by `std::collections::HashMap`, which -is currently SipHash 1-3, though this is subject to change at any point in the -future. - -While its performance is very competitive for medium sized keys, other hashing -algorithms will outperform it for small keys such as integers as well as large keys -such as long strings. However those algorithms will typically not protect against -attacks such as HashDoS. - -The hashing algorithm can be replaced on a per-`Cache` basis using the -`build_with_hasher` method of the `CacheBuilder`. Many alternative algorithms are -available on crates.io, such as the [AHash][ahash-crate] crate. - -[ahash-crate]: https://crates.io/crates/ahash +[doc-sync-cache-expiration]: https://docs.rs/moka/latest/moka/sync/struct.Cache.html#example-time-based-expirations +[doc-future-cache-expiration]: https://docs.rs/moka/latest/moka/future/struct.Cache.html#example-time-based-expirations ## Minimum Supported Rust Versions @@ -504,7 +462,7 @@ to the dependency declaration. ```toml:Cargo.toml [dependencies] -moka = { version = "0.11", default-features = false } +moka = { version = "0.11", default-features = false, features = ["sync"] } # Or moka = { version = "0.11", default-features = false, features = ["future"] } ``` @@ -534,7 +492,7 @@ $ RUSTFLAGS='--cfg skeptic --cfg trybuild' cargo test \ ```console $ cargo +nightly -Z unstable-options --config 'build.rustdocflags="--cfg docsrs"' \ - doc --no-deps --features 'future, dash' + doc --no-deps --features future ``` ## Road Map diff --git a/src/common/deque.rs b/src/common/deque.rs index d2022c5d..a10d64ae 100644 --- a/src/common/deque.rs +++ b/src/common/deque.rs @@ -116,7 +116,6 @@ impl Deque { self.region } - #[cfg(any(test, feature = "sync", feature = "future"))] pub(crate) fn len(&self) -> usize { self.len } diff --git a/src/future/builder.rs b/src/future/builder.rs index 95b9cb59..e3055944 100644 --- a/src/future/builder.rs +++ b/src/future/builder.rs @@ -323,6 +323,13 @@ impl CacheBuilder { builder } + /// Sets the given `expiry` to the cache. + /// + /// See [the example][per-entry-expiration-example] for per-entry expiration + /// policy in the `Cache` documentation. + /// + /// [per-entry-expiration-example]: + /// ./struct.Cache.html#per-entry-expiration-policy pub fn expire_after(self, expiry: impl Expiry + Send + Sync + 'static) -> Self { let mut builder = self; builder.expiration_policy.set_expiry(Arc::new(expiry)); diff --git a/src/future/cache.rs b/src/future/cache.rs index c5590fe4..020debda 100644 --- a/src/future/cache.rs +++ b/src/future/cache.rs @@ -49,14 +49,16 @@ use std::{ /// /// - [Example: `insert`, `get` and `invalidate`](#example-insert-get-and-invalidate) /// - [Avoiding to clone the value at `get`](#avoiding-to-clone-the-value-at-get) +/// - [Sharing a cache across asynchronous tasks](#sharing-a-cache-across-asynchronous-tasks) +/// - [No lock is needed](#no-lock-is-needed) +/// - [Hashing Algorithm](#hashing-algorithm) /// - [Example: Size-based Eviction](#example-size-based-eviction) /// - [Example: Time-based Expirations](#example-time-based-expirations) +/// - [Cache-level TTL and TTI policies](#cache-level-ttl-and-tti-policies) +/// - [Per-entry expiration policy](#per-entry-expiration-policy) /// - [Example: Eviction Listener](#example-eviction-listener) /// - [You should avoid eviction listener to panic](#you-should-avoid-eviction-listener-to-panic) -/// - [Delivery Modes for Eviction Listener](#delivery-modes-for-eviction-listener) -/// - [Thread Safety](#thread-safety) -/// - [Sharing a cache across threads](#sharing-a-cache-across-threads) -/// - [Hashing Algorithm](#hashing-algorithm) +/// - [Delivery modes for eviction listener](#delivery-modes-for-eviction-listener) /// /// # Example: `insert`, `get` and `invalidate` /// @@ -158,6 +160,52 @@ use std::{ /// /// [rustdoc-std-arc]: https://doc.rust-lang.org/stable/std/sync/struct.Arc.html /// +/// # Sharing a cache across asynchronous tasks +/// +/// To share a cache across async tasks (or OS threads), do one of the followings: +/// +/// - Create a clone of the cache by calling its `clone` method and pass it to other +/// task. +/// - If you are using a web application framework such as Actix Web or Axum, +/// you can store a cache in Actix Web's [`web::Data`][actix-web-data] or Axum's +/// [shared state][axum-state-extractor], and access it from each request handler. +/// - Wrap the cache by a `sync::OnceCell` or `sync::Lazy` from +/// [once_cell][once-cell-crate] create, and set it to a `static` variable. +/// +/// Cloning is a cheap operation for `Cache` as it only creates thread-safe +/// reference-counted pointers to the internal data structures. +/// +/// [once-cell-crate]: https://crates.io/crates/once_cell +/// [actix-web-data]: https://docs.rs/actix-web/4.3.1/actix_web/web/struct.Data.html +/// [axum-state-extractor]: https://docs.rs/axum/latest/axum/#sharing-state-with-handlers +/// +/// ## No lock is needed +/// +/// Don't wrap a `Cache` by a lock such as `Mutex` or `RwLock`. All methods provided +/// by the `Cache` are considered thread-safe, and can be safely called by multiple +/// async tasks at the same time. No lock is needed. +/// +/// [once-cell-crate]: https://crates.io/crates/once_cell +/// +/// # Hashing Algorithm +/// +/// By default, `Cache` uses a hashing algorithm selected to provide resistance +/// against HashDoS attacks. It will be the same one used by +/// `std::collections::HashMap`, which is currently SipHash 1-3. +/// +/// While SipHash's performance is very competitive for medium sized keys, other +/// hashing algorithms will outperform it for small keys such as integers as well as +/// large keys such as long strings. However those algorithms will typically not +/// protect against attacks such as HashDoS. +/// +/// The hashing algorithm can be replaced on a per-`Cache` basis using the +/// [`build_with_hasher`][build-with-hasher-method] method of the `CacheBuilder`. +/// Many alternative algorithms are available on crates.io, such as the +/// [AHash][ahash-crate] crate. +/// +/// [build-with-hasher-method]: ./struct.CacheBuilder.html#method.build_with_hasher +/// [ahash-crate]: https://crates.io/crates/ahash +/// /// # Example: Size-based Eviction /// /// ```rust @@ -215,12 +263,18 @@ use std::{ /// /// # Example: Time-based Expirations /// -/// `Cache` supports the following expiration policies: +/// ## Cache-level TTL and TTI policies /// -/// - **Time to live**: A cached entry will be expired after the specified duration -/// past from `insert`. -/// - **Time to idle**: A cached entry will be expired after the specified duration -/// past from `get` or `insert`. +/// `Cache` supports the following cache-level expiration policies: +/// +/// - **Time to live (TTL)**: A cached entry will be expired after the specified +/// duration past from `insert`. +/// - **Time to idle (TTI)**: A cached entry will be expired after the specified +/// duration past from `get` or `insert`. +/// +/// They are a cache-level expiration policies; all entries in the cache will have +/// the same TTL and/or TTI durations. If you want to set different expiration +/// durations for different entries, see the next section. /// /// ```rust /// // Cargo.toml @@ -254,6 +308,144 @@ use std::{ /// } /// ``` /// +/// ## Per-entry expiration policy +/// +/// `Cache` supports per-entry expiration policy via the [`Expiry`][expiry-trait] +/// trait. +/// +/// When a cached entry is inserted, read or updated, a duration can be specified for +/// that individual entry. The entry will be expired after the specified duration +/// past from the operation. +/// +/// [expiry-trait]: ../trait.Expiry.html +/// +/// ```rust +/// // Cargo.toml +/// // +/// // [dependencies] +/// // moka = { version = "0.11", features = ["future"] } +/// // tokio = { version = "1", features = ["rt-multi-thread", "macros" ] } +/// +/// use moka::{future::Cache, Expiry}; +/// use std::time::{Duration, Instant}; +/// +/// // In this example, we will create a `future::Cache` with `u32` as the key, and +/// // `(Expiration, String)` as the value. `Expiration` is an enum to represent the +/// // expiration of the value, and `String` is the application data of the value. +/// +/// /// An enum to represent the expiration of a value. +/// #[derive(Clone, Copy, Debug, Eq, PartialEq)] +/// pub enum Expiration { +/// /// The value never expires. +/// Never, +/// /// The value expires after a short time. (5 seconds in this example) +/// AfterShortTime, +/// /// The value expires after a long time. (15 seconds in this example) +/// AfterLongTime, +/// } +/// +/// impl Expiration { +/// /// Returns the duration of this expiration. +/// pub fn as_duration(&self) -> Option { +/// match self { +/// Expiration::Never => None, +/// Expiration::AfterShortTime => Some(Duration::from_secs(5)), +/// Expiration::AfterLongTime => Some(Duration::from_secs(15)), +/// } +/// } +/// } +/// +/// /// An expiry that implements `moka::Expiry` trait. `Expiry` trait provides the +/// /// default implementations of three callback methods `expire_after_create`, +/// /// `expire_after_read`, and `expire_after_update`. +/// /// +/// /// In this example, we only override the `expire_after_create` method. +/// pub struct MyExpiry; +/// +/// impl Expiry for MyExpiry { +/// /// Returns the duration of the expiration of the value that was just +/// /// created. +/// fn expire_after_create( +/// &self, +/// _key: &u32, +/// value: &(Expiration, String), +/// _current_time: Instant, +/// ) -> Option { +/// let duration = value.0.as_duration(); +/// println!("MyExpiry: expire_after_create called with key {_key} and value {value:?}. Returning {duration:?}."); +/// duration +/// } +/// } +/// +/// #[tokio::main] +/// async fn main() { +/// // Create a `Cache` with an expiry `MyExpiry` and +/// // eviction listener. +/// let expiry = MyExpiry; +/// +/// let eviction_listener = |key, _value, cause| { +/// println!("Evicted key {key}. Cause: {cause:?}"); +/// }; +/// +/// let cache = Cache::builder() +/// .max_capacity(100) +/// .expire_after(expiry) +/// .eviction_listener_with_queued_delivery_mode(eviction_listener) +/// .build(); +/// +/// // Insert some entries into the cache with different expirations. +/// cache +/// .get_with(0, async { (Expiration::AfterShortTime, "a".to_string()) }) +/// .await; +/// +/// cache +/// .get_with(1, async { (Expiration::AfterLongTime, "b".to_string()) }) +/// .await; +/// +/// cache +/// .get_with(2, async { (Expiration::Never, "c".to_string()) }) +/// .await; +/// +/// // Verify that all the inserted entries exist. +/// assert!(cache.contains_key(&0)); +/// assert!(cache.contains_key(&1)); +/// assert!(cache.contains_key(&2)); +/// +/// // Sleep for 6 seconds. Key 0 should expire. +/// println!("\nSleeping for 6 seconds...\n"); +/// tokio::time::sleep(Duration::from_secs(6)).await; +/// println!("Entry count: {}", cache.entry_count()); +/// +/// // Verify that key 0 has been evicted. +/// assert!(!cache.contains_key(&0)); +/// assert!(cache.contains_key(&1)); +/// assert!(cache.contains_key(&2)); +/// +/// // Sleep for 10 more seconds. Key 1 should expire. +/// println!("\nSleeping for 10 seconds...\n"); +/// tokio::time::sleep(Duration::from_secs(10)).await; +/// println!("Entry count: {}", cache.entry_count()); +/// +/// // Verify that key 1 has been evicted. +/// assert!(!cache.contains_key(&1)); +/// assert!(cache.contains_key(&2)); +/// +/// // Manually invalidate key 2. +/// cache.invalidate(&2).await; +/// assert!(!cache.contains_key(&2)); +/// +/// println!("\nSleeping for a second...\n"); +/// tokio::time::sleep(Duration::from_secs(1)).await; +/// println!("Entry count: {}", cache.entry_count()); +/// +/// println!("\nDone!"); +/// } +/// ``` +/// +/// The `Expiry` trait provides three methods `expire_after_create`, +/// `expire_after_read` and `expire_after_update` with default implementations. See +/// [its document][expiry-trait] for more details. +/// /// # Example: Eviction Listener /// /// A `Cache` can be configured with an eviction listener, a closure that is called @@ -444,7 +636,7 @@ use std::{ /// /// [builder-name-method]: ./struct.CacheBuilder.html#method.name /// -/// ## Delivery Modes for Eviction Listener +/// ## Delivery Modes for eviction listener /// /// The [`DeliveryMode`][delivery-mode] specifies how and when an eviction /// notification should be delivered to an eviction listener. Currently, the @@ -466,48 +658,6 @@ use std::{ /// [delivery-mode]: ../notification/enum.DeliveryMode.html /// [sync-delivery-modes]: ../sync/struct.Cache.html#delivery-modes-for-eviction-listener /// -/// # Thread Safety -/// -/// All methods provided by the `Cache` are considered thread-safe, and can be safely -/// accessed by multiple concurrent threads. -/// -/// - `Cache` requires trait bounds `Send`, `Sync` and `'static` for `K` -/// (key), `V` (value) and `S` (hasher state). -/// - `Cache` will implement `Send` and `Sync`. -/// -/// # Sharing a cache across asynchronous tasks -/// -/// To share a cache across async tasks (or OS threads), do one of the followings: -/// -/// - Create a clone of the cache by calling its `clone` method and pass it to other -/// task. -/// - Wrap the cache by a `sync::OnceCell` or `sync::Lazy` from -/// [once_cell][once-cell-crate] create, and set it to a `static` variable. -/// -/// Cloning is a cheap operation for `Cache` as it only creates thread-safe -/// reference-counted pointers to the internal data structures. -/// -/// [once-cell-crate]: https://crates.io/crates/once_cell -/// -/// # Hashing Algorithm -/// -/// By default, `Cache` uses a hashing algorithm selected to provide resistance -/// against HashDoS attacks. It will be the same one used by -/// `std::collections::HashMap`, which is currently SipHash 1-3. -/// -/// While SipHash's performance is very competitive for medium sized keys, other -/// hashing algorithms will outperform it for small keys such as integers as well as -/// large keys such as long strings. However those algorithms will typically not -/// protect against attacks such as HashDoS. -/// -/// The hashing algorithm can be replaced on a per-`Cache` basis using the -/// [`build_with_hasher`][build-with-hasher-method] method of the `CacheBuilder`. -/// Many alternative algorithms are available on crates.io, such as the -/// [AHash][ahash-crate] crate. -/// -/// [build-with-hasher-method]: ./struct.CacheBuilder.html#method.build_with_hasher -/// [ahash-crate]: https://crates.io/crates/ahash -/// pub struct Cache { base: BaseCache, value_initializer: Arc>, diff --git a/src/lib.rs b/src/lib.rs index e4a60182..209881d2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,8 +4,8 @@ // #![deny(rustdoc::broken_intra_doc_links)] #![cfg_attr(docsrs, feature(doc_cfg))] -//! Moka is a fast, concurrent cache library for Rust. Moka is inspired by -//! the [Caffeine][caffeine-git] library for Java. +//! Moka is a fast, concurrent cache library for Rust. Moka is inspired by the +//! [Caffeine][caffeine-git] library for Java. //! //! Moka provides in-memory concurrent cache implementations on top of hash maps. //! They support full concurrency of retrievals and a high expected concurrency for @@ -29,13 +29,16 @@ //! - The total weighted size of entries. (Size aware eviction) //! - Maintains good hit rate by using entry replacement algorithms inspired by //! [Caffeine][caffeine-git]: -//! - Admission to a cache is controlled by the Least Frequently Used (LFU) policy. -//! - Eviction from a cache is controlled by the Least Recently Used (LRU) policy. +//! - Admission to a cache is controlled by the Least Frequently Used (LFU) +//! policy. +//! - Eviction from a cache is controlled by the Least Recently Used (LRU) +//! policy. //! - Supports expiration policies: -//! - Time to live -//! - Time to idle -//! - Supports eviction listener, a callback function that will be called when an entry -//! is removed from the cache. +//! - Time to live. +//! - Time to idle. +//! - Per-entry variable expiration. +//! - Supports eviction listener, a callback function that will be called when an +//! entry is removed from the cache. //! //! # Examples //! @@ -60,8 +63,10 @@ //! - `moka::dash::Cache` → [`mini_moka::sync::Cache`][dash-cache-struct] //! //! [mini-moka-crate]: https://crates.io/crates/mini-moka -//! [unsync-cache-struct]: https://docs.rs/mini-moka/latest/mini_moka/unsync/struct.Cache.html -//! [dash-cache-struct]: https://docs.rs/mini-moka/latest/mini_moka/sync/struct.Cache.html +//! [unsync-cache-struct]: +//! https://docs.rs/mini-moka/latest/mini_moka/unsync/struct.Cache.html +//! [dash-cache-struct]: +//! https://docs.rs/mini-moka/latest/mini_moka/sync/struct.Cache.html //! //! # Minimum Supported Rust Versions //! @@ -82,10 +87,10 @@ //! //! ## Concurrency //! -//! In a concurrent cache (`sync` or `future` cache), the entry replacement -//! algorithms are kept eventually consistent with the map. While updates to the -//! cache are immediately applied to the map, recording of reads and writes may not -//! be immediately reflected on the cache policy's data structures. +//! The entry replacement algorithms are kept eventually consistent with the map. +//! While updates to the cache are immediately applied to the map, recording of reads +//! and writes may not be immediately reflected on the cache policy's data +//! structures. //! //! These structures are guarded by a lock and operations are applied in batches to //! avoid lock contention. There are bounded inter-thread channels to hold these @@ -107,11 +112,10 @@ //! the draining task catches up. //! //! `Cache` does its best to avoid blocking updates by adjusting the interval of -//! draining. But since it has only one worker -//! thread, it cannot always avoid blocking. If this happens very often in your cache -//! (in the future, you can check the statistics of the cache), you may want to -//! switch to `SegmentedCache`. It has multiple internal cache segments and each -//! segment has dedicated draining thread. +//! draining. But since it has only one worker thread, it cannot always avoid +//! blocking. If this happens very often in your cache (in the future, you can check +//! the statistics of the cache), you may want to switch to `SegmentedCache`. It has +//! multiple internal cache segments and each segment has dedicated draining thread. //! //! ## Admission and Eviction //! @@ -144,9 +148,6 @@ //! //! - The time-to-live policy //! - The time-to-idle policy -//! -//! A future release will support the following: -//! //! - The variable expiration (which allows to set different expiration on each //! cached entry) //! @@ -154,12 +155,14 @@ //! //! - The time-to-live policy uses a write-order queue. //! - The time-to-idle policy uses an access-order queue. -//! - The variable expiration will use a [hierarchical timer wheel][timer-wheel] (*1). +//! - The variable expiration will use a [hierarchical timer wheel][timer-wheel] +//! (*1). //! //! *1: If you get 404 page not found when you click on the link to the hierarchical //! timer wheel paper, try to change the URL from `https:` to `http:`. //! -//! [timer-wheel]: http://www.cs.columbia.edu/~nahum/w6998/papers/ton97-timing-wheels.pdf +//! [timer-wheel]: +//! http://www.cs.columbia.edu/~nahum/w6998/papers/ton97-timing-wheels.pdf #[cfg(feature = "future")] #[cfg_attr(docsrs, doc(cfg(feature = "future")))] diff --git a/src/sync/builder.rs b/src/sync/builder.rs index aba6d740..3b3d9ecf 100644 --- a/src/sync/builder.rs +++ b/src/sync/builder.rs @@ -489,6 +489,13 @@ impl CacheBuilder { builder } + /// Sets the given `expiry` to the cache. + /// + /// See [the example][per-entry-expiration-example] for per-entry expiration + /// policy in the `Cache` documentation. + /// + /// [per-entry-expiration-example]: + /// ./struct.Cache.html#per-entry-expiration-policy pub fn expire_after(self, expiry: impl Expiry + Send + Sync + 'static) -> Self { let mut builder = self; builder.expiration_policy.set_expiry(Arc::new(expiry)); diff --git a/src/sync/cache.rs b/src/sync/cache.rs index c9989f54..f79fea6e 100644 --- a/src/sync/cache.rs +++ b/src/sync/cache.rs @@ -45,17 +45,20 @@ use std::{ /// /// - [Example: `insert`, `get` and `invalidate`](#example-insert-get-and-invalidate) /// - [Avoiding to clone the value at `get`](#avoiding-to-clone-the-value-at-get) +/// - [Sharing a cache across threads](#sharing-a-cache-across-threads) +/// - [No lock is needed](#no-lock-is-needed) +/// - [Hashing Algorithm](#hashing-algorithm) /// - [Example: Size-based Eviction](#example-size-based-eviction) /// - [Example: Time-based Expirations](#example-time-based-expirations) +/// - [Cache-level TTL and TTI policies](#cache-level-ttl-and-tti-policies) +/// - [Per-entry expiration policy](#per-entry-expiration-policy) /// - [Example: Eviction Listener](#example-eviction-listener) -/// - [You should avoid eviction listener to panic](#you-should-avoid-eviction-listener-to-panic) -/// - [Delivery Modes for Eviction Listener](#delivery-modes-for-eviction-listener) -/// - [`Immediate` Mode](#immediate-mode) -/// - [`Queued` Mode](#queued-mode) +/// - [You should avoid eviction listener to +/// panic](#you-should-avoid-eviction-listener-to-panic) +/// - [Delivery modes for eviction listener](#delivery-modes-for-eviction-listener) +/// - [`Immediate` mode](#immediate-mode) +/// - [`Queued` mode](#queued-mode) /// - [Example: `Queued` Delivery Mode](#example-queued-delivery-mode) -/// - [Thread Safety](#thread-safety) -/// - [Sharing a cache across threads](#sharing-a-cache-across-threads) -/// - [Hashing Algorithm](#hashing-algorithm) /// /// # Example: `insert`, `get` and `invalidate` /// @@ -137,6 +140,45 @@ use std::{ /// /// [rustdoc-std-arc]: https://doc.rust-lang.org/stable/std/sync/struct.Arc.html /// +/// # Sharing a cache across threads +/// +/// To share a cache across threads, do one of the followings: +/// +/// - Create a clone of the cache by calling its `clone` method and pass it to other +/// thread. +/// - Wrap the cache by a `sync::OnceCell` or `sync::Lazy` from +/// [once_cell][once-cell-crate] create, and set it to a `static` variable. +/// +/// Cloning is a cheap operation for `Cache` as it only creates thread-safe +/// reference-counted pointers to the internal data structures. +/// +/// ## No lock is needed +/// +/// Don't wrap a `Cache` by a lock such as `Mutex` or `RwLock`. All methods provided +/// by the `Cache` are considered thread-safe, and can be safely called by multiple +/// threads at the same time. No lock is needed. +/// +/// [once-cell-crate]: https://crates.io/crates/once_cell +/// +/// # Hashing Algorithm +/// +/// By default, `Cache` uses a hashing algorithm selected to provide resistance +/// against HashDoS attacks. It will be the same one used by +/// `std::collections::HashMap`, which is currently SipHash 1-3. +/// +/// While SipHash's performance is very competitive for medium sized keys, other +/// hashing algorithms will outperform it for small keys such as integers as well as +/// large keys such as long strings. However those algorithms will typically not +/// protect against attacks such as HashDoS. +/// +/// The hashing algorithm can be replaced on a per-`Cache` basis using the +/// [`build_with_hasher`][build-with-hasher-method] method of the `CacheBuilder`. +/// Many alternative algorithms are available on crates.io, such as the +/// [AHash][ahash-crate] crate. +/// +/// [build-with-hasher-method]: ./struct.CacheBuilder.html#method.build_with_hasher +/// [ahash-crate]: https://crates.io/crates/ahash +/// /// # Example: Size-based Eviction /// /// ```rust @@ -184,12 +226,18 @@ use std::{ /// /// # Example: Time-based Expirations /// -/// `Cache` supports the following expiration policies: +/// ## Cache-level TTL and TTI policies /// -/// - **Time to live**: A cached entry will be expired after the specified duration -/// past from `insert`. -/// - **Time to idle**: A cached entry will be expired after the specified duration -/// past from `get` or `insert`. +/// `Cache` supports the following cache-level expiration policies: +/// +/// - **Time to live (TTL)**: A cached entry will be expired after the specified +/// duration past from `insert`. +/// - **Time to idle (TTI)**: A cached entry will be expired after the specified +/// duration past from `get` or `insert`. +/// +/// They are a cache-level expiration policies; all entries in the cache will have +/// the same TTL and/or TTI durations. If you want to set different expiration +/// durations for different entries, see the next section. /// /// ```rust /// use moka::sync::Cache; @@ -213,6 +261,127 @@ use std::{ /// // after 30 minutes (TTL) from the insert(). /// ``` /// +/// ## Per-entry expiration policy +/// +/// `Cache` supports per-entry expiration policy via the [`Expiry`][expiry-trait] +/// trait. +/// +/// When a cached entry is inserted, read or updated, a duration can be specified for +/// that individual entry. The entry will be expired after the specified duration +/// past from the operation. +/// +/// [expiry-trait]: ../trait.Expiry.html +/// +/// ```rust +/// use moka::{sync::Cache, Expiry}; +/// use std::time::{Duration, Instant}; +/// +/// // In this example, we will create a `sync::Cache` with `u32` as the key, and +/// // `(Expiration, String)` as the value. `Expiration` is an enum to represent the +/// // expiration of the value, and `String` is the application data of the value. +/// +/// /// An enum to represent the expiration of a value. +/// #[derive(Clone, Copy, Debug, Eq, PartialEq)] +/// pub enum Expiration { +/// /// The value never expires. +/// Never, +/// /// The value expires after a short time. (5 seconds in this example) +/// AfterShortTime, +/// /// The value expires after a long time. (15 seconds in this example) +/// AfterLongTime, +/// } +/// +/// impl Expiration { +/// /// Returns the duration of this expiration. +/// pub fn as_duration(&self) -> Option { +/// match self { +/// Expiration::Never => None, +/// Expiration::AfterShortTime => Some(Duration::from_secs(5)), +/// Expiration::AfterLongTime => Some(Duration::from_secs(15)), +/// } +/// } +/// } +/// +/// /// An expiry that implements `moka::Expiry` trait. `Expiry` trait provides the +/// /// default implementations of three callback methods `expire_after_create`, +/// /// `expire_after_read`, and `expire_after_update`. +/// /// +/// /// In this example, we only override the `expire_after_create` method. +/// pub struct MyExpiry; +/// +/// impl Expiry for MyExpiry { +/// /// Returns the duration of the expiration of the value that was just +/// /// created. +/// fn expire_after_create( +/// &self, +/// _key: &u32, +/// value: &(Expiration, String), +/// _current_time: Instant, +/// ) -> Option { +/// let duration = value.0.as_duration(); +/// println!("MyExpiry: expire_after_create called with key {_key} and value {value:?}. Returning {duration:?}."); +/// duration +/// } +/// } +/// +/// // Create a `Cache` with an expiry `MyExpiry` and +/// // eviction listener. +/// let expiry = MyExpiry; +/// +/// let eviction_listener = |key, _value, cause| { +/// println!("Evicted key {key}. Cause: {cause:?}"); +/// }; +/// +/// let cache = Cache::builder() +/// .max_capacity(100) +/// .expire_after(expiry) +/// .eviction_listener(eviction_listener) +/// .build(); +/// +/// // Insert some entries into the cache with different expirations. +/// cache.get_with(0, || (Expiration::AfterShortTime, "a".to_string())); +/// cache.get_with(1, || (Expiration::AfterLongTime, "b".to_string())); +/// cache.get_with(2, || (Expiration::Never, "c".to_string())); +/// +/// // Verify that all the inserted entries exist. +/// assert!(cache.contains_key(&0)); +/// assert!(cache.contains_key(&1)); +/// assert!(cache.contains_key(&2)); +/// +/// // Sleep for 6 seconds. Key 0 should expire. +/// println!("\nSleeping for 6 seconds...\n"); +/// std::thread::sleep(Duration::from_secs(6)); +/// println!("Entry count: {}", cache.entry_count()); +/// +/// // Verify that key 0 has been evicted. +/// assert!(!cache.contains_key(&0)); +/// assert!(cache.contains_key(&1)); +/// assert!(cache.contains_key(&2)); +/// +/// // Sleep for 10 more seconds. Key 1 should expire. +/// println!("\nSleeping for 10 seconds...\n"); +/// std::thread::sleep(Duration::from_secs(10)); +/// println!("Entry count: {}", cache.entry_count()); +/// +/// // Verify that key 1 has been evicted. +/// assert!(!cache.contains_key(&1)); +/// assert!(cache.contains_key(&2)); +/// +/// // Manually invalidate key 2. +/// cache.invalidate(&2); +/// assert!(!cache.contains_key(&2)); +/// +/// println!("\nSleeping for a second...\n"); +/// std::thread::sleep(Duration::from_secs(1)); +/// println!("Entry count: {}", cache.entry_count()); +/// +/// println!("\nDone!"); +/// ``` +/// +/// The `Expiry` trait provides three methods `expire_after_create`, +/// `expire_after_read` and `expire_after_update` with default implementations. See +/// [its document][expiry-trait] for more details. +/// /// # Example: Eviction Listener /// /// A `Cache` can be configured with an eviction listener, a closure that is called @@ -398,7 +567,7 @@ use std::{ /// /// [builder-name-method]: ./struct.CacheBuilder.html#method.name /// -/// ## Delivery Modes for Eviction Listener +/// ## Delivery modes for eviction listener /// /// The [`DeliveryMode`][delivery-mode] specifies how and when an eviction /// notifications should be delivered to an eviction listener. The `sync` caches @@ -407,7 +576,7 @@ use std::{ /// /// [delivery-mode]: ../notification/enum.DeliveryMode.html /// -/// ### `Immediate` Mode +/// ### `Immediate` mode /// /// Tne `Immediate` mode is the default delivery mode for the `sync` caches. Use this /// mode when it is import to keep the order of write operations and eviction @@ -426,7 +595,7 @@ use std::{ /// - This mode adds some performance overhead to cache write operations as it uses /// internal per-key lock to guarantee the ordering. /// -/// ### `Queued` Mode +/// ### `Queued` mode /// /// Use this mode when write performance is more important than preserving the order /// of write operations and eviction notifications. @@ -622,58 +791,16 @@ use std::{ /// In `Queued` mode, the notification of the eviction at step 2 can be delivered /// either before or after the re-`insert` at step 3. If the `write_data_file` method /// does not generate unique file name on each call and the notification has not been -/// delivered before step 3, the user thread could overwrite the file created at -/// step 1. And then the notification will be delivered and the eviction listener -/// will remove a wrong file created at step 3 (instead of the correct one created at -/// step 1). This will cause the cache entires and the files on the filesystem to -/// become out of sync. +/// delivered before step 3, the user thread could overwrite the file created at step +/// 1. And then the notification will be delivered and the eviction listener will +/// remove a wrong file created at step 3 (instead of the correct one created at step +/// 1). This will cause the cache entires and the files on the filesystem to become +/// out of sync. /// /// Generating unique file names prevents this problem, as the user thread will never /// overwrite the file created at step 1 and the eviction lister will never remove a /// wrong file. /// -/// # Thread Safety -/// -/// All methods provided by the `Cache` are considered thread-safe, and can be safely -/// accessed by multiple concurrent threads. -/// -/// - `Cache` requires trait bounds `Send`, `Sync` and `'static` for `K` -/// (key), `V` (value) and `S` (hasher state). -/// - `Cache` will implement `Send` and `Sync`. -/// -/// # Sharing a cache across threads -/// -/// To share a cache across threads, do one of the followings: -/// -/// - Create a clone of the cache by calling its `clone` method and pass it to other -/// thread. -/// - Wrap the cache by a `sync::OnceCell` or `sync::Lazy` from -/// [once_cell][once-cell-crate] create, and set it to a `static` variable. -/// -/// Cloning is a cheap operation for `Cache` as it only creates thread-safe -/// reference-counted pointers to the internal data structures. -/// -/// [once-cell-crate]: https://crates.io/crates/once_cell -/// -/// # Hashing Algorithm -/// -/// By default, `Cache` uses a hashing algorithm selected to provide resistance -/// against HashDoS attacks. It will be the same one used by -/// `std::collections::HashMap`, which is currently SipHash 1-3. -/// -/// While SipHash's performance is very competitive for medium sized keys, other -/// hashing algorithms will outperform it for small keys such as integers as well as -/// large keys such as long strings. However those algorithms will typically not -/// protect against attacks such as HashDoS. -/// -/// The hashing algorithm can be replaced on a per-`Cache` basis using the -/// [`build_with_hasher`][build-with-hasher-method] method of the `CacheBuilder`. -/// Many alternative algorithms are available on crates.io, such as the -/// [AHash][ahash-crate] crate. -/// -/// [build-with-hasher-method]: ./struct.CacheBuilder.html#method.build_with_hasher -/// [ahash-crate]: https://crates.io/crates/ahash -/// pub struct Cache { base: BaseCache, value_initializer: Arc>,