A cache for MoonBit: bounded by count or weight, with expiry, loaders and eviction listeners.
let cache = @moonkart.lru_cache(cap=1000)
cache.set("answer", 42)
match cache.get("answer") {
Some(value) => println("hit: \{value}")
None => println("miss")
}moon add moonbitstack/moonkartimport {
"moonbitstack/moonkart",
}| Constructor | Policy | Good at |
|---|---|---|
| tinylfu_cache | W-TinyLFU (frequency + recency) | general workloads; keeps frequently used entries |
| lru_cache | least recently used | simple, predictable recency |
| s3fifo_cache | second-chance FIFO | scan floods and one-hit wonders |
| Trace | Capacity | lru_cache | s3fifo_cache | tinylfu_cache |
|---|---|---|---|---|
| OLTP (914,145 requests) | 1,869 | 0.4164 | 0.4219 | 0.4269 |
| DS1 (2,505,947 requests) | 101,764 | 0.1087 | 0.1109 | 0.2411 |
| S3 (16.4 million requests) | 524,288 | 0.2526 | n/a | 0.4640 |
// At most 500 entries.
let by_count = @moonkart.lru_cache(cap=500)
// At most 10 MiB, where the weight is the byte length of the value.
let by_weight = @moonkart.lru_cache(
max_weight=10L * 1024L * 1024L,
weigher=fn(_key, bytes) { bytes.length().to_int64() },
)// Time-to-live from the moment of writing; reads do not extend it.
let ttl_cache = @moonkart.lru_cache(cap=1000, expiry=@moonkart.ttl(60_000_000_000L))
// Time-to-idle: every read restarts the countdown.
let tti_cache = @moonkart.lru_cache(cap=1000, expiry=@moonkart.tti(30_000_000_000L))let now : @ref.Ref[Int64] = @ref.Ref(0L)
let cache = @moonkart.lru_cache(cap=10, expiry=@moonkart.ttl(1_000_000_000L), clock=fn() { now.val })let user = cache.get_with(user_id, fn(id) raise { fetch_user(id) })let parsed = cache.get_with(text, @moonkart.infallible_loader(fn(t) { parse(t) }))#kart.cached(capacity="1024", policy="lru", ttl="60s")
pub fn lookup(id : Int) -> String {
expensive(id)
}rule(name: "cached", command: "moonx moonbitstack/moonkart/cmd/cached -C \"$mod_dir\" \"$input\" -o \"$output\"")dev_build(rule: "cached", input: "slow.mbt", output: "slow.g.mbt")
formatter(ignore: [ "slow.g.mbt" ])import {
"moonbitstack/moonkart",
"moonbitstack/moonkart/concurrent",
}let cache = @concurrent.AsyncCache::new(@moonkart.lru_cache(cap=1000))
let user = cache.get_with(id, async fn(id) { fetch_user(id) })import {
"moonbitstack/moonkart",
"moonbitstack/moonkart/disk",
}let cache = @moonkart.lru_cache(cap=10_000)
let disk = @disk.DiskCache::open(
cache, "/var/cache/app.bin",
encode_key, decode_key, encode_value, decode_value,
)
disk.set(key, value) // memory, then an appended record
disk.get(key) // memory only
disk.compact() // rewrite the log from the live entriescache.invalidate(key) // one key, returns whether it was present
cache.invalidate_all() // everything
cache.invalidate_if(fn(k, v) { v.is_stale() }) // by predicate, returns how manylet cache = @moonkart.lru_cache(
cap=1000,
removal_listener=fn(key, _value, reason) {
println("\{key} left: \{reason}")
},
)let cache = @moonkart.new(@my_policy.Policy::new())| Item | Meaning |
|---|---|
| lru_cache, s3fifo_cache, tinylfu_cache | constructors for the shipped policies |
| new(policy, ...) | construct with any policy |
| new_with_config(policy, config) | construct from a Config bundle |
| default_config() | defaults for every optional knob |
| ttl(ns), tti(ns) | expiry shorthands |
| system_clock | the default clock, from the runtime |
| set(key, value) | insert or replace |
| get(key) | read, or None |
| contains(key) | presence; no hit, no idle refresh |
| entry(key), entry_or_null(key) | snapshot of an entry |
| remove(key), invalidate(key) | remove one, returns whether it was present |
| invalidate_all(), invalidate_if(pred) | remove many |
| get_with(key, loader), try_get_with(key, loader) | load on miss; the loader may raise |
| infallible_loader(loader) | adapt a loader that cannot fail |
| stats(), size(), policy_size() | counters and sizes |
| Before | After |
|---|---|
| Cache[K, V, P] | Cache[K, V]; the policy is chosen at construction and erased |
| cache[Int, String, @policy.Lru]() | @moonkart.lru_cache(cap=...) |
| get_with(key, fn(k) { ... }) | get_with(key, fn(k) raise { ... }) |
| a loader that cannot fail | wrap it: infallible_loader(fn(k) { ... }) |
| @policy.Policy::hit(id) / admit(id, w) | hit(id, hash) / admit(id, w, hash) |
moon run src/examples/tour
moon run src/examples/01-expiry
moon run src/examples/02-loadingmoon clean && moon fmt && moon check --target all --deny-warn \
&& moon build --target all && moon test --target alltype Clock = () -> Int64type Weigher[K, V] = (K, V) -> Int64pub struct Cache[K, V] {
// private fields
}pub(all) struct CacheStats {
hit_count : Int64
miss_count : Int64
eviction_count : Int64
eviction_weight : Int64
load_success_count : Int64
load_failure_count : Int64
total_load_time : Int64
}pub struct Config[K, V] {
weigher : (K, V) -> Int64?
clock : () -> Int64
expiry : Expiry[K, V]?
removal_listener : (K, V, EvictionReason) -> Unit?
}pub(all) struct Entry[K, V] {
key : K
value : V
weight : Int64
expire_at : Int64?
}pub(all) enum EvictionReason {
Capacity
Expired
Explicit
Replaced
}pub struct Expiry[K, V] {
after_create : (K, V, Int64) -> Int64?
after_read : (K, V, Int64, Int64?) -> Int64?
after_update : (K, V, Int64, Int64?) -> Int64?
}fn[K, V] infallible_loader(loader : (K) -> V) -> ((K) -> V raise)fn[K : Eq + Hash, V] lru_cache(cap? : Int64, max_weight? : Int64, weigher? : (K, V) -> Int64, clock? : () -> Int64, expiry? : Expiry[K, V], removal_listener? : (K, V, EvictionReason) -> Unit) -> Cache[K, V]fn[K : Eq + Hash, V] s3fifo_cache(cap? : Int64, max_weight? : Int64, weigher? : (K, V) -> Int64, clock? : () -> Int64, expiry? : Expiry[K, V], removal_listener? : (K, V, EvictionReason) -> Unit) -> Cache[K, V]fn system_clock() -> Int64fn[K : Eq + Hash, V] tinylfu_cache(cap? : Int64, max_weight? : Int64, window_ratio? : Double, protected_ratio? : Double, weigher? : (K, V) -> Int64, clock? : () -> Int64, expiry? : Expiry[K, V], removal_listener? : (K, V, EvictionReason) -> Unit) -> Cache[K, V]Install
Download zipA cache for MoonBit: bounded by count or weight, with expiry, loaders and eviction listeners.