cascache

package module
v1.4.2 Latest Latest
Warning

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

Go to latest
Published: Mar 23, 2026 License: Apache-2.0 Imports: 14 Imported by: 0

README

CasCache

Provider agnostic CAS like (Compare-And-Set or generation-guarded conditional set) cache with pluggable codecs and a pluggable generation store. Safe single-key reads (no stale values), optional bulk caching with read-side validation, and an opt‑in distributed mode for multi-replica deployments.

[!WARNING] Breaking changes in v1.0 - If you are upgrading from v0.x, please read.

Storage keys have changed. The on-disk and Redis key format has been replaced with a versioned, length-framed layout (cas:v1:val:…, cas:v1:gen:…). This fixes a class of namespace boundary collisions present in earlier releases, but it means that existing cached data will not be read by the new version. Old entries will remain in your backend under their previous keys until they expire or are evicted. They will not conflict with new entries but they will not be reused either. If you run a shared backend such as Redis, deploy all nodes at the same time and expect a cold cache on the first run.

RedisGenStore no longer accepts a namespace. The namespace parameter has been removed from NewRedisGenStore, NewRedisGenStoreWithTTL, and RedisGenStoreOptions. The cache now constructs canonical keys internally and passes them to the gen store as opaque values. Update your constructor calls accordingly (see Distributed generations for updated examples).

GenStore interface uses typed keys. Snapshot, SnapshotMany, and Bump now receive and return genstore.CacheKey instead of plain strings. If you have a custom GenStore implementation, update the method signatures to match. CacheKey values are opaque - store and compare them "as-is" without parsing.

Hooks interface update. GenBumpError now receives a genstore.CacheKey instead of a string. If you implement Hooks directly, update this method signature. The asynchook and sloghook adapters in this module are already updated.


Contents


Why CasCache

TL;DR

Apps that rely on "delete-then-set" patterns can still surface stale data, especially when multiple replicas race one another. cascache gives you this:

After you invalidate a key, the cache will never serve the previous value. No additional delete cycles, manual timing coordination, or best-effort TTL tuning.

It does this with generation-guarded writes (CAS) and read-side validation using a tiny per-key counter.


Pattern What you do What still goes wrong
TTL only Set user:42 for 5m Readers can see up to 5m stale after a write. Reducing TTL increases DB load.
Delete then set DEL user:42 then SET user:42 Races: a reader between DEL and new SET repopulates from old DB snapshot.
Write-through Update DB, then cache Concurrent readers can serve old data until invalidation is coordinated perfectly.
Version in value Store {version, payload} Readers still need current version; coordinating that is the same hard problem.

  • Each key carries a generation. Mutations call Invalidate(key) to bump the generation. Reads accept a cached value only if its stored generation matches the current one, otherwise the entry is deleted and treated as a miss.
  • Writers snapshot the generation before reading from the backing store. SetWithGen(k, v, obs) commits only if the generation is unchanged. If another writer updated the key, the write is skipped, preventing stale data from being reintroduced.
  • If the generation store is slow or unavailable, single-key reads self-heal by treating results as misses, and CAS writes skip. The cache never serves stale data.
  • Bulk entries are checked member by member during reads. If any entry is stale, the bulk payload is discarded and CasCache falls back to single-key fetches. Extras in the bulk are ignored during validation and decode; missing members render it invalid.
  • Works with Ristretto/BigCache/Redis for storage and JSON/CBOR/Msgpack/Proto for payloads. Wire decoding stays tight and zero-copy for payloads.

DB write succeeds  →  Cache.Invalidate(k)       // bump gen; clear single
Read slow path     →  snap, err := TrySnapshotGen(ctx, k) → load DB → SetWithGen(k, v, snap) if err == nil
Read fast path     →  Get(k) validates stored gen == current; else self-heals
Bulk read          →  every member’s gen must match; else drop bulk → singles
Multi-replica      →  use RedisGenStore so all nodes see the same gen

Getting started

1) Build the cache
import (
	"context"
	"time"

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

type User struct{ ID, Name string }

func newUserCache() (cascache.CAS[User], error) {
	rist, err := rp.New(rp.Config{
		NumCounters: 1_000_000,
		MaxCost:     64 << 20, // 64 MiB
		BufferItems: 64,
		Metrics:     false,
	})
	if err != nil { return nil, err }

	return cascache.New[User](cascache.Options[User]{
		Namespace:  "user",
		Provider:   rist,
		Codec:      codec.JSON[User]{},
		DefaultTTL: 5 * time.Minute,
		BulkTTL:    5 * time.Minute,
		// GenStore: nil -> Local (single-process) generations
	})
}
2) Safe single read (never stale)

Rule: Snapshot the generation before touching the DB. If you read first, a concurrent invalidation can bump the generation and your later SetWithGen may reinsert stale data. Use TrySnapshotGen when you want explicit visibility into generation-store failures. SnapshotGen is the convenience form and returns 0 on snapshot failure.

type UserRepo struct {
	Cache cascache.CAS[User]
	// db handle...
}

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

	// CAS snapshot BEFORE reading DB
	obs, snapErr := r.Cache.TrySnapshotGen(ctx, id)

	u, err := r.dbSelectUser(ctx, id) // your DB load
	if err != nil { return User{}, err }

	// If snapshot failed, serve from the DB and skip the cache write.
	if snapErr == nil {
		_ = r.Cache.SetWithGen(ctx, id, u, obs, 0)
	}
	return u, nil
}
3) Mutations invalidate (one line)

Rule: after a successful DB write, call Invalidate(key) to bump the generation.

func (r *UserRepo) UpdateName(ctx context.Context, id, name string) error {
	if err := r.dbUpdateName(ctx, id, name); err != nil { return err }
	_ = r.Cache.Invalidate(ctx, id) // bump gen + clear single
	return nil
}
4) Optional bulk (safe set caching)

If any member is stale, the bulk is dropped and you fall back to singles. In multi-replica apps, use a shared GenStore (below) or disable bulk. Use TrySnapshotGens when you want explicit visibility into generation-store failures. SnapshotGens is the convenience form and returns 0 for any failed snapshot. Successful bulk hits are read-only by default. If you want them to warm single-key entries, set BulkSeed to BulkSeedAll or BulkSeedIfMissing. BulkSeed affects GetBulk hits only. Successful SetBulkWithGens writes use BulkWriteSeed, which defaults to the stricter checked mode. Set BulkWriteSeedFast to reuse validated payloads directly or BulkWriteSeedOff to skip success-path single seeding. BulkSeedIfMissing requires a provider that can do insert-if-missing with native backend support or provider-level atomic coordination via Adder.

func (r *UserRepo) GetMany(ctx context.Context, ids []string) (map[string]User, error) {
	values, missing, cacheErr := r.Cache.GetBulk(ctx, ids)
	if cacheErr != nil {
		// Optional: record metrics/logs and continue with DB fallback.
	}
	if len(missing) == 0 {
		return values, nil
	}

	// Snapshot *before* DB read
	obs, snapErr := r.Cache.TrySnapshotGens(ctx, missing)

	// Load missing from DB in one shot
	loaded, err := r.dbSelectUsers(ctx, missing)
	if err != nil { return nil, err }

	// Index for SetBulkWithGens
	items := make(map[string]User, len(loaded))
	for _, u := range loaded { items[u.ID] = u }

	// If snapshots succeeded, conditionally write bulk (or seed singles if any gen moved).
	if snapErr == nil {
		_ = r.Cache.SetBulkWithGens(ctx, items, obs, 0)
	}

	// Merge and return
	for k, v := range items { values[k] = v }
	return values, nil
}

Distributed generations (multi-replica)

Local generations are correct within a single process. In multi-replica deployments, share generations to eliminate cross-node windows and keep bulks safe across nodes.

LocalGenStore is single-process only. In multi-replica setups, both singles and bulks can be stale on nodes that haven’t observed the bump. Use a shared GenStore (e.g., Redis) for cross-replica correctness or run a single instance.

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

func newUserCacheDistributed() (cascache.CAS[User], error) {
	rist, _ := rp.New(rp.Config{NumCounters:1_000_000, MaxCost:64<<20, BufferItems:64})
	rdb := redis.NewClient(&redis.Options{Addr: "127.0.0.1:6379"})
	gs  := gen.NewRedisGenStoreWithTTL(rdb, 90*24*time.Hour)

	return cascache.New[User](cascache.Options[User]{
		Namespace: "user",
		Provider:  rist,                      // or Redis/BigCache for values
		Codec:     codec.JSON[User]{},
		GenStore:  gs,                        // shared generations
		BulkTTL:   5 * time.Minute,
	})
}

You can reuse the same client across caches:

rdb := redis.NewClusterClient(/* ... */) // or UniversalClient

userCache, _ := cascache.New[user](cascache.Options[user]{
    Namespace: "app:prod:user",
    Provider:  myRedisProvider{Client: rdb},               // same client
    Codec:     c.JSON[user]{},
    GenStore:  gen.NewRedisGenStoreWithTTL(rdb, 24*time.Hour),
})

pageCache, _ := cascache.New[page](cascache.Options[page]{
    Namespace: "app:prod:page",
    Provider:  myRedisProvider{Client: rdb},               // same client
    Codec:     c.JSON[page]{},
    GenStore:  gen.NewRedisGenStoreWithTTL(rdb, 24*time.Hour),
})

permCache, _ := cascache.New[permission](cascache.Options[permission]{
    Namespace: "app:prod:perm",
    Provider:  myRedisProvider{Client: rdb},               // same client
    Codec:     c.JSON[permission]{},
    GenStore:  gen.NewRedisGenStoreWithTTL(rdb, 24*time.Hour),
})

RedisGenStore leaves shared clients open by default. If the genstore owns the client and should close it, use:

gs, err := gen.NewRedisGenStoreWithOptions(gen.RedisGenStoreOptions{
    Client:      rdb,
    TTL:         24 * time.Hour,
    CloseClient: true,
})
if err != nil { /* handle */ }

If you implement a custom GenStore, note that all methods now use genstore.CacheKey instead of plain strings. Treat these values as opaque byte identities - store and compare them as-is without attempting to parse or reconstruct them from logical user keys.


Alternative type name: You can use cascache.Cache[V] instead of cascache.CAS[V]. See Cache Type alias.


Read guards

Generation checks catch most staleness, but there is a small window where the generation is still valid and the source has already changed. A write landed in the DB, but Invalidate hasn't fired yet (or hasn't propagated across replicas). During that window the cache will happily serve the old value because the generation still matches. ReadGuard closes that gap. You give the cache a function that checks whether a decoded value is still good. The cache calls it on every hit, right after decode and generation validation. If the guard says no (returns false or an error), the entry gets deleted and the caller sees a miss. This means your guard runs on every single cache hit. If your guard does a network call, you are adding that latency to every read that would otherwise have been a local cache hit. Keep it cheap - check a version column or a lightweight flag, don't refetch the full row.

Single-key guard

ReadGuard receives the logical key and the decoded value. Return true to serve it, false to reject and delete it.

cache, _ := cascache.New[User](cascache.Options[User]{
    Namespace: "user",
    Provider:  redisProvider,
    Codec:     codec.JSON[User]{},
    GenStore:  gen.NewRedisGenStoreWithTTL(rdb, 24*time.Hour),
    ReadGuard: func(ctx context.Context, key string, cached User) (bool, error) {
        ver, err := dbUserVersion(ctx, key)
        if err != nil {
            return false, err
        }
        return ver == cached.Version, nil
    },
})

This is enough for most cases. If you don't set BulkReadGuard, bulk reads will call ReadGuard once per member, which works fine but does N separate checks.

Bulk guard

If you use bulk caching and want to batch verify in one round-trip instead of N individual checks, set BulkReadGuard. It receives all decoded members at once and returns the set of keys you want to reject.

cache, _ := cascache.New[User](cascache.Options[User]{
    Namespace:  "user",
    Provider:   redisProvider,
    Codec:      codec.JSON[User]{},
    GenStore:   gen.NewRedisGenStoreWithTTL(rdb, 24*time.Hour),
    BulkReadGuard: func(ctx context.Context, cached map[string]User) (map[string]struct{}, error) {
        versions, err := dbUserVersions(ctx, maps.Keys(cached))
        if err != nil {
            return nil, err
        }
        rejected := map[string]struct{}{}
        for key, user := range cached {
            if versions[key] != user.Version {
                rejected[key] = struct{}{}
            }
        }
        return rejected, nil
    },
})

One thing to know: the stored bulk blob is all-or-nothing. If any key fails the guard, the entire bulk entry is deleted because bulk entries are stored as a single blob. CasCache may still return non-rejected members from the already-validated payload for that GetBulk call to avoid unnecessary per-key reads. Rejected members fall back to single-key reads only when ReadGuard is also configured; otherwise they are reported as misses for that call so they cannot be served back from seeded singles. There is no way to partially invalidate a stored bulk entry.

Design

Read path (single)
      Get(k)
        │
  provider.Get("cas:v1:val:s:<nsLen>:<ns>:"+k) ───► [wire.DecodeSingle]
        │
   gen == currentGen("s:<nsLen>:<ns>:"+k) ?
        │ yes                                 no
        ▼                                    ┌───────────────┐
  codec.Decode(payload)                      │ Del(entry)    │
        │                                    │ return miss   │
      return v                               └───────────────┘
Write path (single, CAS)
obs, err := TrySnapshotGen(ctx, k) // BEFORE DB read
v        := DB.Read(k)
if err == nil {
    SetWithGen(k, v, obs)          // write iff currentGen(k) == obs
}
Bulk read validation
GetBulk(keys)   -> provider.Get("cas:v1:val:b:<nsLen>:<ns>:sha256-128(sorted(keys))")
Decode -> [(key,gen,payload)]*n
for each requested key: gen == currentGen("s:<nsLen>:<ns>:"+key) ?
if all valid -> decode requested members, return
else         -> drop bulk, fall back to singles
Components
  • Provider - byte store with TTLs: Ristretto/BigCache/Redis.
  • Codec[V] - serialize/deserialize your V to/from []byte.
  • GenStore - per-key generation counter (Local by default; Redis available).
Keys
  • Single value: cas:v1:val:s:<nsLen>:<ns>:<key>
  • Bulk value: cas:v1:val:b:<nsLen>:<ns>:<sha256-128(sorted(keys))>
  • Gen key: cas:v1:gen:s:<nsLen>:<ns>:<key>
CAS model
  • Per-key generation is monotonic.
  • Reads validate only; no write amplification.
  • Mutations call Invalidate(key) -> bump generation and delete the single entry.

Wire format

Single

+---------+---------+---------+---------------+---------------+-------------------+
| magic   | version | kind    | gen (u64)     | vlen (u32)    | payload (vlen)    |
| "CASC"  |   0x01  |  0x01   | 8 bytes       | 4 bytes       | vlen bytes        |
+---------+---------+---------+---------------+---------------+-------------------+

Bulk

+---------+---------+---------+------------------------+
| magic   | version | kind    | n (u32)                |
+---------+---------+---------+------------------------+
repeated n times:
+----------------+-----------------+----------------+-------------------+------------------+
| keyLen (u16)   | key (keyLen)    | gen (u64)      | vlen (u32)        | payload (vlen)   |
+----------------+-----------------+----------------+-------------------+------------------+

Providers

type Provider interface {
    Get(ctx context.Context, key string) ([]byte, bool, error)
    Set(ctx context.Context, key string, value []byte, cost int64, ttl time.Duration) (bool, error)
    Del(ctx context.Context, key string) error
    Close(ctx context.Context) error
}
  • Ristretto: in-process; per-entry TTL; cost-based eviction.
  • BigCache: in-process; global life window; per-entry TTL ignored.
  • Redis: distributed (optional); per-entry TTL. Pass an existing goredis.UniversalClient and reuse it across caches; set CloseClient: true in the provider config only when this cache owns the client and should close it on teardown.

Use any provider for values. Generations can be local or distributed independently.


Codecs

type Codec[V any] interface {
    Encode(V) ([]byte, error)
    Decode([]byte) (V, error)
}
type JSON[V any] struct{}

You can drop in Msgpack/CBOR/Proto or decorators (compression/encryption). CAS is codec-agnostic.


API

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

    // Single
    Get(ctx context.Context, key string) (V, bool, error)
    SetWithGen(ctx context.Context, key string, value V, observedGen uint64, ttl time.Duration) error
    Invalidate(ctx context.Context, key string) error

    // Bulk
    GetBulk(ctx context.Context, keys []string) (map[string]V, []string, error)
    SetBulkWithGens(ctx context.Context, items map[string]V, observedGens map[string]uint64, ttl time.Duration) error

    // Error-aware generation snapshots
    TrySnapshotGen(ctx context.Context, key string) (uint64, error)
    TrySnapshotGens(ctx context.Context, keys []string) (map[string]uint64, error)

    // Best-effort generation snapshots
    SnapshotGen(ctx context.Context, key string) uint64
    SnapshotGens(ctx context.Context, keys []string) map[string]uint64
}

Notes

  • Time: O(1) singles; O(n) bulk for n members.
  • Allocations: zero-copy wire decode; one string alloc per bulk item.
  • Bulk-hit warming: disabled by default so valid bulk hits do not fan out into single-entry writes unless you opt in with BulkSeed.
  • Post-bulk-write seeding: strict by default so successful SetBulkWithGens writes recheck each single through CAS before seeding it. Use BulkWriteSeedFast or BulkWriteSeedOff only when you explicitly want the performance tradeoff.
ComputeSetCost: func(key string, raw []byte, isBulk bool, n int) int64 {
    if isBulk { return int64(n) }
    return 1
}
  • Cleanup (local gens): periodic prune by last bump time (default retention 30d).

Documentation

Overview

Package cascache implements a provider-agnostic cache with compare-and-swap (CAS) safety via per-key generations. Single-key reads never return stale values. Bulk 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.
  • GenStore: generation counter per logical key. Local (in-process) by default, optional Redis implementation for multi-replica / restart persistence. Custom implementations receive opaque genstore.CacheKey identities, not logical user keys.

Keys:

cas:v1:val:s:<nsLen>:<ns>:<key>         - single entries
cas:v1:val:b:<nsLen>:<ns>:<sha256-128>  - set-shaped entries (128-bit SHA-256 over sorted keys)
cas:v1:gen:s:<nsLen>:<ns>:<key>         - generation counters (RedisGenStore)

CAS pattern:

obs, err := cache.TrySnapshotGen(ctx, k) // before DB read
v        := readFromDB(k)
if err == nil {
	_ = cache.SetWithGen(ctx, k, v, obs, 0) // write iff current gen == obs
}

Index

Constants

This section is empty.

Variables

View Source
var ErrBulkSeedNeedsAdder = errors.New("BulkSeedIfMissing requires Adder")

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

View Source
var ErrMissingObservedGens = errors.New("missing observed generations")

ErrMissingObservedGens identifies a SetBulkWithGens caller error where at least one item key has no corresponding observed generation.

Functions

This section is empty.

Types

type BulkReadGuardFunc added in v1.4.1

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

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

If ReadGuard is also configured, GetBulk rechecks the rejected keys through per-key fallback reads. Otherwise, rejected keys are treated as misses for that GetBulk call so they cannot be served back from seeded singles. Returning a key that was not present in values or returning an error is treated conservatively as a guard failure. Returning an error is treated conservatively as a bulk rejection.

type BulkRejectReason added in v0.3.1

type BulkRejectReason string

BulkRejectReason classifies why a bulk entry was rejected.

const (
	// at least one bulk payload could not be decoded by the codec.
	BulkRejectReasonValueDecode BulkRejectReason = "value_decode"
	// bulk entry was missing members or contained stale generations.
	BulkRejectReasonInvalidOrStale BulkRejectReason = "invalid_or_stale"
	// bulk wire envelope was invalid.
	BulkRejectReasonDecodeError BulkRejectReason = "decode_error"
	// caller omitted at least one observed generation.
	BulkRejectReasonMissingObservedGen BulkRejectReason = "missing_observed_gen"
	// current generations could not be loaded.
	BulkRejectReasonGenSnapshotError BulkRejectReason = "gen_snapshot_error"
	// one observed generation no longer matched.
	BulkRejectReasonGenMismatch BulkRejectReason = "gen_mismatch"
	// an authoritative read guard rejected at least one requested member.
	BulkRejectReasonReadGuardReject BulkRejectReason = "read_guard_reject"
	// an authoritative read guard failed, so the bulk was conservatively dropped.
	BulkRejectReasonReadGuardError BulkRejectReason = "read_guard_error"
)

type BulkSeedMode added in v1.1.0

type BulkSeedMode uint8

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

const (
	// BulkSeedOff returns the bulk hit "as-is" and does not write singles.
	BulkSeedOff BulkSeedMode = iota

	// BulkSeedAll seeds singles after the bulk has already passed
	// generation validation. No additional per-key generation lookup is done.
	BulkSeedAll

	// BulkSeedIfMissing seeds singles only when the provider supports
	// conditional add/set-if-absent with native backend support or
	// provider-level atomic coordination. Cache construction fails if the
	// provider does not implement Adder.
	BulkSeedIfMissing
)

type BulkWriteSeedMode added in v1.4.1

type BulkWriteSeedMode uint8

BulkWriteSeedMode controls how a successful SetBulkWithGens write individual single-key entries.

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

const (
	// BulkWriteSeedStrict seeds singles through SetWithGen after the bulk write
	// has succeeded. Stricter CAS semantics of rechecking
	// each key's generation immediately before the single write lands.
	BulkWriteSeedStrict BulkWriteSeedMode = iota

	// BulkWriteSeedFast seeds singles directly from the validated bulk payloads
	// without a second generation lookup. This is faster but can allow stale
	// singles to land if a generation changes between batch validation and the
	// single writes.
	BulkWriteSeedFast

	// BulkWriteSeedOff skips single seeding after a successful bulk write. The
	// bulk entry is still written, and fallback paths that skip or reject the
	// bulk continue to seed singles best-effort through the checked CAS path.
	BulkWriteSeedOff
)

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)
	SetWithGen(ctx context.Context, key string, value V, observedGen uint64, ttl time.Duration) error
	Invalidate(ctx context.Context, key string) error

	// Bulk (order-agnostic return. Use your own ordering by keys slice)
	GetBulk(ctx context.Context, keys []string) (values map[string]V, missing []string, err error)
	SetBulkWithGens(ctx context.Context, items map[string]V, observedGens map[string]uint64, ttl time.Duration) error

	// Error-aware generation snapshots for CAS writes.
	// These return an error so the caller can decide whether to proceed.
	TrySnapshotGen(ctx context.Context, key string) (uint64, error)
	TrySnapshotGens(ctx context.Context, keys []string) (map[string]uint64, error)

	// Best-effort generation snapshots.
	// Failures are reported through Hooks and the generation falls back to zero.
	SnapshotGen(ctx context.Context, key string) uint64
	SnapshotGens(ctx context.Context, keys []string) map[string]uint64
}

CAS is the provider-agnostic cache interface with compare-and-swap safety via per-key generations. 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 added in v0.0.6

type Hooks interface {
	SelfHealSingle(storageKey string, reason SelfHealReason)
	BulkRejected(namespace string, requested int, reason BulkRejectReason)
	ProviderSetRejected(storageKey string, isBulk bool)
	GenSnapshotError(count int, err error)
	GenBumpError(cacheKey genstore.CacheKey, err error)
	InvalidateOutage(key string, bumpErr, delErr error)
	LocalGenWithBulk()
}

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.
  • GenBumpError: canonical genstore.CacheKey identity.

func Multi added in v0.0.9

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 added in v0.0.5

type InvalidateError struct {
	Key     string
	BumpErr error
	DelErr  error
}

func (*InvalidateError) Error added in v0.0.5

func (e *InvalidateError) Error() string

func (*InvalidateError) Unwrap added in v0.0.5

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

type MissingObservedGensError added in v0.3.1

type MissingObservedGensError struct {
	Missing []string
}

MissingObservedGensError reports which logical keys were missing observed generations.

func (*MissingObservedGensError) Error added in v0.3.1

func (e *MissingObservedGensError) Error() string

func (*MissingObservedGensError) Unwrap added in v0.3.2

func (e *MissingObservedGensError) Unwrap() error

type NopHooks added in v0.0.6

type NopHooks struct{}

NopHooks is a default no-op.

func (NopHooks) BulkRejected added in v0.0.6

func (NopHooks) BulkRejected(string, int, BulkRejectReason)

func (NopHooks) GenBumpError added in v0.0.6

func (NopHooks) GenBumpError(genstore.CacheKey, error)

func (NopHooks) GenSnapshotError added in v0.0.6

func (NopHooks) GenSnapshotError(int, error)

func (NopHooks) InvalidateOutage added in v0.0.6

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

func (NopHooks) LocalGenWithBulk added in v0.0.6

func (NopHooks) LocalGenWithBulk()

func (NopHooks) ProviderSetRejected added in v0.0.6

func (NopHooks) ProviderSetRejected(string, bool)

func (NopHooks) SelfHealSingle added in v0.0.6

func (NopHooks) SelfHealSingle(string, SelfHealReason)

type Op added in v1.2.0

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"
	OpGetBulk    Op = "get_bulk"
	OpSetBulk    Op = "set_bulk"
)

type OpError added in v1.2.0

type OpError struct {
	Op  Op
	Key string // empty for non-key specific failures such as bulk 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 added in v1.2.0

func (e *OpError) Error() string

func (*OpError) Unwrap added in v1.2.0

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
	BulkTTL         time.Duration // bulks; 0 => 10m
	CleanupInterval time.Duration // 0 => 1h
	GenRetention    time.Duration // 0 => 30d
	Disabled        bool          // default false (enabled)
	ComputeSetCost  SetCostFunc   // default 1
	GenStore        gen.GenStore  // nil => LocalGenStore (in-process)
	DisableBulk     bool          // default false => bulk enabled
	ReadGuard       ReadGuardFunc[V]
	BulkReadGuard   BulkReadGuardFunc[V]
	// BulkSeed controls single-entry warming after successful GetBulk hits
	// only. It does not affect how successful SetBulkWithGens writes seed
	// singles; that behavior is controlled separately by BulkWriteSeed.
	// BulkSeedIfMissing requires a provider with native or provider-level
	// atomic add-if-missing support.
	BulkSeed BulkSeedMode
	// BulkWriteSeed controls single-entry materialization after a successful
	// SetBulkWithGens bulk write only. The default is BulkWriteSeedStrict,
	// which re-enters SetWithGen per key to preserve stricter CAS semantics.
	// It does not affect fallback single seeding when the bulk write is
	// skipped, rejected, or bulk mode is disabled.
	BulkWriteSeed BulkWriteSeedMode
	Hooks         Hooks
}

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

type ReadGuardFunc added in v1.4.1

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 added in v0.3.1

type SelfHealReason string

SelfHealReason classifies why a single entry was deleted on read.

const (
	// single-entry wire envelope was invalid.
	SelfHealReasonCorrupt SelfHealReason = "corrupt"
	// stored generation no longer matched the current generation.
	SelfHealReasonGenMismatch SelfHealReason = "gen_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, isBulk bool, bulkCount int) int64

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.

Jump to

Keyboard shortcuts

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