Documentation
¶
Overview ¶
Package cache provides a generic caching interface with support for multiple backend implementations including Redis and in-memory stores.
Index ¶
Examples ¶
Constants ¶
const NoExpiry time.Duration = -1
NoExpiry is the WithExpiry value for entries that should never expire. It is distinct from zero: a zero expiry means "use the cache's configured default".
Variables ¶
var ( ErrNotFound = errors.New("not found") // ErrNamespaceRequired indicates a Flush (or an unscoped DeleteByPrefix) // was attempted on a provider whose backing store may be shared and which // has no configured key namespace — so it cannot know which entries it // owns. Configure a namespace (e.g. the redis provider's // Config.Namespace) to enable whole-cache operations. ErrNamespaceRequired = errors.New("operation requires a configured key namespace") // its backing store is unreachable — typically a tripped circuit breaker. // // It is deliberately distinct from ErrNotFound. A read that answers "not // found" during an outage is indistinguishable from one that answers it // because the key really is absent, and a caller whose whole purpose is to // notice absence — idempotency's FailClosed, say — is then told exactly what // it must not be told. A write that answers nil during an outage is worse: // it reports that a delete happened, and the stale value it did not delete // is served for the rest of its TTL. // // Callers that would rather treat unavailability as a miss can say so, by // checking for this error; callers that must not cannot be tricked into it. ErrUnavailable = errors.New("cache is unavailable") )
Functions ¶
func EffectiveExpiry ¶
EffectiveExpiry resolves a write's options against a cache's default expiry, returning the duration the entry should live: a positive duration, or zero meaning "never expire". The three input states resolve as documented on WithExpiry — an unset/zero expiry takes defaultExpiry, and a negative expiry (NoExpiry) or negative default resolves to no expiry. Providers should treat this as the single source of truth for expiry semantics so backends cannot drift.
Types ¶
type CBORCodec ¶
type CBORCodec[T any] struct{}
CBORCodec is the default Codec, using CBOR (RFC 8949). It is exported, and returned by NewCBORCodec, so a caller can depend on the codec it built rather than on the Codec seam.
func NewCBORCodec ¶
NewCBORCodec returns the default CBOR-backed Codec.
It is the default because a cache holds small values one at a time, which is the shape gob is worst at: gob earns its compactness by amortizing type descriptors across a stream, and a cache entry has to be independently decodable by a process that never saw the stream prologue, so every entry re-transmits the full descriptor. On a five-field session struct that lands gob above JSON on size while staying Go-only. CBOR is smaller than both and readable by anything that speaks a documented wire format.
Types need no annotation: a field with no cbor tag falls back to its json tag. Interface-typed fields are the one thing this codec will not do — see NewGobCodec.
time.Time round-trips to the nanosecond, with its UTC offset. The named location does not survive, here or in any other portable format, so compare decoded times with time.Time.Equal rather than == .
func NewDefaultCodec ¶
NewDefaultCodec returns the Codec a serializing provider uses when the caller supplies none. That is CBOR today; it was gob before.
It exists so "the default" has one spelling that moves when the default does. When CBOR replaced gob, every test and doc comment naming NewGobCodec kept passing while describing something that was no longer true — and authorization.PermissionSet, which carried gob methods precisely because it cannot be encoded structurally, became silently unencodable. A type you intend to cache should be round-tripped through this, not through a named codec, so that the next change of default fails the test instead of the deployment.
The return type names today's default, so a caller that wrote the result into a Codec[T] keeps compiling and a caller that named the concrete type finds out at the next change rather than at the next deployment.
Example ¶
ExampleNewDefaultCodec shows the codec the serializing providers use when given none. Consumers reach for Codec only to replace it — redis.WithCodec accepts any implementation whose Decode round-trips its own Encode. Note the migration caveat: values written under one codec are unreadable through another.
It is also the codec to round-trip a type through before caching it, rather than a named one, so that a type which only a particular codec can encode is caught here rather than in a deployment.
package main
import (
"fmt"
"github.com/primandproper/platform-go/v10/cache"
)
func main() {
type session struct {
UserID string
Roles []string
}
codec := cache.NewDefaultCodec[session]()
encoded, err := codec.Encode(&session{UserID: "u-1", Roles: []string{"admin", "auditor"}})
if err != nil {
panic(err)
}
decoded, err := codec.Decode(encoded)
if err != nil {
panic(err)
}
fmt.Println(decoded.UserID, decoded.Roles)
}
Output: u-1 [admin auditor]
type Cache ¶
type Cache[T any] interface { Get(ctx context.Context, key string) (*T, error) // GetMany fetches multiple keys in as few round trips as possible. // Missing keys are omitted from the returned map, so a key's absence // from the result is a cache miss. GetMany(ctx context.Context, keys []string) (map[string]*T, error) Set(ctx context.Context, key string, value *T, opts ...WriteOption) error // SetIfPresent overwrites key only if it currently holds a value, // reporting ErrNotFound without writing when it does not. It is the // conditional half of Set, and it resolves WriteOptions the same way. // // It exists because "update what is there, and do not create it" is not // expressible as a read followed by a Set. Between those two calls the // entry can be deleted, and the Set then puts it back — which for a // caller whose deletes mean something (a revoked session, a released // claim) undoes the delete rather than losing a race harmlessly. This // is one operation and cannot be interleaved with one. // // It is not a compare-and-swap: it tests existence, not the value. A // caller that must not overwrite a *changed* value wants a lock — see // distributedlock — and the two compose, since this narrows what the // lock has to cover rather than replacing it. // // Providers that store nothing report ErrNotFound always, because // nothing is ever present in them. That makes a noop cache visibly // unable to serve a caller who needs this, which is the honest answer: // reporting success would claim a conditional write happened against a // store that holds no conditions. SetIfPresent(ctx context.Context, key string, value *T, opts ...WriteOption) error // SetMany stores multiple values at once. WriteOptions apply to the // whole batch: every item gets the same expiry resolution as a single // Set call would. SetMany(ctx context.Context, items map[string]*T, opts ...WriteOption) error Delete(ctx context.Context, key string) error // DeleteMany removes multiple keys in as few round trips as possible. // Keys that are already absent are not an error. DeleteMany(ctx context.Context, keys []string) error // DeleteByPrefix removes every entry whose key begins with prefix. // Providers on shared backing stores that have no configured // namespace reject an empty prefix with ErrNamespaceRequired, since // that would delete entries they cannot prove they own. DeleteByPrefix(ctx context.Context, prefix string) error // Flush removes every entry this cache owns. Providers whose backing // store may be shared (redis) require a configured namespace to know // what they own and return ErrNamespaceRequired without one; // providers that wholly own their store (memory) always succeed. Flush(ctx context.Context) error Ping(ctx context.Context) error // Close releases the resources the cache holds — a connection pool, a // background sweep — and is safe to call more than once. It does not // evict anything: entries in a shared backing store outlive the handle // that wrote them. After Close the cache must not be used again. Close() error }
Cache is our wrapper interface for a cache. Batched reads, writes, and deletes are part of the interface — every provider implements them, and batching is the primary access pattern for high-volume consumers.
Writes accept optional WriteOptions. A write with no options (or a zero expiry) uses the cache's configured default expiry; WithExpiry overrides it per call, and WithExpiry(NoExpiry) pins the entry against expiry entirely.
Keys are scoped to the cache instance: a provider with a configured namespace prepends it to every key transparently, so callers never see or supply namespaced keys. There is deliberately no way to reach entries outside the cache's own namespace — a generic inspect-everything client is a debugging tool, not a production surface, and belongs in a separate Debug type if it is ever needed.
Example (Batch) ¶
package main
import (
"context"
"fmt"
"github.com/primandproper/platform-go/v10/cache/memory"
)
func main() {
ctx := context.Background()
c, err := memory.NewInMemoryCache[string](0)
if err != nil {
panic(err)
}
// Batched reads and writes are part of Cache itself — no assertion needed.
one, two := "one", "two"
if err = c.SetMany(ctx, map[string]*string{"k1": &one, "k2": &two}); err != nil {
panic(err)
}
// Missing keys are simply absent from the result.
results, err := c.GetMany(ctx, []string{"k1", "k2", "missing"})
if err != nil {
panic(err)
}
fmt.Println(len(results))
fmt.Println(*results["k1"])
}
Output: 2 one
Example (NotFound) ¶
package main
import (
"context"
"errors"
"fmt"
"github.com/primandproper/platform-go/v10/cache"
"github.com/primandproper/platform-go/v10/cache/memory"
)
func main() {
ctx := context.Background()
c, cacheErr := memory.NewInMemoryCache[string](0)
if cacheErr != nil {
panic(cacheErr)
}
_, err := c.Get(ctx, "nonexistent")
fmt.Println(err)
fmt.Println(errors.Is(err, cache.ErrNotFound))
}
Output: not found true
Example (SetAndGet) ¶
package main
import (
"context"
"fmt"
"github.com/primandproper/platform-go/v10/cache/memory"
)
func main() {
ctx := context.Background()
c, err := memory.NewInMemoryCache[string](0)
if err != nil {
panic(err)
}
value := "cached-value"
if err = c.Set(ctx, "my-key", &value); err != nil {
panic(err)
}
result, err := c.Get(ctx, "my-key")
if err != nil {
panic(err)
}
fmt.Println(*result)
}
Output: cached-value
type Codec ¶
Codec converts values to and from the byte representation a serializing provider stores (redis today; the memory provider holds values directly and never encodes). NewDefaultCodec is what a provider uses when given none. NewGobCodec is the opt-in for values CBOR cannot carry — interface-typed fields, chiefly — and consumers with tight size or latency budgets can supply their own fixed-format Codec through the provider's constructor options.
A Codec must be safe for concurrent use, and Decode must round-trip whatever Encode produced — the same value back, not the same bytes. Encodings that vary between calls (CBOR does not sort map keys) satisfy this; anything that decodes to a different value does not.
A cached type whose fields are all unexported has nothing for a structural codec to encode, and the failure is quiet: CBOR writes the empty map and reads it back with no error, so the cache serves a zero value as a clean hit. Give such a type MarshalBinary and UnmarshalBinary — every codec here honors encoding.BinaryMarshaler — and see authorization.PermissionSet for the shape.
Values written with one codec are unreadable through another, and entries already in a shared store carry no record of which codec wrote them. Changing a deployed cache's codec therefore means changing the provider's namespace so the old entries age out in their own keyspace, or flushing.
type GobCodec ¶
type GobCodec[T any] struct{}
GobCodec is the opt-in Codec for values CBOR cannot carry, using encoding/gob. It is exported, and returned by NewGobCodec, so a caller can depend on the codec it built rather than on the Codec seam.
func NewGobCodec ¶
NewGobCodec returns the gob-backed Codec. Types must be gob-friendly: exported fields only, and interface-typed fields need their concrete types registered with gob.Register.
It was the default until CBOR replaced it (NewCBORCodec, which is smaller on the wire and not Go-only), and is retained for the two things gob does that CBOR does not: interface-typed fields resolved through gob.Register, and decoding into a struct that has drifted from the one that was encoded. Reach for it when a cached value has either property, and keep in mind that entries written by one codec are unreadable through another.
type WriteConfig ¶
type WriteConfig struct {
// Expiry holds the caller's requested expiry: zero for "use the
// cache's default", NoExpiry (or any negative value) for "never
// expire", or a positive duration.
Expiry time.Duration
}
WriteConfig is the resolved per-call write configuration. Providers normally consume it through EffectiveExpiry rather than directly; it is exported so third-party Cache implementations can run the same resolution.
type WriteOption ¶
type WriteOption func(*WriteConfig)
WriteOption configures a single Set or SetMany call.
func WithExpiry ¶
WithExpiry sets the expiry for the entries written by this call. Zero defers to the cache's configured default; NoExpiry (or any negative duration) stores the entries without expiry.
Directories
¶
| Path | Synopsis |
|---|---|
|
Package cachecfg selects and builds a cache.Cache[T] from configuration: either the in-process memory cache or Redis.
|
Package cachecfg selects and builds a cache.Cache[T] from configuration: either the in-process memory cache or Redis. |
|
Package memory is a cache.Cache held in a map in this process.
|
Package memory is a cache.Cache held in a map in this process. |
|
Package cachemock provides moq-generated mock implementations of interfaces in the cache package.
|
Package cachemock provides moq-generated mock implementations of interfaces in the cache package. |
|
Package noop is the cache.Cache for a caller who wants no cache at all: every read misses and every write is accepted and forgotten.
|
Package noop is the cache.Cache for a caller who wants no cache at all: every read misses and every write is accepted and forgotten. |
|
Package redis is a cache.Cache backed by Redis, in either single-node or cluster mode.
|
Package redis is a cache.Cache backed by Redis, in either single-node or cluster mode. |
|
slots
Package slots produces Redis Cluster keys whose slots are pre-planned to distribute evenly across the cluster's nodes.
|
Package slots produces Redis Cluster keys whose slots are pre-planned to distribute evenly across the cluster's nodes. |