Giter Site home page Giter Site logo

mini-moka's Introduction

Mini Moka

GitHub Actions crates.io release docs dependency status

license

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.

Features

  • 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:
  • Supports expiration policies:
    • Time to live
    • Time to idle

Change Log

Table of Contents

Usage

Add this to your Cargo.toml:

[dependencies]
mini_moka = "0.10"

Example: Synchronous Cache

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)));
        }
    }
}

Avoiding to clone the value at get

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);

Example: Size Aware Eviction

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.

Example: Expiration Policies

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 or insert.

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().
}

A note on expiration policies

The cache builders will panic if configured with either time_to_live or time to idle longer than 1000 years. This is done to protect against overflow when computing key expiration.

Minimum Supported Rust Versions

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.

Developing Mini Moka

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

Credits

Caffeine

Mini Moka's architecture is heavily inspired by the Caffeine library for Java. Thanks go to Ben Manes and all contributors of Caffeine.

License

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.

mini-moka's People

Contributors

tatsuya6502 avatar gnomeddev avatar jojodeveloping avatar

Stargazers

danb76 avatar mikione avatar plein avatar Mahmud Bello avatar Paulius Gasparavicius avatar QuarticCat avatar Conrad Ludgate avatar Tiago Carvalho avatar Ibraheem Ahmed avatar Jose Quintana avatar  avatar Vitaliy Grusha avatar Jocelyn Boullier avatar Ryan Pivovar avatar Jamil Maqdis Anton avatar Artur Corrêa Souza avatar Joel Van Eenwyk avatar s1cs avatar a2361253285 avatar Pere Wells avatar Opadc avatar Jacky Alciné avatar  avatar  avatar Shujaat Ali Khan avatar  avatar  avatar Sathvik Birudavolu avatar Ryan James Spencer avatar Yerkebulan Tulibergenov avatar Hugefiver avatar  avatar tsingson avatar YSON avatar liloew avatar Jeff Carpenter avatar  avatar Kevin Takeda avatar Christian Visintin avatar Steve Fan avatar Sandalots avatar Hana avatar Denis Andrejew avatar wei.1024 avatar Yingwen avatar Winter Snow avatar  avatar Morris Tai avatar  avatar Maxime avatar Daniel Edwards avatar Dmitry Belyaev avatar Chaoqian Xu avatar Mike Frager avatar 伊欧 avatar Jan Riemer avatar ℤiλ∀ avatar 听风 avatar Lawrence Onah avatar Rob Ede avatar Nikita avatar Romain Leroux avatar Michael Salaverry avatar Moncef AOUDIA avatar Hiroki Noda avatar Denys Mentiei avatar Christoph Grabo avatar 为世人降下祝福 avatar Andy Yang avatar Carlos Rueda avatar Jim Chen avatar  avatar Xuanwo avatar jiangplus avatar LIU JIE avatar YK avatar MathxH Chen avatar Yesterday17 avatar Wenzhuo Liu avatar  avatar  avatar Yunfei He avatar hanhotfox avatar liect avatar  avatar Bugen Zhao avatar ZHAO Jin-Xiang avatar  avatar biezhihua avatar 夏目贵志 avatar  avatar Jackson Xu avatar Chojan Shang avatar Yiwei Yang avatar Shabbir Hasan avatar Alex Chi Z. avatar Max Meldrum avatar

Watchers

 avatar

mini-moka's Issues

Brush up the eviction handler implementation

As of now (commit #f7a15e24), the following issues might be present with the eviction handler:

  1. invalidate an existing entry will always trigger the eviction handler with RemovalCause::Explicit.
    • a. But there are chances that entry has been already expired. In such case, the eviction handler should be triggered with RemovalCause::Expired.
    • b. The same thing applies to invalidate_all.
  2. There is no code path for RemovalCause::Replaced.
    • a. We should also select RemovalCause::Replaced or RemovalCause::Expired based on the expiration status of the entry.

For 1-a and 2-a, see moka's this unit test: moka sync/cache.rs#L4270-L4280

[Panic] Attempt to add with overflow during `insert`

Hi, we recently started using mini-moka to cache and retrieve data, but we've run into an unusual panic during an insert call. The panic seems rare, but this is the cleaned up stacktrace:

details = \"panicked at 'attempt to add with overflow', /usr/local/cargo/registry/src/index.crates.io-6f17d22bba15001f/mini-moka-0.10.2/src/common/frequency_sketch.rs:183:9\"
backtrace = \"\"\"\n
...
9:     0x56217dd61123 - mini_moka::common::frequency_sketch::FrequencySketch::increment::h702da432b2c28c0d 
10:     0x56217d5d5a31 - mini_moka::sync::base_cache::Inner<K,V,S>::apply_reads::h89098547f143cebc
at /usr/local/cargo/registry/src/index.crates.io-6f17d22bba15001f/mini-moka-0.10.2/src/sync/base_cache.rs:782:35. <mini_moka::sync::base_cache::Inner<K,V,S> as mini_moka::common::concurrent::housekeeper::InnerSync>::sync::h8c6497ded41ff4dd
at /usr/local/cargo/registry/src/index.crates.io-6f17d22bba15001f/mini-moka-0.10.2/src/sync/base_cache.rs:664:17\n 
11:     0x56217d41c561 - mini_moka::common::concurrent::housekeeper::Housekeeper::try_sync::h45ebd0c867b7d29e\n                               
at /usr/local/cargo/registry/src/index.crates.io-6f17d22bba15001f/mini-moka-0.10.2/src/common/concurrent/housekeeper.rs:61:17\n                           mini_moka::sync::base_cache::BaseCache<K,V,S>::apply_reads_writes_if_needed::hf2dc1456ae08dd31\n                               
at /usr/local/cargo/registry/src/index.crates.io-6f17d22bba15001f/mini-moka-0.10.2/src/sync/base_cache.rs:211:17\n                           mini_moka::sync::cache::Cache<K,V,S>::schedule_write_op::he092ec6fa514b5e5\n                               
at /usr/local/cargo/registry/src/index.crates.io-6f17d22bba15001f/mini-moka-0.10.2/src/sync/cache.rs:588:13\n                           mini_moka::sync::cache::Cache<K,V,S>::insert_with_hash::h07b1808c0524e791\n                               
at /usr/local/cargo/registry/src/index.crates.io-6f17d22bba15001f/mini-moka-0.10.2/src/sync/cache.rs:447:9\n                           mini_moka::sync::cache::Cache<K,V,S>::insert::hff040ece82fbc7a4\n                               
at /usr/local/cargo/registry/src/index.crates.io-6f17d22bba15001f/mini-moka-0.10.2/src/sync/cache.rs:441:9

It seems like the panic is occurring here: https://github.com/moka-rs/mini-moka/blob/main/src/common/frequency_sketch.rs#L183.

We're not really doing anything unusual. We're importing mini-moka:

mini-moka = { version = "0.10.2", features = ["sync"] }

Creating a new Cache in our code (with no overrides):

let cache = Cache::new(500);

And (on separate tokio threads) simply calling get and insert depending on reads/writes to the cache. Perhaps this is some type of race condition? Or maybe the addition needs to use wrapping_add, too?

Our rust version is: 1.72.1

Thanks for your help :)

panic: concurrent deque entered unreachable code

version: 0.10.1
problem: I'm using mini-moka to build an in-memeroy cache for a highly concurrent online service. And I've observed occasional panics caused by this lately:

_ => unreachable!(),

I'd appreciate it if you could spare some time to fix this. Thanks!

Here is a part of the backtrace:

#5  0x55db18df8ae0 in rust_begin_unwind at /rustc/864bdf7843e1ceabc824ed86d97006acad6af643/library/std/src/panicking.rs:617
#6  0x55db187906d0 in core::panicking::panic_fmt::haa55128da9cd75d4 at /rustc/864bdf7843e1ceabc824ed86d97006acad6af643/library/core/src/panicking.rs:67
#7  0x55db18790890 in core::panicking::panic::hb4c75d9c5b922684 at /rustc/864bdf7843e1ceabc824ed86d97006acad6af643/library/core/src/panicking.rs:117
#8  0x55db18c729f0 in mini_moka::common::concurrent::deques::Deques<K>::move_to_back_ao::h6634499d6c51838b at /root/.cargo/registry/src/rsproxy.cn-8f6827c7555bfaf8/mini-moka-0.10.1/src/common/concurrent/deques.rs:
#9  0x55db18c2f330 in <mini_moka::sync::base_cache::Inner<K,V,S> as mini_moka::common::concurrent::housekeeper::InnerSync>::sync::h65edb4f953bc2fa3 at /root/.cargo/registry/src/rsproxy.cn-8f6827c7555bfaf8/mini-moka-0.10.1/src/sync/base_cache.rs:667
#10  0x55db18c43110 in mini_moka::sync::base_cache::BaseCache<K,V,S>::get_with_hash::{{closure}}::h1faf687a57deb0fd at /root/.cargo/registry/src/rsproxy.cn-8f6827c7555bfaf8/mini-moka-0.10.1/src/sync/base_cache.rs:153
#11  0x55db18c2baf0 in mini_moka::sync::cache::Cache<K,V,S>::get::haf41a911c172cc48 at /root/.cargo/registry/src/rsproxy.cn-8f6827c7555bfaf8/mini-moka-0.10.1/src/sync/cache.rs:421

[Feature Request] Expose current cache capacity

I'm working on a crate called typesize, generating estimates of struct sizes. I want to write an implementation for mini-moka however the interface is quite opaque, and I can handle under-shooting by generating from the iterator, but I'm also missing the capacity which seems like a pretty simple request.

If the capacity could be exposed for both caches, that would be lovely.

Starting Nov 22, 2023, replies to questions and other inquiries will be delayed

Hi. I am the author of mini-moka and the sole maintainer at this time. I will be delayed in replying to inquiries for the next few weeks because I will be in a hospital 🏥 for medical treatments. I will have some chances to check GitHub Issues and Discussions from there, but will not actively respond to them.

Sorry for the inconvenience. I am hoping my personal life will settle down soon.

Clippy warnings

I will fix or disable these Clippy warnings tomorrow. (It is already midnight in my timezone)

$ cargo +stable clippy --lib --tests --all-features --all-targets

warning: very complex type used. Consider factoring parts into `type` definitions
   --> src/sync/base_cache.rs:565:10
    |
565 |     ) -> Option<(Arc<K>, TrioArc<ValueEntry<K, V>>)> {
    |          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    |
    = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#type_complexity
note: the lint level is defined here
   --> src/lib.rs:1:9
    |
1   | #![warn(clippy::all)]
    |         ^^^^^^^^^^^
    = note: `#[warn(clippy::type_complexity)]` implied by `#[warn(clippy::all)]`

warning: very complex type used. Consider factoring parts into `type` definitions
   --> src/sync/base_cache.rs:578:10
    |
578 |     ) -> Option<(Arc<K>, TrioArc<ValueEntry<K, V>>)> {
    |          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    |
    = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#type_complexity

warning: called `map(f)` on an `Option` value where `f` is a closure that returns the unit type `()`
   --> src/sync/base_cache.rs:894:17
    |
894 | //                 self.remove(&Arc::clone(&kh.key), RemovalCause::Size)
895 | ||                     .map(|(k, v)| (self.eviction_handler)(k, &v.value, RemovalCause::Size));
    | ||___________________________________________________________________________________________^- help: try this: `if let Some((k, v)) = self.remove(&Arc::clone(&kh.key), RemovalCause::Size) { (self.eviction_handler)(k, &v.value, RemovalCause::Size) }`
    |  |___________________________________________________________________________________________|
    | 
    |
    = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#option_map_unit_fn
    = note: `#[warn(clippy::option_map_unit_fn)]` implied by `#[warn(clippy::all)]`

The current beta Clippy emits the following clippy::arc_with_non_send_sync warning. For our case, Clippy is wrong. I will disable it only when the beta Clippy is used (otherwise stable Clippy will fail as it does not have clippy::arc_with_non_send_sync). I will do the same to moka-rs/moka#291.

$ cargo +beta clippy --lib --tests --all-features --all-targets

warning: usage of `Arc<T>` where `T` is not `Send` or `Sync`
   --> src/sync/base_cache.rs:117:20
    |
117 |             inner: Arc::new(inner),
    |                    ^^^^^^^^^^^^^^^
    |
    = help: consider using `Rc<T>` instead or wrapping `T` in a std::sync type like `Mutex<T>`
    = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#arc_with_non_send_sync

... (snip. same as the stable) ...

Native `typesize::TypeSize` implementation?

Hey, I'm the developer of typesize and currently it has a feature locked inaccurate implementation due to privacy, but due to the breaking release needed for #34 to be published that implementation will be pointing to the wrong version.

Would a PR to move the TypeSize implementation into mini-moka under a feature gate be accepted so I don't have to maintain the implementation in typesize? This is similar to xacrimon/dashmap#308.

This would lead to me closing #18 if accepted.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.