caching

package
v0.0.21 Latest Latest
Warning

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

Go to latest
Published: Aug 21, 2026 License: MIT Imports: 7 Imported by: 0

README


type: reference audience: caching user status: draft

Status: draft — pre-human-review. Not yet verified against the current documentation standard. Do not cite as authoritative.

caching — read-through dependency cache

1. Overview

The ReadThroughCache is a single-threaded read-through cache that accumulates missing keys across pending work items and fetches them in partition-grouped batches.

Get() calls declare dependencies; the actual fetch happens later, once enough work items have queued up misses. This trades random-access hit rates (the LRU/LFU target) for sequential-call latency.

Target use case: ETL pipelines, build systems, graph traversals, and ML feature engineering — workloads where processing is cheap, I/O is expensive, and the work can be replayed.

2. Architecture

The system is a state machine with three storage tiers and a discovery / suspend / replay execution loop.

2.1 Storage Hierarchy

  1. L1 Primary (RAM): A native Go map optimized for $O(1)$ access. Contains the "Working Set."
  2. L2 Stash (Victim Cache): A secondary buffer (Memory or Disk) for items evicted from L1. It handles "Thrashing" scenarios where the working set exceeds L1 capacity.
  3. L3 Source (Fetcher): The external truth (DB, S3, API). Accessed only via batch interfaces.

2.2 Execution model

Execution proceeds in phases:

  1. Discovery Phase: The user code runs inside a WorkItem iterator. It requests all necessary keys for the current unit of work.
  2. Accumulation:
    • If Get(Key1) returns false (missing), the cache records the dependency but the user code should continue to call Get(Key2), Get(Key3), etc., if possible.
    • This allows the cache to build a complete picture of the work item's requirements.
  3. Suspension: If any dependencies were missing, the user code returns (aborts logic execution), effectively pausing the work item.
  4. Batching: The cache aggregates all missing keys (Key1, Key2, Key3) across all pending work items.
  5. Fetch: The cache groups keys by Partition and performs a bulk fetch.
  6. Replay: The user code is re-run (IterateReadyWorkItems). This time, data is present, and the logic proceeds to completion.

The processItem function must be written to accumulate misses:

Incorrect (Sequential / N+1 Latency)

// BAD: Aborts immediately. Cache only sees "Key A".
// "Key B" will be discovered only after "Key A" is fetched and the loop restarts.
valA, found := cache.Get("KeyA")
if !found { return }

valB, found := cache.Get("KeyB")
if !found { return }

Correct (Batch / O(1) Latency)

// GOOD: Checks all requirements. Cache sees "Key A" AND "Key B".
// They will be fetched in a single batch.
missing := false

valA, foundA := cache.Get("KeyA")
if !foundA { missing = true }

valB, foundB := cache.Get("KeyB")
if !foundB { missing = true }

// Only abort after registering all needs
if missing { return }

// Proceed with logic
Result = valA + valB

3. Implementation details

3.1 Epoch-Based Pinning

To prevent cache thrashing (evicting data needed by the current batch to make room for other data in the same batch), the system uses Epochs.

  • Mechanism: lastSeen timestamp on every item.
  • Invariant: ensureSpace will never evict an item marked with the currentEpoch.
  • Usage: The user calls AdvanceEpoch() between logical batches to unpin old data.
  • Victim selection among unpinned entries follows SIEVE (insertion ring + access bit + a persistent hand; NSDI '24): measured +2pp hit ratio / ~7% fewer upstream fetches over the previous random pick on a Zipf workload with a 200-op epoch cadence, and up to +10-13pp without epoch shielding (caching_policy_study_test.go). Pins are protection, not policy signals — the hand skips them without touching their access bits.

3.2 Bounded Stash (L2)

When the L1 cache is full of "Pinned" items (Working Set > L1 Capacity), items are spilled to the Stash.

  • Interface: StashBackendI[K, V]. Entries carry their stale flag, so staleness survives the L1→L2 round-trip; Add of an existing key updates in place (never duplicates, never evicts).
  • Implementations:
    • SliceStash: CPU-heavy ($O(N)$ scan), Memory-dense. Good for small L2.
    • MapStash: Memory-heavy, CPU-light ($O(1)$). Good for large RAM L2.
    • S3FIFOStash: S3-FIFO-derived (probationary FIFO + ghost readmission of bouncing victims). Measured about even with the others on a Zipf workload (see caching_policy_study_test.go) — worth trying when victim-bounce traffic dominates.
    • PogrebStash/PebbleStash: Disk-backed. Good for datasets exceeding RAM. Best-effort by contract: storage or codec errors degrade into misses or dropped writes, never into failures of the cache itself.
  • Eviction: The Stash implements a round-robin or random eviction policy when full, bounding L2 size.
  • Conformance: the stashtest package runs the contract suite against a backend; custom implementations should wire it into their tests.

3.3 Circuit Breaker

To prevent cascading failures during outages:

  • State: a bounded side table of key → retry-allowed deadlines — failure bookkeeping never occupies value-store slots.
  • Behavior: a missing key inside its backoff window returns false and suppresses the fetch request until the backoff expires. A stale value whose refresh failed stays resident: GetAcceptStale keeps serving it through the outage while strict Get misses — the stale-while-revalidate contract holds exactly when the upstream is down.
  • Safety: The fetcher is wrapped in recover() to prevent panics from crashing the pipeline.

3.4 Versioned admission and write-through (opt-in)

WithVersioning(orderOf) makes every value carry a monotonic order (int64; intrinsic to the value, so it can never be mismatched) and turns admission into a three-outcome gate: newer replaces; equal confirms (staleness clears, the freshness clock restarts — the revalidation outcome); older is rejected, so a raced fetch returning a pre-write row bounces off instead of resurrecting it. int64 over uint64 is deliberate: the order must sort exactly like the source's ordering column (a signed timestamp in recordstore), and a uint64 cast of a pre-epoch or zero time would poison the key; the same input as int64 orders as "very old" and loses, matching the source's own ORDER BY. Without the option an internal counter reproduces last-insert-wins exactly.

Write-through is a consumer pattern on top of the gate plus the dirty-window pin: the writer populates the cache at commit (AddItem + Pin) and releases at flush (Unpin). The pin makes the entry immune to eviction while the write is not yet durable — the gate alone cannot protect an evicted dirty entry, because a refetch then compares against nothing (the machine-checked unsafe_nopin counterfactual under verification/formal/caching/). Pinned entries may hold L1 beyond its capacity, bounded by the writer's flush cadence — and epoch pinning compounds with it: the overshoot drains only when an AdvanceEpoch cadence lets insert pressure evict again. Deletions write through as versioned tombstone values. Discarded (never-durable) writes must be invalidated by the writer (Delete). MarkAsStaleIfOlder(k, ver) is the version-carrying external-writer signal — redundant signals for data the cache already holds are free.

3.5 Freshness TTL (opt-in)

WithFreshnessTTL(ttl) adds age-based staleness onset for stale-while-revalidate: an entry older than ttl (since admission or the last equal-version confirmation) reads as stale — strict Get misses and queues the refresh, GetAcceptStale keeps serving. The age stamp travels through the stash, so a demotion round-trip cannot rejuvenate an entry (the freshness_ttl_unsafe counterfactual). MarkAsStale remains available either way.

3.6 Negative caching (opt-in)

WithNegativeCaching(ttl) records an absent verdict for requested keys a clean fetch did not deliver. Within the TTL a Get on such a key misses without queueing and without suspending the current work item, so flush-until-quiet replay loops terminate instead of re-probing the upstream forever. The verdict is authoritative: any cached remnant of the key is dropped (contrast the breaker, which preserves stale values — a failure is a guess, an absence is an answer). Off by default; without it, absent keys re-probe on every flush and distinguishing "absent" from "not fetched yet" is the caller's job.

3.7 Partition-Aware Fetching

  • Interface: ItemFetcherI.DeterminePartition(key).
  • Optimization: Keys are grouped by partition before the fetch call. This allows for optimal connection pooling (e.g., one DB query per shard).
  • Contract: batches never contain duplicates; a partition is frozen at queue time; a fetcher may AddItem some keys and then return an error — the delivered keys keep their values, only the rest enter the breaker. A fetcher may read the cache re-entrantly (misses queued during a flush are fetched on the next flush; a nested flush is a no-op), but must not call Iterate*WorkItems or Clear.

4. Design Trade-offs

Feature Advantage Trade-off / Constraint
Single Threaded No mutex contention; simple control flow. Must be owned by a single Goroutine. No concurrent access.
Restart Loop Eliminates manual batching complexity. User logic must be idempotent (safe to run multiple times).
Strict Pinning Guarantees progress for large batches. Requires explicit AdvanceEpoch() call, or memory will leak (logic-wise).
Staleness Supports Stale-While-Revalidate. Eventual consistency; user must opt-in via GetAcceptStale.

5. Usage Guidance

5.1 Basic Setup

// 1. Define Fetcher
fetcher := NewMyDbFetcher()

// 2. Configure Cache (L1=10k, L2=1k items)
cache := caching.NewReadThroughCache[string, Data, int](
    10000,
    fetcher,
    caching.FetchCriteria{MinKeys: 100, MaxWorkItems: 50},
    caching.WithStash(caching.NewSliceStash[string, Data](1000)),
)

5.2 The Processing Loop

workItems := []int{1, 2, 3, ...}

for i, wID := range workItems {
    // 1. SIGNAL NEW BATCH (needed for memory management)
    if i % 100 == 0 {
        cache.AdvanceEpoch()
    }

    // 2. DISCOVERY PHASE
    // If Get() returns false, this loop breaks, and wID is queued.
    for range cache.WorkItem(wID) {
        processItem(wID)
    }

    // 3. EXECUTION PHASE (Replay ready items)
    // Checks if enough items are queued to trigger a batch fetch.
    for readyWID := range cache.IterateReadyWorkItems(ctx) {
        processItem(readyWID)
    }
}

// 4. FLUSH PHASE (Cleanup)
// Forces fetch for any remaining stragglers.
for wID := range cache.IterateRestWorkItems(ctx) {
    processItem(wID)
}

The cache restores the active work-item context for each replay yielded by IterateReadyWorkItems / IterateRestWorkItems, so a cascading Get() miss inside processItem (e.g., one that only becomes visible after the first dependency resolves) will re-enter the pending queue and be retried on the next flush — no manual WorkItem() wrap is needed during replay.

5.3 User Logic Requirements

The processItem function:

  1. Must be Idempotent: It may be called multiple times. Side effects (DB writes, increments) should only happen after all Get() calls succeed.
  2. Must Fail Fast: If cache.Get() returns false, return immediately. Do not attempt to calculate with zero values.
  3. Should Request All: Ideally, request all known dependencies upfront to maximize batch size.

6. Configuration Options

  • WithStash(backend): Swap L2 storage. Built-in options: NewSliceStash (memory-dense, O(n) scan; good for small L2s), NewMapStash (O(1), heavier per entry; good for large in-RAM L2s), and the disk-backed diskbacked.NewPogrebStash / diskbacked.NewPebbleStash (CBOR-encoded, optional soft cap; good when the working set spills beyond RAM).
  • WithMetrics(collector): Inject Prometheus/StatsD hooks.
  • WithErrorBackoff(duration): Circuit-breaker recovery window set at construction time. SetErrorBackoff(duration) does the same thing at runtime — useful for tests and for tuning live.
  • WithVersioning(orderOf): version-gated admission (§3.4). Off by default (internal counter = last-insert-wins).
  • WithFreshnessTTL(ttl): age-based staleness onset (§3.5). Off by default.
  • WithNegativeCaching(ttl): absent-key marking (§3.6). Off by default.

Write-through verbs: Pin(k) / Unpin(k) latch the dirty window (§3.4); MarkAsStaleIfOlder(k, ver) is the version-carrying staleness signal.

Lifecycle and introspection: Clear() drops every entry and all in-flight bookkeeping (call between frames, not with suspended work); Close() releases a disk-backed stash's resources; Len(), StashLen(), QueuedKeys() and PendingWorkItems() report occupancy. Constructors panic on unusable arguments (capacity < 1, nil fetcher).

6.1 Fetch threshold semantics

FetchCriteria exposes three Min/Max pairs (Keys, Partitions, WorkItems). All thresholds are evaluated independently and OR'd — any single threshold being reached triggers a fetch:

  • Max* fires synchronously from inside Get() / GetAcceptStale(), on every queueing path (cold miss and stale refresh alike). The triggering lookup then re-routes onto the post-flush state, so it returns the freshly fetched value directly. A single oversized work item that requests more than MaxKeys keys still chunks naturally: the first MaxKeys keys flush, then discovery continues.
  • Min* is only checked by IterateReadyWorkItems. If no Min is met, the iterator yields nothing — unless the key queue is empty while work items are pending (their keys already flushed synchronously): those replay immediately, no fetch needed.
  • IterateRestWorkItems ignores criteria entirely and always flushes whatever is queued.
  • A zero field disables that threshold. If all three Min* fields are zero, IterateReadyWorkItems treats any non-empty queue as ready.
  • A cancelled context aborts a flush between partitions; unprocessed partitions stay queued for the next flush — nothing is dropped.

7. Anti-Patterns

  1. Sharing across Goroutines: Never wrap this in a Mutex and share it. Create one Cache instance per Worker Goroutine.
  2. Ignoring the Boolean: val := c.Get(k). Never ignore the found boolean. If false, val is zero/garbage.
  3. Heavy Values in Keys: Do not use large structs as Keys (K). Use IDs or Hashes.
  4. Replaying keys that don't exist: a key absent upstream re-probes on every flush by default, so a flush-until-quiet loop never quiesces. Enable WithNegativeCaching (§3.4), or have the fetcher AddItem an explicit sentinel value for keys it knows are absent.
  5. Iterating the cache from a fetcher: reading (Get) is allowed; calling Iterate*WorkItems or Clear from inside FetchItemSinglePartition is not.

Breaking out of WorkItem or a replay loop early is safe: contexts are restored and un-yielded work items stay pending. A work item whose keys were dropped under memory pressure simply replays and re-queues them — progress requires the working set to fit L1+L2 (see §4 pinning).

Documentation

Index

Constants

This section is empty.

Variables

View Source
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]) Cap

func (s *MapStash[K, V]) Cap() int

func (*MapStash[K, V]) Clear added in v0.0.12

func (s *MapStash[K, V]) Clear()

func (*MapStash[K, V]) Delete

func (s *MapStash[K, V]) Delete(key K)

func (*MapStash[K, V]) GetAndRemove

func (s *MapStash[K, V]) GetAndRemove(key K) (e StashEntry[V], found bool)

func (*MapStash[K, V]) Len

func (s *MapStash[K, V]) Len() int

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).

Directories

Path Synopsis
Package stashtest is a conformance suite for caching.StashBackendI implementations.
Package stashtest is a conformance suite for caching.StashBackendI implementations.

Jump to

Keyboard shortcuts

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