grcache

package module
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Jul 10, 2026 License: MIT Imports: 3 Imported by: 0

README ΒΆ

πŸ—„οΈ grcache β€” Generic, Backend-Agnostic Caching for Go

grcache is a generic, backend-agnostic caching abstraction for the gourdian ecosystem β€” the same architectural pattern gourdiantoken uses for token storage (TokenRepository interface β†’ multiple backend implementations), applied instead to general-purpose caching. It is the shared caching layer that grauth (permission/session caching), graudit (read-path caching), and gourdianerp (application-level caching) depend on.

🌐 Part of the gourdian25 ecosystem

grcache is one of several small, independent Go libraries meant to be used together:

  • gourdiantoken β€” JWT access/refresh token issuance, verification, revocation, and rotation.
  • grlog β€” zero-dependency structured logging; grcache's optional Logger interface is satisfied by it directly.
  • grevents β€” an in-process event bus for decoupling producers of state changes from consumers that react to them.
  • graudit β€” an append-only audit log with pluggable storage backends, mirroring grcache's own layout.
  • grpolicy β€” attribute-based policy evaluation (RBAC/ABAC), independent of any notion of "user" or "role".

🎯 Why grcache?

  • One interface, five backends. Write your caching code once against Cache; swap in-memory for Redis (or Postgres, or Mongo, or memcached) by changing one constructor call.
  • Tag-based invalidation everywhere. Every backend supports tagging a key at Set time and bulk-removing everything under a tag later β€” this isn't a Redis-only feature bolted on top.
  • No surprises across backends. A shared conformance test suite runs the exact same behavioral assertions against all five backends, so switching backends doesn't mean re-learning edge-case semantics.
  • Zero dependency for the common case. Import grcache/memory alone and you pull in nothing beyond the Go standard library.
  • Optional, adapter-free logging. Plug in grlog (or any logger with the same three methods) with zero glue code.

πŸ“š Table of Contents

πŸ“¦ Installation

go get github.com/gourdian25/grcache

Each backend is its own importable subpackage, so only add what you use:

go get github.com/gourdian25/grcache/memory      # zero extra dependencies
go get github.com/gourdian25/grcache/redis       # + github.com/redis/go-redis/v9
go get github.com/gourdian25/grcache/memcached   # + github.com/bradfitz/gomemcache
go get github.com/gourdian25/grcache/postgres    # + gorm.io/gorm, gorm.io/driver/postgres
go get github.com/gourdian25/grcache/mongostore  # + go.mongodb.org/mongo-driver

πŸš€ Quick Start

package main

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"

	n, err := cache.InvalidateTag(ctx, "tenant:acme")
	log.Printf("invalidated %d keys", n) // 1
}

See example/example.go for a runnable demo covering all five backends (go run ./example) β€” the networked backends print a "skipping" message and continue if their local service isn't running.

πŸ—οΈ Architecture

grcache (root)              β€” Cache interface, Stats, sentinel errors, Logger interface
  β”œβ”€β”€ grcache/memory         β€” in-process map, zero dependencies
  β”œβ”€β”€ grcache/redis          β€” github.com/redis/go-redis/v9
  β”œβ”€β”€ grcache/memcached      β€” github.com/bradfitz/gomemcache
  β”œβ”€β”€ grcache/postgres       β€” gorm.io/gorm + gorm.io/driver/postgres
  β”œβ”€β”€ grcache/mongostore     β€” go.mongodb.org/mongo-driver
  └── grcache/conformance    β€” shared behavioral test suite (imported by every backend's own tests)

Unlike gourdiantoken and grlog (both flat, single-package repos), grcache uses one subpackage per backend. This is deliberate: a flat package would force every backend's client library into every consumer's dependency graph, even consumers using only grcache/memory β€” see docs/architecture.md for this and grcache's other documented divergences from sibling conventions.

πŸ’Ύ Backends

Backend Package Expiry mechanism Tag storage
In-memory grcache/memory Application sweep goroutine In-process map
Redis grcache/redis Native (EX) + lazy backstop Redis Sets, pipelined SMEMBERS+DEL
Memcached grcache/memcached Native (Expiration) Serialized list β€” best-effort, eventually consistent (see below)
PostgreSQL grcache/postgres Application sweep goroutine Join table (grcache_entry_tags), kept in sync every Set/Delete
MongoDB grcache/mongostore Native TTL index (expireAfterSeconds: 0) Embedded array field on the same document

Redis is the recommended default for production. PostgreSQL and MongoDB exist specifically for test/dev/CI environments where a Redis (or memcached) instance isn't available but a Postgres or Mongo instance already is β€” not as a general recommendation to run a cache on top of a relational or document database in production instead of Redis.

In-memory
cache, err := memory.NewMemoryCache(
	memory.WithSweepInterval(30 * time.Second), // default
	memory.WithLogger(logger),                   // optional
)

Zero external dependencies. Does not coordinate state across processes or replicas β€” running it behind multiple app instances means each instance has its own independent cache that will diverge, which is expected.

Redis
cache, err := redis.NewRedisCache(redis.RedisConfig{
	Addr:         "localhost:6379", // required
	Password:     "",
	DB:           0,
	PoolSize:     100,             // default
	DialTimeout:  5 * time.Second,  // default
	ReadTimeout:  3 * time.Second,  // default
	WriteTimeout: 3 * time.Second,  // default
	Logger:       logger,           // optional
})

Built directly on gourdiantoken's proven Redis conventions: the same Ping-on-construct validation and error-wrapping style. Tags are Redis Sets; InvalidateTag pipelines SMEMBERS + DEL in one round trip. No Lua/EVAL β€” gourdiantoken's own docs claim Lua scripting but its actual code only ever uses Pipeline/SETNX, so grcache matches gourdiantoken's real behavior, not its documentation.

Memcached
cache, err := memcached.NewMemcachedCache(memcached.MemcachedConfig{
	Servers:      []string{"localhost:11211"}, // required
	Timeout:      500 * time.Millisecond,        // default
	MaxIdleConns: 2,                              // default
	Logger:       logger,                         // optional
})

Documented limitation: memcached has no native set/list type, so tags are emulated with a newline-delimited member list under its own key, updated by read-modify-write on every tagged Set. This is explicitly best-effort/eventually-consistent β€” concurrent Set calls tagging the same tag can race and drop a member, meaning that key simply won't be swept up by a later InvalidateTag (the key itself is unaffected; only the tag index entry can be lost). Also note: memcached only supports second-granularity TTLs, so a sub-second ttl rounds up to 1 second rather than truncating to 0 (which would mean "never expire").

PostgreSQL
cache, err := postgres.NewPostgresCache(postgres.PostgresConfig{
	DSN:             "host=localhost user=myuser password=mypass dbname=mydb port=5432 sslmode=disable",
	MaxOpenConns:    0, // database/sql default
	MaxIdleConns:    0, // database/sql default
	ConnMaxLifetime: 0, // reuse indefinitely
	SweepInterval:   30 * time.Second, // default
	Logger:          logger,            // optional
})

Via GORM. Two tables: grcache_entries (key/value/expires_at) and grcache_entry_tags (a composite-indexed join table), auto-migrated on construct. Postgres has no native expiry at all β€” unlike Redis/Mongo, the background sweep here is the only reclamation mechanism, not a backstop; Get/Exists's lazy expiry check is what keeps reads correct between sweeps.

Intended for test/dev/CI environments with a Postgres instance already available but no Redis/memcached β€” prefer Redis in production.

MongoDB
cache, err := mongo.NewMongoCache(mongo.MongoConfig{
	URI:        "mongodb://localhost:27017", // required
	Database:   "myapp",                       // required
	Collection: "grcache_entries",              // default
	Logger:     logger,                         // optional
})

Tags live directly as an array field on the same document β€” no join table needed, unlike Postgres. A TTL index (expireAfterSeconds: 0) gives native, database-managed expiry, the same as Redis's EX; documents with no expiresAt field (ttl=0) are simply never touched by the TTL monitor.

Intended for test/dev/CI environments with a Mongo instance already available but no Redis/memcached β€” prefer Redis in production.

🏷️ 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

Use tags to group related keys (all sessions for a user, all cached rows for a tenant) instead of tracking key lists yourself.

⏱️ 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, and the memory/postgres backends simply never sweep the entry. A negative ttl returns ErrInvalidTTL. Every backend also checks expiry lazily on Get/Exists in addition to whatever background mechanism it uses, so a not-yet-reaped expired entry is never visible to a caller.

πŸ“Š Stats & Observability

stats, err := cache.Stats(ctx)
// stats.Hits, stats.Misses, stats.Evictions, stats.KeyCount, stats.AvgLatency

KeyCount is -1 for backends that can't report it cheaply (Redis, which would need a full SCAN to count keys under its prefix). Stats() is a snapshot only β€” wiring these numbers into Prometheus/OpenTelemetry is a consumer-side or adapter-package concern, not something this package does.

⚠️ 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 {
	// ErrCacheUnavailable, ErrClosed, or ErrInvalidTTL
}

Backend-native errors (redis.Nil, gorm.ErrRecordNotFound, mongo.ErrNoDocuments, memcache.ErrCacheMiss) are always translated into a grcache sentinel before being wrapped β€” a caller using errors.Is against grcache's own sentinels never needs to know which backend is underneath. There is deliberately no IsNotFound(err error) bool helper; use errors.Is directly, consistent with how gourdiantoken's own sentinel errors are consumed.

πŸ“ Optional Logging

Every backend accepts an optional Logger (a Config.Logger field, or memory.WithLogger(...) for the in-memory backend) for diagnostic messages β€” connection failures, sweep-cycle summaries, shutdown. Logging is entirely opt-in: a nil Logger (the default) means grcache logs nothing, and the grcache root package itself does not depend on any logging library.

grcache.Logger is a tiny structural interface (Infof/Warnf/Errorf), satisfied directly by grlog's *grlog.Logger β€” the ecosystem's own recommended choice β€” with no adapter needed:

import (
	"github.com/gourdian25/grlog"
	"github.com/gourdian25/grcache/redis"
)

logger := grlog.NewDefaultLogger()

cache, err := redis.NewRedisCache(redis.RedisConfig{
	Addr:   "localhost:6379",
	Logger: logger,
})

Any logger exposing the same three methods works β€” grlog is not required.

🧩 Backend Compatibility

Dragonfly and Valkey are Redis-protocol compatible β€” point the Redis backend's RedisConfig.Addr at your Dragonfly/Valkey instance; no separate backend is needed. Verify compatibility against the specific commands grcache issues (GET, SET EX, DEL, EXISTS, SADD, SMEMBERS, pipelining) rather than assuming blanket compatibility.

πŸ§ͺ Testing

grcache's tests run against real local services (no mocks, no miniredis/testcontainers-go), mirroring gourdiantoken's testing philosophy. Start the services below before running make test / make race:

docker run -d --name grcache-redis      -p 6379:6379  redis:7 --requirepass redis_password
docker run -d --name grcache-postgres   -p 5432:5432  -e POSTGRES_USER=postgres_user -e POSTGRES_PASSWORD=postgres_password postgres:16
docker run -d --name grcache-mongo      -p 27018:27017 -e MONGO_INITDB_ROOT_USERNAME=root -e MONGO_INITDB_ROOT_PASSWORD=mongo_password mongo:7
docker run -d --name grcache-memcached  -p 11211:11211 memcached:1.6

Then create the Postgres test database once:

docker exec grcache-postgres psql -U postgres_user -d postgres -c "CREATE DATABASE grcache_test;"
Backend Connection
Redis localhost:6379, password redis_password, DB 14
PostgreSQL host=localhost user=postgres_user password=postgres_password dbname=grcache_test port=5432 sslmode=disable
MongoDB mongodb://root:mongo_password@localhost:27018/?directConnection=true, database grcache_test
Memcached localhost:11211

The Redis DB index, Postgres database name, and Mongo database name are deliberately different from gourdiantoken's own test settings (DB 15, postgres_db, and its own Mongo database) so both suites can run against the same local instances without colliding.

Every package independently maintains at least 80% test coverage, matching gourdiantoken's own COVERAGE_MIN convention:

make coverage-check

πŸ“ˆ Benchmarks

make bench

InvalidateTag is benchmarked at 10/1k/100k-key tag cardinality per backend, since it's the operation most likely to have a hidden performance cliff. Indicative numbers from this repo's own dev machine (Apple M4; run make bench for current numbers on your hardware):

Backend 10 keys 1,000 keys 100,000 keys
memory ~9Β΅s ~133Β΅s ~9.2ms
Redis ~3.2ms ~3.1ms ~68ms
PostgreSQL ~26ms ~26ms ~93ms
MongoDB ~7.6ms ~11ms ~404ms
memcached ~4.1ms (10) ~92ms (1,000, capped) not run β€” see below

Redis's numbers above are post-v0.1.1's TxPipeline fix (see CHANGELOG.md) β€” MULTI/EXEC overhead versus the previous non-transactional Pipeline turned out to be within measurement noise (a few percent, not a step change), since go-redis still sends the whole batch in one round trip either way.

memcached's benchmark deliberately caps at 1,000 keys, not 100,000: its list-based tag emulation does a read-modify-write of the entire member list on every tagged Set, so populating a single tag with n keys costs O(nΒ²) total data movement. The 10β†’100β†’1,000 progression above (4ms β†’ 12.6ms β†’ 92ms) already demonstrates the scaling cliff clearly β€” a full 100,000-key run would be impractically slow to include in a routine make bench, which is itself the direct, expected consequence of the documented eventual-consistency tradeoff, not a gap in the measurement.

πŸ—ΊοΈ Roadmap

Explicitly deferred, not forgotten:

  • Prometheus/OpenTelemetry metrics adapter (separate module).
  • Distributed invalidation pub/sub, likely built on top of grevents (already released, not yet wired up here).
  • Cache-aside helper wrappers (e.g. GetOrSet(ctx, key, loader)), deferred to keep v1's surface area minimal until the core interface has been proven in real usage inside grauth.

🚫 Out of Scope

  • General-purpose data store behavior (no query language, no filtering by value, no secondary indexes, no range scans).
  • Distributed cache coherence / invalidation propagation across nodes.
  • Write-through / write-behind persistence patterns.
  • Cache warming / preloading orchestration.
  • Prometheus/OpenTelemetry metrics export (Stats() is a snapshot only).

🀝 Contributing

make fmt              # gofmt
make vet               # go vet
make lint              # golangci-lint (if installed)
make test               # go test -cover ./...
make race                # go test -race ./...  β€” required before any PR touching backend code
make coverage-check        # every package must independently meet 80%

See CLAUDE.md for the full architecture rundown and docs/architecture.md for the reasoning behind grcache's deliberate divergences from gourdiantoken's and grlog's conventions.

πŸš€ Releasing

Releases are built with goreleaser:

make goreleaser-check          # dry run β€” validates .goreleaser.yaml, builds a local snapshot, no tag/push
make release VERSION=vX.Y.Z    # tags, pushes, and runs goreleaser release --clean

See CHANGELOG.md for release history.

πŸ“„ License

MIT β€” see LICENSE.

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.

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

  2. 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"})

  3. 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"}})

  4. 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})

  5. 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 ΒΆ

View Source
var (
	// ErrKeyNotFound indicates the key does not exist or has expired.
	ErrKeyNotFound = errors.New("grcache: key not found")

	// ErrCacheUnavailable indicates the backend could not be reached
	// (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

func OrNop ΒΆ

func OrNop(l Logger) Logger

OrNop returns l if it is non-nil, otherwise NopLogger(). Backends call this once at construction time so every subsequent log call site can assume a non-nil Logger.

Parameters:

  • l: Logger β€” may be nil

Returns:

  • Logger: l unchanged if non-nil, otherwise NopLogger()

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.

Jump to

Keyboard shortcuts

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