ubgo is in early alpha: libraries and APIs may still change.

Skip to content
ubgo Alpha

cache-tiered

Stacks cache backends into L1/L2/L3 tiers with read promotion and per-tier TTL.

go get github.com/ubgo/cache-tiered
Language
Go
Go version
1.24
Core deps
2 direct
Latest tag
v0.1.0
License
Apache-2.0
Go modules
1

Highlights from the README

  • Read-promote. Probe L1→Ln; the first hit is copied into every shallower tier so the next read is faster. Promotions() exposes the count for observability.
  • Write-through (default). Writes go to every tier. The L1 error is authoritative; deeper-tier errors are best-effort so a cold/slow backend cannot break the hot path. WriteOnlyL1 mode writes L1 only.
  • Per-tier TTL. Run a short L1 TTL (bounds cross-pod staleness) and a long L2 TTL from one Set call.
  • Cross-process invalidation. Plug any cache.Invalidation (e.g. Redis Pub/Sub from cache-redis); mutations publish, peers drop their local L1 copy.
  • Atomic ops stay correct. SetNX/Incr/Decr use L1 as the source of truth and mirror the result to deeper tiers.
  • Drop-in. It is a cache.Cache; swap any single backend for a tiered one with no call-site changes.

Code from the README

ctx := context.Background()

b, err := tc.Get(ctx, "k")                       // probe + promote
m, err := tc.GetMulti(ctx, []string{"a", "b"})   // per-key Get
ok, err := tc.Has(ctx, "k")                      // first tier that has it
d, err := tc.TTL(ctx, "k")                       // first tier's reported TTL

err = tc.Set(ctx, "k", []byte("v"), time.Minute) // write-through (per-tier TTL)
err = tc.SetMulti(ctx, map[string]cache.Item{"a": {Value: []byte("1")}})
created, err := tc.SetNX(ctx, "k", []byte("v"), time.Minute) // L1 decides; mirrored down
err = tc.Expire(ctx, "k", time.Hour)             // cascades; per-tier not-found tolerated
err = tc.Touch(ctx, "k")

n, err := tc.Incr(ctx, "hits", 1)                // L1 authoritative; deeper tiers mirrored to n
n, err = tc.Decr(ctx, "hits", 2)

err = tc.Del(ctx, "a", "b")                      // cascade + publish per key
err = tc.DeleteByPrefix(ctx, "user:")            // cascade + publish InvalidateAll
err = tc.Flush(ctx)                              // cascade + publish InvalidateAll

it := tc.Iterate(ctx, cache.IterateOpts{Prefix: "user:"}) // deepest tier (fullest view)
defer it.Close()
for it.Next() { _ = it.Key(); _ = it.Value() }

err = tc.Ping(ctx)                               // first unhealthy tier
s := tc.Stats()                                  // hits/sets summed; entries from deepest tier
p := tc.Promotions()                             // deeper-hit-copied-upward count
err = tc.Close()                                 // idempotent; stops invalidation; closes each tier once

From the cache-tiered README.

At a glance (from the README)

AdapterRoleLatencyCross-process invalidationBest for
cache-tiered (this)composes othersL1 ~ns, then L2/L3Yes (pluggable bus)Hot-path latency + shared consistency
cache-redissingle backendnetworkYes (Pub/Sub source)Shared L2
cache-pgsingle backendnetworkNoDurable L3
cache-memcachedsingle backendnetworkNoMemcached interop tier