diff --git a/.vscode/settings.json b/.vscode/settings.json index 9c0f9d19..a061a0b0 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,8 +1,9 @@ { - "rust-analyzer.cargo.features": ["future", "dash", "unstable-debug-counters"], + "rust-analyzer.cargo.features": ["future", "dash", "logging", "unstable-debug-counters"], "rust-analyzer.server.extraEnv": { "CARGO_TARGET_DIR": "target/ra" }, + "editor.rulers": [85], "cSpell.words": [ "aarch", "actix", diff --git a/Cargo.toml b/Cargo.toml index c45460e7..ed95765b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "moka" -version = "0.8.6" +version = "0.9.0" edition = "2018" rust-version = "1.51" @@ -58,7 +58,7 @@ tagptr = "0.2" # Opt-out serde and stable_deref_trait features # https://github.com/Manishearth/triomphe/pull/5 -triomphe = { version = "0.1", default-features = false } +triomphe = { version = "0.1.3", default-features = false } # Optional dependencies (enabled by default) quanta = { version = "0.10.0", optional = true } @@ -83,11 +83,13 @@ log = { version = "0.4", optional = true } [dev-dependencies] actix-rt = { version = "2.7", default-features = false } +anyhow = "1.0" async-std = { version = "1.11", features = ["attributes"] } +env_logger = "0.9" getrandom = "0.2" reqwest = "0.11.11" skeptic = "0.13" -tokio = { version = "1.19", features = ["rt-multi-thread", "macros", "sync", "time" ] } +tokio = { version = "1.19", features = ["fs", "macros", "rt-multi-thread", "sync", "time" ] } [target.'cfg(trybuild)'.dev-dependencies] trybuild = "1.0" diff --git a/README.md b/README.md index c78c440b..9299e301 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,9 @@ algorithm to determine which entries to evict when the capacity is exceeded. - 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. + [tiny-lfu]: https://github.com/moka-rs/moka/wiki#admission-and-eviction-policies @@ -528,20 +531,23 @@ $ cargo +nightly -Z unstable-options --config 'build.rustdocflags="--cfg docsrs" ## Road Map - [x] `async` optimized caches. (`v0.2.0`) -- [x] Size-aware eviction. (`v0.7.0` via - [#24](https://github.com/moka-rs/moka/pull/24)) -- [X] API stabilization. (Smaller core cache API, shorter names for frequently - used methods) (`v0.8.0` via [#105](https://github.com/moka-rs/moka/pull/105)) +- [x] Size-aware eviction. (`v0.7.0` via [#24][gh-pull-024]) +- [x] API stabilization. (Smaller core cache API, shorter names for frequently + used methods) (`v0.8.0` via [#105][gh-pull-105]) - e.g. - `get_or_insert_with(K, F)` → `get_with(K, F)` - `get_or_try_insert_with(K, F)` → `try_get_with(K, F)` - `blocking_insert(K, V)` → `blocking().insert(K, V)` - `time_to_live()` → `policy().time_to_live()` -- [ ] Notifications on eviction, etc. +- [x] Notifications on eviction. (`v0.9.0` via [#145][gh-pull-145]) - [ ] Cache statistics. (Hit rate, etc.) - [ ] Upgrade TinyLFU to Window-TinyLFU. ([details][tiny-lfu]) - [ ] The variable (per-entry) expiration, using a hierarchical timer wheel. +[gh-pull-024]: https://github.com/moka-rs/moka/pull/24 +[gh-pull-105]: https://github.com/moka-rs/moka/pull/105 +[gh-pull-145]: https://github.com/moka-rs/moka/pull/145 + ## About the Name diff --git a/src/cht/segment.rs b/src/cht/segment.rs index 888e0a58..551aec2d 100644 --- a/src/cht/segment.rs +++ b/src/cht/segment.rs @@ -193,6 +193,10 @@ impl HashMap { } } + pub(crate) fn actual_num_segments(&self) -> usize { + self.segments.len() + } + /// Returns the number of elements in the map. /// /// # Safety @@ -560,10 +564,6 @@ impl HashMap { { bucket::hash(&self.build_hasher, key) } - - pub(crate) fn actual_num_segments(&self) -> usize { - self.segments.len() - } } impl Drop for HashMap { diff --git a/src/common/concurrent/thread_pool.rs b/src/common/concurrent/thread_pool.rs index cb121c40..f900a15f 100644 --- a/src/common/concurrent/thread_pool.rs +++ b/src/common/concurrent/thread_pool.rs @@ -11,6 +11,8 @@ pub(crate) enum PoolName { Housekeeper, #[cfg(any(feature = "sync", feature = "future"))] Invalidator, + #[cfg(any(feature = "sync", feature = "future"))] + RemovalNotifier, } impl PoolName { @@ -19,6 +21,8 @@ impl PoolName { PoolName::Housekeeper => "moka-housekeeper-{}", #[cfg(any(feature = "sync", feature = "future"))] PoolName::Invalidator => "moka-invalidator-{}", + #[cfg(any(feature = "sync", feature = "future"))] + PoolName::RemovalNotifier => "moka-notifier-{}", } } } diff --git a/src/common/error.rs b/src/common/error.rs index 7fa5a3d6..526dd8d1 100644 --- a/src/common/error.rs +++ b/src/common/error.rs @@ -1,5 +1,5 @@ /// The error type for the functionalities around -/// [`Cache#invalidate_entries_if`][invalidate-if] method. +/// [`Cache::invalidate_entries_if`][invalidate-if] method. /// /// [invalidate-if]: ./sync/struct.Cache.html#method.invalidate_entries_if #[derive(thiserror::Error, Debug)] diff --git a/src/future.rs b/src/future.rs index 04319ccf..0c70334c 100644 --- a/src/future.rs +++ b/src/future.rs @@ -15,7 +15,7 @@ pub use { }; /// The type of the unique ID to identify a predicate used by -/// [`Cache#invalidate_entries_if`][invalidate-if] method. +/// [`Cache::invalidate_entries_if`][invalidate-if] method. /// /// A `PredicateId` is a `String` of UUID (version 4). /// diff --git a/src/future/builder.rs b/src/future/builder.rs index 5cb8b001..9bade8e2 100644 --- a/src/future/builder.rs +++ b/src/future/builder.rs @@ -1,5 +1,8 @@ use super::Cache; -use crate::common::{builder_utils, concurrent::Weigher}; +use crate::{ + common::{builder_utils, concurrent::Weigher}, + notification::{self, DeliveryMode, EvictionListener, RemovalCause}, +}; use std::{ collections::hash_map::RandomState, @@ -13,7 +16,7 @@ use std::{ /// /// [cache-struct]: ./struct.Cache.html /// -/// # Examples +/// # Example: Expirations /// /// ```rust /// // Cargo.toml @@ -51,9 +54,12 @@ use std::{ /// #[must_use] pub struct CacheBuilder { + name: Option, max_capacity: Option, initial_capacity: Option, weigher: Option>, + eviction_listener: Option>, + eviction_listener_conf: Option, time_to_live: Option, time_to_idle: Option, invalidator_enabled: bool, @@ -67,9 +73,12 @@ where { fn default() -> Self { Self { + name: None, max_capacity: None, initial_capacity: None, weigher: None, + eviction_listener: None, + eviction_listener_conf: None, time_to_live: None, time_to_idle: None, invalidator_enabled: false, @@ -103,10 +112,13 @@ where let build_hasher = RandomState::default(); builder_utils::ensure_expirations_or_panic(self.time_to_live, self.time_to_idle); Cache::with_everything( + self.name, self.max_capacity, self.initial_capacity, build_hasher, self.weigher, + self.eviction_listener, + self.eviction_listener_conf, self.time_to_live, self.time_to_idle, self.invalidator_enabled, @@ -126,10 +138,13 @@ where { builder_utils::ensure_expirations_or_panic(self.time_to_live, self.time_to_idle); Cache::with_everything( + self.name, self.max_capacity, self.initial_capacity, hasher, self.weigher, + self.eviction_listener, + self.eviction_listener_conf, self.time_to_live, self.time_to_idle, self.invalidator_enabled, @@ -138,6 +153,15 @@ where } impl CacheBuilder { + /// Sets the name of the cache. Currently the name is used for identification + /// only in logging messages. + pub fn name(self, name: &str) -> Self { + Self { + name: Some(name.to_string()), + ..self + } + } + /// Sets the max capacity of the cache. pub fn max_capacity(self, max_capacity: u64) -> Self { Self { @@ -154,7 +178,7 @@ impl CacheBuilder { } } - /// Sets the weigher closure of the cache. + /// Sets the weigher closure to the cache. /// /// The closure should take `&K` and `&V` as the arguments and returns a `u32` /// representing the relative size of the entry. @@ -165,6 +189,35 @@ impl CacheBuilder { } } + /// Sets the eviction listener closure to the cache. + /// + /// The closure should take `Arc`, `V` and [`RemovalCause`][removal-cause] as + /// the arguments. The [queued delivery mode][queued-mode] is used for the + /// listener. + /// + /// # Panics + /// + /// It is very important to make the listener closure not to panic. Otherwise, + /// the cache will stop calling the listener after a panic. This is an intended + /// behavior because the cache cannot know whether is is memory safe or not to + /// call the panicked lister again. + /// + /// [removal-cause]: ../notification/enum.RemovalCause.html + /// [queued-mode]: ../notification/enum.DeliveryMode.html#variant.Queued + pub fn eviction_listener_with_queued_delivery_mode( + self, + listener: impl Fn(Arc, V, RemovalCause) + Send + Sync + 'static, + ) -> Self { + let conf = notification::Configuration::builder() + .delivery_mode(DeliveryMode::Queued) + .build(); + Self { + eviction_listener: Some(Arc::new(listener)), + eviction_listener_conf: Some(conf), + ..self + } + } + /// Sets the time to live of the cache. /// /// A cached entry will be expired after the specified duration past from diff --git a/src/future/cache.rs b/src/future/cache.rs index be988679..2b6240eb 100644 --- a/src/future/cache.rs +++ b/src/future/cache.rs @@ -8,6 +8,7 @@ use crate::{ housekeeper::InnerSync, Weigher, WriteOp, }, + notification::{self, EvictionListener}, sync_base::base_cache::{BaseCache, HouseKeeperArc}, Policy, PredicateError, }; @@ -39,14 +40,27 @@ use std::{ /// /// To use this cache, enable a crate feature called "future". /// -/// # Examples +/// # Table of Contents +/// +/// - [Example: `insert`, `get` and `invalidate`](#example-insert-get-and-invalidate) +/// - [Avoiding to clone the value at `get`](#avoiding-to-clone-the-value-at-get) +/// - [Example: Size-based Eviction](#example-size-based-eviction) +/// - [Example: Time-based Expirations](#example-time-based-expirations) +/// - [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) +/// +/// # Example: `insert`, `get` and `invalidate` /// /// Cache entries are manually added using an insert method, and are stored in the /// cache until either evicted or manually invalidated: /// /// - Inside an async context (`async fn` or `async` block), use -/// [`insert`](#method.insert), [`get_with`](#method.get_with) -/// or [`invalidate`](#method.invalidate) methods for updating the cache and `await` +/// [`insert`](#method.insert), [`get_with`](#method.get_with) or +/// [`invalidate`](#method.invalidate) methods for updating the cache and `await` /// them. /// - Outside any async context, use [`blocking`](#method.blocking) method to access /// blocking version of [`insert`](./struct.BlockingOp.html#method.insert) or @@ -122,8 +136,7 @@ use std::{ /// /// If you want to atomically initialize and insert a value when the key is not /// present, you might want to check other insertion methods -/// [`get_with`](#method.get_with) and -/// [`try_get_with`](#method.try_get_with). +/// [`get_with`](#method.get_with) and [`try_get_with`](#method.try_get_with). /// /// # Avoiding to clone the value at `get` /// @@ -140,7 +153,7 @@ use std::{ /// /// [rustdoc-std-arc]: https://doc.rust-lang.org/stable/std/sync/struct.Arc.html /// -/// # Size-based Eviction +/// # Example: Size-based Eviction /// /// ```rust /// // Cargo.toml @@ -195,7 +208,7 @@ use std::{ /// /// [builder-struct]: ./struct.CacheBuilder.html /// -/// # Time-based Expirations +/// # Example: Time-based Expirations /// /// `Cache` supports the following expiration policies: /// @@ -236,6 +249,218 @@ use std::{ /// } /// ``` /// +/// # Example: Eviction Listener +/// +/// A `Cache` can be configured with an eviction listener, a closure that is called +/// every time there is a cache eviction. The listener takes three parameters: the +/// key and value of the evicted entry, and the +/// [`RemovalCause`](../notification/enum.RemovalCause.html) to indicate why the +/// entry was evicted. +/// +/// An eviction listener can be used to keep other data structures in sync with the +/// cache, for example. +/// +/// The following example demonstrates how to use an eviction listener with +/// time-to-live expiration to manage the lifecycle of temporary files on a +/// filesystem. The cache stores the paths of the files, and when one of them has +/// expired, the eviction lister will be called with the path, so it can remove the +/// file from the filesystem. +/// +/// ```rust +/// // Cargo.toml +/// // +/// // [dependencies] +/// // anyhow = "1.0" +/// // uuid = { version = "1.1", features = ["v4"] } +/// // tokio = { version = "1.18", features = ["fs", "macros", "rt-multi-thread", "sync", "time"] } +/// +/// use moka::future::Cache; +/// +/// use anyhow::{anyhow, Context}; +/// use std::{ +/// io, +/// path::{Path, PathBuf}, +/// sync::Arc, +/// time::Duration, +/// }; +/// use tokio::{fs, sync::RwLock}; +/// use uuid::Uuid; +/// +/// /// The DataFileManager writes, reads and removes data files. +/// struct DataFileManager { +/// base_dir: PathBuf, +/// file_count: usize, +/// } +/// +/// impl DataFileManager { +/// fn new(base_dir: PathBuf) -> Self { +/// Self { +/// base_dir, +/// file_count: 0, +/// } +/// } +/// +/// async fn write_data_file(&mut self, contents: String) -> io::Result { +/// loop { +/// // Generate a unique file path. +/// let mut path = self.base_dir.to_path_buf(); +/// path.push(Uuid::new_v4().as_hyphenated().to_string()); +/// +/// if path.exists() { +/// continue; // This path is already taken by others. Retry. +/// } +/// +/// // We have got a unique file path, so create the file at +/// // the path and write the contents to the file. +/// fs::write(&path, contents).await?; +/// self.file_count += 1; +/// println!( +/// "Created a data file at {:?} (file count: {})", +/// path, self.file_count +/// ); +/// +/// // Return the path. +/// return Ok(path); +/// } +/// } +/// +/// async fn read_data_file(&self, path: impl AsRef) -> io::Result { +/// // Reads the contents of the file at the path, and return the contents. +/// fs::read_to_string(path).await +/// } +/// +/// async fn remove_data_file(&mut self, path: impl AsRef) -> io::Result<()> { +/// // Remove the file at the path. +/// fs::remove_file(path.as_ref()).await?; +/// self.file_count -= 1; +/// println!( +/// "Removed a data file at {:?} (file count: {})", +/// path.as_ref(), +/// self.file_count +/// ); +/// +/// Ok(()) +/// } +/// } +/// +/// #[tokio::main] +/// async fn main() -> anyhow::Result<()> { +/// // Create an instance of the DataFileManager and wrap it with +/// // Arc> so it can be shared across threads. +/// let file_mgr = DataFileManager::new(std::env::temp_dir()); +/// let file_mgr = Arc::new(RwLock::new(file_mgr)); +/// +/// let file_mgr1 = Arc::clone(&file_mgr); +/// let rt = tokio::runtime::Handle::current(); +/// +/// // Create an eviction lister closure. +/// let listener = move |k, v: PathBuf, cause| { +/// // Try to remove the data file at the path `v`. +/// println!( +/// "\n== An entry has been evicted. k: {:?}, v: {:?}, cause: {:?}", +/// k, v, cause +/// ); +/// rt.block_on(async { +/// // Acquire the write lock of the DataFileManager. +/// let mut mgr = file_mgr1.write().await; +/// // Remove the data file. We must handle error cases here to +/// // prevent the listener from panicking. +/// if let Err(_e) = mgr.remove_data_file(v.as_path()).await { +/// eprintln!("Failed to remove a data file at {:?}", v); +/// } +/// }); +/// }; +/// +/// // Create the cache. Set time to live for two seconds and set the +/// // eviction listener. +/// let cache = Cache::builder() +/// .max_capacity(100) +/// .time_to_live(Duration::from_secs(2)) +/// .eviction_listener_with_queued_delivery_mode(listener) +/// .build(); +/// +/// // Insert an entry to the cache. +/// // This will create and write a data file for the key "user1", store the +/// // path of the file to the cache, and return it. +/// println!("== try_get_with()"); +/// let path = cache +/// .try_get_with("user1", async { +/// let mut mgr = file_mgr.write().await; +/// let path = mgr +/// .write_data_file("user data".into()) +/// .await +/// .with_context(|| format!("Failed to create a data file"))?; +/// Ok(path) as anyhow::Result<_> +/// }) +/// .await +/// .map_err(|e| anyhow!("{}", e))?; +/// +/// // Read the data file at the path and print the contents. +/// println!("\n== read_data_file()"); +/// { +/// let mgr = file_mgr.read().await; +/// let contents = mgr +/// .read_data_file(path.as_path()) +/// .await +/// .with_context(|| format!("Failed to read data from {:?}", path))?; +/// println!("contents: {}", contents); +/// } +/// +/// // Sleep for five seconds. While sleeping, the cache entry for key "user1" +/// // will be expired and evicted, so the eviction lister will be called to +/// // remove the file. +/// tokio::time::sleep(Duration::from_secs(5)).await; +/// +/// Ok(()) +/// } +/// ``` +/// +/// ## You should avoid eviction listener to panic +/// +/// It is very important to make an eviction listener closure not to panic. +/// Otherwise, the cache will stop calling the listener after a panic. This is an +/// intended behavior because the cache cannot know whether it is memory safe or not +/// to call the panicked lister again. +/// +/// When a listener panics, the cache will swallow the panic and disable the +/// listener. If you want to know when a listener panics and the reason of the panic, +/// you can enable an optional `logging` feature of Moka and check error-level logs. +/// +/// To enable the `logging`, do the followings: +/// +/// 1. In `Cargo.toml`, add the crate feature `logging` for `moka`. +/// 2. Set the logging level for `moka` to `error` or any lower levels (`warn`, +/// `info`, ...): +/// - If you are using the `env_logger` crate, you can achieve this by setting +/// `RUST_LOG` environment variable to `moka=error`. +/// 3. If you have more than one caches, you may want to set a distinct name for each +/// cache by using cache builder's [`name`][builder-name-method] method. The name +/// will appear in the log. +/// +/// [builder-name-method]: ./struct.CacheBuilder.html#method.name +/// +/// ## 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 +/// `future::Cache` supports only one delivery mode: `Queued` mode. +/// +/// A future version of `future::Cache` will support `Immediate` mode, which will be +/// easier to use in many use cases than queued mode. Unlike the `future::Cache`, +/// the `sync::Cache` already supports it. +/// +/// Once `future::Cache` supports the immediate mode, the `eviction_listener` and +/// `eviction_listener_with_conf` methods will be added to the +/// `future::CacheBuilder`. The former will use the immediate mode, and the latter +/// will take a custom configurations to specify the queued mode. The current method +/// `eviction_listener_with_queued_delivery_mode` will be deprecated. +/// +/// For more details about the delivery modes, see [this section][sync-delivery-modes] +/// of `sync::Cache` documentation. +/// +/// [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 @@ -271,9 +496,9 @@ use std::{ /// 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`][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 @@ -334,6 +559,11 @@ where } impl Cache { + /// Returns cache’s name. + pub fn name(&self) -> Option<&str> { + self.base.name() + } + /// Returns a read-only cache policy of this cache. /// /// At this time, cache policy cannot be modified after cache creation. @@ -420,12 +650,15 @@ where pub fn new(max_capacity: u64) -> Self { let build_hasher = RandomState::default(); Self::with_everything( + None, Some(max_capacity), None, build_hasher, None, None, None, + None, + None, false, ) } @@ -445,21 +678,29 @@ where V: Clone + Send + Sync + 'static, S: BuildHasher + Clone + Send + Sync + 'static, { + // https://rust-lang.github.io/rust-clippy/master/index.html#too_many_arguments + #[allow(clippy::too_many_arguments)] pub(crate) fn with_everything( + name: Option, max_capacity: Option, initial_capacity: Option, build_hasher: S, weigher: Option>, + eviction_listener: Option>, + eviction_listener_conf: Option, time_to_live: Option, time_to_idle: Option, invalidator_enabled: bool, ) -> Self { Self { base: BaseCache::new( + name, max_capacity, initial_capacity, build_hasher.clone(), weigher, + eviction_listener, + eviction_listener_conf, time_to_live, time_to_idle, invalidator_enabled, @@ -756,6 +997,9 @@ where { let hash = self.base.hash(key); if let Some(kv) = self.base.remove_entry(key, hash) { + if self.base.is_removal_notifier_enabled() { + self.base.notify_invalidate(&kv.key, &kv.entry) + } let op = WriteOp::Remove(kv); let hk = self.base.housekeeper.as_ref(); Self::schedule_write_op(&self.base.write_op_ch, op, hk) @@ -913,7 +1157,7 @@ where impl ConcurrentCacheExt for Cache where K: Hash + Eq + Send + Sync + 'static, - V: Send + Sync + 'static, + V: Clone + Send + Sync + 'static, S: BuildHasher + Clone + Send + Sync + 'static, { fn sync(&self) { @@ -1114,14 +1358,29 @@ where #[cfg(test)] mod tests { use super::{Cache, ConcurrentCacheExt}; - use crate::common::time::Clock; + use crate::{common::time::Clock, notification::RemovalCause}; use async_io::Timer; + use parking_lot::Mutex; use std::{convert::Infallible, sync::Arc, time::Duration}; #[tokio::test] async fn basic_single_async_task() { - let mut cache = Cache::new(3); + // The following `Vec`s will hold actual and expected notifications. + let actual = Arc::new(Mutex::new(Vec::new())); + let mut expected = Vec::new(); + + // Create an eviction listener. + let a1 = Arc::clone(&actual); + // We use non-async mutex in the eviction listener (because the listener + // is a regular closure). + let listener = move |k, v, cause| a1.lock().push((k, v, cause)); + + // Create a cache with the eviction listener. + let mut cache = Cache::builder() + .max_capacity(3) + .eviction_listener_with_queued_delivery_mode(listener) + .build(); cache.reconfigure_for_testing(); // Make the cache exterior immutable. @@ -1151,11 +1410,13 @@ mod tests { // "d" should not be admitted because its frequency is too low. cache.insert("d", "david").await; // count: d -> 0 + expected.push((Arc::new("d"), "david", RemovalCause::Size)); cache.sync(); assert_eq!(cache.get(&"d"), None); // d -> 1 assert!(!cache.contains_key(&"d")); cache.insert("d", "david").await; + expected.push((Arc::new("d"), "david", RemovalCause::Size)); cache.sync(); assert!(!cache.contains_key(&"d")); assert_eq!(cache.get(&"d"), None); // d -> 2 @@ -1163,6 +1424,7 @@ mod tests { // "d" should be admitted and "c" should be evicted // because d's frequency is higher than c's. cache.insert("d", "dennis").await; + expected.push((Arc::new("c"), "cindy", RemovalCause::Size)); cache.sync(); assert_eq!(cache.get(&"a"), Some("alice")); assert_eq!(cache.get(&"b"), Some("bob")); @@ -1174,8 +1436,12 @@ mod tests { assert!(cache.contains_key(&"d")); cache.invalidate(&"b").await; + expected.push((Arc::new("b"), "bob", RemovalCause::Explicit)); + cache.sync(); assert_eq!(cache.get(&"b"), None); assert!(!cache.contains_key(&"b")); + + verify_notification_vec(&cache, actual, &expected); } #[test] @@ -1236,7 +1502,20 @@ mod tests { let david = ("david", 15); let dennis = ("dennis", 15); - let mut cache = Cache::builder().max_capacity(31).weigher(weigher).build(); + // The following `Vec`s will hold actual and expected notifications. + let actual = Arc::new(Mutex::new(Vec::new())); + let mut expected = Vec::new(); + + // Create an eviction listener. + let a1 = Arc::clone(&actual); + let listener = move |k, v, cause| a1.lock().push((k, v, cause)); + + // Create a cache with the eviction listener. + let mut cache = Cache::builder() + .max_capacity(31) + .weigher(weigher) + .eviction_listener_with_queued_delivery_mode(listener) + .build(); cache.reconfigure_for_testing(); // Make the cache exterior immutable. @@ -1268,27 +1547,33 @@ mod tests { // "d" must have higher count than 3, which is the aggregated count // of "a" and "c". cache.insert("d", david).await; // count: d -> 0 + expected.push((Arc::new("d"), david, RemovalCause::Size)); cache.sync(); assert_eq!(cache.get(&"d"), None); // d -> 1 assert!(!cache.contains_key(&"d")); cache.insert("d", david).await; + expected.push((Arc::new("d"), david, RemovalCause::Size)); cache.sync(); assert!(!cache.contains_key(&"d")); assert_eq!(cache.get(&"d"), None); // d -> 2 cache.insert("d", david).await; + expected.push((Arc::new("d"), david, RemovalCause::Size)); cache.sync(); assert_eq!(cache.get(&"d"), None); // d -> 3 assert!(!cache.contains_key(&"d")); cache.insert("d", david).await; + expected.push((Arc::new("d"), david, RemovalCause::Size)); cache.sync(); assert!(!cache.contains_key(&"d")); assert_eq!(cache.get(&"d"), None); // d -> 4 // Finally "d" should be admitted by evicting "c" and "a". cache.insert("d", dennis).await; + expected.push((Arc::new("c"), cindy, RemovalCause::Size)); + expected.push((Arc::new("a"), alice, RemovalCause::Size)); cache.sync(); assert_eq!(cache.get(&"a"), None); assert_eq!(cache.get(&"b"), Some(bob)); @@ -1301,6 +1586,8 @@ mod tests { // Update "b" with "bill" (w: 15 -> 20). This should evict "d" (w: 15). cache.insert("b", bill).await; + expected.push((Arc::new("b"), bob, RemovalCause::Replaced)); + expected.push((Arc::new("d"), dennis, RemovalCause::Size)); cache.sync(); assert_eq!(cache.get(&"b"), Some(bill)); assert_eq!(cache.get(&"d"), None); @@ -1310,6 +1597,7 @@ mod tests { // Re-add "a" (w: 10) and update "b" with "bob" (w: 20 -> 15). cache.insert("a", alice).await; cache.insert("b", bob).await; + expected.push((Arc::new("b"), bill, RemovalCause::Replaced)); cache.sync(); assert_eq!(cache.get(&"a"), Some(alice)); assert_eq!(cache.get(&"b"), Some(bob)); @@ -1321,6 +1609,8 @@ mod tests { // Verify the sizes. assert_eq!(cache.entry_count(), 2); assert_eq!(cache.weighted_size(), 25); + + verify_notification_vec(&cache, actual, &expected); } #[tokio::test] @@ -1359,7 +1649,19 @@ mod tests { #[tokio::test] async fn invalidate_all() { - let mut cache = Cache::new(100); + // The following `Vec`s will hold actual and expected notifications. + let actual = Arc::new(Mutex::new(Vec::new())); + let mut expected = Vec::new(); + + // Create an eviction listener. + let a1 = Arc::clone(&actual); + let listener = move |k, v, cause| a1.lock().push((k, v, cause)); + + // Create a cache with the eviction listener. + let mut cache = Cache::builder() + .max_capacity(100) + .eviction_listener_with_queued_delivery_mode(listener) + .build(); cache.reconfigure_for_testing(); // Make the cache exterior immutable. @@ -1380,6 +1682,9 @@ mod tests { // https://github.com/moka-rs/moka/issues/155 cache.invalidate_all(); + expected.push((Arc::new("a"), "alice", RemovalCause::Explicit)); + expected.push((Arc::new("b"), "bob", RemovalCause::Explicit)); + expected.push((Arc::new("c"), "cindy", RemovalCause::Explicit)); cache.sync(); cache.insert("d", "david").await; @@ -1393,6 +1698,8 @@ mod tests { assert!(!cache.contains_key(&"b")); assert!(!cache.contains_key(&"c")); assert!(cache.contains_key(&"d")); + + verify_notification_vec(&cache, actual, &expected); } // This test is for https://github.com/moka-rs/moka/issues/155 @@ -1412,9 +1719,19 @@ mod tests { async fn invalidate_entries_if() -> Result<(), Box> { use std::collections::HashSet; + // The following `Vec`s will hold actual and expected notifications. + let actual = Arc::new(Mutex::new(Vec::new())); + let mut expected = Vec::new(); + + // Create an eviction listener. + let a1 = Arc::clone(&actual); + let listener = move |k, v, cause| a1.lock().push((k, v, cause)); + + // Create a cache with the eviction listener. let mut cache = Cache::builder() .max_capacity(100) .support_invalidation_closures() + .eviction_listener_with_queued_delivery_mode(listener) .build(); cache.reconfigure_for_testing(); @@ -1442,6 +1759,8 @@ mod tests { let names = ["alice", "alex"].iter().cloned().collect::>(); cache.invalidate_entries_if(move |_k, &v| names.contains(v))?; assert_eq!(cache.invalidation_predicate_count(), 1); + expected.push((Arc::new(0), "alice", RemovalCause::Explicit)); + expected.push((Arc::new(2), "alex", RemovalCause::Explicit)); mock.increment(Duration::from_secs(5)); // 10 secs from the start. @@ -1472,6 +1791,9 @@ mod tests { cache.invalidate_entries_if(|_k, &v| v == "alice")?; cache.invalidate_entries_if(|_k, &v| v == "bob")?; assert_eq!(cache.invalidation_predicate_count(), 2); + // key 1 was inserted before key 3. + expected.push((Arc::new(1), "bob", RemovalCause::Explicit)); + expected.push((Arc::new(3), "alice", RemovalCause::Explicit)); // Run the invalidation task and wait for it to finish. (TODO: Need a better way than sleeping) cache.sync(); // To submit the invalidation task. @@ -1488,16 +1810,27 @@ mod tests { assert_eq!(cache.entry_count(), 0); assert_eq!(cache.invalidation_predicate_count(), 0); + verify_notification_vec(&cache, actual, &expected); + Ok(()) } #[tokio::test] async fn time_to_live() { + // The following `Vec`s will hold actual and expected notifications. + let actual = Arc::new(Mutex::new(Vec::new())); + let mut expected = Vec::new(); + + // Create an eviction listener. + let a1 = Arc::clone(&actual); + let listener = move |k, v, cause| a1.lock().push((k, v, cause)); + + // Create a cache with the eviction listener. let mut cache = Cache::builder() .max_capacity(100) .time_to_live(Duration::from_secs(10)) + .eviction_listener_with_queued_delivery_mode(listener) .build(); - cache.reconfigure_for_testing(); let (clock, mock) = Clock::mock(); @@ -1516,6 +1849,7 @@ mod tests { assert!(cache.contains_key(&"a")); mock.increment(Duration::from_secs(5)); // 10 secs. + expected.push((Arc::new("a"), "alice", RemovalCause::Expired)); assert_eq!(cache.get(&"a"), None); assert!(!cache.contains_key(&"a")); @@ -1537,6 +1871,7 @@ mod tests { assert_eq!(cache.entry_count(), 1); cache.insert("b", "bill").await; + expected.push((Arc::new("b"), "bob", RemovalCause::Replaced)); cache.sync(); mock.increment(Duration::from_secs(5)); // 20 secs @@ -1547,6 +1882,7 @@ mod tests { assert_eq!(cache.entry_count(), 1); mock.increment(Duration::from_secs(5)); // 25 secs + expected.push((Arc::new("b"), "bill", RemovalCause::Expired)); assert_eq!(cache.get(&"a"), None); assert_eq!(cache.get(&"b"), None); @@ -1557,15 +1893,26 @@ mod tests { cache.sync(); assert!(cache.is_table_empty()); + + verify_notification_vec(&cache, actual, &expected); } #[tokio::test] async fn time_to_idle() { + // The following `Vec`s will hold actual and expected notifications. + let actual = Arc::new(Mutex::new(Vec::new())); + let mut expected = Vec::new(); + + // Create an eviction listener. + let a1 = Arc::clone(&actual); + let listener = move |k, v, cause| a1.lock().push((k, v, cause)); + + // Create a cache with the eviction listener. let mut cache = Cache::builder() .max_capacity(100) .time_to_idle(Duration::from_secs(10)) + .eviction_listener_with_queued_delivery_mode(listener) .build(); - cache.reconfigure_for_testing(); let (clock, mock) = Clock::mock(); @@ -1601,6 +1948,8 @@ mod tests { assert_eq!(cache.entry_count(), 2); mock.increment(Duration::from_secs(3)); // 15 secs. + expected.push((Arc::new("a"), "alice", RemovalCause::Expired)); + assert_eq!(cache.get(&"a"), None); assert_eq!(cache.get(&"b"), Some("bob")); assert!(!cache.contains_key(&"a")); @@ -1612,6 +1961,8 @@ mod tests { assert_eq!(cache.entry_count(), 1); mock.increment(Duration::from_secs(10)); // 25 secs + expected.push((Arc::new("b"), "bob", RemovalCause::Expired)); + assert_eq!(cache.get(&"a"), None); assert_eq!(cache.get(&"b"), None); assert!(!cache.contains_key(&"a")); @@ -1621,6 +1972,8 @@ mod tests { cache.sync(); assert!(cache.is_table_empty()); + + verify_notification_vec(&cache, actual, &expected); } #[tokio::test] @@ -2177,6 +2530,213 @@ mod tests { ); } + #[tokio::test] + async fn test_removal_notifications() { + // The following `Vec`s will hold actual and expected notifications. + let actual = Arc::new(Mutex::new(Vec::new())); + let mut expected = Vec::new(); + + // Create an eviction listener. + let a1 = Arc::clone(&actual); + let listener = move |k, v, cause| a1.lock().push((k, v, cause)); + + // Create a cache with the eviction listener. + let mut cache = Cache::builder() + .max_capacity(3) + .eviction_listener_with_queued_delivery_mode(listener) + .build(); + cache.reconfigure_for_testing(); + + // Make the cache exterior immutable. + let cache = cache; + + cache.insert('a', "alice").await; + cache.invalidate(&'a').await; + expected.push((Arc::new('a'), "alice", RemovalCause::Explicit)); + + cache.sync(); + assert_eq!(cache.entry_count(), 0); + + cache.insert('b', "bob").await; + cache.insert('c', "cathy").await; + cache.insert('d', "david").await; + cache.sync(); + assert_eq!(cache.entry_count(), 3); + + // This will be rejected due to the size constraint. + cache.insert('e', "emily").await; + expected.push((Arc::new('e'), "emily", RemovalCause::Size)); + cache.sync(); + assert_eq!(cache.entry_count(), 3); + + // Raise the popularity of 'e' so it will be accepted next time. + cache.get(&'e'); + cache.sync(); + + // Retry. + cache.insert('e', "eliza").await; + // and the LRU entry will be evicted. + expected.push((Arc::new('b'), "bob", RemovalCause::Size)); + cache.sync(); + assert_eq!(cache.entry_count(), 3); + + // Replace an existing entry. + cache.insert('d', "dennis").await; + expected.push((Arc::new('d'), "david", RemovalCause::Replaced)); + cache.sync(); + assert_eq!(cache.entry_count(), 3); + + verify_notification_vec(&cache, actual, &expected); + } + + #[tokio::test] + async fn test_removal_notifications_with_updates() { + // The following `Vec`s will hold actual and expected notifications. + let actual = Arc::new(Mutex::new(Vec::new())); + let mut expected = Vec::new(); + + // Create an eviction listener. + let a1 = Arc::clone(&actual); + let listener = move |k, v, cause| a1.lock().push((k, v, cause)); + + // Create a cache with the eviction listener and also TTL and TTI. + let mut cache = Cache::builder() + .eviction_listener_with_queued_delivery_mode(listener) + .time_to_live(Duration::from_secs(7)) + .time_to_idle(Duration::from_secs(5)) + .build(); + cache.reconfigure_for_testing(); + + let (clock, mock) = Clock::mock(); + cache.set_expiration_clock(Some(clock)); + + // Make the cache exterior immutable. + let cache = cache; + + cache.insert("alice", "a0").await; + cache.sync(); + + // Now alice (a0) has been expired by the idle timeout (TTI). + mock.increment(Duration::from_secs(6)); + expected.push((Arc::new("alice"), "a0", RemovalCause::Expired)); + assert_eq!(cache.get(&"alice"), None); + + // We have not ran sync after the expiration of alice (a0), so it is + // still in the cache. + assert_eq!(cache.entry_count(), 1); + + // Re-insert alice with a different value. Since alice (a0) is still + // in the cache, this is actually a replace operation rather than an + // insert operation. We want to verify that the RemovalCause of a0 is + // Expired, not Replaced. + cache.insert("alice", "a1").await; + cache.sync(); + + mock.increment(Duration::from_secs(4)); + assert_eq!(cache.get(&"alice"), Some("a1")); + cache.sync(); + + // Now alice has been expired by time-to-live (TTL). + mock.increment(Duration::from_secs(4)); + expected.push((Arc::new("alice"), "a1", RemovalCause::Expired)); + assert_eq!(cache.get(&"alice"), None); + + // But, again, it is still in the cache. + assert_eq!(cache.entry_count(), 1); + + // Re-insert alice with a different value and verify that the + // RemovalCause of a1 is Expired (not Replaced). + cache.insert("alice", "a2").await; + cache.sync(); + + assert_eq!(cache.entry_count(), 1); + + // Now alice (a2) has been expired by the idle timeout. + mock.increment(Duration::from_secs(6)); + expected.push((Arc::new("alice"), "a2", RemovalCause::Expired)); + assert_eq!(cache.get(&"alice"), None); + assert_eq!(cache.entry_count(), 1); + + // This invalidate will internally remove alice (a2). + cache.invalidate(&"alice").await; + cache.sync(); + assert_eq!(cache.entry_count(), 0); + + // Re-insert, and this time, make it expired by the TTL. + cache.insert("alice", "a3").await; + cache.sync(); + mock.increment(Duration::from_secs(4)); + assert_eq!(cache.get(&"alice"), Some("a3")); + cache.sync(); + mock.increment(Duration::from_secs(4)); + expected.push((Arc::new("alice"), "a3", RemovalCause::Expired)); + assert_eq!(cache.get(&"alice"), None); + assert_eq!(cache.entry_count(), 1); + + // This invalidate will internally remove alice (a2). + cache.invalidate(&"alice").await; + cache.sync(); + assert_eq!(cache.entry_count(), 0); + + verify_notification_vec(&cache, actual, &expected); + } + + // NOTE: To enable the panic logging, run the following command: + // + // RUST_LOG=moka=info cargo test --features 'future, logging' -- \ + // future::cache::tests::recover_from_panicking_eviction_listener --exact --nocapture + // + #[tokio::test] + async fn recover_from_panicking_eviction_listener() { + #[cfg(feature = "logging")] + let _ = env_logger::builder().is_test(true).try_init(); + + // The following `Vec`s will hold actual and expected notifications. + let actual = Arc::new(Mutex::new(Vec::new())); + let mut expected = Vec::new(); + + // Create an eviction listener that panics when it see + // a value "panic now!". + let a1 = Arc::clone(&actual); + let listener = move |k, v, cause| { + if v == "panic now!" { + panic!("Panic now!"); + } + a1.lock().push((k, v, cause)) + }; + + // Create a cache with the eviction listener. + let mut cache = Cache::builder() + .name("My Future Cache") + .eviction_listener_with_queued_delivery_mode(listener) + .build(); + cache.reconfigure_for_testing(); + + // Make the cache exterior immutable. + let cache = cache; + + // Insert an okay value. + cache.insert("alice", "a0").await; + cache.sync(); + + // Insert a value that will cause the eviction listener to panic. + cache.insert("alice", "panic now!").await; + expected.push((Arc::new("alice"), "a0", RemovalCause::Replaced)); + cache.sync(); + + // Insert an okay value. This will replace the previsous + // value "panic now!" so the eviction listener will panic. + cache.insert("alice", "a2").await; + cache.sync(); + // No more removal notification should be sent. + + // Invalidate the okay value. + cache.invalidate(&"alice").await; + cache.sync(); + + verify_notification_vec(&cache, actual, &expected); + } + #[tokio::test] async fn test_debug_format() { let cache = Cache::new(10); @@ -2191,4 +2751,41 @@ mod tests { assert!(debug_str.contains(r#"'c': "cindy""#)); assert!(debug_str.ends_with('}')); } + + type NotificationTuple = (Arc, V, RemovalCause); + + fn verify_notification_vec( + cache: &Cache, + actual: Arc>>>, + expected: &[NotificationTuple], + ) where + K: std::hash::Hash + Eq + std::fmt::Debug + Send + Sync + 'static, + V: Eq + std::fmt::Debug + Clone + Send + Sync + 'static, + S: std::hash::BuildHasher + Clone + Send + Sync + 'static, + { + // Retries will be needed when testing in a QEMU VM. + const MAX_RETRIES: usize = 5; + let mut retries = 0; + loop { + // Ensure all scheduled notifications have been processed. + std::thread::sleep(Duration::from_millis(500)); + + let actual = &*actual.lock(); + if actual.len() != expected.len() { + if retries <= MAX_RETRIES { + retries += 1; + cache.sync(); + continue; + } else { + assert_eq!(actual.len(), expected.len(), "Retries exhausted"); + } + } + + for (i, (actual, expected)) in actual.iter().zip(expected).enumerate() { + assert_eq!(actual, expected, "expected[{}]", i); + } + + break; + } + } } diff --git a/src/lib.rs b/src/lib.rs index b379a61b..a75165de 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -37,6 +37,8 @@ //! - 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. //! //! # Examples //! @@ -173,6 +175,10 @@ pub mod dash; #[cfg_attr(docsrs, doc(cfg(feature = "future")))] pub mod future; +#[cfg(any(feature = "sync", feature = "future"))] +#[cfg_attr(docsrs, doc(cfg(any(feature = "sync", feature = "future"))))] +pub mod notification; + #[cfg(feature = "sync")] #[cfg_attr(docsrs, doc(cfg(feature = "sync")))] pub mod sync; diff --git a/src/notification.rs b/src/notification.rs new file mode 100644 index 00000000..672eb7f8 --- /dev/null +++ b/src/notification.rs @@ -0,0 +1,136 @@ +//! Common data types for notifications. + +pub(crate) mod notifier; + +use std::sync::Arc; + +pub(crate) type EvictionListener = + Arc, V, RemovalCause) + Send + Sync + 'static>; + +pub(crate) type EvictionListenerRef<'a, K, V> = + &'a Arc, V, RemovalCause) + Send + Sync + 'static>; + +// NOTE: Currently, dropping the cache will drop all entries without sending +// notifications. Calling `invalidate_all` method of the cache will trigger +// the notifications, but currently there is no way to know when all entries +// have been invalidated and their notifications have been sent. + +/// Configuration for an eviction listener of a cache. +/// +/// Currently only setting the [`DeliveryMode`][delivery-mode] is supported. +/// +/// [delivery-mode]: ./enum.DeliveryMode.html +#[derive(Clone, Debug, Default)] +pub struct Configuration { + mode: DeliveryMode, +} + +impl Configuration { + pub fn builder() -> ConfigurationBuilder { + ConfigurationBuilder::default() + } + + pub fn delivery_mode(&self) -> DeliveryMode { + self.mode + } +} + +/// Builds a [`Configuration`][conf] with some configuration knobs. +/// +/// Currently only setting the [`DeliveryMode`][delivery-mode] is supported. +/// +/// [conf]: ./struct.Configuration.html +/// [delivery-mode]: ./enum.DeliveryMode.html +#[derive(Default)] +pub struct ConfigurationBuilder { + mode: DeliveryMode, +} + +impl ConfigurationBuilder { + pub fn build(self) -> Configuration { + Configuration { mode: self.mode } + } + + pub fn delivery_mode(self, mode: DeliveryMode) -> Self { + Self { mode } + } +} + +/// Specifies how and when an eviction notification should be delivered to an +/// eviction listener. +/// +/// For more details, see [the document][delivery-mode-doc] of `sync::Cache`. +/// +/// [delivery-mode-doc]: ../sync/struct.Cache.html#delivery-modes-for-eviction-listener +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum DeliveryMode { + /// With this mode, a notification should be delivered to the listener + /// immediately after an entry was evicted. It also guarantees that eviction + /// notifications and cache write operations such and `insert`, `get_with` and + /// `invalidate` for a given cache key are ordered by the time when they + /// occurred. + /// + /// To guarantee the order, cache maintains key-level lock, which will reduce + /// concurrent write performance. + /// + /// Use this mode when the order is more import than the write performance. + Immediate, + /// With this mode, a notification will be delivered to the listener some time + /// after an entry was evicted. Therefore, it does not preserve the order of + /// eviction notifications and write operations. + /// + /// On the other hand, cache does not maintain key-level lock, so there will be + /// no overhead on write performance. + /// + /// Use this mode when write performance is more important than preserving the + /// order of eviction notifications and write operations. + Queued, +} + +impl Default for DeliveryMode { + fn default() -> Self { + Self::Immediate + } +} + +/// Indicates the reason why a cached entry was removed. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum RemovalCause { + /// The entry's expiration timestamp has passed. + Expired, + /// The entry was manually removed by the user. + Explicit, + /// The entry itself was not actually removed, but its value was replaced by + /// the user. + Replaced, + /// The entry was evicted due to size constraints. + Size, +} + +impl RemovalCause { + pub fn was_evicted(&self) -> bool { + matches!(self, Self::Expired | Self::Size) + } +} + +#[cfg(all(test, feature = "sync"))] +pub(crate) mod macros { + + macro_rules! assert_with_mode { + ($cond:expr, $delivery_mode:ident) => { + assert!( + $cond, + "assertion failed. (delivery mode: {:?})", + $delivery_mode + ) + }; + } + + macro_rules! assert_eq_with_mode { + ($left:expr, $right:expr, $delivery_mode:ident) => { + assert_eq!($left, $right, "(delivery mode: {:?})", $delivery_mode) + }; + } + + pub(crate) use {assert_eq_with_mode, assert_with_mode}; +} diff --git a/src/notification/notifier.rs b/src/notification/notifier.rs new file mode 100644 index 00000000..5c2c0940 --- /dev/null +++ b/src/notification/notifier.rs @@ -0,0 +1,399 @@ +use std::{ + sync::{ + atomic::{AtomicBool, Ordering}, + Arc, + }, + time::Duration, +}; + +use crate::{ + common::concurrent::{ + constants::WRITE_RETRY_INTERVAL_MICROS, + thread_pool::{PoolName, ThreadPool, ThreadPoolRegistry}, + }, + notification::{self, DeliveryMode, EvictionListener, EvictionListenerRef, RemovalCause}, +}; + +use crossbeam_channel::{Receiver, Sender, TrySendError}; +use parking_lot::Mutex; + +const CHANNEL_CAPACITY: usize = 1_024; +const SUBMIT_TASK_THRESHOLD: usize = 100; +const MAX_NOTIFICATIONS_PER_TASK: u16 = 5_000; + +pub(crate) enum RemovalNotifier { + Blocking(BlockingRemovalNotifier), + ThreadPool(ThreadPoolRemovalNotifier), +} + +impl RemovalNotifier { + pub(crate) fn new( + listener: EvictionListener, + conf: notification::Configuration, + cache_name: Option, + ) -> Self { + match conf.delivery_mode() { + DeliveryMode::Immediate => { + Self::Blocking(BlockingRemovalNotifier::new(listener, cache_name)) + } + DeliveryMode::Queued => { + Self::ThreadPool(ThreadPoolRemovalNotifier::new(listener, cache_name)) + } + } + } + + pub(crate) fn is_blocking(&self) -> bool { + matches!(self, RemovalNotifier::Blocking(_)) + } + + pub(crate) fn is_batching_supported(&self) -> bool { + matches!(self, RemovalNotifier::ThreadPool(_)) + } + + pub(crate) fn notify(&self, key: Arc, value: V, cause: RemovalCause) + where + K: Send + Sync + 'static, + V: Send + Sync + 'static, + { + match self { + RemovalNotifier::Blocking(notifier) => notifier.notify(key, value, cause), + RemovalNotifier::ThreadPool(notifier) => { + notifier.add_single_notification(key, value, cause) + } + } + } + + pub(crate) fn batch_notify(&self, entries: Vec>) + where + K: Send + Sync + 'static, + V: Send + Sync + 'static, + { + match self { + RemovalNotifier::Blocking(_) => unreachable!(), + RemovalNotifier::ThreadPool(notifier) => notifier.add_multiple_notifications(entries), + } + } + + pub(crate) fn sync(&self) + where + K: Send + Sync + 'static, + V: Send + Sync + 'static, + { + match self { + RemovalNotifier::Blocking(_) => unreachable!(), + RemovalNotifier::ThreadPool(notifier) => notifier.submit_task(), + } + } +} + +pub(crate) struct BlockingRemovalNotifier { + listener: EvictionListener, + is_enabled: AtomicBool, + #[cfg(feature = "logging")] + cache_name: Option, +} + +impl BlockingRemovalNotifier { + fn new(listener: EvictionListener, _cache_name: Option) -> Self { + Self { + listener, + is_enabled: AtomicBool::new(true), + #[cfg(feature = "logging")] + cache_name: _cache_name, + } + } + + fn notify(&self, key: Arc, value: V, cause: RemovalCause) { + use std::panic::{catch_unwind, AssertUnwindSafe}; + + if !self.is_enabled.load(Ordering::Acquire) { + return; + } + + let listener_clo = || (self.listener)(key, value, cause); + + // Safety: It is safe to assert unwind safety here because we will not + // call the listener again if it has been panicked. + let result = catch_unwind(AssertUnwindSafe(listener_clo)); + if let Err(_payload) = result { + self.is_enabled.store(false, Ordering::Release); + #[cfg(feature = "logging")] + log_panic(&*_payload, self.cache_name.as_deref()); + } + } +} + +pub(crate) struct ThreadPoolRemovalNotifier { + snd: Sender>, + state: Arc>, + thread_pool: Arc, +} + +impl Drop for ThreadPoolRemovalNotifier { + fn drop(&mut self) { + let state = &self.state; + // Disallow to create and run a notification task by now. + state.shutdown(); + + // Wait for the notification task to finish. (busy loop) + while state.is_running() { + std::thread::sleep(Duration::from_millis(1)); + } + + ThreadPoolRegistry::release_pool(&self.thread_pool); + } +} + +impl ThreadPoolRemovalNotifier { + fn new(listener: EvictionListener, _cache_name: Option) -> Self { + let (snd, rcv) = crossbeam_channel::bounded(CHANNEL_CAPACITY); + let thread_pool = ThreadPoolRegistry::acquire_pool(PoolName::RemovalNotifier); + let state = NotifierState { + task_lock: Default::default(), + rcv, + listener, + #[cfg(feature = "logging")] + cache_name: _cache_name, + is_enabled: AtomicBool::new(true), + is_running: Default::default(), + is_shutting_down: Default::default(), + }; + Self { + snd, + state: Arc::new(state), + thread_pool, + } + } +} + +impl ThreadPoolRemovalNotifier +where + K: Send + Sync + 'static, + V: Send + Sync + 'static, +{ + fn add_single_notification(&self, key: Arc, value: V, cause: RemovalCause) { + let entry = RemovedEntries::new_single(key, value, cause); + self.send_entries(entry) + .expect("Failed to send notification"); + } + + fn add_multiple_notifications(&self, entries: Vec>) { + let entries = RemovedEntries::new_multi(entries); + self.send_entries(entries) + .expect("Failed to send notification"); + } + + fn send_entries( + &self, + entries: RemovedEntries, + ) -> Result<(), TrySendError>> { + let mut entries = entries; + loop { + self.submit_task_if_necessary(); + match self.snd.try_send(entries) { + Ok(()) => break, + Err(TrySendError::Full(entries1)) => { + entries = entries1; + std::thread::sleep(Duration::from_millis(WRITE_RETRY_INTERVAL_MICROS)); + } + Err(e @ TrySendError::Disconnected(_)) => return Err(e), + } + } + Ok(()) + } + + fn submit_task(&self) { + // TODO: Use compare and exchange to ensure it was false. + + let state = &self.state; + + if state.is_running() || !state.is_enabled() || state.is_shutting_down() { + return; + } + state.set_running(true); + + let task = NotificationTask::new(state); + self.thread_pool.pool.execute(move || { + task.execute(); + }); + } + + fn submit_task_if_necessary(&self) { + if self.snd.len() >= SUBMIT_TASK_THRESHOLD && !self.state.is_running() { + self.submit_task(); // TODO: Error handling? + } + } +} + +struct NotificationTask { + state: Arc>, +} + +impl NotificationTask { + fn new(state: &Arc>) -> Self { + Self { + state: Arc::clone(state), + } + } + + fn execute(&self) { + // Only one task can be executed at a time for a cache segment. + let task_lock = self.state.task_lock.lock(); + let mut count = 0u16; + let mut is_enabled = self.state.is_enabled(); + + if !is_enabled { + return; + } + + while let Ok(entries) = self.state.rcv.try_recv() { + match entries { + RemovedEntries::Single(entry) => { + let result = self.notify(&self.state.listener, entry); + if result.is_err() { + is_enabled = false; + break; + } + count += 1; + } + RemovedEntries::Multi(entries) => { + for entry in entries { + let result = self.notify(&self.state.listener, entry); + if result.is_err() { + is_enabled = false; + break; + } + if self.state.is_shutting_down() { + break; + } + count += 1; + } + } + } + + if count > MAX_NOTIFICATIONS_PER_TASK || self.state.is_shutting_down() { + break; + } + } + + if !is_enabled { + self.state.set_enabled(false); + } + + std::mem::drop(task_lock); + self.state.set_running(false); + } + + /// Returns `Ok(())` when calling the listener succeeded. Returns + /// `Err(panic_payload)` when the listener panicked. + fn notify( + &self, + listener: EvictionListenerRef<'_, K, V>, + entry: RemovedEntry, + ) -> Result<(), Box> { + use std::panic::{catch_unwind, AssertUnwindSafe}; + + let RemovedEntry { key, value, cause } = entry; + let listener_clo = || (listener)(key, value, cause); + + // Safety: It is safe to assert unwind safety here because we will not + // call the listener again if it has been panicked. + let result = catch_unwind(AssertUnwindSafe(listener_clo)); + #[cfg(feature = "logging")] + { + if let Err(payload) = &result { + log_panic(&**payload, self.state.cache_name.as_deref()); + } + } + result + } +} + +struct NotifierState { + task_lock: Mutex<()>, + rcv: Receiver>, + listener: EvictionListener, + #[cfg(feature = "logging")] + cache_name: Option, + is_enabled: AtomicBool, + is_running: AtomicBool, + is_shutting_down: AtomicBool, +} + +impl NotifierState { + fn is_enabled(&self) -> bool { + self.is_enabled.load(Ordering::Acquire) + } + + fn set_enabled(&self, value: bool) { + self.is_enabled.store(value, Ordering::Release); + } + + fn is_running(&self) -> bool { + self.is_running.load(Ordering::Acquire) + } + + fn set_running(&self, value: bool) { + self.is_running.store(value, Ordering::Release); + } + + fn is_shutting_down(&self) -> bool { + self.is_shutting_down.load(Ordering::Acquire) + } + + fn shutdown(&self) { + self.is_shutting_down.store(true, Ordering::Release); + } +} + +pub(crate) struct RemovedEntry { + key: Arc, + value: V, + cause: RemovalCause, +} + +impl RemovedEntry { + pub(crate) fn new(key: Arc, value: V, cause: RemovalCause) -> Self { + Self { key, value, cause } + } +} + +enum RemovedEntries { + Single(RemovedEntry), + Multi(Vec>), +} + +impl RemovedEntries { + fn new_single(key: Arc, value: V, cause: RemovalCause) -> Self { + Self::Single(RemovedEntry::new(key, value, cause)) + } + + fn new_multi(entries: Vec>) -> Self { + Self::Multi(entries) + } +} + +#[cfg(feature = "logging")] +fn log_panic(payload: &(dyn std::any::Any + Send + 'static), cache_name: Option<&str>) { + // Try to downcast the payload into &str or String. + // + // NOTE: Clippy will complain if we use `if let Some(_)` here. + // https://rust-lang.github.io/rust-clippy/master/index.html#manual_map + let message: Option> = + (payload.downcast_ref::<&str>().map(|s| (*s).into())) + .or_else(|| payload.downcast_ref::().map(Into::into)); + + let cn = cache_name + .map(|name| format!("[{}] ", name)) + .unwrap_or_default(); + + if let Some(m) = message { + log::error!( + "{}Disabled the eviction listener because it panicked at '{}'", + cn, + m + ); + } else { + log::error!("{}Disabled the eviction listener because it panicked", cn); + } +} diff --git a/src/sync/builder.rs b/src/sync/builder.rs index 8e284f28..1c708c6d 100644 --- a/src/sync/builder.rs +++ b/src/sync/builder.rs @@ -1,5 +1,8 @@ use super::{Cache, SegmentedCache}; -use crate::common::{builder_utils, concurrent::Weigher}; +use crate::{ + common::{builder_utils, concurrent::Weigher}, + notification::{self, EvictionListener, RemovalCause}, +}; use std::{ collections::hash_map::RandomState, @@ -15,7 +18,7 @@ use std::{ /// [cache-struct]: ./struct.Cache.html /// [seg-cache-struct]: ./struct.SegmentedCache.html /// -/// # Examples +/// # Example: Expirations /// /// ```rust /// use moka::sync::Cache; @@ -43,10 +46,13 @@ use std::{ /// #[must_use] pub struct CacheBuilder { + name: Option, max_capacity: Option, initial_capacity: Option, num_segments: Option, weigher: Option>, + eviction_listener: Option>, + eviction_listener_conf: Option, time_to_live: Option, time_to_idle: Option, invalidator_enabled: bool, @@ -60,10 +66,13 @@ where { fn default() -> Self { Self { + name: None, max_capacity: None, initial_capacity: None, num_segments: None, weigher: None, + eviction_listener: None, + eviction_listener_conf: None, time_to_live: None, time_to_idle: None, invalidator_enabled: false, @@ -98,10 +107,13 @@ where assert!(num_segments != 0); CacheBuilder { + name: self.name, max_capacity: self.max_capacity, initial_capacity: self.initial_capacity, num_segments: Some(num_segments), weigher: None, + eviction_listener: None, + eviction_listener_conf: None, time_to_live: self.time_to_live, time_to_idle: self.time_to_idle, invalidator_enabled: self.invalidator_enabled, @@ -123,10 +135,13 @@ where let build_hasher = RandomState::default(); builder_utils::ensure_expirations_or_panic(self.time_to_live, self.time_to_idle); Cache::with_everything( + self.name, self.max_capacity, self.initial_capacity, build_hasher, self.weigher, + self.eviction_listener, + self.eviction_listener_conf, self.time_to_live, self.time_to_idle, self.invalidator_enabled, @@ -149,10 +164,13 @@ where { builder_utils::ensure_expirations_or_panic(self.time_to_live, self.time_to_idle); Cache::with_everything( + self.name, self.max_capacity, self.initial_capacity, hasher, self.weigher, + self.eviction_listener, + self.eviction_listener_conf, self.time_to_live, self.time_to_idle, self.invalidator_enabled, @@ -179,11 +197,14 @@ where let build_hasher = RandomState::default(); builder_utils::ensure_expirations_or_panic(self.time_to_live, self.time_to_idle); SegmentedCache::with_everything( + self.name, self.max_capacity, self.initial_capacity, self.num_segments.unwrap(), build_hasher, self.weigher, + self.eviction_listener, + self.eviction_listener_conf, self.time_to_live, self.time_to_idle, self.invalidator_enabled, @@ -206,11 +227,14 @@ where { builder_utils::ensure_expirations_or_panic(self.time_to_live, self.time_to_idle); SegmentedCache::with_everything( + self.name, self.max_capacity, self.initial_capacity, self.num_segments.unwrap(), hasher, self.weigher, + self.eviction_listener, + self.eviction_listener_conf, self.time_to_live, self.time_to_idle, self.invalidator_enabled, @@ -219,6 +243,15 @@ where } impl CacheBuilder { + /// Sets the name of the cache. Currently the name is used for identification + /// only in logging messages. + pub fn name(self, name: &str) -> Self { + Self { + name: Some(name.to_string()), + ..self + } + } + /// Sets the max capacity of the cache. pub fn max_capacity(self, max_capacity: u64) -> Self { Self { @@ -235,7 +268,7 @@ impl CacheBuilder { } } - /// Sets the weigher closure of the cache. + /// Sets the weigher closure to the cache. /// /// The closure should take `&K` and `&V` as the arguments and returns a `u32` /// representing the relative size of the entry. @@ -246,6 +279,60 @@ impl CacheBuilder { } } + /// Sets the eviction listener closure to the cache. + /// + /// The closure should take `Arc`, `V` and [`RemovalCause`][removal-cause] as + /// the arguments. The [immediate delivery mode][immediate-mode] is used for the + /// listener. + /// + /// # Panics + /// + /// It is very important to make the listener closure not to panic. Otherwise, + /// the cache will stop calling the listener after a panic. This is an intended + /// behavior because the cache cannot know whether is is memory safe or not to + /// call the panicked lister again. + /// + /// [removal-cause]: ../notification/enum.RemovalCause.html + /// [immediate-mode]: ../notification/enum.DeliveryMode.html#variant.Immediate + pub fn eviction_listener( + self, + listener: impl Fn(Arc, V, RemovalCause) + Send + Sync + 'static, + ) -> Self { + Self { + eviction_listener: Some(Arc::new(listener)), + eviction_listener_conf: Some(Default::default()), + ..self + } + } + + /// Sets the eviction listener closure to the cache with a custom + /// [`Configuration`][conf]. Use this method if you want to change the delivery + /// mode to the queued mode. + /// + /// The closure should take `Arc`, `V` and [`RemovalCause`][removal-cause] as + /// the arguments. + /// + /// # Panics + /// + /// It is very important to make the listener closure not to panic. Otherwise, + /// the cache will stop calling the listener after a panic. This is an intended + /// behavior because the cache cannot know whether is is memory safe or not to + /// call the panicked lister again. + /// + /// [removal-cause]: ../notification/enum.RemovalCause.html + /// [conf]: ../notification/struct.Configuration.html + pub fn eviction_listener_with_conf( + self, + listener: impl Fn(Arc, V, RemovalCause) + Send + Sync + 'static, + conf: notification::Configuration, + ) -> Self { + Self { + eviction_listener: Some(Arc::new(listener)), + eviction_listener_conf: Some(conf), + ..self + } + } + /// Sets the time to live of the cache. /// /// A cached entry will be expired after the specified duration past from diff --git a/src/sync/cache.rs b/src/sync/cache.rs index 2349c794..79cc2f4e 100644 --- a/src/sync/cache.rs +++ b/src/sync/cache.rs @@ -8,6 +8,7 @@ use crate::{ housekeeper::InnerSync, Weigher, WriteOp, }, + notification::{self, EvictionListener}, sync::{Iter, PredicateId}, sync_base::{ base_cache::{BaseCache, HouseKeeperArc}, @@ -37,11 +38,27 @@ use std::{ /// replacement algorithm to determine which entries to evict when the capacity is /// exceeded. /// -/// # Examples +/// # Table of Contents +/// +/// - [Example: `insert`, `get` and `invalidate`](#example-insert-get-and-invalidate) +/// - [Avoiding to clone the value at `get`](#avoiding-to-clone-the-value-at-get) +/// - [Example: Size-based Eviction](#example-size-based-eviction) +/// - [Example: Time-based Expirations](#example-time-based-expirations) +/// - [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) +/// - [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` /// /// Cache entries are manually added using [`insert`](#method.insert) or -/// [`get_with`](#method.get_with) methods, and are stored in -/// the cache until either evicted or manually invalidated. +/// [`get_with`](#method.get_with) methods, and are stored in the cache until either +/// evicted or manually invalidated. /// /// Here's an example of reading and updating a cache by using multiple threads: /// @@ -100,8 +117,7 @@ use std::{ /// /// If you want to atomically initialize and insert a value when the key is not /// present, you might want to check other insertion methods -/// [`get_with`](#method.get_with) and -/// [`try_get_with`](#method.try_get_with). +/// [`get_with`](#method.get_with) and [`try_get_with`](#method.try_get_with). /// /// # Avoiding to clone the value at `get` /// @@ -118,7 +134,7 @@ use std::{ /// /// [rustdoc-std-arc]: https://doc.rust-lang.org/stable/std/sync/struct.Arc.html /// -/// # Size-based Eviction +/// # Example: Size-based Eviction /// /// ```rust /// use std::convert::TryInto; @@ -163,7 +179,7 @@ use std::{ /// /// [builder-struct]: ./struct.CacheBuilder.html /// -/// # Time-based Expirations +/// # Example: Time-based Expirations /// /// `Cache` supports the following expiration policies: /// @@ -194,6 +210,429 @@ use std::{ /// // after 30 minutes (TTL) from the insert(). /// ``` /// +/// # Example: Eviction Listener +/// +/// A `Cache` can be configured with an eviction listener, a closure that is called +/// every time there is a cache eviction. The listener takes three parameters: the +/// key and value of the evicted entry, and the +/// [`RemovalCause`](../notification/enum.RemovalCause.html) to indicate why the +/// entry was evicted. +/// +/// An eviction listener can be used to keep other data structures in sync with the +/// cache, for example. +/// +/// The following example demonstrates how to use an eviction listener with +/// time-to-live expiration to manage the lifecycle of temporary files on a +/// filesystem. The cache stores the paths of the files, and when one of them has +/// expired, the eviction lister will be called with the path, so it can remove the +/// file from the filesystem. +/// +/// ```rust +/// // Cargo.toml +/// // +/// // [dependencies] +/// // anyhow = "1.0" +/// +/// use moka::{sync::Cache, notification}; +/// +/// use anyhow::{anyhow, Context}; +/// use std::{ +/// fs, io, +/// path::{Path, PathBuf}, +/// sync::{Arc, RwLock}, +/// time::Duration, +/// }; +/// +/// /// The DataFileManager writes, reads and removes data files. +/// struct DataFileManager { +/// base_dir: PathBuf, +/// file_count: usize, +/// } +/// +/// impl DataFileManager { +/// fn new(base_dir: PathBuf) -> Self { +/// Self { +/// base_dir, +/// file_count: 0, +/// } +/// } +/// +/// fn write_data_file( +/// &mut self, +/// key: impl AsRef, +/// contents: String +/// ) -> io::Result { +/// // Use the key as a part of the filename. +/// let mut path = self.base_dir.to_path_buf(); +/// path.push(key.as_ref()); +/// +/// assert!(!path.exists(), "Path already exists: {:?}", path); +/// +/// // create the file at the path and write the contents to the file. +/// fs::write(&path, contents)?; +/// self.file_count += 1; +/// println!("Created a data file at {:?} (file count: {})", path, self.file_count); +/// Ok(path) +/// } +/// +/// fn read_data_file(&self, path: impl AsRef) -> io::Result { +/// // Reads the contents of the file at the path, and return the contents. +/// fs::read_to_string(path) +/// } +/// +/// fn remove_data_file(&mut self, path: impl AsRef) -> io::Result<()> { +/// // Remove the file at the path. +/// fs::remove_file(path.as_ref())?; +/// self.file_count -= 1; +/// println!( +/// "Removed a data file at {:?} (file count: {})", +/// path.as_ref(), +/// self.file_count +/// ); +/// +/// Ok(()) +/// } +/// } +/// +/// fn main() -> anyhow::Result<()> { +/// // Create an instance of the DataFileManager and wrap it with +/// // Arc> so it can be shared across threads. +/// let file_mgr = DataFileManager::new(std::env::temp_dir()); +/// let file_mgr = Arc::new(RwLock::new(file_mgr)); +/// +/// let file_mgr1 = Arc::clone(&file_mgr); +/// +/// // Create an eviction lister closure. +/// let listener = move |k, v: PathBuf, cause| { +/// // Try to remove the data file at the path `v`. +/// println!( +/// "\n== An entry has been evicted. k: {:?}, v: {:?}, cause: {:?}", +/// k, v, cause +/// ); +/// +/// // Acquire the write lock of the DataFileManager. We must handle +/// // error cases here to prevent the listener from panicking. +/// match file_mgr1.write() { +/// Err(_e) => { +/// eprintln!("The lock has been poisoned"); +/// } +/// Ok(mut mgr) => { +/// // Remove the data file using the DataFileManager. +/// if let Err(_e) = mgr.remove_data_file(v.as_path()) { +/// eprintln!("Failed to remove a data file at {:?}", v); +/// } +/// } +/// } +/// }; +/// +/// let listener_conf = notification::Configuration::builder() +/// .delivery_mode(notification::DeliveryMode::Queued) +/// .build(); +/// +/// // Create the cache. Set time to live for two seconds and set the +/// // eviction listener. +/// let cache = Cache::builder() +/// .max_capacity(100) +/// .time_to_live(Duration::from_secs(2)) +/// .eviction_listener_with_conf(listener, listener_conf) +/// .build(); +/// +/// // Insert an entry to the cache. +/// // This will create and write a data file for the key "user1", store the +/// // path of the file to the cache, and return it. +/// println!("== try_get_with()"); +/// let key = "user1"; +/// let path = cache +/// .try_get_with(key, || -> anyhow::Result<_> { +/// let mut mgr = file_mgr +/// .write() +/// .map_err(|_e| anyhow::anyhow!("The lock has been poisoned"))?; +/// let path = mgr +/// .write_data_file(key, "user data".into()) +/// .with_context(|| format!("Failed to create a data file"))?; +/// Ok(path) +/// }) +/// .map_err(|e| anyhow!("{}", e))?; +/// +/// // Read the data file at the path and print the contents. +/// println!("\n== read_data_file()"); +/// { +/// let mgr = file_mgr +/// .read() +/// .map_err(|_e| anyhow::anyhow!("The lock has been poisoned"))?; +/// let contents = mgr +/// .read_data_file(path.as_path()) +/// .with_context(|| format!("Failed to read data from {:?}", path))?; +/// println!("contents: {}", contents); +/// } +/// +/// // Sleep for five seconds. While sleeping, the cache entry for key "user1" +/// // will be expired and evicted, so the eviction lister will be called to +/// // remove the file. +/// std::thread::sleep(Duration::from_secs(5)); +/// +/// Ok(()) +/// } +/// ``` +/// +/// ## You should avoid eviction listener to panic +/// +/// It is very important to make an eviction listener closure not to panic. +/// Otherwise, the cache will stop calling the listener after a panic. This is an +/// intended behavior because the cache cannot know whether it is memory safe or not +/// to call the panicked lister again. +/// +/// When a listener panics, the cache will swallow the panic and disable the +/// listener. If you want to know when a listener panics and the reason of the panic, +/// you can enable an optional `logging` feature of Moka and check error-level logs. +/// +/// To enable the `logging`, do the followings: +/// +/// 1. In `Cargo.toml`, add the crate feature `logging` for `moka`. +/// 2. Set the logging level for `moka` to `error` or any lower levels (`warn`, +/// `info`, ...): +/// - If you are using the `env_logger` crate, you can achieve this by setting +/// `RUST_LOG` environment variable to `moka=error`. +/// 3. If you have more than one caches, you may want to set a distinct name for each +/// cache by using cache builder's [`name`][builder-name-method] method. The name +/// will appear in the log. +/// +/// [builder-name-method]: ./struct.CacheBuilder.html#method.name +/// +/// ## 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 +/// (`Cache` and `SegmentedCache`) support two delivery modes: `Immediate` and +/// `Queued` modes. +/// +/// [delivery-mode]: ../notification/enum.DeliveryMode.html +/// +/// ### `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 +/// notifications. +/// +/// This mode has the following characteristics: +/// +/// - The listener is called immediately after an entry was evicted. +/// - The listener is called by the thread who evicted the entry: +/// - The calling thread can be a background eviction thread or a user thread +/// invoking a cache write operation such as `insert`, `get_with` or +/// `invalidate`. +/// - The calling thread is blocked until the listener returns. +/// - This mode guarantees that write operations and eviction notifications for a +/// given cache key are ordered by the time when they occurred. +/// - This mode adds some performance overhead to cache write operations as it uses +/// internal per-key lock to guarantee the ordering. +/// +/// ### `Queued` Mode +/// +/// Use this mode when write performance is more important than preserving the order +/// of write operations and eviction notifications. +/// +/// - The listener will be called some time after an entry was evicted. +/// - A notification will be stashed in a queue. The queue will be processed by +/// dedicated notification thread(s) and that thread will call the listener. +/// - This mode does not preserve the order of write operations and eviction +/// notifications. +/// - This mode adds almost no performance overhead to cache write operations as it +/// does not use the per-key lock. +/// +/// ### Example: `Queued` Delivery Mode +/// +/// Because the `Immediate` mode is the default mode for `sync` caches, the previous +/// example was using it implicitly. +/// +/// The following is the same example but modified for the `Queued` delivery mode. +/// (Showing changed lines only) +/// +/// ```rust +/// // Cargo.toml +/// // +/// // [dependencies] +/// // anyhow = "1.0" +/// // uuid = { version = "1.1", features = ["v4"] } +/// +/// use moka::{sync::Cache, notification}; +/// +/// # use anyhow::{anyhow, Context}; +/// # use std::{ +/// # fs, io, +/// # path::{Path, PathBuf}, +/// # sync::{Arc, RwLock}, +/// # time::Duration, +/// # }; +/// // Use UUID crate to generate a random file name. +/// use uuid::Uuid; +/// +/// # struct DataFileManager { +/// # base_dir: PathBuf, +/// # file_count: usize, +/// # } +/// # +/// impl DataFileManager { +/// # fn new(base_dir: PathBuf) -> Self { +/// # Self { +/// # base_dir, +/// # file_count: 0, +/// # } +/// # } +/// # +/// fn write_data_file( +/// &mut self, +/// _key: impl AsRef, +/// contents: String +/// ) -> io::Result { +/// // We do not use the key for the filename anymore. Instead, we +/// // use UUID to generate a unique filename for each call. +/// loop { +/// // Generate a file path with unique file name. +/// let mut path = self.base_dir.to_path_buf(); +/// path.push(Uuid::new_v4().as_hyphenated().to_string()); +/// +/// if path.exists() { +/// continue; // This path is already taken by others. Retry. +/// } +/// +/// // We have got a unique file path, so create the file at +/// // the path and write the contents to the file. +/// fs::write(&path, contents)?; +/// self.file_count += 1; +/// println!("Created a data file at {:?} (file count: {})", path, self.file_count); +/// +/// // Return the path. +/// return Ok(path); +/// } +/// } +/// +/// // Other associate functions and methods are unchanged. +/// # +/// # fn read_data_file(&self, path: impl AsRef) -> io::Result { +/// # fs::read_to_string(path) +/// # } +/// # +/// # fn remove_data_file(&mut self, path: impl AsRef) -> io::Result<()> { +/// # fs::remove_file(path.as_ref())?; +/// # self.file_count -= 1; +/// # println!( +/// # "Removed a data file at {:?} (file count: {})", +/// # path.as_ref(), +/// # self.file_count +/// # ); +/// # +/// # Ok(()) +/// # } +/// } +/// +/// fn main() -> anyhow::Result<()> { +/// // (Omitted unchanged lines) +/// +/// # let file_mgr = DataFileManager::new(std::env::temp_dir()); +/// # let file_mgr = Arc::new(RwLock::new(file_mgr)); +/// # +/// # let file_mgr1 = Arc::clone(&file_mgr); +/// # +/// // Create an eviction lister closure. +/// // let listener = ... +/// +/// # let listener = move |k, v: PathBuf, cause| { +/// # println!( +/// # "\n== An entry has been evicted. k: {:?}, v: {:?}, cause: {:?}", +/// # k, v, cause +/// # ); +/// # +/// # match file_mgr1.write() { +/// # Err(_e) => { +/// # eprintln!("The lock has been poisoned"); +/// # } +/// # Ok(mut mgr) => { +/// # if let Err(_e) = mgr.remove_data_file(v.as_path()) { +/// # eprintln!("Failed to remove a data file at {:?}", v); +/// # } +/// # } +/// # } +/// # }; +/// # +/// // Create a listener configuration with Queued delivery mode. +/// let listener_conf = notification::Configuration::builder() +/// .delivery_mode(notification::DeliveryMode::Queued) +/// .build(); +/// +/// // Create the cache. +/// let cache = Cache::builder() +/// .max_capacity(100) +/// .time_to_live(Duration::from_secs(2)) +/// // Set the eviction listener with the configuration. +/// .eviction_listener_with_conf(listener, listener_conf) +/// .build(); +/// +/// // Insert an entry to the cache. +/// // ... +/// # println!("== try_get_with()"); +/// # let key = "user1"; +/// # let path = cache +/// # .try_get_with(key, || -> anyhow::Result<_> { +/// # let mut mgr = file_mgr +/// # .write() +/// # .map_err(|_e| anyhow::anyhow!("The lock has been poisoned"))?; +/// # let path = mgr +/// # .write_data_file(key, "user data".into()) +/// # .with_context(|| format!("Failed to create a data file"))?; +/// # Ok(path) +/// # }) +/// # .map_err(|e| anyhow!("{}", e))?; +/// # +/// // Read the data file at the path and print the contents. +/// // ... +/// # println!("\n== read_data_file()"); +/// # { +/// # let mgr = file_mgr +/// # .read() +/// # .map_err(|_e| anyhow::anyhow!("The lock has been poisoned"))?; +/// # let contents = mgr +/// # .read_data_file(path.as_path()) +/// # .with_context(|| format!("Failed to read data from {:?}", path))?; +/// # println!("contents: {}", contents); +/// # } +/// # +/// // Sleep for five seconds. +/// // ... +/// # std::thread::sleep(Duration::from_secs(5)); +/// +/// Ok(()) +/// } +/// ``` +/// +/// As you can see, `DataFileManager::write_data_file` method no longer uses the +/// cache key for the file name. Instead, it generates a UUID-based unique file name +/// on each call. This kind of treatment will be needed for `Queued` mode because +/// notifications will be delivered with some delay. +/// +/// For example, a user thread could do the followings: +/// +/// 1. `insert` an entry, and create a file. +/// 2. The entry is evicted due to size constraint: +/// - This will trigger an eviction notification but it will be fired some time +/// later. +/// - The notification listener will remove the file when it is called, but we +/// cannot predict when the call would be made. +/// 3. `insert` the entry again, and create the file again. +/// +/// 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. +/// +/// 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 @@ -229,9 +668,9 @@ use std::{ /// 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`][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 @@ -292,6 +731,11 @@ where } impl Cache { + /// Returns cache’s name. + pub fn name(&self) -> Option<&str> { + self.base.name() + } + /// Returns a read-only cache policy of this cache. /// /// At this time, cache policy cannot be modified after cache creation. @@ -364,12 +808,15 @@ where pub fn new(max_capacity: u64) -> Self { let build_hasher = RandomState::default(); Self::with_everything( + None, Some(max_capacity), None, build_hasher, None, None, None, + None, + None, false, ) } @@ -389,21 +836,29 @@ where V: Clone + Send + Sync + 'static, S: BuildHasher + Clone + Send + Sync + 'static, { + // https://rust-lang.github.io/rust-clippy/master/index.html#too_many_arguments + #[allow(clippy::too_many_arguments)] pub(crate) fn with_everything( + name: Option, max_capacity: Option, initial_capacity: Option, build_hasher: S, weigher: Option>, + eviction_listener: Option>, + eviction_listener_conf: Option, time_to_live: Option, time_to_idle: Option, invalidator_enabled: bool, ) -> Self { Self { base: BaseCache::new( + name, max_capacity, initial_capacity, build_hasher.clone(), weigher, + eviction_listener, + eviction_listener_conf, time_to_live, time_to_idle, invalidator_enabled, @@ -761,7 +1216,36 @@ where Arc: Borrow, Q: Hash + Eq + ?Sized, { + // Lock the key for removal if blocking removal notification is enabled. + let mut kl = None; + let mut klg = None; + if self.base.is_removal_notifier_enabled() && self.base.is_blocking_removal_notification() { + // To lock the key, we have to get Arc for key (&Q). + // + // TODO: Enhance this if possible. This is rather hack now because + // it cannot prevent race conditions like this: + // + // 1. We miss the key because it does not exist. So we do not lock + // the key. + // 2. Somebody else (other thread) inserts the key. + // 3. We remove the entry for the key, but without the key lock! + // + if let Some(arc_key) = self.base.get_key_with_hash(key, hash) { + kl = self.base.maybe_key_lock(&arc_key); + klg = kl.as_ref().map(|kl| kl.lock()); + } + } + if let Some(kv) = self.base.remove_entry(key, hash) { + if self.base.is_removal_notifier_enabled() { + self.base.notify_invalidate(&kv.key, &kv.entry) + } + // Drop the locks before scheduling write op to avoid a potential dead lock. + // (Scheduling write can do spin lock when the queue is full, and queue will + // be drained by the housekeeping thread that can lock the same key) + std::mem::drop(klg); + std::mem::drop(kl); + let op = WriteOp::Remove(kv); let hk = self.base.housekeeper.as_ref(); Self::schedule_write_op(&self.base.write_op_ch, op, hk).expect("Failed to remove"); @@ -894,7 +1378,7 @@ where impl ConcurrentCacheExt for Cache where K: Hash + Eq + Send + Sync + 'static, - V: Send + Sync + 'static, + V: Clone + Send + Sync + 'static, S: BuildHasher + Clone + Send + Sync + 'static, { fn sync(&self) { @@ -989,165 +1473,232 @@ where #[cfg(test)] mod tests { use super::{Cache, ConcurrentCacheExt}; - use crate::common::time::Clock; - + use crate::{ + common::time::Clock, + notification::{ + self, + macros::{assert_eq_with_mode, assert_with_mode}, + DeliveryMode, RemovalCause, + }, + }; + + use parking_lot::Mutex; use std::{convert::Infallible, sync::Arc, time::Duration}; #[test] fn basic_single_thread() { - let mut cache = Cache::new(3); - cache.reconfigure_for_testing(); - - // Make the cache exterior immutable. - let cache = cache; - - cache.insert("a", "alice"); - cache.insert("b", "bob"); - assert_eq!(cache.get(&"a"), Some("alice")); - assert!(cache.contains_key(&"a")); - assert!(cache.contains_key(&"b")); - assert_eq!(cache.get(&"b"), Some("bob")); - cache.sync(); - // counts: a -> 1, b -> 1 - - cache.insert("c", "cindy"); - assert_eq!(cache.get(&"c"), Some("cindy")); - assert!(cache.contains_key(&"c")); - // counts: a -> 1, b -> 1, c -> 1 - cache.sync(); - - assert!(cache.contains_key(&"a")); - assert_eq!(cache.get(&"a"), Some("alice")); - assert_eq!(cache.get(&"b"), Some("bob")); - assert!(cache.contains_key(&"b")); - cache.sync(); - // counts: a -> 2, b -> 2, c -> 1 - - // "d" should not be admitted because its frequency is too low. - cache.insert("d", "david"); // count: d -> 0 - cache.sync(); - assert_eq!(cache.get(&"d"), None); // d -> 1 - assert!(!cache.contains_key(&"d")); - - cache.insert("d", "david"); - cache.sync(); - assert!(!cache.contains_key(&"d")); - assert_eq!(cache.get(&"d"), None); // d -> 2 - - // "d" should be admitted and "c" should be evicted - // because d's frequency is higher than c's. - cache.insert("d", "dennis"); - cache.sync(); - assert_eq!(cache.get(&"a"), Some("alice")); - assert_eq!(cache.get(&"b"), Some("bob")); - assert_eq!(cache.get(&"c"), None); - assert_eq!(cache.get(&"d"), Some("dennis")); - assert!(cache.contains_key(&"a")); - assert!(cache.contains_key(&"b")); - assert!(!cache.contains_key(&"c")); - assert!(cache.contains_key(&"d")); - - cache.invalidate(&"b"); - assert_eq!(cache.get(&"b"), None); - assert!(!cache.contains_key(&"b")); + run_test(DeliveryMode::Immediate); + run_test(DeliveryMode::Queued); + + fn run_test(delivery_mode: DeliveryMode) { + // The following `Vec`s will hold actual and expected notifications. + let actual = Arc::new(Mutex::new(Vec::new())); + let mut expected = Vec::new(); + + // Create an eviction listener. + let a1 = Arc::clone(&actual); + let listener = move |k, v, cause| a1.lock().push((k, v, cause)); + let listener_conf = notification::Configuration::builder() + .delivery_mode(delivery_mode) + .build(); + + // Create a cache with the eviction listener. + let mut cache = Cache::builder() + .max_capacity(3) + .eviction_listener_with_conf(listener, listener_conf) + .build(); + cache.reconfigure_for_testing(); + + // Make the cache exterior immutable. + let cache = cache; + + cache.insert("a", "alice"); + cache.insert("b", "bob"); + assert_eq_with_mode!(cache.get(&"a"), Some("alice"), delivery_mode); + assert_with_mode!(cache.contains_key(&"a"), delivery_mode); + assert_with_mode!(cache.contains_key(&"b"), delivery_mode); + assert_eq_with_mode!(cache.get(&"b"), Some("bob"), delivery_mode); + cache.sync(); + // counts: a -> 1, b -> 1 + + cache.insert("c", "cindy"); + assert_eq_with_mode!(cache.get(&"c"), Some("cindy"), delivery_mode); + assert_with_mode!(cache.contains_key(&"c"), delivery_mode); + // counts: a -> 1, b -> 1, c -> 1 + cache.sync(); + + assert_with_mode!(cache.contains_key(&"a"), delivery_mode); + assert_eq_with_mode!(cache.get(&"a"), Some("alice"), delivery_mode); + assert_eq_with_mode!(cache.get(&"b"), Some("bob"), delivery_mode); + assert_with_mode!(cache.contains_key(&"b"), delivery_mode); + cache.sync(); + // counts: a -> 2, b -> 2, c -> 1 + + // "d" should not be admitted because its frequency is too low. + cache.insert("d", "david"); // count: d -> 0 + expected.push((Arc::new("d"), "david", RemovalCause::Size)); + cache.sync(); + assert_eq_with_mode!(cache.get(&"d"), None, delivery_mode); // d -> 1 + assert_with_mode!(!cache.contains_key(&"d"), delivery_mode); + + cache.insert("d", "david"); + expected.push((Arc::new("d"), "david", RemovalCause::Size)); + cache.sync(); + assert_with_mode!(!cache.contains_key(&"d"), delivery_mode); + assert_eq_with_mode!(cache.get(&"d"), None, delivery_mode); // d -> 2 + + // "d" should be admitted and "c" should be evicted + // because d's frequency is higher than c's. + cache.insert("d", "dennis"); + expected.push((Arc::new("c"), "cindy", RemovalCause::Size)); + cache.sync(); + assert_eq_with_mode!(cache.get(&"a"), Some("alice"), delivery_mode); + assert_eq_with_mode!(cache.get(&"b"), Some("bob"), delivery_mode); + assert_eq_with_mode!(cache.get(&"c"), None, delivery_mode); + assert_eq_with_mode!(cache.get(&"d"), Some("dennis"), delivery_mode); + assert_with_mode!(cache.contains_key(&"a"), delivery_mode); + assert_with_mode!(cache.contains_key(&"b"), delivery_mode); + assert_with_mode!(!cache.contains_key(&"c"), delivery_mode); + assert_with_mode!(cache.contains_key(&"d"), delivery_mode); + + cache.invalidate(&"b"); + expected.push((Arc::new("b"), "bob", RemovalCause::Explicit)); + cache.sync(); + assert_eq_with_mode!(cache.get(&"b"), None, delivery_mode); + assert_with_mode!(!cache.contains_key(&"b"), delivery_mode); + + verify_notification_vec(&cache, actual, &expected, delivery_mode); + } } #[test] fn size_aware_eviction() { - let weigher = |_k: &&str, v: &(&str, u32)| v.1; - - let alice = ("alice", 10); - let bob = ("bob", 15); - let bill = ("bill", 20); - let cindy = ("cindy", 5); - let david = ("david", 15); - let dennis = ("dennis", 15); - - let mut cache = Cache::builder().max_capacity(31).weigher(weigher).build(); - cache.reconfigure_for_testing(); - - // Make the cache exterior immutable. - let cache = cache; - - cache.insert("a", alice); - cache.insert("b", bob); - assert_eq!(cache.get(&"a"), Some(alice)); - assert!(cache.contains_key(&"a")); - assert!(cache.contains_key(&"b")); - assert_eq!(cache.get(&"b"), Some(bob)); - cache.sync(); - // order (LRU -> MRU) and counts: a -> 1, b -> 1 - - cache.insert("c", cindy); - assert_eq!(cache.get(&"c"), Some(cindy)); - assert!(cache.contains_key(&"c")); - // order and counts: a -> 1, b -> 1, c -> 1 - cache.sync(); - - assert!(cache.contains_key(&"a")); - assert_eq!(cache.get(&"a"), Some(alice)); - assert_eq!(cache.get(&"b"), Some(bob)); - assert!(cache.contains_key(&"b")); - cache.sync(); - // order and counts: c -> 1, a -> 2, b -> 2 - - // To enter "d" (weight: 15), it needs to evict "c" (w: 5) and "a" (w: 10). - // "d" must have higher count than 3, which is the aggregated count - // of "a" and "c". - cache.insert("d", david); // count: d -> 0 - cache.sync(); - assert_eq!(cache.get(&"d"), None); // d -> 1 - assert!(!cache.contains_key(&"d")); - - cache.insert("d", david); - cache.sync(); - assert!(!cache.contains_key(&"d")); - assert_eq!(cache.get(&"d"), None); // d -> 2 - - cache.insert("d", david); - cache.sync(); - assert_eq!(cache.get(&"d"), None); // d -> 3 - assert!(!cache.contains_key(&"d")); - - cache.insert("d", david); - cache.sync(); - assert!(!cache.contains_key(&"d")); - assert_eq!(cache.get(&"d"), None); // d -> 4 - - // Finally "d" should be admitted by evicting "c" and "a". - cache.insert("d", dennis); - cache.sync(); - assert_eq!(cache.get(&"a"), None); - assert_eq!(cache.get(&"b"), Some(bob)); - assert_eq!(cache.get(&"c"), None); - assert_eq!(cache.get(&"d"), Some(dennis)); - assert!(!cache.contains_key(&"a")); - assert!(cache.contains_key(&"b")); - assert!(!cache.contains_key(&"c")); - assert!(cache.contains_key(&"d")); - - // Update "b" with "bill" (w: 15 -> 20). This should evict "d" (w: 15). - cache.insert("b", bill); - cache.sync(); - assert_eq!(cache.get(&"b"), Some(bill)); - assert_eq!(cache.get(&"d"), None); - assert!(cache.contains_key(&"b")); - assert!(!cache.contains_key(&"d")); - - // Re-add "a" (w: 10) and update "b" with "bob" (w: 20 -> 15). - cache.insert("a", alice); - cache.insert("b", bob); - cache.sync(); - assert_eq!(cache.get(&"a"), Some(alice)); - assert_eq!(cache.get(&"b"), Some(bob)); - assert_eq!(cache.get(&"d"), None); - assert!(cache.contains_key(&"a")); - assert!(cache.contains_key(&"b")); - assert!(!cache.contains_key(&"d")); - - // Verify the sizes. - assert_eq!(cache.entry_count(), 2); - assert_eq!(cache.weighted_size(), 25); + run_test(DeliveryMode::Immediate); + run_test(DeliveryMode::Queued); + + fn run_test(delivery_mode: DeliveryMode) { + let weigher = |_k: &&str, v: &(&str, u32)| v.1; + + let alice = ("alice", 10); + let bob = ("bob", 15); + let bill = ("bill", 20); + let cindy = ("cindy", 5); + let david = ("david", 15); + let dennis = ("dennis", 15); + + // The following `Vec`s will hold actual and expected notifications. + let actual = Arc::new(Mutex::new(Vec::new())); + let mut expected = Vec::new(); + + // Create an eviction listener. + let a1 = Arc::clone(&actual); + let listener = move |k, v, cause| a1.lock().push((k, v, cause)); + let listener_conf = notification::Configuration::builder() + .delivery_mode(delivery_mode) + .build(); + + // Create a cache with the eviction listener. + let mut cache = Cache::builder() + .max_capacity(31) + .weigher(weigher) + .eviction_listener_with_conf(listener, listener_conf) + .build(); + cache.reconfigure_for_testing(); + + // Make the cache exterior immutable. + let cache = cache; + + cache.insert("a", alice); + cache.insert("b", bob); + assert_eq_with_mode!(cache.get(&"a"), Some(alice), delivery_mode); + assert_with_mode!(cache.contains_key(&"a"), delivery_mode); + assert_with_mode!(cache.contains_key(&"b"), delivery_mode); + assert_eq_with_mode!(cache.get(&"b"), Some(bob), delivery_mode); + cache.sync(); + // order (LRU -> MRU) and counts: a -> 1, b -> 1 + + cache.insert("c", cindy); + assert_eq_with_mode!(cache.get(&"c"), Some(cindy), delivery_mode); + assert_with_mode!(cache.contains_key(&"c"), delivery_mode); + // order and counts: a -> 1, b -> 1, c -> 1 + cache.sync(); + + assert_with_mode!(cache.contains_key(&"a"), delivery_mode); + assert_eq_with_mode!(cache.get(&"a"), Some(alice), delivery_mode); + assert_eq_with_mode!(cache.get(&"b"), Some(bob), delivery_mode); + assert_with_mode!(cache.contains_key(&"b"), delivery_mode); + cache.sync(); + // order and counts: c -> 1, a -> 2, b -> 2 + + // To enter "d" (weight: 15), it needs to evict "c" (w: 5) and "a" (w: 10). + // "d" must have higher count than 3, which is the aggregated count + // of "a" and "c". + cache.insert("d", david); // count: d -> 0 + expected.push((Arc::new("d"), david, RemovalCause::Size)); + cache.sync(); + assert_eq_with_mode!(cache.get(&"d"), None, delivery_mode); // d -> 1 + assert_with_mode!(!cache.contains_key(&"d"), delivery_mode); + + cache.insert("d", david); + expected.push((Arc::new("d"), david, RemovalCause::Size)); + cache.sync(); + assert_with_mode!(!cache.contains_key(&"d"), delivery_mode); + assert_eq_with_mode!(cache.get(&"d"), None, delivery_mode); // d -> 2 + + cache.insert("d", david); + expected.push((Arc::new("d"), david, RemovalCause::Size)); + cache.sync(); + assert_eq_with_mode!(cache.get(&"d"), None, delivery_mode); // d -> 3 + assert_with_mode!(!cache.contains_key(&"d"), delivery_mode); + + cache.insert("d", david); + expected.push((Arc::new("d"), david, RemovalCause::Size)); + cache.sync(); + assert_with_mode!(!cache.contains_key(&"d"), delivery_mode); + assert_eq_with_mode!(cache.get(&"d"), None, delivery_mode); // d -> 4 + + // Finally "d" should be admitted by evicting "c" and "a". + cache.insert("d", dennis); + expected.push((Arc::new("c"), cindy, RemovalCause::Size)); + expected.push((Arc::new("a"), alice, RemovalCause::Size)); + cache.sync(); + assert_eq_with_mode!(cache.get(&"a"), None, delivery_mode); + assert_eq_with_mode!(cache.get(&"b"), Some(bob), delivery_mode); + assert_eq_with_mode!(cache.get(&"c"), None, delivery_mode); + assert_eq_with_mode!(cache.get(&"d"), Some(dennis), delivery_mode); + assert_with_mode!(!cache.contains_key(&"a"), delivery_mode); + assert_with_mode!(cache.contains_key(&"b"), delivery_mode); + assert_with_mode!(!cache.contains_key(&"c"), delivery_mode); + assert_with_mode!(cache.contains_key(&"d"), delivery_mode); + + // Update "b" with "bill" (w: 15 -> 20). This should evict "d" (w: 15). + cache.insert("b", bill); + expected.push((Arc::new("b"), bob, RemovalCause::Replaced)); + expected.push((Arc::new("d"), dennis, RemovalCause::Size)); + cache.sync(); + assert_eq_with_mode!(cache.get(&"b"), Some(bill), delivery_mode); + assert_eq_with_mode!(cache.get(&"d"), None, delivery_mode); + assert_with_mode!(cache.contains_key(&"b"), delivery_mode); + assert_with_mode!(!cache.contains_key(&"d"), delivery_mode); + + // Re-add "a" (w: 10) and update "b" with "bob" (w: 20 -> 15). + cache.insert("a", alice); + cache.insert("b", bob); + expected.push((Arc::new("b"), bill, RemovalCause::Replaced)); + cache.sync(); + assert_eq_with_mode!(cache.get(&"a"), Some(alice), delivery_mode); + assert_eq_with_mode!(cache.get(&"b"), Some(bob), delivery_mode); + assert_eq_with_mode!(cache.get(&"d"), None, delivery_mode); + assert_with_mode!(cache.contains_key(&"a"), delivery_mode); + assert_with_mode!(cache.contains_key(&"b"), delivery_mode); + assert_with_mode!(!cache.contains_key(&"d"), delivery_mode); + + // Verify the sizes. + assert_eq_with_mode!(cache.entry_count(), 2, delivery_mode); + assert_eq_with_mode!(cache.weighted_size(), 25, delivery_mode); + + verify_notification_vec(&cache, actual, &expected, delivery_mode); + } } #[test] @@ -1179,255 +1730,352 @@ mod tests { #[test] fn invalidate_all() { - let mut cache = Cache::new(100); - cache.reconfigure_for_testing(); - - // Make the cache exterior immutable. - let cache = cache; - - cache.insert("a", "alice"); - cache.insert("b", "bob"); - cache.insert("c", "cindy"); - assert_eq!(cache.get(&"a"), Some("alice")); - assert_eq!(cache.get(&"b"), Some("bob")); - assert_eq!(cache.get(&"c"), Some("cindy")); - assert!(cache.contains_key(&"a")); - assert!(cache.contains_key(&"b")); - assert!(cache.contains_key(&"c")); - - // `cache.sync()` is no longer needed here before invalidating. The last - // modified timestamp of the entries were updated when they were inserted. - // https://github.com/moka-rs/moka/issues/155 - - cache.invalidate_all(); - cache.sync(); - - cache.insert("d", "david"); - cache.sync(); - - assert!(cache.get(&"a").is_none()); - assert!(cache.get(&"b").is_none()); - assert!(cache.get(&"c").is_none()); - assert_eq!(cache.get(&"d"), Some("david")); - assert!(!cache.contains_key(&"a")); - assert!(!cache.contains_key(&"b")); - assert!(!cache.contains_key(&"c")); - assert!(cache.contains_key(&"d")); + run_test(DeliveryMode::Immediate); + run_test(DeliveryMode::Queued); + + fn run_test(delivery_mode: DeliveryMode) { + // The following `Vec`s will hold actual and expected notifications. + let actual = Arc::new(Mutex::new(Vec::new())); + let mut expected = Vec::new(); + + // Create an eviction listener. + let a1 = Arc::clone(&actual); + let listener = move |k, v, cause| a1.lock().push((k, v, cause)); + let listener_conf = notification::Configuration::builder() + .delivery_mode(delivery_mode) + .build(); + + // Create a cache with the eviction listener. + let mut cache = Cache::builder() + .max_capacity(100) + .eviction_listener_with_conf(listener, listener_conf) + .build(); + cache.reconfigure_for_testing(); + + // Make the cache exterior immutable. + let cache = cache; + + cache.insert("a", "alice"); + cache.insert("b", "bob"); + cache.insert("c", "cindy"); + assert_eq_with_mode!(cache.get(&"a"), Some("alice"), delivery_mode); + assert_eq_with_mode!(cache.get(&"b"), Some("bob"), delivery_mode); + assert_eq_with_mode!(cache.get(&"c"), Some("cindy"), delivery_mode); + assert_with_mode!(cache.contains_key(&"a"), delivery_mode); + assert_with_mode!(cache.contains_key(&"b"), delivery_mode); + assert_with_mode!(cache.contains_key(&"c"), delivery_mode); + + // `cache.sync()` is no longer needed here before invalidating. The last + // modified timestamp of the entries were updated when they were inserted. + // https://github.com/moka-rs/moka/issues/155 + + cache.invalidate_all(); + expected.push((Arc::new("a"), "alice", RemovalCause::Explicit)); + expected.push((Arc::new("b"), "bob", RemovalCause::Explicit)); + expected.push((Arc::new("c"), "cindy", RemovalCause::Explicit)); + cache.sync(); + + cache.insert("d", "david"); + cache.sync(); + + assert_with_mode!(cache.get(&"a").is_none(), delivery_mode); + assert_with_mode!(cache.get(&"b").is_none(), delivery_mode); + assert_with_mode!(cache.get(&"c").is_none(), delivery_mode); + assert_eq_with_mode!(cache.get(&"d"), Some("david"), delivery_mode); + assert_with_mode!(!cache.contains_key(&"a"), delivery_mode); + assert_with_mode!(!cache.contains_key(&"b"), delivery_mode); + assert_with_mode!(!cache.contains_key(&"c"), delivery_mode); + assert_with_mode!(cache.contains_key(&"d"), delivery_mode); + + verify_notification_vec(&cache, actual, &expected, delivery_mode); + } } #[test] fn invalidate_entries_if() -> Result<(), Box> { - use std::collections::HashSet; - - let mut cache = Cache::builder() - .max_capacity(100) - .support_invalidation_closures() - .build(); - cache.reconfigure_for_testing(); - - let (clock, mock) = Clock::mock(); - cache.set_expiration_clock(Some(clock)); - - // Make the cache exterior immutable. - let cache = cache; - - cache.insert(0, "alice"); - cache.insert(1, "bob"); - cache.insert(2, "alex"); - cache.sync(); - - mock.increment(Duration::from_secs(5)); // 5 secs from the start. - cache.sync(); - - assert_eq!(cache.get(&0), Some("alice")); - assert_eq!(cache.get(&1), Some("bob")); - assert_eq!(cache.get(&2), Some("alex")); - assert!(cache.contains_key(&0)); - assert!(cache.contains_key(&1)); - assert!(cache.contains_key(&2)); - - let names = ["alice", "alex"].iter().cloned().collect::>(); - cache.invalidate_entries_if(move |_k, &v| names.contains(v))?; - assert_eq!(cache.base.invalidation_predicate_count(), 1); - - mock.increment(Duration::from_secs(5)); // 10 secs from the start. - - cache.insert(3, "alice"); - - // Run the invalidation task and wait for it to finish. (TODO: Need a better way than sleeping) - cache.sync(); // To submit the invalidation task. - std::thread::sleep(Duration::from_millis(200)); - cache.sync(); // To process the task result. - std::thread::sleep(Duration::from_millis(200)); + run_test(DeliveryMode::Immediate)?; + run_test(DeliveryMode::Queued)?; + + fn run_test(delivery_mode: DeliveryMode) -> Result<(), Box> { + use std::collections::HashSet; + + // The following `Vec`s will hold actual and expected notifications. + let actual = Arc::new(Mutex::new(Vec::new())); + let mut expected = Vec::new(); + + // Create an eviction listener. + let a1 = Arc::clone(&actual); + let listener = move |k, v, cause| a1.lock().push((k, v, cause)); + let listener_conf = notification::Configuration::builder() + .delivery_mode(delivery_mode) + .build(); + + // Create a cache with the eviction listener. + let mut cache = Cache::builder() + .max_capacity(100) + .support_invalidation_closures() + .eviction_listener_with_conf(listener, listener_conf) + .build(); + cache.reconfigure_for_testing(); + + let (clock, mock) = Clock::mock(); + cache.set_expiration_clock(Some(clock)); + + // Make the cache exterior immutable. + let cache = cache; + + cache.insert(0, "alice"); + cache.insert(1, "bob"); + cache.insert(2, "alex"); + cache.sync(); + + mock.increment(Duration::from_secs(5)); // 5 secs from the start. + cache.sync(); + + assert_eq_with_mode!(cache.get(&0), Some("alice"), delivery_mode); + assert_eq_with_mode!(cache.get(&1), Some("bob"), delivery_mode); + assert_eq_with_mode!(cache.get(&2), Some("alex"), delivery_mode); + assert_with_mode!(cache.contains_key(&0), delivery_mode); + assert_with_mode!(cache.contains_key(&1), delivery_mode); + assert_with_mode!(cache.contains_key(&2), delivery_mode); + + let names = ["alice", "alex"].iter().cloned().collect::>(); + cache.invalidate_entries_if(move |_k, &v| names.contains(v))?; + assert_eq_with_mode!(cache.base.invalidation_predicate_count(), 1, delivery_mode); + expected.push((Arc::new(0), "alice", RemovalCause::Explicit)); + expected.push((Arc::new(2), "alex", RemovalCause::Explicit)); + + mock.increment(Duration::from_secs(5)); // 10 secs from the start. + + cache.insert(3, "alice"); + + // Run the invalidation task and wait for it to finish. (TODO: Need a better way than sleeping) + cache.sync(); // To submit the invalidation task. + std::thread::sleep(Duration::from_millis(200)); + cache.sync(); // To process the task result. + std::thread::sleep(Duration::from_millis(200)); + + assert_with_mode!(cache.get(&0).is_none(), delivery_mode); + assert_with_mode!(cache.get(&2).is_none(), delivery_mode); + assert_eq_with_mode!(cache.get(&1), Some("bob"), delivery_mode); + // This should survive as it was inserted after calling invalidate_entries_if. + assert_eq_with_mode!(cache.get(&3), Some("alice"), delivery_mode); + + assert_with_mode!(!cache.contains_key(&0), delivery_mode); + assert_with_mode!(cache.contains_key(&1), delivery_mode); + assert_with_mode!(!cache.contains_key(&2), delivery_mode); + assert_with_mode!(cache.contains_key(&3), delivery_mode); + + assert_eq_with_mode!(cache.entry_count(), 2, delivery_mode); + assert_eq_with_mode!(cache.invalidation_predicate_count(), 0, delivery_mode); + + mock.increment(Duration::from_secs(5)); // 15 secs from the start. + + cache.invalidate_entries_if(|_k, &v| v == "alice")?; + cache.invalidate_entries_if(|_k, &v| v == "bob")?; + assert_eq_with_mode!(cache.invalidation_predicate_count(), 2, delivery_mode); + // key 1 was inserted before key 3. + expected.push((Arc::new(1), "bob", RemovalCause::Explicit)); + expected.push((Arc::new(3), "alice", RemovalCause::Explicit)); + + // Run the invalidation task and wait for it to finish. (TODO: Need a better way than sleeping) + cache.sync(); // To submit the invalidation task. + std::thread::sleep(Duration::from_millis(200)); + cache.sync(); // To process the task result. + std::thread::sleep(Duration::from_millis(200)); + + assert_with_mode!(cache.get(&1).is_none(), delivery_mode); + assert_with_mode!(cache.get(&3).is_none(), delivery_mode); + + assert_with_mode!(!cache.contains_key(&1), delivery_mode); + assert_with_mode!(!cache.contains_key(&3), delivery_mode); + + assert_eq_with_mode!(cache.entry_count(), 0, delivery_mode); + assert_eq_with_mode!(cache.invalidation_predicate_count(), 0, delivery_mode); + + verify_notification_vec(&cache, actual, &expected, delivery_mode); + + Ok(()) + } - assert!(cache.get(&0).is_none()); - assert!(cache.get(&2).is_none()); - assert_eq!(cache.get(&1), Some("bob")); - // This should survive as it was inserted after calling invalidate_entries_if. - assert_eq!(cache.get(&3), Some("alice")); + Ok(()) + } - assert!(!cache.contains_key(&0)); - assert!(cache.contains_key(&1)); - assert!(!cache.contains_key(&2)); - assert!(cache.contains_key(&3)); + #[test] + fn time_to_live() { + run_test(DeliveryMode::Immediate); + run_test(DeliveryMode::Queued); - assert_eq!(cache.entry_count(), 2); - assert_eq!(cache.invalidation_predicate_count(), 0); + fn run_test(delivery_mode: DeliveryMode) { + // The following `Vec`s will hold actual and expected notifications. + let actual = Arc::new(Mutex::new(Vec::new())); + let mut expected = Vec::new(); - mock.increment(Duration::from_secs(5)); // 15 secs from the start. + // Create an eviction listener. + let a1 = Arc::clone(&actual); + let listener = move |k, v, cause| a1.lock().push((k, v, cause)); + let listener_conf = notification::Configuration::builder() + .delivery_mode(delivery_mode) + .build(); - cache.invalidate_entries_if(|_k, &v| v == "alice")?; - cache.invalidate_entries_if(|_k, &v| v == "bob")?; - assert_eq!(cache.invalidation_predicate_count(), 2); + // Create a cache with the eviction listener. + let mut cache = Cache::builder() + .max_capacity(100) + .time_to_live(Duration::from_secs(10)) + .eviction_listener_with_conf(listener, listener_conf) + .build(); + cache.reconfigure_for_testing(); - // Run the invalidation task and wait for it to finish. (TODO: Need a better way than sleeping) - cache.sync(); // To submit the invalidation task. - std::thread::sleep(Duration::from_millis(200)); - cache.sync(); // To process the task result. - std::thread::sleep(Duration::from_millis(200)); + let (clock, mock) = Clock::mock(); + cache.set_expiration_clock(Some(clock)); - assert!(cache.get(&1).is_none()); - assert!(cache.get(&3).is_none()); + // Make the cache exterior immutable. + let cache = cache; - assert!(!cache.contains_key(&1)); - assert!(!cache.contains_key(&3)); + cache.insert("a", "alice"); + cache.sync(); - assert_eq!(cache.entry_count(), 0); - assert_eq!(cache.invalidation_predicate_count(), 0); + mock.increment(Duration::from_secs(5)); // 5 secs from the start. + cache.sync(); - Ok(()) - } + assert_eq_with_mode!(cache.get(&"a"), Some("alice"), delivery_mode); + assert_with_mode!(cache.contains_key(&"a"), delivery_mode); - #[test] - fn time_to_live() { - let mut cache = Cache::builder() - .max_capacity(100) - .time_to_live(Duration::from_secs(10)) - .build(); + mock.increment(Duration::from_secs(5)); // 10 secs. + expected.push((Arc::new("a"), "alice", RemovalCause::Expired)); + assert_eq_with_mode!(cache.get(&"a"), None, delivery_mode); + assert_with_mode!(!cache.contains_key(&"a"), delivery_mode); - cache.reconfigure_for_testing(); + assert_eq_with_mode!(cache.iter().count(), 0, delivery_mode); - let (clock, mock) = Clock::mock(); - cache.set_expiration_clock(Some(clock)); + cache.sync(); + assert_with_mode!(cache.is_table_empty(), delivery_mode); - // Make the cache exterior immutable. - let cache = cache; + cache.insert("b", "bob"); + cache.sync(); - cache.insert("a", "alice"); - cache.sync(); + assert_eq_with_mode!(cache.entry_count(), 1, delivery_mode); - mock.increment(Duration::from_secs(5)); // 5 secs from the start. - cache.sync(); + mock.increment(Duration::from_secs(5)); // 15 secs. + cache.sync(); - assert_eq!(cache.get(&"a"), Some("alice")); - assert!(cache.contains_key(&"a")); + assert_eq_with_mode!(cache.get(&"b"), Some("bob"), delivery_mode); + assert_with_mode!(cache.contains_key(&"b"), delivery_mode); + assert_eq_with_mode!(cache.entry_count(), 1, delivery_mode); - mock.increment(Duration::from_secs(5)); // 10 secs. - assert_eq!(cache.get(&"a"), None); - assert!(!cache.contains_key(&"a")); + cache.insert("b", "bill"); + expected.push((Arc::new("b"), "bob", RemovalCause::Replaced)); + cache.sync(); - assert_eq!(cache.iter().count(), 0); + mock.increment(Duration::from_secs(5)); // 20 secs + cache.sync(); - cache.sync(); - assert!(cache.is_table_empty()); + assert_eq_with_mode!(cache.get(&"b"), Some("bill"), delivery_mode); + assert_with_mode!(cache.contains_key(&"b"), delivery_mode); + assert_eq_with_mode!(cache.entry_count(), 1, delivery_mode); - cache.insert("b", "bob"); - cache.sync(); + mock.increment(Duration::from_secs(5)); // 25 secs + expected.push((Arc::new("b"), "bill", RemovalCause::Expired)); - assert_eq!(cache.entry_count(), 1); + assert_eq_with_mode!(cache.get(&"a"), None, delivery_mode); + assert_eq_with_mode!(cache.get(&"b"), None, delivery_mode); + assert_with_mode!(!cache.contains_key(&"a"), delivery_mode); + assert_with_mode!(!cache.contains_key(&"b"), delivery_mode); - mock.increment(Duration::from_secs(5)); // 15 secs. - cache.sync(); + assert_eq_with_mode!(cache.iter().count(), 0, delivery_mode); - assert_eq!(cache.get(&"b"), Some("bob")); - assert!(cache.contains_key(&"b")); - assert_eq!(cache.entry_count(), 1); + cache.sync(); + assert_with_mode!(cache.is_table_empty(), delivery_mode); - cache.insert("b", "bill"); - cache.sync(); + verify_notification_vec(&cache, actual, &expected, delivery_mode); + } + } - mock.increment(Duration::from_secs(5)); // 20 secs - cache.sync(); + #[test] + fn time_to_idle() { + run_test(DeliveryMode::Immediate); + run_test(DeliveryMode::Queued); - assert_eq!(cache.get(&"b"), Some("bill")); - assert!(cache.contains_key(&"b")); - assert_eq!(cache.entry_count(), 1); + fn run_test(delivery_mode: DeliveryMode) { + // The following `Vec`s will hold actual and expected notifications. + let actual = Arc::new(Mutex::new(Vec::new())); + let mut expected = Vec::new(); - mock.increment(Duration::from_secs(5)); // 25 secs + // Create an eviction listener. + let a1 = Arc::clone(&actual); + let listener = move |k, v, cause| a1.lock().push((k, v, cause)); + let listener_conf = notification::Configuration::builder() + .delivery_mode(delivery_mode) + .build(); - assert_eq!(cache.get(&"a"), None); - assert_eq!(cache.get(&"b"), None); - assert!(!cache.contains_key(&"a")); - assert!(!cache.contains_key(&"b")); + // Create a cache with the eviction listener. + let mut cache = Cache::builder() + .max_capacity(100) + .time_to_idle(Duration::from_secs(10)) + .eviction_listener_with_conf(listener, listener_conf) + .build(); + cache.reconfigure_for_testing(); - assert_eq!(cache.iter().count(), 0); + let (clock, mock) = Clock::mock(); + cache.set_expiration_clock(Some(clock)); - cache.sync(); - assert!(cache.is_table_empty()); - } + // Make the cache exterior immutable. + let cache = cache; - #[test] - fn time_to_idle() { - let mut cache = Cache::builder() - .max_capacity(100) - .time_to_idle(Duration::from_secs(10)) - .build(); + cache.insert("a", "alice"); + cache.sync(); - cache.reconfigure_for_testing(); + mock.increment(Duration::from_secs(5)); // 5 secs from the start. + cache.sync(); - let (clock, mock) = Clock::mock(); - cache.set_expiration_clock(Some(clock)); + assert_eq_with_mode!(cache.get(&"a"), Some("alice"), delivery_mode); - // Make the cache exterior immutable. - let cache = cache; + mock.increment(Duration::from_secs(5)); // 10 secs. + cache.sync(); - cache.insert("a", "alice"); - cache.sync(); + cache.insert("b", "bob"); + cache.sync(); - mock.increment(Duration::from_secs(5)); // 5 secs from the start. - cache.sync(); + assert_eq_with_mode!(cache.entry_count(), 2, delivery_mode); - assert_eq!(cache.get(&"a"), Some("alice")); + mock.increment(Duration::from_secs(2)); // 12 secs. + cache.sync(); - mock.increment(Duration::from_secs(5)); // 10 secs. - cache.sync(); + // contains_key does not reset the idle timer for the key. + assert_with_mode!(cache.contains_key(&"a"), delivery_mode); + assert_with_mode!(cache.contains_key(&"b"), delivery_mode); + cache.sync(); - cache.insert("b", "bob"); - cache.sync(); + assert_eq_with_mode!(cache.entry_count(), 2, delivery_mode); - assert_eq!(cache.entry_count(), 2); + mock.increment(Duration::from_secs(3)); // 15 secs. + expected.push((Arc::new("a"), "alice", RemovalCause::Expired)); - mock.increment(Duration::from_secs(2)); // 12 secs. - cache.sync(); - - // contains_key does not reset the idle timer for the key. - assert!(cache.contains_key(&"a")); - assert!(cache.contains_key(&"b")); - cache.sync(); + assert_eq_with_mode!(cache.get(&"a"), None, delivery_mode); + assert_eq_with_mode!(cache.get(&"b"), Some("bob"), delivery_mode); + assert_with_mode!(!cache.contains_key(&"a"), delivery_mode); + assert_with_mode!(cache.contains_key(&"b"), delivery_mode); - assert_eq!(cache.entry_count(), 2); + assert_eq_with_mode!(cache.iter().count(), 1, delivery_mode); - mock.increment(Duration::from_secs(3)); // 15 secs. - assert_eq!(cache.get(&"a"), None); - assert_eq!(cache.get(&"b"), Some("bob")); - assert!(!cache.contains_key(&"a")); - assert!(cache.contains_key(&"b")); + cache.sync(); + assert_eq_with_mode!(cache.entry_count(), 1, delivery_mode); - assert_eq!(cache.iter().count(), 1); + mock.increment(Duration::from_secs(10)); // 25 secs + expected.push((Arc::new("b"), "bob", RemovalCause::Expired)); - cache.sync(); - assert_eq!(cache.entry_count(), 1); + assert_eq_with_mode!(cache.get(&"a"), None, delivery_mode); + assert_eq_with_mode!(cache.get(&"b"), None, delivery_mode); + assert_with_mode!(!cache.contains_key(&"a"), delivery_mode); + assert_with_mode!(!cache.contains_key(&"b"), delivery_mode); - mock.increment(Duration::from_secs(10)); // 25 secs - assert_eq!(cache.get(&"a"), None); - assert_eq!(cache.get(&"b"), None); - assert!(!cache.contains_key(&"a")); - assert!(!cache.contains_key(&"b")); + assert_eq_with_mode!(cache.iter().count(), 0, delivery_mode); - assert_eq!(cache.iter().count(), 0); + cache.sync(); + assert_with_mode!(cache.is_table_empty(), delivery_mode); - cache.sync(); - assert!(cache.is_table_empty()); + verify_notification_vec(&cache, actual, &expected, delivery_mode); + } } #[test] @@ -1931,6 +2579,376 @@ mod tests { ); } + #[test] + fn test_removal_notifications() { + run_test(DeliveryMode::Immediate); + run_test(DeliveryMode::Queued); + + fn run_test(delivery_mode: DeliveryMode) { + // The following `Vec`s will hold actual and expected notifications. + let actual = Arc::new(Mutex::new(Vec::new())); + let mut expected = Vec::new(); + + // Create an eviction listener. + let a1 = Arc::clone(&actual); + let listener = move |k, v, cause| a1.lock().push((k, v, cause)); + let listener_conf = notification::Configuration::builder() + .delivery_mode(delivery_mode) + .build(); + + // Create a cache with the eviction listener. + let mut cache = Cache::builder() + .max_capacity(3) + .eviction_listener_with_conf(listener, listener_conf) + .build(); + cache.reconfigure_for_testing(); + + // Make the cache exterior immutable. + let cache = cache; + + cache.insert('a', "alice"); + cache.invalidate(&'a'); + expected.push((Arc::new('a'), "alice", RemovalCause::Explicit)); + + cache.sync(); + assert_eq_with_mode!(cache.entry_count(), 0, delivery_mode); + + cache.insert('b', "bob"); + cache.insert('c', "cathy"); + cache.insert('d', "david"); + cache.sync(); + assert_eq_with_mode!(cache.entry_count(), 3, delivery_mode); + + // This will be rejected due to the size constraint. + cache.insert('e', "emily"); + expected.push((Arc::new('e'), "emily", RemovalCause::Size)); + cache.sync(); + assert_eq_with_mode!(cache.entry_count(), 3, delivery_mode); + + // Raise the popularity of 'e' so it will be accepted next time. + cache.get(&'e'); + cache.sync(); + + // Retry. + cache.insert('e', "eliza"); + // and the LRU entry will be evicted. + expected.push((Arc::new('b'), "bob", RemovalCause::Size)); + cache.sync(); + assert_eq_with_mode!(cache.entry_count(), 3, delivery_mode); + + // Replace an existing entry. + cache.insert('d', "dennis"); + expected.push((Arc::new('d'), "david", RemovalCause::Replaced)); + cache.sync(); + assert_eq_with_mode!(cache.entry_count(), 3, delivery_mode); + + verify_notification_vec(&cache, actual, &expected, delivery_mode); + } + } + + #[test] + fn test_immediate_removal_notifications_with_updates() { + // The following `Vec` will hold actual notifications. + let actual = Arc::new(Mutex::new(Vec::new())); + + // Create an eviction listener. + let a1 = Arc::clone(&actual); + let listener = move |k, v, cause| a1.lock().push((k, v, cause)); + let listener_conf = notification::Configuration::builder() + .delivery_mode(DeliveryMode::Immediate) + .build(); + + // Create a cache with the eviction listener and also TTL and TTI. + let mut cache = Cache::builder() + .eviction_listener_with_conf(listener, listener_conf) + .time_to_live(Duration::from_secs(7)) + .time_to_idle(Duration::from_secs(5)) + .build(); + cache.reconfigure_for_testing(); + + let (clock, mock) = Clock::mock(); + cache.set_expiration_clock(Some(clock)); + + // Make the cache exterior immutable. + let cache = cache; + + cache.insert("alice", "a0"); + cache.sync(); + + // Now alice (a0) has been expired by the idle timeout (TTI). + mock.increment(Duration::from_secs(6)); + assert_eq!(cache.get(&"alice"), None); + + // We have not ran sync after the expiration of alice (a0), so it is + // still in the cache. + assert_eq!(cache.entry_count(), 1); + + // Re-insert alice with a different value. Since alice (a0) is still + // in the cache, this is actually a replace operation rather than an + // insert operation. We want to verify that the RemovalCause of a0 is + // Expired, not Replaced. + cache.insert("alice", "a1"); + { + let mut a = actual.lock(); + assert_eq!(a.len(), 1); + assert_eq!(a[0], (Arc::new("alice"), "a0", RemovalCause::Expired)); + a.clear(); + } + + cache.sync(); + + mock.increment(Duration::from_secs(4)); + assert_eq!(cache.get(&"alice"), Some("a1")); + cache.sync(); + + // Now alice has been expired by time-to-live (TTL). + mock.increment(Duration::from_secs(4)); + assert_eq!(cache.get(&"alice"), None); + + // But, again, it is still in the cache. + assert_eq!(cache.entry_count(), 1); + + // Re-insert alice with a different value and verify that the + // RemovalCause of a1 is Expired (not Replaced). + cache.insert("alice", "a2"); + { + let mut a = actual.lock(); + assert_eq!(a.len(), 1); + assert_eq!(a[0], (Arc::new("alice"), "a1", RemovalCause::Expired)); + a.clear(); + } + + cache.sync(); + + assert_eq!(cache.entry_count(), 1); + + // Now alice (a2) has been expired by the idle timeout. + mock.increment(Duration::from_secs(6)); + assert_eq!(cache.get(&"alice"), None); + assert_eq!(cache.entry_count(), 1); + + // This invalidate will internally remove alice (a2). + cache.invalidate(&"alice"); + cache.sync(); + assert_eq!(cache.entry_count(), 0); + + { + let mut a = actual.lock(); + assert_eq!(a.len(), 1); + assert_eq!(a[0], (Arc::new("alice"), "a2", RemovalCause::Expired)); + a.clear(); + } + + // Re-insert, and this time, make it expired by the TTL. + cache.insert("alice", "a3"); + cache.sync(); + mock.increment(Duration::from_secs(4)); + assert_eq!(cache.get(&"alice"), Some("a3")); + cache.sync(); + mock.increment(Duration::from_secs(4)); + assert_eq!(cache.get(&"alice"), None); + assert_eq!(cache.entry_count(), 1); + + // This invalidate will internally remove alice (a2). + cache.invalidate(&"alice"); + cache.sync(); + + assert_eq!(cache.entry_count(), 0); + + { + let mut a = actual.lock(); + assert_eq!(a.len(), 1); + assert_eq!(a[0], (Arc::new("alice"), "a3", RemovalCause::Expired)); + a.clear(); + } + } + + // This test ensures the key-level lock for the immediate notification + // delivery mode is working so that the notifications for a given key + // should always be ordered. This is true even if multiple client threads + // try to modify the entries for the key at the same time. (This test will + // run three client threads) + #[test] + fn test_key_lock_used_by_immediate_removal_notifications() { + use std::thread::{sleep, spawn}; + + const KEY: &str = "alice"; + + type Val = &'static str; + + #[derive(PartialEq, Eq, Debug)] + enum Event { + Insert(Val), + Invalidate(Val), + BeginNotify(Val, RemovalCause), + EndNotify(Val, RemovalCause), + } + + // The following `Vec will hold actual notifications. + let actual = Arc::new(Mutex::new(Vec::new())); + + // Create an eviction listener. + // Note that this listener is slow and will take ~100 ms to complete. + let a0 = Arc::clone(&actual); + let listener = move |_k, v, cause| { + a0.lock().push(Event::BeginNotify(v, cause)); + sleep(Duration::from_millis(100)); + a0.lock().push(Event::EndNotify(v, cause)); + }; + let listener_conf = notification::Configuration::builder() + .delivery_mode(DeliveryMode::Immediate) + .build(); + + // Create a cache with the eviction listener and also TTL. + let cache = Cache::builder() + .eviction_listener_with_conf(listener, listener_conf) + .time_to_live(Duration::from_millis(200)) + .build(); + + // - Notifications for the same key must not overlap. + + // Time Event + // ----- ------------------------------------- + // 0000: Insert value a0 + // 0200: a0 expired + // 0210: Insert value a1 -> expired a0 (N-A0) + // 0220: Insert value a2 (waiting) (A-A2) + // 0310: N-A0 processed + // A-A2 finished waiting -> replace a1 (N-A1) + // 0320: Invalidate (waiting) (R-A2) + // 0410: N-A1 processed + // R-A2 finished waiting -> explicit a2 (N-A2) + // 0510: N-A2 processed + + let expected = vec![ + Event::Insert("a0"), + Event::Insert("a1"), + Event::BeginNotify("a0", RemovalCause::Expired), + Event::Insert("a2"), + Event::EndNotify("a0", RemovalCause::Expired), + Event::BeginNotify("a1", RemovalCause::Replaced), + Event::Invalidate("a2"), + Event::EndNotify("a1", RemovalCause::Replaced), + Event::BeginNotify("a2", RemovalCause::Explicit), + Event::EndNotify("a2", RemovalCause::Explicit), + ]; + + // 0000: Insert value a0 + actual.lock().push(Event::Insert("a0")); + cache.insert(KEY, "a0"); + // Call `sync` to set the last modified for the KEY immediately so that + // this entry should expire in 200 ms from now. + cache.sync(); + + // 0210: Insert value a1 -> expired a0 (N-A0) + let thread1 = { + let a1 = Arc::clone(&actual); + let c1 = cache.clone(); + spawn(move || { + sleep(Duration::from_millis(210)); + a1.lock().push(Event::Insert("a1")); + c1.insert(KEY, "a1"); + }) + }; + + // 0220: Insert value a2 (waiting) (A-A2) + let thread2 = { + let a2 = Arc::clone(&actual); + let c2 = cache.clone(); + spawn(move || { + sleep(Duration::from_millis(220)); + a2.lock().push(Event::Insert("a2")); + c2.insert(KEY, "a2"); + }) + }; + + // 0320: Invalidate (waiting) (R-A2) + let thread3 = { + let a3 = Arc::clone(&actual); + let c3 = cache.clone(); + spawn(move || { + sleep(Duration::from_millis(320)); + a3.lock().push(Event::Invalidate("a2")); + c3.invalidate(&KEY); + }) + }; + + for t in vec![thread1, thread2, thread3] { + t.join().expect("Failed to join"); + } + + let actual = actual.lock(); + assert_eq!(actual.len(), expected.len()); + + for (i, (actual, expected)) in actual.iter().zip(&expected).enumerate() { + assert_eq!(actual, expected, "expected[{}]", i); + } + } + + // NOTE: To enable the panic logging, run the following command: + // + // RUST_LOG=moka=info cargo test --features 'logging' -- \ + // sync::cache::tests::recover_from_panicking_eviction_listener --exact --nocapture + // + #[test] + fn recover_from_panicking_eviction_listener() { + #[cfg(feature = "logging")] + let _ = env_logger::builder().is_test(true).try_init(); + + run_test(DeliveryMode::Immediate); + run_test(DeliveryMode::Queued); + + fn run_test(delivery_mode: DeliveryMode) { + // The following `Vec`s will hold actual and expected notifications. + let actual = Arc::new(Mutex::new(Vec::new())); + let mut expected = Vec::new(); + + // Create an eviction listener that panics when it see + // a value "panic now!". + let a1 = Arc::clone(&actual); + let listener = move |k, v, cause| { + if v == "panic now!" { + panic!("Panic now!"); + } + a1.lock().push((k, v, cause)) + }; + let listener_conf = notification::Configuration::builder() + .delivery_mode(delivery_mode) + .build(); + + // Create a cache with the eviction listener. + let mut cache = Cache::builder() + .name("My Sync Cache") + .eviction_listener_with_conf(listener, listener_conf) + .build(); + cache.reconfigure_for_testing(); + + // Make the cache exterior immutable. + let cache = cache; + + // Insert an okay value. + cache.insert("alice", "a0"); + cache.sync(); + + // Insert a value that will cause the eviction listener to panic. + cache.insert("alice", "panic now!"); + expected.push((Arc::new("alice"), "a0", RemovalCause::Replaced)); + cache.sync(); + + // Insert an okay value. This will replace the previsous + // value "panic now!" so the eviction listener will panic. + cache.insert("alice", "a2"); + cache.sync(); + // No more removal notification should be sent. + + // Invalidate the okay value. + cache.invalidate(&"alice"); + cache.sync(); + + verify_notification_vec(&cache, actual, &expected, delivery_mode); + } + } + #[test] fn test_debug_format() { let cache = Cache::new(10); @@ -1945,4 +2963,51 @@ mod tests { assert!(debug_str.contains(r#"'c': "cindy""#)); assert!(debug_str.ends_with('}')); } + + type NotificationTuple = (Arc, V, RemovalCause); + + fn verify_notification_vec( + cache: &Cache, + actual: Arc>>>, + expected: &[NotificationTuple], + delivery_mode: DeliveryMode, + ) where + K: std::hash::Hash + Eq + std::fmt::Debug + Send + Sync + 'static, + V: Eq + std::fmt::Debug + Clone + Send + Sync + 'static, + S: std::hash::BuildHasher + Clone + Send + Sync + 'static, + { + // Retries will be needed when testing in a QEMU VM. + const MAX_RETRIES: usize = 5; + let mut retries = 0; + loop { + // Ensure all scheduled notifications have been processed. + cache.sync(); + std::thread::sleep(Duration::from_millis(500)); + + let actual = &*actual.lock(); + if actual.len() != expected.len() { + if retries <= MAX_RETRIES { + retries += 1; + continue; + } else { + assert_eq!( + actual.len(), + expected.len(), + "Retries exhausted (delivery mode: {:?})", + delivery_mode + ); + } + } + + for (i, (actual, expected)) in actual.iter().zip(expected).enumerate() { + assert_eq!( + actual, expected, + "expected[{}] (delivery mode: {:?})", + i, delivery_mode + ); + } + + break; + } + } } diff --git a/src/sync/segment.rs b/src/sync/segment.rs index 7d07eef6..1aa984a8 100644 --- a/src/sync/segment.rs +++ b/src/sync/segment.rs @@ -1,6 +1,7 @@ use super::{cache::Cache, CacheBuilder, ConcurrentCacheExt}; use crate::{ common::concurrent::Weigher, + notification::{self, EvictionListener}, sync_base::iter::{Iter, ScanningGet}, Policy, PredicateError, }; @@ -95,6 +96,7 @@ where pub fn new(max_capacity: u64, num_segments: usize) -> Self { let build_hasher = RandomState::default(); Self::with_everything( + None, Some(max_capacity), None, num_segments, @@ -102,6 +104,8 @@ where None, None, None, + None, + None, false, ) } @@ -116,6 +120,11 @@ where } impl SegmentedCache { + /// Returns cache’s name. + pub fn name(&self) -> Option<&str> { + self.inner.segments[0].name() + } + /// Returns a read-only cache policy of this cache. /// /// At this time, cache policy cannot be modified after cache creation. @@ -196,22 +205,28 @@ where /// Panics if `num_segments` is 0. #[allow(clippy::too_many_arguments)] pub(crate) fn with_everything( + name: Option, max_capacity: Option, initial_capacity: Option, num_segments: usize, build_hasher: S, weigher: Option>, + eviction_listener: Option>, + eviction_listener_conf: Option, time_to_live: Option, time_to_idle: Option, invalidator_enabled: bool, ) -> Self { Self { inner: Arc::new(Inner::new( + name, max_capacity, initial_capacity, num_segments, build_hasher, weigher, + eviction_listener, + eviction_listener_conf, time_to_live, time_to_idle, invalidator_enabled, @@ -489,7 +504,7 @@ where impl ConcurrentCacheExt for SegmentedCache where K: Hash + Eq + Send + Sync + 'static, - V: Send + Sync + 'static, + V: Clone + Send + Sync + 'static, S: BuildHasher + Clone + Send + Sync + 'static, { fn sync(&self) { @@ -571,11 +586,14 @@ where /// Panics if `num_segments` is 0. #[allow(clippy::too_many_arguments)] fn new( + name: Option, max_capacity: Option, initial_capacity: Option, num_segments: usize, build_hasher: S, weigher: Option>, + eviction_listener: Option>, + eviction_listener_conf: Option, time_to_live: Option, time_to_idle: Option, invalidator_enabled: bool, @@ -592,10 +610,13 @@ where let segments = (0..actual_num_segments) .map(|_| { Cache::with_everything( + name.clone(), seg_max_capacity, seg_init_capacity, build_hasher.clone(), weigher.as_ref().map(Arc::clone), + eviction_listener.as_ref().map(Arc::clone), + eviction_listener_conf.clone(), time_to_live, time_to_idle, invalidator_enabled, @@ -641,65 +662,98 @@ where #[cfg(test)] mod tests { use super::{ConcurrentCacheExt, SegmentedCache}; + use crate::notification::{ + self, + macros::{assert_eq_with_mode, assert_with_mode}, + DeliveryMode, RemovalCause, + }; + use parking_lot::Mutex; use std::{sync::Arc, time::Duration}; #[test] fn basic_single_thread() { - let mut cache = SegmentedCache::new(3, 1); - cache.reconfigure_for_testing(); - - // Make the cache exterior immutable. - let cache = cache; - - cache.insert("a", "alice"); - cache.insert("b", "bob"); - assert_eq!(cache.get(&"a"), Some("alice")); - assert!(cache.contains_key(&"a")); - assert!(cache.contains_key(&"b")); - assert_eq!(cache.get(&"b"), Some("bob")); - cache.sync(); - // counts: a -> 1, b -> 1 - - cache.insert("c", "cindy"); - assert_eq!(cache.get(&"c"), Some("cindy")); - assert!(cache.contains_key(&"c")); - // counts: a -> 1, b -> 1, c -> 1 - cache.sync(); - - assert!(cache.contains_key(&"a")); - assert_eq!(cache.get(&"a"), Some("alice")); - assert_eq!(cache.get(&"b"), Some("bob")); - assert!(cache.contains_key(&"b")); - cache.sync(); - // counts: a -> 2, b -> 2, c -> 1 - - // "d" should not be admitted because its frequency is too low. - cache.insert("d", "david"); // count: d -> 0 - cache.sync(); - assert_eq!(cache.get(&"d"), None); // d -> 1 - assert!(!cache.contains_key(&"d")); - - cache.insert("d", "david"); - cache.sync(); - assert!(!cache.contains_key(&"d")); - assert_eq!(cache.get(&"d"), None); // d -> 2 - - // "d" should be admitted and "c" should be evicted - // because d's frequency is higher than c's. - cache.insert("d", "dennis"); - cache.sync(); - assert_eq!(cache.get(&"a"), Some("alice")); - assert_eq!(cache.get(&"b"), Some("bob")); - assert_eq!(cache.get(&"c"), None); - assert_eq!(cache.get(&"d"), Some("dennis")); - assert!(cache.contains_key(&"a")); - assert!(cache.contains_key(&"b")); - assert!(!cache.contains_key(&"c")); - assert!(cache.contains_key(&"d")); - - cache.invalidate(&"b"); - assert_eq!(cache.get(&"b"), None); - assert!(!cache.contains_key(&"b")); + run_test(DeliveryMode::Immediate); + run_test(DeliveryMode::Queued); + + fn run_test(delivery_mode: DeliveryMode) { + // The following `Vec`s will hold actual and expected notifications. + let actual = Arc::new(Mutex::new(Vec::new())); + let mut expected = Vec::new(); + + // Create an eviction listener. + let a1 = Arc::clone(&actual); + let listener = move |k, v, cause| a1.lock().push((k, v, cause)); + let listener_conf = notification::Configuration::builder() + .delivery_mode(delivery_mode) + .build(); + + // Create a cache with the eviction listener. + let mut cache = SegmentedCache::builder(1) + .max_capacity(3) + .eviction_listener_with_conf(listener, listener_conf) + .build(); + cache.reconfigure_for_testing(); + + // Make the cache exterior immutable. + let cache = cache; + + cache.insert("a", "alice"); + cache.insert("b", "bob"); + assert_eq_with_mode!(cache.get(&"a"), Some("alice"), delivery_mode); + assert_with_mode!(cache.contains_key(&"a"), delivery_mode); + assert_with_mode!(cache.contains_key(&"b"), delivery_mode); + assert_eq_with_mode!(cache.get(&"b"), Some("bob"), delivery_mode); + cache.sync(); + // counts: a -> 1, b -> 1 + + cache.insert("c", "cindy"); + assert_eq_with_mode!(cache.get(&"c"), Some("cindy"), delivery_mode); + assert_with_mode!(cache.contains_key(&"c"), delivery_mode); + // counts: a -> 1, b -> 1, c -> 1 + cache.sync(); + + assert_with_mode!(cache.contains_key(&"a"), delivery_mode); + assert_eq_with_mode!(cache.get(&"a"), Some("alice"), delivery_mode); + assert_eq_with_mode!(cache.get(&"b"), Some("bob"), delivery_mode); + assert_with_mode!(cache.contains_key(&"b"), delivery_mode); + cache.sync(); + // counts: a -> 2, b -> 2, c -> 1 + + // "d" should not be admitted because its frequency is too low. + cache.insert("d", "david"); // count: d -> 0 + expected.push((Arc::new("d"), "david", RemovalCause::Size)); + cache.sync(); + assert_eq_with_mode!(cache.get(&"d"), None, delivery_mode); // d -> 1 + assert_with_mode!(!cache.contains_key(&"d"), delivery_mode); + + cache.insert("d", "david"); + expected.push((Arc::new("d"), "david", RemovalCause::Size)); + cache.sync(); + assert_with_mode!(!cache.contains_key(&"d"), delivery_mode); + assert_eq_with_mode!(cache.get(&"d"), None, delivery_mode); // d -> 2 + + // "d" should be admitted and "c" should be evicted + // because d's frequency is higher than c's. + cache.insert("d", "dennis"); + expected.push((Arc::new("c"), "cindy", RemovalCause::Size)); + cache.sync(); + assert_eq_with_mode!(cache.get(&"a"), Some("alice"), delivery_mode); + assert_eq_with_mode!(cache.get(&"b"), Some("bob"), delivery_mode); + assert_eq_with_mode!(cache.get(&"c"), None, delivery_mode); + assert_eq_with_mode!(cache.get(&"d"), Some("dennis"), delivery_mode); + assert_with_mode!(cache.contains_key(&"a"), delivery_mode); + assert_with_mode!(cache.contains_key(&"b"), delivery_mode); + assert_with_mode!(!cache.contains_key(&"c"), delivery_mode); + assert_with_mode!(cache.contains_key(&"d"), delivery_mode); + + cache.invalidate(&"b"); + expected.push((Arc::new("b"), "bob", RemovalCause::Explicit)); + cache.sync(); + assert_eq_with_mode!(cache.get(&"b"), None, delivery_mode); + assert_with_mode!(!cache.contains_key(&"b"), delivery_mode); + + verify_notification_vec(&cache, actual, &expected, delivery_mode); + } } #[test] @@ -723,103 +777,132 @@ mod tests { #[test] fn size_aware_eviction() { - let weigher = |_k: &&str, v: &(&str, u32)| v.1; - - let alice = ("alice", 10); - let bob = ("bob", 15); - let bill = ("bill", 20); - let cindy = ("cindy", 5); - let david = ("david", 15); - let dennis = ("dennis", 15); - - let mut cache = SegmentedCache::builder(1) - .max_capacity(31) - .weigher(weigher) - .build(); - cache.reconfigure_for_testing(); - - // Make the cache exterior immutable. - let cache = cache; - - cache.insert("a", alice); - cache.insert("b", bob); - assert_eq!(cache.get(&"a"), Some(alice)); - assert!(cache.contains_key(&"a")); - assert!(cache.contains_key(&"b")); - assert_eq!(cache.get(&"b"), Some(bob)); - cache.sync(); - // order (LRU -> MRU) and counts: a -> 1, b -> 1 - - cache.insert("c", cindy); - assert_eq!(cache.get(&"c"), Some(cindy)); - assert!(cache.contains_key(&"c")); - // order and counts: a -> 1, b -> 1, c -> 1 - cache.sync(); - - assert!(cache.contains_key(&"a")); - assert_eq!(cache.get(&"a"), Some(alice)); - assert_eq!(cache.get(&"b"), Some(bob)); - assert!(cache.contains_key(&"b")); - cache.sync(); - // order and counts: c -> 1, a -> 2, b -> 2 - - // To enter "d" (weight: 15), it needs to evict "c" (w: 5) and "a" (w: 10). - // "d" must have higher count than 3, which is the aggregated count - // of "a" and "c". - cache.insert("d", david); // count: d -> 0 - cache.sync(); - assert_eq!(cache.get(&"d"), None); // d -> 1 - assert!(!cache.contains_key(&"d")); - - cache.insert("d", david); - cache.sync(); - assert!(!cache.contains_key(&"d")); - assert_eq!(cache.get(&"d"), None); // d -> 2 - - cache.insert("d", david); - cache.sync(); - assert_eq!(cache.get(&"d"), None); // d -> 3 - assert!(!cache.contains_key(&"d")); - - cache.insert("d", david); - cache.sync(); - assert!(!cache.contains_key(&"d")); - assert_eq!(cache.get(&"d"), None); // d -> 4 - - // Finally "d" should be admitted by evicting "c" and "a". - cache.insert("d", dennis); - cache.sync(); - assert_eq!(cache.get(&"a"), None); - assert_eq!(cache.get(&"b"), Some(bob)); - assert_eq!(cache.get(&"c"), None); - assert_eq!(cache.get(&"d"), Some(dennis)); - assert!(!cache.contains_key(&"a")); - assert!(cache.contains_key(&"b")); - assert!(!cache.contains_key(&"c")); - assert!(cache.contains_key(&"d")); - - // Update "b" with "bill" (w: 15 -> 20). This should evict "d" (w: 15). - cache.insert("b", bill); - cache.sync(); - assert_eq!(cache.get(&"b"), Some(bill)); - assert_eq!(cache.get(&"d"), None); - assert!(cache.contains_key(&"b")); - assert!(!cache.contains_key(&"d")); - - // Re-add "a" (w: 10) and update "b" with "bob" (w: 20 -> 15). - cache.insert("a", alice); - cache.insert("b", bob); - cache.sync(); - assert_eq!(cache.get(&"a"), Some(alice)); - assert_eq!(cache.get(&"b"), Some(bob)); - assert_eq!(cache.get(&"d"), None); - assert!(cache.contains_key(&"a")); - assert!(cache.contains_key(&"b")); - assert!(!cache.contains_key(&"d")); - - // Verify the sizes. - assert_eq!(cache.entry_count(), 2); - assert_eq!(cache.weighted_size(), 25); + run_test(DeliveryMode::Immediate); + run_test(DeliveryMode::Queued); + + fn run_test(delivery_mode: DeliveryMode) { + let weigher = |_k: &&str, v: &(&str, u32)| v.1; + + let alice = ("alice", 10); + let bob = ("bob", 15); + let bill = ("bill", 20); + let cindy = ("cindy", 5); + let david = ("david", 15); + let dennis = ("dennis", 15); + + // The following `Vec`s will hold actual and expected notifications. + let actual = Arc::new(Mutex::new(Vec::new())); + let mut expected = Vec::new(); + + // Create an eviction listener. + let a1 = Arc::clone(&actual); + let listener = move |k, v, cause| a1.lock().push((k, v, cause)); + let listener_conf = notification::Configuration::builder() + .delivery_mode(delivery_mode) + .build(); + + // Create a cache with the eviction listener. + let mut cache = SegmentedCache::builder(1) + .max_capacity(31) + .weigher(weigher) + .eviction_listener_with_conf(listener, listener_conf) + .build(); + cache.reconfigure_for_testing(); + + // Make the cache exterior immutable. + let cache = cache; + + cache.insert("a", alice); + cache.insert("b", bob); + assert_eq_with_mode!(cache.get(&"a"), Some(alice), delivery_mode); + assert_with_mode!(cache.contains_key(&"a"), delivery_mode); + assert_with_mode!(cache.contains_key(&"b"), delivery_mode); + assert_eq_with_mode!(cache.get(&"b"), Some(bob), delivery_mode); + cache.sync(); + // order (LRU -> MRU) and counts: a -> 1, b -> 1 + + cache.insert("c", cindy); + assert_eq_with_mode!(cache.get(&"c"), Some(cindy), delivery_mode); + assert_with_mode!(cache.contains_key(&"c"), delivery_mode); + // order and counts: a -> 1, b -> 1, c -> 1 + cache.sync(); + + assert_with_mode!(cache.contains_key(&"a"), delivery_mode); + assert_eq_with_mode!(cache.get(&"a"), Some(alice), delivery_mode); + assert_eq_with_mode!(cache.get(&"b"), Some(bob), delivery_mode); + assert_with_mode!(cache.contains_key(&"b"), delivery_mode); + cache.sync(); + // order and counts: c -> 1, a -> 2, b -> 2 + + // To enter "d" (weight: 15), it needs to evict "c" (w: 5) and "a" (w: 10). + // "d" must have higher count than 3, which is the aggregated count + // of "a" and "c". + cache.insert("d", david); // count: d -> 0 + expected.push((Arc::new("d"), david, RemovalCause::Size)); + cache.sync(); + assert_eq_with_mode!(cache.get(&"d"), None, delivery_mode); // d -> 1 + assert_with_mode!(!cache.contains_key(&"d"), delivery_mode); + + cache.insert("d", david); + expected.push((Arc::new("d"), david, RemovalCause::Size)); + cache.sync(); + assert_with_mode!(!cache.contains_key(&"d"), delivery_mode); + assert_eq_with_mode!(cache.get(&"d"), None, delivery_mode); // d -> 2 + + cache.insert("d", david); + expected.push((Arc::new("d"), david, RemovalCause::Size)); + cache.sync(); + assert_eq_with_mode!(cache.get(&"d"), None, delivery_mode); // d -> 3 + assert_with_mode!(!cache.contains_key(&"d"), delivery_mode); + + cache.insert("d", david); + expected.push((Arc::new("d"), david, RemovalCause::Size)); + cache.sync(); + assert_with_mode!(!cache.contains_key(&"d"), delivery_mode); + assert_eq_with_mode!(cache.get(&"d"), None, delivery_mode); // d -> 4 + + // Finally "d" should be admitted by evicting "c" and "a". + cache.insert("d", dennis); + expected.push((Arc::new("c"), cindy, RemovalCause::Size)); + expected.push((Arc::new("a"), alice, RemovalCause::Size)); + cache.sync(); + assert_eq_with_mode!(cache.get(&"a"), None, delivery_mode); + assert_eq_with_mode!(cache.get(&"b"), Some(bob), delivery_mode); + assert_eq_with_mode!(cache.get(&"c"), None, delivery_mode); + assert_eq_with_mode!(cache.get(&"d"), Some(dennis), delivery_mode); + assert_with_mode!(!cache.contains_key(&"a"), delivery_mode); + assert_with_mode!(cache.contains_key(&"b"), delivery_mode); + assert_with_mode!(!cache.contains_key(&"c"), delivery_mode); + assert_with_mode!(cache.contains_key(&"d"), delivery_mode); + + // Update "b" with "bill" (w: 15 -> 20). This should evict "d" (w: 15). + cache.insert("b", bill); + expected.push((Arc::new("b"), bob, RemovalCause::Replaced)); + expected.push((Arc::new("d"), dennis, RemovalCause::Size)); + cache.sync(); + assert_eq_with_mode!(cache.get(&"b"), Some(bill), delivery_mode); + assert_eq_with_mode!(cache.get(&"d"), None, delivery_mode); + assert_with_mode!(cache.contains_key(&"b"), delivery_mode); + assert_with_mode!(!cache.contains_key(&"d"), delivery_mode); + + // Re-add "a" (w: 10) and update "b" with "bob" (w: 20 -> 15). + cache.insert("a", alice); + cache.insert("b", bob); + expected.push((Arc::new("b"), bill, RemovalCause::Replaced)); + cache.sync(); + assert_eq_with_mode!(cache.get(&"a"), Some(alice), delivery_mode); + assert_eq_with_mode!(cache.get(&"b"), Some(bob), delivery_mode); + assert_eq_with_mode!(cache.get(&"d"), None, delivery_mode); + assert_with_mode!(cache.contains_key(&"a"), delivery_mode); + assert_with_mode!(cache.contains_key(&"b"), delivery_mode); + assert_with_mode!(!cache.contains_key(&"d"), delivery_mode); + + // Verify the sizes. + assert_eq_with_mode!(cache.entry_count(), 2, delivery_mode); + assert_eq_with_mode!(cache.weighted_size(), 25, delivery_mode); + + verify_notification_vec(&cache, actual, &expected, delivery_mode); + } } #[test] @@ -859,121 +942,188 @@ mod tests { #[test] fn invalidate_all() { - let mut cache = SegmentedCache::new(100, 4); - cache.reconfigure_for_testing(); - - // Make the cache exterior immutable. - let cache = cache; - - cache.insert("a", "alice"); - cache.insert("b", "bob"); - cache.insert("c", "cindy"); - assert_eq!(cache.get(&"a"), Some("alice")); - assert_eq!(cache.get(&"b"), Some("bob")); - assert_eq!(cache.get(&"c"), Some("cindy")); - assert!(cache.contains_key(&"a")); - assert!(cache.contains_key(&"b")); - assert!(cache.contains_key(&"c")); - - // `cache.sync()` is no longer needed here before invalidating. The last - // modified timestamp of the entries were updated when they were inserted. - // https://github.com/moka-rs/moka/issues/155 - - cache.invalidate_all(); - cache.sync(); - - cache.insert("d", "david"); - cache.sync(); - - assert!(cache.get(&"a").is_none()); - assert!(cache.get(&"b").is_none()); - assert!(cache.get(&"c").is_none()); - assert_eq!(cache.get(&"d"), Some("david")); - assert!(!cache.contains_key(&"a")); - assert!(!cache.contains_key(&"b")); - assert!(!cache.contains_key(&"c")); - assert!(cache.contains_key(&"d")); + run_test(DeliveryMode::Immediate); + run_test(DeliveryMode::Queued); + + fn run_test(delivery_mode: DeliveryMode) { + use std::collections::HashMap; + + // The following `HashMap`s will hold actual and expected notifications. + // Note: We use `HashMap` here as the order of invalidations is non-deterministic. + let actual = Arc::new(Mutex::new(HashMap::new())); + let mut expected = HashMap::new(); + + // Create an eviction listener. + let a1 = Arc::clone(&actual); + let listener = move |k, v, cause| { + a1.lock().insert(k, (v, cause)); + }; + let listener_conf = notification::Configuration::builder() + .delivery_mode(delivery_mode) + .build(); + + // Create a cache with the eviction listener. + let mut cache = SegmentedCache::builder(4) + .max_capacity(100) + .eviction_listener_with_conf(listener, listener_conf) + .build(); + cache.reconfigure_for_testing(); + + // Make the cache exterior immutable. + let cache = cache; + + cache.insert("a", "alice"); + cache.insert("b", "bob"); + cache.insert("c", "cindy"); + assert_eq_with_mode!(cache.get(&"a"), Some("alice"), delivery_mode); + assert_eq_with_mode!(cache.get(&"b"), Some("bob"), delivery_mode); + assert_eq_with_mode!(cache.get(&"c"), Some("cindy"), delivery_mode); + assert_with_mode!(cache.contains_key(&"a"), delivery_mode); + assert_with_mode!(cache.contains_key(&"b"), delivery_mode); + assert_with_mode!(cache.contains_key(&"c"), delivery_mode); + + // `cache.sync()` is no longer needed here before invalidating. The last + // modified timestamp of the entries were updated when they were inserted. + // https://github.com/moka-rs/moka/issues/155 + + cache.invalidate_all(); + expected.insert(Arc::new("a"), ("alice", RemovalCause::Explicit)); + expected.insert(Arc::new("b"), ("bob", RemovalCause::Explicit)); + expected.insert(Arc::new("c"), ("cindy", RemovalCause::Explicit)); + cache.sync(); + + cache.insert("d", "david"); + cache.sync(); + + assert_with_mode!(cache.get(&"a").is_none(), delivery_mode); + assert_with_mode!(cache.get(&"b").is_none(), delivery_mode); + assert_with_mode!(cache.get(&"c").is_none(), delivery_mode); + assert_eq_with_mode!(cache.get(&"d"), Some("david"), delivery_mode); + assert_with_mode!(!cache.contains_key(&"a"), delivery_mode); + assert_with_mode!(!cache.contains_key(&"b"), delivery_mode); + assert_with_mode!(!cache.contains_key(&"c"), delivery_mode); + assert_with_mode!(cache.contains_key(&"d"), delivery_mode); + + verify_notification_map(&cache, actual, &expected, delivery_mode); + } } #[test] fn invalidate_entries_if() -> Result<(), Box> { - use std::collections::HashSet; - - const SEGMENTS: usize = 4; - - let mut cache = SegmentedCache::builder(SEGMENTS) - .max_capacity(100) - .support_invalidation_closures() - .build(); - cache.reconfigure_for_testing(); - - let mut mock = cache.create_mock_expiration_clock(); - - // Make the cache exterior immutable. - let cache = cache; - - cache.insert(0, "alice"); - cache.insert(1, "bob"); - cache.insert(2, "alex"); - cache.sync(); - mock.increment(Duration::from_secs(5)); // 5 secs from the start. - cache.sync(); - - assert_eq!(cache.get(&0), Some("alice")); - assert_eq!(cache.get(&1), Some("bob")); - assert_eq!(cache.get(&2), Some("alex")); - assert!(cache.contains_key(&0)); - assert!(cache.contains_key(&1)); - assert!(cache.contains_key(&2)); - - let names = ["alice", "alex"].iter().cloned().collect::>(); - cache.invalidate_entries_if(move |_k, &v| names.contains(v))?; - assert_eq!(cache.invalidation_predicate_count(), SEGMENTS); - - mock.increment(Duration::from_secs(5)); // 10 secs from the start. - - cache.insert(3, "alice"); - - // Run the invalidation task and wait for it to finish. (TODO: Need a better way than sleeping) - cache.sync(); // To submit the invalidation task. - std::thread::sleep(Duration::from_millis(200)); - cache.sync(); // To process the task result. - std::thread::sleep(Duration::from_millis(200)); - - assert!(cache.get(&0).is_none()); - assert!(cache.get(&2).is_none()); - assert_eq!(cache.get(&1), Some("bob")); - // This should survive as it was inserted after calling invalidate_entries_if. - assert_eq!(cache.get(&3), Some("alice")); - - assert!(!cache.contains_key(&0)); - assert!(cache.contains_key(&1)); - assert!(!cache.contains_key(&2)); - assert!(cache.contains_key(&3)); - - assert_eq!(cache.entry_count(), 2); - assert_eq!(cache.invalidation_predicate_count(), 0); - - mock.increment(Duration::from_secs(5)); // 15 secs from the start. - - cache.invalidate_entries_if(|_k, &v| v == "alice")?; - cache.invalidate_entries_if(|_k, &v| v == "bob")?; - assert_eq!(cache.invalidation_predicate_count(), SEGMENTS * 2); - - // Run the invalidation task and wait for it to finish. (TODO: Need a better way than sleeping) - cache.sync(); // To submit the invalidation task. - std::thread::sleep(Duration::from_millis(200)); - cache.sync(); // To process the task result. - std::thread::sleep(Duration::from_millis(200)); - - assert!(cache.get(&1).is_none()); - assert!(cache.get(&3).is_none()); - - assert!(!cache.contains_key(&1)); - assert!(!cache.contains_key(&3)); - - assert_eq!(cache.entry_count(), 0); - assert_eq!(cache.invalidation_predicate_count(), 0); + run_test(DeliveryMode::Immediate)?; + run_test(DeliveryMode::Queued)?; + + fn run_test(delivery_mode: DeliveryMode) -> Result<(), Box> { + use std::collections::{HashMap, HashSet}; + + const SEGMENTS: usize = 4; + + // The following `HashMap`s will hold actual and expected notifications. + // Note: We use `HashMap` here as the order of invalidations is non-deterministic. + let actual = Arc::new(Mutex::new(HashMap::new())); + let mut expected = HashMap::new(); + + // Create an eviction listener. + let a1 = Arc::clone(&actual); + let listener = move |k, v, cause| { + a1.lock().insert(k, (v, cause)); + }; + let listener_conf = notification::Configuration::builder() + .delivery_mode(delivery_mode) + .build(); + + // Create a cache with the eviction listener. + let mut cache = SegmentedCache::builder(SEGMENTS) + .max_capacity(100) + .support_invalidation_closures() + .eviction_listener_with_conf(listener, listener_conf) + .build(); + cache.reconfigure_for_testing(); + + let mut mock = cache.create_mock_expiration_clock(); + + // Make the cache exterior immutable. + let cache = cache; + + cache.insert(0, "alice"); + cache.insert(1, "bob"); + cache.insert(2, "alex"); + cache.sync(); + mock.increment(Duration::from_secs(5)); // 5 secs from the start. + cache.sync(); + + assert_eq_with_mode!(cache.get(&0), Some("alice"), delivery_mode); + assert_eq_with_mode!(cache.get(&1), Some("bob"), delivery_mode); + assert_eq_with_mode!(cache.get(&2), Some("alex"), delivery_mode); + assert_with_mode!(cache.contains_key(&0), delivery_mode); + assert_with_mode!(cache.contains_key(&1), delivery_mode); + assert_with_mode!(cache.contains_key(&2), delivery_mode); + + let names = ["alice", "alex"].iter().cloned().collect::>(); + cache.invalidate_entries_if(move |_k, &v| names.contains(v))?; + assert_eq_with_mode!( + cache.invalidation_predicate_count(), + SEGMENTS, + delivery_mode + ); + expected.insert(Arc::new(0), ("alice", RemovalCause::Explicit)); + expected.insert(Arc::new(2), ("alex", RemovalCause::Explicit)); + + mock.increment(Duration::from_secs(5)); // 10 secs from the start. + + cache.insert(3, "alice"); + + // Run the invalidation task and wait for it to finish. (TODO: Need a better way than sleeping) + cache.sync(); // To submit the invalidation task. + std::thread::sleep(Duration::from_millis(200)); + cache.sync(); // To process the task result. + std::thread::sleep(Duration::from_millis(200)); + + assert_with_mode!(cache.get(&0).is_none(), delivery_mode); + assert_with_mode!(cache.get(&2).is_none(), delivery_mode); + assert_eq_with_mode!(cache.get(&1), Some("bob"), delivery_mode); + // This should survive as it was inserted after calling invalidate_entries_if. + assert_eq_with_mode!(cache.get(&3), Some("alice"), delivery_mode); + + assert_with_mode!(!cache.contains_key(&0), delivery_mode); + assert_with_mode!(cache.contains_key(&1), delivery_mode); + assert_with_mode!(!cache.contains_key(&2), delivery_mode); + assert_with_mode!(cache.contains_key(&3), delivery_mode); + + assert_eq_with_mode!(cache.entry_count(), 2, delivery_mode); + assert_eq_with_mode!(cache.invalidation_predicate_count(), 0, delivery_mode); + + mock.increment(Duration::from_secs(5)); // 15 secs from the start. + + cache.invalidate_entries_if(|_k, &v| v == "alice")?; + cache.invalidate_entries_if(|_k, &v| v == "bob")?; + assert_eq_with_mode!( + cache.invalidation_predicate_count(), + SEGMENTS * 2, + delivery_mode + ); + expected.insert(Arc::new(1), ("bob", RemovalCause::Explicit)); + expected.insert(Arc::new(3), ("alice", RemovalCause::Explicit)); + + // Run the invalidation task and wait for it to finish. (TODO: Need a better way than sleeping) + cache.sync(); // To submit the invalidation task. + std::thread::sleep(Duration::from_millis(200)); + cache.sync(); // To process the task result. + std::thread::sleep(Duration::from_millis(200)); + + assert_with_mode!(cache.get(&1).is_none(), delivery_mode); + assert_with_mode!(cache.get(&3).is_none(), delivery_mode); + + assert_with_mode!(!cache.contains_key(&1), delivery_mode); + assert_with_mode!(!cache.contains_key(&3), delivery_mode); + + assert_eq_with_mode!(cache.entry_count(), 0, delivery_mode); + assert_eq_with_mode!(cache.invalidation_predicate_count(), 0, delivery_mode); + + verify_notification_map(&cache, actual, &expected, delivery_mode); + + Ok(()) + } Ok(()) } @@ -1444,4 +1594,99 @@ mod tests { assert!(debug_str.contains(r#"'c': "cindy""#)); assert!(debug_str.ends_with('}')); } + + type NotificationPair = (V, RemovalCause); + type NotificationTriple = (Arc, V, RemovalCause); + + fn verify_notification_vec( + cache: &SegmentedCache, + actual: Arc>>>, + expected: &[NotificationTriple], + delivery_mode: DeliveryMode, + ) where + K: std::hash::Hash + Eq + std::fmt::Debug + Send + Sync + 'static, + V: Eq + std::fmt::Debug + Clone + Send + Sync + 'static, + S: std::hash::BuildHasher + Clone + Send + Sync + 'static, + { + // Retries will be needed when testing in a QEMU VM. + const MAX_RETRIES: usize = 5; + let mut retries = 0; + loop { + // Ensure all scheduled notifications have been processed. + std::thread::sleep(Duration::from_millis(500)); + + let actual = &*actual.lock(); + if actual.len() != expected.len() { + if retries <= MAX_RETRIES { + retries += 1; + cache.sync(); + continue; + } else { + assert_eq!( + actual.len(), + expected.len(), + "Retries exhausted (delivery mode: {:?})", + delivery_mode + ); + } + } + + for (i, (actual, expected)) in actual.iter().zip(expected).enumerate() { + assert_eq!( + actual, expected, + "expected[{}] (delivery mode: {:?})", + i, delivery_mode + ); + } + + break; + } + } + + fn verify_notification_map( + cache: &SegmentedCache, + actual: Arc, NotificationPair>>>, + expected: &std::collections::HashMap, NotificationPair>, + delivery_mode: DeliveryMode, + ) where + K: std::hash::Hash + Eq + std::fmt::Display + Send + Sync + 'static, + V: Eq + std::fmt::Debug + Clone + Send + Sync + 'static, + S: std::hash::BuildHasher + Clone + Send + Sync + 'static, + { + // Retries will be needed when testing in a QEMU VM. + const MAX_RETRIES: usize = 5; + let mut retries = 0; + loop { + // Ensure all scheduled notifications have been processed. + std::thread::sleep(Duration::from_millis(500)); + + let actual = &*actual.lock(); + if actual.len() != expected.len() { + if retries <= MAX_RETRIES { + retries += 1; + cache.sync(); + continue; + } else { + assert_eq!( + actual.len(), + expected.len(), + "Retries exhausted (delivery mode: {:?})", + delivery_mode + ); + } + } + + for actual_key in actual.keys() { + assert_eq!( + actual.get(actual_key), + expected.get(actual_key), + "expected[{}] (delivery mode: {:?})", + actual_key, + delivery_mode + ); + } + + break; + } + } } diff --git a/src/sync_base.rs b/src/sync_base.rs index 24c022a3..8ca085f9 100644 --- a/src/sync_base.rs +++ b/src/sync_base.rs @@ -1,9 +1,10 @@ pub(crate) mod base_cache; mod invalidator; pub(crate) mod iter; +mod key_lock; /// The type of the unique ID to identify a predicate used by -/// [`Cache#invalidate_entries_if`][invalidate-if] method. +/// [`Cache::invalidate_entries_if`][invalidate-if] method. /// /// A `PredicateId` is a `String` of UUID (version 4). /// diff --git a/src/sync_base/base_cache.rs b/src/sync_base/base_cache.rs index 68c93228..b659bde4 100644 --- a/src/sync_base/base_cache.rs +++ b/src/sync_base/base_cache.rs @@ -1,6 +1,7 @@ use super::{ invalidator::{GetOrRemoveEntry, InvalidationResult, Invalidator, KeyDateLite, PredicateFun}, iter::ScanningGet, + key_lock::{KeyLock, KeyLockMap}, PredicateId, }; @@ -24,6 +25,11 @@ use crate::{ time::{CheckedTimeOps, Clock, Instant}, CacheRegion, }, + notification::{ + self, + notifier::{RemovalNotifier, RemovedEntry}, + EvictionListener, RemovalCause, + }, Policy, PredicateError, }; @@ -80,6 +86,10 @@ impl Drop for BaseCache { } impl BaseCache { + pub(crate) fn name(&self) -> Option<&str> { + self.inner.name() + } + pub(crate) fn policy(&self) -> Policy { self.inner.policy() } @@ -92,23 +102,57 @@ impl BaseCache { self.inner.weighted_size() } + #[inline] + pub(crate) fn is_removal_notifier_enabled(&self) -> bool { + self.inner.is_removal_notifier_enabled() + } + + #[inline] + #[cfg(feature = "sync")] + pub(crate) fn is_blocking_removal_notification(&self) -> bool { + self.inner.is_blocking_removal_notification() + } + + pub(crate) fn notify_invalidate(&self, key: &Arc, entry: &TrioArc>) + where + K: Send + Sync + 'static, + V: Clone + Send + Sync + 'static, + { + self.inner.notify_invalidate(key, entry); + } + #[cfg(feature = "unstable-debug-counters")] pub fn debug_stats(&self) -> CacheDebugStats { self.inner.debug_stats() } } +impl BaseCache +where + K: Hash + Eq, + S: BuildHasher, +{ + pub(crate) fn maybe_key_lock(&self, key: &Arc) -> Option> { + self.inner.maybe_key_lock(key) + } +} + impl BaseCache where K: Hash + Eq + Send + Sync + 'static, V: Clone + Send + Sync + 'static, S: BuildHasher + Clone + Send + Sync + 'static, { + // https://rust-lang.github.io/rust-clippy/master/index.html#too_many_arguments + #[allow(clippy::too_many_arguments)] pub(crate) fn new( + name: Option, max_capacity: Option, initial_capacity: Option, build_hasher: S, weigher: Option>, + eviction_listener: Option>, + eviction_listener_conf: Option, time_to_live: Option, time_to_idle: Option, invalidator_enabled: bool, @@ -116,10 +160,13 @@ where let (r_snd, r_rcv) = crossbeam_channel::bounded(READ_LOG_SIZE); let (w_snd, w_rcv) = crossbeam_channel::bounded(WRITE_LOG_SIZE); let inner = Arc::new(Inner::new( + name, max_capacity, initial_capacity, build_hasher, weigher, + eviction_listener, + eviction_listener_conf, r_rcv, w_rcv, time_to_live, @@ -202,6 +249,16 @@ where } } + #[cfg(feature = "sync")] + pub(crate) fn get_key_with_hash(&self, key: &Q, hash: u64) -> Option> + where + Arc: Borrow, + Q: Hash + Eq + ?Sized, + { + self.inner + .get_key_value_and(key, hash, |k, _entry| Arc::clone(k)) + } + #[inline] pub(crate) fn remove_entry(&self, key: &Q, hash: u64) -> Option> where @@ -306,6 +363,10 @@ where let mut op1 = None; let mut op2 = None; + // Lock the key for update if blocking removal notification is enabled. + let kl = self.maybe_key_lock(&key); + let _klg = &kl.as_ref().map(|kl| kl.lock()); + // Since the cache (cht::SegmentedHashMap) employs optimistic locking // strategy, insert_with_or_modify() may get an insert/modify operation // conflicted with other concurrent hash table operations. In that case, it @@ -340,11 +401,13 @@ where // prevent this new ValueEntry from being evicted by an expiration policy. // 3. This method will update the policy_weight with the new weight. let old_weight = old_entry.policy_weight(); + let old_timestamps = (old_entry.last_accessed(), old_entry.last_modified()); let entry = self.new_value_entry_from(value.clone(), ts, weight, old_entry); let cnt = op_cnt2.fetch_add(1, Ordering::Relaxed); op2 = Some(( cnt, TrioArc::clone(old_entry), + old_timestamps, WriteOp::Upsert { key_hash: KeyHash::new(Arc::clone(&key), hash), value_entry: TrioArc::clone(&entry), @@ -358,15 +421,30 @@ where match (op1, op2) { (Some((_cnt, ins_op)), None) => ins_op, - (None, Some((_cnt, old_entry, upd_op))) => { + (None, Some((_cnt, old_entry, (old_last_accessed, old_last_modified), upd_op))) => { old_entry.unset_q_nodes(); + if self.is_removal_notifier_enabled() { + self.inner + .notify_upsert(key, &old_entry, old_last_accessed, old_last_modified); + } upd_op } - (Some((cnt1, ins_op)), Some((cnt2, old_entry, upd_op))) => { + ( + Some((cnt1, ins_op)), + Some((cnt2, old_entry, (old_last_accessed, old_last_modified), upd_op)), + ) => { if cnt1 > cnt2 { ins_op } else { old_entry.unset_q_nodes(); + if self.is_removal_notifier_enabled() { + self.inner.notify_upsert( + key, + &old_entry, + old_last_accessed, + old_last_modified, + ); + } upd_op } } @@ -457,6 +535,67 @@ where } } +struct EvictionState<'a, K, V> { + counters: EvictionCounters, + notifier: Option<&'a RemovalNotifier>, + removed_entries: Option>>, +} + +impl<'a, K, V> EvictionState<'a, K, V> { + fn new( + entry_count: u64, + weighted_size: u64, + notifier: Option<&'a RemovalNotifier>, + ) -> Self { + let removed_entries = notifier.and_then(|n| { + if n.is_batching_supported() { + Some(Vec::new()) + } else { + None + } + }); + + Self { + counters: EvictionCounters::new(entry_count, weighted_size), + notifier, + removed_entries, + } + } + + fn is_notifier_enabled(&self) -> bool { + self.notifier.is_some() + } + + fn add_removed_entry( + &mut self, + key: Arc, + entry: &TrioArc>, + cause: RemovalCause, + ) where + K: Send + Sync + 'static, + V: Clone + Send + Sync + 'static, + { + debug_assert!(self.is_notifier_enabled()); + + if let Some(removed) = &mut self.removed_entries { + removed.push(RemovedEntry::new(key, entry.value.clone(), cause)); + } else if let Some(notifier) = self.notifier { + notifier.notify(key, entry.value.clone(), cause); + } + } + + fn notify_multiple_removals(&mut self) + where + K: Send + Sync + 'static, + V: Send + Sync + 'static, + { + if let (Some(notifier), Some(removed)) = (self.notifier, self.removed_entries.take()) { + notifier.batch_notify(removed); + notifier.sync(); + } + } +} + struct EvictionCounters { entry_count: u64, weighted_size: u64, @@ -525,6 +664,7 @@ enum AdmissionResult { type CacheStore = crate::cht::SegmentedHashMap, TrioArc>, S>; pub(crate) struct Inner { + name: Option, max_capacity: Option, entry_count: AtomicCell, weighted_size: AtomicCell, @@ -539,6 +679,8 @@ pub(crate) struct Inner { time_to_idle: Option, valid_after: AtomicInstant, weigher: Option>, + removal_notifier: Option>, + key_locks: Option>, invalidator_enabled: bool, invalidator: RwLock>>, has_expiration_clock: AtomicBool, @@ -547,6 +689,10 @@ pub(crate) struct Inner { // functions/methods used by BaseCache impl Inner { + fn name(&self) -> Option<&str> { + self.name.as_deref() + } + fn policy(&self) -> Policy { Policy::new(self.max_capacity, 1, self.time_to_live, self.time_to_idle) } @@ -557,10 +703,24 @@ impl Inner { } #[inline] - pub(crate) fn weighted_size(&self) -> u64 { + fn weighted_size(&self) -> u64 { self.weighted_size.load() } + #[inline] + fn is_removal_notifier_enabled(&self) -> bool { + self.removal_notifier.is_some() + } + + #[inline] + #[cfg(feature = "sync")] + fn is_blocking_removal_notification(&self) -> bool { + self.removal_notifier + .as_ref() + .map(|rn| rn.is_blocking()) + .unwrap_or_default() + } + #[cfg(feature = "unstable-debug-counters")] pub fn debug_stats(&self) -> CacheDebugStats { let ec = self.entry_count.load(); @@ -573,6 +733,71 @@ impl Inner { self.frequency_sketch.read().table_size(), ) } + + #[inline] + fn current_time_from_expiration_clock(&self) -> Instant { + if self.has_expiration_clock.load(Ordering::Relaxed) { + Instant::new( + self.expiration_clock + .read() + .as_ref() + .expect("Cannot get the expiration clock") + .now(), + ) + } else { + Instant::now() + } + } + + fn num_cht_segments(&self) -> usize { + self.cache.actual_num_segments() + } + + #[inline] + fn time_to_live(&self) -> Option { + self.time_to_live + } + + #[inline] + fn time_to_idle(&self) -> Option { + self.time_to_idle + } + + #[inline] + fn has_expiry(&self) -> bool { + self.time_to_live.is_some() || self.time_to_idle.is_some() + } + + #[inline] + fn is_write_order_queue_enabled(&self) -> bool { + self.time_to_live.is_some() || self.invalidator_enabled + } + + #[inline] + fn valid_after(&self) -> Option { + self.valid_after.instant() + } + + #[inline] + fn set_valid_after(&self, timestamp: Instant) { + self.valid_after.set_instant(timestamp); + } + + #[inline] + fn has_valid_after(&self) -> bool { + self.valid_after.is_set() + } +} + +// functions/methods used by BaseCache +impl Inner +where + K: Hash + Eq, + S: BuildHasher, +{ + fn maybe_key_lock(&self, key: &Arc) -> Option> { + self.key_locks.as_ref().map(|kls| kls.key_lock(key)) + } } // functions/methods used by BaseCache @@ -586,10 +811,13 @@ where // https://rust-lang.github.io/rust-clippy/master/index.html#too_many_arguments #[allow(clippy::too_many_arguments)] fn new( + name: Option, max_capacity: Option, initial_capacity: Option, build_hasher: S, weigher: Option>, + eviction_listener: Option>, + eviction_listener_conf: Option, read_op_ch: Receiver>, write_op_ch: Receiver>, time_to_live: Option, @@ -605,8 +833,24 @@ where initial_capacity, build_hasher.clone(), ); + let (removal_notifier, key_locks) = if let Some(listener) = eviction_listener { + let rn = RemovalNotifier::new( + listener, + eviction_listener_conf.unwrap_or_default(), + name.clone(), + ); + if rn.is_blocking() { + let kl = KeyLockMap::with_hasher(build_hasher.clone()); + (Some(rn), Some(kl)) + } else { + (Some(rn), None) + } + } else { + (None, None) + }; Self { + name, max_capacity: max_capacity.map(|n| n as u64), entry_count: Default::default(), weighted_size: Default::default(), @@ -621,6 +865,8 @@ where time_to_idle, valid_after: Default::default(), weigher, + removal_notifier, + key_locks, invalidator_enabled, // When enabled, this field will be set later via the set_invalidator method. invalidator: RwLock::new(None), @@ -684,45 +930,6 @@ where self.cache.keys(cht_segment, Arc::clone) } - fn num_cht_segments(&self) -> usize { - self.cache.actual_num_segments() - } - - #[inline] - fn time_to_live(&self) -> Option { - self.time_to_live - } - - #[inline] - fn time_to_idle(&self) -> Option { - self.time_to_idle - } - - #[inline] - fn has_expiry(&self) -> bool { - self.time_to_live.is_some() || self.time_to_idle.is_some() - } - - #[inline] - fn is_write_order_queue_enabled(&self) -> bool { - self.time_to_live.is_some() || self.invalidator_enabled - } - - #[inline] - fn valid_after(&self) -> Option { - self.valid_after.instant() - } - - #[inline] - fn set_valid_after(&self, timestamp: Instant) { - self.valid_after.set_instant(timestamp); - } - - #[inline] - fn has_valid_after(&self) -> bool { - self.valid_after.is_set() - } - #[inline] fn register_invalidation_predicate( &self, @@ -750,21 +957,6 @@ where fn weigh(&self, key: &K, value: &V) -> u32 { self.weigher.as_ref().map(|w| w(key, value)).unwrap_or(1) } - - #[inline] - fn current_time_from_expiration_clock(&self) -> Instant { - if self.has_expiration_clock.load(Ordering::Relaxed) { - Instant::new( - self.expiration_clock - .read() - .as_ref() - .expect("Cannot get the expiration clock") - .now(), - ) - } else { - Instant::now() - } - } } impl GetOrRemoveEntry for Arc> @@ -783,9 +975,21 @@ where condition: F, ) -> Option>> where + K: Send + Sync + 'static, + V: Clone + Send + Sync + 'static, F: FnMut(&Arc, &TrioArc>) -> bool, { - self.cache.remove_if(key, hash, condition) + // Lock the key for removal if blocking removal notification is enabled. + let kl = self.maybe_key_lock(key); + let _klg = &kl.as_ref().map(|kl| kl.lock()); + + let maybe_entry = self.cache.remove_if(key, hash, condition); + if let Some(entry) = &maybe_entry { + if self.is_removal_notifier_enabled() { + self.notify_single_removal(Arc::clone(key), entry, RemovalCause::Explicit); + } + } + maybe_entry } } @@ -810,7 +1014,7 @@ mod batch_size { impl InnerSync for Inner where K: Hash + Eq + Send + Sync + 'static, - V: Send + Sync + 'static, + V: Clone + Send + Sync + 'static, S: BuildHasher + Clone + Send + Sync + 'static, { fn sync(&self, max_repeats: usize) -> Option { @@ -820,7 +1024,8 @@ where let current_ec = self.entry_count.load(); let current_ws = self.weighted_size.load(); - let mut counters = EvictionCounters::new(current_ec, current_ws); + let mut eviction_state = + EvictionState::new(current_ec, current_ws, self.removal_notifier.as_ref()); while should_sync && calls <= max_repeats { let r_len = self.read_op_ch.len(); @@ -830,11 +1035,11 @@ where let w_len = self.write_op_ch.len(); if w_len > 0 { - self.apply_writes(&mut deqs, w_len, &mut counters); + self.apply_writes(&mut deqs, w_len, &mut eviction_state); } - if self.should_enable_frequency_sketch(&counters) { - self.enable_frequency_sketch(&counters); + if self.should_enable_frequency_sketch(&eviction_state.counters) { + self.enable_frequency_sketch(&eviction_state.counters); } calls += 1; @@ -843,7 +1048,11 @@ where } if self.has_expiry() || self.has_valid_after() { - self.evict_expired(&mut deqs, batch_size::EVICTION_BATCH_SIZE, &mut counters); + self.evict_expired( + &mut deqs, + batch_size::EVICTION_BATCH_SIZE, + &mut eviction_state, + ); } if self.invalidator_enabled { @@ -853,27 +1062,30 @@ where invalidator, &mut deqs, batch_size::INVALIDATION_BATCH_SIZE, - &mut counters, + &mut eviction_state, ); } } } // Evict if this cache has more entries than its capacity. - let weights_to_evict = self.weights_to_evict(&counters); + let weights_to_evict = self.weights_to_evict(&eviction_state.counters); if weights_to_evict > 0 { self.evict_lru_entries( &mut deqs, batch_size::EVICTION_BATCH_SIZE, weights_to_evict, - &mut counters, + &mut eviction_state, ); } + eviction_state.notify_multiple_removals(); + debug_assert_eq!(self.entry_count.load(), current_ec); debug_assert_eq!(self.weighted_size.load(), current_ws); - self.entry_count.store(counters.entry_count); - self.weighted_size.store(counters.weighted_size); + self.entry_count.store(eviction_state.counters.entry_count); + self.weighted_size + .store(eviction_state.counters.weighted_size); if should_sync { Some(SyncPace::Fast) @@ -962,7 +1174,14 @@ where } } - fn apply_writes(&self, deqs: &mut Deques, count: usize, counters: &mut EvictionCounters) { + fn apply_writes( + &self, + deqs: &mut Deques, + count: usize, + eviction_state: &mut EvictionState<'_, K, V>, + ) where + V: Clone, + { use WriteOp::*; let freq = self.frequency_sketch.read(); let ch = &self.write_op_ch; @@ -974,9 +1193,17 @@ where value_entry: entry, old_weight, new_weight, - }) => self.handle_upsert(kh, entry, old_weight, new_weight, deqs, &freq, counters), + }) => self.handle_upsert( + kh, + entry, + old_weight, + new_weight, + deqs, + &freq, + eviction_state, + ), Ok(Remove(KvEntry { key: _key, entry })) => { - Self::handle_remove(deqs, entry, counters) + Self::handle_remove(deqs, entry, &mut eviction_state.counters) } Err(_) => break, }; @@ -992,30 +1219,47 @@ where new_weight: u32, deqs: &mut Deques, freq: &FrequencySketch, - counters: &mut EvictionCounters, - ) { + eviction_state: &mut EvictionState<'_, K, V>, + ) where + V: Clone, + { entry.set_dirty(false); - if entry.is_admitted() { - // The entry has been already admitted, so treat this as an update. - counters.saturating_sub(0, old_weight); - counters.saturating_add(0, new_weight); - deqs.move_to_back_ao(&entry); - deqs.move_to_back_wo(&entry); - return; - } + { + let counters = &mut eviction_state.counters; + + if entry.is_admitted() { + // The entry has been already admitted, so treat this as an update. + counters.saturating_sub(0, old_weight); + counters.saturating_add(0, new_weight); + deqs.move_to_back_ao(&entry); + deqs.move_to_back_wo(&entry); + return; + } - if self.has_enough_capacity(new_weight, counters) { - // There are enough room in the cache (or the cache is unbounded). - // Add the candidate to the deques. - self.handle_admit(kh, &entry, new_weight, deqs, counters); - return; + if self.has_enough_capacity(new_weight, counters) { + // There are enough room in the cache (or the cache is unbounded). + // Add the candidate to the deques. + self.handle_admit(kh, &entry, new_weight, deqs, counters); + return; + } } if let Some(max) = self.max_capacity { if new_weight as u64 > max { // The candidate is too big to fit in the cache. Reject it. - self.cache.remove(&Arc::clone(&kh.key), kh.hash); + + // Lock the key for removal if blocking removal notification is enabled. + let kl = self.maybe_key_lock(&kh.key); + let _klg = &kl.as_ref().map(|kl| kl.lock()); + + let removed = self.cache.remove(&Arc::clone(&kh.key), kh.hash); + if let Some(entry) = removed { + if eviction_state.is_notifier_enabled() { + let key = Arc::clone(&kh.key); + eviction_state.add_removed_entry(key, &entry, RemovalCause::Size); + } + } return; } } @@ -1033,11 +1277,23 @@ where // Try to remove the victims from the cache (hash map). for victim in victim_nodes { let element = unsafe { &victim.as_ref().element }; - if let Some((_vic_key, vic_entry)) = + + // Lock the key for removal if blocking removal notification is enabled. + let kl = self.maybe_key_lock(element.key()); + let _klg = &kl.as_ref().map(|kl| kl.lock()); + + if let Some((vic_key, vic_entry)) = self.cache.remove_entry(element.key(), element.hash()) { + if eviction_state.is_notifier_enabled() { + eviction_state.add_removed_entry( + vic_key, + &vic_entry, + RemovalCause::Size, + ); + } // And then remove the victim from the deques. - Self::handle_remove(deqs, vic_entry, counters); + Self::handle_remove(deqs, vic_entry, &mut eviction_state.counters); } else { // Could not remove the victim from the cache. Skip this // victim node as its ValueEntry might have been @@ -1048,12 +1304,21 @@ where skipped_nodes = skipped; // Add the candidate to the deques. - self.handle_admit(kh, &entry, new_weight, deqs, counters); + self.handle_admit(kh, &entry, new_weight, deqs, &mut eviction_state.counters); } AdmissionResult::Rejected { skipped_nodes: s } => { skipped_nodes = s; + + // Lock the key for removal if blocking removal notification is enabled. + let kl = self.maybe_key_lock(&kh.key); + let _klg = &kl.as_ref().map(|kl| kl.lock()); + // Remove the candidate from the cache (hash map). - self.cache.remove(&Arc::clone(&kh.key), kh.hash); + let key = Arc::clone(&kh.key); + self.cache.remove(&key, kh.hash); + if eviction_state.is_notifier_enabled() { + eviction_state.add_removed_entry(key, &entry, RemovalCause::Size); + } } }; @@ -1202,12 +1467,14 @@ where &self, deqs: &mut Deques, batch_size: usize, - counters: &mut EvictionCounters, - ) { + eviction_state: &mut EvictionState<'_, K, V>, + ) where + V: Clone, + { let now = self.current_time_from_expiration_clock(); if self.is_write_order_queue_enabled() { - self.remove_expired_wo(deqs, batch_size, now, counters); + self.remove_expired_wo(deqs, batch_size, now, eviction_state); } if self.time_to_idle.is_some() || self.has_valid_after() { @@ -1219,7 +1486,7 @@ where ); let mut rm_expired_ao = - |name, deq| self.remove_expired_ao(name, deq, wo, batch_size, now, counters); + |name, deq| self.remove_expired_ao(name, deq, wo, batch_size, now, eviction_state); rm_expired_ao("window", window); rm_expired_ao("probation", probation); @@ -1235,37 +1502,64 @@ where write_order_deq: &mut Deque>, batch_size: usize, now: Instant, - counters: &mut EvictionCounters, - ) { + eviction_state: &mut EvictionState<'_, K, V>, + ) where + V: Clone, + { let tti = &self.time_to_idle; let va = &self.valid_after(); for _ in 0..batch_size { // Peek the front node of the deque and check if it is expired. - let key_hash = deq.peek_front().and_then(|node| { + let key_hash_cause = deq.peek_front().and_then(|node| { // TODO: Skip the entry if it is dirty. See `evict_lru_entries` method as an example. - if is_expired_entry_ao(tti, va, node, now) { - Some((Arc::clone(node.element.key()), node.element.hash())) - } else { - None + match is_entry_expired_ao_or_invalid(tti, va, node, now) { + (true, _) => Some(( + Arc::clone(node.element.key()), + node.element.hash(), + RemovalCause::Expired, + )), + (false, true) => Some(( + Arc::clone(node.element.key()), + node.element.hash(), + RemovalCause::Explicit, + )), + (false, false) => None, } }); - if key_hash.is_none() { + if key_hash_cause.is_none() { break; } - let (key, hash) = key_hash.as_ref().map(|(k, h)| (k, *h)).unwrap(); + let (key, hash, cause) = key_hash_cause + .as_ref() + .map(|(k, h, c)| (k, *h, *c)) + .unwrap(); + + // Lock the key for removal if blocking removal notification is enabled. + let kl = self.maybe_key_lock(key); + let _klg = &kl.as_ref().map(|kl| kl.lock()); // Remove the key from the map only when the entry is really // expired. This check is needed because it is possible that the entry in // the map has been updated or deleted but its deque node we checked - // above have not been updated yet. + // above has not been updated yet. let maybe_entry = self .cache .remove_if(key, hash, |_, v| is_expired_entry_ao(tti, va, v, now)); if let Some(entry) = maybe_entry { - Self::handle_remove_with_deques(deq_name, deq, write_order_deq, entry, counters); + if eviction_state.is_notifier_enabled() { + let key = Arc::clone(key); + eviction_state.add_removed_entry(key, &entry, cause); + } + Self::handle_remove_with_deques( + deq_name, + deq, + write_order_deq, + entry, + &mut eviction_state.counters, + ); } else if !self.try_skip_updated_entry(key, hash, deq_name, deq, write_order_deq) { break; } @@ -1310,33 +1604,43 @@ where deqs: &mut Deques, batch_size: usize, now: Instant, - counters: &mut EvictionCounters, - ) { + eviction_state: &mut EvictionState<'_, K, V>, + ) where + V: Clone, + { let ttl = &self.time_to_live; let va = &self.valid_after(); for _ in 0..batch_size { - let key = deqs.write_order.peek_front().and_then(|node| { + let key_cause = deqs.write_order.peek_front().and_then( // TODO: Skip the entry if it is dirty. See `evict_lru_entries` method as an example. - if is_expired_entry_wo(ttl, va, node, now) { - Some(Arc::clone(node.element.key())) - } else { - None - } - }); + |node| match is_entry_expired_wo_or_invalid(ttl, va, node, now) { + (true, _) => Some((Arc::clone(node.element.key()), RemovalCause::Expired)), + (false, true) => Some((Arc::clone(node.element.key()), RemovalCause::Explicit)), + (false, false) => None, + }, + ); - if key.is_none() { + if key_cause.is_none() { break; } - let key = key.as_ref().unwrap(); + let (key, cause) = key_cause.as_ref().unwrap(); let hash = self.hash(key); + // Lock the key for removal if blocking removal notification is enabled. + let kl = self.maybe_key_lock(key); + let _klg = &kl.as_ref().map(|kl| kl.lock()); + let maybe_entry = self .cache .remove_if(key, hash, |_, v| is_expired_entry_wo(ttl, va, v, now)); if let Some(entry) = maybe_entry { - Self::handle_remove(deqs, entry, counters); + if eviction_state.is_notifier_enabled() { + let key = Arc::clone(key); + eviction_state.add_removed_entry(key, &entry, *cause); + } + Self::handle_remove(deqs, entry, &mut eviction_state.counters); } else if let Some(entry) = self.cache.get(key, hash) { if entry.last_modified().is_none() { deqs.move_to_back_ao(&entry); @@ -1363,9 +1667,11 @@ where invalidator: &Invalidator, deqs: &mut Deques, batch_size: usize, - counters: &mut EvictionCounters, - ) { - self.process_invalidation_result(invalidator, deqs, counters); + eviction_state: &mut EvictionState<'_, K, V>, + ) where + V: Clone, + { + self.process_invalidation_result(invalidator, deqs, eviction_state); self.submit_invalidation_task(invalidator, &mut deqs.write_order, batch_size); } @@ -1373,15 +1679,17 @@ where &self, invalidator: &Invalidator, deqs: &mut Deques, - counters: &mut EvictionCounters, - ) { + eviction_state: &mut EvictionState<'_, K, V>, + ) where + V: Clone, + { if let Some(InvalidationResult { invalidated, is_done, }) = invalidator.task_result() { - for KvEntry { key: _, entry } in invalidated { - Self::handle_remove(deqs, entry, counters); + for KvEntry { key: _key, entry } in invalidated { + Self::handle_remove(deqs, entry, &mut eviction_state.counters); } if is_done { deqs.write_order.reset_cursor(); @@ -1394,7 +1702,9 @@ where invalidator: &Invalidator, write_order: &mut Deque>, batch_size: usize, - ) { + ) where + V: Clone, + { let now = self.current_time_from_expiration_clock(); // If the write order queue is empty, we are done and can remove the predicates @@ -1434,8 +1744,10 @@ where deqs: &mut Deques, batch_size: usize, weights_to_evict: u64, - counters: &mut EvictionCounters, - ) { + eviction_state: &mut EvictionState<'_, K, V>, + ) where + V: Clone, + { const DEQ_NAME: &str = "probation"; let mut evicted = 0u64; let (deq, write_order_deq) = (&mut deqs.probation, &mut deqs.write_order); @@ -1470,6 +1782,10 @@ where None => break, }; + // Lock the key for removal if blocking removal notification is enabled. + let kl = self.maybe_key_lock(&key); + let _klg = &kl.as_ref().map(|kl| kl.lock()); + let maybe_entry = self.cache.remove_if(&key, hash, |_, v| { if let Some(lm) = v.last_modified() { lm == ts @@ -1479,8 +1795,17 @@ where }); if let Some(entry) = maybe_entry { + if eviction_state.is_notifier_enabled() { + eviction_state.add_removed_entry(key, &entry, RemovalCause::Size); + } let weight = entry.policy_weight(); - Self::handle_remove_with_deques(DEQ_NAME, deq, write_order_deq, entry, counters); + Self::handle_remove_with_deques( + DEQ_NAME, + deq, + write_order_deq, + entry, + &mut eviction_state.counters, + ); evicted = evicted.saturating_add(weight as u64); } else if !self.try_skip_updated_entry(&key, hash, DEQ_NAME, deq, write_order_deq) { break; @@ -1489,6 +1814,73 @@ where } } +impl Inner +where + K: Send + Sync + 'static, + V: Clone + Send + Sync + 'static, +{ + fn notify_single_removal( + &self, + key: Arc, + entry: &TrioArc>, + cause: RemovalCause, + ) { + if let Some(notifier) = &self.removal_notifier { + notifier.notify(key, entry.value.clone(), cause) + } + } + + #[inline] + fn notify_upsert( + &self, + key: Arc, + entry: &TrioArc>, + last_accessed: Option, + last_modified: Option, + ) { + let now = self.current_time_from_expiration_clock(); + + let mut cause = RemovalCause::Replaced; + + if let Some(last_accessed) = last_accessed { + if is_expired_by_tti(&self.time_to_idle, last_accessed, now) { + cause = RemovalCause::Expired; + } + } + + if let Some(last_modified) = last_modified { + if is_expired_by_ttl(&self.time_to_live, last_modified, now) { + cause = RemovalCause::Expired; + } else if is_invalid_entry(&self.valid_after(), last_modified) { + cause = RemovalCause::Explicit; + } + } + + self.notify_single_removal(key, entry, cause); + } + + #[inline] + fn notify_invalidate(&self, key: &Arc, entry: &TrioArc>) { + let now = self.current_time_from_expiration_clock(); + + let mut cause = RemovalCause::Explicit; + + if let Some(last_accessed) = entry.last_accessed() { + if is_expired_by_tti(&self.time_to_idle, last_accessed, now) { + cause = RemovalCause::Expired; + } + } + + if let Some(last_modified) = entry.last_modified() { + if is_expired_by_ttl(&self.time_to_live, last_modified, now) { + cause = RemovalCause::Expired; + } + } + + self.notify_single_removal(Arc::clone(key), entry, cause); + } +} + // // for testing // @@ -1529,17 +1921,8 @@ fn is_expired_entry_ao( now: Instant, ) -> bool { if let Some(ts) = entry.last_accessed() { - if let Some(va) = valid_after { - if ts < *va { - return true; - } - } - if let Some(tti) = time_to_idle { - let checked_add = ts.checked_add(*tti); - if checked_add.is_none() { - panic!("ttl overflow") - } - return checked_add.unwrap() <= now; + if is_invalid_entry(valid_after, ts) || is_expired_by_tti(time_to_idle, ts, now) { + return true; } } false @@ -1553,22 +1936,85 @@ fn is_expired_entry_wo( now: Instant, ) -> bool { if let Some(ts) = entry.last_modified() { - if let Some(va) = valid_after { - if ts < *va { - return true; - } + if is_invalid_entry(valid_after, ts) || is_expired_by_ttl(time_to_live, ts, now) { + return true; } - if let Some(ttl) = time_to_live { - let checked_add = ts.checked_add(*ttl); - if checked_add.is_none() { - panic!("ttl overflow"); - } - return checked_add.unwrap() <= now; + } + false +} + +#[inline] +fn is_entry_expired_ao_or_invalid( + time_to_idle: &Option, + valid_after: &Option, + entry: &impl AccessTime, + now: Instant, +) -> (bool, bool) { + if let Some(ts) = entry.last_accessed() { + let expired = is_expired_by_tti(time_to_idle, ts, now); + let invalid = is_invalid_entry(valid_after, ts); + return (expired, invalid); + } + (false, false) +} + +#[inline] +fn is_entry_expired_wo_or_invalid( + time_to_live: &Option, + valid_after: &Option, + entry: &impl AccessTime, + now: Instant, +) -> (bool, bool) { + if let Some(ts) = entry.last_modified() { + let expired = is_expired_by_ttl(time_to_live, ts, now); + let invalid = is_invalid_entry(valid_after, ts); + return (expired, invalid); + } + (false, false) +} + +#[inline] +fn is_invalid_entry(valid_after: &Option, entry_ts: Instant) -> bool { + if let Some(va) = valid_after { + if entry_ts < *va { + return true; } } false } +#[inline] +fn is_expired_by_tti( + time_to_idle: &Option, + entry_last_accessed: Instant, + now: Instant, +) -> bool { + if let Some(tti) = time_to_idle { + let checked_add = entry_last_accessed.checked_add(*tti); + if checked_add.is_none() { + panic!("tti overflow") + } + return checked_add.unwrap() <= now; + } + false +} + +#[inline] +fn is_expired_by_ttl( + time_to_live: &Option, + entry_last_modified: Instant, + now: Instant, +) -> bool { + if let Some(ttl) = time_to_live { + let checked_add = entry_last_modified.checked_add(*ttl); + if checked_add.is_none() { + panic!("ttl overflow"); + } + return checked_add.unwrap() <= now; + } + false +} + #[cfg(test)] mod tests { use super::BaseCache; @@ -1583,12 +2029,15 @@ mod tests { let ensure_sketch_len = |max_capacity, len, name| { let cache = BaseCache::::new( + None, Some(max_capacity), None, RandomState::default(), None, None, None, + None, + None, false, ); cache.inner.enable_frequency_sketch_for_testing(); diff --git a/src/sync_base/invalidator.rs b/src/sync_base/invalidator.rs index 42597de7..ba120b0e 100644 --- a/src/sync_base/invalidator.rs +++ b/src/sync_base/invalidator.rs @@ -39,6 +39,8 @@ pub(crate) trait GetOrRemoveEntry { condition: F, ) -> Option>> where + K: Send + Sync + 'static, + V: Clone + Send + Sync + 'static, F: FnMut(&Arc, &TrioArc>) -> bool; } @@ -189,7 +191,7 @@ impl Invalidator { pub(crate) fn submit_task(&self, candidates: Vec>, is_truncated: bool) where K: Hash + Eq + Send + Sync + 'static, - V: Send + Sync + 'static, + V: Clone + Send + Sync + 'static, S: BuildHasher + Send + Sync + 'static, { let ctx = &self.scan_context; @@ -372,7 +374,11 @@ where } } - fn execute(&self) { + fn execute(&self) + where + K: Send + Sync + 'static, + V: Clone + Send + Sync + 'static, + { let cache_lock = self.scan_context.cache.lock(); // Restore the Weak pointer to Inner. @@ -399,6 +405,8 @@ where fn do_execute(&self, cache: &Arc) -> ScanResult where Arc: GetOrRemoveEntry, + K: Send + Sync + 'static, + V: Clone + Send + Sync + 'static, { let predicates = self.scan_context.predicates.lock(); let mut invalidated = Vec::default(); @@ -460,6 +468,8 @@ where ) -> Option>> where Arc: GetOrRemoveEntry, + K: Send + Sync + 'static, + V: Clone + Send + Sync + 'static, { cache.remove_key_value_if(key, hash, |_, v| { if let Some(lm) = v.last_modified() { diff --git a/src/sync_base/key_lock.rs b/src/sync_base/key_lock.rs new file mode 100644 index 00000000..03dacfdf --- /dev/null +++ b/src/sync_base/key_lock.rs @@ -0,0 +1,85 @@ +use std::{ + hash::{BuildHasher, Hash}, + sync::Arc, +}; + +use crate::cht::SegmentedHashMap; + +use parking_lot::{Mutex, MutexGuard}; +use triomphe::Arc as TrioArc; + +const LOCK_MAP_NUM_SEGMENTS: usize = 64; + +// We need the `where` clause here because of the Drop impl. +pub(crate) struct KeyLock<'a, K, S> +where + Arc: Eq + Hash, + S: BuildHasher, +{ + map: &'a SegmentedHashMap, TrioArc>, S>, + key: Arc, + hash: u64, + lock: TrioArc>, +} + +impl<'a, K, S> Drop for KeyLock<'a, K, S> +where + Arc: Eq + Hash, + S: BuildHasher, +{ + fn drop(&mut self) { + if TrioArc::count(&self.lock) <= 1 { + self.map + .remove_if(&self.key, self.hash, |_k, v| TrioArc::count(v) <= 1); + } + } +} + +impl<'a, K, S> KeyLock<'a, K, S> +where + Arc: Eq + Hash, + S: BuildHasher, +{ + fn new(map: &'a LockMap, key: &Arc, hash: u64, lock: TrioArc>) -> Self { + Self { + map, + key: Arc::clone(key), + hash, + lock, + } + } + + pub(crate) fn lock(&self) -> MutexGuard<'_, ()> { + self.lock.lock() + } +} + +type LockMap = SegmentedHashMap, TrioArc>, S>; + +pub(crate) struct KeyLockMap { + locks: LockMap, +} + +impl KeyLockMap +where + Arc: Eq + Hash, + S: BuildHasher, +{ + pub(crate) fn with_hasher(hasher: S) -> Self { + Self { + locks: SegmentedHashMap::with_num_segments_and_hasher(LOCK_MAP_NUM_SEGMENTS, hasher), + } + } + + pub(crate) fn key_lock(&self, key: &Arc) -> KeyLock<'_, K, S> { + let hash = self.locks.hash(key); + let kl = TrioArc::new(Mutex::new(())); + match self + .locks + .insert_if_not_present(Arc::clone(key), hash, kl.clone()) + { + None => KeyLock::new(&self.locks, key, hash, kl), + Some(existing_kl) => KeyLock::new(&self.locks, key, hash, existing_kl), + } + } +}