Documentation
¶
Index ¶
- Variables
- type CacheOption
- func WithErrorBackoff[K comparable, V any, W comparable](d time.Duration) CacheOption[K, V, W]
- func WithFreshnessTTL[K comparable, V any, W comparable](ttl time.Duration) CacheOption[K, V, W]
- func WithMetrics[K comparable, V any, W comparable](m MetricsCollectorI) CacheOption[K, V, W]
- func WithNegativeCaching[K comparable, V any, W comparable](ttl time.Duration) CacheOption[K, V, W]
- func WithStash[K comparable, V any, W comparable](backend StashBackendI[K, V]) CacheOption[K, V, W]
- func WithVersioning[K comparable, V any, W comparable](orderOf func(V) int64) CacheOption[K, V, W]
- type FetchCriteria
- type ItemFetcherI
- type ItemTargetI
- type MapStash
- type MetricsCollectorI
- type ReadThroughCache
- func (inst *ReadThroughCache[K, V, W]) AddItem(k K, v V)
- func (inst *ReadThroughCache[K, V, W]) AddItemIter2(it iter.Seq2[K, V])
- func (inst *ReadThroughCache[K, V, W]) AddItemSlice(k []K, v []V)
- func (inst *ReadThroughCache[K, V, W]) AdvanceEpoch()
- func (inst *ReadThroughCache[K, V, W]) Clear()
- func (inst *ReadThroughCache[K, V, W]) Close() error
- func (inst *ReadThroughCache[K, V, W]) Delete(k K)
- func (inst *ReadThroughCache[K, V, W]) Get(k K) (v V, has bool)
- func (inst *ReadThroughCache[K, V, W]) GetAcceptStale(k K) (v V, has bool, stale bool)
- func (inst *ReadThroughCache[K, V, W]) IterateReadyWorkItems(ctx context.Context) iter.Seq[W]
- func (inst *ReadThroughCache[K, V, W]) IterateRestWorkItems(ctx context.Context) iter.Seq[W]
- func (inst *ReadThroughCache[K, V, W]) Len() int
- func (inst *ReadThroughCache[K, V, W]) MarkAsStale(k K)
- func (inst *ReadThroughCache[K, V, W]) MarkAsStaleIfOlder(k K, ver int64)
- func (inst *ReadThroughCache[K, V, W]) PendingWorkItems() int
- func (inst *ReadThroughCache[K, V, W]) Pin(k K)
- func (inst *ReadThroughCache[K, V, W]) QueuedKeys() int
- func (inst *ReadThroughCache[K, V, W]) SetErrorBackoff(d time.Duration)
- func (inst *ReadThroughCache[K, V, W]) StashLen() int
- func (inst *ReadThroughCache[K, V, W]) Unpin(k K)
- func (inst *ReadThroughCache[K, V, W]) WorkItem(wk W) iter.Seq[functional.NilIteratorValueType]
- type S3FIFOStash
- type SliceStash
- type StashBackendI
- type StashEntry
Constants ¶
This section is empty.
Variables ¶
var PackageProps = packageprops.Props{ WASMWASI: packageprops.WASMCompiles, WASMJS: packageprops.WASMCompiles, WASMFreestanding: packageprops.WASMCompiles, }
PackageProps records this package's curated properties (ADR-0080). Seeded by `boxer code analysis golang wasmsurvey props generate`; curate by hand. The same group's `props verify` reconciles it.
Functions ¶
This section is empty.
Types ¶
type CacheOption ¶
type CacheOption[K comparable, V any, W comparable] func(*ReadThroughCache[K, V, W])
CacheOption defines a functional option for configuring the ReadThroughCache.
func WithErrorBackoff ¶
func WithErrorBackoff[K comparable, V any, W comparable](d time.Duration) CacheOption[K, V, W]
WithErrorBackoff configures the duration a key remains in the Error state (Circuit Breaker open) before a retry is allowed. Default is 5 seconds.
func WithFreshnessTTL ¶ added in v0.0.12
func WithFreshnessTTL[K comparable, V any, W comparable](ttl time.Duration) CacheOption[K, V, W]
WithFreshnessTTL enables age-based staleness onset for stale-while-revalidate: an entry older than ttl (since it was admitted or last confirmed by an equal-version revalidation) reads as stale — strict Get misses and queues a refresh, GetAcceptStale serves it while the refresh is in flight. The age stamp travels through the stash, so demotion does not reset freshness. Zero (the default) disables age staleness; MarkAsStale remains available either way.
func WithMetrics ¶
func WithMetrics[K comparable, V any, W comparable](m MetricsCollectorI) CacheOption[K, V, W]
WithMetrics configures the observability collector. If not provided, a no-op collector is used.
func WithNegativeCaching ¶ added in v0.0.12
func WithNegativeCaching[K comparable, V any, W comparable](ttl time.Duration) CacheOption[K, V, W]
WithNegativeCaching enables absent-key marking: after a clean fetch, requested keys the fetcher did not deliver are treated as absent upstream for ttl. A Get on an absent-marked key misses without queueing a fetch and without suspending the current work item, so replay loops over keys that do not exist terminate instead of re-fetching forever.
Disabled by default (ttl <= 0 keeps it off): misses on absent keys then re-queue on every discovery pass, and distinguishing "absent" from "not fetched yet" is the caller's job.
func WithStash ¶
func WithStash[K comparable, V any, W comparable](backend StashBackendI[K, V]) CacheOption[K, V, W]
WithStash configures a custom L2/Victim cache backend. If not provided, the cache defaults to a memory-dense SliceStash with 50% of the L1 capacity.
func WithVersioning ¶ added in v0.0.12
func WithVersioning[K comparable, V any, W comparable](orderOf func(V) int64) CacheOption[K, V, W]
WithVersioning makes admission version-gated: every value carries a monotonic order extracted by orderOf (the version is intrinsic to the value, so it can never be mismatched against it), and an insert for a cached key is admitted only by the three-outcome rule:
- newer than the cached copy → replace, mark fresh;
- equal to the cached copy → keep, clear staleness, restart the freshness clock (a revalidation confirmed currency);
- older than the cached copy → reject (a raced fetch returning a pre-write row bounces off instead of resurrecting it).
The order type is int64, chosen deliberately over uint64: it must be order-isomorphic with the source's ordering column (recordstore's Order is a signed DateTime64 — `ent.Order.UnixNano()` is the canonical orderOf). A uint64 conversion of a pre-epoch or zero time would wrap to a huge value and permanently poison the key's admission; the same input as int64 orders as "very old" and is rejected — exactly the verdict the source's own ORDER BY would give that row. Negative orders are fine: admission compares only against entries that exist, so no sentinel value is reserved.
Without this option the cache uses an internal monotonic counter, which is exactly last-insert-wins (the pre-versioning semantics).
Write-through consumers pair the gate with Pin/Unpin: the gate alone cannot protect a written-but-unflushed version that gets EVICTED (a refetch then compares against nothing) — see the dirty-pin counterfactual in verification/formal/caching.
type FetchCriteria ¶
type FetchCriteria struct {
// MinWorkItems is the minimum number of distinct pending work items
// before IterateReadyWorkItems will flush. Zero disables.
MinWorkItems int
// MaxWorkItems forces a synchronous flush from inside Get once at least
// this many distinct work items are pending. Zero disables.
MaxWorkItems int
// MinKeys is the minimum number of queued keys before
// IterateReadyWorkItems will flush. Zero disables.
MinKeys int
// MaxKeys forces a synchronous flush from inside Get once at least this
// many keys are queued. Zero disables.
MaxKeys int
// MinPartitions is the minimum number of distinct partitions present in
// the queue before IterateReadyWorkItems will flush. Zero disables.
MinPartitions int
// MaxPartitions forces a synchronous flush from inside Get once at
// least this many distinct partitions are queued. Zero disables.
MaxPartitions int
}
FetchCriteria controls when a queued batch is flushed to the fetcher.
All Min and Max thresholds are evaluated independently and **OR'd**: any single threshold being reached triggers a fetch. Max thresholds fire synchronously from inside Get / GetAcceptStale on every queueing path (cold miss and stale refresh alike), so a single oversized work item still gets chunked. Min thresholds are checked by IterateReadyWorkItems and require a follow-up call; when nothing is queued but work items are pending (their keys already flushed synchronously), IterateReadyWorkItems replays them without fetching. IterateRestWorkItems always flushes regardless of criteria.
A zero value on a threshold disables it. If all three Min fields are zero, IterateReadyWorkItems treats any non-empty queue as ready.
type ItemFetcherI ¶
type ItemFetcherI[K comparable, V any] interface { DeterminePartition(key K) uint64 FetchItemSinglePartition(ctx context.Context, partition uint64, keys []K, target ItemTargetI[K, V]) error }
ItemFetcherI retrieves values from the backing source (L3) in partition-grouped batches.
Contract:
- keys never contains duplicates; every key was queued by a miss (or a stale refresh) since the previous flush.
- The fetcher may retain the keys slice after returning; the cache does not reuse it.
- The fetcher may call target.AddItem for some keys and then return an error for the rest: delivered keys keep their fresh values, only the undelivered ones enter the circuit-breaker backoff.
- Keys the fetcher does not deliver on a nil-error return are treated as absent upstream (recorded only when negative caching is enabled).
- DeterminePartition is evaluated once, at queue time — a key's partition is frozen until it is fetched.
- The fetcher may read the cache (Get/GetAcceptStale) re-entrantly; misses queued during a flush are fetched on the NEXT flush (a nested flush is a no-op). It must not call Iterate*WorkItems or Clear.
type ItemTargetI ¶
type ItemTargetI[K comparable, V any] interface { AddItem(k K, v V) }
ItemTargetI is the sink a fetcher delivers values into.
type MapStash ¶
type MapStash[K comparable, V any] struct { // contains filtered or unexported fields }
func NewMapStash ¶
func NewMapStash[K comparable, V any](capacity int) *MapStash[K, V]
func (*MapStash[K, V]) Add ¶
func (s *MapStash[K, V]) Add(key K, e StashEntry[V]) bool
func (*MapStash[K, V]) GetAndRemove ¶
func (s *MapStash[K, V]) GetAndRemove(key K) (e StashEntry[V], found bool)
type MetricsCollectorI ¶
type MetricsCollectorI interface {
RecordHit(l1 bool, stale bool) // See interface doc for semantics.
RecordMiss() // Item not found, fetch queued (or suppressed)
RecordFetchError(count int) // Number of keys failed
RecordEviction(toStash bool) // See interface doc for semantics.
RecordFetchDuration(d time.Duration) // Time taken by fetcher
}
MetricsCollectorI defines the observability hooks.
RecordHit semantics:
- l1: true for a Primary (L1) hit, false for a Stash (L2) hit.
- stale: true when the served value was marked stale (only possible via GetAcceptStale; strict Get treats stale as a miss).
RecordEviction semantics:
- toStash=true: an L1 item was demoted to L2 (preserved, no data loss).
- toStash=false: an item was dropped from the cache entirely (data loss), either because the stash overflowed during an L1 demotion or because a freshly fetched value was spilled directly to a full stash.
Both can fire from a single operation if an L1 demotion finds the stash full: one (true) for the demoted L1 item, one (false) for the displaced stash item.
type ReadThroughCache ¶
type ReadThroughCache[K comparable, V any, W comparable] struct { // contains filtered or unexported fields }
ReadThroughCache is a single-goroutine, batching, read-through cache with work-item suspend/replay bookkeeping (see the package README).
func NewReadThroughCache ¶
func NewReadThroughCache[K comparable, V any, W comparable]( capacity int, fetcher ItemFetcherI[K, V], criteria FetchCriteria, opts ...CacheOption[K, V, W], ) *ReadThroughCache[K, V, W]
NewReadThroughCache creates a new dependency-aware batching cache.
Parameters:
- capacity: The maximum number of items in the Primary (L1) store. Must be >= 1.
- fetcher: The implementation for partition-aware data retrieval. Must not be nil.
- criteria: The batching thresholds (Min/Max keys, partitions, etc.).
- opts: Optional configuration (WithStash, WithMetrics, WithErrorBackoff, WithNegativeCaching).
func (*ReadThroughCache[K, V, W]) AddItem ¶
func (inst *ReadThroughCache[K, V, W]) AddItem(k K, v V)
AddItem inserts a value — a fetch delivery or a write-through population. Under WithVersioning admission is gated by the value's intrinsic order (see the option doc); without it, last insert wins. Either way the delivery clears the key's breaker and absent marks.
func (*ReadThroughCache[K, V, W]) AddItemIter2 ¶
func (inst *ReadThroughCache[K, V, W]) AddItemIter2(it iter.Seq2[K, V])
func (*ReadThroughCache[K, V, W]) AddItemSlice ¶
func (inst *ReadThroughCache[K, V, W]) AddItemSlice(k []K, v []V)
AddItemSlice inserts parallel key/value slices; the lengths must match.
func (*ReadThroughCache[K, V, W]) AdvanceEpoch ¶
func (inst *ReadThroughCache[K, V, W]) AdvanceEpoch()
func (*ReadThroughCache[K, V, W]) Clear ¶ added in v0.0.12
func (inst *ReadThroughCache[K, V, W]) Clear()
Clear drops every cached entry and all in-flight bookkeeping (queued keys, pending work items, breaker and absent marks). Call it between frames, not with suspended work items.
func (*ReadThroughCache[K, V, W]) Close ¶ added in v0.0.12
func (inst *ReadThroughCache[K, V, W]) Close() error
Close releases resources held by the stash (disk-backed stashes hold file locks); in-memory stashes make it a no-op. The cache must not be used afterwards.
func (*ReadThroughCache[K, V, W]) Delete ¶
func (inst *ReadThroughCache[K, V, W]) Delete(k K)
Delete removes the key from both tiers, clears its breaker and absent marks, and dequeues any pending fetch for it — a deleted key must not be resurrected by an in-flight batch.
func (*ReadThroughCache[K, V, W]) Get ¶
func (inst *ReadThroughCache[K, V, W]) Get(k K) (v V, has bool)
Get retrieves an item. Lookup order: L1 → L2 → miss. On miss the key is queued for the next batch fetch and the current work item (if any) is marked pending. A stale entry is a miss (the refresh is queued); a key inside the circuit-breaker or negative-cache window misses without queueing.
func (*ReadThroughCache[K, V, W]) GetAcceptStale ¶
func (inst *ReadThroughCache[K, V, W]) GetAcceptStale(k K) (v V, has bool, stale bool)
GetAcceptStale retrieves an item, allowing stale data (soft hit). A stale entry is served with stale=true while its refresh queues in the background — including while the circuit breaker holds the refresh back.
func (*ReadThroughCache[K, V, W]) IterateReadyWorkItems ¶
func (inst *ReadThroughCache[K, V, W]) IterateReadyWorkItems(ctx context.Context) iter.Seq[W]
IterateReadyWorkItems yields work items that are ready for replay. With a non-empty queue this means the fetch criteria are met (the flush happens first); with an empty queue any pending items are replayed directly — their keys were already flushed synchronously by a Max threshold. (Items pending on an open circuit breaker replay too and re-suspend; that is bounded by the backoff window.)
func (*ReadThroughCache[K, V, W]) IterateRestWorkItems ¶
func (inst *ReadThroughCache[K, V, W]) IterateRestWorkItems(ctx context.Context) iter.Seq[W]
IterateRestWorkItems forces a fetch of all pending keys and yields remaining work.
func (*ReadThroughCache[K, V, W]) Len ¶ added in v0.0.12
func (inst *ReadThroughCache[K, V, W]) Len() int
Len returns the number of entries resident in the primary (L1) store.
func (*ReadThroughCache[K, V, W]) MarkAsStale ¶
func (inst *ReadThroughCache[K, V, W]) MarkAsStale(k K)
MarkAsStale flags a cached entry as stale in whichever tier it resides: the next strict Get misses and queues a refresh, while GetAcceptStale keeps serving the old value until fresh data lands. Unknown keys are a no-op.
func (*ReadThroughCache[K, V, W]) MarkAsStaleIfOlder ¶ added in v0.0.12
func (inst *ReadThroughCache[K, V, W]) MarkAsStaleIfOlder(k K, ver int64)
MarkAsStaleIfOlder is the version-carrying external-writer signal: it stales the cached entry only when its order is below ver, so a redundant signal for data the cache already holds is free. Without WithVersioning the cached orders are internal counters, incomparable to the caller's domain, and the signal degrades to an unconditional MarkAsStale.
func (*ReadThroughCache[K, V, W]) PendingWorkItems ¶ added in v0.0.12
func (inst *ReadThroughCache[K, V, W]) PendingWorkItems() int
PendingWorkItems returns the number of work items suspended on misses.
func (*ReadThroughCache[K, V, W]) Pin ¶ added in v0.0.12
func (inst *ReadThroughCache[K, V, W]) Pin(k K)
Pin makes the key's entry immune to eviction and demotion until Unpin — the write-through dirty-window latch: the writer pins at Commit and unpins at Flush, so a written-but-unflushed version can never be evicted and then shadowed by an older durable refetch (see the nopin counterfactual in verification/formal/caching). A stash-resident entry is hoisted into L1; pinned entries may hold L1 beyond its capacity, bounded by the caller's flush cadence. Note that epoch pinning compounds with this: while every resident entry is read within the current epoch, the overshoot cannot drain — an AdvanceEpoch cadence is what lets insert pressure bring L1 back to capacity. Pinning an uncached key is a no-op. Idempotent.
Delete and Clear remove pinned entries too: explicit invalidation overrides the latch. Do not invalidate keys with unflushed local writes.
func (*ReadThroughCache[K, V, W]) QueuedKeys ¶ added in v0.0.12
func (inst *ReadThroughCache[K, V, W]) QueuedKeys() int
QueuedKeys returns the number of keys queued for the next batch fetch.
func (*ReadThroughCache[K, V, W]) SetErrorBackoff ¶
func (inst *ReadThroughCache[K, V, W]) SetErrorBackoff(d time.Duration)
SetErrorBackoff adjusts the circuit-breaker window at runtime; it is the mutable twin of WithErrorBackoff (kept deliberately — useful for tests and live tuning).
func (*ReadThroughCache[K, V, W]) StashLen ¶ added in v0.0.12
func (inst *ReadThroughCache[K, V, W]) StashLen() int
StashLen returns the number of entries resident in the stash (L2).
func (*ReadThroughCache[K, V, W]) Unpin ¶ added in v0.0.12
func (inst *ReadThroughCache[K, V, W]) Unpin(k K)
Unpin releases a Pin; the entry becomes ordinarily evictable again. No-op for unpinned or uncached keys. Idempotent.
func (*ReadThroughCache[K, V, W]) WorkItem ¶
func (inst *ReadThroughCache[K, V, W]) WorkItem(wk W) iter.Seq[functional.NilIteratorValueType]
WorkItem marks wk as the active work item for the duration of the loop body; misses inside it register wk as pending. The previous context is restored on exit — including early break and panic — so nesting is safe.
type S3FIFOStash ¶ added in v0.0.12
type S3FIFOStash[K comparable, V any] struct { // contains filtered or unexported fields }
func NewS3FIFOStash ¶ added in v0.0.12
func NewS3FIFOStash[K comparable, V any](capacity int) *S3FIFOStash[K, V]
func (*S3FIFOStash[K, V]) Add ¶ added in v0.0.12
func (s *S3FIFOStash[K, V]) Add(key K, e StashEntry[V]) bool
func (*S3FIFOStash[K, V]) Cap ¶ added in v0.0.12
func (s *S3FIFOStash[K, V]) Cap() int
func (*S3FIFOStash[K, V]) Clear ¶ added in v0.0.12
func (s *S3FIFOStash[K, V]) Clear()
func (*S3FIFOStash[K, V]) Delete ¶ added in v0.0.12
func (s *S3FIFOStash[K, V]) Delete(key K)
func (*S3FIFOStash[K, V]) GetAndRemove ¶ added in v0.0.12
func (s *S3FIFOStash[K, V]) GetAndRemove(key K) (e StashEntry[V], found bool)
func (*S3FIFOStash[K, V]) Len ¶ added in v0.0.12
func (s *S3FIFOStash[K, V]) Len() int
type SliceStash ¶
type SliceStash[K comparable, V any] struct { // contains filtered or unexported fields }
func NewSliceStash ¶
func NewSliceStash[K comparable, V any](capacity int) *SliceStash[K, V]
func (*SliceStash[K, V]) Add ¶
func (s *SliceStash[K, V]) Add(key K, e StashEntry[V]) bool
func (*SliceStash[K, V]) Cap ¶
func (s *SliceStash[K, V]) Cap() int
func (*SliceStash[K, V]) Clear ¶ added in v0.0.12
func (s *SliceStash[K, V]) Clear()
func (*SliceStash[K, V]) Delete ¶
func (s *SliceStash[K, V]) Delete(key K)
func (*SliceStash[K, V]) GetAndRemove ¶
func (s *SliceStash[K, V]) GetAndRemove(key K) (e StashEntry[V], found bool)
func (*SliceStash[K, V]) Len ¶
func (s *SliceStash[K, V]) Len() int
type StashBackendI ¶
type StashBackendI[K comparable, V any] interface { // GetAndRemove attempts to retrieve an entry. // If found, the item MUST be removed from the stash (atomic promote). GetAndRemove(key K) (e StashEntry[V], found bool) // Add inserts an entry into the stash, carrying its state intact. // An existing key MUST be updated in place — never duplicated — and an // update MUST NOT evict. If the stash is full and the key is new, the // implementation MUST evict an item to make room. // Returns: // evicted: true if a valid item was dropped to make space (Data Loss). Add(key K, e StashEntry[V]) (evicted bool) // Delete removes the item if it exists (invalidation). Delete(key K) // Len returns the current number of items. Len() int // Cap returns the maximum capacity (0 = unbounded, disk-backed only). Cap() int // Clear removes every item. Clear() }
StashBackendI acts as the L2/Victim cache storage. Implementations handle their own storage layout, eviction policy, and capacity management.
The interface is infallible: disk-backed implementations must degrade a storage or codec error into a miss (GetAndRemove) or a silent no-op (Add, Delete) — the stash is best-effort by contract, the fetcher remains the source of truth.
type StashEntry ¶ added in v0.0.12
type StashEntry[V any] struct { Value V // Ver is the entry's monotonic order (see WithVersioning); 0 in // internal-counter mode is never compared across restarts. Ver int64 // Stamp is the freshness stamp in nanoseconds on the cache clock // (see WithFreshnessTTL). Stamp int64 Stale bool }
StashEntry is the record an entry carries through the L2 tier. Entry state — the stale flag, the monotonic version, the freshness stamp — must survive demotion and promotion intact (the tier-boundary law: a state bit that does not travel with the entry is silently laundered at the first demotion; see the formal specs under verification/formal/caching).
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package stashtest is a conformance suite for caching.StashBackendI implementations.
|
Package stashtest is a conformance suite for caching.StashBackendI implementations. |