Cache examples
A tour of the cache — values and TTLs, counters and rate limits, distributed locks, cache-aside with Remember, invalidation, and what changes when there is no redis at all.
Read Cache first for the concepts — these files show them wired together.
git clone https://gitlab.finema.co/finema/idin-core.git
cd idin-core/v2/examples/cache
go run .Storing values
What goes in, what comes back, and the two operations that are not plain "store this" — a write that only happens when nothing is there, and a counter whose window is its own TTL.
Coordination
The one thing in this section that is not about speed: making a piece of work happen once across every replica of a service.
Patterns and testing
Cache-aside as one call, keeping the copy from going stale, and proving both in a test that needs no redis.
Putting it together
main.go wires one cache and runs every example against it. It falls back to the in-process memory cache when no redis is configured or reachable, so go run . works on a laptop with nothing installed — which is the property the rest of the topic is built around.
// Command cache is a runnable tour of the v2 cache: values, counters, locks,
// cache-aside, invalidation and testing.
//
// Each example lives in its own file:
//
// 01_values.go get/set/delete, encoding, TTLs, key naming, prefixes
// 02_counters.go Incr as a one-call rate limiter, and counters worth having
// 03_locks.go Lock/LockWait, lock TTLs, and what a crashed holder leaves
// 04_remember.go cache-aside with Remember, stampedes, negative caching
// 05_invalidation.go invalidate on write, DelByPrefix, telling other replicas
// 06_testing.go NewMemoryCache in tests, and code that survives no cache
//
// It runs with no redis. With no CACHE_* configuration — or with one that will
// not answer — it falls back to the in-process memory cache, which is a real
// cache in every way except that it cannot span processes. So `go run .` works
// on a laptop with nothing installed, and the code below is unchanged either
// way, which is the point the whole package is built around.
//
// Run it with: go run ./examples/cache
package main
import (
"context"
"time"
core "gitlab.finema.co/finema/idin-core/v2"
)
func main() {
env, err := core.NewEnv()
if err != nil {
panic(err)
}
// 1. one cache handle for the process. app.Shutdown closes it, so nothing
// here calls Close on the cache it handed over.
cache, reason := openCache(env)
app, err := core.NewApp(env, core.WithCache("default", cache))
if err != nil {
panic(err)
}
app.Log().Info("cache backend chosen", "why", reason)
logCacheMode(app)
// 2. ctx.Cache() returns a handle already bound to this context, which is
// why none of the cache calls in the examples take a ctx of their own.
// A cancelled request abandons its cache calls with it.
ctx := app.NewContext(context.Background(), core.ModeTest)
runValues(ctx)
runCounters(ctx)
runLocks(ctx)
runRemember(ctx)
runInvalidation(app, ctx)
runTesting(ctx)
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
_ = app.Shutdown(shutdownCtx)
}
// openCache picks a backend the way a service should: use redis when it is
// configured and reachable, and degrade to something that still works when it
// is not.
//
// The fallback here is the *memory* cache rather than the disabled one, because
// this is a demo and the examples are more interesting when values come back.
// A real service usually wants the opposite default — NewApp with no cache at
// all gives the disabled backend, whose reads miss and whose writes are
// dropped, so the code path stays identical in every environment. Falling back
// to memory in a multi-replica deployment is worse than having no cache: rate
// limits count per pod, locks exclude nothing, and invalidation messages reach
// one replica out of N.
func openCache(env core.IENV) (core.ICache, string) {
cfg := env.Config()
if cfg.CacheHost == "" && cfg.CacheAddrs == "" && cfg.CacheConnectionString == "" {
return core.NewMemoryCache(), "no CACHE_* configuration: in-process memory cache"
}
// NewCache PINGs before returning, so a misconfigured cache is a boot error
// rather than a surprise on the first request that touches it. A service
// would panic here; the tour prefers to keep running.
c, err := core.NewCache(env)
if err != nil {
return core.NewMemoryCache(), "redis is configured but unreachable (" + err.Error() + ")"
}
return c, "redis"
}