cache

package
v0.12.0 Latest Latest
Warning

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

Go to latest
Published: Nov 3, 2026 License: MIT Imports: 4 Imported by: 0

README

cache

import "github.com/kitsunium/sdk/pkg/v1/data/cache"

Package cache is the public facade for the SDK's caching, and it offers two surfaces because there are two different jobs.

The primitive: Cache[K,V]

Cache is a generic LRU + TTL map — construct it, Set, Fetch, done. No port, no error surface, no vocabulary. Reach for it when you want a bounded map with expiry inside one component.

c := cache.New[string, int](cache.Config[string, int]{MaxEntries: 1024, DefaultTTL: time.Minute})
c.Set("answer", 42)
v, ok := c.Fetch("answer") // 42, true
The domain: Store[V]

Store is the cache as an INTERFACE, with the three things a primitive deliberately does not have: invalidation by tag, protection against a stampede of concurrent misses, and the ability to put one store in front of another. Reach for it when a cache is part of your architecture rather than an implementation detail — when something else has to be able to invalidate what you cached, or when the thing behind the cache must not be hit N times the instant a hot key expires.

store, err := cache.NewMemory[Profile](cache.MemoryConfig{
    MaxEntries: 10_000,       // refused if zero — a capacity is your decision
    DefaultTTL: time.Minute,
})

loader, _ := store.(cache.Loader[Profile])
profile, err := loader.Load(ctx, key, func(ctx context.Context) (cache.Entry[Profile], error) {
    p, err := db.Profile(ctx, id)
    return cache.Entry[Profile]{Value: p, TTL: time.Minute, Tags: []string{"user:" + id}}, err
})

tagger, _ := store.(cache.Tagger)
removed, err := tagger.InvalidateTag(ctx, "user:"+id) // every entry derived from that user

The two do not compete: the domain store is BUILT on the primitive, and Cache stays exactly what it was.

Fetch, not Get — on both surfaces

A hit promotes the entry in the recency order and moves the counters, so a read MUTATES. Naming it Get would promise a purity neither surface offers, and in the domain store it takes a write lock for precisely that reason.

Stampede protection stops at the process boundary

Loader.Load guarantees ONE fill per key across every goroutine in THIS process. It does not coordinate across replicas. Ten instances of a service each running this still send ten concurrent requests to the origin when a hot key expires — so a fleet's worst-case origin load falls by the concurrency factor within an instance, not to one. Cross-process collapsing needs a shared lease, which is a distributed system and not something a cache can promise. Size the origin against the number of replicas, not against the number of requests.

Leaser.LoadLeased takes that lease, from a lock.Locker the processes share — the lock domain's file locker on one machine. Over a chain whose far tier every process reaches, the origin then sees one fill per key; over a memory store alone, which shares no value, the processes only take turns, one fill at a time.

Nothing across tiers is atomic

NewChain is two independent stores, and there is no transaction between them. Every multi-tier operation has a window in which they disagree; the package chooses which side of that window is safer and says so, rather than implying a consistency it cannot deliver. See internal/service/data/cache's CLAUDE.md for each choice and its reason.

Index

Constants

const NoExpiry time.Duration = -1

NoExpiry is the Entry.TTL that stores an entry with no deadline.

It is deliberately distinct from the zero TTL, which means "use the store's default". A store with a one-minute default holding one entry that must never expire is a real combination, and reading both meanings out of 0 would make the second one unsayable.

Initialised with github.com/kitsunium/sdk/internal/core/data/cache.NoExpiry.

const PolicyLRU Policy = 0

PolicyLRU evicts the least-recently-used entry. It is the zero Policy.

Initialised with github.com/kitsunium/sdk/internal/kernel/collections/cache.PolicyLRU.

const PolicyS3FIFO Policy = 1

PolicyS3FIFO admits new keys into a small probationary queue and keeps the ones hit there in a main one, so a one-time scan does not flush the hot set.

Initialised with github.com/kitsunium/sdk/internal/kernel/collections/cache.PolicyS3FIFO.

Variables

var BackendFailed *errs.Error

BackendFailed is returned when a store could not serve an operation. A MISS is not this: a miss is (zero, false, nil).

Initialised with github.com/kitsunium/sdk/internal/core/data/cache.CacheBackendFailed.

Code 0.2.18.2 · reason CACHE_BACKEND_FAILED · public "The cache backend could not serve the operation"

var ChainMisconfigured *errs.Error

ChainMisconfigured is returned by NewChain for a tier set it cannot honour.

Initialised with github.com/kitsunium/sdk/internal/core/data/cache.CacheChainMisconfigured.

Code 0.3.48.1 · reason CACHE_CHAIN_MISCONFIGURED · public "The cache chain cannot be built from the given tiers"

var EntryRejected *errs.Error

EntryRejected is returned by Set for an entry no store can represent — an empty key, an empty tag, or a negative TTL other than NoExpiry.

Initialised with github.com/kitsunium/sdk/internal/core/data/cache.CacheEntryRejected.

Code 0.2.18.4 · reason CACHE_ENTRY_REJECTED · public "The cache entry is not storable as given"

var FillFailed *errs.Error

FillFailed is returned by Load when your fill fails and its error carries no SDK code of its own. Nothing is stored: caching a failure turns one bad minute at the origin into a full TTL of confident wrong answers.

Initialised with github.com/kitsunium/sdk/internal/core/data/cache.CacheFillFailed.

Code 0.2.18.3 · reason CACHE_FILL_FAILED · public "The cache fill function failed and nothing was stored"

var LeaseFailed *errs.Error

LeaseFailed is returned by LoadLeased when its locker could not answer: the lease on the key could be neither taken nor given up.

Initialised with github.com/kitsunium/sdk/internal/core/data/cache.CacheLeaseFailed.

Code 0.3.48.3 · reason CACHE_LEASE_FAILED · public "The cache could not coordinate the fill with other processes"

var Misconfigured *errs.Error

Misconfigured is returned by a store constructor whose configuration it cannot honour — a non-positive capacity, a negative default TTL.

Initialised with github.com/kitsunium/sdk/internal/core/data/cache.CacheMisconfigured.

Code 0.2.18.1 · reason CACHE_MISCONFIGURED · public "The cache cannot be built from the given configuration"

var TierFailed *errs.Error

TierFailed is returned when a chained operation failed inside one tier. Its fields name the position, because a chain's caller holds one Store and would otherwise have no way to tell which backend broke.

Initialised with github.com/kitsunium/sdk/internal/core/data/cache.CacheTierFailed.

Code 0.3.48.2 · reason CACHE_TIER_FAILED · public "A tier of the cache chain could not serve the operation"

func Absent

func Absent(cause error, ttl time.Duration) error

Absent marks a fill's failure as an answer to cache: the key does not exist at the origin. A store's Load keeps a negative entry for ttl and returns the same error — errors.Is still finds cause — without calling the fill again. A ttl that is not positive caches nothing.

func AbsentTTLOf

func AbsentTTLOf(err error) (ttl time.Duration, marked bool)

AbsentTTLOf returns the lifetime Absent put on err, and whether it put one.

func New

func New[K comparable, V any](cfg Config[K, V]) *Cache[K, V]

New builds a Cache from cfg. A nil cfg.Clock defaults to the system clock.

func NewChain

func NewChain[V any](cfg ChainConfig, tiers ...Store[V]) (store Store[V], err error)

NewChain puts each store in front of the next: tiers[0] is consulted first, the last is the authority. A hit in a far tier is promoted into every nearer one, carrying its REMAINING TTL and its tags.

Every tier must implement EntryFetcher and Tagger, checked here and refused rather than discovered per call: a value promoted without its tags is one Tagger can no longer reach, so an invalidation would report success while a nearer tier kept serving the entry.

func NewMemory

func NewMemory[V any](cfg MemoryConfig) (store Store[V], err error)

NewMemory builds an in-process store: an LRU with TTL, a tag index, and stampede protection on Load.

The returned Store also implements EntryFetcher, Tagger and Loader.

A non-positive MaxEntries is REFUSED, not defaulted. A capacity is the whole statement of how much memory you are willing to spend, so any number chosen for you would be arbitrary — and the value a zero would naturally mean, unbounded, is the dangerous one.

func NewStriped

func NewStriped[K comparable, V any](cfg Config[K, V], stripes int, hash func(key K) uint64) *Striped[K, V]

NewStriped builds a Striped of the given number of stripes (below 1 is 1) from cfg, whose bounds it divides between them. A nil hash uses hash/maphash over a seed of its own.

func NewWeighedMemory

func NewWeighedMemory[V any](cfg MemoryConfig, weigh func(value V) int64) (store Store[V], err error)

NewWeighedMemory builds an in-process store bounded by MaxBytes as well as MaxEntries, each value measured by weigh when it is stored. An entry heavier than a stripe's share of MaxBytes is refused at Set.

type BulkFetcher

BulkFetcher is the sibling that reads many keys in one call — one lock per stripe, one pass per tier.

type BulkFetcher[V any] = corecache.BulkFetcher[V]

BulkFetcher is an alias of github.com/kitsunium/sdk/internal/core/data/cache.BulkFetcher, declared as:

type BulkFetcher[V any] interface {
	FetchMany(ctx context.Context, keys []string) (map[string]V, error)
}

func (BulkFetcher) FetchMany
func (BulkFetcher) FetchMany(ctx context.Context, keys []string) (map[string]V, error)

FetchMany returns the values of the keys the store holds, and no entry for a key it does not. It mutates the store as Fetch does. Duplicate keys are read once.

type BulkSetter

BulkSetter is the sibling that stores many entries in one call, validating every entry before storing any.

type BulkSetter[V any] = corecache.BulkSetter[V]

BulkSetter is an alias of github.com/kitsunium/sdk/internal/core/data/cache.BulkSetter, declared as:

type BulkSetter[V any] interface {
	SetMany(ctx context.Context, entries map[string]EntryValue[V]) error
}

func (BulkSetter) SetMany
func (BulkSetter) SetMany(ctx context.Context, entries map[string]EntryValue[V]) error

SetMany stores every entry under its key. Every entry is validated before any is stored: one a store refuses — an empty key or tag, a TTL Set would refuse — fails the call with [CacheEntryRejected] and stores nothing.

type Cache

Cache is the public alias for the generic LRU+TTL primitive.

type Cache[K comparable, V any] = kcache.Cache[K, V]

Cache is an alias of github.com/kitsunium/sdk/internal/kernel/collections/cache.Cache, declared as:

type Cache[K comparable, V any] struct{}

func (Cache) Delete
func (*Cache) Delete(key K)

Delete removes key if present. An explicit Delete does NOT fire OnEvict (that hook is for capacity/expiry removals only).

func (Cache) Expire
func (*Cache) Expire() int

Expire removes every entry whose deadline has passed and reports how many it removed. Each counts as an eviction and reaches OnEvict after the lock is released, as a lazy expiry does. The cache starts no goroutine: the caller decides when, and how often, to call it — on a ticker of its own, or on its scheduler. It costs O(k log n) for k expired entries; an entry without a deadline costs nothing, and a cache without any reads no clock.

func (Cache) Fetch
func (*Cache) Fetch(key K) (value V, ok bool)

Fetch returns the value for key and whether it was present and unexpired. A hit promotes the entry to most-recently-used (hence Fetch, not a pure Get).

func (Cache) Keys
func (*Cache) Keys() iter.Seq[K]

Keys yields the keys present and unexpired when the loop starts, in no particular order. They are copied under the read lock and yielded after it is released, so the loop's body may call the cache — Set, Delete, Fetch — without deadlocking; what the body changes is not seen by the loop. A walk is not a read: no entry is promoted and no counter moves. Each loop allocates one slice of the keys.

func (Cache) Len
func (*Cache) Len() int

Len returns the current live entry count (including not-yet-reaped expired entries — expiry is lazy unless Expire runs).

func (Cache) Purge
func (*Cache) Purge()

Purge drops every entry without firing OnEvict.

func (Cache) Set
func (*Cache) Set(key K, val V)

Set stores key=val with the cache's default TTL.

func (Cache) SetTTL
func (*Cache) SetTTL(key K, val V, ttl time.Duration)

SetTTL stores key=val with an explicit ttl (0 = no expiry), overriding the default for this entry only.

func (Cache) Stats
func (*Cache) Stats() StatsValue

Stats returns a point-in-time copy of the hit/miss/eviction counters.

type ChainConfig

ChainConfig parameterises NewChain.

type ChainConfig = svccache.ChainConfig

ChainConfig is an alias of github.com/kitsunium/sdk/internal/service/data/cache.ChainConfig, declared as:

type ChainConfig struct {
	// OnPromoteError, if non-nil, is called when a value served from a FAR
	// tier could not be written into a nearer one.
	//
	// A failed promotion is not a failed read: the caller already has the
	// value, and failing the call would turn a degraded cache into a degraded
	// service. But it is also not nothing — a near tier that rejects every
	// promotion is a tier that will never be used again, and with this hook nil
	// that is completely invisible. Wire it.
	OnPromoteError func(key string, err error)
	// Clock is what LoadLeased waits on between two reads while another
	// process holds a key's lease. nil means clock.System. Nothing else in
	// the chain reads time: every tier keeps its own clock.
	Clock          clock.Timed
	// LeasePoll is how often LoadLeased reads the chain again while another
	// process holds a key's lease. 0 means 25ms, the file locker's own poll;
	// negative is refused.
	LeasePoll      time.Duration
}

type Config

Config is the public alias for the primitive's constructor configuration.

type Config[K comparable, V any] = kcache.Config[K, V]

Config is an alias of github.com/kitsunium/sdk/internal/kernel/collections/cache.Config, declared as:

type Config[K comparable, V any] struct {
	// MaxEntries caps the live entry count (LRU eviction above it). 0 = unbounded.
	MaxEntries int
	// DefaultTTL applies to Set entries (SetTTL overrides per-entry). 0 = no expiry.
	DefaultTTL time.Duration
	// Clock is the time source (injectable for tests). nil = clock.System.
	Clock      clock.Clock
	// OnEvict, if non-nil, is called with the key+value of every entry removed by
	// capacity eviction or expiry (not by an explicit Delete/Set-overwrite).
	OnEvict    func(key K, val V)
	// MaxWeight caps the summed weight of the live entries, evicting above it as
	// MaxEntries does; both apply when both are set. 0 = unbounded. An entry
	// heavier than MaxWeight alone is stored and evicts every other entry: the
	// primitive has no error to refuse it with.
	MaxWeight  int64
	// WeightOf, if non-nil, weighs an entry when it is stored; a negative answer
	// counts as 0, and one above math.MaxInt32 as math.MaxInt32. nil = every
	// entry weighs 1, so MaxWeight is then a count.
	WeightOf   func(key K, val V) int64
	// Policy chooses the victim when the cache is full. The zero value is
	// PolicyLRU.
	Policy     Policy
}

type Entry

Entry is what a caller stores: the value, its lifetime, and the tags by which it can later be invalidated in bulk.

type Entry[V any] = corecache.EntryValue[V]

Entry is an alias of github.com/kitsunium/sdk/internal/core/data/cache.EntryValue, declared as:

type Entry[V any] struct {
	// Value is the payload.
	Value V
	// TTL is how long the entry stays fetchable.
	//
	//   > 0        this exact lifetime
	//   0          the store's configured default
	//   [NoExpiry] no deadline at all
	//
	// Any other negative value is a mistake, not a third meaning, and is
	// refused with [CacheEntryRejected] rather than reinterpreted.
	TTL   time.Duration
	// Tags label the entry for bulk invalidation. Order is not significant and
	// duplicates are collapsed. An empty tag is refused: it names a set nobody
	// can ask for, so accepting it would index the entry under a key that can
	// only ever be reached by accident.
	//
	// Tags cost memory in the store's reverse index — see
	// internal/service/data/cache/CLAUDE.md, which states the per-pair cost and
	// measures it.
	Tags  []string
}

type EntryFetcher

EntryFetcher is the sibling that returns the whole entry — value, REMAINING TTL, tags — rather than only the value. Both stores built here implement it.

type EntryFetcher[V any] = corecache.EntryFetcher[V]

EntryFetcher is an alias of github.com/kitsunium/sdk/internal/core/data/cache.EntryFetcher, declared as:

type EntryFetcher[V any] interface {
	FetchEntry(ctx context.Context, key string) (EntryValue[V], bool, error)
}

func (EntryFetcher) FetchEntry
func (EntryFetcher) FetchEntry(ctx context.Context, key string) (EntryValue[V], bool, error)

FetchEntry is Store.Fetch returning the stored entry — value, remaining TTL and tags. It mutates the store for the same reasons Fetch does.

The TTL it reports is the REMAINING time, not the one originally supplied: promoting an entry with its original TTL would extend its life every time it moved between tiers.

type Expirer

Expirer is the sibling that removes a store's expired entries on demand. Expiry is otherwise lazy; the store starts no goroutine, so call Expire on your own ticker or scheduler.

type Expirer = corecache.Expirer

Expirer is an alias of github.com/kitsunium/sdk/internal/core/data/cache.Expirer, declared as:

type Expirer interface {
	// Expire removes every entry whose deadline has passed and reports how many
	// it removed. A chain sweeps every tier that can, far to near, and reports
	// the total of removals, as InvalidateTag counts them.
	Expire(ctx context.Context) (removed int, err error)
}

func (Expirer) Expire
func (Expirer) Expire(ctx context.Context) (removed int, err error)

Expire removes every entry whose deadline has passed and reports how many it removed. A chain sweeps every tier that can, far to near, and reports the total of removals, as InvalidateTag counts them.

type Fill

Fill computes the entry for a key the cache does not hold: a query, a request, a derivation. It is your code.

type Fill[V any] = corecache.Fill[V]

Fill is an alias of github.com/kitsunium/sdk/internal/core/data/cache.Fill, declared as:

type Fill[V any] func(ctx context.Context) (EntryValue[V], error)

type Leaser

Leaser is the sibling that carries stampede protection across processes: on a miss it takes a lease named after the key from a lock.Locker the processes share, so one of them fills while the others wait for its value. A memory store alone shares no value: there the lease only takes turns, one fill at a time.

type Leaser[V any] = corecache.Leaser[V]

Leaser is an alias of github.com/kitsunium/sdk/internal/core/data/cache.Leaser, declared as:

type Leaser[V any] interface {
	LoadLeased(ctx context.Context, key string, fill Fill[V], locker lock.Locker) (V, error)
}

func (Leaser) LoadLeased
func (Leaser) LoadLeased(ctx context.Context, key string, fill Fill[V], locker lock.Locker) (V, error)

LoadLeased is Load with a lease named "cache:" + key taken from locker on a miss, after the in-process collapse. The holder reads the store again, fills, stores and releases. A process that finds the lease held fills nothing: it reads the store again every poll step of the store's clock until the holder's value lands or its own ctx ends — or until the lease comes free without a value (the holder failed, died, or let a lease that can expire lapse), when it takes the lease and reads once more before filling. A nil locker makes it Load. A locker that cannot answer fails the call with [CacheLeaseFailed]. A lease that can expire is not renewed during the fill: its lifetime must outlast the fill.

type Loader

Loader is the sibling that fills a miss while collapsing concurrent misses on one key into a single fill — within one process, and not across replicas. See the package documentation before sizing an origin against it.

type Loader[V any] = corecache.Loader[V]

Loader is an alias of github.com/kitsunium/sdk/internal/core/data/cache.Loader, declared as:

type Loader[V any] interface {
	Load(ctx context.Context, key string, fill Fill[V]) (V, error)
}

func (Loader) Load
func (Loader) Load(ctx context.Context, key string, fill Fill[V]) (V, error)

Load returns the value for key, calling fill exactly once across all concurrent callers of this key in this process if the cache does not hold it. A fill that fails stores nothing and returns [CacheFillFailed] — unless the failure already carries an SDK code, in which case it propagates untouched (ADR 0005 §origin wins).

fill does NOT receive ctx. It receives the shared call's context: the first caller's values with every deadline removed, cancelled only once every waiting caller has gone. A caller whose own ctx ends while it waits gets its own ctx.Err() and leaves the fill running for the others.

type MemoryConfig

MemoryConfig parameterises NewMemory. Its zero value is NOT a working cache — MaxEntries must be stated, and is refused rather than defaulted.

type MemoryConfig = svccache.MemoryConfig

MemoryConfig is an alias of github.com/kitsunium/sdk/internal/service/data/cache.MemoryConfig, declared as:

type MemoryConfig struct {
	// MaxEntries caps the live entry count; the least-recently-used entry is
	// evicted above it. It MUST be positive.
	MaxEntries int
	// DefaultTTL applies to every entry whose own TTL is zero. Zero means the
	// default is "no deadline", so entries leave only by eviction, by Delete,
	// or by tag invalidation. Negative is refused.
	DefaultTTL time.Duration
	// Clock is the time source, injectable so TTL is testable. nil means
	// clock.System. Only the reading half of the port is required — a cache
	// reads time (ADR 0039); LoadLeased alone waits, on Clock when it can wait
	// (clock.System, a clock.ManualClock) and on clock.System otherwise. A
	// clock.CoarseClock makes a TTL'd hit's clock read an atomic load, and
	// every expiry late by up to its resolution.
	Clock      clock.Clock
	// Stripes splits the store into independent stripes, each with its own
	// lock, its own share of MaxEntries and MaxBytes and its own tag index, so
	// goroutines working on keys of different stripes never wait for one
	// another. 0 means 1, one store as before; above MaxEntries (or a positive
	// MaxBytes) it is held there, and above 1024 at 1024. Negative is refused.
	Stripes    int
	// MaxBytes bounds the summed weight of the live values, as measured by the
	// weigher handed to [NewWeighedMemory]; entries are evicted above it as
	// above MaxEntries, both bounds applying. 0 means no byte bound. An entry
	// heavier than one stripe's share of it is refused with
	// CACHE_ENTRY_REJECTED at Set. Negative is refused, and so is a positive
	// MaxBytes given to [NewMemory], which has no weigher to measure it with.
	MaxBytes   int64
	// Policy is the eviction policy: least recently used (the zero), or
	// S3-FIFO, which keeps a hot set through a scan.
	Policy     kcache.Policy
	// LeasePoll is how often LoadLeased reads the store again while another
	// process holds a key's lease. 0 means 25ms, the file locker's own poll;
	// negative is refused.
	LeasePoll  time.Duration
}

type Policy

Policy chooses the entry a full cache evicts: PolicyLRU, the zero, or PolicyS3FIFO.

type Policy = kcache.Policy

Policy is an alias of github.com/kitsunium/sdk/internal/kernel/collections/cache.Policy, declared as:

type Policy uint8

type Refresher

Refresher is the sibling that refreshes a hot entry before it expires: the value is served from the cache while one background fill replaces it.

type Refresher[V any] = corecache.Refresher[V]

Refresher is an alias of github.com/kitsunium/sdk/internal/core/data/cache.Refresher, declared as:

type Refresher[V any] interface {
	LoadRefreshing(ctx context.Context, key string, fill Fill[V], before time.Duration) (V, error)
}

func (Refresher) LoadRefreshing
func (Refresher) LoadRefreshing(ctx context.Context, key string, fill Fill[V], before time.Duration) (V, error)

LoadRefreshing is Load, except on a hit whose remaining lifetime is below before: the value is returned at once and ONE background fill per key starts, on the caller's context values without its cancellation, bounded by the entry's remaining lifetime; what it returns replaces the entry, and a failure leaves the entry to expire. A hit outside that window starts nothing. A before that is not positive is refused with [CacheMisconfigured].

type Stats

Stats is the public alias for the primitive's counters snapshot.

type Stats = kcache.StatsValue

Stats is an alias of github.com/kitsunium/sdk/internal/kernel/collections/cache.StatsValue, declared as:

type Stats struct {
	Hits      uint64
	Misses    uint64
	Evictions uint64
	// Weight is the summed weight of the live entries when the cache weighs
	// them — Config.MaxWeight or Config.WeightOf set — and 0 when it does not.
	Weight    int64
	// Policy is the eviction policy that runs: Config.Policy, or PolicyLRU
	// for a value the cache does not know.
	Policy    Policy
}

type Store

Store is the cache port: Fetch, Set, Delete.

The method set is FROZEN at three. New capabilities arrive as SIBLING interfaces you reach by type assertion — EntryFetcher, Tagger and Loader — because this alias publishes the interface, Go interfaces are structural, and a fourth method would break every downstream implementer at compile time with no deprecation window.

type Store[V any] = corecache.Store[V]

Store is an alias of github.com/kitsunium/sdk/internal/core/data/cache.Store, declared as:

type Store[V any] interface {
	Delete(ctx context.Context, key string) error
	Fetch(ctx context.Context, key string) (V, bool, error)
	Set(ctx context.Context, key string, entry EntryValue[V]) error
}

func (Store) Delete
func (Store) Delete(ctx context.Context, key string) error

Delete removes key. It is idempotent: deleting a key that names nothing reports nil, because a caller who wanted it gone has got what they asked for.

func (Store) Fetch
func (Store) Fetch(ctx context.Context, key string) (V, bool, error)

Fetch returns the value stored under key and whether it was present and unexpired. It MUTATES the store — recency, counters, lazy expiry — which is why it is not called Get. A miss is (zero, false, nil), never an error: a cache that does not hold something has done nothing wrong.

func (Store) Set
func (Store) Set(ctx context.Context, key string, entry EntryValue[V]) error

Set stores entry under key, replacing whatever was there. An empty key, an empty tag, or a negative TTL that is not NoExpiry is refused with [CacheEntryRejected] rather than stored.

type Striped

Striped spreads keys over independent [Cache]s, each behind its own lock, dividing the bounds between them.

type Striped[K comparable, V any] = kcache.Striped[K, V]

Striped is an alias of github.com/kitsunium/sdk/internal/kernel/collections/cache.Striped, declared as:

type Striped[K comparable, V any] struct{}

func (Striped) Delete
func (*Striped) Delete(key K)

Delete removes key from its stripe, without firing OnEvict.

func (Striped) Expire
func (*Striped) Expire() int

Expire sweeps every stripe, one lock at a time, and reports how many entries it removed in total.

func (Striped) Fetch
func (*Striped) Fetch(key K) (value V, ok bool)

Fetch returns the value for key and whether it was present and unexpired, from key's stripe, recording the hit as that stripe's policy does.

func (Striped) Keys
func (*Striped) Keys() iter.Seq[K]

Keys yields the keys of every stripe, a stripe at a time, each stripe's copied under its read lock and yielded after it is released, as Cache.Keys does, so the loop's body may call s. The stripes are read one after another, so under concurrent writes the walk is no single instant's.

func (Striped) Len
func (*Striped) Len() int

Len returns the live entry count summed over the stripes. The stripes are read one after another, so under concurrent writes the sum is no single instant's.

func (Striped) Purge
func (*Striped) Purge()

Purge drops every entry of every stripe without firing OnEvict.

func (Striped) Set
func (*Striped) Set(key K, val V)

Set stores key=val in key's stripe with the default TTL.

func (Striped) SetTTL
func (*Striped) SetTTL(key K, val V, ttl time.Duration)

SetTTL stores key=val in key's stripe with an explicit ttl (0 = no expiry).

func (Striped) Stats
func (*Striped) Stats() StatsValue

Stats returns the counters summed over the stripes, read one after another.

func (Striped) Stripes
func (*Striped) Stripes() int

Stripes reports how many stripes s spreads its keys over.

type Tagger

Tagger is the sibling for invalidating every entry carrying a tag.

The cost is proportional to the entries carrying the tag, not to the size of the store — measured in internal/service/data/cache/BENCH.md at two store sizes.

type Tagger = corecache.Tagger

Tagger is an alias of github.com/kitsunium/sdk/internal/core/data/cache.Tagger, declared as:

type Tagger interface {
	// InvalidateTag removes every entry carrying tag and reports how many it
	// removed. A tag that names nothing removes zero and reports nil.
	//
	// The cost is proportional to the number of entries carrying the tag, NOT
	// to the size of the store — see internal/service/data/cache/BENCH.md, which
	// measures it at two store sizes for exactly this claim.
	InvalidateTag(ctx context.Context, tag string) (removed int, err error)
}

func (Tagger) InvalidateTag
func (Tagger) InvalidateTag(ctx context.Context, tag string) (removed int, err error)

InvalidateTag removes every entry carrying tag and reports how many it removed. A tag that names nothing removes zero and reports nil.

The cost is proportional to the number of entries carrying the tag, NOT to the size of the store — see internal/service/data/cache/BENCH.md, which measures it at two store sizes for exactly this claim.

Generated from docs/api by tools/genindex.

Documentation

Overview

Package cache is the public facade for the SDK's caching, and it offers two surfaces because there are two different jobs.

The primitive: Cache[K,V]

Cache is a generic LRU + TTL map — construct it, Set, Fetch, done. No port, no error surface, no vocabulary. Reach for it when you want a bounded map with expiry inside one component.

c := cache.New[string, int](cache.Config[string, int]{MaxEntries: 1024, DefaultTTL: time.Minute})
c.Set("answer", 42)
v, ok := c.Fetch("answer") // 42, true

The domain: Store[V]

Store is the cache as an INTERFACE, with the three things a primitive deliberately does not have: invalidation by tag, protection against a stampede of concurrent misses, and the ability to put one store in front of another. Reach for it when a cache is part of your architecture rather than an implementation detail — when something else has to be able to invalidate what you cached, or when the thing behind the cache must not be hit N times the instant a hot key expires.

store, err := cache.NewMemory[Profile](cache.MemoryConfig{
    MaxEntries: 10_000,       // refused if zero — a capacity is your decision
    DefaultTTL: time.Minute,
})

loader, _ := store.(cache.Loader[Profile])
profile, err := loader.Load(ctx, key, func(ctx context.Context) (cache.Entry[Profile], error) {
    p, err := db.Profile(ctx, id)
    return cache.Entry[Profile]{Value: p, TTL: time.Minute, Tags: []string{"user:" + id}}, err
})

tagger, _ := store.(cache.Tagger)
removed, err := tagger.InvalidateTag(ctx, "user:"+id) // every entry derived from that user

The two do not compete: the domain store is BUILT on the primitive, and Cache stays exactly what it was.

Fetch, not Get — on both surfaces

A hit promotes the entry in the recency order and moves the counters, so a read MUTATES. Naming it Get would promise a purity neither surface offers, and in the domain store it takes a write lock for precisely that reason.

Stampede protection stops at the process boundary

Loader.Load guarantees ONE fill per key across every goroutine in THIS process. It does not coordinate across replicas. Ten instances of a service each running this still send ten concurrent requests to the origin when a hot key expires — so a fleet's worst-case origin load falls by the concurrency factor within an instance, not to one. Cross-process collapsing needs a shared lease, which is a distributed system and not something a cache can promise. Size the origin against the number of replicas, not against the number of requests.

Leaser.LoadLeased takes that lease, from a lock.Locker the processes share — the lock domain's file locker on one machine. Over a chain whose far tier every process reaches, the origin then sees one fill per key; over a memory store alone, which shares no value, the processes only take turns, one fill at a time.

Nothing across tiers is atomic

NewChain is two independent stores, and there is no transaction between them. Every multi-tier operation has a window in which they disagree; the package chooses which side of that window is safer and says so, rather than implying a consistency it cannot deliver. See internal/service/data/cache's CLAUDE.md for each choice and its reason.

Index

Constants

NoExpiry is the Entry.TTL that stores an entry with no deadline.

It is deliberately distinct from the zero TTL, which means "use the store's default". A store with a one-minute default holding one entry that must never expire is a real combination, and reading both meanings out of 0 would make the second one unsayable.

Variables

View Source
var (
	// Misconfigured is returned by a store constructor whose configuration it
	// cannot honour — a non-positive capacity, a negative default TTL.
	Misconfigured = corecache.CacheMisconfigured
	// BackendFailed is returned when a store could not serve an operation. A
	// MISS is not this: a miss is (zero, false, nil).
	BackendFailed = corecache.CacheBackendFailed
	// FillFailed is returned by Load when your fill fails and its error
	// carries no SDK code of its own. Nothing is stored: caching a failure
	// turns one bad minute at the origin into a full TTL of confident wrong
	// answers.
	FillFailed = corecache.CacheFillFailed
	// EntryRejected is returned by Set for an entry no store can represent —
	// an empty key, an empty tag, or a negative TTL other than [NoExpiry].
	EntryRejected = corecache.CacheEntryRejected
	// ChainMisconfigured is returned by [NewChain] for a tier set it cannot
	// honour.
	ChainMisconfigured = corecache.CacheChainMisconfigured
	// TierFailed is returned when a chained operation failed inside one tier.
	// Its fields name the position, because a chain's caller holds one Store
	// and would otherwise have no way to tell which backend broke.
	TierFailed = corecache.CacheTierFailed
	// LeaseFailed is returned by LoadLeased when its locker could not answer:
	// the lease on the key could be neither taken nor given up.
	LeaseFailed = corecache.CacheLeaseFailed
)

Functions

func Absent added in v0.12.0

func Absent(cause error, ttl time.Duration) error

Absent marks a fill's failure as an answer to cache: the key does not exist at the origin. A store's Load keeps a negative entry for ttl and returns the same error — errors.Is still finds cause — without calling the fill again. A ttl that is not positive caches nothing.

func AbsentTTLOf added in v0.12.0

func AbsentTTLOf(err error) (ttl time.Duration, marked bool)

AbsentTTLOf returns the lifetime Absent put on err, and whether it put one.

Types

type BulkFetcher added in v0.12.0

type BulkFetcher[V any] = corecache.BulkFetcher[V]

BulkFetcher is the sibling that reads many keys in one call — one lock per stripe, one pass per tier.

type BulkSetter added in v0.12.0

type BulkSetter[V any] = corecache.BulkSetter[V]

BulkSetter is the sibling that stores many entries in one call, validating every entry before storing any.

type Cache

type Cache[K comparable, V any] = kcache.Cache[K, V]

Cache is the public alias for the generic LRU+TTL primitive.

func New

func New[K comparable, V any](cfg Config[K, V]) *Cache[K, V]

New builds a Cache from cfg. A nil cfg.Clock defaults to the system clock.

type ChainConfig

type ChainConfig = svccache.ChainConfig

ChainConfig parameterises NewChain.

type Config

type Config[K comparable, V any] = kcache.Config[K, V]

Config is the public alias for the primitive's constructor configuration.

type Entry

type Entry[V any] = corecache.EntryValue[V]

Entry is what a caller stores: the value, its lifetime, and the tags by which it can later be invalidated in bulk.

type EntryFetcher

type EntryFetcher[V any] = corecache.EntryFetcher[V]

EntryFetcher is the sibling that returns the whole entry — value, REMAINING TTL, tags — rather than only the value. Both stores built here implement it.

type Expirer added in v0.12.0

type Expirer = corecache.Expirer

Expirer is the sibling that removes a store's expired entries on demand. Expiry is otherwise lazy; the store starts no goroutine, so call Expire on your own ticker or scheduler.

type Fill

type Fill[V any] = corecache.Fill[V]

Fill computes the entry for a key the cache does not hold: a query, a request, a derivation. It is your code.

type Leaser added in v0.12.0

type Leaser[V any] = corecache.Leaser[V]

Leaser is the sibling that carries stampede protection across processes: on a miss it takes a lease named after the key from a lock.Locker the processes share, so one of them fills while the others wait for its value. A memory store alone shares no value: there the lease only takes turns, one fill at a time.

type Loader

type Loader[V any] = corecache.Loader[V]

Loader is the sibling that fills a miss while collapsing concurrent misses on one key into a single fill — within one process, and not across replicas. See the package documentation before sizing an origin against it.

type MemoryConfig

type MemoryConfig = svccache.MemoryConfig

MemoryConfig parameterises NewMemory. Its zero value is NOT a working cache — MaxEntries must be stated, and is refused rather than defaulted.

type Policy added in v0.12.0

type Policy = kcache.Policy

Policy chooses the entry a full cache evicts: PolicyLRU, the zero, or PolicyS3FIFO.

const (
	// PolicyLRU evicts the least-recently-used entry. It is the zero Policy.
	PolicyLRU Policy = kcache.PolicyLRU
	// PolicyS3FIFO admits new keys into a small probationary queue and keeps
	// the ones hit there in a main one, so a one-time scan does not flush the
	// hot set.
	PolicyS3FIFO Policy = kcache.PolicyS3FIFO
)

type Refresher added in v0.12.0

type Refresher[V any] = corecache.Refresher[V]

Refresher is the sibling that refreshes a hot entry before it expires: the value is served from the cache while one background fill replaces it.

type Stats

type Stats = kcache.StatsValue

Stats is the public alias for the primitive's counters snapshot.

type Store

type Store[V any] = corecache.Store[V]

Store is the cache port: Fetch, Set, Delete.

The method set is FROZEN at three. New capabilities arrive as SIBLING interfaces you reach by type assertion — EntryFetcher, Tagger and Loader — because this alias publishes the interface, Go interfaces are structural, and a fourth method would break every downstream implementer at compile time with no deprecation window.

func NewChain

func NewChain[V any](cfg ChainConfig, tiers ...Store[V]) (store Store[V], err error)

NewChain puts each store in front of the next: tiers[0] is consulted first, the last is the authority. A hit in a far tier is promoted into every nearer one, carrying its REMAINING TTL and its tags.

Every tier must implement EntryFetcher and Tagger, checked here and refused rather than discovered per call: a value promoted without its tags is one Tagger can no longer reach, so an invalidation would report success while a nearer tier kept serving the entry.

func NewMemory

func NewMemory[V any](cfg MemoryConfig) (store Store[V], err error)

NewMemory builds an in-process store: an LRU with TTL, a tag index, and stampede protection on Load.

The returned Store also implements EntryFetcher, Tagger and Loader.

A non-positive MaxEntries is REFUSED, not defaulted. A capacity is the whole statement of how much memory you are willing to spend, so any number chosen for you would be arbitrary — and the value a zero would naturally mean, unbounded, is the dangerous one.

func NewWeighedMemory added in v0.12.0

func NewWeighedMemory[V any](cfg MemoryConfig, weigh func(value V) int64) (store Store[V], err error)

NewWeighedMemory builds an in-process store bounded by MaxBytes as well as MaxEntries, each value measured by weigh when it is stored. An entry heavier than a stripe's share of MaxBytes is refused at Set.

type Striped added in v0.12.0

type Striped[K comparable, V any] = kcache.Striped[K, V]

Striped spreads keys over independent [Cache]s, each behind its own lock, dividing the bounds between them.

func NewStriped added in v0.12.0

func NewStriped[K comparable, V any](cfg Config[K, V], stripes int, hash func(key K) uint64) *Striped[K, V]

NewStriped builds a Striped of the given number of stripes (below 1 is 1) from cfg, whose bounds it divides between them. A nil hash uses hash/maphash over a seed of its own.

type Tagger

type Tagger = corecache.Tagger

Tagger is the sibling for invalidating every entry carrying a tag.

The cost is proportional to the entries carrying the tag, not to the size of the store — measured in internal/service/data/cache/BENCH.md at two store sizes.

Jump to

Keyboard shortcuts

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