Project: supercache
Module: github.com/Code0987/supercache
This document freezes product intent and the technical decisions for v1.
How to change this product (humans and agents) is normative below and detailed in docs/WORKFLOW.md and AGENTS.md. Do not invent a shorter path for data-plane work.
docs → review → revision → tests → coding → product docs → review → revision
→ bench (local) → revision → commit → PR → bench monitor (CI)
→ merge only if overall drift < 10% (and user says merge)
| Step | What | Gate |
|---|---|---|
| Docs | Design in docs/design/<yyyy-mm-dd>-<short-name>.md |
— |
| Review | Person reviews design | Explicit go-ahead: looks good / do it / implement |
| Revision | Update design from feedback | Re-approval if contract changed |
| Tests | Failing tests from the design table | — |
| Coding | Minimum code to pass go test ./... |
— |
| Product docs | Same PR: API.md, OpenAPI, PLAN, OPERATIONS, CLUSTER_FLOWS, README, sc help | Must match the new surface |
| Review | Check vs design / reviewer feedback | Issues fixed |
| Revision | Address code review | Tests still green |
| Bench (local) | Micros / smoke flagged in the design | No Get-hit alloc jump |
| Revision | Perf/correctness from local bench | Re-test + re-bench |
| Commit | Feature branch only — never main |
— |
| PR | gh pr create |
— |
| Bench monitor | gh pr checks <n> --watch + <!-- supercache-bench-comment --> |
CI test + bench green |
| Merge | User says merge (or merge-if-green) and gate passes | See below |
revision means fix feedback and re-stop at the next gate. Do not skip gates.
| Kind of change | Path |
|---|---|
| New product behavior (types, API, replication, consistency) | Full path above |
| Bug on Get / Put / Delete / store / fan-out / membership | Full path (design may be short) |
| Refactor, same contract | Short design note in PR; keep tests; PR + CI bench + 10% gate |
| Docs / comments / this workflow | Docs → commit → PR (no product bench required) |
One design → one PR. No stacking a second feature on the same branch. Never push or commit to main.
After a green bench job, read the sticky PR comment <!-- supercache-bench-comment --> (same runner, main vs PR).
| Check | Pass |
|---|---|
| Shared smoke Δ ops/s | each cell within ±10% |
| Shared micro Δ ns/op | each bench within ±10% |
| Get-hit / StoreGetHit allocs/op | unchanged (any increase = fail) |
| New micros only on PR | report only; not a fail by themselves |
Eligible to merge only if the table passes, product docs match the shipped surface (or the PR says no public change), and the user said merge (or clearly pre-authorized merge-if-green). CI green alone is not ship. If the user said merge but drift ≥10% or allocs jumped: do not merge; report numbers.
gh pr create
gh pr checks <n> --watch
# read <!-- supercache-bench-comment -->
# summarize; wait for merge
gh pr merge <n> --merge --delete-branch
Full template, stop rules, and resume checklist: docs/WORKFLOW.md. Agent short card: AGENTS.md. Bench how-to: docs/BENCHMARKS.md.
- Shared in-cluster cache for read-heavy runtime storage
- Thread-safe / multi-service-safe access via gRPC clients and an embeddable Engine (single-node / test)
- Automatic fetch on miss via pluggable
DataSource(reduce backend / DB load) in load-through keyspaces - Multi-node cluster for availability and aggregate read QPS (not for unbounded memory growth — see §2)
- Bounded memory with per-keyspace LRU (
MaxBytes) and TTL / optional negative TTL - Automatic node discovery and reaction to topology changes among cache nodes
- KeySpace isolation with runtime updates, warmup, hot-key tracking, and DataSource protection
- Structured keyspace types (in addition to opaque KV): Bloom filters, exact sets, sorted sets — same owner + RF fan-out model
- Ops surface: admin diagnostics, health probes, cluster events, OpenTelemetry, hosted OpenAPI
- Secure peer mesh: TLS, Client vs Peer isolation, optional gossip membership auth
- Linearizable or transactional workloads
- Quorum, fencing, or split-brain prevention
- Disk persistence / WAL / snapshots
- Multi-key atomicity or strict global write order (beyond per-entry LWW versioning)
- Full Redis protocol; streams, HyperLogLog, multi-key ZUNION/ZINTER
- CRDTs
- Memory capacity that scales linearly with node count under the v1 replication model
- Embedding every application replica as a ring peer (anti-pattern; see §4)
v1 is an eventually consistent, multi-replica cache:
| Concern | Behavior |
|---|---|
| Memory | Each node holds its own LRU-bounded working set. After fan-out, a key exists on R ring members (owner + successors), not every node. Cluster memory ≈ R × unique working set / N per node. |
| Owner | Consistent-hash coordinator for write ACK, preferred miss-load coalescing, and version allocation — not a sole storage shard. Ring owner of a name (KV key, Bloom/set/zset name). |
| Reads | Local hit if this node has a copy. CacheOnly miss forwards to the owner (replicas store the repair; non-replicas do not). Structured reads (BloomTest / SetContains / ZScore / range) local on replica, owner-forward otherwise. |
| Writes | Owner applies + assigns version → ACK → async fan-out to the other R−1 replicas (KV ApplyPut; structured types use item-level flags or bit OR — see §7 / §9.6). |
| Scale-out value | More nodes → more unique key capacity (at fixed R) and more read QPS; HA is R copies, not N. |
per_node_budget ≈ sum(keyspace.MaxBytes) + overhead (indexes, hot-key tracker, buffers)
hot_set_cluster ≈ size of keys that stay hot longer than fan-out + TTL dynamics
effective_hot_cost ≈ hot_set_cluster × N (worst case: full replication of hot set)
Each key costs O(R × value size) after fan-out (R = keyspace.ReplicationFactor, default 3, cap N). Size MaxBytes for the slice of keys this node replicas; unique cluster capacity grows with N/R.
Owner-only storage + remote Get would scale memory with N but would break the stated Put contract (async fan-out to every peer) and local-hit latency goals for read-heavy workloads. Revisit as v2 if capacity becomes the bottleneck.
| Topic | Choice |
|---|---|
| Language | Go |
| Consistency | Eventually consistent, bounded by TTL / Delete / versioned LWW apply |
| Peer write path | Owner ACK + async fan-out to R−1 replicas; peer failures logged, not retried, not returned on Put |
| Persistence | None |
| Workload | Read-heavy |
| Client API | gRPC Cache service + in-process Engine + cmd/sc + in-repo Python (clients/python) and Node (clients/node) clients |
| Local store | Custom versioned LRU (pkg/store) with structure caches for set/zset — not stock groupcache |
| Membership | hashicorp/memberlist (gossip); optional WithGossipSecret |
| Hash ring | Consistent hash with virtual nodes over cache-node peers only |
| v1 topology | Only supercache-node processes join the ring; apps use pkg/client |
| Write metadata | Owner-assigned monotonic version per key (see §6) |
| Security | TLS everywhere; separate Client and Peer listeners; Peer mTLS |
┌────────────┐ ┌────────────┐ ┌────────────┐
│ App pod A │ │ App pod B │ │ App pod C │
│ pkg/client │ │ pkg/client │ │ pkg/client │
└─────┬──────┘ └─────┬──────┘ └─────┬──────┘
│ gRPC Cache │ │
└───────────────┼───────────────┘
▼
┌────────────────────────┐
│ supercache-node × N │ ← gossip ring members only
│ Cache :client-port │
│ Peer :peer-port │ ← mesh internal only
│ Admin :admin-port │
└────────────────────────┘
| Mode | Who | Ring member? | Use |
|---|---|---|---|
| Cluster node | cmd/supercache-node |
Yes | Production |
| Client library | pkg/client |
No | Apps dial seeds / service discovery |
| Embedded Engine | pkg/engine in-process |
No (default) | Unit tests, single-process demos via WithSingleNode() |
Anti-pattern: one ring peer per request-serving app replica (churn, ownership thrash, warmup storms, huge attack surface).
Heterogeneous apps: share SuperCache nodes; keyspaces and DataSources are configured on the cache nodes (or sidecars that are cache nodes), not differently on every app binary.
SuperCache is eventually consistent. Fast reads with bounded staleness; not linearizable or transactional.
| Operation | Contract |
|---|---|
| Get | Returns a local copy if present. On CacheOnly miss, forwards to the owner (replica stores the result; non-replica does not). May lag other replicas or the source-of-truth. Invalid on ModeBloom / ModeSet / ModeZSet / ModeGeo / ModeList / ModeHash / ModeCounter / ModeJSON / ModeBitmap / ModeHLL / ModeTopK / ModeCMS / ModeVectorSet / ModeStream. |
| Put / PutMany | Returns once the key's owner has accepted the write (assigned version, local apply). Value is async fan-out to the other R−1 replicas on the ring (ReplicationFactor, default 3; negative = all peers). Non-replica peer failures are not contacted. Replica failures: log + metric only (not in Put error). Invalid on structured modes. |
| Delete / DeleteMany | Owner installs a tombstone and fans it through the same replica apply+hint pool as Put (Fanout.Apply, sync first attempt). Failed RPCs are hinted and replayed (LWW so a later delete supersedes a queued put). Returns structured multi-error if any replica is unreachable on the first attempt. Topology handoff uses the same pool. Applies to KV keys and named Bloom / set / zset / geo / list / hash / counter / JSON / bitmap / HLL / TopK / CMS / vector-set / stream entries. |
| BloomAdd / BloomTest | ModeBloom only. Add ORs bits on owner + replicas (not LWW of the bitset). Test: local if replica has the filter, else owner-forward. Missing filter → test false. No per-item delete. |
| SetAdd / SetRemove / SetContains / SetCard / SetMembers | ModeSet only. Owner serializes mutations; item-level fan-out (FlagSetAdd / FlagSetRemove). Contains/card/members: local on replica, owner-forward otherwise. Missing set → contains false, card 0, empty members. |
| ZAdd / ZRem / ZScore / ZCard / ZRange / ZRangeByScore | ModeZSet only. Same ownership pattern as ModeSet; item-level FlagZSetAdd / FlagZSetRem; score float64 (NaN rejected). Equal scores order by member bytes. Range by Redis-style rank or inclusive score window. |
| GeoAdd / GeoRem / GeoPos / GeoCard / GeoDist / GeoRadius | ModeGeo only. Same ownership as ModeSet; item-level FlagGeoAdd / FlagGeoRem; WGS84; radius haversine meters. |
| LPush / RPush / LPop / RPop / LLen / LIndex / LRange | ModeList only. Owner applies the op then fans out a full FlagList snapshot (hints coalesce per name). Non-owner pop uses peer ListPop. Redis-style indexes. |
| HSet / HGet / HDel / HExists / HLen / HGetAll | ModeHash only. Owner serializes mutations; item-level FlagHashSet / FlagHashDel (field ops commute). Replica with a local hash is local-only on field miss (same as SetContains). Missing hash → HGet ok=false, HLen 0, empty HGetAll. Empty after last HDel until Delete(name). |
| Incr / CounterGet | ModeCounter only. Owner-serialized Incr(delta) returns the new int64 (peer CounterIncr from non-owner). Replicas install a FlagCounter snapshot (hints coalesce). Replica CounterGet may lag; Incr is authoritative. Missing → CounterGet 0, ok=false. Live 0 stays until Delete(name). Overflow → invalid argument (no wrap). Get/Put invalid. |
| JsonSet / JsonGet / JsonDel | ModeJSON only. Named nested JSON document; tiny path ($ / .ident / ["k"] / [n>=0]). Owner applies the op then fans out a full FlagJSON snapshot. Replica JsonGet may lag. Missing doc or path → ok=false. JsonDel $ clears to live {} until Delete(name). Get/Put invalid. |
| BitSet / BitGet / BitCount / BitPos | ModeBitmap only. Named packed bit vector; Redis MSB-first bit order; BITCOUNT/BITPOS byte windows (whole = 0,-1). Owner applies the op then fans out a full FlagBitmap snapshot. Replica BitGet may lag. Missing name → BitGet ok=false; live past-end → ok=true, bit false. Empty-until-delete (clear last 1 leaves live all-zero). Get/Put invalid. BitSet is ACK-only (no old bit). |
| HLLAdd / HLLCount | ModeHLL only. Approximate distinct count of hashed items; FNV-1a 64, p=14 dense 12 KiB. Owner applies the op then fans out a full FlagHLL snapshot. Replica HLLCount may lag. Missing name → ok=false. Empty-until-delete. Get/Put invalid. HLLAdd is ACK-only (no Redis changed-bool). Estimates do not match Redis PFCOUNT. |
| TopKAdd / TopKList | ModeTopK only. Approximate heavy-hitters; Space-Saving, +1 observations. Owner applies the op then fans out a full FlagTopK snapshot. Replica TopKList may lag. Missing name → ok=false. Empty-until-delete. Get/Put invalid. TopKAdd is ACK-only. Estimates do not match Redis TOPK. |
| CMSIncr / CMSQuery | ModeCMS only. Approximate frequency of a named item; Count-Min, d=4 w=2048 dense 64 KiB. Owner applies the op then fans out a full FlagCMS snapshot. Replica CMSQuery may lag. Missing name → ok=false. Empty-until-delete. Get/Put invalid. CMSIncr is ACK-only (n==0 means 1; no returned estimate). Estimates do not match Redis CMS.QUERY. |
| VAdd / VRem / VSim / VCard / VDim / VEmb | ModeVectorSet only. Named embedding set; brute-force K-NN (VectorMetric cosine/L2/IP). Owner applies then fans a full FlagVectorSet snapshot (inbox A/R owner-only). Replica VSim may lag. Missing name → VSim empty, VCard/VDim present=false. Last rem keeps empty set (dim locked). Get/Put invalid. |
| XAdd / XRange / XRevRange / XLen / XDel / XTrim | ModeStream only. Named append-only log; owner-minted millis-seq ids; opaque []byte payload. Owner XAdd returns the id (peer StreamAdd from non-owner). Replicas install a full FlagStream snapshot (S; inbox A/D/T owner-only). Replica XRange may lag. Missing name → XRange empty, XLen present=false. Empty after last del/trim stays live until Delete(name). Get/Put invalid. |
| UpdateKeySpace / DeleteKeySpace | Local to the calling node. Re-issue on every node for cluster-wide rollout. Drift is unsupported in v1; expose config generation on /peers for detection. |
| Where | Guarantee |
|---|---|
| Owner node after successful Put / SetAdd / ZAdd / GeoAdd / LPush / HSet / Incr / JsonSet / JsonDel / BitSet / HLLAdd / TopKAdd / CMSIncr / BloomAdd / XAdd | Local read verbs see the update immediately. |
| Initiating client (via gRPC) | No automatic same-socket RYOW unless the client dials the owner and reads there, or the client enables optional client-side sticky buffer (out of scope for v1 server). Document: Write success means owner has the value; other nodes may lag until fan-out. |
| Optional v1.1 | Client library may cache last write locally for RYOW within process — not server contract. |
This replaces the earlier ambiguous “same node sees Put immediately” wording that conflicted with owner-only apply.
- Steady state: peers usually learn Puts within about one fan-out RTT under light load; TTL is the hard bound when fan-out fails.
- Network partition: each side accepts reads/writes independently; no quorum, no fencing. Short TTLs bound post-heal staleness; versions reduce flip-flop from delayed applies.
- Out-of-band SoT writes: cache keeps old value until TTL or Delete (load-through keyspaces).
- Good fit: read-heavy, tolerate seconds of staleness.
- Bad fit: linearizable reads, financial / at-most-once ledgers, CRDT counters, strict write order, sole copy of irreplaceable data. Named
ModeCounteris in-scope for rate limits and other best-effort counts (retry can double-apply).
| Mechanism | Role |
|---|---|
| TTL | Primary bound on serving a value |
| Versioned LWW | Prevents older ApplyPut/Prefetch from overwriting newer data |
| LRU / MaxBytes | Memory bound; may drop before TTL |
| Delete | Best-effort invalidate |
| Negative TTL | Bounds repeated SoT misses |
Every stored entry carries:
Entry {
value []byte
version uint64 // owner-assigned; strictly increasing per key on that owner generation
expire_at int64 // unix nano absolute; 0 = no TTL
flags uint32 // bit0 = negative entry
keyspace string // implicit in map; on wire explicit
}
- On Put, owner sets
version = max(prev, last_issued)+1for that key (in-memory counter per key or per keyspace with care). - On owner change, new owner starts from
max(observed versions for key, 1)when it first sees the key; delayed applies from old owners are rejected ifversion <= local. - Do not use wall clock as the sole LWW key (skew). Wall clock may still set
expire_at = now + ttlat apply time on each peer (receive-time + remaining TTL on fan-out is simpler and skew-tolerant):- Normative: wire carries
ttl_nanosand/or absoluteexpire_atfrom owner; peers store owner’sexpire_atso expiry is aligned cluster-wide when clocks are roughly OK; document short TTLs still required.
- Normative: wire carries
ApplyPut(entry):
if local missing OR entry.version > local.version → store entry
else → ignore (metric: apply_stale)
ApplyDelete(key, delete_version):
if delete_version < local.version → ignore
else → install versioned tombstone (required; never a bare remove)
Prefetch / DataSource fill:
must go through same version assignment on owner (or use version 0 fill only if absent — see modes)
Tombstones (v1, required): every accepted Delete / ApplyDelete installs a versioned marker so a delayed ApplyPut cannot resurrect the key. LRU must not evict an unexpired tombstone (MaxBytes may overshoot). Marker lifetime is keyspace.Config.TombstoneTTL (0 → DefaultTombstoneTTL 5m; negative → never expire). After expiry a late Put may land; the key’s own TTL still bounds any value.
Last owner-accepted Put with highest version wins cluster-wide as fan-outs land. Temporary divergence is allowed; versions make convergence monotonic, not random last-apply.
Each keyspace has a mode. Opaque KV modes use Get/Put/Delete. Structured modes reject Get/Put and expose type-specific verbs (see docs/API.md).
- Get miss → load via
DataSource(owner-coalesced; see §9.1) - Put allowed as explicit override / warm; gets a new version like any Put
- After eviction of a Put value, miss may reload SoT (Put is not durable)
- Out-of-band SoT change: caller must Delete or wait TTL
DataSourcerequired
- No DataSource (or never consulted)
- Get miss → not found (no load)
- Put / Delete are the only mutators
- Eviction / TTL → data gone (acceptable; no persistence)
| Mode | Ring identity | App verbs | Wire flags (peer ApplyPut) |
|---|---|---|---|
LoadThrough / CacheOnly |
KV key | Get, Put, Delete (+ batch) | value entry / negative |
ModeBloom |
filter name |
BloomAdd, BloomTest; Delete(name) | FlagBloom snapshot, item-add OR path |
ModeSet |
set name |
SetAdd, SetRemove, SetContains, SetCard, SetMembers; Delete(name) | FlagSet, FlagSetAdd, FlagSetRemove |
ModeZSet |
zset name |
ZAdd, ZRem, ZScore, ZCard, ZRange, ZRangeByScore; Delete(name) | FlagZSet, FlagZSetAdd, FlagZSetRem |
ModeGeo |
index name |
GeoAdd, GeoRem, GeoPos, GeoCard, GeoDist, GeoRadius; Delete(name) | FlagGeo, FlagGeoAdd, FlagGeoRem |
ModeList |
list name |
LPush, RPush, LPop, RPop, LLen, LIndex, LRange; Delete(name) | FlagList snapshot; owner-inbox FlagListLPush / FlagListRPush |
ModeHash |
hash name |
HSet, HGet, HDel, HExists, HLen, HGetAll; Delete(name) | FlagHash, FlagHashSet, FlagHashDel |
ModeCounter |
counter name |
Incr, CounterGet; Delete(name) | FlagCounter snapshot only |
ModeJSON |
document name |
JsonSet, JsonGet, JsonDel; Delete(name) | FlagJSON snapshot; owner-inbox FlagJSONSet / FlagJSONDel |
ModeBitmap |
bitmap name |
BitSet, BitGet, BitCount, BitPos; Delete(name) | FlagBitmap snapshot; owner-inbox FlagBitmapSet |
ModeHLL |
sketch name |
HLLAdd, HLLCount; Delete(name) | FlagHLL snapshot; owner-inbox FlagHLLAdd |
ModeTopK |
table name |
TopKAdd, TopKList; Delete(name) | FlagTopK snapshot; owner-inbox FlagTopKAdd |
ModeCMS |
sketch name |
CMSIncr, CMSQuery; Delete(name) | FlagCMS snapshot; owner-inbox FlagCMSIncr |
ModeVectorSet |
set name |
VAdd, VRem, VSim, VCard, VDim, VEmb; Delete(name) | FlagVectorSet snapshot; owner-inbox same flag + A/R prefix |
ModeStream |
stream name |
XAdd, XRange, XRevRange, XLen, XDel, XTrim; Delete(name) | FlagStream snapshot; owner-inbox same flag + A/D/T prefix |
Public reference: docs/API.md, OpenAPI api/openapi/cache.openapi.yaml, proto api/proto/cache.proto.
- Named filter per key (
name);BloomAdd/BloomTest - Bit OR on add (not LWW of the whole bitset); no per-item delete
Delete(name)tombstones the filter; handoff OR-merges bitsets- Config:
BloomBits/BloomHashes(defaults inpkg/keyspace); bitset size must fitMaxBytes - Package:
pkg/bloom - Design: docs/design/2026-08-11-bloom-filter.md
- Named set;
SetAdd/SetRemove/SetContains/SetCard/SetMembers - Owner-serialized writes; item-level fan-out; versioned full-set handoff snapshot
- Encode: length-prefixed members; store keeps live set cache + lazy encode on Peek/handoff
- Empty set after last remove may remain until
Delete(name)(Card 0) - Package:
pkg/set - Design: docs/design/2026-08-13-mode-set.md
- Named zset;
ZAdd/ZRem/ZScore/ZCard/ZRange/ZRangeByScore - Score is IEEE-754
float64(NaN → invalid argument; ±Inf allowed) - Equal scores ordered by raw member byte order (Redis-like)
ZRangeranks: 0-based inclusive, negative indices like Redis (-1= last)ZRangeByScore: inclusive[min, max], ascending- Owner-serialized writes; item-level fan-out; versioned full zset handoff
- Encode: sorted records of
float64 LE score+ uvarint(len) + member - Package:
pkg/zset; CLI:sc zadd|zrem|zscore|zcard|zrange|zrangebyscore - Design: docs/design/2026-08-13-mode-zset.md
- Named index;
GeoAdd/GeoRem/GeoPos/GeoCard/GeoDist/GeoRadius - WGS84 lon
[-180,180], lat[-90,90]; NaN/Inf/OOB rejected - Distance: haversine meters (mean radius 6371.0088 km); radius nearest-first;
limit<=0= all - Same ownership / item-level fan-out / snapshot handoff as ModeSet
- Package:
pkg/geo; CLI:sc geoadd|georem|geopos|geocard|geodist|georadius - Design: docs/design/2026-08-19-mode-geo.md
- Named list;
LPush/RPush/LPop/RPop/LLen/LIndex/LRange - Index 0 = head; Redis-style negatives on LIndex/LRange
- Not commutative: replicas install a full
FlagListsnapshot after each owner mutate (existing hint coalesce is one pending op per name) - Non-owner pop: peer
ListPop - Empty after last pop: LLen 0 until
Delete(name) - Package:
pkg/listx; CLI:sc lpush|rpush|lpop|rpop|llen|lindex|lrange - Design: docs/design/2026-08-19-mode-list.md
- Named hash;
HSet/HGet/HDel/HExists/HLen/HGetAll - Field →
[]byte; empty field invalid; empty value allowed - Owner-serialized writes; item-level fan-out; versioned full-hash handoff (
FlagHash, not union) - Replica with
HasHashis local-only (field miss does not owner-forward).hintIDstays(ks, name)except BloomAdd - Empty after last HDel: HLen 0 until
Delete(name) - Package:
pkg/hashx; CLI:sc hset|hget|hdel|hexists|hlen|hgetall - Design: docs/design/2026-08-20-mode-hash.md
- Named counter;
Incr(delta)returns new value;CounterGet;Delete(name) - Owner-serialized writes; snapshot fan-out (
FlagCounter); non-ownerIncruses peerCounterIncr - Encode: 8-byte two’s-complement LE; overflow → invalid argument (no wrap)
- Keyspace TTL slides on Incr; fixed-window rate limits put the window id in the name
- Live
0stays untilDelete(name) - Package:
pkg/counter; CLI:sc incr|cget - Design: docs/design/2026-08-20-mode-counter.md
- Named document;
JsonSet/JsonGet/JsonDel;Delete(name)tombstone - Tiny path:
$/.ident/["utf8"]/[n>=0]; objects auto-created, arrays not - Owner applies the op then fans out a full
FlagJSONsnapshot (hints coalesce). Inbox flags never go to replicas - Numbers via
encoding/jsonUseNumber. Live JSONnullis present; missing name is not JsonDel $→ live{}untilDelete(name)- Package:
pkg/jsonx; CLI:sc jsonset|jsonget|jsondel - Design: docs/design/2026-08-21-mode-json.md
- Named packed bit vector;
BitSet/BitGet/BitCount/BitPos;Delete(name)tombstone - Redis MSB-first of byte 0 (
pkg/bitmapx);BITCOUNT/BITPOSwindows are bytes (negatives like List; whole =0,-1) - Owner applies the op then fans out a full
FlagBitmapsnapshot (hints coalesce). InboxFlagBitmapSetnever goes to replicas - Missing name →
BitGetok=false; live past-end → implicit 0,ok=true. Live all-zero stays untilDelete(name) BitSetis ACK-only (does not return the old bit). Encoded length bounded byMaxValueSize- Package:
pkg/bitmapx; CLI:sc bitset|bitget|bitcount|bitpos - Design: docs/design/2026-08-21-mode-bitmap.md
- Named HLL sketch;
HLLAdd/HLLCount;Delete(name)tombstone pkg/hllx: FNV-1a 64,p=14, 16384 × 6-bit dense registers (12 KiB, no header)- Owner applies the op then fans out a full
FlagHLLsnapshot (hints coalesce). InboxFlagHLLAddnever goes to replicas - Missing name →
HLLCountok=false; live sketch (even estimate 0) →ok=true. First add creates;Delete(name)only drop HLLAddis ACK-only (no Redis changed-bool). Estimates do not match RedisPFCOUNT- PLAN §1 “HyperLogLog” means not RESP / not a Redis clone — a SuperCache mode named HLL is allowed
- Package:
pkg/hllx; CLI:sc hlladd|hllcount - Design: docs/design/2026-08-25-mode-hll.md
- Named Space-Saving table;
TopKAdd/TopKList;Delete(name)tombstone pkg/topkx: +1 observations, K = keyspaceTopKSize(0 → 100); no Redis TOPK numeric parity- Owner applies the op then fans out a full
FlagTopKsnapshot (hints coalesce). InboxFlagTopKAddnever goes to replicas - Missing name →
TopKListok=false; live table (including occupied < K) →ok=true. First add creates;Delete(name)only drop TopKAddis ACK-only (no count / evicted / changed-bool). Not RedisTOPK.*- Package:
pkg/topkx; CLI:sc topkadd|topklist; live billboard:examples/billboard - Design: docs/design/2026-08-31-mode-topk.md
- Named Count-Min Sketch;
CMSIncr/CMSQuery;Delete(name)tombstone pkg/cmsx: FNV-1a 64 + mix64, d=4 w=2048 dense 64 KiB;n==0means 1; no Redis CMS numeric parity- Owner applies the op then fans out a full
FlagCMSsnapshot (hints coalesce). InboxFlagCMSIncrnever goes to replicas - Missing name →
CMSQueryok=false; live sketch (even estimate 0) →ok=true. First incr creates;Delete(name)only drop CMSIncris ACK-only (no returned estimate). Not RedisCMS.*- Package:
pkg/cmsx; CLI:sc cmsincr|cmsquery; billboard complement:examples/billboard - Design: docs/design/2026-09-01-mode-cms.md
- Named log;
XAdd/XRange/XRevRange/XLen/XDel/XTrim;Delete(name)tombstone - Owner-minted
millis-seqids; one opaque[]bytepayload (client encodes). Exclusive start is a leading(onstart - Owner
XAddreturns the id (peerStreamAddfrom a non-owner).XDel/XTrimare ACK-only inboxApplyPut - Replicas install a full
FlagStreamsnapshot (S; inboxA/D/Towner-only). ReplicaXRangemay lag - Missing name →
XRangeempty,XLenpresent=false. Empty after last del/trim stays live untilDelete(name) StreamMaxLen0 = no auto-trim; hard cap 4096. Get/Put invalid. Not consumer groups / not blockingXREAD- Package:
pkg/stream; CLI:sc xadd|xlen|xrange|xrevrange|xdel|xtrim - Design: docs/design/2026-09-08-mode-stream.md
| Incoming | vs local | Result |
|---|---|---|
| Put / ApplyPut higher version | any | accept |
| Put / ApplyPut lower/equal version | any | reject |
| Load fill (LoadThrough) | missing | accept with new owner version then fan-out |
| Load fill | present non-expired | do not overwrite (hit path shouldn’t load) |
| Prefetch | same as load fill rules | |
| Negative entry | higher version / miss path | store negative; Put always overrides negative with higher version |
| ApplyDelete adequate version | present or missing | install tombstone |
| FlagSetAdd / FlagZSetAdd / FlagHashSet / Bloom item-add | structure present | mutate under mutex; version gate |
| FlagSet / FlagZSet / FlagBloom / FlagHash / FlagCounter / FlagJSON / FlagBitmap / FlagHLL / FlagTopK / FlagCMS / FlagVectorSet / FlagStream snapshot | structure or tombstone | install if version > local (equal ignore; tombstone blocks stale) |
App services
pkg/client · cmd/sc (not in ring)
│
gRPC Cache :client
(KV · Bloom · Set · ZSet · Geo · List · Hash · Counter · JSON · Bitmap · HLL · TopK · CMS · VectorSet · Stream)
▼
┌─────────────────────┐
│ supercache-node │
│ Engine │
│ KeySpaces · Events │
└──────────┬──────────┘
│
┌────────────────┼────────────────┐
▼ ▼ ▼
Local store Owner routing Peer mesh
(LRU + TTL/ (hash ring) gRPC Peer :peer
version + async fan-out
set/zset/list/hash/json (value / item flags)
caches)
│ │ │
▼ ▼ ▼
DataSource ◄── protect memberlist gossip
(LoadThrough only)
│
Admin HTTP · /docs · OpenTelemetry
Get(keyspace, key) on node N:
1. local := store.Get(ks, key)
2. if local present and not expired:
if negative → return NotFound
else → return value // HIT
3. if keyspace.mode == CacheOnly:
return NotFound
4. // LoadThrough miss
singleflight(ks, key):
owner := ring.Owner(key)
if owner != self:
// Prefer owner-coalesced load (cluster singleflight)
resp, err := peer.GetOrLoad(owner, ks, key) // owner runs steps 5–7
if err == nil:
// optional: do not require local store; client got value from owner path
// v1: cache locally with same version/expiry from response (no new version)
store.AcceptIfNewer(resp.entry)
return resp
// owner down / error → fall through to local load once
// owner == self OR fallback:
5. protect.Allow(ks) or return Unavailable
6. val, err := DataSource.Load(ctx, key)
if err == NotFound → store negative (owner assigns version) → fan-out ApplyPut(negative) → return NotFound
if err != nil → return err
7. entry := owner.AssignVersion(val, ttl); store; async FanoutApplyPut(entry); return val
Owner down: non-owner may load locally (availability over singleflight purity); still assigns version only if it becomes… v1 simplification: on owner-down fallback, node loads and stores locally only without fan-out or temporarily acts as loader with version from time/local counter tagged with loader_id — normative v1: local-only fill on owner-down (no fan-out), metric get_owner_fallback_local; rely on TTL and later owner Put/load for repair.
Put(ks, key, val, ttl):
1. owner := ring.Owner(key)
2. if self != owner → return peer.ForwardPut(owner, ...)
3. entry := {val, version: next(key), expire_at, flags:0}
4. store.Set(entry)
5. async fanout:
for peer in ring.PeersExceptSelf():
workerPool.Go: ApplyPut(peer, entry) // timeout T; on err metric; no retry
6. return OK
PutMany: group keys by owner; per-key independent success; max batch size B (default 100);
return per-key errors; not atomic.
Delete(ks, key):
1. owner := ring.Owner(key) // still used to mint delete_version
2. delete_version := owner.NextVersion(key) // or max(local, known)+1 via forward to owner
3. results := parallel ApplyDelete(ks, key, delete_version) to owner + all peers
(owner applies locally if self)
4. if any failure → return MultiError{PeerErrors...}
5. else → OK
// Document: success = all currently known peers ACKed ApplyDelete.
// Not a guarantee against concurrent Put with higher version racing in.
1. memberlist join/leave/update
2. recompute ring; bump ring_generation
3. emit Events
4. warmup: bounded workers; version-checked AcceptIfNewer only
5. in-flight fan-out uses membership snapshot at schedule time; include ring_generation on peer RPC
Ownership change does not migrate bytes; refill via Get miss, Put, or warmup.
Local only; validate; atomic swap config; if MaxBytes shrinks, rely on store cost eviction.
/peers and metrics expose keyspace_config_hash for drift detection. Cluster rollout = ops re-issue.
Normative shape shared by Set, ZSet, Geo, and Hash (Bloom differs only in mutate semantics; List, Counter, JSON, Bitmap, and HLL fan out a full snapshot):
Mutate (SetAdd / SetRemove / ZAdd / ZRem / GeoAdd / GeoRem / HSet / HDel / BloomAdd) on node N:
1. reject if keyspace.mode wrong or member/score/field invalid
2. owner := ring.Owner(name)
3. if self != owner → peer.ApplyPut(owner, name, Flag*Add|Rem|Set|Del, payload)
4. else: nextVersion(name); store mutate under mutex; async fan-out Flag* to R−1
5. return OK // fan-out failures: metric/hint (like Put)
Incr (ModeCounter; List-class snapshot, not step-3 item flag):
1. if self != owner → peer.CounterIncr (returns new n)
2. else: CIncr under store mutex (stored = local+1); fan-out FlagCounter snapshot at PeekVersion
3. overflow / wrong type → invalid argument; entry unchanged
LPush / RPush / LPop / RPop (ModeList; snapshot, not step-3 item flag):
1. if self != owner → ApplyPut inbox FlagListLPush / FlagListRPush (push) or peer ListPop (pop)
2. else: LPush/RPush/LPop/RPop under store mutex (stored = local+1); fan-out FlagList snapshot at PeekVersion
3. replica ApplyPut of inbox flags is ignored
JsonSet / JsonDel (ModeJSON; List-class snapshot, not step-3 item flag):
1. if self != owner → ApplyPut inbox FlagJSONSet / FlagJSONDel (ACK-only)
2. else: JSet / JDel under store mutex (stored = local+1); fan-out FlagJSON snapshot at PeekVersion
3. replica ApplyPut of inbox flags is ignored
BitSet (ModeBitmap; List-class snapshot, not step-3 item flag):
1. if self != owner → ApplyPut inbox FlagBitmapSet (ACK-only)
2. else: BSet under store mutex (stored = local+1); fan-out FlagBitmap snapshot at PeekVersion
3. replica ApplyPut of inbox flags is ignored
HLLAdd (ModeHLL; List-class snapshot, not step-3 item flag):
1. if self != owner → ApplyPut inbox FlagHLLAdd (ACK-only)
2. else: HLLAdd under store mutex (stored = local+1); fan-out FlagHLL snapshot at PeekVersion
3. replica ApplyPut of inbox flags is ignored
TopKAdd (ModeTopK; List-class snapshot, not step-3 item flag):
1. if self != owner → ApplyPut inbox FlagTopKAdd (ACK-only)
2. else: TopKAdd under store mutex (stored = local+1); fan-out FlagTopK snapshot at PeekVersion
3. replica ApplyPut of inbox flags is ignored
CMSIncr (ModeCMS; List-class snapshot, not step-3 item flag):
1. if self != owner → ApplyPut inbox FlagCMSIncr (ACK-only)
2. else: CMSIncr under store mutex (stored = local+1); fan-out FlagCMS snapshot at PeekVersion
3. replica ApplyPut of inbox flags is ignored
XAdd / XDel / XTrim (ModeStream; List-class snapshot, Value[0] inbox):
1. if self != owner → peer StreamAdd (XAdd, returns id) or ApplyPut inbox A/D/T (XDel / XTrim, ACK-only)
2. else: mutate under store mutex (stored = local+1); mint millis-seq on XAdd; fan-out FlagStream snapshot
3. replica ApplyPut of inbox A/D/T is ignored; S snapshot installs LWW
Read (SetContains / ZScore / GeoPos / HGet / HExists / HLen / HGetAll / BloomTest / CounterGet / JsonGet / BitGet / BitCount / BitPos / HLLCount / TopKList / CMSQuery / VSim / XRange / XLen):
1. if local live structure → answer from store cache (field miss stays local)
BitGet / BitCount / BitPos: local extract if HasBitmap (replica may lag)
HLLCount: local extract if HasHLL (replica may lag)
TopKList: local extract if HasTopK (replica may lag)
CMSQuery: local extract if HasCMS (replica may lag)
XRange / XLen: local extract if HasStream (replica may lag)
2. else if owner self → missing → empty/false
3. else → peer.GetOrLoad(owner) structure snapshot; optional install if holdsReplica
4. decode / answer
Handoff: include structure entries in LocalEntries; ApplyPut FlagSet|FlagZSet|FlagGeo|FlagHash|FlagBloom|FlagCounter|FlagJSON|FlagBitmap|FlagHLL|FlagTopK|FlagCMS|FlagVectorSet|FlagStream
if incoming version > local (tombstone still blocks stale).
Bloom add ORs bits (no full-blob LWW on item add). Set/ZSet item ops apply under version gates; concurrent ZAdd same member → last owner write wins for that score.
| Need | Approach |
|---|---|
| MaxBytes | cost-based eviction; tombstones / Bloom / Set / ZSet / Geo / List / Hash / Counter / JSON / Bitmap / HLL / TopK / CMS / VectorSet / Stream protected while live |
| TTL | expire_at on entry; lazy expire |
| Set / Delete | First-class AcceptIfNewer / DeleteIfVersion |
| Negative | Envelope flag |
| Concurrent | Store mutex |
| ModeSet / ModeZSet / ModeGeo / ModeList / ModeHash | Live structure cache + dirty lazy encode |
| ModeCounter | Eager 8-byte FlagCounter (no dirty cache) |
| ModeJSON | jCache + eager encode after mutate; 0-alloc Peek/Get flush when !jDirty |
| ModeBitmap | packed Value (no dirty cache); protect FlagBitmap |
| ModeHLL | dense register Value (no dirty cache); protect FlagHLL |
| ModeTopK | encoded table Value (no dirty cache); protect FlagTopK |
| ModeCMS | dense counter Value (no dirty cache); protect FlagCMS |
| ModeVectorSet | encoded member table Value (no dirty cache); protect FlagVectorSet |
| ModeStream | encoded log Value (no dirty cache); protect FlagStream |
| ModeBloom | Bitset in entry value; in-place OR under mutex |
Not using stock golang/groupcache for distribution or primary API (get-only, HTTP peers, no Put/Delete/TTL).
type Store interface {
Get(key string) (Entry, bool)
Peek(key string) (Entry, bool)
AcceptIfNewer(key string, e Entry) bool
// Bloom / Set / ZSet methods (see pkg/store)
// metrics: items, cost, hits, misses, evictions
}type Engine interface {
// KV — ModeCacheOnly / ModeLoadThrough
Get(ctx context.Context, keyspace, key string) ([]byte, error)
Put(ctx context.Context, keyspace, key string, value []byte, opts ...PutOption) error
PutMany(ctx context.Context, keyspace string, kvs []KV, opts ...PutOption) error
Delete(ctx context.Context, keyspace, key string) error
DeleteMany(ctx context.Context, keyspace string, keys []string) error
// ModeBloom
BloomAdd(ctx context.Context, keyspace, name string, item []byte) error
BloomTest(ctx context.Context, keyspace, name string, item []byte) (bool, error)
// ModeSet
SetAdd(ctx context.Context, keyspace, name string, item []byte) error
SetRemove(ctx context.Context, keyspace, name string, item []byte) error
SetContains(ctx context.Context, keyspace, name string, item []byte) (bool, error)
SetCard(ctx context.Context, keyspace, name string) (int, error)
SetMembers(ctx context.Context, keyspace, name string) ([][]byte, error)
// ModeZSet
ZAdd(ctx context.Context, keyspace, name string, member []byte, score float64) error
ZRem(ctx context.Context, keyspace, name string, member []byte) error
ZScore(ctx context.Context, keyspace, name string, member []byte) (float64, bool, error)
ZCard(ctx context.Context, keyspace, name string) (int, error)
ZRange(ctx context.Context, keyspace, name string, start, stop int) ([]ZMember, error)
ZRangeByScore(ctx context.Context, keyspace, name string, min, max float64) ([]ZMember, error)
// ModeGeo
GeoAdd(ctx context.Context, keyspace, name string, member []byte, lon, lat float64) error
GeoRem(ctx context.Context, keyspace, name string, member []byte) error
GeoPos(ctx context.Context, keyspace, name string, member []byte) (lon, lat float64, ok bool, err error)
GeoCard(ctx context.Context, keyspace, name string) (int, error)
GeoDist(ctx context.Context, keyspace, name string, a, b []byte) (meters float64, ok bool, err error)
GeoRadius(ctx context.Context, keyspace, name string, lon, lat, radiusM float64, limit int) ([]GeoMember, error)
// ModeList
LPush(ctx context.Context, keyspace, name string, item []byte) error
RPush(ctx context.Context, keyspace, name string, item []byte) error
LPop(ctx context.Context, keyspace, name string) (item []byte, ok bool, err error)
RPop(ctx context.Context, keyspace, name string) (item []byte, ok bool, err error)
LLen(ctx context.Context, keyspace, name string) (int, error)
LIndex(ctx context.Context, keyspace, name string, idx int) (item []byte, ok bool, err error)
LRange(ctx context.Context, keyspace, name string, start, stop int) ([][]byte, error)
// ModeHash
HSet(ctx context.Context, keyspace, name string, field, value []byte) error
HGet(ctx context.Context, keyspace, name string, field []byte) (value []byte, ok bool, err error)
HDel(ctx context.Context, keyspace, name string, field []byte) error
HExists(ctx context.Context, keyspace, name string, field []byte) (bool, error)
HLen(ctx context.Context, keyspace, name string) (int, error)
HGetAll(ctx context.Context, keyspace, name string) ([]HashField, error)
// ModeCounter
Incr(ctx context.Context, keyspace, name string, delta int64) (int64, error)
CounterGet(ctx context.Context, keyspace, name string) (int64, bool, error)
// ModeJSON
JsonSet(ctx context.Context, keyspace, name, path string, value []byte) error
JsonGet(ctx context.Context, keyspace, name, path string) (json []byte, ok bool, err error)
JsonDel(ctx context.Context, keyspace, name, path string) error
// ModeBitmap
BitSet(ctx context.Context, keyspace, name string, offset uint64, bit bool) error
BitGet(ctx context.Context, keyspace, name string, offset uint64) (bit bool, ok bool, err error)
BitCount(ctx context.Context, keyspace, name string, start, end int) (n int64, err error)
BitPos(ctx context.Context, keyspace, name string, bit bool, start, end int) (pos int64, found bool, err error)
// ModeHLL
HLLAdd(ctx context.Context, keyspace, name string, item []byte) error
HLLCount(ctx context.Context, keyspace, name string) (n uint64, ok bool, err error)
// ModeTopK
TopKAdd(ctx context.Context, keyspace, name string, item []byte) error
TopKList(ctx context.Context, keyspace, name string) (entries []TopKEntry, ok bool, err error)
// ModeCMS
CMSIncr(ctx context.Context, keyspace, name string, item []byte, n uint64) error
CMSQuery(ctx context.Context, keyspace, name string, item []byte) (n uint64, ok bool, err error)
// ModeVectorSet
VAdd(ctx context.Context, keyspace, name string, member []byte, vec []float32) error
VRem(ctx context.Context, keyspace, name string, member []byte) error
VSim(ctx context.Context, keyspace, name string, vec []float32, k int) ([]VSimHit, error)
VCard(ctx context.Context, keyspace, name string) (n int, present bool, err error)
VDim(ctx context.Context, keyspace, name string) (dim int, present bool, err error)
VEmb(ctx context.Context, keyspace, name string, member []byte) (vec []float32, found bool, err error)
// ModeStream
XAdd(ctx context.Context, keyspace, name string, payload []byte) (id string, err error)
XRange(ctx context.Context, keyspace, name, start, end string, count int) ([]StreamEntry, error)
XRevRange(ctx context.Context, keyspace, name, start, end string, count int) ([]StreamEntry, error)
XLen(ctx context.Context, keyspace, name string) (n int, present bool, err error)
XDel(ctx context.Context, keyspace, name, id string) error
XTrim(ctx context.Context, keyspace, name string, maxLen int) error
UpdateKeySpace(cfg KeySpaceConfig) error
DeleteKeySpace(name string) error
Events() <-chan ClusterEvent
}
type PutOption func(*putConfig) // WithTTL(d time.Duration)
type PeerError struct {
PeerID string
Op string
Err error
}
type MultiError struct {
Errors []PeerError
}
func (m *MultiError) Error() string
func (m *MultiError) Unwrap() []errortype KeySpaceMode int
const (
ModeLoadThrough KeySpaceMode = iota
ModeCacheOnly
ModeBloom
ModeSet
ModeZSet
ModeGeo
ModeList
ModeHash
ModeCounter
ModeJSON
ModeBitmap
ModeHLL
ModeTopK
ModeCMS
ModeVectorSet
ModeStream
)
type KeySpaceConfig struct {
Name string
Mode KeySpaceMode
TTL time.Duration
NegativeTTL time.Duration // 0 = disabled; ignored for structure modes
TombstoneTTL time.Duration // 0 = 5m; negative = never expire
MaxBytes int64
LoadTimeout time.Duration
PeerTimeout time.Duration
WarmKeys []string
RefreshInterval time.Duration // 0 = disabled refresh-ahead
RateLimitRPS float64 // 0 = use global
CircuitBreaker CircuitConfig
DataSource DataSource // required if LoadThrough
ReplicationFactor int // 0 = default 3; negative = all peers
BloomBits int // ModeBloom; 0 = default
BloomHashes int // ModeBloom; 0 = default
TopKSize int // ModeTopK; 0 = default 100
VectorDim int // ModeVectorSet; 0 = first add locks
VectorMetric VectorMetric // ModeVectorSet; cosine / l2 / ip
StreamMaxLen int // ModeStream; 0 = no auto-trim; cap 4096
}| Feature | Placement |
|---|---|
| Automatic fetch on miss | LoadThrough Get path §9.1 |
| Multi-node availability + read scale | Replicated mesh §2 |
| Reduced backend load | Local hits, owner singleflight, negative TTL, protect |
| TTL + LRU + MaxBytes + negative TTL | Store envelope + cost eviction |
| ModeBloom / ModeSet / ModeZSet / ModeGeo / ModeList / ModeHash / ModeCounter / ModeJSON / ModeBitmap / ModeHLL / ModeTopK / ModeCMS / ModeVectorSet / ModeStream | §7, §9.6; pkg/bloom, pkg/set, pkg/zset, pkg/geo, pkg/listx, pkg/hashx, pkg/counter, pkg/jsonx, pkg/bitmapx, pkg/hllx, pkg/topkx, pkg/cmsx, pkg/vecset, pkg/stream |
| Node discovery | memberlist among supercache-nodes |
| KeySpace overrides | KeySpaceConfig |
| Dynamic keyspace updates | Local UpdateKeySpace + config hash |
| Warmup / hot keys / refresh-ahead | warmup pkg; version-checked apply |
| DataSource protection | protect pkg global + per-KS |
| Cluster events | Engine.Events buffered; drop-oldest on slow consumer |
| Admin diagnostics + OpenAPI docs | admin HTTP /docs; GitHub Pages |
| OTel | telemetry pkg |
| TLS + gossip auth | transport + memberlist secret |
| CLI | cmd/sc get/put/del, bloom, sadd*, z*, geo*, l*, h*, incr/cget, json*, bitset/bitget/bitcount/bitpos, hlladd/hllcount, topkadd/topklist, cmsincr/cmsquery, vadd*, xadd* |
| Control | v1 default |
|---|---|
| Worker pool | fixed size (e.g. 64) or min(64, 8*N) |
| Per-peer timeout | KeySpace PeerTimeout (e.g. 100–500ms) |
| Max in-flight ApplyPuts | bound queue (e.g. 10k); on overflow drop + metric fanout_dropped (still no retry) |
| Batching | optional ApplyPutMany later; v1 unary OK |
| ring_generation | sent on peer RPC; peer may accept applies regardless (cache) but metrics on mismatch |
| Delete | same pool; gather errors into MultiError |
Write scalability: O(N) RPCs per Put — acceptable for moderate N (e.g. 3–15 nodes). Document upper design target; not infinite horizontal write scale.
| Limit | Default | Notes |
|---|---|---|
| Max key length | 512 bytes | reject larger |
| Max value size | 1 MiB | reject larger |
| PutMany/DeleteMany batch | 100 keys | |
| Hot-key top-K per keyspace | 1024 | fixed memory |
| Events channel buffer | 64 | drop-oldest |
Client listener (Cache only) — see api/proto/cache.proto and OpenAPI api/openapi/cache.openapi.yaml:
service Cache {
// KV
rpc Get(GetRequest) returns (GetResponse);
rpc Put(PutRequest) returns (PutResponse);
rpc PutMany(PutManyRequest) returns (PutManyResponse);
rpc Delete(DeleteRequest) returns (DeleteResponse);
rpc DeleteMany(DeleteManyRequest) returns (DeleteManyResponse);
// ModeBloom
rpc BloomAdd(BloomAddRequest) returns (BloomAddResponse);
rpc BloomTest(BloomTestRequest) returns (BloomTestResponse);
// ModeSet
rpc SetAdd(SetAddRequest) returns (SetAddResponse);
rpc SetRemove(SetRemoveRequest) returns (SetRemoveResponse);
rpc SetContains(SetContainsRequest) returns (SetContainsResponse);
rpc SetCard(SetCardRequest) returns (SetCardResponse);
rpc SetMembers(SetMembersRequest) returns (SetMembersResponse);
// ModeZSet
rpc ZAdd(ZAddRequest) returns (ZAddResponse);
rpc ZRem(ZRemRequest) returns (ZRemResponse);
rpc ZScore(ZScoreRequest) returns (ZScoreResponse);
rpc ZCard(ZCardRequest) returns (ZCardResponse);
rpc ZRange(ZRangeRequest) returns (ZRangeResponse);
rpc ZRangeByScore(ZRangeByScoreRequest) returns (ZRangeResponse);
// ModeGeo
rpc GeoAdd(GeoAddRequest) returns (GeoAddResponse);
rpc GeoRem(GeoRemRequest) returns (GeoRemResponse);
rpc GeoPos(GeoPosRequest) returns (GeoPosResponse);
rpc GeoCard(GeoCardRequest) returns (GeoCardResponse);
rpc GeoDist(GeoDistRequest) returns (GeoDistResponse);
rpc GeoRadius(GeoRadiusRequest) returns (GeoRadiusResponse);
// ModeList
rpc LPush(LPushRequest) returns (LPushResponse);
rpc RPush(RPushRequest) returns (RPushResponse);
rpc LPop(LPopRequest) returns (LPopResponse);
rpc RPop(RPopRequest) returns (RPopResponse);
rpc LLen(LLenRequest) returns (LLenResponse);
rpc LIndex(LIndexRequest) returns (LIndexResponse);
rpc LRange(LRangeRequest) returns (LRangeResponse);
// ModeHash
rpc HSet(HSetRequest) returns (HSetResponse);
rpc HGet(HGetRequest) returns (HGetResponse);
rpc HDel(HDelRequest) returns (HDelResponse);
rpc HExists(HExistsRequest) returns (HExistsResponse);
rpc HLen(HLenRequest) returns (HLenResponse);
rpc HGetAll(HGetAllRequest) returns (HGetAllResponse);
// ModeCounter
rpc Incr(IncrRequest) returns (IncrResponse);
rpc CounterGet(CounterGetRequest) returns (CounterGetResponse);
// ModeJSON
rpc JsonSet(JsonSetRequest) returns (JsonSetResponse);
rpc JsonGet(JsonGetRequest) returns (JsonGetResponse);
rpc JsonDel(JsonDelRequest) returns (JsonDelResponse);
// ModeBitmap
rpc BitSet(BitSetRequest) returns (BitSetResponse);
rpc BitGet(BitGetRequest) returns (BitGetResponse);
rpc BitCount(BitCountRequest) returns (BitCountResponse);
rpc BitPos(BitPosRequest) returns (BitPosResponse);
// ModeHLL
rpc HLLAdd(HLLAddRequest) returns (HLLAddResponse);
rpc HLLCount(HLLCountRequest) returns (HLLCountResponse);
// ModeTopK
rpc TopKAdd(TopKAddRequest) returns (TopKAddResponse);
rpc TopKList(TopKListRequest) returns (TopKListResponse);
// ModeCMS
rpc CMSIncr(CMSIncrRequest) returns (CMSIncrResponse);
rpc CMSQuery(CMSQueryRequest) returns (CMSQueryResponse);
// ModeVectorSet
rpc VAdd(VAddRequest) returns (VAddResponse);
rpc VRem(VRemRequest) returns (VRemResponse);
rpc VSim(VSimRequest) returns (VSimResponse);
rpc VCard(VCardRequest) returns (VCardResponse);
rpc VDim(VDimRequest) returns (VDimResponse);
rpc VEmb(VEmbRequest) returns (VEmbResponse);
// ModeStream
rpc XAdd(XAddRequest) returns (XAddResponse);
rpc XRange(XRangeRequest) returns (XRangeResponse);
rpc XRevRange(XRevRangeRequest) returns (XRangeResponse);
rpc XLen(XLenRequest) returns (XLenResponse);
rpc XDel(XDelRequest) returns (XDelResponse);
rpc XTrim(XTrimRequest) returns (XTrimResponse);
}
// Messages carry: keyspace, key/name, value/item/member, score, start/stop,
// and on internal paths: version, expire_at_unix_nano, flags, ring_generation
// DeleteResponse / DeleteManyResponse: repeated PeerFailure { peer_id, message }
// PutManyResponse: repeated KeyError { key, message } // partial OK
// ZMember { bytes member; double score }
// HashField { bytes field; bytes value }
Peer listener (internal only — never same port/credentials as public Cache):
service Peer {
rpc ApplyPut(ApplyPutRequest) returns (ApplyPutResponse);
rpc ApplyDelete(ApplyDeleteRequest) returns (ApplyDeleteResponse);
rpc GetOrLoad(GetOrLoadRequest) returns (GetOrLoadResponse);
rpc ForwardPut(PutRequest) returns (PutResponse);
rpc Prefetch(PrefetchRequest) returns (PrefetchResponse);
rpc ListPop(ListPopRequest) returns (ListPopResponse); // ModeList non-owner pop
rpc CounterIncr(CounterIncrRequest) returns (CounterIncrResponse); // ModeCounter non-owner incr
rpc StreamAdd(StreamAddRequest) returns (StreamAddResponse); // ModeStream non-owner XAdd
}
| Endpoint | Purpose |
|---|---|
GET /healthz |
Process up |
GET /readyz |
gRPC servers up; memberlist running; (single-node: ready with ring size 1) |
GET /peers |
Peers, ring_generation, keyspace_config_hash |
GET /keyspaces |
Stats: bytes, items, hits, misses, breaker, hot keys, fan-out errors/drops |
| Control | v1 requirement |
|---|---|
| TLS client↔node | Yes (configurable disable only for local dev) |
| TLS peer↔peer | Yes |
| Separate ports | Client / Peer / Admin |
| Peer auth | mTLS (mesh certs); reject Cache credentials on Peer port |
| Client auth | Optional mTLS or static API token in v1; network policy assumed in k8s |
| Gossip | Optional WithGossipSecret; document rotation = rolling restart with dual secrets if needed later |
| Admin | Bind localhost / private network; optional token |
Threat: client that can reach Peer port can poison cache — mitigated by port split + mTLS.
- Approximate top-K / sampled hits per keyspace (bounded)
- On join/leave: bounded concurrent prefetch of WarmKeys + hot keys
- Refresh-ahead if
RefreshInterval > 0and key still hot - All fills use DataSource protections and version rules (never clobber newer Put)
- Hot-key sketches are local; joiners start cold (acceptable v1)
engine.Get|Put|Delete, peer.ApplyPut|ApplyDelete|GetOrLoad, datasource.Load, warmup.Prefetch
- hits / misses / negative_hits
- get/put/delete latency
- datasource latency / errors
put_fanout_errors,fanout_dropped,apply_staledelete_peer_errors- store: evictions, cost_used, item_count
- breaker state, ratelimit_drops
ring_generation, peer_countget_owner_fallback_local- warmup counts
- Fan-out error ratio
- Fan-out queue depth
- Owner Put latency p99
| Scenario | Behavior |
|---|---|
| Steady state | Owner Put → async fan-out; Gets mostly fresh after ~RTT |
| Fan-out fail | Owner fresh; failed peers stale until TTL / Delete / newer Put |
| Partition | Independent accepts; versions reduce delayed overwrite chaos |
| Heal | No merge protocol; LWW + TTL + Delete converge |
| SoT out-of-band (LoadThrough) | Stale until TTL or Delete |
| Peer down on Delete | MultiError; that peer may serve key |
| Owner down on Put | Put fails (no ACK authority) |
| Owner down on Get miss | Local-only fill fallback (LoadThrough); no fan-out |
| Backend down | Breaker open; fail fast |
| Config drift across nodes | Unsupported; detect via config hash metric |
github.com/Code0987/supercache/
cmd/supercache-node/
api/proto/ // Cache + Peer
pkg/engine/
pkg/keyspace/
pkg/store/ // Store interface + Ristretto impl
pkg/datasource/
pkg/protect/
pkg/membership/ // memberlist wrapper, ring
pkg/client/ // apps import this
pkg/warmup/
pkg/admin/
pkg/telemetry/
internal/peer/ // Peer service impl (not for apps)
internal/ring/
| Area | Options | v1 choice | Why |
|---|---|---|---|
| Local cache | groupcache, mailgun/groupcache, Ristretto, freecache, bigcache, custom | Ristretto (+ Store interface) | Put/Delete/TTL; avoid get-only + dual peer stacks |
| Distribution | Redis cluster, groupcache peers, true shard, full mesh replicate | Full mesh replicate + owner coord | Matches Put fan-out contract + local read hits; memory trade-off documented |
| Membership | K8s endpoints only, serf, memberlist, etcd | memberlist | Decoupled from K8s; standard gossip |
| Consistency | Quorum, linearizable owner reads, pure TTL no versions | EC + version LWW | Keeps no-quorum product goal; fixes multi-writer/prefetch races |
| Topology | Peer-per-app-pod vs dedicated nodes | Dedicated supercache-node | Churn and security |
| Embed vs sidecar | Library-only mesh | Client lib + nodes | Clear ops boundary |
| # | Milestone | Deliverables |
|---|---|---|
| 1 | Store + single-node Engine | Store interface, Ristretto impl, envelope (version/ttl/negative), KeySpace modes, Get/Put/Delete local, LoadThrough singleflight, limits, unit tests |
| 2 | Protections + OTel + admin | rate limit, breaker, traces/metrics, /healthz /readyz /keyspaces |
| 3 | Membership + ring + Put fan-out | memberlist, ring, owner ForwardPut, ApplyPut + versions, worker pool, TLS + port split, gossip secret |
| 4 | Cluster Get/Delete | GetOrLoad, Delete multi-peer MultiError, owner-down fallback |
| 5 | Warmup + events | hot keys, join/leave prefetch, refresh-ahead, Events |
| 6 | Polish | UpdateKeySpace docs + config hash, PutMany/DeleteMany, load/chaos tests, client RYOW docs |
Milestone 1 does not depend on groupcache.
SuperCache is a Go, gRPC, gossip-clustered, keyspace-oriented cache with a bounded local store (Ristretto), load-through DataSources, and async multi-peer fan-out. Dedicated cache nodes form the mesh; applications use a client library. It optimizes read latency and backend offload for workloads that tolerate seconds of staleness. Memory is per-node LRU-bounded and hot data is replicated, not sharded for capacity. It is not a coordination system, durable source of truth, or linearizable store.
Do not use SuperCache for:
- Leader election or distributed locks requiring linearizability
- Financial balances / counters that must not double-apply
- Sole copy of irreplaceable data
- Cross-key transactions
- Assuming one
UpdateKeySpaceupdates the whole cluster - Expecting cluster RAM capacity ≈ N × single-node for the same hot working set without budgeting R× replication
- Running every app replica as a gossip peer
- Race-free concurrent Get/Put under
go test -race - Singleflight: one DataSource load per key under stampede (single owner path)
- Memory respects MaxBytes under load
- Ring updates on join/leave; Events fire
- Put succeeds when owner up even if some fan-out targets fail (
fanout_errorsincrements) - Stale ApplyPut with lower version does not overwrite newer value
- Delete returns MultiError when a peer is down
- TTL expires entries in tests; negative TTL suppresses repeat loads
- Peer port rejects unauthenticated clients in integration test
- OTel metrics include hits, misses, load latency, fan-out errors
| Architect issue | Resolution in this plan |
|---|---|
| Sharding vs fan-out | §2 replicated mesh; capacity formula; non-goal linear memory scale |
| RYOW contradiction | §5 owner-only immediate visibility; client lag documented |
| No versioning | §6 owner versions + LWW apply rules |
| groupcache unfit | §10 custom LRU Store interface |
| DataSource vs Put | §7 LoadThrough vs CacheOnly + precedence |
| Structured types | §7 ModeBloom / ModeSet / ModeZSet / ModeGeo / ModeList / ModeHash / ModeCounter / ModeJSON / ModeBitmap / ModeHLL / ModeTopK / ModeCMS / ModeVectorSet / ModeStream; §9.6; §11 Engine API; §14 Cache RPCs |
| Library vs peers | §4 nodes-only ring; pkg/client for apps |
| Multi-error / PutMany | §11 MultiError; §9.2–9.3 batch rules |
| Get miss underspecified | §9.1 normative algorithm |
| Limits / codec | §6 Entry; §14 limits |
| Security Peer/Client | §15 separate ports + Peer mTLS |
| Fan-out backpressure | §13 worker pool + queue bounds |
| Alternatives | §20 |
| Milestones invent design | §21 store-first; pinned deps |
| Warmup races | version-checked apply §6 §16 |
| Doc | Role |
|---|---|
| docs/WORKFLOW.md | Normative change path (design → TDD → PR → bench → merge) |
| AGENTS.md | Short agent rules + 10% drift bar |
| docs/BENCHMARKS.md | How to read CI and local benches |
| docs/design/ | Per-change design drafts (approved before code) |
End of plan. Implement against this document unless a later revision supersedes it. Process changes go through §0 / WORKFLOW.md first.