Documentation
¶
Overview ¶
Package cache provides a driver-backed cache with typed access, atomic locks, tagged invalidation and events.
There are two layers. Store is the driver contract: it deals only in []byte, so a driver never has to know how a value is encoded. Cache sits above it and owns encoding, key prefixing, events and the generic API most callers want.
Index ¶
- Constants
- Variables
- func Add(ctx context.Context, key string, value any, ttl time.Duration) (bool, error)
- func BlockUntil(ctx context.Context, wait, interval time.Duration, ...) (bool, error)
- func Decrement(ctx context.Context, key string, delta int64) (int64, error)
- func Flush(ctx context.Context) error
- func Forget(ctx context.Context, key string) (bool, error)
- func FormatCounter(n int64) []byte
- func Get[T any](ctx context.Context, key string) (T, bool, error)
- func GetMany[T any](ctx context.Context, keys []string) (map[string]T, error)
- func Has(ctx context.Context, key string) (bool, error)
- func Increment(ctx context.Context, key string, delta int64) (int64, error)
- func ParseCounter(value []byte) (int64, error)
- func Pull[T any](ctx context.Context, key string) (T, bool, error)
- func Put(ctx context.Context, key string, value any, ttl time.Duration) error
- func PutForever(ctx context.Context, key string, value any) error
- func Register(name string, factory Factory)
- func Remember[T any](ctx context.Context, key string, ttl time.Duration, ...) (T, error)
- func RememberForever[T any](ctx context.Context, key string, fn func(context.Context) (T, error), ...) (T, error)
- func RunLocked(ctx context.Context, l Lock, fn func(context.Context) error) (bool, error)
- func SetGlobal(c *Cache)
- func SupportedDrivers() []string
- type Cache
- func (c *Cache) Add(ctx context.Context, key string, value any, ttl time.Duration) (bool, error)
- func (c *Cache) Codec() Codec
- func (c *Cache) Decrement(ctx context.Context, key string, delta int64) (int64, error)
- func (c *Cache) Flush(ctx context.Context) error
- func (c *Cache) Forever(ctx context.Context, key string, value any) error
- func (c *Cache) Forget(ctx context.Context, key string) (bool, error)
- func (c *Cache) GetAs[T any](ctx context.Context, key string) (T, bool, error)
- func (c *Cache) GetManyAs[T any](ctx context.Context, keys []string) (map[string]T, error)
- func (c *Cache) Has(ctx context.Context, key string) (bool, error)
- func (c *Cache) Increment(ctx context.Context, key string, delta int64) (int64, error)
- func (c *Cache) Listen(name string, l Listener)
- func (c *Cache) Lock(name string, ttl time.Duration, opts ...LockOption) Lock
- func (c *Cache) Missing(ctx context.Context, key string) (bool, error)
- func (c *Cache) PullAs[T any](ctx context.Context, key string) (T, bool, error)
- func (c *Cache) Put(ctx context.Context, key string, value any, ttl time.Duration) error
- func (c *Cache) RememberAs[T any](ctx context.Context, key string, ttl time.Duration, ...) (T, error)
- func (c *Cache) RememberForeverAs[T any](ctx context.Context, key string, fn func(context.Context) (T, error), ...) (T, error)
- func (c *Cache) RestoreLock(name, owner string, ttl time.Duration) Lock
- func (c *Cache) Store() Store
- func (c *Cache) Tags(names ...string) *Cache
- type CacheFlushed
- type CacheHit
- type CacheMissed
- type Codec
- type Config
- type Event
- type Factory
- type FileConfig
- type GobCodec
- type JSONCodec
- type KeyForgotten
- type KeyWritten
- type Listener
- type Lock
- type LockOption
- type LockProvider
- type Option
- type Provider
- type Pruner
- type RedisConfig
- type RememberOption
- type Store
- type TagIndex
Constants ¶
const ( EventHit = "cache.hit" EventMissed = "cache.missed" EventKeyWritten = "cache.key_written" EventKeyForgotten = "cache.key_forgotten" EventFlushed = "cache.flushed" )
Event names, usable as the key when cache events are bridged onto the application's event emitter.
const ConfigStub = `package configs
import "github.com/lemmego/api/config"
func init() {
config.Set("cache", config.M{
// file keeps the cache on disk, so the CLI and the server share it.
// memory is faster but private to one process, which makes
// ` + "`lemmego run cache:clear`" + ` unable to reach the server's cache.
"driver": config.MustEnv("CACHE_DRIVER", "file"),
// Every key is written under this prefix, and it is what bounds a
// flush — important on a Redis database shared with sessions.
"prefix": config.MustEnv("CACHE_PREFIX", "lemmego_cache:"),
// Seconds.
"ttl": config.MustEnv("CACHE_TTL", 3600),
// json is portable and readable; gob preserves Go types exactly.
"codec": config.MustEnv("CACHE_CODEC", "json"),
// Publish cache events onto the application emitter. Listeners run
// synchronously on the goroutine that touched the cache.
"events": config.MustEnv("CACHE_EVENTS", false),
// Serve without a cache rather than failing to boot when the
// configured backend is unreachable. Every read then misses.
"lenient": config.MustEnv("CACHE_LENIENT", false),
"stores": config.M{
"file": config.M{
"path": config.MustEnv("CACHE_FILE_PATH", "storage/framework/cache"),
},
"redis": config.M{
"addr": config.MustEnv("CACHE_REDIS_ADDR", "127.0.0.1:6379"),
"password": config.MustEnv("CACHE_REDIS_PASSWORD", ""),
"db": config.MustEnv("CACHE_REDIS_DB", 1),
},
},
})
}
`
ConfigStub is the cache configuration published into a project by `lemmego publish --tags=config`, and written by `lemmego new`.
const Forever time.Duration = 0
Forever is the TTL that means "this entry does not expire".
Note the deliberate difference from Laravel, where a non-positive TTL deletes the key. A zero time.Duration is what an uninitialised struct field holds, and "unset config silently deletes your cache entry" is a trap. At this layer a non-positive TTL therefore stores forever; the Cache layer removes the ambiguity entirely by rejecting a non-positive TTL in Put and offering Cache.Forever as the only way to write a non-expiring entry.
const MemoryGCInterval = time.Minute
MemoryGCInterval is how often the memory driver sweeps expired entries.
const TagConfig = "cache-config"
TagConfig is what `lemmego publish --tags` selects this module's configuration on.
Variables ¶
var ( // ErrMiss reports that a key is not in the cache. Drivers return it from // Get rather than a nil value, so a cached empty value stays // distinguishable from an absent one, and so the distinction survives // error wrapping through decorators such as the tagged store. ErrMiss = errors.New("cache: key not found") // ErrNotNumeric reports an Increment or Decrement against a value that is // not a counter. ErrNotNumeric = errors.New("cache: value is not numeric") // ErrUnsupported reports that a driver cannot perform the operation. It is // always better than a silent no-op: a lock that pretends to have been // acquired is worse than one that refuses. ErrUnsupported = errors.New("cache: operation not supported by this store") )
var ErrInvalidTTL = errors.New("cache: ttl must be positive; use Forever to store without expiry")
ErrInvalidTTL reports a non-positive TTL passed to Put.
At the Store layer a non-positive TTL means forever. Here it is refused, so that an unset duration — a zero struct field, a config key that did not parse — is a loud error rather than a silent write of a value that never expires. Forever is how you ask for that on purpose.
var ErrNotInitialized = errors.New("cache: provider is not initialized")
ErrNotInitialized reports a package-level call made before the provider has installed a cache.
The package functions return it rather than panicking, so an application that has not configured a cache fails the one call rather than the process — and so a caller can treat a missing cache as a permanent miss if it wants to.
var ErrTagsRequireTTL = errors.New("cache: this store cannot store a value forever under tags; give it a ttl or use a store with a tag index")
ErrTagsRequireTTL reports an attempt to store a non-expiring value under tags on a store that cannot enumerate tag members.
Tag invalidation works by changing the namespace keys are written under, which makes the old entries unreachable but leaves them to expire on their own. An entry stored forever would never expire, so it would occupy space permanently with no way to reach or remove it. Refusing is better than a leak nobody can find; a store implementing TagIndex can delete the members and has no such limit.
Functions ¶
func BlockUntil ¶
func BlockUntil(ctx context.Context, wait, interval time.Duration, acquire func(context.Context) (bool, error)) (bool, error)
BlockUntil is the shared Block implementation: poll acquire with jittered backoff until the deadline. Jitter matters because without it every waiter wakes on the same schedule and they retry in lockstep forever.
func FormatCounter ¶
FormatCounter renders a counter as a store writes it.
func ParseCounter ¶
ParseCounter reads a counter, returning ErrNotNumeric for anything else.
func PutForever ¶
PutForever stores value under key with no expiry.
func Register ¶
Register makes a driver available to Build under name.
Drivers register themselves from an init function and are selected by a blank import, the way database/sql drivers are. The indirection is what lets the drivers depend on this package — they need Store and ErrMiss — without this package depending on them, and it means an application that only caches in memory does not compile a Redis client into its binary.
Importing github.com/lemmego/cache/drivers registers all four built-in stores at once.
func Remember ¶
func Remember[T any](ctx context.Context, key string, ttl time.Duration, fn func(context.Context) (T, error), opts ...RememberOption) (T, error)
Remember returns the cached value for key, computing and storing it on a miss.
func RememberForever ¶
func RememberForever[T any](ctx context.Context, key string, fn func(context.Context) (T, error), opts ...RememberOption) (T, error)
RememberForever is Remember with no expiry.
func RunLocked ¶
RunLocked is the shared Get implementation. The release is deferred so a panic in fn still frees the lock rather than holding it until its TTL.
func SetGlobal ¶
func SetGlobal(c *Cache)
SetGlobal installs the cache the package-level functions use. The Provider calls it at boot; tests call it with nil to tear it down.
func SupportedDrivers ¶
func SupportedDrivers() []string
SupportedDrivers lists the registered driver names.
Types ¶
type Cache ¶
type Cache struct {
// contains filtered or unexported fields
}
Cache is the API almost everything should use. It owns encoding, key prefixing and events, and delegates storage to a Store.
Cache is a concrete type and there is deliberately no interface over it. Several of its methods declare their own type parameters, which Go 1.27 permits only on concrete types: a method with type parameters can never satisfy an interface, so a Cacher abstraction carrying GetAs[T] would not compile and adding one later would silently delete the typed API.
func FromApp ¶
FromApp resolves the cache from the service container.
Other providers in this framework spell this Get, but here that name belongs to the generic facade function callers reach for constantly — cache.Get[User](ctx, key) — so the resolver takes the longer name rather than the API you type every day.
func Tags ¶
Tags returns a tagged view of the installed cache. With no cache installed it returns nil, and the methods on a nil *Cache would panic — so prefer the method form, cache.Global().Tags(...), where the cache may be absent.
func (*Cache) GetAs ¶
GetAs returns the decoded value for key.
The bool distinguishes a miss from a stored zero value, which matters for every type whose zero is meaningful — 0, false, "". A miss is not an error: err is reserved for a store that failed or a value that would not decode, so callers can write `if err != nil { return err }` and mean it.
func (*Cache) GetManyAs ¶
GetManyAs returns the decoded values present among keys. Absent keys are omitted rather than reported.
func (*Cache) Lock ¶
Lock returns an atomic lock named name.
The scope of the exclusion is the scope of the store: one process for memory, one host for file, the cluster for redis. A store that cannot lock returns a lock that refuses rather than one that pretends.
func (*Cache) Put ¶
Put stores value under key for ttl. A non-positive ttl is an error; use Forever to store without expiry.
func (*Cache) RememberAs ¶
func (c *Cache) RememberAs[T any]( ctx context.Context, key string, ttl time.Duration, fn func(context.Context) (T, error), opts ...RememberOption, ) (T, error)
RememberAs returns the cached value for key, computing and storing it on a miss.
Concurrent callers in this process collapse to a single call to fn. If fn fails, nothing is cached and the error is returned — a failed computation must not be remembered as though it had succeeded.
func (*Cache) RememberForeverAs ¶
func (c *Cache) RememberForeverAs[T any]( ctx context.Context, key string, fn func(context.Context) (T, error), opts ...RememberOption, ) (T, error)
RememberForeverAs is RememberAs with no expiry.
func (*Cache) RestoreLock ¶
RestoreLock rebinds a lock to a known owner token, so a lock taken in one place can be released in another — acquired in a web request, released by the job the request queued.
type CacheFlushed ¶
type CacheFlushed struct {
Tags []string
}
CacheFlushed reports a whole cache, or one tag namespace, being emptied.
func (CacheFlushed) EventName ¶
func (CacheFlushed) EventName() string
type CacheMissed ¶
CacheMissed reports a key that was not found.
func (CacheMissed) EventName ¶
func (CacheMissed) EventName() string
type Codec ¶
type Codec interface {
// Name identifies the codec in configuration and in error messages.
Name() string
Marshal(v any) ([]byte, error)
Unmarshal(data []byte, v any) error
}
Codec turns values into the bytes a Store holds, and back.
The driver never sees a Go value, which is what keeps a driver from needing to know about gob type registration, and what makes a value written by one process readable by another written in a different language.
type Config ¶
type Config struct {
// Driver selects the store: memory, file, redis or null.
Driver string
// Prefix namespaces every key. It also bounds what Flush removes, which is
// why a store sharing its backing with other data insists on one.
Prefix string
// DefaultTTL is used where a caller does not give one.
DefaultTTL time.Duration
// Codec selects the encoding: json or gob.
Codec string
// Events enables bridging cache events onto the application's emitter.
// Off by default: dispatching is synchronous, so a listener runs on the
// goroutine that read the cache.
Events bool
// Lenient degrades to the null store when the configured backend cannot be
// reached, rather than failing the boot. The cache then misses on every
// read, which is slow but serving.
Lenient bool
File FileConfig
Redis RedisConfig
}
Config describes how to build a cache.
func DefaultConfig ¶
func DefaultConfig() *Config
DefaultConfig returns the configuration used when nothing overrides it.
The driver is file rather than memory on purpose: the CLI and the server are separate processes, so a memory cache would make `cache:clear` a silent no-op and leave the two with different views of the same data.
type Event ¶
type Event interface{ EventName() string }
Event is one thing that happened to the cache.
Events are concrete types rather than a map or an any payload, so a listener reads e.Key instead of asserting its way to it.
type GobCodec ¶
type GobCodec struct{}
GobCodec preserves Go types exactly, including unexported fields via GobEncoder. In exchange, any type reached through an interface field must be registered with gob.Register first, and the bytes are readable only by Go.
type JSONCodec ¶
type JSONCodec struct{}
JSONCodec is the default. It is readable in redis-cli, portable across languages, and cannot be tripped up by an unregistered type.
Its limitation is Go type fidelity: a value round-tripped through any arrives as float64 rather than int, and unexported fields are dropped. Decode into a concrete type — which the generic API does — and neither matters.
type KeyForgotten ¶
KeyForgotten reports a key removed.
func (KeyForgotten) EventName ¶
func (KeyForgotten) EventName() string
type KeyWritten ¶
KeyWritten reports a value stored. A zero TTL means it was stored forever.
func (KeyWritten) EventName ¶
func (KeyWritten) EventName() string
type Listener ¶
Listener receives events.
It is called on the goroutine that caused the event, so a slow listener slows the cache operation that triggered it — and it is handed that caller's context, so a listener can read whatever the caller put there. That is what makes a cache event attributable: an observer reading a request id from the context can say which request made the lookup, which a func(Event) could never do.
The context is the caller's own, so it may already be cancelled — a listener that does further work with it should expect that rather than assume a live context.
type Lock ¶
type Lock interface {
// Acquire takes the lock if it is free, and reports whether it did.
Acquire(ctx context.Context) (bool, error)
// Block waits up to wait for the lock, polling with backoff. Reaching the
// deadline returns (false, nil): a lock that was busy is an expected
// outcome, not a failure.
Block(ctx context.Context, wait time.Duration) (bool, error)
// Get acquires the lock, runs fn, and releases it — including if fn
// panics. It reports whether the lock was acquired; fn does not run if it
// was not.
Get(ctx context.Context, fn func(context.Context) error) (bool, error)
// Release gives up the lock if this owner still holds it, and reports
// whether it did. A false return means the lock had already expired and
// been taken by someone else.
Release(ctx context.Context) (bool, error)
// ForceRelease removes the lock whoever holds it.
ForceRelease(ctx context.Context) error
// Owner returns this holder's token, which RestoreLock can rebind so a
// lock taken in one place is released in another.
Owner() string
}
Lock is an atomic lock held across whatever scope its store spans: a process for the memory store, a host for the file store, a cluster for redis. The store's documentation says which, and the distinction matters — a process-local lock guarding work that runs on three machines guards nothing.
A lock carries an owner token so that only the holder can release it. Without that, a lock whose TTL expired mid-work would be released by its original holder after someone else had already acquired it.
func NewLock ¶
func NewLock(name string, ttl time.Duration, opts ...LockOption) Lock
NewLock returns an atomic lock from the installed cache.
It is NewLock rather than Lock because Lock is the interface it returns. With no cache installed the lock refuses to acquire rather than pretending, so a caller that does not check never proceeds as though it held one.
type LockOption ¶
type LockOption func(*lockOptions)
LockOption configures a lock.
func WithOwner ¶
func WithOwner(token string) LockOption
WithOwner binds the lock to a known owner token instead of a fresh one.
func WithRetryInterval ¶
func WithRetryInterval(d time.Duration) LockOption
WithRetryInterval sets how often Block re-checks the lock.
type LockProvider ¶
LockProvider is implemented by stores that can back an atomic lock.
type Option ¶
type Option func(*Cache)
Option configures a Cache.
func WithEventSink ¶
WithEventSink registers a listener for every event, which is how cache events are bridged onto an application's event emitter.
type Provider ¶
type Provider struct {
// Config overrides what the application's cache config says. Fields left
// zero fall through to it.
Config *Config
// contains filtered or unexported fields
}
Provider wires a cache into an application.
Register it in bootstrap/providers.go, and import the drivers you want:
import _ "github.com/lemmego/cache/drivers"
func LoadProviders() []app.Provider {
return []app.Provider{ &cache.Provider{} }
}
func (*Provider) AddCommands ¶
AddCommands contributes the cache CLI commands.
func (*Provider) AddPublishables ¶
func (p *Provider) AddPublishables() []*app.Publishable
AddPublishables offers the cache config file to `lemmego publish`.
func (*Provider) Provide ¶
Provide builds the cache and registers it.
An application with no cache configuration gets a working cache on the defaults rather than an error: returning one from Provide aborts the boot, and a project that has not published a cache config has not asked for the app to stop. Configuration that is present but wrong is a different matter and is reported.
type Pruner ¶
Pruner is implemented by stores that need an explicit sweep to reclaim expired entries, rather than reclaiming them in the background.
type RedisConfig ¶
type RedisConfig struct {
Addr string
Password string
DB int
PoolSize int
// AllowFlushDB lets Flush empty the whole database instead of scanning for
// the prefix. Only safe when the database holds nothing but this cache.
AllowFlushDB bool
}
RedisConfig configures the redis driver.
type RememberOption ¶
type RememberOption func(*rememberOptions)
RememberOption configures a Remember call.
func LockFor ¶
func LockFor(ttl time.Duration) RememberOption
LockFor makes a rebuild take a lock, so that across processes only one rebuilds the value.
Without it, Remember collapses concurrent rebuilds within one process but not between processes — four servers missing the same key at once run the callback four times. With it, three of them wait for the winner and then read what it wrote. Use it when the callback is expensive enough that duplicating it matters, and note that it is only as strong as the store's lock: on a memory store it still only spans one process.
func WaitFor ¶
func WaitFor(d time.Duration) RememberOption
WaitFor sets how long a loser waits for the rebuild to finish.
type Store ¶
type Store interface {
// Get returns the raw value for key, or ErrMiss if it is absent or expired.
Get(ctx context.Context, key string) ([]byte, error)
// GetMany returns the values present among keys. Absent keys are omitted
// from the map rather than reported as ErrMiss: in a bulk read, some
// missing is the normal case and not a failure.
GetMany(ctx context.Context, keys []string) (map[string][]byte, error)
// Put stores value under key. A non-positive ttl stores it forever.
Put(ctx context.Context, key string, value []byte, ttl time.Duration) error
// PutMany stores every pair under one shared expiry.
PutMany(ctx context.Context, values map[string][]byte, ttl time.Duration) error
// Add stores value only if key is absent, and reports whether it did.
// It must be atomic: it is the primitive locks are built on.
Add(ctx context.Context, key string, value []byte, ttl time.Duration) (bool, error)
// Increment adds delta to the counter at key and returns the new value,
// creating the key at delta if it is absent. An existing expiry is
// preserved. A non-counter value returns ErrNotNumeric.
Increment(ctx context.Context, key string, delta int64) (int64, error)
// Decrement subtracts delta from the counter at key.
Decrement(ctx context.Context, key string, delta int64) (int64, error)
// Forget removes key and reports whether it was present.
Forget(ctx context.Context, key string) (bool, error)
// Flush removes every entry this store owns.
//
// It must not remove anything else. A store sharing its backing with other
// data — a redis database that also holds sessions, a directory that also
// holds other files — must scope the removal to its own prefix.
Flush(ctx context.Context) error
// Prefix returns the namespace every key this store writes lives under.
Prefix() string
}
Store is the driver contract.
Implementations deal only in bytes. Every method takes a context so drivers that talk over a network can honour cancellation; drivers that cannot are free to ignore it, and say so in their documentation.
Keys passed in are logical: the store applies its own Prefix. A caller must not prepend it, and two stores with different prefixes over the same backing must not see each other's entries.
A Store must be safe for concurrent use.
func Build ¶
Build creates the store a Config describes.
A backend that cannot be reached is reported here rather than on first use, so a wrong address fails the boot instead of surfacing later as a failed request. Config.Lenient turns that into a warning and a store that keeps nothing: every read then misses, which is slow but still serving.
type TagIndex ¶
type TagIndex interface {
AddTagEntries(ctx context.Context, tag string, keys ...string) error
TagEntries(ctx context.Context, tag string) ([]string, error)
ForgetTagEntries(ctx context.Context, tag string) error
}
TagIndex is implemented by stores that can enumerate the keys written under a tag. Tag invalidation works without it by bumping a version, but only a store with an index can flush entries written with Forever.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package drivers registers every built-in cache store.
|
Package drivers registers every built-in cache store. |
|
store
|
|
|
file
Package file provides a cache store backed by the filesystem.
|
Package file provides a cache store backed by the filesystem. |
|
memory
Package memory provides an in-process cache store.
|
Package memory provides an in-process cache store. |
|
null
Package null provides a cache store that discards everything.
|
Package null provides a cache store that discards everything. |
|
redis
Package redis provides a cache store backed by Redis.
|
Package redis provides a cache store backed by Redis. |
|
Package storetest is a conformance suite every cache store must pass.
|
Package storetest is a conformance suite every cache store must pass. |