entcache

package module
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Jul 30, 2026 License: Apache-2.0 Imports: 21 Imported by: 0

README

entcache

Go Reference License

A production-ready cache driver for ent with a variety of storage options and cache-aside strategies:

  • Context-level — per-request cache attached to a context.Context (e.g. HTTP request or GraphQL resolve) that eliminates duplicate queries within a single request.
  • Driver-level — process-level cache embedded in the ent.Client and shared across all goroutines.
  • Remote-level — persistent, shared cache backed by go-redis, rueidis (with stampede protection), or NATS JetStream KV.
  • Multi-level — hierarchical cache structure (e.g. L1 LRU memory cache + L2 remote Redis/NATS store) for optimal latency and durability.
  • Mutation-aware invalidation — ent hooks automatically invalidate stale cache entries when entity mutations (create, update, delete) occur.

Compatible with standard database/sql drivers as well as native drivers like entpgx.

Installation

go get github.com/incroy/entcache

Quick Start

With database/sql Driver
// Open the database connection.
db, err := sql.Open(dialect.Postgres, "postgres://localhost:5432/mydb?sslmode=disable")
if err != nil {
    log.Fatal("opening database", err)
}

// Wrap the sql.Driver with entcache.Driver.
drv := entcache.NewDriver(
    sql.OpenDB(dialect.Postgres, db),
    entcache.TTL(time.Minute),
)

// Create an ent.Client.
client := ent.NewClient(ent.Driver(drv))

// Skip the cache during schema migration.
if err := client.Schema.Create(entcache.Skip(ctx)); err != nil {
    log.Fatal("running schema migration", err)
}

// First call hits the database.
u, err := client.User.Get(ctx, id)

// Second call is served from cache.
u, err = client.User.Get(ctx, id)
With entpgx (Native pgx Driver)

entpgx provides a native pgxpool.Pool-based dialect.Driver that bypasses database/sql entirely. entcache wraps it seamlessly:

import (
    "github.com/incroy/entpgx"
    "github.com/jackc/pgx/v5/pgxpool"
    "github.com/incroy/entcache"
)

// Create a pgxpool.Pool.
pool, err := pgxpool.New(ctx, "postgres://localhost:5432/mydb?sslmode=disable")
if err != nil {
    log.Fatal(err)
}

// Create the entpgx driver.
pgxDrv := entpgx.NewDriver(pool)

// Wrap with entcache. entcache works with any dialect.Driver.
drv := entcache.NewDriver(
    pgxDrv,
    entcache.TTL(time.Minute),
    entcache.Levels(
        entcache.NewLRU(1024),
    ),
)

// Create an ent.Client.
client := ent.NewClient(ent.Driver(drv))

// Skip cache during migrations.
if err := client.Schema.Create(entcache.Skip(ctx)); err != nil {
    log.Fatal(err)
}

// Queries are cached transparently.
u, err := client.User.Get(ctx, id)

High Level Design

On a high level, entcache.Driver decorates the Query method of the given driver, and for each call, generates a cache key (i.e. hash) from its arguments (statement and query parameters). After the query is executed, the driver records the raw values of the returned rows (sql.Rows) and stores them in the cache store with the generated cache key. Subsequent identical queries replay the recorded rows directly from cache without hitting the database, provided the entry has not expired or been evicted.

+------------+       1. Query(SQL, Args)       +------------------+       2. Get(Key)       +-------------+
| ent.Client | ------------------------------> | entcache.Driver  | ----------------------> | Cache Store |
+------------+                                 +------------------+                         +-------------+
                                                         |                                         |
                                                         | (Cache Miss)                            | (Hit)
                                                         v                                         v
                                               +------------------+                       +-----------------+
                                               | Wrapped Driver   |                       | Replay Cached   |
                                               | (SQL Database)   |                       | Rows            |
                                               +------------------+                       +-----------------+

The package provides a rich set of options to configure entry TTLs, control hash functions, set up multi-level cache hierarchies, invalidate/skip entries on-demand, and perform automatic mutation-aware invalidation.

Caching Levels

entcache provides several builtin cache levels:

  1. context.Context Cache — Attached to a request (e.g. HTTP request or GraphQL resolve). Used to eliminate duplicate database queries executed during the same request lifecycle.
  2. Driver-Level Cache — Embedded in ent.Client. Shared across all goroutines in the application process.
  3. Remote-Level Cache — Remote cache (Redis, Rueidis, NATS KV) providing persistence and sharing cache entries across multiple service replicas.
  4. Multi-Level Cache — Hierarchical cache structure combining fast in-memory LRU caching with remote persistent backends.

Context-Level Cache

Scoped to a single context.Context (e.g. *http.Request). The context carries an LRU cache (configurable) to eliminate duplicate database queries executed during the same request lifecycle.

This option is ideal for applications that require strong data consistency while preventing duplicate database queries within a request. For example, given the following GraphQL query:

query($ids: [ID!]!) {
    nodes(ids: $ids) {
        ... on User {
            id
            name
            todos {
                id
                owner {
                    id
                    name
                }
            }
        }
    }
}

A naive GraphQL resolver executes 1 query for fetching $N$ users, $N$ queries for fetching todos of each user, and another query for each todo item to fetch its owner (the classic N+1 Problem).

Ent optimizes this by batching execution into 3 queries:

  1. Fetch $N$ users
  2. Fetch todo items for all users
  3. Fetch owners of all todo items

With entcache, the number of queries is further reduced from 3 to 2, because the 1st query (fetching users) and 3rd query (fetching owners of todos) execute identical SQL statements, allowing the 3rd query to be served directly from the request-level cache.

context-level-cache

Usage In GraphQL

Instantiate entcache.Driver with ContextLevel():

drv := entcache.NewDriver(sqlDrv, entcache.ContextLevel())
client := ent.NewClient(ent.Driver(drv))

Wrap the request context.Context with entcache.NewContext when a GraphQL query arrives:

// GraphQL middleware
srv.AroundResponses(func(ctx context.Context, next graphql.ResponseHandler) *graphql.Response {
    if op := graphql.GetOperationContext(ctx).Operation; op != nil && op.Operation == ast.Query {
        ctx = entcache.NewContext(ctx)
    }
    return next(ctx)
})
HTTP Middleware Example
srv.Use(func(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        if r.Method == http.MethodGet {
            r = r.WithContext(entcache.NewContext(r.Context()))
        }
        next.ServeHTTP(w, r)
    })
})

A full runnable server example is located in examples/ctxlevel.


Driver-Level Cache

A driver-level cache stores cache entries on the ent.Client. Since an application typically creates one driver per database instance, this acts as a process-level cache shared across all application goroutines.

driver-level-cache

Create a default driver-level cache (unlimited LRU):
drv := entcache.NewDriver(sqlDrv)
client := ent.NewClient(ent.Driver(drv))
Set TTL to 1 second:
drv := entcache.NewDriver(sqlDrv, entcache.TTL(time.Second))
client := ent.NewClient(ent.Driver(drv))
Limit LRU size and set TTL:
drv := entcache.NewDriver(
    sqlDrv,
    entcache.TTL(time.Second),
    entcache.Levels(entcache.NewLRU(128)),
)
client := ent.NewClient(ent.Driver(drv))

Remote-Level Cache

Remote-level caching shares cached entries across multiple application instances. A remote cache layer is resistant to application deployments and restarts, reducing database load across distributed microservices.

Redis (go-redis)
rdb := redis.NewClient(&redis.Options{Addr: ":6379"})
drv := entcache.NewDriver(sqlDrv,
    entcache.TTL(time.Minute),
    entcache.Levels(entcache.NewRedis(rdb)),
)
Rueidis (with Stampede Protection)

High-performance Redis client featuring stampede protection. When a cache miss occurs, only the first caller fetches from the database — concurrent callers wait on a channel until the cache entry is populated.

c, err := rueidis.NewClient(rueidis.ClientOption{
    InitAddress: []string{"127.0.0.1:6379"},
})
if err != nil {
    log.Fatal(err)
}
drv := entcache.NewDriver(sqlDrv,
    entcache.TTL(time.Minute),
    entcache.Levels(entcache.NewRueidis(c)),
)
NATS JetStream KV

Distributed cache backed by NATS JetStream KeyValue. Supports per-key TTL via Create and real-time invalidation notifications via Watch.

nc, _ := nats.Connect(nats.DefaultURL)
js, _ := jetstream.New(nc)
kv, _ := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{
    Bucket: "entcache",
    MaxAge: 10 * time.Minute, // bucket-level TTL
})
drv := entcache.NewDriver(sqlDrv,
    entcache.TTL(time.Minute),
    entcache.Levels(entcache.NewNatsKV(kv)),
)

The NatsKV backend also exposes Create (SETNX equivalent) and Watch for building custom invalidation patterns:

nkv := entcache.NewNatsKV(kv)

// Watch for changes — useful for cross-process local cache invalidation.
watcher, _ := nkv.Watch(ctx, ">") // watch all keys
go func() {
    for entry := range watcher.Updates() {
        if entry != nil {
            localCache.Del(ctx, entry.Key())
        }
    }
}()

Multi-Level Cache

A cache hierarchy structures cache stores by access speed and capacity (e.g. L1 in-memory LRU + L2 remote Redis/NATS). Lookups cascade down the hierarchy: L1 → L2 → Database.

multi-level-cache

rdb := redis.NewClient(&redis.Options{
    Addr: ":6379",
})
drv := entcache.NewDriver(
    sqlDrv,
    entcache.TTL(time.Minute),
    entcache.Levels(
        entcache.NewLRU(256),   // Level 1: fast in-process memory
        entcache.NewRedis(rdb), // Level 2: durable shared Redis
    ),
)
client := ent.NewClient(ent.Driver(drv))

A full runnable server example is located in examples/multilevel.

Mutation-Aware Invalidation

entcache supports automatic cache invalidation on entity mutations (create, update, delete). A ChangeSet records modified entity keys and evicts stale cache entries on subsequent queries.

// Create a ChangeSet with GC interval.
cs := entcache.NewChangeSet(5 * time.Minute)
cs.Start()
defer cs.Stop()

// Create the cached driver with ChangeSet.
drv := entcache.NewDriver(sqlDrv,
    entcache.TTL(time.Minute),
    entcache.WithKeyTTL(time.Hour),      // longer TTL for Get-by-ID queries
    entcache.WithChangeSet(cs),
)

// Create the client and register the mutation hook.
client := ent.NewClient(ent.Driver(drv))
client.Use(entcache.DataChangeNotify(drv))

// Now, when a User is updated:
client.User.UpdateOneID(42).SetName("new-name").Save(ctx)

// The next Get for that user will bypass the cache and re-fetch from DB.
u, _ := client.User.Get(entcache.WithEntryKey(ctx, "User", 42), 42)
Dual TTL Strategy

Use short TTLs for hash-addressed queries (arbitrary SELECTs) and longer TTLs for key-addressed queries (Get-by-ID), since key-addressed queries are precisely invalidated by the mutation hook:

drv := entcache.NewDriver(sqlDrv,
    entcache.TTL(time.Minute),           // hash queries: 1 minute TTL
    entcache.WithKeyTTL(time.Hour),      // key queries: 1 hour TTL (invalidated on mutation)
    entcache.WithChangeSet(cs),
)

Per-Query Cache Control

Use context options to adjust caching behavior on individual queries:

// Skip the cache entirely.
client.User.Query().All(entcache.Skip(ctx))

// Skip and invalidate the cache entry.
client.User.Query().All(entcache.Evict(ctx))

// Override TTL for a specific query.
client.User.Query().All(entcache.WithTTL(ctx, 30*time.Second))

// Use a custom cache key.
client.User.Query().All(entcache.WithKey(ctx, "my-custom-key"))

// Structured entry key for precise invalidation.
client.User.Get(entcache.WithEntryKey(ctx, "User", 42), 42)

// Don't cache empty results (e.g. entity not yet created).
client.User.Get(entcache.SkipNotFound(ctx), 42)

Full Production Example (entpgx + Rueidis + Mutation Hook)

package main

import (
    "context"
    "log"
    "time"

    "github.com/incroy/entcache"
    "github.com/incroy/entpgx"
    "github.com/jackc/pgx/v5/pgxpool"
    "github.com/redis/rueidis"

    "myapp/ent"
)

func main() {
    ctx := context.Background()

    // 1. Database: native pgx pool via entpgx.
    pool, err := pgxpool.New(ctx, "postgres://localhost:5432/mydb")
    if err != nil {
        log.Fatal(err)
    }

    // 2. Remote Cache: rueidis with stampede protection.
    rc, err := rueidis.NewClient(rueidis.ClientOption{
        InitAddress: []string{"127.0.0.1:6379"},
    })
    if err != nil {
        log.Fatal(err)
    }

    // 3. ChangeSet for mutation-aware invalidation.
    cs := entcache.NewChangeSet(5 * time.Minute)
    cs.Start()
    defer cs.Stop()

    // 4. Wrap drivers: entpgx -> entcache -> ent.Client.
    pgxDrv := entpgx.NewDriver(pool)
    drv := entcache.NewDriver(pgxDrv,
        entcache.TTL(time.Minute),
        entcache.WithKeyTTL(30*time.Minute),
        entcache.WithChangeSet(cs),
        entcache.Levels(
            entcache.NewLRU(512),
            entcache.NewRueidis(rc),
        ),
    )
    client := ent.NewClient(ent.Driver(drv))

    // 5. Register the mutation hook.
    client.Use(entcache.DataChangeNotify(drv))

    // 6. Run migrations (skip cache).
    if err := client.Schema.Create(entcache.Skip(ctx)); err != nil {
        log.Fatal(err)
    }

    // Queries are cached across L1 memory and L2 Redis.
    // Mutations automatically invalidate stale keys!
}

API Reference

Full documentation is available at pkg.go.dev/github.com/incroy/entcache.

Option Functions
Function Description
TTL(d) Default cache TTL for hash-addressed queries
WithKeyTTL(d) Separate TTL for key-addressed (Get-by-ID) queries
Hash(fn) Custom hash function for cache key generation
Levels(...) Configure one or more cache backends
ContextLevel() Use context-scoped caching
WithChangeSet(cs) Enable mutation-aware invalidation
Context Helpers
Function Description
Skip(ctx) Skip the cache for this query
Evict(ctx) Skip and invalidate the cache entry
WithTTL(ctx, d) Override TTL for this query
WithKey(ctx, k) Use a custom cache key
WithEntryKey(ctx, typ, id) Structured key for precise invalidation
SkipNotFound(ctx) Don't cache empty results
NewContext(ctx, ...) Attach a cache to the context (for ContextLevel)
Cache Backends
Backend Constructor Use Case
LRU NewLRU(maxEntries) In-process, bounded cache
Redis (go-redis) NewRedis(client) Shared remote cache
Rueidis NewRueidis(client) High-perf Redis with stampede protection
NATS JetStream KV NewNatsKV(kv) Distributed cache with Watch notifications

Documentation

Index

Constants

This section is empty.

Variables

View Source
var ErrNotFound = errors.New("entcache: entry was not found")

ErrNotFound is returned by Get when an Entry does not exist in the cache.

Functions

func DataChangeNotify

func DataChangeNotify(drv *Driver) ent.Hook

DataChangeNotify returns an ent Hook that marks changed entity keys in the Driver's ChangeSet whenever a mutation (create, update, delete) is committed. This enables automatic cache invalidation for key-addressed queries.

Usage:

drv := entcache.NewDriver(sqlDrv, entcache.WithChangeSet(cs))
client := ent.NewClient(ent.Driver(drv))
client.Use(entcache.DataChangeNotify(drv))

func Evict

func Evict(ctx context.Context) context.Context

Evict returns a new Context that tells the Driver to skip and invalidate the cache entry on Query.

client.T.Query().All(entcache.Evict(ctx))

func NewContext

func NewContext(ctx context.Context, levels ...AddGetDeleter) context.Context

NewContext returns a new Context that carries a cache.

func Skip

func Skip(ctx context.Context) context.Context

Skip returns a new Context that tells the Driver to skip the cache entry on Query.

client.T.Query().All(entcache.Skip(ctx))

func SkipNotFound

func SkipNotFound(ctx context.Context) context.Context

SkipNotFound returns a new Context that tells the Driver to skip caching when the query result contains zero rows. This prevents caching empty results for entities that may be created shortly after.

client.User.Get(entcache.SkipNotFound(ctx), 42)

func WithEntryKey

func WithEntryKey(ctx context.Context, typ string, id any) context.Context

WithEntryKey returns a new Context with a structured entity key (e.g. "User:42") and marks the query as key-addressed. Key-addressed queries are eligible for the longer KeyTTL and precise invalidation via ChangeSet.

client.User.Get(entcache.WithEntryKey(ctx, "User", 42), 42)

func WithKey

func WithKey(ctx context.Context, key Key) context.Context

WithKey returns a new Context that carries the Key for the cache entry. Note that, this option should not be used if the ent.Client query involves more than 1 SQL query (e.g. eager loading).

client.T.Query().All(entcache.WithKey(ctx, "key"))

func WithTTL

func WithTTL(ctx context.Context, ttl time.Duration) context.Context

WithTTL returns a new Context that carries the TTL for the cache entry.

client.T.Query().All(entcache.WithTTL(ctx, time.Second))

Types

type AddGetDeleter

type AddGetDeleter interface {
	Del(ctx context.Context, k Key) error
	Add(ctx context.Context, k Key, e *Entry, ttl time.Duration) error
	Get(ctx context.Context, k Key) (*Entry, error)
}

AddGetDeleter defines the interface for getting, adding and deleting entries from the cache.

func FromContext

func FromContext(ctx context.Context) (AddGetDeleter, bool)

FromContext returns the cache value stored in ctx, if any.

type ChangeSet

type ChangeSet struct {
	// contains filtered or unexported fields
}

ChangeSet tracks entity keys that have been modified (created, updated, or deleted). It is used by the Driver to detect stale cache entries and force re-queries. A background GC goroutine prunes entries older than the GC interval.

func NewChangeSet

func NewChangeSet(gcInterval time.Duration) *ChangeSet

NewChangeSet creates a new ChangeSet with the given GC interval. If gcInterval is <= 0, the default of 5 minutes is used.

func (*ChangeSet) Changed

func (cs *ChangeSet) Changed(key Key, since time.Time) bool

Changed reports whether the given key has been marked as changed since the given time. This is used by the Driver to decide whether a cache hit should be evicted and re-fetched.

func (*ChangeSet) Clear

func (cs *ChangeSet) Clear(keys ...Key)

Clear removes the change markers for the given keys, acknowledging that the cache has been refreshed.

func (*ChangeSet) Mark

func (cs *ChangeSet) Mark(keys ...Key)

Mark records one or more keys as changed at the current time.

func (*ChangeSet) Start

func (cs *ChangeSet) Start()

Start begins the background GC goroutine that prunes stale change markers. Call Stop to terminate it.

func (*ChangeSet) Stop

func (cs *ChangeSet) Stop()

Stop terminates the background GC goroutine.

type Driver

type Driver struct {
	dialect.Driver
	*Options
	// contains filtered or unexported fields
}

A Driver is an SQL cached client. Users should use the constructor below for creating new driver.

func NewDriver

func NewDriver(drv dialect.Driver, opts ...Option) *Driver

NewDriver returns a new Driver an existing driver and optional configuration functions. For example:

entcache.NewDriver(
	drv,
	entcache.TTL(time.Minute),
	entcache.Levels(
		NewLRU(256),
		NewRedis(redis.NewClient(&redis.Options{
			Addr: ":6379",
		})),
	),
)

func (*Driver) ExecContext

func (d *Driver) ExecContext(ctx context.Context, query string, args ...any) (stdsql.Result, error)

ExecContext calls ExecContext of the underlying driver, or fails if it is not supported.

func (*Driver) Query

func (d *Driver) Query(ctx context.Context, query string, args, v any) error

Query implements the Querier interface for the driver. It falls back to the underlying wrapped driver in case of caching error.

Stampede protection: concurrent identical queries are deduplicated via singleflight. Only the first caller hits the database; others receive the same result.

func (*Driver) QueryContext

func (d *Driver) QueryContext(ctx context.Context, query string, args ...any) (*stdsql.Rows, error)

QueryContext calls QueryContext of the underlying driver, or fails if it is not supported. Note, this method is not part of the caching layer since Ent does not use it by default.

func (*Driver) Stats

func (d *Driver) Stats() Stats

Stats returns a copy of the cache statistics.

type Entry

type Entry struct {
	Columns []string
	Values  [][]driver.Value
}

Entry defines an entry to store in a cache.

func (Entry) MarshalBinary

func (e Entry) MarshalBinary() ([]byte, error)

MarshalBinary implements the encoding.BinaryMarshaler interface.

func (*Entry) UnmarshalBinary

func (e *Entry) UnmarshalBinary(buf []byte) error

UnmarshalBinary implements the encoding.BinaryUnmarshaler interface.

type Key

type Key any

A Key defines a comparable Go value. See http://golang.org/ref/spec#Comparison_operators

func DefaultHash

func DefaultHash(query string, args []any) (Key, error)

DefaultHash provides the default implementation for converting a query and its argument to a cache key.

func NewEntryKey

func NewEntryKey(typ string, id any) Key

NewEntryKey constructs a structured cache key from an entity type name and ID. This produces keys like "User:42" that enable precise invalidation via ChangeSet.

type LRU

type LRU struct {
	*lru.Cache
	// contains filtered or unexported fields
}

LRU provides an LRU cache that implements the AddGetDeleter interface.

func NewLRU

func NewLRU(maxEntries int) *LRU

NewLRU creates a new Cache. If maxEntries is zero, the cache has no limit.

func (*LRU) Add

func (l *LRU) Add(_ context.Context, k Key, e *Entry, ttl time.Duration) error

Add adds the entry to the cache.

func (*LRU) Del

func (l *LRU) Del(_ context.Context, k Key) error

Del deletes an entry from the cache.

func (*LRU) Get

func (l *LRU) Get(_ context.Context, k Key) (*Entry, error)

Get gets an entry from the cache.

type NatsKV

type NatsKV struct {
	// contains filtered or unexported fields
}

NatsKV provides a remote cache backed by NATS JetStream KeyValue and implements the AddGetDeleter interface.

NATS JetStream KV supports:

  • Create as a SETNX equivalent (only sets if key doesn't exist), with optional per-key TTL via jetstream.KeyTTL.
  • Watch for invalidation notifications.
  • Bucket-level TTL (MaxAge) for automatic expiry of all keys.

For per-key TTL, the implementation uses Create (which accepts KVCreateOpt) when TTL > 0. Put is used as a fallback when no TTL is needed since it does not accept TTL options.

func NewNatsKV

func NewNatsKV(kv jetstream.KeyValue) *NatsKV

NewNatsKV returns a new NATS JetStream KeyValue cache level. The bucket should be created/configured externally. If you need automatic expiry, set MaxAge on the KeyValueConfig when creating the bucket.

js, _ := jetstream.New(nc)
kv, _ := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{
	Bucket: "entcache",
	MaxAge: 10 * time.Minute, // bucket-level TTL
})
entcache.NewNatsKV(kv)

func (*NatsKV) Add

func (n *NatsKV) Add(ctx context.Context, k Key, e *Entry, ttl time.Duration) error

Add adds the entry to the cache using Put (unconditional overwrite). NATS KV Put does not support per-key TTL — expiry is governed by the bucket's MaxAge configuration. The ttl parameter is used with a Delete-then-Create approach when ttl > 0 to leverage Create's KeyTTL option for per-key expiry.

func (*NatsKV) Create

func (n *NatsKV) Create(ctx context.Context, k Key, e *Entry, ttl time.Duration) error

Create adds the entry to the cache only if the key does not already exist. This is the SETNX (set-if-not-exists) equivalent for NATS KV, useful for stampede protection: only the first caller that wins the Create will populate the cache, others will get an error and should wait or re-check.

func (*NatsKV) Del

func (n *NatsKV) Del(ctx context.Context, k Key) error

Del deletes an entry from the cache.

func (*NatsKV) Get

func (n *NatsKV) Get(ctx context.Context, k Key) (*Entry, error)

Get gets an entry from the cache.

func (*NatsKV) Watch

func (n *NatsKV) Watch(ctx context.Context, pattern string, opts ...jetstream.WatchOpt) (jetstream.KeyWatcher, error)

Watch returns a watcher for changes on keys matching the given pattern. This is the invalidation-notification equivalent: callers can watch for key updates and deletions to trigger cache invalidation in local caches when used in a multi-level setup.

The returned KeyWatcher should be stopped by the caller when no longer needed.

type Option

type Option func(*Options)

Option allows configuring the cache driver using functional options.

func ContextLevel

func ContextLevel() Option

ContextLevel configures the driver to work with context/request level cache. Users that use this option, should wraps the *http.Request context with the cache value as follows:

ctx = entcache.NewContext(ctx)

ctx = entcache.NewContext(ctx, entcache.NewLRU(128))

func Hash

func Hash(hash func(query string, args []any) (Key, error)) Option

Hash configures an optional Hash function for converting a query and its arguments to a cache key.

func Levels

func Levels(levels ...AddGetDeleter) Option

Levels configures the Driver to work with the given cache levels. For example, in process LRU cache and a remote Redis cache.

func TTL

func TTL(ttl time.Duration) Option

TTL configures the period of time that an Entry is valid in the cache.

func WithChangeSet

func WithChangeSet(cs *ChangeSet) Option

WithChangeSet configures the Driver to use the given ChangeSet for mutation-aware cache invalidation.

func WithKeyTTL

func WithKeyTTL(ttl time.Duration) Option

WithKeyTTL configures a separate TTL for key-addressed queries (e.g. Get-by-ID). Key-addressed queries can have a longer TTL because they are precisely invalidated via the ChangeSet. If not set, the regular TTL is used.

type Options

type Options struct {
	// TTL defines the period of time that an Entry
	// is valid in the cache (used for hash-addressed queries).
	TTL time.Duration

	// KeyTTL defines the period of time that a key-addressed Entry
	// (e.g. Get-by-ID queries) is valid in the cache. Key-addressed
	// queries can have a longer TTL because they are precisely
	// invalidated via the ChangeSet. If zero, TTL is used.
	KeyTTL time.Duration

	// Cache defines the GetAddDeleter (cache implementation)
	// for holding the cache entries. If no cache implementation
	// was provided, an LRU cache with no limit is used.
	Cache AddGetDeleter

	// Hash defines an optional Hash function for converting
	// a query and its arguments to a cache key. If no Hash
	// function was provided, the DefaultHash is used.
	Hash func(query string, args []any) (Key, error)

	// ChangeSet holds the mutation change tracker. When set,
	// the Driver checks whether cached entries have been
	// invalidated by mutations before returning them.
	ChangeSet *ChangeSet

	// Logf function. If provided, the Driver will call it with
	// errors that can not be handled.
	Log func(...any)
}

Options wraps the basic configuration cache options.

type Redis

type Redis struct {
	// contains filtered or unexported fields
}

Redis provides a remote cache backed by go-redis and implements the AddGetDeleter interface.

func NewRedis

func NewRedis(c redis.Cmdable) *Redis

NewRedis returns a new Redis cache level from the given Redis connection.

entcache.NewRedis(redis.NewClient(&redis.Options{
	Addr: ":6379"
}))

entcache.NewRedis(redis.NewClusterClient(&redis.ClusterOptions{
	Addrs: []string{":7000", ":7001", ":7002"},
}))

func (*Redis) Add

func (r *Redis) Add(ctx context.Context, k Key, e *Entry, ttl time.Duration) error

Add adds the entry to the cache.

func (*Redis) Del

func (r *Redis) Del(ctx context.Context, k Key) error

Del deletes an entry from the cache.

func (*Redis) Get

func (r *Redis) Get(ctx context.Context, k Key) (*Entry, error)

Get gets an entry from the cache.

type Rueidis

type Rueidis struct {
	// contains filtered or unexported fields
}

Rueidis provides a remote cache backed by rueidis and implements the AddGetDeleter interface with stampede protection.

Stampede protection (inspired by rueidisaside): when a cache miss occurs, the first caller for a given key proceeds to fetch from the database while concurrent callers for the same key block on a channel until the first caller populates the cache. This prevents multiple identical database queries from being executed simultaneously.

func NewRueidis

func NewRueidis(c rueidis.Client) *Rueidis

NewRueidis returns a new Rueidis cache level from the given rueidis client.

c, _ := rueidis.NewClient(rueidis.ClientOption{
	InitAddress: []string{"127.0.0.1:6379"},
})
entcache.NewRueidis(c)

func (*Rueidis) Add

func (r *Rueidis) Add(ctx context.Context, k Key, e *Entry, ttl time.Duration) error

Add adds the entry to the cache. If a stampede wait channel exists for this key, it is closed to unblock any goroutines waiting on Get.

func (*Rueidis) Del

func (r *Rueidis) Del(ctx context.Context, k Key) error

Del deletes an entry from the cache.

func (*Rueidis) Get

func (r *Rueidis) Get(ctx context.Context, k Key) (*Entry, error)

Get gets an entry from the cache. If the key is not found, it returns ErrNotFound. Callers can use the stampede protection via the Driver's singleflight integration — this method itself is non-blocking.

func (*Rueidis) Register

func (r *Rueidis) Register(key string) (wait <-chan struct{}, first bool)

Register registers interest in a key for stampede protection. Returns a channel that will be closed when the key is populated via Add, and a boolean indicating whether this caller is the first (i.e. should fetch). If first is true, the caller is responsible for calling Add or Unregister.

func (*Rueidis) Unregister

func (r *Rueidis) Unregister(key string)

Unregister removes a stampede wait channel without populating the cache. Use this when the first caller encounters an error fetching from the database.

type Stats

type Stats struct {
	Gets   uint64
	Hits   uint64
	Errors uint64
}

Stats represents the cache statistics of the driver.

Jump to

Keyboard shortcuts

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