Cache expensive work
gas/cache implements gas.CacheProvider twice: in-memory for development and
single-instance deployments, and Valkey (Redis-compatible) for anything running
more than one process. Your services depend on the interface, so which one is
running is a wiring decision.
go get github.com/gasmod/gas/cacheWiring
Section titled “Wiring”app := gas.NewApp( gas.WithServiceInstance[gas.ConfigProvider](cfg), gas.WithSingletonService[gas.Logger](gaslog.NewSlogLogger()),
// Swap cachemem.New() for cachevk.New() and nothing else changes: // services depend on gas.CacheProvider, not on either backend. gas.WithSingletonService[gas.CacheProvider](cachemem.New()),)func production() gas.Option { return gas.WithSingletonService[gas.CacheProvider](cachevk.New())}Cache-aside
Section titled “Cache-aside”type Service struct { cache gas.CacheProvider db gas.DatabaseProvider}
// Read through the cache, fall back to the database, then populate. A cache// miss is a sentinel error, not a nil value, so an empty result stays// distinguishable from an absent one.func (s *Service) Profile(ctx context.Context, id string) (*Profile, error) { key := "profile:" + id
if raw, err := s.cache.Get(ctx, key); err == nil { var p Profile if json.Unmarshal(raw, &p) == nil { return &p, nil } // A corrupt entry is not fatal; fall through and refresh it. } else if !errors.Is(err, cache.ErrKeyNotFound) { return nil, err }
p, err := s.loadProfile(ctx, id) if err != nil { return nil, err }
if raw, err := json.Marshal(p); err == nil { _ = s.cache.Set(ctx, key, raw, 5*time.Minute) } return p, nil}Two details worth copying. A miss is cache.ErrKeyNotFound, a sentinel rather
than a nil value, so an empty result stays distinguishable from an absent one.
And a cache failure that is not a miss is returned rather than swallowed, so a
broken cache surfaces instead of quietly turning into load on your database.
Invalidating
Section titled “Invalidating”func (s *Service) Rename(ctx context.Context, id, name string) error { if err := s.saveName(ctx, id, name); err != nil { return err } // Delete rather than overwrite: the next read repopulates from the // source of truth, so a failed write cannot leave a stale entry behind. return s.cache.Delete(ctx, "profile:"+id)}Prefer deleting over overwriting. If the write to the source of truth succeeds and the cache write then fails, an overwrite leaves a stale entry that outlives the request; a delete just costs the next reader a miss.
Set takes a TTL per entry. Passing zero uses Cache.DefaultTTL, which is
itself zero by default, meaning entries never expire. On the in-memory backend
that makes an unbounded cache, so set either DefaultTTL or MaxEntries if
keys are user-controlled.
Config
Section titled “Config”Memory
| Field | Default | Description |
|---|---|---|
Cache.MaxEntries |
0 |
Maximum entries; 0 is unlimited |
Cache.CleanupInterval |
1m |
How often expired entries are evicted; 0 disables |
Cache.DefaultTTL |
0 |
TTL when Set is called with 0; 0 never expires |
Valkey
| Field | Default | Description |
|---|---|---|
Cache.Addr |
localhost:6379 |
Server address |
Cache.Password |
Empty means no auth | |
Cache.DB |
0 |
Database number |
Cache.DialTimeout |
5s |
Timeout for new connections |
Cache.WriteTimeout |
3s |
Write and periodic PING deadline |
Cache.ConnRetries |
0 |
Connection retry attempts |
Cache.ConnRetryInterval |
2s |
Base retry interval, doubling each attempt |
Health and readiness
Section titled “Health and readiness”The Valkey backend implements both. CheckHealth reports only whether the
service is closed, because the client reconnects internally and a restart would
not fix a transient network fault. CheckReady issues a PING, so traffic
drains off an instance while the dependency is unreachable.
The in-memory backend implements neither on purpose: it has no external dependency and no warmup, so liveness and readiness are exactly the service lifecycle the framework already tracks.
Testing
Section titled “Testing”cachetest.MockCache records calls and takes GetFn, SetFn, DeleteFn, and
ExistsFn overrides, so cache-aside logic can be tested for both the hit and
the miss path without a server. See Test a service.