Stretto is a Rust implementation for ristretto. A high performance memory-bound Rust cache.

Overview

Stretto

Stretto is a pure Rust implementation for https://github.com/dgraph-io/ristretto.

A high performance thread-safe memory-bound Rust cache.

English | 简体中文

github Build codecov

docs.rs crates.io rustc

license-apache license-mit

Features

  • Internal Mutability - Do not need to use Arc <...>> for concurrent code, you just need Cache<...> or AsyncCache<...>
  • Sync and Async - Stretto support async by tokio and sync by crossbeam.
    • In sync, Cache starts two extra OS level threads. One is policy thread, the other is writing thread.
    • In async, AsyncCache starts two extra green threads. One is policy thread, the other is writing thread.
  • Store policy Stretto only store the value, which means the cache does not store the key.
  • High Hit Ratios - with Dgraph's developers unique admission/eviction policy pairing, Ristretto's performance is best in class.
    • Eviction: SampledLFU - on par with exact LRU and better performance on Search and Database traces.
    • Admission: TinyLFU - extra performance with little memory overhead (12 bits per counter).
  • Fast Throughput - use a variety of techniques for managing contention and the result is excellent throughput.
  • Cost-Based Eviction - any large new item deemed valuable can evict multiple smaller items (cost could be anything).
  • Fully Concurrent - you can use as many threads as you want with little throughput degradation.
  • Metrics - optional performance metrics for throughput, hit ratios, and other stats.
  • Simple API - just figure out your ideal CacheBuilder/AsyncCacheBuilder values and you're off and running.

Table of Contents

Installation

  • Use Cache.
[dependencies]
stretto = "0.2"

or

[dependencies]
stretto = { version = "0.2", features = ["sync"] }
  • Use AsyncCache
[dependencies]
stretto = { version = "0.2", features = ["async"] }
  • Use both Cache and AsyncCache
[dependencies]
stretto = { version = "0.2", features = ["full"] }

Usage

Example

Sync

use stretto::{Cache, DefaultKeyBuilder};
use std::time::Duration;

fn main() {
    let c = Cache::new(12960, 1e6 as i64, DefaultKeyBuilder::default()).unwrap();

    // set a value with a cost of 1
    c.insert("a", "a", 1);

    // set a value with a cost of 1 and ttl
    c.insert_with_ttl("b", "b", 1, Duration::from_secs(3));
    
    // wait for value to pass through buffers
    c.wait().unwrap();

    // when we get the value, we will get a ValueRef, which contains a RwLockReadGuard
    // so when we finish use this value, we must release the ValueRef
    let v = c.get(&"a").unwrap();
    assert_eq!(v.value(), &"a");
    v.release();

    // lock will be auto released when out of scope
    {
        // when we get the value, we will get a ValueRef, which contains a RwLockWriteGuard
        // so when we finish use this value, we must release the ValueRefMut
        let mut v = c.get_mut(&"a").unwrap();
        v.write("aa");
        assert_eq!(v.value(), &"aa");
        // release the value 
    }

    // if you just want to do one operation
    let v = c.get_mut(&"a").unwrap();
    v.write_once("aaa");

    let v = c.get(&"a").unwrap();
    assert_eq!(v.value(), &"aaa");
    v.release();

    // clear the cache
    c.clear().unwrap();
    // wait all the operations are finished
    c.wait().unwrap();
    assert!(c.get(&"a").is_none());
}

Async

use stretto::{AsyncCache, DefaultKeyBuilder};
use std::time::Duration;

#[tokio::main]
async fn main() {
    let c = AsyncCache::new(12960, 1e6 as i64, DefaultKeyBuilder::default()).unwrap();

    // set a value with a cost of 1
    c.insert("a", "a", 1).await;

    // set a value with a cost of 1 and ttl
    c.insert_with_ttl("b", "b", 1, Duration::from_secs(3)).await;
    
    // wait for value to pass through buffers
    c.wait().await.unwrap();

    
    // when we get the value, we will get a ValueRef, which contains a RwLockReadGuard
    // so when we finish use this value, we must release the ValueRef
    let v = c.get(&"a").unwrap();
    assert_eq!(v.value(), &"a");
    // release the value
    v.release(); // or drop(v)

    // lock will be auto released when out of scope
    {
        // when we get the value, we will get a ValueRef, which contains a RwLockWriteGuard
        // so when we finish use this value, we must release the ValueRefMut
        let mut v = c.get_mut(&"a").unwrap();
        v.write("aa");
        assert_eq!(v.value(), &"aa");
        // release the value
    }
    

    // if you just want to do one operation
    let v = c.get_mut(&"a").unwrap();
    v.write_once("aaa");

    let v = c.get(&"a").unwrap();
    println!("{}", v);
    assert_eq!(v.value(), &"aaa");
    v.release();

    // clear the cache
    c.clear().unwrap();
    // wait all the operations are finished
    c.wait().await.unwrap();

    assert!(c.get(&"a").is_none());
}

Config

The CacheBuilder struct is used when creating Cache instances if you want to customize the Cache settings.

num_counters

num_counters is the number of 4-bit access counters to keep for admission and eviction. Dgraph's developers have seen good performance in setting this to 10x the number of items you expect to keep in the cache when full.

For example, if you expect each item to have a cost of 1 and max_cost is 100, set num_counters to 1,000. Or, if you use variable cost values but expect the cache to hold around 10,000 items when full, set num_counters to 100,000. The important thing is the number of unique items in the full cache, not necessarily the max_cost value.

max_cost

max_cost is how eviction decisions are made. For example, if max_cost is 100 and a new item with a cost of 1 increases total cache cost to 101, 1 item will be evicted.

max_cost can also be used to denote the max size in bytes. For example, if max_cost is 1,000,000 (1MB) and the cache is full with 1,000 1KB items, a new item (that's accepted) would cause 5 1KB items to be evicted.

max_cost could be anything as long as it matches how you're using the cost values when calling insert.

key_builder

pub trait KeyBuilder
   Eq + ?
   Sized> {
    
   /// hash_index is used to hash the key to u64
    
   fn 
   hash_index(
   &
   self, key: 
   &K) -> 
   u64;

    
   /// if you want a 128bit hashes, you should implement this method,
    
   /// or leave this method return 0
    
   fn 
   hash_conflict(
   &
   self, key: 
   &K) -> 
   u64 { 
   0 }

    
   /// build the key to 128bit hashes.
    
   fn 
   build_key(
   &
   self, k: 
   &K) -> (
   u64, 
   u64) {
        (
   self.
   hash_index(k), 
   self.
   hash_conflict(k))
    }
}
  

KeyBuilder is the hashing algorithm used for every key. In Stretto, the Cache will never store the real key. The key will be processed by KeyBuilder. Stretto has two default built-in key builder, one is TransparentKeyBuilder, the other is DefaultKeyBuilder. If your key implements TransparentKey trait, you can use TransparentKeyBuilder which is faster than DefaultKeyBuilder. Otherwise, you should use DefaultKeyBuilder You can also write your own key builder for the Cache, by implementing KeyBuilder trait.

Note that if you want 128bit hashes you should use the full (u64, u64), otherwise just fill the u64 at the 0 position, and it will behave like any 64bit hash.

buffer_size

buffer_size is the size of the insert buffers. The Dgraph's developers find that 32 * 1024 gives a good performance.

If for some reason you see insert performance decreasing with lots of contention (you shouldn't), try increasing this value in increments of 32 * 1024. This is a fine-tuning mechanism and you probably won't have to touch this.

metrics

Metrics is true when you want real-time logging of a variety of stats. The reason this is a CacheBuilder flag is because there's a 10% throughput performance overhead.

ignore_internal_cost

Set to true indicates to the cache that the cost of internally storing the value should be ignored. This is useful when the cost passed to set is not using bytes as units. Keep in mind that setting this to true will increase the memory usage.

cleanup_duration

The Cache will cleanup the expired values every 500ms by default.

update_validator

pub trait UpdateValidator
   : Send + Sync + 'static {
    
   /// should_update is called when a value already exists in cache and is being updated.
    
   fn 
   should_update(
   &
   self, prev: 
   &V, curr: 
   &V) -> 
   bool;
}
  

By default, the Cache will always update the value if the value already exists in the cache, this trait is for you to check if the value should be updated.

callback

pub trait CacheCallback
   Send + 
   Sync>: Send + Sync + 'static {
    
   /// on_exit is called whenever a value is removed from cache. This can be
    
   /// used to do manual memory deallocation. Would also be called on eviction
    
   /// and rejection of the value.
    
   fn 
   on_exit(
   &
   self, val: 
   Option
   
    );

    
    /// on_evict is called for every eviction and passes the hashed key, value,
    
    /// and cost to the function.
    
    fn 
    on_evict(
    &
    self, item: Item
    
     ) {
        
     self.
     on_exit(item.val)
    }

    
     /// on_reject is called for every rejection done via the policy.
    
     fn 
     on_reject(
     &
     self, item: Item
     
      ) {
        
      self.
      on_exit(item.val)
    }
}
     
    
   
  

CacheCallback is for customize some extra operations on values when related event happens.

coster

pub trait Coster
   : Send + Sync + 'static {
    
   /// cost evaluates a value and outputs a corresponding cost. This function
    
   /// is ran after insert is called for a new item or an item update with a cost
    
   /// param of 0.
    
   fn 
   cost(
   &
   self, val: 
   &V) -> 
   i64;
}
  

Cost is a trait you can pass to the CacheBuilder in order to evaluate item cost at runtime, and only for the insert calls that aren't dropped (this is useful if calculating item cost is particularly expensive, and you don't want to waste time on items that will be dropped anyways).

To signal to Stretto that you'd like to use this Coster trait:

  1. Set the Coster field to your own Coster implementation.
  2. When calling insert for new items or item updates, use a cost of 0.

hasher

The hasher for the Cache, default is SipHasher.

Acknowledgements

  • Thanks Dgraph's developers for providing amazing Go version Ristretto implementation.

License

Licensed under either of Apache License, Version 2.0 or MIT license at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this project by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.
Comments
  • How to print `sync::cache` state

    How to print `sync::cache` state

    Suppose I have a sync::Cache<_, _> and I'd like to display its content(for example for debugging purposes). How can I do that? I didn't find any way.

    opened by kakoc 3
  • Keep getting `already borrowed: BorrowMutError`. Guidelines on troubleshooting

    Keep getting `already borrowed: BorrowMutError`. Guidelines on troubleshooting

    I've been trying to troubleshoot some panics using stretto-0.3.3 mapping to stretto::cache::axync::AsyncCache::insert_with_ttl and I think panicking at this point with the message: already borrowed: BorrowMutError.

    I've increased my counters and it seems to have helped, but still get it pretty often.

    Any guidance on how to troubleshoot this?

    opened by sebasmagri 3
  • Use mimalloc and update release profiles for Rust benchmarks

    Use mimalloc and update release profiles for Rust benchmarks

    As mentioned in a Reddit comment 😃

    This improves benchmarks considerably on my machine:

    ---Go Ristretto Finished in 16ms---
    hit: 98858 miss: 25754 keys-added: 6657 keys-updated: 12865 keys-evicted: 5598 cost-added: 6234984 cost-evicted:
     5235204 sets-dropped: 0 sets-rejected: 6231 gets-dropped: 21504 gets-kept: 102976 gets-total: 124612 hit-ratio:
     0.79
    ---Sync Stretto Finished in 9ms---
    Metrics::Op {
      "hit": 96501,
      "miss": 28111,
      "keys-added": 25447,
      "keys-updated": 2653,
      "keys-evicted": 24420,
      "cost-added": 24838984,
      "cost-evicted": 23839632,
      "sets-dropped": 0,
      "sets-rejected": 0,
      "gets-dropped": 0,
      "gets-kept": 0,
      "gets-total": 124612,
      "hit-ratio": 0.77
    }
    ---Async Stretto Finished in 11ms---
    Metrics::Op {
      "hit": 97583,
      "miss": 27029,
      "keys-added": 25630,
      "keys-updated": 1389,
      "keys-evicted": 24600,
      "cost-added": 24983368,
      "cost-evicted": 23983788,
      "sets-dropped": 0,
      "sets-rejected": 0,
      "gets-dropped": 0,
      "gets-kept": 0,
      "gets-total": 124612,
      "hit-ratio": 0.78
    }
    ---Sync Moka Finished in 17ms---
    ---Async Moka Finished in 18ms---
    
    opened by sd2k 3
  • Possible deadlock in `wait` in `async_std` runtime

    Possible deadlock in `wait` in `async_std` runtime

    Code below may freeze on wait() call. It happens most of the time for me (macOS).

    use async_std::task;
    use stretto::AsyncCache;
    
    fn main() {
        let max_cost = 10_000;
        let lru = AsyncCache::new(max_cost * 10, max_cost as i64, task::spawn)
            .expect("failed to create cache");
    
        task::block_on(task::spawn(async move {
            for i in 0..10_000 {
                println!("i = {i}, len before insert = {}", lru.len());
    
                let value = 123;
                let cost = 1;
                lru.insert(i, value, cost).await;
                lru.wait().await.unwrap(); // <-- freezes here
            }
    
            println!("done");
        }));
    }
    

    I also have a question. Is there an easy way to predict how many items a cache will fit (with parameters above) if we always use cost = 1? Seems like it's capping out at ~175 items in an example above (max_cost = 10_000, with each item cost = 1).

    bug 
    opened by stepantubanov 2
  • Can you use a cache after clear()?

    Can you use a cache after clear()?

    Should you be able to insert into the cache after you clear() it? The test case below fails on the final line since the get() returns None (after insert and wait). Looking at the code, I believe this is a bug and not a design decision; however, I don't see an attempt to use a cache after clear in the examples or tests.

        use claim::*;
        use pretty_assertions::assert_eq;
    
        #[test]
        fn test_basic_cache_add_after_clear() {
            let ttl = Duration::from_secs(60);
            block_on(async {
                let cache = AsyncCache::builder(1000, 100)
                    .set_metrics(true)
                    .set_ignore_internal_cost(true)
                    .finalize()
                    .expect("failed creating cache");
    
                assert!(cache.insert_with_ttl("foo".to_string(), 17.to_string(), 1, ttl).await);
                assert!(cache.insert_with_ttl("bar".to_string(), "otis".to_string(), 1, ttl).await);
                assert_ok!(cache.wait().await);
    
                assert_eq!(assert_some!(cache.get(&"foo".to_string())).value(), &17.to_string());
                assert_eq!(assert_some!(cache.get(&"bar".to_string())).value(), &"otis".to_string());
    
                assert_ok!(cache.clear());
                assert_ok!(cache.wait().await);
    
                assert_none!(cache.get(&"foo".to_string()));
    
                assert!(cache.insert_with_ttl("zed".to_string(), 33.to_string(), 1, ttl).await);
                assert_ok!(cache.wait().await);
    
                assert_none!(cache.get(&"bar".to_string()));
                assert_eq!(assert_some!(cache.get(&"zed".to_string())).value(), &33.to_string());
            });
        }
    

    error message in my project:

    ---- phases::sense::clearinghouse::cache::tests::test_basic_cache_add_after_clear stdout ----
    thread 'phases::sense::clearinghouse::cache::tests::test_basic_cache_add_after_clear' panicked at 
    'assertion failed, expected Some(..), got None', src/phases/sense/clearinghouse/cache.rs:272:24
    
    bug help wanted 
    opened by dmrolfs 2
  • [feature] Getting TTL value of entry

    [feature] Getting TTL value of entry

    proposing a new method that gets ttl value of an entry.

    let val_ref = cache.get(&key).unwrap();
    let ttl: std::time::Duration = val_ref.ttl();
    let value = val_ref.value();
    
    help wanted feature-request 
    opened by ClSlaid 2
  • [i18n] translate README into Simplified Chinese.

    [i18n] translate README into Simplified Chinese.

    - change zh_CN to zh_hans to avoid some boring problems
    - 将 zh_CN 改为 zh_hans(希望某天会有人改一稿 zh_hant 出来)
    - further proofreading and retouching required
    - 需要校对与润色
    

    fixes #15

    Signed-off-by: ClSlaid [email protected]

    opened by ClSlaid 2
  • Some feedback

    Some feedback

    Hey there, very cool library. I was looking it over and noticed a few miscellaneous things that might improve it. In no particular order:

    There are a couple instances of sentinel values in the api.

    • Duration::ZERO indicates infinite ttl. This isn't what I would expect. Consider using Duration::MAX instead. (should make implementation simpler too)
    • When 0 is provided as an external cost, the provided Coster kicks in. This makes 0 a special value despite being a perfectly valid cost setting. Consider taking either an Option cost in setter methods, or using a separate setter method that doesn't take a cost parameter, and uses Coster to assign one.

    It's advisable to make invalid states unrepresentable. Costs are expressed as i64, not u64. This struck me as odd. Are negative costs valid?

    This call to tokio select has only one future, which doesn't make sense considering what select does (it races multiple futures).

    Cargo clippy is a really handy tool for improving code. Among other things, it helps catch errors and keeps code idiomatic. I do recommend trying it out on this repo as can give really useful suggestions and teach you about rust idioms. It currently reports 49 suggestions for this codebase.

    This last one might just be personal preference. Macros are immensely useful, but do have costs, like confusing rustfmt or sometimes confusing people trying to understand the code. That said, I see couple instances where use of custom macros can be reduced. This for example could be replaced with:

    #[cfg(feature = "async")]
    mod aync_test {
    

    The consistent and idiomatic formatting is much appreciated by the way. Make it a lot easier to dive into the code.

    Again I think this library is super cool and nicely written. If you'd like me to address any or all of these points via pr please let me know.

    enhancement 
    opened by bddap 2
  • How does the max_cost works?

    How does the max_cost works?

    Hi, I have a question about the max_cost argument in the AsyncCache. When I set it to 2 and insert 2 item each have 1 cost, then one of the item is missing. Maybe I misinterpret something here. Code:

    use stretto::AsyncCache;
    
    #[tokio::main]
    async fn main() {
        let c = AsyncCache::new(20, 2, tokio::spawn).unwrap();
        c.insert(1, 2, 1).await;
        c.insert(2, 3, 1).await;
    
        c.wait().await.unwrap();
    
        c.get(&1).unwrap(); // Get a panic
    }
    
    opened by LinkTed 1
  • Use-After-Free in SharedNonNull?

    Use-After-Free in SharedNonNull?

    Hi. Thanks for an interesting library. I'm wondering what you had in mind for SharedNonNull under src/utils.rs. It's not being used anywhere, but it's also somewhat buggy.

    As of fb980da9deaf97b88ae84206291699ddc8d492e5, There seems to be use-after-free coming from SharedNonNull. For example: cargo miri test utils::test::test_shared_non_null returns error: Undefined Behavior: pointer to alloc189467 was dereferenced after this allocation got freed.

    In that case, I suspect 3 is being deallocated right after SharedNonNull is created, so the inner NonNull is hanging. I am not too familiar with your library, but it seems you might want some Rc like behavior to prevent the original location from being deallocated. Otherwise, it seems you will always get use-after-free when creating from a value that lives only for 1 line.

    Full error message

    error: Undefined Behavior: pointer to alloc189467 was dereferenced after this allocation got freed
       --> /home/ubuntu/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/ptr/non_null.rs:327:18
        |
    327 |         unsafe { &*self.as_ptr() }
        |                  ^^^^^^^^^^^^^^^ pointer to alloc189467 was dereferenced after this allocation got freed
        |
        = help: this indicates a bug in the program: it performed an invalid operation, and caused Undefined Behavior
        = help: see https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html for further information
                
        = note: inside `std::ptr::NonNull::<i32>::as_ref` at /home/ubuntu/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/ptr/non_null.rs:327:18
    note: inside `utils::SharedNonNull::<i32>::as_ref` at src/utils.rs:261:9
       --> src/utils.rs:261:9
        |
    261 |         self.ptr.as_ref()
        |         ^^^^^^^^^^^^^^^^^
    note: inside `utils::test::test_shared_non_null` at src/utils.rs:360:26
       --> src/utils.rs:360:26
        |
    360 |         let r = unsafe { snn.as_ref() };
        |                          ^^^^^^^^^^^^
    note: inside closure at src/utils.rs:358:5
       --> src/utils.rs:358:5
        |
    357 |       #[test]
        |       ------- in this procedural macro expansion
    358 | /     fn test_shared_non_null() {
    359 | |         let snn = SharedNonNull::new(&mut 3);
    360 | |         let r = unsafe { snn.as_ref() };
    361 | |         assert_eq!(r, &3);
    ...   |
    365 | |         }
    366 | |     }
        | |_____^
        = note: inside `<[closure@src/utils.rs:358:5: 366:6] as std::ops::FnOnce<()>>::call_once - shim` at /home/ubuntu/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/ops/function.rs:227:5
        = note: inside `<fn() as std::ops::FnOnce<()>>::call_once - shim(fn())` at /home/ubuntu/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/ops/function.rs:227:5
        = note: inside `test::__rust_begin_short_backtrace::<fn()>` at /home/ubuntu/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/test/src/lib.rs:585:5
        = note: inside closure at /home/ubuntu/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/test/src/lib.rs:576:30
        = note: inside `<[closure@test::run_test::{closure#2}] as std::ops::FnOnce<()>>::call_once - shim(vtable)` at /home/ubuntu/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/ops/function.rs:227:5
        = note: inside `<std::boxed::Box<dyn std::ops::FnOnce() + std::marker::Send> as std::ops::FnOnce<()>>::call_once` at /home/ubuntu/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/alloc/src/boxed.rs:1811:9
        = note: inside `<std::panic::AssertUnwindSafe<std::boxed::Box<dyn std::ops::FnOnce() + std::marker::Send>> as std::ops::FnOnce<()>>::call_once` at /home/ubuntu/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/panic/unwind_safe.rs:271:9
        = note: inside `std::panicking::r#try::do_call::<std::panic::AssertUnwindSafe<std::boxed::Box<dyn std::ops::FnOnce() + std::marker::Send>>, ()>` at /home/ubuntu/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/panicking.rs:406:40
        = note: inside `std::panicking::r#try::<(), std::panic::AssertUnwindSafe<std::boxed::Box<dyn std::ops::FnOnce() + std::marker::Send>>>` at /home/ubuntu/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/panicking.rs:370:19
        = note: inside `std::panic::catch_unwind::<std::panic::AssertUnwindSafe<std::boxed::Box<dyn std::ops::FnOnce() + std::marker::Send>>, ()>` at /home/ubuntu/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/panic.rs:133:14
        = note: inside `test::run_test_in_process` at /home/ubuntu/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/test/src/lib.rs:608:18
        = note: inside closure at /home/ubuntu/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/test/src/lib.rs:500:39
        = note: inside `test::run_test::run_test_inner` at /home/ubuntu/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/test/src/lib.rs:538:13
        = note: inside `test::run_test` at /home/ubuntu/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/test/src/lib.rs:572:28
        = note: inside `test::run_tests::<[closure@test::run_tests_console::{closure#2}]>` at /home/ubuntu/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/test/src/lib.rs:313:17
        = note: inside `test::run_tests_console` at /home/ubuntu/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/test/src/console.rs:290:5
        = note: inside `test::test_main` at /home/ubuntu/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/test/src/lib.rs:124:15
        = note: inside `test::test_main_static` at /home/ubuntu/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/test/src/lib.rs:143:5
        = note: inside `main`
        = note: inside `<fn() as std::ops::FnOnce<()>>::call_once - shim(fn())` at /home/ubuntu/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/ops/function.rs:227:5
        = note: inside `std::sys_common::backtrace::__rust_begin_short_backtrace::<fn(), ()>` at /home/ubuntu/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/sys_common/backtrace.rs:123:18
        = note: inside closure at /home/ubuntu/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/rt.rs:145:18
        = note: inside `std::ops::function::impls::<impl std::ops::FnOnce<()> for &dyn std::ops::Fn() -> i32 + std::marker::Sync + std::panic::RefUnwindSafe>::call_once` at /home/ubuntu/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/ops/function.rs:259:13
        = note: inside `std::panicking::r#try::do_call::<&dyn std::ops::Fn() -> i32 + std::marker::Sync + std::panic::RefUnwindSafe, i32>` at /home/ubuntu/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/panicking.rs:406:40
        = note: inside `std::panicking::r#try::<i32, &dyn std::ops::Fn() -> i32 + std::marker::Sync + std::panic::RefUnwindSafe>` at /home/ubuntu/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/panicking.rs:370:19
        = note: inside `std::panic::catch_unwind::<&dyn std::ops::Fn() -> i32 + std::marker::Sync + std::panic::RefUnwindSafe, i32>` at /home/ubuntu/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/panic.rs:133:14
        = note: inside closure at /home/ubuntu/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/rt.rs:128:48
        = note: inside `std::panicking::r#try::do_call::<[closure@std::rt::lang_start_internal::{closure#2}], isize>` at /home/ubuntu/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/panicking.rs:406:40
        = note: inside `std::panicking::r#try::<isize, [closure@std::rt::lang_start_internal::{closure#2}]>` at /home/ubuntu/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/panicking.rs:370:19
        = note: inside `std::panic::catch_unwind::<[closure@std::rt::lang_start_internal::{closure#2}], isize>` at /home/ubuntu/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/panic.rs:133:14
        = note: inside `std::rt::lang_start_internal` at /home/ubuntu/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/rt.rs:128:20
        = note: inside `std::rt::lang_start::<()>` at /home/ubuntu/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/rt.rs:144:17
        = note: this error originates in the attribute macro `test` (in Nightly builds, run with -Z macro-backtrace for more info)
    
    error: aborting due to previous error
    
    error: test failed, to rerun pass '--lib'
    
    opened by YoshikiTakashima 1
  • Update moka-rs/main in bench

    Update moka-rs/main in bench

    Hi. I just found Stretto. Very nice!

    This PR updates the stuff in the bench directory:

    1. Fix compile errors in bench/ristretto-rs/src/main.rs.
    2. Update bench/moka-rs/src/main.rs:
      • Show some statistics (hit, miss, gets total and hit ratio) at the end of run.
      • Like Stretto and Ristretto, use precalculated hashes for keys, instead of calculating them at get and insert times.
    3. Ensure to clear bench/bin before building binaries and copying them into there. I found overriding binaries there somehow made them corrupted on my Mac. (Maybe due to an anti-virus software?)

    Just FYI, here is a benchmark result from my Mac:

    Disclaimer

    • I am the Moka author.
    • This is an apples-vs-oranges comparison as Moka v0.6.1 does not support cost-based eviction while others do.

    Result

    # macOS Big Sur (arm64), Apple M1 chip
    # Rust 1.57.0 stable
    
    $ ./bench.sh
    ...
    
    Generating mock data...
    ---Go Ristretto Finished in 16ms---
    hit: 98690 miss: 24431 keys-added: 6763 keys-updated: 11565 keys-evicted: 5705 cost-added: 6243336 cost-evicted: 5243616 sets-dropped: 0 sets-rejected: 6103 gets-dropped: 21760 gets-kept: 101248 gets-total: 123121 hit-ratio: 0.80
    ---Sync Stretto Finished in 18ms---
    Metrics::Op {
      "hit": 96257,
      "miss": 26864,
      "keys-added": 26593,
      "keys-updated": 262,
      "keys-evicted": 25571,
      "cost-added": 25817500,
      "cost-evicted": 24817532,
      "sets-dropped": 0,
      "sets-rejected": 0,
      "gets-dropped": 0,
      "gets-kept": 0,
      "gets-total": 123121,
      "hit-ratio": 0.78
    }
    ---Async Stretto Finished in 20ms---
    Metrics::Op {
      "hit": 96758,
      "miss": 26363,
      "keys-added": 25897,
      "keys-updated": 461,
      "keys-evicted": 24863,
      "cost-added": 25120192,
      "cost-evicted": 24120308,
      "sets-dropped": 0,
      "sets-rejected": 0,
      "gets-dropped": 0,
      "gets-kept": 0,
      "gets-total": 123121,
      "hit-ratio": 0.79
    }
    ---Sync Moka Finished in 19ms---
    OpMetrics { hit: 121825, miss: 1296, gets_total: 123121, hit_ratio: 0.99 }
    ---Async Moka Finished in 19ms---
    OpMetrics { hit: 121825, miss: 1296, gets_total: 123121, hit_ratio: 0.99 }
    

    Some thoughts:

    • Moka's hit ratio is somewhat higher than others (0.99 vs 0.79, 0.80).
      • This might be caused by the difference in cache admission policies:
        • Stretto and Ristretto use a write buffer and while entries are in the buffer, get will not return them. (cache miss)
        • Moka uses a write buffer too but while entries are in the buffer, get will return them. (cache hit)
      • You might want to adjust the key distribution in the dataset to reduce the hit ratio in Moka.
    opened by tatsuya6502 1
  • failure: cache::test::sync_test::test_cache_drop_updates, when running with multiple threads

    failure: cache::test::sync_test::test_cache_drop_updates, when running with multiple threads

    I haven't figured out if the failure is caused by a problem with the library or with the test. I do seem to have narrowed it down to only failing when the test is run with multiple threads and allowing all the test to be run. When the test is limited to a single thread, or when the test is run in isolation (cargo test --release --lib test_cache_drop_updates -- --test-threads=4), it seems to pass each time.

    cargo test and cargo test --release often, but not always, fails with

    failures:
    
    ---- cache::test::sync_test::test_cache_drop_updates stdout ----
    thread 'cache::test::sync_test::test_cache_drop_updates' panicked at 'assertion failed: c.insert(1, \"0\".to_string(), 10)', src/cache/test.rs:680:13
    note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
    
    
    failures:
        cache::test::sync_test::test_cache_drop_updates
    

    but running with cargo test -- --test-threads=1 sees it pass each time. I wonder if other tests that preceded this one are having an affect because when the one test is run in isolation, it always passes too.

    If it were determined the test isn't designed for multiple threads, I would suggest creating a .cargo/config.toml with

    [env]
    RUST_TEST_THREADS = { value = "1" }
    

    I would offer a PR but as I said at the top, I don't know if a real problem is being masked by just running the one test or just running with one thread.

    opened by FrankReh 0
  • Deadlock with AsyncCache when using wait and nested caches

    Deadlock with AsyncCache when using wait and nested caches

    We have encountered a deadlock situation when attempting to have nested caches, as in a key-key-value multi-dimensional map, where there are many spawned tasks trying to interact with the cache at once, and the tasks attempt to call wait after insert of the inner value.

    All the tokio (non-blocking) threads become blocked on trying to obtain a lock on the cache. The await inside wait allows one of the other tasks destined to block to be scheduled, eventually causing deadlock. The code below, inner_cache.wait() happens while a read lock on lru is held. Avoiding the inner_cache.wait() call prevents the deadlock.

    We have restructured our code so it no longer uses nested caches and avoids this situation, but I wanted to share this discovery. This may not be a usage pattern that is intended to work or be supported. But to my eyes it is not immediately obvious that this would deadlock. I speculate that if the CacheProcessor was spawned as a dedicated thread instead of a green thread it might be able to complete the wait group and unstick things (we didn't experience a deadlock with this same design pattern when using the sync Cache).

    Here is an example test that can demonstrate the deadlock:

        #[tokio::test(flavor = "multi_thread")]
        async fn test_wait_with_read_lock() {
            let max_cost = 10_000;
            let lru = Arc::new(
                AsyncCacheBuilder::new(max_cost * 10, max_cost as i64)
                    .set_ignore_internal_cost(true)
                    .finalize(tokio::spawn)
                    .expect("failed to create cache"),
            );
    
            let key = 1;
            let cost = 1;
    
            let mut tasks = Vec::new();
    
            for i in 1..=10_000 {
                let lru = Arc::clone(&lru);
                tasks.push(tokio::spawn(async move {
                    let inner_cache = match lru.get(&key) {
                        Some(v) => v,
                        None => {
                            let inner_lru = AsyncCacheBuilder::new(max_cost * 10, max_cost as i64)
                                .set_ignore_internal_cost(true)
                                .finalize(tokio::spawn)
                                .expect("failed to create cache");
                            lru.insert(key, inner_lru, cost).await;
                            lru.wait().await.unwrap();
                            lru.get(&key).unwrap()
                        }
                    };
                    let inner_cache = inner_cache.value();
                    inner_cache.insert(i, 123, cost).await;
                    eprintln!("i = {i}, len before wait = {}", inner_cache.len());
                    // removing this wait avoids deadlock
                    inner_cache.wait().await.unwrap();
                }));
            }
    
            for task in tasks {
                task.await.unwrap();
            }
        }
    
    opened by jrray 1
  • Entry inserted without TTL (cache.try_insert) removed after 30 minutes.

    Entry inserted without TTL (cache.try_insert) removed after 30 minutes.

    I am using the AsyncCache and have entries that I do not want to expire and use the try_insert() method. I am seeing these properties being removed from the cache after 30 minutes.

    bug help wanted 
    opened by dmrolfs 1
  • Get-or-insert-with semantics / Dogpile Effect Mitigation

    Get-or-insert-with semantics / Dogpile Effect Mitigation

    Async Caches benefit from being able to ensure that - if a value is not currently cached - a future imminently fulfilling the entry will be passed to the other asynchronous readers. I haven't found a way to implement this pattern without adding another layer of internal mutability within the existing Stretto cache; is there an officially endorsed way to deduplicate cache fulfillment requests? If not, I'd like to request the feature.

    help wanted feature-request 
    opened by Dessix 2
  • Stacks on close() while code compiled as cdylib

    Stacks on close() while code compiled as cdylib

    It's great that with your lib i can do such things

    lazy_static! {
        static ref CACHE: Cache<String, TcpStream> = {
            let cache = Cache::new(12000, 1e6 as i64).unwrap();
            cache
        };
    }
    

    But there is 2 issues while compiling code as cdylib:

    1. Without cache.close() in DLL_PROCESS_DETACH (yeah, my lib is for windows), it just throws some kind of error, which rust detects while I loading this dll - STATUS_ACCESS_VIOLATION. In another programs this issues cause whole program crash
    2. I find out to prevent that issue i need to call cache.close() in order to clear some memory, stop additional threads, but now it just stacks on closing and do nothing until I kill process I really need your help, because i literally can't find another in-memory cache which can be defined as global value with lazy_static
    opened by Numenorean 1
Releases(v0.2.0)
Owner
Al Liu
Rustacean/Gopher, distributed system engineer. In the previous two years, writing Go, but now, writing Rust most of the time.
Al Liu
A general-purpose distributed memory cache system compatible with Memcached

memcrsd memcached server implementation in Rust memcrsd is a key value store implementation in Rust. It is compatible with binary protocol of memcache

null 274 Dec 14, 2022
ConstDB - an in-memory cache store which aims at master-master replications

A redis-like cache store that implements CRDTs and active-active replications.

null 27 Aug 15, 2022
A read-only, memory-mapped cache.

mmap-cache A low-level API for a memory-mapped cache of a read-only key-value store. Design The [Cache] index is an [fst::Map], which maps from arbitr

Duncan 3 Jun 28, 2022
This is a Rust implementation for HashiCorp's golang-lru. This crate contains three LRU based cache, LRUCache, TwoQueueCache and AdaptiveCache.

This is a Rust implementation for HashiCorp's golang-lru. This crate contains three LRU based cache, LRUCache, TwoQueueCache and AdaptiveCache.

Al Liu 84 Jan 3, 2023
A generational arena based LRU Cache implementation in 100% safe rust.

generational-lru Crate providing a 100% safe, generational arena based LRU cache implementation. use generational_lru::lrucache::{LRUCache, CacheError

Arindam Das 37 Dec 21, 2022
A native stateless cache implementation.

fBNC fBNC, Blockchain Native Cache. A native stateless storage library for block chain. Its value is to improve the stability and security of online s

Findora Foundation 1 Jan 12, 2022
Rust cache structures and easy function memoization

cached Caching structures and simplified function memoization cached provides implementations of several caching structures as well as a handy macro f

James Kominick 996 Jan 7, 2023
A set of safe Least Recently Used (LRU) map/cache types for Rust

LruMap A set of safe Least-Recently-Used (LRU) cache types aimed at providing flexible map-like structures that automatically evict the least recently

Khonsu Labs 4 Sep 24, 2022
Rust type wrapper to cache hash of potentially large structures.

CachedHash For a type T, CachedHash<T> wraps T and implements Hash in a way that caches T's hash value. This is useful when T is expensive to hash (fo

pali 3 Dec 8, 2022
A lightweight key-value cache system developed for experimental purposes

A lightweight key-value cache system developed for experimental purposes. It can also include distributed systems setup if I can.

Burak Selim Senyurt 8 Jul 23, 2022
Read-optimized cache of Cardano on-chain entities

Read-optimized cache of Cardano on-chain entities Intro Scrolls is a tool for building and maintaining read-optimized collections of Cardano's on-chai

TxPipe 58 Dec 2, 2022
Turn your discord cache back to viewable images.

discache ?? Every time you view an Image in Discord, it gets saved in your cache folder as an unviewable file. Discache allows you to convert those fi

sam 2 Dec 14, 2022
Non-volatile, distributed file cache backed by content-addressed storage

blobnet A low-latency file server that responds to requests for chunks of file data. This acts as a non-volatile, over-the-network content cache. Inte

Modal Labs 12 Dec 31, 2022
Key-value cache RESP server with support for key expirations ⌛

BADER-DB (بادِر) Key-value cache RESP server with support for key expirations ⌛ Supported Features • Getting Started • Basic Usage • Cache Eviction •

Mahmoud Salem 7 Apr 21, 2023
Attribute-Level Caching in Heterogeneous In-Memory DBMS

Alchemy Attribute-Level Caching in Heterogeneous In-Memory DBMS Alchemy is a DRAM-PM hybrid database engine built from scratch to achieve high perform

Xiangpeng Hao 14 Jun 20, 2022
Rust binary memcached implementation

bmemcached-rs Rust binary memcached implementation (ON GOING) Usage extern crate bmemcached; use std::sync::Arc; use std::thread; use bmemcached::Me

Jayson Reis 25 Jun 15, 2022
This is a Rust implementation for popular caches (support no_std).

Caches This is a Rust implementation for popular caches (support no_std). See Introduction, Installation and Usages for more details. English | 简体中文 I

Al Liu 83 Dec 11, 2022
memcache client for rust

rust-memcache rust-memcache is a memcached client written in pure rust. Install The crate is called memcache and you can depend on it via cargo: [depe

An Long 104 Dec 23, 2022
A proxy implement with http / socks5 in-bound and vmess out-bound, written in Rust and tokio.rs

tokio-vmess an Asynchronous proxy implement with http / socks5 in-bound and vmess out-bound, written in Rust and tokio Run example first, Fill out the

irumeria 7 Oct 3, 2022
Easy c̵̰͠r̵̛̠ö̴̪s̶̩̒s̵̭̀-t̶̲͝h̶̯̚r̵̺͐e̷̖̽ḁ̴̍d̶̖̔ ȓ̵͙ė̶͎ḟ̴͙e̸̖͛r̶̖͗ë̶̱́ṉ̵̒ĉ̷̥e̷͚̍ s̷̹͌h̷̲̉a̵̭͋r̷̫̊ḭ̵̊n̷̬͂g̵̦̃ f̶̻̊ơ̵̜ṟ̸̈́ R̵̞̋ù̵̺s̷̖̅ţ̸͗!̸̼͋

Rust S̵̓i̸̓n̵̉ I̴n̴f̶e̸r̵n̷a̴l mutability! Howdy, friendly Rust developer! Ever had a value get m̵̯̅ð̶͊v̴̮̾ê̴̼͘d away right under your nose just when

null 294 Dec 23, 2022