cache

package
v0.4.0 Latest Latest
Warning

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

Go to latest
Published: Oct 8, 2026 License: Apache-2.0 Imports: 21 Imported by: 0

Documentation

Overview

Package cache keeps values for a while in a shared store (memory, the database, or Redis through drivers/redis), and provides locks across the app's instances.

Set it up once, then use the package functions with any context the app created:

cache.ForApp(app, redis.CacheDriver()) // CACHE_STORE picks the store

stats, err := cache.Remember(ctx, "stats", 10*time.Minute, computeStats)
err = cache.Set(ctx, "profile:7", profile, time.Hour)
profile, ok, err := cache.Get[Profile](ctx, "profile:7")
n, err := cache.Increment(ctx, "logins:"+ip, 1, time.Minute)
err = cache.TryWithLock(ctx, "reports", 10*time.Minute, sendReports)

Values are encoded as JSON. Keys get a prefix (CACHE_PREFIX, default the app's name and ":cache:") so apps can share a store.

See docs/site/guides/cache.md.

Index

Constants

View Source
const Forever time.Duration = 0

Forever is the ttl of items that never expire.

View Source
const MaxKeyLen = 250

MaxKeyLen is the longest key, prefix included, every store accepts.

Variables

View Source
var ErrLockHeld = errors.New("cache: lock is held")

ErrLockHeld is returned by TryWithLock when another holder has the lock.

View Source
var ErrNoCache = errors.New("cache: no cache in context (call cache.ForApp while setting up the app, or use cache.WithCache)")

ErrNoCache is returned when the context has no cache: the app didn't call ForApp, or the context didn't come from the app.

Functions

func Add

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

Add stores v under key only if the key isn't in the cache, and reports whether it did. Only one of several concurrent Adds of a key succeeds.

func CreateTable

func CreateTable(s *migrate.Schema, table string) error

CreateTable creates a table for a DatabaseStore: key (the primary key, up to 255 characters, compared exactly), value and expires_at (Unix milliseconds, NULL for never). Other packages that keep items in a DatabaseStore (session) use it in their migrations.

func Flush

func Flush(ctx context.Context) error

Flush removes every key of the cache (those with its prefix; with no prefix, everything in the store).

func Forget

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

Forget removes key from the cache.

func Get

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

Get returns the value stored under key, decoded as a T, and whether it was there:

profile, ok, err := cache.Get[Profile](ctx, "profile:7")

func Has

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

Has reports whether key is in the cache.

func Increment

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

Increment adds delta (negative to decrement) to the counter under key and returns its new value. A new counter starts at 0 and expires after ttl (Forever: never); incrementing doesn't extend it, which makes windows for rate limits:

n, err := cache.Increment(ctx, "logins:"+ip, 1, time.Minute)

Read a counter with Get[int64].

func Migrations

func Migrations(table string) *migrate.Set

Migrations returns the migration creating the database store's table (default "cache") with CreateTable. Pass it to migrate.ForApp with the app's own:

migrate.ForApp(app, []*migrate.Set{migrations.All, cache.Migrations("")})

func Remember

func Remember[T any](ctx context.Context, key string, ttl time.Duration, fn func(ctx context.Context) (T, error)) (T, error)

Remember returns the value under key, or computes it with fn, stores it for ttl and returns it:

stats, err := cache.Remember(ctx, "stats", 10*time.Minute, func(ctx context.Context) (Stats, error) {
	return computeStats(ctx)
})

Concurrent calls for one key in this process wait for a single fn call (if its caller's context ends first, a waiter computes the value itself). fn's error is returned and nothing is stored. The cache is an optimization here: if the store fails, or holds a value that no longer decodes as a T (after a deploy changed the type), Remember logs it and uses fn.

func Set

func Set(ctx context.Context, key string, v any, ttl time.Duration) error

Set stores v, encoded as JSON, under key for ttl (Forever: no expiry):

err := cache.Set(ctx, "profile:7", profile, time.Hour)

func TryWithLock

func TryWithLock(ctx context.Context, name string, ttl time.Duration, fn func(ctx context.Context) error) error

TryWithLock runs fn holding the lock called name if it is free, and returns ErrLockHeld without running it otherwise: work that one instance at a time should do, such as a scheduled task.

func WithCache

func WithCache(ctx context.Context, c *Cache) context.Context

WithCache returns ctx with c as the cache the package functions use.

func WithLock

func WithLock(ctx context.Context, name string, ttl time.Duration, fn func(ctx context.Context) error) error

WithLock runs fn holding the lock called name, waiting for it as Lock.Acquire does, and releases it when fn returns.

Types

type Cache

type Cache struct {
	// contains filtered or unexported fields
}

Cache stores values of any type, encoded as JSON, in a Store under a key prefix. Handlers reach it through their context: ForApp adds it to every context the app creates, and the package functions (Get, Set, Remember, …) find it there. A Cache is safe for concurrent use.

func ForApp

func ForApp(app *anetos.App, drivers ...Driver) (*Cache, error)

ForApp sets up the app's cache from the CACHE_* settings: it opens the store with the driver CACHE_STORE names (memory and database are built in; pass others, such as redis.CacheDriver()), makes the cache available in every context the app creates (for Get, Set, Remember, …) and to anetos.Resolve, closes the store at shutdown, and adds the cache:clear command.

c, err := cache.ForApp(app, redis.CacheDriver())

The database store needs db.Connect first, and its table from Migrations.

func From

func From(ctx context.Context) (*Cache, error)

From returns the cache in ctx, or ErrNoCache.

func New

func New(store Store, prefix string) *Cache

New returns a Cache over store, with every key prefixed by prefix ("blog:").

func (*Cache) Prefix

func (c *Cache) Prefix() string

Prefix returns the prefix of the cache's keys.

func (*Cache) Store

func (c *Cache) Store() Store

Store returns the cache's store.

type Config

type Config struct {
	// Store is the store's driver: memory, database, or one passed to
	// ForApp (redis). CACHE_STORE, default memory.
	Store string `env:"CACHE_STORE" default:"memory"`
	// Prefix starts every key, so apps (and, in Redis, sessions and
	// queues) can share a store: cache:clear removes only its keys.
	// CACHE_PREFIX, default APP_NAME followed by ":cache:" ("blog:cache:").
	Prefix string `env:"CACHE_PREFIX"`
	// Table is the database store's table. CACHE_TABLE, default cache.
	Table string `env:"CACHE_TABLE" default:"cache"`
}

Config selects and configures the app's cache.

func LoadConfig

func LoadConfig(src config.Source) (Config, error)

LoadConfig reads the CACHE_* settings.

type DatabaseStore

type DatabaseStore struct {
	// contains filtered or unexported fields
}

DatabaseStore keeps items in a table of the app's database, so every instance of the app shares them (and their locks) without another server. Create the table with Migrations. Expiry follows the database server's clock, so instances with drifting clocks agree on it.

With PostgreSQL and MySQL it uses its own connections from the pool, never a transaction in the context: what it writes inside a transaction stays if the transaction rolls back, and a lock taken there is visible to others at once. Each cache call made inside a transaction therefore needs a second connection: keep DB_MAX_OPEN_CONNS above the number of transactions that run at once. SQLite has one writer at a time, so there the store joins the context's transaction instead (a separate write would wait for it): its writes roll back with it.

func NewDatabaseStore

func NewDatabaseStore(d *db.DB, table string) *DatabaseStore

NewDatabaseStore returns a store in table (default "cache") of d.

func (*DatabaseStore) Add

func (s *DatabaseStore) Add(ctx context.Context, key string, value []byte, ttl time.Duration) (bool, error)

Add implements Store: an insert that does nothing if the key is there; if it was, but expired, the row is deleted and the insert retried.

func (*DatabaseStore) Close

func (s *DatabaseStore) Close() error

Close implements Store; the database belongs to the app, so it does nothing.

func (*DatabaseStore) Delete

func (s *DatabaseStore) Delete(ctx context.Context, key string) error

Delete implements Store.

func (*DatabaseStore) DeleteIf

func (s *DatabaseStore) DeleteIf(ctx context.Context, key string, value []byte) (bool, error)

DeleteIf implements Store.

func (*DatabaseStore) ExpireIf

func (s *DatabaseStore) ExpireIf(ctx context.Context, key string, value []byte, ttl time.Duration) (bool, error)

ExpireIf implements Store.

func (*DatabaseStore) Flush

func (s *DatabaseStore) Flush(ctx context.Context, prefix string) error

Flush implements Store.

func (*DatabaseStore) Get

func (s *DatabaseStore) Get(ctx context.Context, key string) ([]byte, bool, error)

Get implements Store.

func (*DatabaseStore) Increment

func (s *DatabaseStore) Increment(ctx context.Context, key string, delta int64, ttl time.Duration) (int64, error)

Increment implements Store: it locks the row in a transaction (or adds it when missing), so concurrent increments wait for each other.

func (*DatabaseStore) Replace

func (s *DatabaseStore) Replace(ctx context.Context, key string, value []byte, ttl time.Duration) (bool, error)

Replace implements Store.

func (*DatabaseStore) Set

func (s *DatabaseStore) Set(ctx context.Context, key string, value []byte, ttl time.Duration) error

Set implements Store.

type Driver

type Driver struct {
	// Name is the value of CACHE_STORE that selects the driver.
	Name string
	// Open returns the store for the app. It may add providers to the app,
	// for example to check a server when the app boots.
	Open func(app *anetos.App, cfg Config) (Store, error)
}

Driver opens a store for ForApp. The memory and database drivers are built in; driver modules provide others (redis.CacheDriver()).

func DatabaseDriver

func DatabaseDriver() Driver

DatabaseDriver is the database store's driver (CACHE_STORE=database), in the table CACHE_TABLE. It uses the app's database: call db.Connect before cache.ForApp.

func MemoryDriver

func MemoryDriver() Driver

MemoryDriver is the memory store's driver (CACHE_STORE=memory).

type Lock

type Lock struct {
	// contains filtered or unexported fields
}

Lock is a named lock in the cache, held by one owner at a time across every process that shares the cache (with a memory store: within one process). It expires after its ttl, so a crashed holder can't keep it forever; extend it for longer work:

lock := cache.NewLock(ctx, "reports:monthly", 10*time.Minute)
if err := lock.Acquire(ctx); err != nil { // waits for it
	return err
}
defer lock.Release(context.WithoutCancel(ctx)) // also when ctx is canceled

A Lock value belongs to one owner; don't share it between goroutines that should exclude each other.

func NewLock

func NewLock(ctx context.Context, name string, ttl time.Duration) *Lock

NewLock returns the lock called name, held for ttl (> 0) once acquired, in the cache of ctx.

func (*Lock) Acquire

func (l *Lock) Acquire(ctx context.Context) error

Acquire waits for the lock and takes it, polling more slowly the longer it waits (up to a second). It returns ctx's error if ctx ends first:

ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
err := lock.Acquire(ctx) // context.DeadlineExceeded after 5s

func (*Lock) Extend

func (l *Lock) Extend(ctx context.Context, ttl time.Duration) (bool, error)

Extend sets the lock's remaining time to ttl if this owner still holds it, and reports whether it did.

func (*Lock) Name

func (l *Lock) Name() string

Name returns the lock's name.

func (*Lock) Release

func (l *Lock) Release(ctx context.Context) (bool, error)

Release frees the lock if this owner still holds it, and reports whether it did (false: it had expired, and may be someone else's now).

func (*Lock) TryAcquire

func (l *Lock) TryAcquire(ctx context.Context) (bool, error)

TryAcquire takes the lock if it is free, and reports whether it did.

type MemoryStore

type MemoryStore struct {
	// contains filtered or unexported fields
}

MemoryStore keeps items in the process's memory: fast, and gone when the process stops. Each instance of an app has its own, so locks only exclude code in the same process; use the database or Redis store to share a cache between instances. Expired items are removed as they are read, and the rest every minute or so while items are written.

func NewMemoryStore

func NewMemoryStore() *MemoryStore

NewMemoryStore returns an empty memory store.

func (*MemoryStore) Add

func (s *MemoryStore) Add(_ context.Context, key string, value []byte, ttl time.Duration) (bool, error)

Add implements Store.

func (*MemoryStore) Close

func (s *MemoryStore) Close() error

Close implements Store; it removes every item.

func (*MemoryStore) Delete

func (s *MemoryStore) Delete(_ context.Context, key string) error

Delete implements Store.

func (*MemoryStore) DeleteIf

func (s *MemoryStore) DeleteIf(_ context.Context, key string, value []byte) (bool, error)

DeleteIf implements Store.

func (*MemoryStore) ExpireIf

func (s *MemoryStore) ExpireIf(_ context.Context, key string, value []byte, ttl time.Duration) (bool, error)

ExpireIf implements Store.

func (*MemoryStore) Flush

func (s *MemoryStore) Flush(_ context.Context, prefix string) error

Flush implements Store.

func (*MemoryStore) Get

func (s *MemoryStore) Get(_ context.Context, key string) ([]byte, bool, error)

Get implements Store.

func (*MemoryStore) Increment

func (s *MemoryStore) Increment(_ context.Context, key string, delta int64, ttl time.Duration) (int64, error)

Increment implements Store.

func (*MemoryStore) Len

func (s *MemoryStore) Len() int

Len returns the number of items, expired ones not yet removed included.

func (*MemoryStore) Replace

func (s *MemoryStore) Replace(_ context.Context, key string, value []byte, ttl time.Duration) (bool, error)

Replace implements Store.

func (*MemoryStore) Set

func (s *MemoryStore) Set(_ context.Context, key string, value []byte, ttl time.Duration) error

Set implements Store.

type Store

type Store interface {
	// Get returns the value of key, and whether it was there.
	Get(ctx context.Context, key string) ([]byte, bool, error)
	// Set stores value under key.
	Set(ctx context.Context, key string, value []byte, ttl time.Duration) error
	// Add stores value under key only if the key is absent, atomically,
	// and reports whether it did.
	Add(ctx context.Context, key string, value []byte, ttl time.Duration) (bool, error)
	// Replace stores value under key only if the key is there, atomically,
	// and reports whether it did. Sessions are saved with it, so a session
	// removed meanwhile (logged out) isn't written back.
	Replace(ctx context.Context, key string, value []byte, ttl time.Duration) (bool, error)
	// Delete removes key; a missing key is not an error.
	Delete(ctx context.Context, key string) error
	// Increment adds delta to the integer stored under key, atomically,
	// and returns the new value. A missing key starts at 0 and gets ttl;
	// an existing one keeps its expiry. The value is stored as decimal
	// text.
	Increment(ctx context.Context, key string, delta int64, ttl time.Duration) (int64, error)
	// DeleteIf removes key if it holds value, atomically, and reports
	// whether it did. Locks are released with it.
	DeleteIf(ctx context.Context, key string, value []byte) (bool, error)
	// ExpireIf sets the ttl (> 0) of key if it holds value, atomically,
	// and reports whether it did. Locks are extended with it.
	ExpireIf(ctx context.Context, key string, value []byte, ttl time.Duration) (bool, error)
	// Flush removes every key that starts with prefix ("": every key of
	// the store).
	Flush(ctx context.Context, prefix string) error
	// Close releases the store's resources.
	Close() error
}

Store is a cache backend: memory, database, Redis. Keys arrive with the cache's prefix. A ttl of Forever (0) means no expiry; expired items behave as if absent in every method. Stores are safe for concurrent use.

Directories

Path Synopsis
Package cachetest is a conformance suite for cache stores.
Package cachetest is a conformance suite for cache stores.

Jump to

Keyboard shortcuts

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