Documentation
¶
Overview ¶
Package redcache is a typed cache-aside for Redis, built on the rueidis client. It combines rueidis client-side caching with distributed SET NX locking so that, across every process, only one caller populates a missing key while the rest wait on the invalidation push for the populated value — a stampede-resistant cache behind a single generic Cache[K, V] interface.
How it works ¶
A read registers an in-process lock entry, then issues a client-side-cached GET. That GET serves a dual purpose: it returns any cached value and subscribes the connection to Redis invalidation for the key. On a miss, one caller wins a distributed lock (SET NX with a UUIDv7 value), runs the loader, and atomically replaces the lock with the real value via a Lua CAS. Every other caller — in this process or another — waits for the resulting invalidation, with jittered polling as a fallback until the lock TTL, and then reads the populated value. In-process leader/follower coordination collapses a thundering herd on one key to a single Redis SET NX.
Choosing a constructor ¶
Open one Conn (it owns the rueidis client and invalidation stream), then derive typed views over it that all share that single client:
- NewString — Cache[string, V]: string keys, typed values. The common case.
- NewBytes — Cache[string, []byte]: zero-copy opaque payloads.
- New — Cache[K, V]: typed keys via a KeyCodec (and typed values).
The views are operations-only; lifecycle (Close) and the raw-client escape hatch (Client) live on the Conn. Deriving a view does no I/O, returns no error, and panics on a nil codec.
Minimal example ¶
conn, err := redcache.Open(
rueidis.ClientOption{InitAddress: []string{"127.0.0.1:6379"}},
)
if err != nil {
return err
}
defer conn.Close()
cache := redcache.NewString[string](conn, redcache.StringCodec{})
v, err := cache.Get(ctx, time.Minute, "k", func(ctx context.Context, key string) (string, error) {
return loadFromUpstream(ctx, key) // runs only on a miss, once per key
})
Beyond read-through ¶
Every Cache also supports write-through priming — Set, ForceSet, and Touch (with Multi variants) — for populating or extending entries without a read-through miss. Opt into background refresh of stale-but-valid entries with WithRefreshAfterFraction (and WithRefreshBeta for XFetch-style probabilistic early expiration). Wire counters through WithMetrics.
Values, codecs, and absence ¶
Values are stored through a Codec; keys through a KeyCodec. JSONCodec is the typical choice; StringCodec and UnsafeBytesCodec are identity codecs. The string path is safe to retain because strings are immutable; the bytes path is zero-copy and returns borrowed memory. Decode failures on read are wrapped with ErrDecode. Absence semantics are the caller's to own — there is no ErrNotFound sentinel; cache a *T, sql.Null[T], or a domain sentinel inside V to represent "not found" and avoid cache penetration.
Index ¶
- Constants
- Variables
- type BatchKeyError
- type Cache
- type Codec
- type Conn
- type JSONCodec
- type KeyCodec
- type KeyCodecFunc
- type Logger
- type Metrics
- type NoopMetrics
- func (NoopMetrics) CacheHits(int64)
- func (NoopMetrics) CacheMisses(int64)
- func (NoopMetrics) InvalidationError()
- func (NoopMetrics) LoaderDuration(time.Duration)
- func (NoopMetrics) LoaderErrors(int64)
- func (NoopMetrics) LockContended(int64)
- func (NoopMetrics) LockLost(string)
- func (NoopMetrics) LockWaitDuration(time.Duration)
- func (NoopMetrics) RedisError(string)
- func (NoopMetrics) RefreshDropped(int64)
- func (NoopMetrics) RefreshError(string)
- func (NoopMetrics) RefreshPanicked(string)
- func (NoopMetrics) RefreshSkipped(int64)
- func (NoopMetrics) RefreshTriggered(int64)
- type Option
- func WithClientBuilder(b func(option rueidis.ClientOption) (rueidis.Client, error)) Option
- func WithLockPrefix(p string) Option
- func WithLockTTL(d time.Duration) Option
- func WithLogger(l Logger) Option
- func WithMetrics(m Metrics) Option
- func WithRefreshAfterFraction(f float64) Option
- func WithRefreshBeta(b float64) Option
- func WithRefreshLockPrefix(p string) Option
- func WithRefreshQueueSize(n int) Option
- func WithRefreshTimeout(d time.Duration) Option
- func WithRefreshWorkers(n int) Option
- type StringCodec
- type StringKeyCodec
- type UnsafeBytesCodec
Examples ¶
Constants ¶
const ( // DefaultLockPrefix tags in-Redis lock values so reads recognise a lock as // a miss. DefaultLockPrefix = "__redcache:lock:" // DefaultRefreshPrefix prefixes refresh-ahead dedup lock keys. DefaultRefreshPrefix = "__redcache:refresh:" )
Variables ¶
var ErrDecode = errors.New("decode failed")
ErrDecode is returned (wrapped) from typed reads when a stored value cannot be decoded. The library does not auto-evict — the caller decides whether to log, Del, or retry.
var ErrInvalidTTL = errors.New("redcache: ttl must be positive; use Del to remove")
ErrInvalidTTL is returned by TTL-bearing methods when ttl is not positive. Redis rejects PX 0; use Del to remove an entry.
var ErrLockLost = errors.New("lock was lost or expired before value could be set")
ErrLockLost is returned when the distributed lock was stolen or expired before the value could be written.
Functions ¶
This section is empty.
Types ¶
type BatchKeyError ¶ added in v0.3.0
type BatchKeyError[K comparable] struct { Failed map[K]error Succeeded []K }
BatchKeyError is the typed, key-preserving error returned via errors.As from the multi-key write methods (SetMulti, ForceSetMulti) on partial failure. K is the cache's key type. All accessors are nil-safe.
func (*BatchKeyError[K]) Error ¶ added in v0.3.0
func (e *BatchKeyError[K]) Error() string
Error formats the batch outcome with failed keys sorted by their %v rendering for stable output.
func (*BatchKeyError[K]) ErrorFor ¶ added in v0.3.0
func (e *BatchKeyError[K]) ErrorFor(k K) error
ErrorFor returns the error for k, or nil.
func (*BatchKeyError[K]) HasError ¶ added in v0.3.0
func (e *BatchKeyError[K]) HasError(k K) bool
HasError reports whether k failed.
func (*BatchKeyError[K]) HasFailures ¶ added in v0.3.0
func (e *BatchKeyError[K]) HasFailures() bool
HasFailures reports whether any key failed.
type Cache ¶ added in v0.3.0
type Cache[K comparable, V any] interface { // Get returns the cached value for k, calling fn on a miss. Only one caller // across all processes runs fn for a given key; the rest wait on the // resulting invalidation. Decode errors on read are wrapped with ErrDecode. Get(ctx context.Context, ttl time.Duration, k K, fn func(context.Context, K) (V, error)) (V, error) // GetMulti returns cached values for keys, calling fn for misses. SETs are // grouped by Redis cluster slot. A decode error aborts the batch (wrapped // with ErrDecode). GetMulti(ctx context.Context, ttl time.Duration, keys []K, fn func(context.Context, []K) (map[K]V, error)) (map[K]V, error) // Peek is a read-only, client-side-cached lookup with no loader and no lock. // It returns (value, true, nil) on a cached hit, (zero, false, nil) on a miss // or when the key currently holds a lock value, and (zero, false, err) on a // real Redis or decode error. ttl is the client-side-cache subscription TTL, // like Get. Peek(ctx context.Context, ttl time.Duration, k K) (V, bool, error) // Set populates k via fn under a write lock, writing the value to every // subscribed client. On callback error the prior value is restored. Set(ctx context.Context, ttl time.Duration, k K, fn func(context.Context, K) (V, error)) error // SetMulti populates keys via fn under write locks. Partial failures surface // as *BatchKeyError[K] via errors.As. SetMulti(ctx context.Context, ttl time.Duration, keys []K, fn func(context.Context, []K) (map[K]V, error)) error // ForceSet writes v unconditionally, bypassing locks. In-progress Get // callers on the same key retry transparently and observe the force-set // value; in-progress Set callers receive ErrLockLost and their pending set // is abandoned (not retried). ForceSet(ctx context.Context, ttl time.Duration, k K, v V) error // ForceSetMulti writes values unconditionally. Encode failures are collected // per-key; successfully-encoded entries are still written. Partial failures // surface as *BatchKeyError[K]. ForceSetMulti(ctx context.Context, ttl time.Duration, values map[K]V) error // Del removes a key, triggering invalidation on all subscribed clients. Del(ctx context.Context, k K) error // DelMulti removes keys, triggering invalidation. DelMulti(ctx context.Context, keys []K) error // Touch sets the TTL of a cached value. No-ops on a missing key or lock // value. Clients caching the key are invalidated and re-fetch it (with the // new TTL) on their next read. Touch(ctx context.Context, ttl time.Duration, k K) error // TouchMulti extends the TTL of cached values, with the same invalidation // behavior as Touch. TouchMulti(ctx context.Context, ttl time.Duration, keys []K) error }
Cache is the typed cache-aside surface: a generic interface over a key type K and value type V, and a pure operational handle. Read methods run the stampede-protected lock loop; write methods populate every subscribed client's cache. It carries no lifecycle or raw-client access — those live on the owning Conn (see Open/New) — so a Cache is safe to inject into code that should not be able to close the shared connection, and trivial to fake in tests.
func New ¶ added in v0.3.0
New derives a typed Cache[K, V] view over c with its own key/value codecs. The view shares c's client and invalidation stream and is a pure operational handle — lifecycle (Close) and the raw-client escape hatch live on the Conn, not on the view, so a view is safe to hand to code that should not be able to tear the connection down. Deriving a view does no I/O and cannot fail; keyCodec and valCodec must be non-nil (passing nil panics — a programmer error).
Example ¶
A Conn owns one Redis client and invalidation stream; New/NewString derive typed views over it — one client backing multiple value types.
package main
import (
"context"
"fmt"
"time"
"github.com/redis/rueidis"
"github.com/dcbickfo/redcache"
)
func main() {
conn, err := redcache.Open(
rueidis.ClientOption{InitAddress: []string{"127.0.0.1:6379"}},
redcache.WithLockTTL(5*time.Second),
)
if err != nil {
panic(err)
}
defer conn.Close() // closing the Conn closes the shared client (and all views)
// users and loginCounts share one client, connection, and invalidation stream.
users := redcache.NewString[string](conn, redcache.StringCodec{})
loginCounts := redcache.New[string, int](conn, redcache.StringKeyCodec{}, redcache.JSONCodec[int]{})
_ = users
n, err := loginCounts.Get(context.Background(), time.Minute, "u-123",
func(ctx context.Context, key string) (int, error) {
return 7, nil
},
)
if err != nil {
panic(err)
}
fmt.Println(n)
}
Output:
Example (TypedKeys) ¶
New keys the cache by a domain type via a KeyCodec. KeyCodecFunc adapts a plain function into a KeyCodec.
package main
import (
"context"
"fmt"
"time"
"github.com/redis/rueidis"
"github.com/dcbickfo/redcache"
)
func main() {
type UserID int64
type User struct {
ID UserID
Name string
}
userIDCodec := redcache.KeyCodecFunc[UserID](func(id UserID) (string, error) {
return fmt.Sprintf("user:%d", id), nil
})
conn, err := redcache.Open(
rueidis.ClientOption{InitAddress: []string{"127.0.0.1:6379"}},
redcache.WithLockTTL(5*time.Second),
)
if err != nil {
panic(err)
}
defer conn.Close()
cache := redcache.New[UserID, User](conn, userIDCodec, redcache.JSONCodec[User]{})
u, err := cache.Get(context.Background(), time.Minute, UserID(123),
func(ctx context.Context, id UserID) (User, error) {
// Load from your data source; runs only on a miss.
return User{ID: id, Name: "alice"}, nil
},
)
if err != nil {
panic(err)
}
fmt.Println(u.Name)
}
Output:
func NewBytes ¶ added in v0.3.0
NewBytes is NewString with UnsafeBytesCodec — a zero-copy raw []byte view. The decoded slice aliases borrowed memory; do not mutate or retain it.
func NewString ¶ added in v0.3.0
NewString is New with StringKeyCodec preset (enabling the K=string fast path).
Example ¶
package main
import (
"context"
"fmt"
"time"
"github.com/redis/rueidis"
"github.com/dcbickfo/redcache"
)
func main() {
conn, err := redcache.Open(
rueidis.ClientOption{
InitAddress: []string{"127.0.0.1:6379"},
},
redcache.WithLockTTL(5*time.Second),
)
if err != nil {
panic(err)
}
defer conn.Close()
client := redcache.NewString[string](conn, redcache.StringCodec{})
val, err := client.Get(context.Background(), time.Minute, "example:get", func(ctx context.Context, key string) (string, error) {
// Called only on cache miss — fetch from your data source.
return "hello", nil
})
if err != nil {
panic(err)
}
fmt.Println(val)
// This example dials Redis, so it omits an Output: directive (which would
// make `go test` run it). It is compiled to keep the snippet honest.
}
Output:
Example (GetMulti) ¶
package main
import (
"context"
"fmt"
"time"
"github.com/redis/rueidis"
"github.com/dcbickfo/redcache"
)
func main() {
conn, err := redcache.Open(
rueidis.ClientOption{
InitAddress: []string{"127.0.0.1:6379"},
},
redcache.WithLockTTL(5*time.Second),
)
if err != nil {
panic(err)
}
defer conn.Close()
client := redcache.NewString[string](conn, redcache.StringCodec{})
keys := []string{"example:multi:a", "example:multi:b"}
vals, err := client.GetMulti(context.Background(), time.Minute, keys, func(ctx context.Context, keys []string) (map[string]string, error) {
// Called only for keys not in cache — fetch from your data source.
result := make(map[string]string, len(keys))
for _, k := range keys {
result[k] = "value-for-" + k
}
return result, nil
})
if err != nil {
panic(err)
}
fmt.Println(len(vals))
// Redis-dependent; omits Output: so `go test` does not run it.
}
Output:
type Codec ¶ added in v0.3.0
Codec encodes and decodes V into the envelope payload. Implementations must be concurrent-safe.
Ownership of return values:
- Encode's returned slice is handed to the library and must not be mutated by the caller afterward. The library may alias it without copying, so the codec must not retain or later modify it either.
- Decode's input slice is borrowed from library-internal memory and is only valid for the duration of the call; it must not be retained.
Identity codecs (UnsafeBytesCodec) alias this borrowed memory directly and so trade safety for zero copies — the decoded []byte must not outlive the call or be mutated. JSONCodec returns owned decoded values. StringCodec returns immutable strings that are safe to retain, and cache fast paths may avoid extra byte copies for string-valued views.
type Conn ¶ added in v0.3.0
type Conn struct {
// contains filtered or unexported fields
}
Conn owns one rueidis client, its invalidation stream, and a lock namespace. Derive typed cache views over it with New, NewString, or NewBytes — they all share the single client and invalidation subscription. Lifecycle stays on the Conn: open it once, derive all the views you need, and close the Conn when done with all of them.
func Open ¶ added in v0.3.0
func Open(clientOption rueidis.ClientOption, opts ...Option) (*Conn, error)
Open builds a Conn with its own rueidis.Client (wired for invalidation). Derive typed views with New/NewString/NewBytes.
type JSONCodec ¶ added in v0.3.0
type JSONCodec[V any] struct{}
JSONCodec encodes V via encoding/json.
type KeyCodec ¶ added in v0.3.0
KeyCodec encodes a typed key K into the Redis key string. Must be deterministic, concurrent-safe, produce a non-empty key, and encode distinct logical keys to distinct Redis keys.
type KeyCodecFunc ¶ added in v0.3.0
KeyCodecFunc adapts a func to KeyCodec[K].
func (KeyCodecFunc[K]) EncodeKey ¶ added in v0.3.0
func (f KeyCodecFunc[K]) EncodeKey(k K) (string, error)
EncodeKey calls f.
type Logger ¶ added in v0.1.6
Logger is the slog-shaped subset the cache calls into. *slog.Logger satisfies it.
type Metrics ¶ added in v0.1.7
type Metrics interface {
CacheHits(n int64)
CacheMisses(n int64)
LockContended(n int64)
// LockWaitDuration fires once per resolved wait, regardless of outcome
// (invalidation, ctx cancel, or lockTTL). Typically histogrammed.
LockWaitDuration(d time.Duration)
// LoaderDuration measures foreground origin-loader latency (Get/GetMulti
// miss, Set/SetMulti). Background refresh is excluded. Typically histogrammed.
LoaderDuration(d time.Duration)
// LoaderErrors fires when a foreground loader returns an error; n is the
// number of keys the loader was responsible for (1 for single, len(missing)
// for multi).
LoaderErrors(n int64)
// RedisError fires when a Redis command fails. op is one of "read", "lock",
// "set", "del", "touch". Lock-lost, redis-nil, and script-drift parse
// failures are not Redis errors and do not fire it.
RedisError(op string)
RefreshTriggered(n int64)
// RefreshSkipped covers both local and distributed dedup.
RefreshSkipped(n int64)
// RefreshDropped fires when the refresh queue is full.
RefreshDropped(n int64)
// LockLost fires when a CAS detected a stolen lock (e.g. via ForceSet).
LockLost(key string)
RefreshError(key string)
// RefreshPanicked fires once per key when a refresh callback panicked.
RefreshPanicked(key string)
// InvalidationError fires when an invalidation message couldn't be parsed.
InvalidationError()
}
Metrics receives observability events. Methods run on the hot path and must be concurrent-safe. High-volume events (hits/misses/contention/refresh counts) are batched per operation and emitted once with n. Diagnostics (LockLost, RefreshError, RefreshPanicked) carry the affected key. Embed NoopMetrics to opt in to a subset.
type NoopMetrics ¶ added in v0.1.7
type NoopMetrics struct{}
NoopMetrics is the zero-value Metrics implementation. Embed and override.
Example ¶
Pass a Metrics implementation via WithMetrics to count cache events. The cache calls these on the hot path, batched per operation. This example invokes them directly (no Redis needed) to show the counting shape.
package main
import (
"fmt"
"sync/atomic"
"github.com/dcbickfo/redcache"
)
// countingMetrics shows the embed-and-override pattern: embed NoopMetrics and
// implement only the events you care about.
type countingMetrics struct {
redcache.NoopMetrics
hits, misses atomic.Int64
}
func (m *countingMetrics) CacheHits(n int64) { m.hits.Add(n) }
func (m *countingMetrics) CacheMisses(n int64) { m.misses.Add(n) }
func main() {
m := &countingMetrics{}
// In real use: open a Conn and derive redcache.NewString[string](conn, codec) with redcache.WithMetrics(m) passed to Open.
m.CacheHits(3)
m.CacheMisses(1)
fmt.Println(m.hits.Load(), m.misses.Load())
}
Output: 3 1
func (NoopMetrics) CacheHits ¶ added in v0.2.0
func (NoopMetrics) CacheHits(int64)
func (NoopMetrics) CacheMisses ¶ added in v0.2.0
func (NoopMetrics) CacheMisses(int64)
func (NoopMetrics) InvalidationError ¶ added in v0.1.7
func (NoopMetrics) InvalidationError()
func (NoopMetrics) LoaderDuration ¶ added in v0.3.0
func (NoopMetrics) LoaderDuration(time.Duration)
func (NoopMetrics) LoaderErrors ¶ added in v0.3.0
func (NoopMetrics) LoaderErrors(int64)
func (NoopMetrics) LockContended ¶ added in v0.1.7
func (NoopMetrics) LockContended(int64)
func (NoopMetrics) LockLost ¶ added in v0.1.7
func (NoopMetrics) LockLost(string)
func (NoopMetrics) LockWaitDuration ¶ added in v0.2.0
func (NoopMetrics) LockWaitDuration(time.Duration)
func (NoopMetrics) RedisError ¶ added in v0.3.0
func (NoopMetrics) RedisError(string)
func (NoopMetrics) RefreshDropped ¶ added in v0.1.7
func (NoopMetrics) RefreshDropped(int64)
func (NoopMetrics) RefreshError ¶ added in v0.1.7
func (NoopMetrics) RefreshError(string)
func (NoopMetrics) RefreshPanicked ¶ added in v0.1.7
func (NoopMetrics) RefreshPanicked(string)
func (NoopMetrics) RefreshSkipped ¶ added in v0.1.7
func (NoopMetrics) RefreshSkipped(int64)
func (NoopMetrics) RefreshTriggered ¶ added in v0.1.7
func (NoopMetrics) RefreshTriggered(int64)
type Option ¶ added in v0.3.0
type Option func(*config)
Option configures a cache. Options are applied in order; later wins.
func WithClientBuilder ¶ added in v0.3.0
WithClientBuilder overrides rueidis.NewClient when building the internal client. Useful as a test seam.
func WithLockPrefix ¶ added in v0.3.0
WithLockPrefix sets the in-Redis prefix tagged onto lock values so reads can recognise a lock as a miss. Defaults to DefaultLockPrefix.
func WithLockTTL ¶ added in v0.3.0
WithLockTTL bounds both how long a Redis lock survives and how long callers wait for one. Defaults to 10s; values below 100ms are rejected.
func WithLogger ¶ added in v0.3.0
WithLogger sets the logger. Defaults to slog.Default().
func WithMetrics ¶ added in v0.3.0
WithMetrics sets the metrics sink. Defaults to NoopMetrics. Methods run on the hot path; impls must be concurrent-safe.
func WithRefreshAfterFraction ¶ added in v0.3.0
WithRefreshAfterFraction enables refresh-ahead. Reads with remaining TTL below (1 - fraction) * ttl may trigger a background refresh while still returning the cached value. Must be in [0, 1); 0 disables.
func WithRefreshBeta ¶ added in v0.3.0
WithRefreshBeta enables XFetch-style probabilistic sampling within the refresh window, weighting by recorded compute time so slow values get more headroom. 0 (default) = always refresh below the floor; 1.0 matches canonical XFetch (Vattani et al). Multi-key writes record fn duration divided evenly across returned values.
func WithRefreshLockPrefix ¶ added in v0.3.0
WithRefreshLockPrefix sets the prefix for refresh-ahead dedup keys. Defaults to DefaultRefreshPrefix. Refresh lock keys embed the data key for uniqueness; refresh-ahead correctness does not require Redis cluster-slot co-location.
func WithRefreshQueueSize ¶ added in v0.3.0
WithRefreshQueueSize bounds pending refresh jobs; over-full drops are counted via RefreshDropped. Defaults to 64.
func WithRefreshTimeout ¶ added in v0.3.0
WithRefreshTimeout bounds how long a refresh-ahead callback may run. Defaults to the data ttl passed to Get/GetMulti (not LockTTL). The refresh lock itself still uses LockTTL; the back-write that records a slow-but-successful result is decoupled from this timeout so it is not lost.
func WithRefreshWorkers ¶ added in v0.3.0
WithRefreshWorkers sets the refresh worker pool size. Defaults to 4.
type StringCodec ¶ added in v0.3.0
type StringCodec struct{}
StringCodec is the identity codec for string.
type StringKeyCodec ¶ added in v0.3.0
type StringKeyCodec struct{}
StringKeyCodec is the identity KeyCodec for string keys.
type UnsafeBytesCodec ¶ added in v0.3.0
type UnsafeBytesCodec struct{}
UnsafeBytesCodec is the zero-copy identity codec for []byte. It aliases library-internal memory in both directions: Encode hands its input straight to the library (which may alias it), and Decode returns a []byte backed by the cache's borrowed read buffer. The decoded slice must not be mutated or retained past the call. Use a copying codec if you need an owned value.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
examples
|
|
|
metrics
command
Package main demonstrates wiring a custom Metrics implementation.
|
Package main demonstrates wiring a custom Metrics implementation. |
|
string-cache
command
Package main demonstrates a migration-friendly string-key/string-value cache.
|
Package main demonstrates a migration-friendly string-key/string-value cache. |
|
typed-cache
command
Package main demonstrates typed keys and JSON-encoded values.
|
Package main demonstrates typed keys and JSON-encoded values. |
|
internal
|
|
|
cmdx
Package cmdx provides Redis cluster slot calculation utilities.
|
Package cmdx provides Redis cluster slot calculation utilities. |
|
lockpool
Package lockpool provides fast lock value generation using an atomic counter and a per-instance UUID prefix.
|
Package lockpool provides fast lock value generation using an atomic counter and a per-instance UUID prefix. |
|
poolx
Package poolx provides typed sync.Pool wrappers for reusing slice headers across multi-key call paths.
|
Package poolx provides typed sync.Pool wrappers for reusing slice headers across multi-key call paths. |
|
syncx
Package syncx provides generic typed wrappers around standard library sync primitives.
|
Package syncx provides generic typed wrappers around standard library sync primitives. |
|
Package redcacheotel provides a drop-in OpenTelemetry adapter for the redcache.Metrics interface.
|
Package redcacheotel provides a drop-in OpenTelemetry adapter for the redcache.Metrics interface. |
|
Package redcachetest provides an in-memory test double for redcache.Cache.
|
Package redcachetest provides an in-memory test double for redcache.Cache. |