Mini Moka is a fast, concurrent cache library for Rust. Mini Moka is a light edition of Moka.
Mini Moka provides cache implementations on top of hash maps. They support full concurrency of retrievals and a high expected concurrency for updates. Mini Moka also provides a non-thread-safe cache implementation for single thread applications.
All caches perform a best-effort bounding of a hash map using an entry replacement algorithm to determine which entries to evict when the capacity is exceeded.
- Thread-safe, highly concurrent in-memory cache implementation.
- A cache can be bounded by one of the followings:
- The maximum number of entries.
- The total weighted size of entries. (Size aware eviction)
- Maintains near optimal hit ratio by using an entry replacement algorithms inspired
by Caffeine:
- Admission to a cache is controlled by the Least Frequently Used (LFU) policy.
- Eviction from a cache is controlled by the Least Recently Used (LRU) policy.
- More details and some benchmark results are available here.
- Supports expiration policies:
- Time to live
- Time to idle
- Features
- Change Log
- Usage
- Example: Synchronous Cache
- Avoiding to clone the value at
get
- Examples (Part 2)
- Minimum Supported Rust Versions
- Developing Mini Moka
- Credits
- License
Add this to your Cargo.toml
:
[dependencies]
mini_moka = "0.10"
The thread-safe, synchronous caches are defined in the sync
module.
Cache entries are manually added using insert
method, 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:
// Use the synchronous cache.
use mini_moka::sync::Cache;
use std::thread;
fn value(n: usize) -> String {
format!("value {}", n)
}
fn main() {
const NUM_THREADS: usize = 16;
const NUM_KEYS_PER_THREAD: usize = 64;
// Create a cache that can store up to 10,000 entries.
let cache = Cache::new(10_000);
// Spawn threads and read and update the cache simultaneously.
let threads: Vec<_> = (0..NUM_THREADS)
.map(|i| {
// To share the same cache across the threads, clone it.
// This is a cheap operation.
let my_cache = cache.clone();
let start = i * NUM_KEYS_PER_THREAD;
let end = (i + 1) * NUM_KEYS_PER_THREAD;
thread::spawn(move || {
// Insert 64 entries. (NUM_KEYS_PER_THREAD = 64)
for key in start..end {
my_cache.insert(key, value(key));
// get() returns Option<String>, a clone of the stored value.
assert_eq!(my_cache.get(&key), Some(value(key)));
}
// Invalidate every 4 element of the inserted entries.
for key in (start..end).step_by(4) {
my_cache.invalidate(&key);
}
})
})
.collect();
// Wait for all threads to complete.
threads.into_iter().for_each(|t| t.join().expect("Failed"));
// Verify the result.
for key in 0..(NUM_THREADS * NUM_KEYS_PER_THREAD) {
if key % 4 == 0 {
assert_eq!(cache.get(&key), None);
} else {
assert_eq!(cache.get(&key), Some(value(key)));
}
}
}
For the concurrent cache (sync
cache), the return type of get
method is
Option<V>
instead of Option<&V>
, where V
is the value type. Every time get
is
called for an existing key, it creates a clone of the stored value V
and returns
it. This is because the Cache
allows concurrent updates from threads so a value
stored in the cache can be dropped or replaced at any time by any other thread. get
cannot return a reference &V
as it is impossible to guarantee the value outlives
the reference.
If you want to store values that will be expensive to clone, wrap them by
std::sync::Arc
before storing in a cache. Arc
is a thread-safe
reference-counted pointer and its clone()
method is cheap.
use std::sync::Arc;
let key = ...
let large_value = vec![0u8; 2 * 1024 * 1024]; // 2 MiB
// When insert, wrap the large_value by Arc.
cache.insert(key.clone(), Arc::new(large_value));
// get() will call Arc::clone() on the stored value, which is cheap.
cache.get(&key);
If different cache entries have different "weights" — e.g. each entry has
different memory footprints — you can specify a weigher
closure at the cache
creation time. The closure should return a weighted size (relative size) of an entry
in u32
, and the cache will evict entries when the total weighted size exceeds its
max_capacity
.
use std::convert::TryInto;
use mini_moka::sync::Cache;
fn main() {
let cache = Cache::builder()
// A weigher closure takes &K and &V and returns a u32 representing the
// relative size of the entry. Here, we use the byte length of the value
// String as the size.
.weigher(|_key, value: &String| -> u32 {
value.len().try_into().unwrap_or(u32::MAX)
})
// This cache will hold up to 32MiB of values.
.max_capacity(32 * 1024 * 1024)
.build();
cache.insert(0, "zero".to_string());
}
Note that weighted sizes are not used when making eviction selections.
Mini Moka supports the following expiration policies:
- Time to live: A cached entry will be expired after the specified duration past
from
insert
. - Time to idle: A cached entry will be expired after the specified duration past
from
get
orinsert
.
To set them, use the CacheBuilder
.
use mini_moka::sync::Cache;
use std::time::Duration;
fn main() {
let cache = Cache::builder()
// Time to live (TTL): 30 minutes
.time_to_live(Duration::from_secs(30 * 60))
// Time to idle (TTI): 5 minutes
.time_to_idle(Duration::from_secs( 5 * 60))
// Create the cache.
.build();
// This entry will expire after 5 minutes (TTI) if there is no get().
cache.insert(0, "zero");
// This get() will extend the entry life for another 5 minutes.
cache.get(&0);
// Even though we keep calling get(), the entry will expire
// after 30 minutes (TTL) from the insert().
}
The cache builders will panic if configured with either time_to_live
or time to idle
longer than 1000 years. This is done to protect against overflow when computing
key expiration.
Mini Moka's minimum supported Rust versions (MSRV) are the followings:
Feature | MSRV |
---|---|
default features | Rust 1.76.0 (Feb 8, 2024) |
It will keep a rolling MSRV policy of at least 6 months. If only the default features are enabled, MSRV will be updated conservatively. When using other features, MSRV might be updated more frequently, up to the latest stable. In both cases, increasing MSRV is not considered a semver-breaking change.
Running All Tests
To run all tests including doc tests on the README, use the following command:
$ RUSTFLAGS='--cfg trybuild' cargo test --all-features
Generating the Doc
$ cargo +nightly -Z unstable-options --config 'build.rustdocflags="--cfg docsrs"' \
doc --no-deps
Mini Moka's architecture is heavily inspired by the Caffeine library for Java. Thanks go to Ben Manes and all contributors of Caffeine.
Mini Moka is distributed under either of
- The MIT license
- The Apache License (Version 2.0)
at your option.
See LICENSE-MIT and LICENSE-APACHE for details.