Documentation
¶
Overview ¶
Package cache stores values with a TTL behind a Store (memory, database; Redis in drivers/redis). Values are JSON-encoded, so typed reads use the generic functions:
user, err := cache.Remember(ctx, app.Cache, "user:42", time.Hour, func(ctx context.Context) (User, error) {
return orm.QueryOf[User](db).Find(ctx, 42)
})
Cache wraps a Store with a key prefix and adds Put, Forget, Increment and distributed locks (Cache.NewLock); Get and Remember decode typed values. Apps get one from app.Cache(), configured by CACHE_STORE.
See https://github.com/AlexGalhardo/vukem/blob/main/docs/subsystems.md#cache.
Index ¶
- Constants
- Variables
- func Get[T any](ctx context.Context, c *Cache, key string) (v T, ok bool, err error)
- func Layers(st Store) iter.Seq[Store]
- func Migrations(table string) []schema.Migration
- func Remember[T any](ctx context.Context, c *Cache, key string, ttl time.Duration, ...) (T, error)
- func Shared(st Store) bool
- type Cache
- func (c *Cache) Flush(ctx context.Context) error
- func (c *Cache) Forget(ctx context.Context, keys ...string) error
- func (c *Cache) Increment(ctx context.Context, key string, delta int64, ttl time.Duration) (int64, error)
- func (c *Cache) NewLock(name string, ttl time.Duration) *Lock
- func (c *Cache) Put(ctx context.Context, key string, v any, ttl time.Duration) error
- func (c *Cache) Store() Store
- type Database
- func (d *Database) Add(ctx context.Context, key string, value []byte, ttl time.Duration) (bool, error)
- func (d *Database) CreateTable(ctx context.Context) error
- func (d *Database) Delete(ctx context.Context, keys ...string) error
- func (d *Database) DeleteIf(ctx context.Context, key string, value []byte) (bool, error)
- func (d *Database) Flush(ctx context.Context) error
- func (d *Database) Get(ctx context.Context, key string) ([]byte, bool, error)
- func (d *Database) Increment(ctx context.Context, key string, delta int64, ttl time.Duration) (int64, error)
- func (d *Database) Prune(ctx context.Context) (int64, error)
- func (d *Database) Set(ctx context.Context, key string, value []byte, ttl time.Duration) error
- func (*Database) Shared() bool
- type Lock
- type Memory
- func (m *Memory) Add(_ context.Context, key string, value []byte, ttl time.Duration) (bool, error)
- func (m *Memory) Delete(_ context.Context, keys ...string) error
- func (m *Memory) DeleteIf(_ context.Context, key string, value []byte) (bool, error)
- func (m *Memory) Flush(context.Context) error
- func (m *Memory) Get(_ context.Context, key string) ([]byte, bool, error)
- func (m *Memory) Increment(_ context.Context, key string, delta int64, ttl time.Duration) (int64, error)
- func (m *Memory) Len() int
- func (m *Memory) Set(_ context.Context, key string, value []byte, ttl time.Duration) error
- func (*Memory) Shared() bool
- type Sharer
- type Store
Examples ¶
Constants ¶
const DefaultTable = "cache"
DefaultTable is the table the database store owns when NewDatabase is called with an empty name (CACHE_TABLE). It is listed with every other table vukem owns in docs/database.md#tables-the-framework-owns.
const MigrationID = "20261018000002_vukem_create_cache_table"
MigrationID is the id of the migration Migrations returns; it names the framework's own migration in vukem migrate:status.
Variables ¶
var ErrNotAcquired = errors.New("cache: lock not acquired")
ErrNotAcquired is returned by Lock.Block when the wait expired.
Functions ¶
func Get ¶
Get reads key into T; ok is false when the key is missing or expired.
Example ¶
package main
import (
"context"
"fmt"
"time"
"github.com/AlexGalhardo/vukem/cache"
)
func main() {
ctx := context.Background()
c := cache.New(cache.NewMemory(1000), "")
_ = c.Put(ctx, "greeting", "hello", time.Minute)
v, ok, err := cache.Get[string](ctx, c, "greeting")
fmt.Println(v, ok, err)
_, ok, _ = cache.Get[string](ctx, c, "missing")
fmt.Println(ok)
}
Output: hello true <nil> false
func Layers ¶ added in v0.26.0
Layers yields st and then each store underneath it, following wrappers (metrics, tracing) that expose the store they wrap with an Unwrap() Store method. It stops at a store without Unwrap, at nil, or after 32 stores, whichever comes first; callers looking for a capability treat a chain cut by the limit as not having it.
func Migrations ¶ added in v0.29.0
Migrations returns the migration that creates the cache table (the default when table is empty), so an app can manage it with vukem migrate instead of leaving it to the boot-time CreateTable. The statements are the ones CreateTable runs, which stays as the fallback for apps that do not register it.
On a database where the table already exists (the store created it before the app opted in) the migration records itself and changes nothing; Down drops the table.
func Remember ¶
func Remember[T any](ctx context.Context, c *Cache, key string, ttl time.Duration, fn func(ctx context.Context) (T, error), ) (T, error)
Remember returns the cached value or computes it with fn and stores it for ttl. Concurrent misses for the same key in this process run fn once (singleflight), so an expired hot key does not stampede the database.
Example ¶
package main
import (
"context"
"fmt"
"time"
"github.com/AlexGalhardo/vukem/cache"
)
type user struct {
ID int64
Name string
}
func main() {
ctx := context.Background()
c := cache.New(cache.NewMemory(1000), "app:")
loads := 0
load := func(context.Context) (user, error) {
loads++
return user{ID: 42, Name: "Ada"}, nil
}
first, _ := cache.Remember(ctx, c, "user:42", time.Hour, load)
second, _ := cache.Remember(ctx, c, "user:42", time.Hour, load)
fmt.Println(first.Name, second.Name, "loads:", loads)
}
Output: Ada Ada loads: 1
func Shared ¶ added in v0.26.0
Shared reports whether st is shared by every server of the app. The first store in Layers that implements Sharer answers; when none does (an undeclared store, nil, or an Unwrap chain longer than the limit), st is not shared, so a check that needs a shared store fails closed instead of trusting an unknown one.
Types ¶
type Cache ¶
type Cache struct {
// contains filtered or unexported fields
}
Cache is a Store with a key prefix, JSON encoding and stampede protection.
func (*Cache) Increment ¶
func (c *Cache) Increment(ctx context.Context, key string, delta int64, ttl time.Duration) (int64, error)
Increment adds delta to the integer at key (counters, rate limits).
func (*Cache) NewLock ¶
NewLock prepares a lock on name that expires after ttl if never released.
Example ¶
package main
import (
"context"
"fmt"
"time"
"github.com/AlexGalhardo/vukem/cache"
)
func main() {
ctx := context.Background()
c := cache.New(cache.NewMemory(1000), "")
lock := c.NewLock("reports", time.Minute)
got, _ := lock.TryAcquire(ctx)
again, _ := c.NewLock("reports", time.Minute).TryAcquire(ctx)
fmt.Println(got, again)
_ = lock.Release(ctx)
}
Output: true false
type Database ¶
type Database struct {
// contains filtered or unexported fields
}
Database stores entries in a table (SQLite, Postgres or MySQL): cache(key TEXT PRIMARY KEY, value TEXT NOT NULL, expires_at BIGINT); value is BYTEA on Postgres with expires_at in Unix milliseconds (0 = never). CreateTable creates it. On MySQL key and value are binary (VARBINARY, LONGBLOB) so lookups stay exact and case-sensitive.
func NewDatabase ¶
NewDatabase returns a database store using table (default "cache").
func (*Database) Add ¶
func (d *Database) Add(ctx context.Context, key string, value []byte, ttl time.Duration) (bool, error)
Add implements Store: expired rows are replaced, live ones kept.
func (*Database) CreateTable ¶
CreateTable creates the cache table if missing. It is the fallback for apps that do not register Migrations: the statements are the same, so running it after the migration changes nothing.
func (*Database) Increment ¶
func (d *Database) Increment(ctx context.Context, key string, delta int64, ttl time.Duration) (int64, error)
Increment implements Store with one upsert that adds delta to the row (or creates it), so it is atomic across connections and servers: rate limits count with it. A live row keeps its expiry; an expired one restarts at delta with ttl. A value that is not an integer is left as it is and Increment fails. MySQL has no RETURNING: there the upsert and a SELECT of the locked row share a transaction.
func (*Database) Prune ¶
Prune deletes expired rows (scheduled hourly as cache:prune), in batches so a large backlog never holds one long lock.
type Lock ¶
type Lock struct {
// contains filtered or unexported fields
}
Lock is a distributed (store-wide) mutual exclusion lock.
type Memory ¶
type Memory struct {
// contains filtered or unexported fields
}
Memory is an in-process LRU store with TTLs. MaxEntries bounds memory (least recently used entries are evicted).
func (*Memory) Increment ¶
func (m *Memory) Increment(_ context.Context, key string, delta int64, ttl time.Duration) (int64, error)
Increment implements Store.
func (*Memory) Len ¶
Len returns the number of stored entries (expired ones included until they are touched).
type Sharer ¶ added in v0.26.0
type Sharer interface {
}
Sharer is implemented by a Store that states whether every server of the app sees the same entries: Shared reports true for a store backed by a database or Redis, false for one kept in this process (Memory).
Features that coordinate servers through the cache (OnOneServer tasks, RATE_LIMIT_STORE=cache) ask Shared. A store wrapper (metrics, tracing) forwards the answer of the store it wraps, either by implementing Sharer or by exposing that store with an Unwrap() Store method.
The SQLite Database store reports true, but a SQLite file is shared only by the processes on one host: servers on several hosts need PostgreSQL, MySQL or Redis.
type Store ¶
type Store interface {
Get(ctx context.Context, key string) ([]byte, bool, error)
Set(ctx context.Context, key string, value []byte, ttl time.Duration) error
// Add sets key only when it is absent (or expired) and reports whether
// it did; locks rely on it being atomic.
Add(ctx context.Context, key string, value []byte, ttl time.Duration) (bool, error)
Delete(ctx context.Context, keys ...string) error
// DeleteIf deletes key only when it holds value (lock release).
DeleteIf(ctx context.Context, key string, value []byte) (bool, error)
// Increment adds delta to an integer value (missing keys start at 0
// with ttl) and returns the new value.
Increment(ctx context.Context, key string, delta int64, ttl time.Duration) (int64, error)
Flush(ctx context.Context) error
}
Store is a cache backend. Keys arrive already prefixed.