cache

package
v1.16.1 Latest Latest
Warning

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

Go to latest
Published: May 6, 2026 License: Apache-2.0 Imports: 10 Imported by: 2

README

pkg/util/cache

Intention

pkg/util/cache is not one coherent cache abstraction. It is a utility package that currently contains several cache strategies with different cost and correctness tradeoffs.

The main reason this package exists today is RefreshAheadCache, which is the repository's higher-performance cache for indexed sets of resources where request-path refresh latency and visibility guarantees matter. The smaller TimeoutCache and LRUExpireCache helpers are still live and useful, but they solve simpler problems and do not share the same model.

The current cache families are:

  • TimeoutCache for a single cached value with a timeout and explicit invalidation
  • LRUExpireCache for typed LRU+TTL storage over apimachinery's cache, with defensive deep-copy semantics by default
  • RefreshAheadCache for background-refreshed indexed snapshots, synchronous invalidation, epoch-based snapshot identity, and local write-through overlay behavior

Invariants And Guard Rails

  • Choose the cache type for its operational model, not just for convenience. These types do not implement interchangeable caching semantics.
  • TimeoutCache is the simple TTL/invalidate model. Once the value expires or is invalidated, the next caller that needs fresh data must pay the refresh cost.
  • RefreshAheadCache exists to avoid pushing that refresh cost onto normal read paths. Run() performs an initial blocking load, then keeps the cache warm with periodic refresh.
  • RefreshAheadCache.Invalidate() is deliberately synchronous. On success, callers can assume the refreshed data is visible in that cache instance before control returns.
  • RefreshAheadCache is designed around uniquely indexed sets of resources and a single cache instance. Its correctness model is not a distributed coherence protocol.
  • RefreshAheadCache local write-through helpers rely on a strict usage rule: the corresponding backend write must already have committed synchronously and atomically before the cache is updated locally.
  • RefreshAheadCache epochs describe the identity of the visible cache snapshot. Callers may memoize derived work against an epoch and reuse it until that epoch changes.
  • LRUExpireCache defaults to deep-copy behavior to reduce accidental mutation of cached values. ZeroCopy() is an explicit tradeoff that gives speed back to the caller at the cost of safety.

Caveats

  • The package boundary is broad and a little ugly. TimeoutCache, LRUExpireCache, and RefreshAheadCache are related only in the loose sense that they all cache things.
  • TimeoutCache is simple but leaves refresh ownership with whoever reads after expiry or invalidation. That pushes refresh latency onto the unlucky reader, which is the main problem RefreshAheadCache is intended to solve for hotter read paths.
  • RefreshAheadCache is sophisticated enough that correctness depends on usage discipline, not just on calling the exported methods. The overlay model only protects local writes against refreshes already in flight; it is not a long-lived reconciliation layer.
  • RefreshAheadCache assumes a single writer-view per cache instance. If one process writes and another process reads through a different cache instance, read-your-writes is not guaranteed.
  • RefreshAheadCache is optimized for pointer-based zero-copy reads and snapshot reuse. That helps performance, but it means callers need to understand that individual items are shared references, not defensive deep copies.
  • Invalidate() only becomes operational after Run() has initialized the refresh loop. Calling it before startup can block indefinitely waiting for a refresh channel that does not exist yet. This is a real lifecycle hazard, not a graceful mode.
  • LRUExpireCache.Add() and Get() silently degrade on deep-copy failure by dropping the value or returning a miss.
  • TimeoutCache.Invalidate() currently mutates cache state without taking the same lock used by Get() and Set(). That is an implementation wart, not a design feature.

Documentation

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrConflict is raised when our data source returns the same
	// index multiple times.
	ErrConflict = errors.New("a cache index was witnessed more than once")
	// ErrInvalid is raised when there is no cache data e.g. it has
	// not been started.
	ErrInvalid = errors.New("cache invalid")
	// ErrNotFound is returned when the requested index is not in the
	// cache.
	ErrNotFound = errors.New("cache index not found")
	// ErrWorkerPanic is used to handle worker panics.
	ErrWorkerPanic = errors.New("worker panic")
)

Functions

This section is empty.

Types

type Cacheable added in v1.15.0

type Cacheable[T any] interface {
	// Index generates an index for the hash table.
	Index() string
	// Equal is used to detect modifications of the data.
	Equal(other *T) bool
}

Cacheable defines a cacheable type.

type CacheablePointer added in v1.15.0

type CacheablePointer[T any] interface {
	*T
	Cacheable[T]
}

CacheablePointer defines a pointer to a type that implements the Cacheable interface.

type Epoch added in v1.15.0

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

Epoch represents a revision of the cache data.

func (Epoch) Valid added in v1.15.0

func (e Epoch) Valid(previous Epoch) bool

Valid checks if a previous epoch is still valid against the current one.

type GetSnapshot added in v1.15.0

type GetSnapshot[T any] struct {
	// Epoch is the revision of the cache data.  The client can memoize any
	// transformations applied to the items and reuse those until a time
	// when the epoch becomes invalid.
	Epoch Epoch
	// Item is the individual cache item.
	Item *T
}

GetSnapshot is a user view of cache data.

type IndexFunc added in v1.15.0

type IndexFunc[T any, TP CacheablePointer[T]] func(t TP) string

IndexFunc provides the client a way to define how indexes are generated from cache resources. The index must be unique across all resources.

type LRUExpireCache added in v1.0.0

type LRUExpireCache[K comparable, T any] struct {
	// contains filtered or unexported fields
}

LRUExpireCache is a drop in replacement for the apimachinery version that makes it typed with generics, and also data immutable via deep copies.

func NewLRUExpireCache added in v1.0.0

func NewLRUExpireCache[K comparable, T any](maxSize int) *LRUExpireCache[K, T]

func (*LRUExpireCache[K, T]) Add added in v1.0.0

func (c *LRUExpireCache[K, T]) Add(key K, value T, ttl time.Duration)

func (*LRUExpireCache[K, T]) Get added in v1.0.0

func (c *LRUExpireCache[K, T]) Get(key K) (T, bool)

func (*LRUExpireCache[K, T]) ZeroCopy added in v1.14.0

func (c *LRUExpireCache[K, T]) ZeroCopy()

ZeroCopy allows the contents to be modifed, which is probably not what you want, but it is extremely fast, avoiding a deep copy and all the pain reflection inflicts.

type ListSnapshot added in v1.15.0

type ListSnapshot[T any] struct {
	// Epoch is the revision of the cache data.  The client can memoize any
	// transformations applied to the items and reuse those until a time
	// when the epoch becomes invalid.
	Epoch Epoch
	// Items are the individual cache items.  No ordering constraints are placed
	// on the snapshot items as this is likely to be more efficient downstream
	// after any potential filtering operations.
	Items []*T
}

ListSnapshot is a user view of cache data.

type RefreshAheadCache added in v1.15.0

type RefreshAheadCache[T any, TP CacheablePointer[T]] struct {
	// contains filtered or unexported fields
}

RefreshAheadCache is a hightly efficient high performance cache implementation for sets of resources.

Synchronization

One of the key observations with a timeout cache that is lazily loaded on a cache miss is that someone needs to pay a performance penalty. This implementation focuses on background synchronization of data either periodically or explicitly through a synchronization operation.

Explicit synchronization is deliberately blocking so that when control is returned back to the client, the expected data is guaranteed to be in the cache ready for subsequent use.

Either the entire cache can be refreshed, which facilitates addition of resources out-of-band, or synchronization of individual resources for example on creation or update to avoid having to perform a potentially costly rebuild.

Local writes are applied through a write-through overlay. A local mutation is immediately visible in the effective cache view. If a refresh is already in flight when that mutation happens, the overlay survives that refresh so the stale backend snapshot cannot erase the new value.

Correctness depends on two usage constraints:

  • This is a single-instance cache. If callers write through one process and read through another cache instance, read-your-writes is not guaranteed.
  • InsertIfAbsent and Upsert must only be called after the corresponding backend write has synchronously and atomically committed. A refresh that starts after such a write is assumed to observe that committed state.

Once a later refresh starts after the mutation, the backend snapshot is treated as authoritative again and the overlay entry is discarded. In other words, the overlay only bridges writes that land during an in-flight refresh; it is not a long-lived reconciliation layer.

This model has primarily been designed and tested for singleton-style writes, for example creates keyed by a unique primary key where only one object can exist for that key.

For a single cache instance, concurrent calls to InsertIfAbsent and Upsert are serialized by the cache lock. However, if multiple writers concurrently commit different values for the same key in the backend, the cache does not try to apply any extra heuristic to pick a local winner beyond that serialization order. In that case the visible value will still converge back to the backend on the next authoritative refresh.

Read Performance

Background synchronization ensures every client read will perform equally well. To facilitate efficient lookups of individual resources in the cache each resource will be indexed via some form of hashing function that uniquely identfies that resource.

The cache features pre-population so it will be ready to use, and this synchronization status can be fed directly into Kubernetes readiness probes to facilitate a seemless rolling upgrade experience.

Read Safety

If we naively passed a slice up to the client, then that could be accidentally mutated through filtering operations such as slices.Delete. The easy fix is to perform a deep copy of the data, but this is costly due to the use of runtime reflection.

We implement a halfway policy that is both performant and also less likely to fall foul of accidental mutation. Cache resources are only ever referred by pointer, facilitating zero copy for individual resources. When a client wishes to list all resources we return a new array that can be mutated pointing to each of the resources, doing only the minimal amount of work as is necessary.

Downstream Performance Optimization

Synchronization events are epochs. When a client lists resources, not only does it return an array of resource pointers, it also returns the epoch for which it is valid for.

This allows clients to memoize any transformations made against the cached resurces and further improve performance. An example of this is JSON encoding which uses runtime type reflection and is relatively costly.

func NewRefreshAheadCache added in v1.15.0

func NewRefreshAheadCache[T any, TP CacheablePointer[T]](refresh RefreshFunc[T, TP], options *RefreshAheadCacheOptions) *RefreshAheadCache[T, TP]

NewRefreshAheadCache constructs a new refresh ahead cache.

func (*RefreshAheadCache[T, TP]) Get added in v1.15.0

func (c *RefreshAheadCache[T, TP]) Get(index string) (*GetSnapshot[T], error)

Get does a zero copy read of a specified item.

func (*RefreshAheadCache[T, TP]) InsertIfAbsent added in v1.16.0

func (c *RefreshAheadCache[T, TP]) InsertIfAbsent(item TP) error

InsertIfAbsent inserts item into the effective cache view when the key is not already present.

Callers must only invoke this after the corresponding backend insert has synchronously committed. This path is primarily intended for singleton-style inserts where a key can only be created once. The inserted value remains authoritative until a later refresh that started after the insert replaces it with backend state.

func (*RefreshAheadCache[T, TP]) Invalidate added in v1.15.0

func (c *RefreshAheadCache[T, TP]) Invalidate() error

Invalidate performs a synchronous invalidation of the cache and only returns control to the client when the refresh has completed, guaranteeing on success that the cache will contain any new values.

func (*RefreshAheadCache[T, TP]) List added in v1.15.0

func (c *RefreshAheadCache[T, TP]) List() (*ListSnapshot[T], error)

List does a zero copy read of all items.

func (*RefreshAheadCache[T, TP]) Run added in v1.15.0

func (c *RefreshAheadCache[T, TP]) Run(ctx context.Context) error

Run performs a synchronous refresh to pre load cache data and starts the background refresher.

func (*RefreshAheadCache[T, TP]) Upsert added in v1.16.0

func (c *RefreshAheadCache[T, TP]) Upsert(item TP) error

Upsert writes item into the effective cache view whether or not the key is already present.

Callers must only invoke this after the corresponding backend write has synchronously committed. If multiple writers race to upsert different values for the same key, this cache does not define an additional winner-selection policy beyond local serialization; the next authoritative refresh remains the point where the cache is guaranteed to converge back to backend state. The written value remains authoritative until a later refresh that started after the write replaces it with backend state.

type RefreshAheadCacheOptions added in v1.15.0

type RefreshAheadCacheOptions struct {
	// RefreshPeriod controls how often to refresh data.
	RefreshPeriod time.Duration
}

RefreshAheadCacheOptions allows the cache to be configured in various ways.

type RefreshFunc added in v1.15.0

type RefreshFunc[T any, TP CacheablePointer[T]] func(ctx context.Context) ([]TP, error)

RefreshFunc provides the client a way to define how to load the cache data. It is recommended that any post processing that happens on raw data also occurs during a refresh to hide the cost.

type TimeoutCache

type TimeoutCache[V any] struct {
	// contains filtered or unexported fields
}

TimeoutCache provides a cache with timeout.

func New

func New[V any](refresh time.Duration) *TimeoutCache[V]

New gets a new cache.

func NewWithClock added in v1.11.0

func NewWithClock[V any](refresh time.Duration, c clock) *TimeoutCache[V]

func (*TimeoutCache[V]) Get

func (m *TimeoutCache[V]) Get() (value V, ok bool)

Get returns the cached value if set and it hasn't timed out and returns true. If it has timed out, it will return V's zero value and false, and will need to be set again.

func (*TimeoutCache[V]) Invalidate added in v1.11.0

func (m *TimeoutCache[V]) Invalidate()

func (*TimeoutCache[V]) Set

func (m *TimeoutCache[V]) Set(value V)

Set remembers the value and resets the invalid time based on when the cache was set.

Jump to

Keyboard shortcuts

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