Data structures

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

#caching#performance#rate-limits#memory

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 error

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.

memcache_get(key: str) → any?

The value at key, or null when there is none or it has expired.

memcache_delete(key: str) → bool

Takes key out of the cache; whether it was there.

memcache_incr(key: str, by: int, ttl_seconds: int) → int

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".

memcache_stats() → any

How the cache is doing: { entries, bytes, max_bytes, hits, misses } since the server started.

memcache_clear() → null

Empties the cache.

Get it

$ quillo install memory_cache               # memory_cache.qwer into this folder$ quillo install memory_cache --example     # and its example, memory_cache.qwe

Or with curl:

$ curl -O https://quillolang.org/plugins/files/memory_cache.qwer$ curl -O https://quillolang.org/plugins/files/memory_cache.qwe

Then, in your program, all of it or only what you call:

main.qwe
use memory_cache.qweruse memcache_set, memcache_get from memory_cache      // Quillo 0.6: only these

It uses this crate, which cargo fetches the first time the plugin is built:

[dependencies]
moka = { version = "0.12", features = ["sync"] }

Example

memory_cache.qwe
// 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

memory_cache.qwer
//! 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();}
Autocomplete 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.searchautocompleteformsmemory8 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.cachingshardingdistributedscaling7 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.gamesrankingmemorygamification7 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.analyticsmonitoringreportsmemory7 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.analyticscountingmemoryreports7 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.cachinghttpheadersperformance7 views · 6 downloads
Thread
88Useful? Like it, or add to its thread below.

Thread 0 posts

On Memory cache: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.