Documentation
¶
Overview ¶
Package cache is nForge's key/value cache: one non-generic *Cache backed by Redis or by a bounded in-process map, chosen by configuration.
Objects (Get, Set, SetIfAbsent, GetOrLoad) are serialized with the framework codec registry in the configured format (json or cbor), with its payload limit, required fields and validators. Raw byte operations (GetBytes, SetBytes, SetBytesIfAbsent) and counters (Increment, decimal ASCII) bypass the codec. No envelope is added to stored values, so a key written with one codec cannot be read with another: changing cache.codec needs the old entries to expire, be invalidated, or a new cache.prefix.
Every key is namespaced (see keyspace); with a tenant resolver the tenant of the operation's context is part of the namespace and is required.
The package depends only on the codec package and the standard library (plus go-redis behind unexported adapters).
Index ¶
- Constants
- Variables
- func ApplyDefaults(cfg *Config)
- func ValidateConfig(cfg Config) error
- type ByteSize
- type Cache
- func (c *Cache) Delete(ctx context.Context, keys ...string) (int64, error)
- func (c *Cache) Driver() string
- func (c *Cache) Exists(ctx context.Context, key string) (bool, error)
- func (c *Cache) Expire(ctx context.Context, key string, ttl time.Duration) (bool, error)
- func (c *Cache) Get(ctx context.Context, key string, target any) error
- func (c *Cache) GetBytes(ctx context.Context, key string) ([]byte, error)
- func (c *Cache) GetOrLoad(ctx context.Context, key string, target any, ttl time.Duration, ...) error
- func (c *Cache) Increment(ctx context.Context, key string, delta int64) (int64, error)
- func (c *Cache) Ping(ctx context.Context) error
- func (c *Cache) Set(ctx context.Context, key string, value any, ttl time.Duration) error
- func (c *Cache) SetBytes(ctx context.Context, key string, value []byte, ttl time.Duration) error
- func (c *Cache) SetBytesIfAbsent(ctx context.Context, key string, value []byte, ttl time.Duration) (bool, error)
- func (c *Cache) SetIfAbsent(ctx context.Context, key string, value any, ttl time.Duration) (bool, error)
- func (c *Cache) Shutdown(ctx context.Context) error
- func (c *Cache) TTL(ctx context.Context, key string) (Expiration, error)
- type Config
- type Error
- type Expiration
- type LimitsConfig
- type MemoryConfig
- type Option
- type RedisConfig
- type RedisPoolConfig
- type RedisRetry
- type RedisTLSConfig
- type RedisTimeouts
Constants ¶
const ( DriverRedis = "redis" DriverMemory = "memory" )
Drivers.
const ( CodecJSON = "json" CodecCBOR = "cbor" )
Object codecs (short names of the framework codec registry).
const ( B ByteSize = 1 KB = 1024 * B MB = 1024 * KB GB = 1024 * MB )
Size units.
Variables ¶
var ( ErrDisabled = errors.New("cache: disabled") ErrClosed = errors.New("cache: closed") ErrInvalidConfig = errors.New("cache: invalid config") ErrUnsupportedDriver = errors.New("cache: unsupported driver") ErrUnsupportedCodec = errors.New("cache: unsupported codec") ErrInvalidKey = errors.New("cache: invalid key") ErrTenantRequired = errors.New("cache: tenant required") ErrInvalidTTL = errors.New("cache: invalid ttl") ErrInvalidTarget = errors.New("cache: invalid target") ErrNotFound = errors.New("cache: not found") ErrEncode = errors.New("cache: encode failed") ErrDecode = errors.New("cache: decode failed") ErrValueTooLarge = errors.New("cache: value too large") ErrNotInteger = errors.New("cache: value is not an integer") ErrCounterOverflow = errors.New("cache: counter overflow") // ErrOutcomeUnknown means a mutation may have reached Redis but no // authoritative reply arrived: it may or may not have been applied, and // it is never replayed automatically. It also matches ErrUnavailable. ErrOutcomeUnknown = errors.New("cache: outcome unknown") ErrLoadCapacity = errors.New("cache: too many concurrent loads") ErrLoaderPanic = errors.New("cache: loader panicked") )
Sentinel categories. Every error returned by a Cache method (except the application errors returned by a GetOrLoad loader, which are passed through) matches one of them with errors.Is; details are in *Error.
Functions ¶
func ApplyDefaults ¶
func ApplyDefaults(cfg *Config)
ApplyDefaults fills the zero values with the defaults. read_retries is a pointer so that an explicit 0 (no retry) differs from "unset".
func ValidateConfig ¶
ValidateConfig checks cfg after applying the defaults to a copy. A disabled config is always valid; only the selected driver's section is checked, and nothing is dialed or read from disk.
Types ¶
type ByteSize ¶
type ByteSize int64
ByteSize is a size in bytes that accepts units in YAML and env files: "1mb", "512kb", "1.5gb", "100b" or a plain number of bytes. Units are base 1024 and case-insensitive ("5MB", "5 MiB"), the same syntax as config.ByteSize (the cache package cannot import config).
func ParseByteSize ¶
ParseByteSize parses a size such as "1mb" or "1048576".
func (*ByteSize) UnmarshalText ¶
UnmarshalText implements encoding.TextUnmarshaler (env files).
type Cache ¶
type Cache struct {
// contains filtered or unexported fields
}
Cache is safe for concurrent use. Create it with Open and release it with Shutdown; after Shutdown every method returns ErrClosed.
func Open ¶
Open validates a deep copy of cfg (after ApplyDefaults), creates the selected driver and pings it with ctx. A disabled cfg returns ErrDisabled. On any failure the partially created resources are released. Open never panics or exits; deciding that a failure is fatal is the caller's job.
func (*Cache) Delete ¶
Delete removes the given keys (exact keys only: no patterns) and returns how many distinct existing keys were removed. Every key is validated before anything is deleted; duplicates are removed once.
func (*Cache) Exists ¶
Exists reports whether a live entry exists (false, nil when missing or expired).
func (*Cache) Expire ¶
Expire sets a new TTL (> 0) on an existing entry without touching its value; false, nil when the key is missing. To make an entry persistent, write it again with ttl 0.
func (*Cache) Get ¶
Get decodes the object stored at key into target (a non-nil pointer). On a miss (ErrNotFound) or any error the target is left unchanged. A decode failure is ErrDecode, never a miss.
func (*Cache) GetBytes ¶
GetBytes returns an independent copy of a raw value; a present empty value is a non-nil empty slice. A missing key is ErrNotFound.
func (*Cache) GetOrLoad ¶
func (c *Cache) GetOrLoad(ctx context.Context, key string, target any, ttl time.Duration, load func(context.Context) (any, error)) error
GetOrLoad reads the object at key into target; on a miss it calls load, stores the encoded result with ttl and decodes it into target.
- A hit never calls load. Any read error other than ErrNotFound (Redis down, corrupt data) is returned without calling load.
- Concurrent callers of the same full key (tenant included) in this process share one load: the leader rechecks the cache, runs load once with a context detached from the first caller (values kept, cancellation not) and bounded by cache.load_timeout, and encodes the result once. Each caller decodes its own copy into its own target. The leader's loader and TTL govern the flight, so callers of one key must use compatible loaders and TTLs. It is not a distributed lock: other instances may load the same key concurrently.
- Load errors are returned unchanged (and not cached); application errors must not be sent blindly to HTTP clients. A panic in load becomes ErrLoaderPanic for every caller.
- Storing the result is best effort: a backend write failure, a full memory cache or a value larger than max_value_bytes is logged as a warning and the loaded value is still returned.
- A caller whose context ends returns its context error; the load is canceled only when every caller has left (or on Shutdown).
- At most cache.limits.max_inflight_loads distinct flights run at once; beyond that a new key fails with ErrLoadCapacity (joining an existing flight is always allowed).
target is only assigned on success.
func (*Cache) Increment ¶
Increment atomically adds delta (any sign, or zero) to a decimal int64 counter and returns the new value. A missing key starts at 0 and becomes a persistent counter; an existing TTL is preserved. Non-integer contents give ErrNotInteger and overflow ErrCounterOverflow, both without changing the entry. Counter keys must not be used for objects (and vice versa).
func (*Cache) Ping ¶
Ping checks the backend (Redis PING; the memory driver only checks that the cache is open). It needs no tenant and is used by readiness probes.
func (*Cache) Set ¶
Set encodes value with the configured codec and stores it, replacing any previous value and TTL. ttl 0 = no expiration.
func (*Cache) SetBytes ¶
SetBytes stores a copy of value unchanged (nil is an empty value, not a deletion), replacing any previous value and TTL. ttl 0 = no expiration.
func (*Cache) SetBytesIfAbsent ¶
func (c *Cache) SetBytesIfAbsent(ctx context.Context, key string, value []byte, ttl time.Duration) (bool, error)
SetBytesIfAbsent atomically stores value and its TTL only when no live entry exists. It returns false, nil when the key exists (value and TTL untouched). A lost Redis reply is ErrOutcomeUnknown, never false.
func (*Cache) SetIfAbsent ¶
func (c *Cache) SetIfAbsent(ctx context.Context, key string, value any, ttl time.Duration) (bool, error)
SetIfAbsent is Set only when no live entry exists; see SetBytesIfAbsent.
func (*Cache) Shutdown ¶
Shutdown stops admitting operations (permanently: ErrClosed), stops the memory cleanup worker, cancels shared loaders and waiting GetOrLoad callers, waits for the operations in progress and then closes the driver (Redis pool, or clears the memory). If ctx ends first it returns the context error and leaves the cache closed; a later Shutdown keeps waiting for the remaining cooperative work. It is idempotent and safe to call concurrently.
type Config ¶
type Config struct {
Enabled bool `yaml:"enabled"` // default: false (deps.Cache is nil)
Driver string `yaml:"driver"` // redis (default) | memory
Codec string `yaml:"codec"` // object codec: json (default) | cbor; must be enabled in the codec registry
Prefix string `yaml:"prefix"` // optional; "" derives nforge:<env>:[<tenant>:]<app.name>:
Limits LimitsConfig `yaml:"limits"` //
LoadTimeout time.Duration `yaml:"load_timeout"` // bounds a shared GetOrLoad loader; default: 5s
Redis RedisConfig `yaml:"redis"` // used only when driver is redis
Memory MemoryConfig `yaml:"memory"` // used only when driver is memory
}
Config is the `cache:` section. It is standalone (no framework imports): config.Config embeds it and deps opens it; other programs can call Open directly. Zero values of caps and timeouts take the defaults below; see ApplyDefaults.
type Error ¶
type Error struct {
Op string // public method, e.g. "get", "set_if_absent"
Driver string // "redis" | "memory" | "" (before a backend exists)
Kind error // the sentinel category
Reason string // bounded machine-readable detail, e.g. "timeout", "auth"
// contains filtered or unexported fields
}
Error is the typed error of the cache. Its message never contains keys, tenant IDs, cached contents, credentials, URLs or Redis error text: only the operation, the driver, the category and a bounded reason. Unwrap exposes only safe causes: context errors and framework codec errors.
type Expiration ¶
type Expiration struct {
Exists bool
Persistent bool
// Remaining is the time left of an expiring key; never negative, and it
// may round to zero while the key is still live.
Remaining time.Duration
}
Expiration is the result of TTL: a missing key, a persistent key or an expiring one. Redis's negative sentinels (-1, -2) never leak out.
type LimitsConfig ¶
type LimitsConfig struct {
MaxKeyBytes int `yaml:"max_key_bytes"` // full resolved key (prefix included); default: 1024, at most 64 KiB
MaxValueBytes ByteSize `yaml:"max_value_bytes"` // encoded or raw value; default: 1mb, at most 64 MiB
MaxInflightLoads int `yaml:"max_inflight_loads"` // distinct concurrent GetOrLoad flights; default: 1024
}
LimitsConfig bounds keys, values and concurrent loads.
type MemoryConfig ¶
type MemoryConfig struct {
MaxEntries int `yaml:"max_entries"` // default: 10000
MaxBytes ByteSize `yaml:"max_bytes"` // key + value bytes; default: 64mb
CleanupInterval time.Duration `yaml:"cleanup_interval"` // expired-entry sweep; default: 30s
}
MemoryConfig bounds the in-process driver. The budget counts key and value bytes only, not the Go map/list/allocator overhead: it is not a hard cap on the process memory.
type Option ¶
type Option func(*options) error
Option configures Open. Nil values and conflicting duplicates are rejected by Open with ErrInvalidConfig.
func WithCodecRegistry ¶
WithCodecRegistry binds the registry used for object values. Without it, Open captures codec.Default() once; the registry never changes afterwards.
func WithLogger ¶
WithLogger sets the logger for the few events the cache logs itself (best-effort GetOrLoad write failures). Default: a discarding logger.
func WithNamespace ¶
WithNamespace provides the trusted environment and application name used by the default prefix nforge:<env>:[<tenant>:]<app>:. It is required when cache.prefix is empty.
func WithTenantResolver ¶
WithTenantResolver enables tenant isolation: every operation resolves the tenant from its context and inserts it into the key namespace; a missing tenant fails with ErrTenantRequired before any I/O. It is an adapter to the framework's existing tenant context (deps binds database.TenantFromContext), not a separate tenant system. The resolver must return a trusted, already validated ID.
type RedisConfig ¶
type RedisConfig struct {
URL string `yaml:"url"` // optional redis:// or rediss:// URL; wins for address, credentials, database and TLS
Host string `yaml:"host"` // default: localhost
Port int `yaml:"port"` // default: 6379
Username string `yaml:"username"` // optional (ACL user)
Password string `yaml:"password"` // optional; no AUTH is sent when username and password are empty
Database int `yaml:"database"` // default: 0
TLS RedisTLSConfig `yaml:"tls"` //
Pool RedisPoolConfig `yaml:"pool"` //
Timeouts RedisTimeouts `yaml:"timeouts"` //
Retry RedisRetry `yaml:"retry"` //
}
RedisConfig configures the Redis driver. Never log it with a plain formatter expecting secrets: Format and LogValue redact the URL, the password and the CA file path.
type RedisPoolConfig ¶
type RedisPoolConfig struct {
Size int `yaml:"size"` // default: 32
MinIdleConns int `yaml:"min_idle_conns"` // default: 2
MaxIdleConns int `yaml:"max_idle_conns"` // default: 32
MaxActiveConns int `yaml:"max_active_conns"` // hard cap of open connections; default: 32
MaxConnIdleTime time.Duration `yaml:"max_conn_idle_time"` // default: 5m
MaxConnLifetime time.Duration `yaml:"max_conn_lifetime"` // default: 30m
}
RedisPoolConfig sizes the connection pool.
type RedisRetry ¶
type RedisRetry struct {
ReadRetries *int `yaml:"read_retries"` // extra read attempts, 0..3; default: 1 (explicit 0 disables)
MinBackoff time.Duration `yaml:"min_backoff"` // default: 50ms
MaxBackoff time.Duration `yaml:"max_backoff"` // default: 500ms
}
RedisRetry configures the retries of pure reads (Get, GetBytes, Exists, TTL, Ping) on transport failures. Mutations are never retried.
type RedisTLSConfig ¶
type RedisTLSConfig struct {
Enabled bool `yaml:"enabled"` // default: false; a rediss:// URL always enables TLS
ServerName string `yaml:"server_name"` // default: the Redis host
CAFile string `yaml:"ca_file"` // PEM roots; default: the system roots
}
RedisTLSConfig enables TLS with hostname verification and TLS 1.2+. There is deliberately no option to skip verification.
type RedisTimeouts ¶
type RedisTimeouts struct {
Dial time.Duration `yaml:"dial"` // default: 1s
Read time.Duration `yaml:"read"` // default: 1s
Write time.Duration `yaml:"write"` // default: 1s
Pool time.Duration `yaml:"pool"` // wait for a free connection; default: 500ms
Operation time.Duration `yaml:"operation"` // whole operation, retries included; default: 2s
}
RedisTimeouts bound every phase of an operation; a caller deadline that is earlier always wins.