Skip to content

About

Unique Value Cache. An LRU cache with de-duplicated values.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Latest commit

 

History

8 Commits

Folders and files

Repository files navigation

dedup-cache

dedup-cache

Concurrent in-memory cache with per-key TTL and value deduplication — identical payloads share one Arc<V> across keys.

Built for workloads where memory matters: large or repeated values, many keys pointing at the same blob. One dependency (parking_lot).

When to use it

  • Large values — configs, JSON blobs, file contents, model weights, serialized structs
  • High duplicate rate — many keys referencing the same payload (aliases, content-addressed keys, fan-out caches)
  • Per-key TTL — session tokens, cached API responses with mixed lifetimes
  • Concurrent access — sharded reads/writes without pulling in a heavier cache stack

When not to use it

  • Tiny or cheap values — u64, small enums, Copy types. Interning adds Arc overhead, hashing, and pool bookkeeping that costs more than storing inline.
  • Mostly unique values — if every key owns a distinct payload, dedup never fires and you pay for the intern pool for nothing. Prefer moka or quick_cache. If you are not sure which side of this line your workload falls on, the dedup monitor measures it and will tell you.
  • Exact LRU or advanced admission — eviction is sampled approximate-LRU; no W-TinyLFU-style policy yet.
  • Proactive expiry below capacity — expired entries are removed on get and during at-capacity inserts, not by a background sweeper.

See docs/architecture.md for internals.

Examples

Basic insert, TTL, and dedup

use std::sync::Arc;
use std::time::Duration;
use dedup_cache::{Cache, CacheConfig};

let cache = Cache::new(CacheConfig::new(10_000).with_num_shards(16));

cache.insert_with_ttl("session:abc".into(), r#"{"user":1}"#.into(), Some(Duration::from_secs(300)));
cache.insert("config:theme".into(), "dark".into());

// Same payload under two keys → one Arc in memory
let blob = r#"{"shared":true}"#.to_string();
cache.insert("alias:a".into(), blob.clone());
cache.insert("alias:b".into(), blob);
assert!(Arc::ptr_eq(
    &cache.get(&"alias:a".to_string()).unwrap(),
    &cache.get(&"alias:b".to_string()).unwrap(),
));

Default TTL with per-key override

use std::time::Duration;
use dedup_cache::{Cache, CacheConfig};

let cache = Cache::new(
    CacheConfig::new(10_000)
        .with_num_shards(16)
        .with_default_ttl(Duration::from_secs(60)),
);

cache.insert("user:1".into(), r#"{"name":"Ada"}"#.to_string());
cache.insert_with_ttl("token".into(), "secret-token".to_string(), Some(Duration::from_secs(5)));

Concurrent use

use std::sync::Arc;
use std::thread;
use dedup_cache::{Cache, CacheConfig};

let cache = Arc::new(Cache::new(CacheConfig::new(50_000).with_num_shards(64)));

thread::spawn({
    let cache = Arc::clone(&cache);
    move || cache.insert("key".into(), "computed-payload".to_string())
});

if let Some(v) = cache.get(&"key".to_string()) {
    // shared Arc — cheap to clone, safe across threads
}

Is dedup paying off?

Deduplication only helps when values are both large and shared. An opt-in monitor measures whether that is true for your workload — and nets out what the intern pool itself costs, so the answer is in bytes rather than a ratio that cannot tell 20-byte values from 2 MB blobs.

use std::time::Duration;
use dedup_cache::{Cache, CacheConfig, DedupMonitor, DedupVerdict};

let cache: Cache<String, String> = Cache::with_monitor(
    CacheConfig::new(100_000),
    DedupMonitor::new(|value: &String| value.len())
        // Warn at most once an hour, checked every 65,536 inserts.
        .with_check_every_inserts(65_536)
        .with_min_interval(Duration::from_secs(3_600))
        .with_sink(|report| eprintln!("{}", report.advice().unwrap_or_default())),
);

cache.insert("alias:a".into(), "payload".to_string());

// Or pull the numbers whenever you like, warning or not.
let report = cache.dedup_report();
match report.verdict {
    DedupVerdict::Counterproductive { net_cost_bytes } => {
        // The intern pool costs more than the duplication it removes.
    }
    DedupVerdict::Effective { net_saved_bytes } => {}
    _ => {}
}

Without a monitor there is no byte accounting and no weigher is ever called; dedup_report() still returns exact entry and distinct-value counts, and stats().dedup_ratio() is always free. Enabling the monitor costs about 30 ns per insert for small values and is lost in the noise for large ones. Set .with_check_every_inserts(0) for pull-only reporting with no background work at all, and enable the log-warnings feature plus .with_log_sink() to route warnings through the log crate.

Run the examples

cargo run --example basic
cargo run --example concurrent_session_cache --release
cargo run --example dedup_monitor --release

dedup_monitor runs three workloads through the same cache — shared 4 KiB blobs, unique 24-byte values, and small values shared 2:1 — and prints what the monitor concludes about each.

License

Licensed under the MIT License.

About

Unique Value Cache. An LRU cache with de-duplicated values.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages