Documentation
¶
Overview ¶
Package cache provides a cache for nexus apps. The default Manager is in-memory only and pulls NO heavy dependencies (no Redis client, no gocache, no Prometheus) — values are stored in an in-process TTL map (patrickmn/go-cache) and serialized with msgpack.
Redis is opt-in, database/sql-style: blank-import the redis backend and it registers itself, after which a Manager in "production" mode keeps a Redis connection and transparently fails over to memory on outage:
import (
"github.com/paulmanoni/nexus/extension/cache"
_ "github.com/paulmanoni/nexus/extension/cache/redis" // enable Redis
)
Without that import the binary never links go-redis. Typical wiring with fx:
fx.New(
fx.Provide(zap.NewExample),
cache.Module, // provides *cache.Manager + *cache.Config
fx.Invoke(func(app *nexus.App, m *cache.Manager) {
app.Register(m.AsResource("session-cache", "Hybrid redis/memory"))
}),
)
Without fx, call NewConfig() + NewManager(cfg, logger) and Start().
Index ¶
- Variables
- func Bind[T any](name string, build func() *Config, opts ...BindOption) nexus.Option
- func ManifestEnv() []manifest.EnvVar
- func ManifestService() manifest.ServiceNeed
- func NewMetricsStore(mgr *Manager) metrics.Store
- func RegisterRedis(f func(*Manager) RedisSupervisor)
- type Backend
- type BindOption
- type Config
- type Manager
- func (m *Manager) ActivateRedis(b Backend)
- func (m *Manager) AsResource(name, description string, opts ...resource.Option) resource.Resource
- func (m *Manager) Clear(ctx context.Context) error
- func (m *Manager) Config() *Config
- func (m *Manager) Context() context.Context
- func (m *Manager) Delete(ctx context.Context, key string) error
- func (m *Manager) FallBackToMemory()
- func (m *Manager) Get(ctx context.Context, key string, out any) error
- func (m *Manager) IsRedisConnected() bool
- func (m *Manager) Logger() *zap.Logger
- func (m *Manager) NexusEnv() []manifest.EnvVar
- func (m *Manager) NexusServices() []manifest.ServiceNeed
- func (m *Manager) Set(ctx context.Context, key string, value any, ttl time.Duration) error
- func (m *Manager) Start()
- func (m *Manager) Stop()
- type RedisSupervisor
Constants ¶
This section is empty.
Variables ¶
var ErrNotFound = errors.New("cache: key not found")
ErrNotFound is returned by Get when the key is absent (or the cache isn't initialized). Distinct so callers can branch on a miss vs a real error.
Module provides *Config (from env) and *Manager into the Fx graph. Consume it by taking *cache.Manager in your constructors.
Functions ¶
func Bind ¶ added in v1.13.0
Bind wires a named cache connection declaratively — the cache counterpart to nexus.Database / pubsub.Broker. It lives here (not in the nexus root) so that importing the root package does NOT drag Redis, gocache, and Prometheus into the build; an app pays for those only when it actually calls cache.Bind. T must embed *Manager:
type SessionCache struct{ *cache.Manager }
nexus.Run(cfg,
cache.Bind[SessionCache]("session", func() *cache.Config {
c := cache.NewConfig()
c.RedisHost = nexus.Get[string]("cache.redis.host")
c.RedisPort = nexus.Get[string]("cache.redis.port")
return c
}, cache.WithDefault()),
)
build() runs in the fx constructor (so nexus.Get resolves), the framework Start()s the manager on boot and Stop()s it on shutdown, and the connection is registered as a dashboard resource via the manager's AsResource (which reports Redis-vs-memory health). Handlers inject *SessionCache. T not embedding *Manager panics at wiring time.
func ManifestEnv ¶
ManifestEnv lists the env vars this package's NewConfig reads. Apps that include cache.Module in their nexus.Run options should also add nexus.DeclareEnvList(cache.ManifestEnv()) so the deploy-time manifest reflects the cache's configuration surface — otherwise an orchestration platform reading the manifest won't know REDIS_HOST / REDIS_PORT / REDIS_PASSWORD / APP_ENV are required.
Two design notes:
We expose a slice rather than implementing manifest.EnvProvider on *Manager. EnvProvider would only be detected once *Manager enters the graph, but in print mode we want declarations visible WITHOUT firing constructors (Manager's constructor dials Redis). Static slices are side-effect-free by definition.
We expose ManifestService too — the cache's logical sidecar ("redis"). Both are returned together as a convenience but kept as separate functions so an app that, say, uses an external managed Redis (no provisioned sidecar needed) can include ManifestEnv() but skip ManifestService().
func ManifestService ¶
func ManifestService() manifest.ServiceNeed
ManifestService describes the Redis sidecar this package would like the orchestration platform to provision. Optional=true reflects that the cache layer degrades to in-memory when Redis is absent — the platform can skip provisioning in dev environments where that trade-off is acceptable.
func NewMetricsStore ¶ added in v1.13.0
NewMetricsStore returns a metrics.Store backed by a nexus *Manager — the in-memory path uses go-cache, and the Redis path persists counters so multi-replica deploys see aggregated totals. It lives in package cache (not metrics) so that extension/metrics — which the framework core imports for the default in-process store — never drags Redis/gocache into the build. Opt in explicitly:
Config.Stores.Metrics = cache.NewMetricsStore(mgr)
Semantics: counters are best-effort. Every Record does a read-modify- write against the cache under the endpoint key, so concurrent writers (or replicas in Redis mode) can step on each other's increments. For a dashboard view that's fine; for exact counts under contention point the app at a Prometheus collector instead. Keys are namespaced under "nexus.metrics." with a 24h TTL.
func RegisterRedis ¶ added in v1.13.0
func RegisterRedis(f func(*Manager) RedisSupervisor)
RegisterRedis installs the Redis supervisor factory. Called from extension/cache/redis's init(), so a blank import is all an app needs to enable Redis (the database/sql driver pattern). Not part of the surface apps call directly.
Types ¶
type Backend ¶ added in v1.13.0
type Backend interface {
Get(ctx context.Context, key string, out any) error
Set(ctx context.Context, key string, value any, ttl time.Duration) error
Delete(ctx context.Context, key string) error
Clear(ctx context.Context) error
}
Backend is a cache storage tier. The memory backend (default) and the optional Redis backend (extension/cache/redis) both implement it. Values are passed already-typed; an implementation is responsible for its own serialization. Get returns ErrNotFound (or any non-nil error) on a miss.
type BindOption ¶ added in v1.13.0
type BindOption func(*bindConfig)
BindOption tunes how Bind registers the dashboard resource.
func WithDefault ¶ added in v1.13.0
func WithDefault() BindOption
WithDefault marks this cache as the default for its kind.
func WithDescription ¶ added in v1.13.0
func WithDescription(s string) BindOption
WithDescription overrides the dashboard resource description.
type Config ¶
type Config struct {
// Environment controls Redis behavior. "production" attempts Redis and
// keeps reconnecting; anything else stays on memory. Redis only ever
// engages when the redis backend is registered (blank-import
// extension/cache/redis); otherwise this is a no-op and the Manager is
// memory-only regardless of Environment.
Environment string
RedisHost string
RedisPort string
RedisPassword string
RedisDB int
// DefaultExpiry is the in-memory store's default TTL. CleanupExpiry is
// its GC tick.
DefaultExpiry time.Duration
CleanupExpiry time.Duration
// ConnectTimeout caps the initial Redis ping during connect attempts.
ConnectTimeout time.Duration
// ReconnectInterval controls how often the manager retries Redis when
// it's down. 0 defaults to 30s.
ReconnectInterval time.Duration
// PersistPath, when non-empty, makes the in-memory store survive
// process restarts: Start tries LoadFile, Stop calls SaveFile.
// Aimed at `nexus dev`'s auto-restart loop so auth tokens (or
// any cached state) don't evaporate on every Go save. Redis still
// wins when reachable; this is the memory-only path's escape
// hatch. Set explicitly, or let NewConfig auto-pick
// ".nexus/dev-cache.gob" when NEXUS_DEV=1.
PersistPath string
}
Config holds cache configuration. Populate via NewConfig (env-driven) or construct directly.
func NewConfig ¶
func NewConfig() *Config
NewConfig builds a Config from env vars: APP_ENV, REDIS_HOST, REDIS_PORT, REDIS_PASSWORD, NEXUS_DEV_CACHE_FILE. Defaults: env=development, host=localhost, port=6379, db=0, 15m/10m expiries, 5s connect timeout, 30s reconnect. PersistPath auto-defaults to ".nexus/dev-cache.gob" when NEXUS_DEV=1 (set by `nexus dev`) so tokens + cached state survive the auto-restart loop without anything to wire by hand.
func (*Config) RedisAddress ¶
RedisAddress returns host:port, filling in localhost:6379 when blank.
type Manager ¶
type Manager struct {
// contains filtered or unexported fields
}
Manager is the live cache. Get/Set/Delete are safe for concurrent use; the active backend flips between Redis and memory atomically under a mutex when the Redis supervisor reports a connectivity change.
func NewManager ¶
NewManager constructs a memory-backed Manager. When the Redis backend is registered (blank-import extension/cache/redis) and Environment is "production", Start() launches the async connect + reconnect loop — an unreachable Redis never delays boot; the Manager serves from memory until Redis comes up, then flips atomically.
func Provide ¶
Provide is the fx provider: builds a Manager from a *Config, ties its Start/Stop to the Fx lifecycle, AND self-registers as a manifest.EnvProvider + ServiceDependencyProvider so the orchestration manifest reflects this cache's REDIS_HOST/PORT/ PASSWORD requirements without the app's main.go having to call nexus.DeclareEnv / DeclareService.
reg comes from the fx graph — *nexus.App satisfies the interface (it has matching methods) and is provided by fxEarlyOptions, so every app that uses cache.Module gets the registration for free. Apps that build their own *App via nexus.New() still wire it the same way.
func (*Manager) ActivateRedis ¶ added in v1.13.0
ActivateRedis swaps the active backend to b and marks Redis connected. Called by the supervisor on a successful (re)connect.
func (*Manager) AsResource ¶
AsResource builds a nexus resource.Resource for this Manager. Backend ("redis" vs "memory") is reported live via WithDetails.
func (*Manager) Config ¶ added in v1.13.0
Config, Logger, and Context expose the bits the Redis supervisor needs without it reaching into Manager internals.
func (*Manager) FallBackToMemory ¶ added in v1.13.0
func (m *Manager) FallBackToMemory()
FallBackToMemory reverts the active backend to the in-memory store. Called by the supervisor when Redis goes away.
func (*Manager) Get ¶
Get deserializes the cached value under key into out. Returns ErrNotFound when the key is missing.
func (*Manager) IsRedisConnected ¶
IsRedisConnected reports whether Redis is the currently active store.
func (*Manager) NexusEnv ¶
── Auto-discovered manifest providers ─────────────────────────────
*Manager implements manifest.EnvProvider and manifest.ServiceDependencyProvider so nexus.Provide auto-walks the constructed Manager at print-mode boot and pulls these declarations into the manifest. No nexus.DeclareEnv / DeclareService calls in the application's main.go — adoption is just `cache.Module`.
Methods are static (no read of m.config) so print mode gets the same answer pre-Start as a fully-running Manager — preserves the side-effect-free contract.
func (*Manager) NexusServices ¶
func (m *Manager) NexusServices() []manifest.ServiceNeed
type RedisSupervisor ¶ added in v1.13.0
type RedisSupervisor interface {
Start()
Stop()
}
RedisSupervisor is the lifecycle the optional Redis backend installs. Start kicks off the connect + reconnect loop (swapping the Manager's active backend via ActivateRedis / FallBackToMemory); Stop tears it down.