stretto

repository·main·Indexed 19 days ago

https://github.com/al8n/stretto

A high-performance, thread-safe, memory-bound cache implemented in pure Rust. A port of the Ristretto cache, stretto is optimized for database workloads and high-throughput scenarios using TinyLFU for admission and SampledLFU for eviction. It supports both synchronous (Cache) and runtime-agnostic asynchronous (AsyncCache) usage, featuring cost-based eviction and internal mutability to avoid wrapping in Arc<RwLock<...>>.

Tokens
7.9K
Snippets
26
Records
33
Agent score
61%

What's inside stretto

  1. Overview of Stretto features

    main

    Stretto is a high-performance, thread-safe, memory-bound Rust cache. It is a pure Rust implementation of the Ristretto cache.

    Key features include:

    • Internal Mutability: You can use Cache<...> or AsyncCache<...> directly in concurrent code without needing to wrap them in Arc<RwLock<...>>.
    • Sync and Async Support: Supports both synchronous and runtime-agnostic asynchronous usage.
    • High Hit Ratios: Uses a combination of TinyLFU for admission and SampledLFU for eviction to achieve best-in-class performance.
    • Cost-Based Eviction: Allows for eviction based on arbitrary costs, enabling large items to evict multiple smaller items.
    • Fully Concurrent: Designed for high throughput with minimal contention across many threads.
    • Store Policy: Stretto stores only the value, not the key.
    • Metrics: Provides optional performance metrics for throughput, hit ratios, and other statistics.
  2. When to use Stretto vs other caches

    main

    Stretto is optimized for specific access patterns using a TinyLFU admission policy. Choosing the right cache depends on your workload's relationship between working set size and cache capacity, as well as frequency skew.

    Use Stretto when:

    • Capacity is small relative to the working set (typically $\le \sim 25%$).
    • Access frequencies are skewed (e.g., Zipf-like distributions where certain keys are much 'hotter' than others).
    • Example workloads: OLTP, P1–P13 (Workstation traces), and S3 traces at lower capacities.

    Use Moka or QuickCache when:

    • The cache is sized to hold most of the data (capacity $\approx$ working set).
    • Traffic consists of bursty scans or has weak frequency skew (e.g., DS1 database traces or search-engine traces like S1/S2 at high capacities).
    • The workload is scan-heavy or has a wide, churning keyspace.
  3. Compare Stretto Sync vs Async performance

    main

    Stretto provides both a synchronous Cache and an asynchronous AsyncCache. Since v0.9.0, both use the same TinyLFU policy and 64-stripe insert buffer.

    Key Differences:

    • Hit Ratio: On workloads that fit Stretto's ideal regime (e.g., OLTP, P-series, S3 at low capacity), the hit ratios of Cache and AsyncCache track within $\sim 1$ percentage point. They are functionally identical in terms of admission decisions.
    • Scan/Burst Workloads: In scenarios where the producer-side insert channel saturates (scan-heavy or bursty traffic), the hit ratios may diverge by 5–20 points due to different drop-on-overflow timing mechanisms (crossbeam_channel::bounded for sync vs async_channel for async).
    • Recommendation: Use AsyncCache for runtime ergonomics. The hit ratio is not a significant tradeoff for workloads that match Stretto's intended design.
  4. Reproduce Stretto benchmarks

    main

    To reproduce the benchmark results shown in the documentation, you can use the cachebench repository. This requires cloning the repo recursively and running the benchmark command with the appropriate features enabled.

    Prerequisites:

    • git (with recursive support)
    • zstd for decompressing trace files
    • cargo (Rust toolchain)

    Steps:

    1. Clone the repository: git clone --recursive https://github.com/al8n/cachebench.git
    2. Decompress the traces: cd cachebench/cache-trace/arc && for f in *.lis.zst; do zstd -d "$f"; done && cd ../..
    3. Run the benchmark command provided below.
    git clone --recursive https://github.com/al8n/cachebench.git
    cd cachebench/cache-trace/arc && for f in *.lis.zst; do zstd -d "$f"; done && cd ../..
    cargo run --release --features "stretto,quick_cache,tiny-ufo,moka-v012" -- \
        -f oltp,p1,p2,p3,p4,p5,p6,p7,p8,p9,p10,p11,p12,p13,p14,s1,s2,s3,ds1,concat,merge-p,merge-s -n 16
  5. Install Stretto

    main

    Add stretto to your Cargo.toml dependencies. You can use the standard synchronous Cache or an AsyncCache with specific async runtimes like tokio or smol via feature flags.

    # Standard synchronous Cache
    [dependencies]
    stretto = "0.9"
    
    # AsyncCache with tokio
    [dependencies]
    stretto = { version = "0.9", features = ["tokio"] }
    
    # AsyncCache with smol
    [dependencies]
    stretto = { version = "0.9", features = ["smol"] }
  6. Configure Cache using CacheBuilder

    main

    Use the CacheBuilder struct to customize cache settings when creating a new Cache instance. Key configuration options include:

    • num_counters: Number of 4-bit access counters for admission/eviction. A good rule of thumb is 10x the expected number of items in a full cache.
    • max_cost: Defines the eviction threshold. Can represent a count of items or a byte budget.
    • ignore_internal_cost: If true (default), max_cost tracks only user-supplied costs. If false, it accounts for ~56 bytes of per-entry bookkeeping (key, conflict, version, etc.).
    • metrics: Enable for real-time stats (note: carries a ~10% throughput overhead).
    • cleanup_duration: Frequency of expired value cleanup (default is 500ms).
    • buffer_size: Controlled via set_insert_stripe_high_water(items). Tuning this affects how many items are batched before being sent to the policy processor. Values between 64–256 are recommended for caches under 10K capacity.
  7. Use ExpirationMap to manage TTL-based entries

    main

    The ExpirationMap is a specialized structure used to track keys and their associated conflict values (typically used for cache management) organized by their expiration time. It uses a bucketed approach where entries are grouped into time-based buckets to allow efficient bulk cleanup.

    Note: ExpirationMap is marked as pub(crate), meaning it is intended for internal use within the stretto crate. If you are consuming the library as a dependency, you will likely interact with these capabilities through a higher-level Cache or ShardedMap API rather than directly.

  8. Implement the KeyBuilder trait

    main

    Stretto does not store the actual key; it uses a KeyBuilder to process keys into hashes.

    • TransparentKeyBuilder: Use this if your key implements the TransparentKey trait (faster).
    • DefaultKeyBuilder: Use this for all other keys.
    • Custom Implementation: Implement the KeyBuilder trait to define your own hashing logic. If you want 128-bit hashes, implement hash_conflict and use the full (u64, u64) return from build_key. Otherwise, return 0 for hash_conflict to behave like a 64-bit hash.
    pub trait KeyBuilder {
        type Key: Hash + Eq + ?Sized;
    
        /// hash_index is used to hash the key to u64
        fn hash_index<Q>(&self, key: &Q) -> u64
            where 
                Self::Key: core::borrow::Borrow<Q>,
                Q: Hash + Eq + ?Sized;
    
        /// if you want a 128bit hashes, you should implement this method,
        /// or leave this method return 0
        fn hash_conflict<Q>(&self, key: &Q) -> u64
            where 
                Self::Key: core::borrow::Borrow<Q>,
                Q: Hash + Eq + ?Sized
        {
            0
        }
    
        /// build the key to 128bit hashes.
        fn build_key<Q>(&self, k: &Q) -> (u64, u64) 
            where 
                Self::Key: core::borrow::Borrow<Q>,
                Q: Hash + Eq + ?Sized
        {
            (self.hash_index(k), self.hash_conflict(k))
        }
    }
  9. Implement the UpdateValidator trait

    main

    The UpdateValidator trait allows you to control whether an existing value in the cache should be updated when a new value for the same key is inserted. By default, the cache always updates.

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

    main

    The Coster trait allows you to evaluate the cost of an item at runtime. This is efficient because the cost function is only run for items that are actually accepted into the cache.

    To use a Coster:

    1. Set the coster field in your CacheBuilder to your implementation.
    2. When calling insert, pass a cost of 0 for new items or updates. Stretto will then use the Coster to calculate the actual cost.
    pub trait Coster: Send + Sync + 'static {
        type Value: Send + Sync + 'static;
    
        /// cost evaluates a value and outputs a corresponding cost.
        fn cost(&self, val: &Self::Value) -> i64;
    }
  11. Implement the CacheCallback trait

    main

    Use CacheCallback to perform custom operations when cache events occur. This is useful for manual memory deallocation or logging.

    • on_exit: Called whenever a value is removed (eviction, rejection, or manual removal).
    • on_evict: Called specifically for evictions; passes the Item<Self::Value>.
    • on_reject: Called when the policy rejects an insertion.
    pub trait CacheCallback: Send + Sync + 'static {
        type Value: Send + Sync + 'static;
    
        /// on_exit is called whenever a value is removed from cache.
        fn on_exit(&self, val: Option<Self::Value>);
    
        /// 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::Value>) {
            self.on_exit(item.val)
        }
    
        /// on_reject is called for every rejection done via the policy.
        fn on_reject(&self, item: Item<Self::Value>) {
            self.on_exit(item.val)
        }
    }
  12. Use AsyncCache for asynchronous caching

    main

    When the async feature is enabled, use AsyncCache to manage cached items in an asynchronous environment. You can build a cache using AsyncCacheBuilder.

    For convenience, if both async and tokio features are enabled, you can use the TokioCache type alias. Similarly, if async and smol are enabled, you can use SmolCache.

    // Example usage of TokioCache (requires 'async' and 'tokio' features)
    // TokioCache<K, V, ...> is an alias for AsyncCache<K, V, TokioRuntime, ...>