{"id":30774366,"url":"https://github.com/unkn0wn-root/cascache","last_synced_at":"2025-10-26T22:35:38.353Z","repository":{"id":311817024,"uuid":"1043168652","full_name":"unkn0wn-root/cascache","owner":"unkn0wn-root","description":"CAS-safe cache with read-validated singles, bulk set validation, pluggable providers/codecs, and optional shared generations.","archived":false,"fork":false,"pushed_at":"2025-08-26T20:04:41.000Z","size":99,"stargazers_count":34,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-08-27T03:24:57.377Z","etag":null,"topics":["bigcache","cache","caching","go","golang","redis","ristretto"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/unkn0wn-root.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2025-08-23T09:29:49.000Z","updated_at":"2025-08-27T03:05:13.000Z","dependencies_parsed_at":"2025-08-27T03:28:27.070Z","dependency_job_id":"24535ddb-fb04-46f0-83d3-c458a4c2880b","html_url":"https://github.com/unkn0wn-root/cascache","commit_stats":null,"previous_names":["unkn0wn-root/cascache"],"tags_count":10,"template":false,"template_full_name":null,"purl":"pkg:github/unkn0wn-root/cascache","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/unkn0wn-root%2Fcascache","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/unkn0wn-root%2Fcascache/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/unkn0wn-root%2Fcascache/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/unkn0wn-root%2Fcascache/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/unkn0wn-root","download_url":"https://codeload.github.com/unkn0wn-root/cascache/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/unkn0wn-root%2Fcascache/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":273703688,"owners_count":25153002,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-09-05T02:00:09.113Z","response_time":402,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["bigcache","cache","caching","go","golang","redis","ristretto"],"created_at":"2025-09-05T02:54:07.056Z","updated_at":"2025-10-26T22:35:33.323Z","avatar_url":"https://github.com/unkn0wn-root.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# CasCache\n\nProvider-agnostic CAS like (**C**ompare-**A**nd-**S**et or generation-guarded conditional set) cache with pluggable codecs and a pluggable generation store.\nSafe single-key reads (no stale values), optional bulk caching with read-side validation,\nand an opt‑in distributed mode for multi-replica deployments.\n\n---\n\n## Contents\n- [Why CasCache](#why-cascache)\n- [Getting started](#getting-started)\n- [Design](#design)\n- [Wire format](#wire-format)\n- [Providers](#providers)\n- [Codecs](#codecs)\n- [Distributed generations (multi-replica)](#distributed-generations-multi-replica)\n- [API](#api)\n- [Cache Type alias](#cache-type-alias)\n- [Performance notes](#performance-notes)\n\n---\n\n## Why CasCache\n\n#### TL;DR\nIf you’ve ever shipped “delete-then-set” and still served stale data (or fought weird race windows in multi-replica setups), **cascache** gives you a simple guarantee:\n\n\u003e **After you invalidate a key, the cache will never serve the old value again.**\n\u003e No ad-hoc deletes, no timing games, no best-effort TTLs.\n\nIt does this with **generation-guarded writes** (CAS) and **read-side validation** using a tiny per-key counter.\n\n---\n\n### What goes wrong with “normal” caches\n\n| Pattern             | What you do                            | What still goes wrong                                                                 |\n|---------------------|----------------------------------------|---------------------------------------------------------------------------------------|\n| **TTL only**        | Set `user:42` for 5m                   | Readers can see **up to 5m stale** after a write. Reducing TTL increases DB load.     |\n| **Delete then set** | `DEL user:42` then `SET user:42`       | Races: a reader between `DEL` and new `SET` repopulates from **old DB snapshot**.     |\n| **Write-through**   | Update DB, then cache                  | Concurrent readers can serve **old data** until invalidation is coordinated perfectly.|\n| **Version in value**| Store `{version, payload}`             | Readers still need **current version**; coordinating that is the same hard problem.   |\n\n---\n\n### What CasCache guarantees\n\n- **No stale reads after invalidate:**\n  Each key has a **generation**. Mutations call `Invalidate(key)` → bump gen. Reads accept a cached value **only if** its stored gen == current gen; otherwise it is **deleted** and treated as a miss.\n\n- **Safe conditional writes (CAS):**\n  Writers snapshot gen **before** reading the DB. `SetWithGen(k, v, obs)` only commits if gen hasn’t changed. If something else updated the key, your write is **skipped** (prevents racing old data into the cache).\n\n- **Graceful failure modes:**\n  If the gen store is slow/unavailable, singles/reads **self-heal** (treat as miss) and CAS writes **skip**. You don’t serve stale; you just do a little more work.\n\n- **Optional bulk that isn’t risky:**\n  Bulk entries are validated **member-by-member** on read. If any is stale, the bulk is dropped and you fall back to singles. (Extras in the bulk are ignored; missing members invalidate the bulk.)\n\n- **Pluggable:**\n  Works with **Ristretto/BigCache/Redis** for values and **JSON/CBOR/Msgpack/Proto** for payloads. Wire decode is tight and zero-copy for payloads.\n\n---\n\n### When to use it\n\n- You render entities that **must not be stale** after updates (profiles, product detail, permissions, pricing, feature flags).\n- You’ve got **multiple replicas** and coordinating invalidations is painful.\n- You want **predictable semantics** under incidents: serve fresh or miss, never “maybe stale.”\n\n### When *not* to use it\n\n- “A little staleness is fine” (feed pages, metrics tiles). Plain TTL might be enough.\n- Keys are **write-hot** (every read followed by a write) - caching won’t help.\n- You need **dogpile prevention** (single-flight). CasCache doesn’t include it; add it at the call site if needed.\n\n---\n\n### Why this beats the usual tricks\n\n- **TTL** trades freshness for load. CasCache gives **freshness** and keeps TTLs for eviction only.\n- **Delete-then-set** has races. Generations remove the race by making freshness a **property of the read**, not perfect timing.\n- **Write-through** still needs coordination. CAS makes coordination trivial: **bump → snapshot → compare**.\n\n---\n\n### Minimal mental model\n\n```text\nDB write succeeds  →  Cache.Invalidate(k)       // bump gen; clear single\nRead slow path     →  snap := SnapshotGen(k) → load DB → SetWithGen(k, v, snap)\nRead fast path     →  Get(k) validates stored gen == current; else self-heals\nBulk read          →  every member’s gen must match; else drop bulk → singles\nMulti-replica      →  use RedisGenStore so all nodes see the same gen\n```\n\n---\n\n## Getting started\n#### 1) Build the cache\n\n```go\nimport (\n\t\"context\"\n\t\"time\"\n\n\t\"github.com/unkn0wn-root/cascache\"\n\t\"github.com/unkn0wn-root/cascache/codec\"\n\trp \"github.com/unkn0wn-root/cascache/provider/ristretto\"\n)\n\ntype User struct{ ID, Name string }\n\nfunc newUserCache() (cascache.CAS[User], error) {\n\trist, err := rp.New(rp.Config{\n\t\tNumCounters: 1_000_000,\n\t\tMaxCost:     64 \u003c\u003c 20, // 64 MiB\n\t\tBufferItems: 64,\n\t\tMetrics:     false,\n\t})\n\tif err != nil { return nil, err }\n\n\treturn cascache.New[User](cascache.Options[User]{\n\t\tNamespace:  \"user\",\n\t\tProvider:   rist,\n\t\tCodec:      codec.JSON[User]{},\n\t\tDefaultTTL: 5 * time.Minute,\n\t\tBulkTTL:    5 * time.Minute,\n\t\t// GenStore: nil -\u003e Local (single-process) generations\n\t})\n}\n```\n\n#### 2) Safe single read (never stale)\n\n```go\ntype UserRepo struct {\n\tCache cascache.CAS[User]\n\t// db handle...\n}\n\nfunc (r *UserRepo) GetByID(ctx context.Context, id string) (User, error) {\n\tif u, ok, _ := r.Cache.Get(ctx, id); ok {\n\t\treturn u, nil\n\t}\n\n\t// CAS snapshot BEFORE reading DB\n\tobs := r.Cache.SnapshotGen(id)\n\n\tu, err := r.dbSelectUser(ctx, id) // your DB load\n\tif err != nil { return User{}, err }\n\n\t// Conditionally cache only if generation didn't move\n\t_ = r.Cache.SetWithGen(ctx, id, u, obs, 0)\n\treturn u, nil\n}\n```\n\n#### 3) Mutations invalidate (one line)\n\u003e Rule: after a successful DB write, call Invalidate(key) to bump the generation.\n\n```go\nfunc (r *UserRepo) UpdateName(ctx context.Context, id, name string) error {\n\tif err := r.dbUpdateName(ctx, id, name); err != nil { return err }\n\t_ = r.Cache.Invalidate(ctx, id) // bump gen + clear single\n\treturn nil\n}\n```\n\n#### 4) Optional bulk (safe set caching)\n\u003e If any member is stale, the bulk is dropped and you fall back to singles. In multi-replica apps, use a shared GenStore (below) or disable bulk.\n\n```go\nfunc (r *UserRepo) GetMany(ctx context.Context, ids []string) (map[string]User, error) {\n\tvalues, missing, _ := r.Cache.GetBulk(ctx, ids)\n\tif len(missing) == 0 {\n\t\treturn values, nil\n\t}\n\n\t// Snapshot *before* DB read\n\tobs := r.Cache.SnapshotGens(missing)\n\n\t// Load missing from DB in one shot\n\tloaded, err := r.dbSelectUsers(ctx, missing)\n\tif err != nil { return nil, err }\n\n\t// Index for SetBulkWithGens\n\titems := make(map[string]User, len(loaded))\n\tfor _, u := range loaded { items[u.ID] = u }\n\n\t// Conditionally write bulk (or it will seed singles if any gen moved)\n\t_ = r.Cache.SetBulkWithGens(ctx, items, obs, 0)\n\n\t// Merge and return\n\tfor k, v := range items { values[k] = v }\n\treturn values, nil\n}\n```\n\n---\n\n## Distributed generations (multi-replica)\n\nLocal generations are correct within a single process. In multi-replica deployments, share generations to eliminate cross-node windows and keep bulks safe across nodes.\n\u003e LocalGenStore is **single-process only**. In multi-replica setups, both singles and bulks can be stale on nodes that haven’t observed the bump.\nUse a shared GenStore (e.g., Redis) for cross-replica correctness or run a single instance.\n\n\n```go\nimport \"github.com/redis/go-redis/v9\"\n\nfunc newUserCacheDistributed() (cascache.CAS[User], error) {\n\trist, _ := rp.New(rp.Config{NumCounters:1_000_000, MaxCost:64\u003c\u003c20, BufferItems:64})\n\trdb := redis.NewClient(\u0026redis.Options{Addr: \"127.0.0.1:6379\"})\n\tgs  := cascache.NewRedisGenStoreWithTTL(rdb, \"user\", 90*24*time.Hour)\n\n\treturn cascache.New[User](cascache.Options[User]{\n\t\tNamespace: \"user\",\n\t\tProvider:  rist,                      // or Redis/BigCache for values\n\t\tCodec:     codec.JSON[User]{},\n\t\tGenStore:  gs,                        // shared generations\n\t\tBulkTTL:   5 * time.Minute,\n\t})\n}\n```\n\nYou can reuse the same client across caches:\n\n```go\nrdb := redis.NewClusterClient(/* ... */) // or UniversalClient\n\nuserCache, _ := cascache.New[user](cascache.Options[user]{\n    Namespace: \"app:prod:user\",\n    Provider:  myRedisProvider{Client: rdb},               // same client\n    Codec:     c.JSON[user]{},\n    GenStore:  gen.NewRedisGenStoreWithTTL(rdb, \"app:prod:user\", 24*time.Hour),\n})\n\npageCache, _ := cascache.New[page](cascache.Options[page]{\n    Namespace: \"app:prod:page\",\n    Provider:  myRedisProvider{Client: rdb},               // same client\n    Codec:     c.JSON[page]{},\n    GenStore:  gen.NewRedisGenStoreWithTTL(rdb, \"app:prod:page\", 24*time.Hour),\n})\n\npermCache, _ := cascache.New[permission](cascache.Options[permission]{\n    Namespace: \"app:prod:perm\",\n    Provider:  myRedisProvider{Client: rdb},               // same client\n    Codec:     c.JSON[permission]{},\n    GenStore:  gen.NewRedisGenStoreWithTTL(rdb, \"app:prod:perm\", 24*time.Hour),\n})\n```\n---\n\n\u003e **Alternative type name:** You can use `cascache.Cache[V]` instead of `cascache.CAS[V]`. See [Cache Type alias](#cache-type-alias).\n\n---\n\n## Design\n\n### Read path (single)\n\n```\n      Get(k)\n        │\n  provider.Get(\"single:\u003cns\u003e:\"+k) ───► [wire.DecodeSingle]\n        │\n   gen == currentGen(\"single:\u003cns\u003e:\"+k) ?\n        │ yes                                 no\n        ▼                                    ┌───────────────┐\n  codec.Decode(payload)                      │ Del(entry)    │\n        │                                    │ return miss   │\n      return v                               └───────────────┘\n```\n\n### Write path (single, CAS)\n\n```\nobs := SnapshotGen(k)        // BEFORE DB read\nv   := DB.Read(k)\nSetWithGen(k, v, obs)        // write iff currentGen(k) == obs\n```\n\n### Bulk read validation\n\n```\nGetBulk(keys)   -\u003e provider.Get(\"bulk:\u003cns\u003e:hash(sorted(keys))\")\nDecode -\u003e [(key,gen,payload)]*n\nfor each item: gen == currentGen(\"single:\u003cns\u003e:\"+key) ?\nif all valid -\u003e decode all, return\nelse         -\u003e drop bulk, fall back to singles\n```\n\n### Components\n- **Provider** - byte store with TTLs: Ristretto/BigCache/Redis.\n- **Codec[V]** - serialize/deserialize your `V` to/from `[]byte`.\n- **GenStore** - per-key generation counter (Local by default; Redis available).\n\n### Keys\n- Single entry: `single:\u003cns\u003e:\u003ckey\u003e`\n- Bulk entry:   `bulk:\u003cns\u003e:\u003cfirst16(sha256(sorted(keys))\u003e\u003e`\n\n### CAS model\n- Per-key generation is **monotonic**.\n- Reads validate only; no write amplification.\n- Mutations call `Invalidate(key)` → bump generation and delete the single entry.\n\n---\n\n## Wire format\n\nSmall binary envelope before the codec payload. Big-endian integers. Magic `\"CASC\"`.\n\n**Single**\n\n```\n+---------+---------+---------+---------------+---------------+-------------------+\n| magic   | version | kind    | gen (u64)     | vlen (u32)    | payload (vlen)    |\n| \"CASC\"  |   0x01  |  0x01   | 8 bytes       | 4 bytes       | vlen bytes        |\n+---------+---------+---------+---------------+---------------+-------------------+\n```\n\n**Bulk**\n\n```\n+---------+---------+---------+------------------------+\n| magic   | version | kind    | n (u32)                |\n+---------+---------+---------+------------------------+\nrepeated n times:\n+----------------+-----------------+----------------+-------------------+------------------+\n| keyLen (u16)   | key (keyLen)    | gen (u64)      | vlen (u32)        | payload (vlen)   |\n+----------------+-----------------+----------------+-------------------+------------------+\n```\n\nDecoders are zero-copy for payloads and keys (one `string` alloc per bulk item).\n\n---\n\n## Providers\n\n```go\ntype Provider interface {\n    Get(ctx context.Context, key string) ([]byte, bool, error)\n    Set(ctx context.Context, key string, value []byte, cost int64, ttl time.Duration) (bool, error)\n    Del(ctx context.Context, key string) error\n    Close(ctx context.Context) error\n}\n```\n\n- **Ristretto**: in-process; per-entry TTL; cost-based eviction.\n- **BigCache**: in-process; global life window; per-entry TTL ignored.\n- **Redis**: distributed (optional); per-entry TTL.\n\nUse any provider for values. Generations can be local or distributed independently.\n\n---\n\n## Codecs\n\n```go\ntype Codec[V any] interface {\n    Encode(V) ([]byte, error)\n    Decode([]byte) (V, error)\n}\ntype JSON[V any] struct{}\n```\n\nYou can drop in Msgpack/CBOR/Proto or decorators (compression/encryption). CAS is codec-agnostic.\n\n---\n\n## API\n\n```go\ntype CAS[V any] interface {\n    Enabled() bool\n    Close(context.Context) error\n\n    // Single\n    Get(ctx context.Context, key string) (V, bool, error)\n    SetWithGen(ctx context.Context, key string, value V, observedGen uint64, ttl time.Duration) error\n    Invalidate(ctx context.Context, key string) error\n\n    // Bulk\n    GetBulk(ctx context.Context, keys []string) (map[string]V, []string, error)\n    SetBulkWithGens(ctx context.Context, items map[string]V, observedGens map[string]uint64, ttl time.Duration) error\n\n    // Generations\n    SnapshotGen(key string) uint64\n    SnapshotGens(keys []string) map[string]uint64\n}\n```\n---\n\n## Cache Type Alias\n\nFor readability, we provide a type alias:\n\n```go\ntype Cache[V any] = CAS[V]\n```\n\nYou may use either name. They are **identical types**. Example:\n\n```go\nvar a cascache.CAS[User]\nvar b cascache.Cache[User]\n\na = b // ok\nb = a // ok\n```\n\nIn examples we often use `CAS` to emphasize the CAS semantics, but `Cache` is equally valid and may read more naturally in your codebase.\n\n---\n\n## Performance notes\n\n- **Time:** O(1) singles; O(n) bulk for n members.\n- **Allocations:** zero-copy wire decode; one `string` alloc per bulk item.\n- **Ristretto cost hint:** evict bulks first under pressure.\n```go\nComputeSetCost: func(key string, raw []byte, isBulk bool, n int) int64 {\n    if isBulk { return int64(n) }\n    return 1\n}\n```\n- **TTLs:** `DefaultTTL`, `BulkTTL`. For BigCache, the global life window applies.\n- **Cleanup (local gens):** periodic prune by last bump time (default retention 30d).\n\n---\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Funkn0wn-root%2Fcascache","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Funkn0wn-root%2Fcascache","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Funkn0wn-root%2Fcascache/lists"}