cascache

package module
v3.1.3 Latest Latest
Warning

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

Go to latest
Published: Apr 1, 2026 License: Apache-2.0 Imports: 12 Imported by: 0

README

CasCache

TL;DR

If you run multiple pods, replicas, or services that share a cache and you need to make sure a cached key always reflects the latest known state and not something a slow writer quietly overwrote three seconds ago - CasCache is built for that. It will not serve stale data and it will not let a late write silently win or override. When it cannot prove a value is current, it treats the entry as a miss instead of serving something outdated.


Most caches treat writes as unconditional: you SET a value and it sticks until someone deletes it or it expires. That works fine until two requests overlap.

  1. request A reads a user record from the database
  2. while A is still working, request B updates that same record and invalidates the cache
  3. request A finishes and writes the now-outdated record back into the cache

The cache now holds stale data and nobody knows. A DEL followed by a SET does not prevent this because nothing ties the SET back to the state that existed when the read started.

CasCache fixes this by remembering what version of a key you saw before you started your work. When you try to write, it checks whether that version is still current. If something changed in between, the write is rejected. On reads, it checks again - a cached value is only served if it still matches the latest known version. If anything is off, the cache treats it as a miss.

Index

Performance

CasCache does more work than a plain cache. Here is what that costs, measured against plain Redis with the same codec and payload.

Single-key Redis operations
Operation Plain Redis CasCache Extra cost Redis round trips
Single read (Get) ~17µs ~33µs +16µs 2 (value + fence check)
Snapshot then set (full fill) ~16µs ~36µs +20µs 3 (miss + snapshot + Lua conditional set)
Single write (SetIfVersion) ~16µs ~23µs +7µs 1 (Lua script, atomic)
Invalidate ~15µs ~16µs +1µs 1 (Lua script, atomic)

Single-key reads are the most expensive path because every Get needs two round trips: one for the value and one for the authoritative fence. Writes and invalidates use Lua scripts to stay atomic in a single round trip. The extra cost per operation is in the low tens of microseconds.

Batch operations
Operation Extra cost vs plain Redis Redis round trips
Batch get (32 small keys) ~4% 2 (batch blob + MGET all fences)
Batch get (32 medium keys) ~1% 2 (batch blob + MGET all fences)

Batch reads amortize the fence-check cost across all keys with a single MGET, so the overhead nearly disappears.

50,000 req/s target (mixed workload, 30 seconds)

Tested on goos=darwin, goarch=arm64, cpu=Apple M4 Max, using 5 API containers plus PostgreSQL and Redis. Both runs used the same mixed workload with a 50,000 req/s offered load for 30s and issued 1,500,000 requests. The comparison baseline here is ordinary Redis cache-aside: read miss -> load from Postgres -> store in Redis, write -> update Postgres -> delete the Redis entry.

Metric CasCache Redis cache-aside baseline
p50 1.9ms 0.8ms
p95 22.9ms 15.3ms
p99 207.8ms 201.2ms
Max 1337.1ms 1091.6ms
Stale reads 0 195,461

At p50 the gap is about 1.1ms (1.9ms vs 0.8ms). At p95 the gap is about 7.6ms (22.9ms vs 15.3ms). At p99 the gap is about 6.6ms (207.8ms vs 201.2ms). The Redis cache-aside baseline was faster in this run, but it served 195,461 stale reads while CasCache served 0.

Why

What you get:

  • a slow writer that finishes after an invalidate cannot silently overwrite the cache with outdated data
  • reads only serve values that still match the current version state, so your users do not see stale results just because something was cached
  • batch reads check every member before serving the batch, not just the batch key itself
  • if Redis or your backend has a bad moment, the cache degrades to misses or skipped writes instead of quietly serving data it cannot verify

What CasCache does not try to solve:

  • if your "source-of-truth" write succeeds but Invalidate fails, the cache does not know the source moved forward
  • it does not prove that the value you just loaded from your database, api etc. was the newest one that existed anywhere - it only proves the cache entry still matches the last known version

How it works

CasCache keeps authoritative version state for every logical key.

The normal fill path is:

  1. SnapshotVersion
  2. do your service, app, business logic (db, API etc.)
  3. SetIfVersion

Use SetIfVersionWithTTL only when you need to override the cache's default TTL for that write.

The normal write path is:

  1. write where you want
  2. Invalidate

That means the cache never trusts a value just because it exists. A value must still match the current version state when it is read.

About source reads

CasCache guarantees cache freshness against its authoritative version state. It does not guarantee that the value you just loaded from a database or API was the newest value that existed anywhere.

If a fill reads from a lagging database replica or an "eventually consistent" API, that request can still observe old data from the source. What CasCache prevents is that old data being accepted back into the cache after the key has moved to a new fence. Once version state changes, old cache entries stop validating on read and stale refill attempts are rejected on write.

So the guarantee is closer to "no stale cache refill after a successful invalidate" than "the cache can prove your source read was globally current." If a path needs that stronger guarantee, the fill has to read from an authority that is fresh enough for your consistency model, or use ReadGuard / BatchReadGuard on critical read paths.

Read guards

Most paths do not need a read guard. The normal version check already guarantees that once a key is invalidated, older cached bytes stop being valid.

ReadGuard is a guard function you pass through options that the cache calls on every hit for a given key. At its simplest it is just extra cache-level logic - you look at the decoded value and decide whether it is still good enough to serve. The tradeoff comes when your guard calls another authority (API, DB, whatever). That is an extra I/O round trip per key, which means more pressure on that authority for every single cache hit and some extra lookup latency on the cache itself. For batch endpoints, use BatchReadGuard so a single source call can validate many keys at once instead of hitting the authority per member.

Typical cases where a read guard makes sense:

  • fills come from a lagging replica or eventually consistent API, but one endpoint must only serve values confirmed by a primary or another authoritative source
  • the cached value carries a revision, timestamp, or some business state field that must still match source state before use

Example:

type User struct {
	ID        string    `json:"id"`
	Name      string    `json:"name"`
	UpdatedAt time.Time `json:"updated_at"`
}

type UserMetaStore interface {
	CurrentUpdatedAt(ctx context.Context, id string) (time.Time, error)
}

var meta UserMetaStore

cache, err := cascache.New(cascache.Options[User]{
	Namespace: "user",
	Provider:  provider,
	Codec:     codec.JSON[User]{},
	ReadGuard: func(ctx context.Context, key string, cached User) (bool, error) {
		current, err := meta.CurrentUpdatedAt(ctx, key)
		if err != nil {
			return false, err
		}
		return cached.UpdatedAt.Equal(current), nil
	},
})

Installation

go get github.com/unkn0wn-root/cascache/v3

Quick start

Build a cache
package main

import (
	"context"
	"time"

	"github.com/unkn0wn-root/cascache/v3"
	"github.com/unkn0wn-root/cascache/v3/codec"
	ristrettoprovider "github.com/unkn0wn-root/cascache/v3/provider/ristretto"
)

type User struct {
	ID   string `json:"id"`
	Name string `json:"name"`
}

func newUserCache() (cascache.CAS[User], error) {
	provider, err := ristrettoprovider.New(ristrettoprovider.Config{
		NumCounters: 1_000_000,
		MaxCost:     64 << 20,
		BufferItems: 64,
	})
	if err != nil {
		return nil, err
	}

	return cascache.New(cascache.Options[User]{
		Namespace:  "user",
		Provider:   provider,
		Codec:      codec.JSON[User]{},
		DefaultTTL: 5 * time.Minute,
		BatchTTL:   5 * time.Minute,
	})
}

func main() {
	cache, err := newUserCache()
	if err != nil {
		panic(err)
	}
	defer cache.Close(context.Background())
}
Read path
type UserStore interface {
	Load(ctx context.Context, id string) (User, error)
}

type UserRepo struct {
	Cache cascache.CAS[User]
	Store UserStore
}

func (r *UserRepo) GetByID(ctx context.Context, id string) (User, error) {
	if user, ok, err := r.Cache.Get(ctx, id); err != nil {
		return User{}, err
	} else if ok {
		return user, nil
	}

	version, err := r.Cache.SnapshotVersion(ctx, id)
	if err != nil {
		return User{}, err
	}

	user, err := r.Store.Load(ctx, id)
	if err != nil {
		return User{}, err
	}

	// SetIfVersion returns (WriteResult, error). A version mismatch is not
	// an error - it means another request invalidated this key while you were
	// loading. The cache skips the write and the next reader will fill it
	// with fresh data. Here we explicitly ignore the error and return value,
	// but cascache gives you the possibility to handle them if you want to
	// short-circuit, log, or react to a failed cache write.
	_, _ = r.Cache.SetIfVersion(ctx, id, user, version)
	return user, nil
}
Write path
type UserWriter interface {
	Save(ctx context.Context, user User) error
}

type UserWriteRepo struct {
	Cache  cascache.CAS[User]
	Writer UserWriter
}

func (r *UserWriteRepo) Save(ctx context.Context, user User) error {
	if err := r.Writer.Save(ctx, user); err != nil {
		return err
	}

	// Treat invalidate failures as real incidents.
	return r.Cache.Invalidate(ctx, user.ID)
}

Choosing a topology

CasCache can be used in a few different shapes. The right choice depends on where values live and whether replicas need shared freshness decisions.

Constructor Use it when Notes
cascache.New(...) values live in any supported provider and one process owns freshness decisions default version store is local and in-process
cascache.New(...) + redis.NewVersionStore(...) values should stay outside Redis, but replicas must agree on freshness common pattern for per-node Ristretto or BigCache plus shared Redis version state
redis.New(...) both values and version state should live in Redis preferred Redis entry point; includes Redis-native single-key compare-and-write and invalidate

Use the lower-level Redis constructors only when you are intentionally composing a custom topology:

  • redis.NewVersionStore(...)
  • redis.NewProvider(...)
  • redis.NewKeyMutator(...)

If values live in Redis, simply use redis.New(...) to make things easy for you.

Redis example

package main

import (
	"context"
	"time"

	goredis "github.com/redis/go-redis/v9"

	"github.com/unkn0wn-root/cascache/v3/codec"
	cascacheredis "github.com/unkn0wn-root/cascache/v3/redis"
)

type User struct {
	ID   string `json:"id"`
	Name string `json:"name"`
}

func newRedisUserCache() error {
	rdb := goredis.NewClient(&goredis.Options{
		Addr: "127.0.0.1:6379",
	})

	cache, err := cascacheredis.New(cascacheredis.Options[User]{
		Namespace:  "user",
		Client:     rdb,
		Codec:      codec.JSON[User]{},
		DefaultTTL: 5 * time.Minute,
		BatchTTL:   5 * time.Minute,
	})
	if err != nil {
		return err
	}
	defer cache.Close(context.Background())

	return nil
}

Batch APIs

CasCache also supports grouped batch entries:

  • GetMany
  • SnapshotVersions
  • SetIfVersions
  • SetIfVersionsWithTTL when you need a per-call TTL override

On read, the cache tries the batch entry first but checks every member against current version state before serving it. If any member is stale, undecodable, or missing, the whole batch is rejected and the cache falls back to single-key reads.

On write, a batch stores all members as one combined value, but each member is still checked individually. Writing as a batch does not make the write atomic across keys.

The default seed behavior is:

  • BatchReadSeedOff
  • BatchWriteSeedStrict

That keeps reads simple by default and preserves per-key CAS checks when singles are materialized after a successful batch write.

Providers

This repository currently includes:

  • provider/ristretto
  • provider/bigcache
  • redis as a Redis-backed provider and full Redis topology

Provider notes:

  • Ristretto may reject writes under pressure; CasCache reports that as provider_rejected
  • BigCache ignores per-entry TTL and uses its global LifeWindow
  • Redis supports per-entry TTL and the Redis-native single-key mutation path

Codecs

  • codec.JSON
  • codec.NewCBOR / codec.MustCBOR
  • codec.Msgpack
  • codec.NewProtobuf
  • codec.Bytes
  • codec.String

Hooks

CasCache exposes a small hook surface for operational events such as:

  • self-healed corrupt or stale entries
  • rejected batches
  • provider write rejections
  • version-store snapshot and bump errors
  • invalidate outages

Helpful but totaly optional packages:

  • hooks/slog for structured logging
  • hooks/async for non-blocking hook fan-out

Hooks should stay cheap and non-blocking. If they can block, wrap them in hooks/async.

Documentation

Overview

Package cascache implements a provider-agnostic cache with compare-and-swap (CAS) safety via per-key version fences. Single-key reads never return stale values. Batch results are validated on read (per member) and rejected if any member is stale.

Components:

  • Provider: byte store with TTL (e.g. Ristretto, BigCache, Redis).
  • Codec[V]: (de)serializes V <-> []byte.
  • VersionStore: authoritative version state per logical key. Local (in-process) by default, optional Redis implementation for multi-replica / restart persistence. Custom implementations receive opaque version.CacheKey identities, not logical user keys.
  • KeyWriter / KeyInvalidator: optional backend-native fast paths for single-key compare-and-write and invalidate. KeyMutator is the alias for implementations that provide both. See the redis subpackage for the built-in Redis implementation.

Keys:

cas:v3:val:{<slot>}:s:<nsLen>:<ns>:<key>        - single entries
cas:v3:val:b:<nsLen>:<ns>:<sha256-128>          - set-shaped entries (128-bit SHA-256 over sorted keys)
cas:v3:ver:{<slot>}:s:<nsLen>:<ns>:<key>        - authoritative version state

CAS pattern:

obs, err := cache.SnapshotVersion(ctx, k) // before DB read
v        := readFromDB(k)
if err == nil {
	_, _ = cache.SetIfVersion(ctx, k, v, obs) // write iff current version == obs
}

Index

Constants

This section is empty.

Variables

View Source
var ErrBatchReadSeedNeedsAdder = errors.New("BatchReadSeedIfMissing requires Adder")

ErrBatchReadSeedNeedsAdder identifies an invalid configuration where BatchReadSeedIfMissing is requested with a provider that does not implement Adder.

Functions

This section is empty.

Types

type BatchReadGuardFunc

type BatchReadGuardFunc[V any] func(ctx context.Context, values map[string]V) (rejected map[string]struct{}, err error)

BatchReadGuardFunc is the batch form of ReadGuardFunc for validated GetMany hits. The input map contains only the requested logical keys that survived wire and fence checks. Return the keys that failed validation. Any non-empty result deletes the stored batch entry because batch values are stored as a single blob.

If ReadGuard is also configured, GetMany rechecks the rejected keys through per-key fallback reads. Otherwise, rejected keys are treated as misses for that GetMany call so they cannot be served back from seeded singles.

type BatchReadSeedMode

type BatchReadSeedMode uint8

BatchReadSeedMode controls whether a successful batch read validated members as individual single-key entries. The zero/default value is BatchReadSeedOff so batch hits stay read-only unless the caller explicitly opts into warming singles.

const (
	BatchReadSeedOff BatchReadSeedMode = iota
	BatchReadSeedAll
	BatchReadSeedIfMissing
)

type BatchRejectReason

type BatchRejectReason string

BatchRejectReason classifies why a batch entry was rejected.

const (
	// at least one batch payload could not be decoded by the codec.
	BatchRejectReasonValueDecode BatchRejectReason = "value_decode"
	// at least one requested member was absent from the stored batch entry.
	BatchRejectReasonIncompleteBatch BatchRejectReason = "incomplete_batch"
	// authoritative version state was missing for at least one requested member.
	BatchRejectReasonVersionMissing BatchRejectReason = "version_missing"
	// at least one requested member no longer matched authoritative version state.
	BatchRejectReasonVersionMismatch BatchRejectReason = "version_mismatch"
	// batch wire envelope was invalid.
	BatchRejectReasonDecodeError BatchRejectReason = "decode_error"
	// an authoritative read guard rejected at least one requested member.
	BatchRejectReasonReadGuardReject BatchRejectReason = "read_guard_reject"
	// an authoritative read guard failed, so the batch was conservatively dropped.
	BatchRejectReasonReadGuardError BatchRejectReason = "read_guard_error"
)

type BatchWriteResult

type BatchWriteResult struct {
	Outcome       WriteOutcome
	SeededSingles bool
}

BatchWriteResult describes the result of a versioned write through the batch entry path.

func (BatchWriteResult) Stored

func (r BatchWriteResult) Stored() bool

Stored reports whether the batch entry landed in the provider.

type BatchWriteSeedMode

type BatchWriteSeedMode uint8

BatchWriteSeedMode controls how a successful SetIfVersions write individual single-key entries.

The zero/default value is BatchWriteSeedStrict, which routes each single through SetIfVersion again so the post-batch seeding path preserves the same per-key CAS recheck as standalone writes. Higher-throughput systems can opt into BatchWriteSeedFast to reuse the validated batch payloads directly, or BatchWriteSeedOff to skip success path single seeding entirely.

const (
	BatchWriteSeedStrict BatchWriteSeedMode = iota
	BatchWriteSeedFast
	BatchWriteSeedOff
)

type CAS

type CAS[V any] interface {
	Enabled() bool
	Close(context.Context) error

	// Single
	Get(ctx context.Context, key string) (v V, ok bool, err error)
	SnapshotVersion(ctx context.Context, key string) (Version, error)
	SetIfVersion(ctx context.Context, key string, value V, version Version) (WriteResult, error)
	SetIfVersionWithTTL(ctx context.Context, key string, value V, version Version, ttl time.Duration) (WriteResult, error)
	Invalidate(ctx context.Context, key string) error

	// Batch (order-agnostic return. Use your own ordering by keys slice)
	GetMany(ctx context.Context, keys []string) (values map[string]V, missing []string, err error)
	SnapshotVersions(ctx context.Context, keys []string) (map[string]Version, error)
	SetIfVersions(ctx context.Context, items []VersionedValue[V]) (BatchWriteResult, error)
	SetIfVersionsWithTTL(ctx context.Context, items []VersionedValue[V], ttl time.Duration) (BatchWriteResult, error)
}

CAS is the provider-agnostic cache interface with compare-and-swap safety via per-key version fences. V is the caller's value type serialization is handled by the configured Codec[V].

func New

func New[V any](opts Options[V]) (CAS[V], error)

type Cache

type Cache[V any] = CAS[V]

Cache is an alias for CAS so callers can write cascache.Cache[V] if preferred.

type Hooks

type Hooks interface {
	SelfHealSingle(storageKey string, reason SelfHealReason)
	BatchRejected(namespace string, requested int, reason BatchRejectReason)
	ProviderSetRejected(storageKey string, isBatch bool)
	VersionSnapshotError(count int, err error)
	VersionCreateError(cacheKey version.CacheKey, err error)
	VersionAdvanceError(cacheKey version.CacheKey, err error)
	InvalidateOutage(key string, bumpErr, delErr error)
	LocalVersionStoreWithBatch()
}

Hooks are lightweight callbacks for high-signal events. Implementations MUST be cheap and non-blocking; do not perform I/O. If work may block, buffer it and drop on backpressure (best effort).

Key-bearing callbacks intentionally expose different key kinds:

  • SelfHealSingle / ProviderSetRejected: provider storage keys.
  • VersionCreateError / VersionAdvanceError: canonical version.CacheKey identity.

func Multi

func Multi(hs ...Hooks) Hooks

Multi returns a Hooks implementation that fans out to all provided hooks in order. Nil entries are silently skipped. Panics from any hook propagate to the caller.

Example usage:

logH := sloghook.New(slog.Default(), sloghook.Options{SelfHealEvery: 10}) metH := promhook.New(...) // some kind of metrics adapter auditH := myAuditHook{...} // audit adapter

fan-out mh := cascache.MultiHooks{logH, metH, auditH}

Either: single async queue for the whole fan-out hooks := asynchook.New(mh, 1, 1000)

Or: give each hook its own queue (isolate backpressure)

hooks := cascache.MultiHooks{
    asynchook.New(logH,   1, 1000),
    asynchook.New(metH,   1, 1000),
    asynchook.New(auditH, 1, 1000),
}

type InvalidateError

type InvalidateError struct {
	Key        string
	AdvanceErr error
	DelErr     error
}

func (*InvalidateError) Error

func (e *InvalidateError) Error() string

func (*InvalidateError) Unwrap

func (e *InvalidateError) Unwrap() []error

type KeyInvalidator

type KeyInvalidator interface {
	Invalidate(ctx context.Context, versionKey version.CacheKey, valueKey string) error
}

KeyInvalidator is an optional backend-native fast path for single-key invalidation. versionKey identifies the canonical authoritative version state tracked by the configured VersionStore. valueKey identifies the provider storage key for the encoded single value entry. Implementations are responsible for coordinating those two keys so Invalidate preserves the same contract as the generic cache path.

type KeyMutator

type KeyMutator interface {
	KeyWriter
	KeyInvalidator
}

type KeyWriter

type KeyWriter interface {
	// payload is the codec-encoded caller value, not the final wire envelope.
	// Implementations are responsible for writing a value stamped with the
	// authoritative fence that actually won the compare/init step.
	SetIfVersion(ctx context.Context, versionKey version.CacheKey, valueKey string, expected version.Snapshot, payload []byte, ttl time.Duration) (stored bool, err error)
}

KeyWriter is an optional backend-native fast path for single-key compare-and-write operations. versionKey identifies the canonical authoritative version state tracked by the configured VersionStore. valueKey identifies the provider storage key for the encoded single value entry. Implementations are responsible for coordinating those two keys so SetIfVersion preserves the same freshness contract as the generic cache path.

type NopHooks

type NopHooks struct{}

NopHooks is a default no-op.

func (NopHooks) BatchRejected

func (NopHooks) BatchRejected(string, int, BatchRejectReason)

func (NopHooks) InvalidateOutage

func (NopHooks) InvalidateOutage(string, error, error)

func (NopHooks) LocalVersionStoreWithBatch

func (NopHooks) LocalVersionStoreWithBatch()

func (NopHooks) ProviderSetRejected

func (NopHooks) ProviderSetRejected(string, bool)

func (NopHooks) SelfHealSingle

func (NopHooks) SelfHealSingle(string, SelfHealReason)

func (NopHooks) VersionAdvanceError

func (NopHooks) VersionAdvanceError(version.CacheKey, error)

func (NopHooks) VersionCreateError

func (NopHooks) VersionCreateError(version.CacheKey, error)

func (NopHooks) VersionSnapshotError

func (NopHooks) VersionSnapshotError(int, error)

type Op

type Op string

Op identifies the logical cache operation that failed.

const (
	OpGet           Op = "get"
	OpSet           Op = "set"
	OpAdd           Op = "add"
	OpSnapshot      Op = "snapshot"
	OpInvalidate    Op = "invalidate"
	OpGetMany       Op = "get_many"
	OpSetIfVersions Op = "set_if_versions"
)

type OpError

type OpError struct {
	Op  Op
	Key string // empty for non-key specific failures such as batch path failures

	// Err is the underlying cause.
	// Error panics if Err is nil.
	Err error
}

OpError reports an operation failure and, when applicable, the logical key that triggered it.

func (*OpError) Error

func (e *OpError) Error() string

func (*OpError) Unwrap

func (e *OpError) Unwrap() error

type Options

type Options[V any] struct {
	Namespace string // logical namespace to isolate the keyspace
	Provider  pr.Provider
	Codec     c.Codec[V]

	DefaultTTL     time.Duration // singles; 0 => 10m
	BatchTTL       time.Duration // batches; 0 => 10m
	Disabled       bool          // default false (enabled)
	ComputeSetCost SetCostFunc   // default 1
	VersionStore   version.Store // nil => LocalStore (in-process)
	KeyWriter      KeyWriter
	KeyInvalidator KeyInvalidator
	DisableBatch   bool // default false => batch enabled
	ReadGuard      ReadGuardFunc[V]
	BatchReadGuard BatchReadGuardFunc[V]
	BatchReadSeed  BatchReadSeedMode
	BatchWriteSeed BatchWriteSeedMode
	Hooks          Hooks
}

Options configures the CAS cache. Namespace, Provider, and Codec are required.

type ReadGuardFunc

type ReadGuardFunc[V any] func(ctx context.Context, key string, value V) (allow bool, err error)

ReadGuardFunc can veto serving a decoded cache hit for a single logical key. It is intended for critical paths that need an authoritative source check before a cached value may be returned. Return allow=false to reject the entry as stale or unsafe. Any returned error is treated conservatively as a rejection and the caller receives a miss.

type SelfHealReason

type SelfHealReason string

SelfHealReason classifies why a single entry was deleted on read.

const (
	// single-entry wire envelope was invalid.
	SelfHealReasonCorrupt SelfHealReason = "corrupt"
	// no authoritative version state existed for the key.
	SelfHealReasonVersionMissing SelfHealReason = "version_missing"
	// stored version fence no longer matched the current authoritative fence.
	SelfHealReasonVersionMismatch SelfHealReason = "version_mismatch"
	// payload could not be decoded by the configured codec.
	SelfHealReasonValueDecode SelfHealReason = "value_decode"
	// an authoritative read guard rejected the entry.
	SelfHealReasonReadGuardReject SelfHealReason = "read_guard_reject"
	// an authoritative read guard errored, so the entry was conservatively dropped.
	SelfHealReasonReadGuardError SelfHealReason = "read_guard_error"
)

type SetCostFunc

type SetCostFunc func(key string, raw []byte, isBatch bool, memberCount int) int64

SetCostFunc computes the provider cost used for a cache write. The returned value is passed through to Provider.Set (and Adder.Add when applicable). Providers that use admission or weighted eviction can interpret it as entry weight; providers that ignore cost may discard it. key is the provider storage key cascache is writing. raw is the fully encoded wire value that will be stored. isBatch reports whether the write targets the grouped batch-entry path rather than a single-key entry. memberCount is 1 for single writes and the number of logical keys contained in a batch entry.

type Version

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

Version is the per-key freshness token returned by the cache. Callers should treat it as an compare-only value and pass it back unchanged to versioned write APIs.

The zero value is the missing-version token.

func (Version) Equal

func (v Version) Equal(other Version) bool

func (Version) IsMissing

func (v Version) IsMissing() bool

type VersionedValue

type VersionedValue[V any] struct {
	Key     string
	Value   V
	Version Version
}

VersionedValue is the caller-facing unit for versioned multi-key writes.

type WriteOutcome

type WriteOutcome string

WriteOutcome describes what happened during a versioned write attempt.

const (
	WriteOutcomeStored           WriteOutcome = "stored"
	WriteOutcomeVersionMismatch  WriteOutcome = "version_mismatch"
	WriteOutcomeSnapshotError    WriteOutcome = "snapshot_error"
	WriteOutcomeProviderRejected WriteOutcome = "provider_rejected"
	WriteOutcomeDisabled         WriteOutcome = "disabled"
)

type WriteResult

type WriteResult struct {
	Outcome WriteOutcome
}

func (WriteResult) Stored

func (r WriteResult) Stored() bool

Stored reports whether the write landed in the provider.

Directories

Path Synopsis
hooks
async
usage:
usage:
slog
Package sloghook provides a slog-based implementation of cascache.Hooks.
Package sloghook provides a slog-based implementation of cascache.Hooks.
internal
wire
Package wire contains the compact, versioned on-the-wire format used by cascache to store values in the underlying Provider.
Package wire contains the compact, versioned on-the-wire format used by cascache to store values in the underlying Provider.
Package provider defines the storage abstraction used by cascache.
Package provider defines the storage abstraction used by cascache.
Package redis contains the Redis backend for cascache.
Package redis contains the Redis backend for cascache.
Package version contains the lower-level primitives behind cascache's authoritative version state.
Package version contains the lower-level primitives behind cascache's authoritative version state.

Jump to

Keyboard shortcuts

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