Documentation
¶
Overview ¶
Package cache is a small, typed caching layer with two implementations and one rule: a cache failure is never a request failure.
Every method here degrades to a miss. A Redis that is down makes the service slower; it must not make it broken. That is the single most important property of a cache wrapper and the one most hand-rolled ones get wrong, by returning the Redis error up the stack and turning a latency problem into a 500.
Example ¶
GetOrSet is the usual shape: return the cached value, or load it, store it and return it.
package main
import (
"context"
"fmt"
"time"
"github.com/IncepTools/go/src/platform/cache"
)
type product struct {
ID string `json:"id"`
Name string `json:"name"`
}
func main() {
c := cache.NewMemory(time.Minute)
defer c.Close() //nolint:errcheck
ctx := context.Background()
loads := 0
load := func(context.Context) (product, error) {
loads++
return product{ID: "9c1e", Name: "Desk lamp"}, nil
}
key := cache.Key("catalog", "acct-1a3c", "product", "9c1e")
for range 3 {
p, err := cache.GetOrSet(ctx, c, key, time.Minute, load)
if err != nil {
panic(err)
}
_ = p
}
fmt.Println("loads:", loads)
}
Output: loads: 1
Index ¶
- Variables
- func Get[T any](ctx context.Context, c Cache, key string) (T, bool)
- func GetOrSet[T any](ctx context.Context, c Cache, key string, ttl time.Duration, ...) (T, error)
- func Key(parts ...string) string
- func Set[T any](ctx context.Context, c Cache, key string, value T, ttl time.Duration)
- type Cache
- type Memory
- func (m *Memory) Close() error
- func (m *Memory) Delete(_ context.Context, keys ...string) error
- func (m *Memory) DeletePrefix(_ context.Context, prefix string) error
- func (m *Memory) Get(_ context.Context, key string) ([]byte, error)
- func (m *Memory) Ping(context.Context) error
- func (m *Memory) Set(_ context.Context, key string, value []byte, ttl time.Duration) error
- type Noop
- type Redis
- func (r *Redis) Close() error
- func (r *Redis) Delete(ctx context.Context, keys ...string) error
- func (r *Redis) DeletePrefix(ctx context.Context, prefix string) error
- func (r *Redis) Get(ctx context.Context, key string) ([]byte, error)
- func (r *Redis) Ping(ctx context.Context) error
- func (r *Redis) Set(ctx context.Context, key string, value []byte, ttl time.Duration) error
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var ErrMiss = errors.New("cache: miss")
ErrMiss is returned by Get when the key is absent. It is the only error a caller normally handles; everything else is logged and reported as a miss.
Functions ¶
func GetOrSet ¶
func GetOrSet[T any](ctx context.Context, c Cache, key string, ttl time.Duration, load func(ctx context.Context) (T, error)) (T, error)
GetOrSet returns the cached value, or calls load, stores the result and returns it.
load's error is returned to the caller UNCACHED. Caching a failure is occasionally what you want — see Negative below — but it must be a decision, never a default: a cached 500 outlives the incident that caused it.
Example (DoesNotCacheFailures) ¶
A failure is never cached. Caching one means a transient outage outlives the incident that caused it.
package main
import (
"context"
"errors"
"fmt"
"time"
"github.com/IncepTools/go/src/platform/cache"
)
type product struct {
ID string `json:"id"`
Name string `json:"name"`
}
func main() {
c := cache.NewMemory(time.Minute)
defer c.Close() //nolint:errcheck
loads := 0
load := func(context.Context) (product, error) {
loads++
return product{}, errors.New("upstream down")
}
for range 3 {
_, _ = cache.GetOrSet(context.Background(), c, "k", time.Minute, load)
}
fmt.Println("loads:", loads)
}
Output: loads: 3
func Key ¶
Key builds a namespaced cache key from parts: Key("catalog", "product", id) gives "catalog:product:<id>".
Always namespace. An un-namespaced key collides across modules, and the collision presents as one module reading another's value with the right type and the wrong meaning — which is far harder to find than a crash.
Example ¶
Always namespace a key, and include the account where the data is per-account. A key without it is one customer's data served to another.
package main
import (
"fmt"
"github.com/IncepTools/go/src/platform/cache"
)
func main() {
fmt.Println(cache.Key("catalog", "acct-1a3c", "product", "9c1e"))
}
Output: catalog:acct-1a3c:product:9c1e
Types ¶
type Cache ¶
type Cache interface {
Get(ctx context.Context, key string) ([]byte, error)
Set(ctx context.Context, key string, value []byte, ttl time.Duration) error
Delete(ctx context.Context, keys ...string) error
// DeletePrefix removes every key under a prefix. On Redis this is a SCAN,
// not a KEYS: KEYS blocks the server for the length of the keyspace, and
// on a production instance that is an outage.
DeletePrefix(ctx context.Context, prefix string) error
Ping(ctx context.Context) error
}
Cache is the storage contract.
An implementation MUST treat a backend failure on Get as a miss, and a backend failure on Set as a no-op. It should log; it must not propagate.
type Memory ¶
type Memory struct {
// contains filtered or unexported fields
}
Memory is an in-process cache with TTL expiry.
It is correct for a single instance, for development, and for data that is genuinely per-process. It is NOT shared: with N replicas you have N caches that invalidate independently, so a Delete on one leaves the other N-1 serving the old value until it expires. Where that matters, use Redis.
func NewMemory ¶
NewMemory returns an in-process cache that sweeps expired entries every interval (default 1 minute). Call Close to stop the sweeper.
func (*Memory) DeletePrefix ¶
DeletePrefix implements Cache.
type Noop ¶
type Noop struct{}
Noop is a cache that stores nothing. Inject it to disable caching without making every call site nil-check.
func (Noop) DeletePrefix ¶
DeletePrefix implements Cache.
type Redis ¶
type Redis struct {
// contains filtered or unexported fields
}
Redis is a shared cache backed by a Redis server.
func NewRedis ¶
func NewRedis(client redis.UniversalClient, prefix string) *Redis
NewRedis wraps a client. prefix is prepended to every key.
func (*Redis) Delete ¶
Delete implements Cache.
Unlike Get and Set, a delete failure is RETURNED. An invalidation that silently did not happen leaves the cache serving data the database no longer has, and the caller may want to refuse the write rather than accept that.
func (*Redis) DeletePrefix ¶
DeletePrefix removes every key under a prefix using SCAN in batches.
SCAN, never KEYS: KEYS is O(keyspace) and blocks the single-threaded server for its duration, which on a production instance is an outage caused by a cache invalidation.