Memory cache
An in-memory cache shared by all requests, with a lifetime per entry, a size limit that drops the least used first, counters for rate limits, and hit statistics.
Version 1.0.0 · 6 functions · 1 crate
Functions
What use memory_cache.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use memcache_set, memcache_get from memory_cache (Quillo 0.6).
memcache_set(key: str, value: any, ttl_seconds: int) → nullor an errorKeeps value (any value) at key for ttl_seconds seconds (0: until it is pushed out for room). The whole cache holds 64 MB by default, MEMORY_CACHE_MB to change it; one value may be up to 16 MB of JSON.
memcache_get(key: str) → any?The value at key, or null when there is none or it has expired.
memcache_delete(key: str) → boolTakes key out of the cache; whether it was there.
memcache_incr(key: str, by: int, ttl_seconds: int) → intAdds by to the number at key (0 when there is none or it is not a number) and gives the new number; a new key lives ttl_seconds, and adding does not extend it. For rate limits: "at most 100 requests a minute from this IP".
memcache_stats() → anyHow the cache is doing: { entries, bytes, max_bytes, hits, misses } since the server started.
memcache_clear() → nullEmpties the cache.
Get it
$ quillo install memory_cache # memory_cache.qwer into this folder$ quillo install memory_cache --example # and its example, memory_cache.qweOr with curl:
$ curl -O https://quillolang.org/plugins/files/memory_cache.qwer$ curl -O https://quillolang.org/plugins/files/memory_cache.qweThen, in your program, all of it or only what you call:
use memory_cache.qweruse memcache_set, memcache_get from memory_cache // Quillo 0.6: only theseIt uses this crate, which cargo fetches the first time the plugin is built:
moka = { version = "0.12", features = ["sync"] }Example
// A slow report kept in memory for 30 seconds, and a rate limit of 5 requests a minute per visitor.//// quillo run memory_cache.qwe then open http://localhost:8080/reportuse "memory_cache.qwer"fn slow_report() { sleep(200) { total: 4521, made_at: now() }}route GET /report { let kept = memcache_get("report") if kept != null { return { from: "cache", report: kept } } let fresh = slow_report() memcache_set("report", fresh, 30)? { from: "database", report: fresh }}route GET /limited { let n = memcache_incr(`rate:${request.ip}`, 1, 60) if n > 5 { reply 429 { error: "Slow down: 5 requests a minute." } } { ok: true, used: n }}route GET /demo { memcache_set("short", "gone soon", 1)? memcache_set("user:7", { name: "Ada", roles: ["admin"] }, 0)? let before = memcache_get("short") sleep(1100) { before, after: memcache_get("short"), user: memcache_get("user:7"), counter: [memcache_incr("c", 1, 0), memcache_incr("c", 2, 0)], stats: memcache_stats() }}$ quillo run memory_cache.qwe$ curl 'localhost:8080/report'$ curl 'localhost:8080/demo'Source
//! An in-memory cache shared by all requests, with a lifetime per entry and a size limit: slow lookups, API answers and rendered fragments kept for a while, the least used dropped first when it is full. No Redis needed for one server.//! version: 1.0.0//!//! [dependencies]//! moka = { version = "0.12", features = ["sync"] }use moka::sync::Cache;use moka::Expiry;use serde_json::{json, Value};use std::sync::atomic::{AtomicU64, Ordering};use std::sync::{Arc, Mutex, OnceLock};use std::time::{Duration, Instant};#[derive(Clone)]struct Entry { value: Arc<Value>, /// When the entry ends, if ever: kept as a moment, so changing the value keeps it. deadline: Option<Instant>, bytes: u32,}struct PerEntry;impl Expiry<String, Entry> for PerEntry { fn expire_after_create(&self, _k: &String, v: &Entry, at: Instant) -> Option<Duration> { v.deadline.map(|d| d.saturating_duration_since(at)) } fn expire_after_update(&self, _k: &String, v: &Entry, at: Instant, _left: Option<Duration>) -> Option<Duration> { v.deadline.map(|d| d.saturating_duration_since(at)) }}fn max_bytes() -> u64 { std::env::var("MEMORY_CACHE_MB").ok().and_then(|m| m.parse::<u64>().ok()).unwrap_or(64).clamp(1, 16_384) * 1024 * 1024}fn cache() -> &'static Cache<String, Entry> { static C: OnceLock<Cache<String, Entry>> = OnceLock::new(); C.get_or_init(|| { Cache::builder() .max_capacity(max_bytes()) .weigher(|k: &String, v: &Entry| (k.len() as u32).saturating_add(v.bytes).saturating_add(64)) .expire_after(PerEntry) .build() })}static HITS: AtomicU64 = AtomicU64::new(0);static MISSES: AtomicU64 = AtomicU64::new(0);static INCR: Mutex<()> = Mutex::new(());fn deadline(seconds: i64) -> Option<Instant> { if seconds > 0 { Instant::now().checked_add(Duration::from_secs(seconds.min(1_000_000_000) as u64)) } else { None }}/// Keeps `value` (any value) at `key` for `ttl_seconds` seconds (0: until it is pushed out for room). The whole cache holds 64 MB by default, `MEMORY_CACHE_MB` to change it; one value may be up to 16 MB of JSON.pub fn memcache_set(key: &str, value: serde_json::Value, ttl_seconds: i64) -> Result<(), String> { let bytes = serde_json::to_string(&value).map(|s| s.len()).unwrap_or(0); if bytes > 16 * 1024 * 1024 { return Err("values up to 16 MB".into()); } cache().insert(key.to_string(), Entry { value: Arc::new(value), deadline: deadline(ttl_seconds), bytes: bytes as u32 }); Ok(())}/// The value at `key`, or null when there is none or it has expired.pub fn memcache_get(key: &str) -> Option<Value> { match cache().get(key) { Some(e) => { HITS.fetch_add(1, Ordering::Relaxed); Some((*e.value).clone()) } None => { MISSES.fetch_add(1, Ordering::Relaxed); None } }}/// Takes `key` out of the cache; whether it was there.pub fn memcache_delete(key: &str) -> bool { cache().remove(key).is_some()}/// Adds `by` to the number at `key` (0 when there is none or it is not a number) and gives the new number; a new key lives `ttl_seconds`, and adding does not extend it. For rate limits: "at most 100 requests a minute from this IP".pub fn memcache_incr(key: &str, by: i64, ttl_seconds: i64) -> i64 { let _one_at_a_time = INCR.lock().unwrap_or_else(|e| e.into_inner()); let c = cache(); let e = match c.get(key) { Some(e) => Entry { value: Arc::new(json!(e.value.as_i64().unwrap_or(0).saturating_add(by))), ..e }, None => Entry { value: Arc::new(json!(by)), deadline: deadline(ttl_seconds), bytes: 20 }, }; let n = e.value.as_i64().unwrap_or(0); c.insert(key.to_string(), e); n}/// How the cache is doing: `{ entries, bytes, max_bytes, hits, misses }` since the server started.pub fn memcache_stats() -> Value { let c = cache(); c.run_pending_tasks(); json!({ "entries": c.entry_count(), "bytes": c.weighted_size(), "max_bytes": max_bytes(), "hits": HITS.load(Ordering::Relaxed), "misses": MISSES.load(Ordering::Relaxed) })}/// Empties the cache.pub fn memcache_clear() { cache().invalidate_all();}Related
trie.qwer · 1.0.0A prefix index for autocomplete kept in memory by name: entries with weights and data, the best completions of what was typed, at a word's start too.8 views · 11 downloadsConsistent hashing consistent_hash.qwer · 1.0.0Which server or shard a key goes to, stable as servers come and go: jump hashing for numbered shards, rendezvous hashing for named servers, with replicas and weights.7 views · 7 downloadsLeaderboards leaderboard.qwer · 1.0.0Leaderboards in memory shared by all requests: scores set, kept at their best or added to, a player's rank, the top N, and the players around one.7 views · 6 downloadsPercentiles quantiles.qwer · 1.0.0Streaming percentiles kept in memory by name: p50, p95 and p99 of response times or order values with a t-digest that stays small, and the rank of any value.7 views · 8 downloadsSketches sketches.qwer · 1.0.0Counting at scale in little memory, kept by name for all requests: unique visitors by HyperLogLog, merged across days, and top pages or search terms by a count-min sketch.7 views · 7 downloadsHTTP caching http_cache.qwer · 1.0.0HTTP caching as RFC 9111 says: Cache-Control read and written, ETags made, conditional requests answered 304, and a stored response's freshness, age and time to live in a browser or a CDN.7 views · 6 downloadsThread 0 posts
On Memory cache: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.