A cache for MoonBit: bounded by count or weight, with expiry, loaders and eviction listeners.
let cache = @moonkart.tinylfu_cache(cap=1000)
cache.set("answer", 42)
assert_eq(cache.get("answer"), Some(42))
assert_eq(cache.get("unknown"), None)moon add moonbitstack/moonkart// moon.pkg
import {
"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)
by_count.set("a", 1)
assert_eq(by_count.size(), 1L)// At most 12 units of weight, where a value weighs one per character.
let by_weight = @moonkart.lru_cache(
max_weight=12L,
weigher=fn(_key : String, value : String) { value.length().to_int64() },
)
by_weight.set("foo", "123456") // 6
by_weight.set("emm", "123456") // 6 + 6 = 12
assert_eq(by_weight.size(), 2L)
by_weight.set("bar", "123456") // 18 > 12, so the oldest entry leaves
assert_eq(by_weight.contains("foo"), false)
assert_eq(by_weight.size(), 2L)// Time to live from the moment of writing; reads do not extend it.
let now : @ref.Ref[Int64] = @ref.Ref(0L)
let ttl_cache = @moonkart.lru_cache(
cap=100,
expiry=@moonkart.ttl(10L),
clock=fn() { now.val },
)
ttl_cache.set("user:7", "Vega")
assert_eq(ttl_cache.contains("user:7"), true)
now.val = 11L
assert_eq(ttl_cache.contains("user:7"), false)// Time to idle: every read restarts the countdown.
let idle_now : @ref.Ref[Int64] = @ref.Ref(0L)
let tti_cache = @moonkart.lru_cache(
cap=100,
expiry=@moonkart.tti(10L),
clock=fn() { idle_now.val },
)
tti_cache.set("session", "open")
idle_now.val = 6L
assert_eq(tti_cache.get("session"), Some("open")) // read at 6, so it lives to 16
idle_now.val = 15L
assert_eq(tti_cache.contains("session"), true)
idle_now.val = 17L
assert_eq(tti_cache.contains("session"), false)let fixed_now : @ref.Ref[Int64] = @ref.Ref(0L)
let mixed = @moonkart.lru_cache(
cap=100,
expiry=@moonkart.ttl(1_000L),
clock=fn() { fixed_now.val },
)
mixed.set("short", "lived", expire_after=10L) // ten nanoseconds from now
mixed.set("long", "lived") // the cache's rule
assert_eq(mixed.entry("short").expire_at, Some(10L))
assert_eq(mixed.entry("long").expire_at, Some(1_000L))
fixed_now.val = 11L
assert_eq(mixed.contains("short"), false)
assert_eq(mixed.contains("long"), true)let users = @moonkart.lru_cache(cap=64)
let loads : @ref.Ref[Int] = @ref.Ref(0)
let fetch_user = fn(id : Int) raise {
loads.val = loads.val + 1
if id < 0 {
fail("no such user")
}
"user-\{id}"
}
assert_eq(users.get_with(7, fetch_user), "user-7")
assert_eq(users.get_with(7, fetch_user), "user-7")
assert_eq(loads.val, 1) // the second call was a plain hitlet failed = users.get_with(-1, fetch_user) catch { _ => "none" }
assert_eq(failed, "none")
assert_eq(users.contains(-1), false) // a failed load leaves nothing behindlet parsed = users.try_get_with(
12,
@moonkart.infallible_loader(fn(id : Int) {
if id % 2 == 0 {
Some("user-\{id}")
} else {
None
}
}),
)
assert_eq(parsed, Some("user-12"))
assert_eq(users.size(), 2L)#kart.cached(capacity="1024", policy="lru", ttl="60s")
#kart.count_calls
pub fn lookup(id : Int) -> String {
"item-\{id}"
}#kart.once
pub fn banner() -> String {
"kart no. 128"
}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" ])// moon.pkg
import {
"moonbitstack/moonkart",
"moonbitstack/moonkart/concurrent",
}let async_cache = @concurrent.AsyncCache::new(@moonkart.lru_cache(cap=64))
let value = async_cache.get_with(1, id => "user-\{id}")
assert_eq(value, "user-1")
assert_eq(async_cache.get(1), Some("user-1"))let sharded = @concurrent.ShardedCache::new(
shards=4,
make=fn(_index) { @moonkart.lru_cache(cap=256L) },
)
sharded.set("user:1", "Leo")
assert_eq(sharded.get("user:1"), Some("Leo"))
assert_eq(sharded.shards(), 4)
assert_eq(sharded.stats().hit_count + sharded.stats().miss_count, 1L)// moon.pkg
import {
"moonbitstack/moonkart",
"moonbitstack/moonkart/disk",
}async fn persist(path : String) -> Unit raise {
let disk = @disk.DiskCache::open(
@moonkart.lru_cache(cap=10_000),
path,
fn(key : String) { @utf8.encode(key) },
fn(bytes) { Some(@utf8.decode_lossy(bytes)) },
fn(value : String) { @utf8.encode(value) },
fn(bytes) { Some(@utf8.decode_lossy(bytes)) },
max_bytes=64L * 1024L * 1024L, // at most 64 MiB of live records
)
disk.set("greeting", "hello") // memory, then an appended record
assert_eq(disk.get("greeting"), Some("hello"))
assert_eq(disk.stats().entries, 1L)
disk.compact() // rewrite the file from the live records
}let c = @moonkart.lru_cache(cap=8)
c.set("a", 1)
c.set("b", 2)
assert_eq(c.invalidate("a"), true) // one key, returns whether it was present
assert_eq(c.invalidate_if(fn(_k, value) { value > 1 }), 1L) // by predicate, returns how many
c.invalidate_all() // everything
assert_eq(c.size(), 0L)let gone : Array[String] = []
let tracked = @moonkart.lru_cache(
cap=2,
removal_listener=fn(key : Int, _value : String, reason) {
let why = match reason {
Capacity => "capacity"
Expired => "expired"
Explicit => "explicit"
Replaced => "replaced"
}
gone.push("\{key}:\{why}")
},
)
tracked.set(1, "a")
tracked.set(2, "b")
tracked.set(3, "c")
assert_eq(gone, ["1:capacity"])let counted = @moonkart.lru_cache(cap=4)
counted.set(1, 1)
ignore(counted.get(1))
ignore(counted.get(2))
let s = counted.stats()
assert_eq(s.hit_count, 1L)
assert_eq(s.miss_count, 1L)
assert_eq(s.hit_rate(), 0.5)pub struct Fifo {
order : Array[Int64]
cap : Int64
}
impl @policy.Policy for Fifo with fn hit(
self : Fifo,
_id : Int64,
_hash : Int32,
) -> Unit {
ignore(self)
}
impl @policy.Policy for Fifo with fn admit(
self : Fifo,
id : Int64,
_weight : Int64,
_hash : Int32,
) -> Array[Int64] {
self.order.push(id)
if self.order.length().to_int64() > self.cap {
[self.order.remove(0)]
} else {
[]
}
}
impl @policy.Policy for Fifo with fn forget(self : Fifo, id : Int64) -> Unit {
for i, v in self.order {
if v == id {
ignore(self.order.remove(i))
break
}
}
}
impl @policy.Policy for Fifo with fn size(self : Fifo) -> Int32 {
self.order.length()
}let fifo_policy : Fifo = { order: [], cap: 2L }
let fifo = @moonkart.new(fifo_policy)
fifo.set(1, "a")
fifo.set(2, "b")
fifo.set(3, "c")
assert_eq(fifo.contains(1), false)
assert_eq(fifo.size(), 2L)| 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 |
| set(key, value, expire_after=ns) | insert or replace, with a deadline for this write only |
| 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 |
| to_array() | every live entry as (key, value) |
| stats(), size(), policy_size() | counters and sizes |
| CacheStats::merge(a, b) | two sets of counters added together |
type 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(all) 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(all) 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.