cache

package
v1.23.1 Latest Latest
Warning

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

Go to latest
Published: Jun 23, 2026 License: MIT Imports: 16 Imported by: 0

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

Constants

This section is empty.

Variables

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

View Source
var Module = di.Module("nexus-cache",
	di.Provide(
		NewConfig,
		Provide,
	),
)

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

func Bind[T any](name string, build func() *Config, opts ...BindOption) nexus.Option

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

func ManifestEnv() []manifest.EnvVar

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:

  1. 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.

  2. 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

func NewMetricsStore(mgr *Manager) metrics.Store

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

func (c *Config) RedisAddress() string

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

func NewManager(cfg *Config, logger *zap.Logger) *Manager

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

func Provide(lc di.Lifecycle, cfg *Config, logger *zap.Logger, reg manifest.Registrar) *Manager

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

func (m *Manager) ActivateRedis(b Backend)

ActivateRedis swaps the active backend to b and marks Redis connected. Called by the supervisor on a successful (re)connect.

func (*Manager) AsResource

func (m *Manager) AsResource(name, description string, opts ...resource.Option) resource.Resource

AsResource builds a nexus resource.Resource for this Manager. Backend ("redis" vs "memory") is reported live via WithDetails.

func (*Manager) Clear

func (m *Manager) Clear(ctx context.Context) error

Clear wipes every key in the active store.

func (*Manager) Config added in v1.13.0

func (m *Manager) Config() *Config

Config, Logger, and Context expose the bits the Redis supervisor needs without it reaching into Manager internals.

func (*Manager) Context added in v1.13.0

func (m *Manager) Context() context.Context

func (*Manager) Delete

func (m *Manager) Delete(ctx context.Context, key string) error

Delete removes key from the active store.

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

func (m *Manager) Get(ctx context.Context, key string, out any) error

Get deserializes the cached value under key into out. Returns ErrNotFound when the key is missing.

func (*Manager) IsRedisConnected

func (m *Manager) IsRedisConnected() bool

IsRedisConnected reports whether Redis is the currently active store.

func (*Manager) Logger added in v1.13.0

func (m *Manager) Logger() *zap.Logger

func (*Manager) NexusEnv

func (m *Manager) NexusEnv() []manifest.EnvVar

── 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

func (*Manager) Set

func (m *Manager) Set(ctx context.Context, key string, value any, ttl time.Duration) error

Set stores value under key with the given TTL.

func (*Manager) Start

func (m *Manager) Start()

Start restores the persist file (if any) and, when the Redis backend is registered and Environment is "production", launches the supervisor. Safe to call once.

func (*Manager) Stop

func (m *Manager) Stop()

Stop snapshots the persist file (if any), stops the supervisor, and cancels the manager context. Idempotent.

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.

Directories

Path Synopsis
Package redis is the opt-in Redis backend for the nexus cache.
Package redis is the opt-in Redis backend for the nexus cache.

Jump to

Keyboard shortcuts

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