Help us shape the future of Go! Take the 2026 Go Developer Survey

cache

package module
v0.3.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 1, 2026 License: MIT Imports: 23 Imported by: 0

README

lemmego/cache

A cache for Lemmego applications: several stores behind one API, with typed access, atomic locks, tagged invalidation and events.

import (
    "github.com/lemmego/cache"
    _ "github.com/lemmego/cache/drivers"
)

users, err := cache.Remember(ctx, "users:active", 5*time.Minute,
    func(ctx context.Context) ([]User, error) {
        return repo.ActiveUsers(ctx)
    })

Two layers

Store is the driver contract and deals only in []byte, so a driver never has to know how a value is encoded. Cache sits above it and owns encoding, events and the generic API.

Cache is a concrete type and there is no interface over it. Several of its methods declare their own type parameters, which Go permits only on concrete types — a method with type parameters can never satisfy an interface — so a Cacher interface would silently cost you the typed API.

Stores

memory file redis null
Survives a restart no yes yes —
Shared between processes no same host yes —
Locks this process this host cluster-wide none (see below)
Tag flush removes entries yes on expiry yes —
Stores Forever under tags yes refused yes —

The default is file, not memory: the CLI and the server are separate processes, so a memory cache would leave cache:clear unable to reach the cache the server is using.

null keeps nothing. Reads always miss, so it exercises the cache-miss path on every request — useful in tests, and what lenient falls back to when the real backend is unreachable. Its locks always "succeed" and therefore exclude nothing; that is Laravel's behaviour too, and it is worth knowing before you rely on a lock in a test.

Only the drivers you import are compiled in. _ "github.com/lemmego/cache/drivers" registers all four; import individual packages under store/ to keep a Redis client out of a binary that will never use one.

Typed access

user, found, err := c.GetAs[User](ctx, "user:7")

The bool is not redundant with the error. A miss is not a failure, so err stays reserved for a store that broke or a value that would not decode — and the bool is what separates a miss from a cached 0, false or "".

Remember

Remember computes a value on a miss and caches it. Concurrent callers in one process collapse to a single computation. If the callback fails, nothing is cached: a failed computation must not be remembered as though it had worked.

Across processes, add cache.LockFor(d) — one process rebuilds while the others wait and then read what it wrote. It is opt-in because it costs a lock round trip, and because it is only as strong as the store's lock.

Locks

lock := c.Lock("import", time.Minute)
ran, err := lock.Get(ctx, func(ctx context.Context) error {
    return importEverything(ctx)
})

Locks carry an owner token, so only the holder can release one. Without that, a holder whose lease expired mid-work would release a lock someone else had already taken, and two workers would run at once. On Redis the release is a single Lua compare-and-delete for the same reason — reading the owner and then deleting are two operations with a window between them.

Block returns (false, nil) at its deadline. A lock that was busy is an expected outcome, not an error.

A store that cannot lock returns a lock that refuses, rather than one that pretends to have been acquired.

Tags

c.Tags("posts").Put(ctx, "1", post, time.Hour)
c.Tags("posts").Flush(ctx)

Tags returns a *Cache, so everything — including Remember — works on a tagged cache with no second API.

Flushing mints a new version for each tag, which is one write per tag and no scan. Entries under the old version become unreachable immediately and then expire on their own. On a store that can enumerate tag members (memory, redis) they are deleted as well, which is what makes a non-expiring tagged entry safe; on one that cannot (file), writing Forever under tags is refused rather than leaking an entry nothing can reach.

Events

c.Listen(cache.EventMissed, func(ctx context.Context, e cache.Event) {
    slog.InfoContext(ctx, "cache miss", "key", e.(cache.CacheMissed).Key)
})

Events cost nothing when nobody is listening: subscriptions are summarised into one atomic word, and the event value is built inside that check. Listeners run synchronously on the goroutine that touched the cache, and a listener that panics is contained rather than allowed to fail the request.

A listener is handed the context of the call that caused the event, which is what makes a cache operation attributable: an observer that reads a request id from the context can say which request made the lookup. The context is the caller's own, so it may already be cancelled — a listener doing further work with it should expect that.

Set events: true in configuration to also publish them on the application's event emitter. That bridge drops the context, because app.Dispatch takes none.

Configuration

lemmego publish --tags=config writes internal/configs/cache.go. The settings that matter most:

Key Default
driver file memory, file, redis, null
prefix lemmego_cache: Namespaces keys, and bounds what a flush removes
ttl 3600 Seconds
codec json json or gob
events false Publish events on the app emitter
lenient false Serve without a cache rather than failing to boot

An unreachable backend fails the boot by default, so a wrong address stops a deployment instead of quietly degrading it. lenient turns that into a warning and a cache that keeps nothing.

prefix is load-bearing on Redis. A Lemmego application usually shares one Redis database between its cache, its sessions and its queue, so Flush scans for the prefix rather than calling FLUSHDB — which would log every user out and drop queued work.

Commands

lemmego run cache:clear      empty the cache
lemmego run cache:forget k   remove one key
lemmego run cache:prune      reclaim expired entries (file store)

Testing

go test ./...
go test -race ./...
GOWORK=off go test ./...

storetest is exported, so a store of your own can prove itself against the same contract the built-in drivers are held to:

storetest.Run(t, storetest.Capabilities{...}, newStore)

Redis has a second suite against a real server, skipped unless configured:

CACHE_REDIS_ADDR=127.0.0.1:6379 go test ./store/redis/

It covers what a simulator cannot — real expiry, the server's own Lua engine, SCAN paging, and clients genuinely contending for a lock. Locks should not be trusted on the strength of a simulator.

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

View Source
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.

View Source
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`.

View Source
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.

View Source
const MemoryGCInterval = time.Minute

MemoryGCInterval is how often the memory driver sweeps expired entries.

View Source
const TagConfig = "cache-config"

TagConfig is what `lemmego publish --tags` selects this module's configuration on.

Variables

View Source
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")
)
View Source
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.

View Source
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.

View Source
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 Add

func Add(ctx context.Context, key string, value any, ttl time.Duration) (bool, error)

Add stores value only if key is absent, and reports whether it did.

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 Decrement

func Decrement(ctx context.Context, key string, delta int64) (int64, error)

Decrement subtracts delta from a counter.

func Flush

func Flush(ctx context.Context) error

Flush empties the cache.

func Forget

func Forget(ctx context.Context, key string) (bool, error)

Forget removes key and reports whether it was present.

func FormatCounter

func FormatCounter(n int64) []byte

FormatCounter renders a counter as a store writes it.

func Get

func Get[T any](ctx context.Context, key string) (T, bool, error)

Get returns the decoded value for key from the installed cache.

func GetMany

func GetMany[T any](ctx context.Context, keys []string) (map[string]T, error)

GetMany returns the decoded values present among keys.

func Has

func Has(ctx context.Context, key string) (bool, error)

Has reports whether key is present.

func Increment

func Increment(ctx context.Context, key string, delta int64) (int64, error)

Increment adds delta to a counter.

func ParseCounter

func ParseCounter(value []byte) (int64, error)

ParseCounter reads a counter, returning ErrNotNumeric for anything else.

func Pull

func Pull[T any](ctx context.Context, key string) (T, bool, error)

Pull returns the decoded value for key and removes it.

func Put

func Put(ctx context.Context, key string, value any, ttl time.Duration) error

Put stores value under key for ttl.

func PutForever

func PutForever(ctx context.Context, key string, value any) error

PutForever stores value under key with no expiry.

func Register

func Register(name string, factory Factory)

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

func RunLocked(ctx context.Context, l Lock, fn func(context.Context) error) (bool, error)

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

func FromApp(a app.App) *Cache

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 Global

func Global() *Cache

Global returns the installed cache, or nil.

func New

func New(store Store, opts ...Option) *Cache

New returns a Cache over store.

func Tags

func Tags(names ...string) *Cache

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) Add

func (c *Cache) Add(ctx context.Context, key string, value any, ttl time.Duration) (bool, error)

Add stores value only if key is absent, and reports whether it did.

func (*Cache) Codec

func (c *Cache) Codec() Codec

Codec returns the configured codec.

func (*Cache) Decrement

func (c *Cache) Decrement(ctx context.Context, key string, delta int64) (int64, error)

Decrement subtracts delta from a counter.

func (*Cache) Flush

func (c *Cache) Flush(ctx context.Context) error

Flush empties the cache — or, on a tagged cache, just that tag namespace.

func (*Cache) Forever

func (c *Cache) Forever(ctx context.Context, key string, value any) error

Forever stores value under key with no expiry.

func (*Cache) Forget

func (c *Cache) Forget(ctx context.Context, key string) (bool, error)

Forget removes a key and reports whether it was present.

func (*Cache) GetAs

func (c *Cache) GetAs[T any](ctx context.Context, key string) (T, bool, error)

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

func (c *Cache) GetManyAs[T any](ctx context.Context, keys []string) (map[string]T, error)

GetManyAs returns the decoded values present among keys. Absent keys are omitted rather than reported.

func (*Cache) Has

func (c *Cache) Has(ctx context.Context, key string) (bool, error)

Has reports whether a key is present and unexpired.

func (*Cache) Increment

func (c *Cache) Increment(ctx context.Context, key string, delta int64) (int64, error)

Increment adds delta to a counter, preserving any expiry it already has.

func (*Cache) Listen

func (c *Cache) Listen(name string, l Listener)

Listen registers a listener for one event name.

func (*Cache) Lock

func (c *Cache) Lock(name string, ttl time.Duration, opts ...LockOption) 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) Missing

func (c *Cache) Missing(ctx context.Context, key string) (bool, error)

Missing is the negation of Has.

func (*Cache) PullAs

func (c *Cache) PullAs[T any](ctx context.Context, key string) (T, bool, error)

PullAs returns the decoded value for key and removes it.

func (*Cache) Put

func (c *Cache) Put(ctx context.Context, key string, value any, ttl time.Duration) error

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

func (c *Cache) RestoreLock(name, owner string, ttl time.Duration) Lock

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.

func (*Cache) Store

func (c *Cache) Store() Store

Store returns the underlying driver, for the rare caller that needs bytes or a driver-specific capability.

func (*Cache) Tags

func (c *Cache) Tags(names ...string) *Cache

Tags returns a view of the cache namespaced by tags, so a group of entries can be invalidated together.

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 CacheHit

type CacheHit struct {
	Key   string
	Tags  []string
	Value []byte
}

CacheHit reports a key that was found. Value is the stored bytes, before decoding.

func (CacheHit) EventName

func (CacheHit) EventName() string

type CacheMissed

type CacheMissed struct {
	Key  string
	Tags []string
}

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 Factory

type Factory func(ctx context.Context, cfg *Config) (Store, error)

Factory builds a store from a resolved configuration.

type FileConfig

type FileConfig struct {
	Path string
}

FileConfig configures the file driver.

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.

func (GobCodec) Marshal

func (GobCodec) Marshal(v any) ([]byte, error)

func (GobCodec) Name

func (GobCodec) Name() string

func (GobCodec) Unmarshal

func (GobCodec) Unmarshal(data []byte, v any) error

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.

func (JSONCodec) Marshal

func (JSONCodec) Marshal(v any) ([]byte, error)

func (JSONCodec) Name

func (JSONCodec) Name() string

func (JSONCodec) Unmarshal

func (JSONCodec) Unmarshal(data []byte, v any) error

type KeyForgotten

type KeyForgotten struct {
	Key  string
	Tags []string
}

KeyForgotten reports a key removed.

func (KeyForgotten) EventName

func (KeyForgotten) EventName() string

type KeyWritten

type KeyWritten struct {
	Key   string
	Tags  []string
	Value []byte
	TTL   time.Duration
}

KeyWritten reports a value stored. A zero TTL means it was stored forever.

func (KeyWritten) EventName

func (KeyWritten) EventName() string

type Listener

type Listener func(context.Context, Event)

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

type LockProvider interface {
	NewLock(name, owner string, ttl time.Duration) Lock
}

LockProvider is implemented by stores that can back an atomic lock.

type Option

type Option func(*Cache)

Option configures a Cache.

func WithCodec

func WithCodec(c Codec) Option

WithCodec sets how values are encoded. The default is JSON.

func WithEventSink

func WithEventSink(sink Listener) Option

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

func (p *Provider) AddCommands() []app.Command

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) Cache

func (p *Provider) Cache() *Cache

Cache returns the cache this provider built, or nil before Provide runs.

func (*Provider) Provide

func (p *Provider) Provide(a app.App) error

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.

func (*Provider) Shutdown

func (p *Provider) Shutdown(context.Context) error

Shutdown releases the store's resources. It is safe to call more than once.

type Pruner

type Pruner interface {
	Prune(ctx context.Context) error
}

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

func Build(ctx context.Context, cfg *Config) (Store, error)

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.

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL