Documentation
ΒΆ
Overview ΒΆ
Package grcache provides a generic, backend-agnostic caching abstraction for the gourdian ecosystem.
Overview: grcache is the same architectural pattern gourdiantoken uses for token storage β a small interface (Cache) implemented by multiple interchangeable backends β applied to general-purpose caching instead. It is the shared caching layer grauth (permission/session caching), graudit (read-path caching), and gourdianerp (application-level caching) depend on.
Key Features:
- A single Cache interface: Get, Set, Delete, Exists, InvalidateTag, Stats, Close β implemented identically by all five backends
- Per-key TTL on every Set call, with ttl=0 meaning "no expiry"
- Tag-based bulk invalidation: associate a key with one or more tags at Set time, then remove every key under a tag with one InvalidateTag call
- A Stats() snapshot (hits, misses, evictions, key count, avg latency)
- Sentinel errors (ErrKeyNotFound, ErrCacheUnavailable, ErrInvalidTTL, ErrClosed) usable with errors.Is
- Optional diagnostic logging via any logger satisfying the small Logger interface β grlog.Logger works with no adapter
Getting Started:
import (
"context"
"log"
"time"
"github.com/gourdian25/grcache/memory"
)
func main() {
cache, err := memory.NewMemoryCache()
if err != nil {
log.Fatal(err)
}
defer cache.Close()
ctx := context.Background()
if err := cache.Set(ctx, "user:42", []byte("alice"), time.Minute, "tenant:acme"); err != nil {
log.Fatal(err)
}
val, err := cache.Get(ctx, "user:42")
if err != nil {
log.Fatal(err)
}
log.Println(string(val)) // "alice"
}
Backends:
Each backend lives in its own importable subpackage so that consumers who only need one backend don't pull in the client libraries the others depend on β importing grcache/memory alone adds zero external dependencies.
In-memory (grcache/memory) β default, zero external dependency, for local development and testing. A background goroutine sweeps expired entries; Get/Exists also check expiry lazily as a correctness backstop.
cache, err := memory.NewMemoryCache(memory.WithSweepInterval(30 * time.Second))
Redis (grcache/redis) β primary production backend. Tags are stored as Redis Sets; InvalidateTag pipelines SMEMBERS + DEL.
cache, err := redis.NewRedisCache(redis.RedisConfig{Addr: "localhost:6379"})
Memcached (grcache/memcached) β secondary backend. Tags are emulated via a serialized member list, which is best-effort/eventually consistent under concurrent writes (see grcache/memcached's package doc for the exact tradeoff).
cache, err := memcached.NewMemcachedCache(memcached.MemcachedConfig{Servers: []string{"localhost:11211"}})
PostgreSQL (grcache/postgres) β via GORM. Tags live in a separate join table kept in sync with the entries table on every Set/Delete. Intended for test/dev/CI environments with Postgres already available but no Redis/memcached; prefer Redis in production.
cache, err := postgres.NewPostgresCache(postgres.PostgresConfig{DSN: dsn})
MongoDB (grcache/mongo) β tags live directly on the document as an array field. A TTL index (expireAfterSeconds: 0) gives native, database-managed expiry, the same as Redis's EX. Same intended use case as PostgreSQL above: test/dev/CI without Redis/memcached.
cache, err := mongo.NewMongoCache(mongo.MongoConfig{URI: uri, Database: "myapp"})
Dragonfly and Valkey are Redis-protocol compatible β point RedisConfig.Addr at either instead of building a separate backend.
Tag-Based Invalidation:
cache.Set(ctx, "session:abc", data, time.Hour, "user:42", "tenant:acme") cache.Set(ctx, "session:def", data, time.Hour, "user:42") n, err := cache.InvalidateTag(ctx, "user:42") // removes both sessions, n == 2
TTL Semantics:
A ttl of 0 passed to Set means "no expiry" on every backend. Redis stores no EX flag; Mongo omits the expiresAt field entirely so its TTL index never touches the document; the memory and postgres backends simply never sweep it. A negative ttl returns ErrInvalidTTL. Expiry is always checked lazily on Get/Exists in addition to whatever background mechanism a backend uses (sweep goroutine, native TTL), so a not-yet-reaped expired entry is never visible to a caller.
Error Handling:
val, err := cache.Get(ctx, key)
if errors.Is(err, grcache.ErrKeyNotFound) {
// cache miss β expected control flow, not a failure
} else if err != nil {
// backend unavailable or another real failure
}
Backend-native errors (redis.Nil, gorm.ErrRecordNotFound, mongo.ErrNoDocuments, memcache.ErrCacheMiss) are always translated into a grcache sentinel before being wrapped β a caller using only errors.Is against grcache's own sentinels never needs to know which backend is underneath.
Optional Logging:
Every backend accepts an optional Logger for diagnostic messages (connection failures, sweep-cycle summaries, shutdown). A nil Logger (the default) means grcache logs nothing.
import "github.com/gourdian25/grlog"
logger := grlog.NewDefaultLogger()
cache, err := redis.NewRedisCache(redis.RedisConfig{
Addr: "localhost:6379",
Logger: logger,
})
Architecture:
The root grcache package holds only the Cache interface, Stats, sentinel errors, and the Logger interface β stdlib only, no external dependencies. Each backend subpackage implements Cache against its own client library. A shared conformance package (imported sideways by every backend's own tests, never the reverse) runs one behavioral test suite against all five backends through the Cache interface, enforcing identical behavior for every scenario the suite covers. It is not an exhaustive proof of parity β see conformance.go for the current scenario list and each backend's own test file for anything scenario-specific it adds on top.
Testing:
grcache's own tests run against real local Redis/Postgres/Mongo/memcached instances β no mocks, no miniredis, no testcontainers-go β mirroring gourdiantoken's testing philosophy. See CLAUDE.md and README.md for the exact connection settings and docker commands to stand up each service.
Performance:
Run `make bench` for current numbers. InvalidateTag is benchmarked at 10/1k/100k-key tag cardinality per backend; memcached's list-based tag emulation is the one backend with a documented O(nΒ²) scaling cliff at high cardinality, a direct consequence of its best-effort tag model, not an implementation oversight.
Best Practices:
- Always `defer cache.Close()` β it stops background goroutines (memory, postgres) and closes underlying connections (redis, mongo, memcached)
- Treat ErrKeyNotFound as expected control flow, not an error to log
- Use tags to group related keys (e.g. all data for one user or tenant) rather than tracking key lists yourself
- Don't rely on the in-memory backend for state shared across processes or replicas β use Redis, Postgres, or Mongo for that
Out of Scope:
grcache is explicitly not a general-purpose data store: there is no query language, no filtering by value, no secondary indexes, and no range scans. It does not attempt distributed cache coherence β multiple in-memory backend instances across processes are expected to diverge; use a shared backend (Redis, Postgres, Mongo) for anything requiring shared state. It is not a durability layer: a cache miss must never be treated as data loss by callers, and there is no write-through/write-behind persistence. Cache warming/preloading orchestration is a consumer-side concern. Stats() is a snapshot only; Prometheus/OpenTelemetry export is a separate adapter-package concern, not part of this package.
License: MIT Repository: https://github.com/gourdian25/grcache
Index ΒΆ
Constants ΒΆ
This section is empty.
Variables ΒΆ
var ( // ErrKeyNotFound indicates the key does not exist or has expired. ErrKeyNotFound = errors.New("grcache: key not found") // (connection failure, timeout, etc.). ErrCacheUnavailable = errors.New("grcache: backend unavailable") // ErrInvalidTTL indicates a negative ttl was passed to Set. ErrInvalidTTL = errors.New("grcache: invalid ttl") // ErrClosed indicates a method was called after Close. ErrClosed = errors.New("grcache: cache is closed") )
Sentinel errors for use with errors.Is. Backend implementations translate their own native errors (redis.Nil, gorm.ErrRecordNotFound, mongo.ErrNoDocuments, memcache.ErrCacheMiss, ...) into these sentinels before wrapping with fmt.Errorf("...: %w", ...) β a backend-native error must never leak through the Cache interface unwrapped, since doing so would break the backend-agnostic contract the interface exists to provide.
There is deliberately no IsNotFound(err error) bool helper: callers use errors.Is(err, grcache.ErrKeyNotFound) directly, consistent with how gourdiantoken's own sentinel errors are consumed.
Example:
if _, err := cache.Get(ctx, key); errors.Is(err, grcache.ErrKeyNotFound) {
// expected cache miss, not a failure
}
Functions ΒΆ
This section is empty.
Types ΒΆ
type Cache ΒΆ
type Cache interface {
// Get retrieves the raw bytes stored at key.
//
// Parameters:
// - ctx: context.Context β propagated to the backend's network/DB call
// - key: string β the cache key to look up
//
// Returns:
// - []byte: a copy of the stored value; safe for the caller to mutate
// - error: ErrKeyNotFound if key does not exist or has expired,
// ErrCacheUnavailable if the backend could not be reached,
// ErrClosed if called after Close
//
// Example:
//
// val, err := cache.Get(ctx, "user:42")
// if errors.Is(err, grcache.ErrKeyNotFound) {
// // cache miss β expected control flow, not a failure
// } else if err != nil {
// return err
// }
Get(ctx context.Context, key string) ([]byte, error)
// Set stores val at key with the given ttl, optionally associating it
// with one or more tags for later bulk removal via InvalidateTag.
//
// Parameters:
// - ctx: context.Context
// - key: string β the cache key
// - val: []byte β the value to store; Set copies it, so the caller's
// slice may be reused/mutated afterward
// - ttl: time.Duration β 0 means "no expiry" on every backend (Redis
// stores no EX flag, Mongo omits the TTL-indexed field, memory/
// postgres simply never sweep it); negative returns ErrInvalidTTL
// - tags: ...string β optional (nil/empty is valid); each tag can
// later be passed to InvalidateTag to remove every key under it
//
// Returns:
// - error: ErrInvalidTTL for a negative ttl, ErrCacheUnavailable on a
// backend failure, ErrClosed if called after Close
//
// Example:
//
// err := cache.Set(ctx, "session:abc", data, time.Hour, "user:42", "tenant:acme")
Set(ctx context.Context, key string, val []byte, ttl time.Duration, tags ...string) error
// Delete removes key. Deleting a non-existent key is not an error.
//
// Parameters:
// - ctx: context.Context
// - key: string β the cache key to remove
//
// Returns:
// - error: ErrCacheUnavailable on a backend failure, ErrClosed if
// called after Close; never an error for a key that doesn't exist
Delete(ctx context.Context, key string) error
// Exists reports whether key is present and not expired.
//
// Parameters:
// - ctx: context.Context
// - key: string β the cache key to check
//
// Returns:
// - bool: true if key exists and has not expired
// - error: ErrCacheUnavailable on a backend failure, ErrClosed if
// called after Close (never ErrKeyNotFound β a missing key is
// reported as (false, nil))
Exists(ctx context.Context, key string) (bool, error)
// InvalidateTag deletes every key currently associated with tag.
//
// Parameters:
// - ctx: context.Context
// - tag: string β the tag whose member keys should be removed
//
// Returns:
// - int: the number of keys removed (0 if the tag has no members,
// which is not an error)
// - error: ErrCacheUnavailable on a backend failure, ErrClosed if
// called after Close
//
// Example:
//
// n, err := cache.InvalidateTag(ctx, "tenant:acme")
// // n is how many keys under "tenant:acme" were removed
InvalidateTag(ctx context.Context, tag string) (int, error)
// Stats returns a snapshot of hit/miss/eviction counters and basic
// latency info since the cache was created or last reset.
//
// Parameters:
// - ctx: context.Context
//
// Returns:
// - Stats: see the Stats type doc comment for field semantics
// - error: ErrCacheUnavailable on a backend failure, ErrClosed if
// called after Close
Stats(ctx context.Context) (Stats, error)
// Close releases any underlying connections/resources (stops background
// sweep goroutines for memory/postgres, closes the client for redis/
// mongo/memcached). After Close, every other method returns ErrClosed.
// Close is idempotent β calling it more than once is safe and returns
// nil on every call after the first.
//
// Returns:
// - error: any error encountered while releasing resources
Close() error
}
Cache is the primary interface all backends implement. Every backend subpackage (grcache/memory, grcache/redis, grcache/memcached, grcache/postgres, grcache/mongo) provides a New<Backend>Cache constructor returning a Cache, so application code can depend on this interface alone and swap backends without changing call sites.
type Logger ΒΆ
type Logger interface {
Infof(format string, args ...interface{})
Warnf(format string, args ...interface{})
Errorf(format string, args ...interface{})
}
Logger is the minimal logging interface grcache backends accept for optional diagnostic logging (constructor connectivity failures, background sweep-cycle summaries, shutdown). It is satisfied structurally by *grlog.Logger's printf-style methods (Infof/Warnf/Errorf) β grcache itself does not import grlog, so plugging in a logger is entirely opt-in and adds no dependency for consumers who don't want one. Any logger exposing the same three methods works; grlog is simply the ecosystem's own recommended choice.
A nil Logger passed to any backend's Config/Option is replaced with NopLogger() β logging is always optional, never required for a backend to function.
Example:
import "github.com/gourdian25/grlog"
logger := grlog.NewDefaultLogger()
cache, err := redis.NewRedisCache(redis.RedisConfig{
Addr: "localhost:6379",
Logger: logger, // *grlog.Logger satisfies grcache.Logger directly
})
func NopLogger ΒΆ
func NopLogger() Logger
NopLogger returns a Logger that discards every message. It is the default used by every backend when no Logger is configured.
Returns:
- Logger: a non-nil, no-op implementation safe to call from any goroutine
type Stats ΒΆ
type Stats struct {
// Hits is the number of Get calls that found a live (non-expired) entry.
Hits uint64
// Misses is the number of Get calls for a key that did not exist or had
// expired.
Misses uint64
// Evictions is the number of entries removed by a backend's own
// expiry/reclamation mechanism (a sweep goroutine for memory/postgres),
// not by an explicit Delete or InvalidateTag call.
Evictions uint64
// KeyCount is the current number of live keys, or -1 if the backend
// cannot report this cheaply (Redis, which would need a full SCAN to
// count keys under its prefix).
KeyCount int64
// AvgLatency is reserved for future use; backends may leave it zero.
AvgLatency time.Duration
}
Stats is a snapshot of cache counters and latency, returned by Cache.Stats. Counters are cumulative since the cache was constructed β there is no Reset method; construct a new Cache to start counters over.
Directories
ΒΆ
| Path | Synopsis |
|---|---|
|
Package conformance is a shared behavioral test suite run against every grcache backend via the common Cache interface.
|
Package conformance is a shared behavioral test suite run against every grcache backend via the common Cache interface. |
|
Command example is a standalone runnable demonstration of every grcache backend.
|
Command example is a standalone runnable demonstration of every grcache backend. |
|
Package memcached is grcache's secondary, lower-priority backend (behind Redis).
|
Package memcached is grcache's secondary, lower-priority backend (behind Redis). |
|
Package memory is grcache's default, zero-external-dependency backend.
|
Package memory is grcache's default, zero-external-dependency backend. |
|
Package mongostore is a grcache backend for test/dev/CI environments that have a MongoDB instance available but not Redis or memcached β it is not a recommended production alternative to grcache/redis.
|
Package mongostore is a grcache backend for test/dev/CI environments that have a MongoDB instance available but not Redis or memcached β it is not a recommended production alternative to grcache/redis. |
|
Package postgres is a grcache backend for test/dev/CI environments that have a PostgreSQL instance available but not Redis or memcached β it is not a recommended production alternative to grcache/redis.
|
Package postgres is a grcache backend for test/dev/CI environments that have a PostgreSQL instance available but not Redis or memcached β it is not a recommended production alternative to grcache/redis. |
|
Package redis is grcache's primary production backend, built directly on gourdiantoken's proven Redis handling conventions: the same Ping-on-construct validation, the same error-wrapping style, and a transactional pipeline (TxPipeline, i.e.
|
Package redis is grcache's primary production backend, built directly on gourdiantoken's proven Redis handling conventions: the same Ping-on-construct validation, the same error-wrapping style, and a transactional pipeline (TxPipeline, i.e. |