Documentation
¶
Overview ¶
Package sanecache is a small in-memory cache that aims to be predictable before it is fast.
Writes are synchronous, so a value is readable the moment Set returns. A value that cannot fit is refused with an error instead of being accepted and dropped later. Budgets are expressed in bytes, because a limit on the number of entries says nothing about the memory a process will use when entries are documents rather than integers. "The upstream says this key does not exist" is a first class answer rather than a marker smuggled inside the value type. And TTLs can carry jitter, so a batch of keys warmed by one request does not expire in lockstep and stampede the upstream.
Example ¶
package main
import (
"fmt"
"time"
"github.com/andared/sanecache"
)
type article struct {
ID string
Body string
}
func main() {
c := sanecache.New(sanecache.Options[string, *article]{
TTL: 10 * time.Minute,
Jitter: 10,
MaxBytes: 1 << 20,
// Resident size, not serialized size: a decoded struct costs several
// times its payload. Measure the ratio rather than guessing it.
Cost: func(a *article) int64 { return int64(len(a.Body)) * 3 },
})
defer c.Close()
if err := c.Set("a1", &article{ID: "a1", Body: "hello"}); err != nil {
fmt.Println("set:", err)
return
}
if a, ok := c.Get("a1"); ok {
fmt.Println("cached:", a.Body)
}
}
Output: cached: hello
Index ¶
- Variables
- type Cache
- func (c *Cache[K, V]) Bytes() int64
- func (c *Cache[K, V]) Clear()
- func (c *Cache[K, V]) Close()
- func (c *Cache[K, V]) Delete(key K) bool
- func (c *Cache[K, V]) Get(key K) (V, bool)
- func (c *Cache[K, V]) Len() int
- func (c *Cache[K, V]) Lookup(key K) (V, Status)
- func (c *Cache[K, V]) Set(key K, value V) error
- func (c *Cache[K, V]) SetNegative(key K) error
- func (c *Cache[K, V]) SetNegativeTTL(key K, ttl time.Duration) error
- func (c *Cache[K, V]) SetTTL(key K, value V, ttl time.Duration) error
- func (c *Cache[K, V]) Stats() Stats
- type EvictReason
- type Options
- type Policy
- type Stats
- type Status
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var ( // ErrTooLarge is returned by Set when the value's cost exceeds the budget of // the shard it hashes to, so caching it could never succeed. It is reported // rather than swallowed: a steady trickle of ErrTooLarge means every request // for those keys goes to the upstream, which is invisible in the hit rate. ErrTooLarge = errors.New("sanecache: value cost exceeds the shard budget") // ErrNegativeDisabled is returned by SetNegative when Options.NegativeTTL was // not set. Negative entries without a TTL would pin a "does not exist" answer // for the lifetime of the process. ErrNegativeDisabled = errors.New("sanecache: negative caching is disabled (Options.NegativeTTL is unset)") )
Functions ¶
This section is empty.
Types ¶
type Cache ¶
type Cache[K comparable, V any] struct { // contains filtered or unexported fields }
Cache is a sharded, TTL-based cache. It is safe for concurrent use. The zero value is not usable; call New.
func New ¶
func New[K comparable, V any](o Options[K, V]) *Cache[K, V]
New builds a cache from o. It panics on options that cannot describe a working cache, such as a byte budget without a Cost function: those are programming mistakes, and failing at construction beats a cache that silently misbehaves.
func (*Cache[K, V]) Clear ¶
func (c *Cache[K, V]) Clear()
Clear empties the cache. OnEvict is not called.
func (*Cache[K, V]) Close ¶
func (c *Cache[K, V]) Close()
Close stops the background sweeper. The cache stays usable afterwards; entries then expire only on lookup. Calling Close more than once is safe, and a cache that is simply dropped stops its sweeper too.
func (*Cache[K, V]) Delete ¶
Delete removes key and reports whether it was present. OnEvict is not called.
func (*Cache[K, V]) Get ¶
Get returns the cached value. A cached "does not exist" answer reports false, same as a miss; use Lookup to tell the two apart.
func (*Cache[K, V]) Len ¶
Len reports how many entries are held, including expired ones not yet swept.
func (*Cache[K, V]) Lookup ¶
Lookup returns the cached value and how the cache answered.
Example ¶
Telling "nobody asked yet" apart from "the upstream says it does not exist".
package main
import (
"fmt"
"time"
"github.com/andared/sanecache"
)
func main() {
c := sanecache.New(sanecache.Options[string, string]{
TTL: time.Minute,
NegativeTTL: 30 * time.Second,
})
defer c.Close()
_, status := c.Lookup("gone")
fmt.Println("before asking the upstream:", status)
// The upstream answered "no such key". Remember that, or every render of a
// template naming this id will ask again.
if err := c.SetNegative("gone"); err != nil {
fmt.Println("set negative:", err)
return
}
_, status = c.Lookup("gone")
fmt.Println("after:", status)
// Get collapses the two, because both mean "you have no value".
_, ok := c.Get("gone")
fmt.Println("Get reports a hit:", ok)
}
Output: before asking the upstream: miss after: negative Get reports a hit: false
func (*Cache[K, V]) Set ¶
Set caches value under key for the configured TTL.
Example ¶
A value that cannot fit is refused rather than accepted and dropped later.
package main
import (
"errors"
"fmt"
"github.com/andared/sanecache"
)
func main() {
c := sanecache.New(sanecache.Options[string, []byte]{
MaxBytes: 1024,
Cost: func(b []byte) int64 { return int64(len(b)) },
})
defer c.Close()
err := c.Set("huge", make([]byte, 4096))
fmt.Println("rejected:", errors.Is(err, sanecache.ErrTooLarge))
// The rejection is counted, so keys that can never be cached show up as
// their own signal instead of an unexplained miss rate.
_, ok := c.Get("huge")
fmt.Println("cached:", ok, "rejections:", c.Stats().Rejections)
}
Output: rejected: true cached: false rejections: 1
func (*Cache[K, V]) SetNegative ¶
SetNegative records that the upstream reports no such key, for the configured NegativeTTL. Without it, a template that names a deleted object hits the upstream on every single render.
func (*Cache[K, V]) SetNegativeTTL ¶
SetNegativeTTL is SetNegative with an explicit lifetime.
func (*Cache[K, V]) SetTTL ¶
SetTTL caches value under key for ttl, overriding Options.TTL. A ttl of zero means the entry never expires on its own.
func (*Cache[K, V]) Stats ¶
Stats returns a snapshot of the counters. It walks every shard, so poll it on a metrics interval rather than per request.
Example ¶
package main
import (
"fmt"
"time"
"github.com/andared/sanecache"
)
func main() {
c := sanecache.New(sanecache.Options[int, string]{
TTL: time.Minute,
NegativeTTL: time.Minute,
})
defer c.Close()
_ = c.Set(1, "one")
_ = c.SetNegative(2)
c.Get(1) // hit
c.Lookup(2) // negative
c.Get(3) // miss
s := c.Stats()
// A cached negative counts towards the hit rate: it saved the same upstream
// call a positive answer would have.
fmt.Printf("hits=%d negatives=%d misses=%d rate=%.2f\n",
s.Hits, s.Negatives, s.Misses, s.HitRate())
}
Output: hits=1 negatives=1 misses=1 rate=0.67
type EvictReason ¶
type EvictReason uint8
EvictReason explains why an entry left the cache. Explicit Delete and Clear calls do not report a reason.
const ( ReasonEvicted EvictReason = iota // dropped to stay inside the budget ReasonExpired // its TTL ran out ReasonReplaced // a later Set overwrote the key )
The reasons an entry can leave the cache on its own.
func (EvictReason) String ¶
func (r EvictReason) String() string
type Options ¶
type Options[K comparable, V any] struct { // TTL is how long a value stays valid. Zero means entries never expire on // their own, which only makes sense for a cache bounded by MaxBytes or // MaxEntries, or one whose entries all get an explicit TTL via SetTTL. TTL time.Duration // NegativeTTL enables SetNegative and sets how long a cached "does not // exist" answer lives. It is usually much shorter than TTL: an object that // does not exist yet may appear at any moment. NegativeTTL time.Duration // Jitter spreads expiry times by up to this percentage in either direction, // so keys written together do not expire together. Must be 0..100. Jitter int // MaxBytes is the total budget in bytes, split evenly across shards. It // requires Cost. Zero means unbounded. MaxBytes int64 // MaxEntries caps the number of entries, split evenly across shards. Zero // means unbounded. Prefer MaxBytes unless entries are uniform in size. MaxEntries int // Cost reports the memory a value occupies, in bytes. It is what MaxBytes is // measured against, so it should approximate resident size rather than // serialized size: a decoded struct commonly costs several times its JSON. // Measure it once rather than guessing (see the README). Cost func(V) int64 // Shards splits the cache into independently locked parts, rounded up to a // power of two. Zero and one both mean a single lock. More shards reduce // contention but make the budget approximate: each shard gets an equal slice // of MaxBytes, and an uneven key distribution leaves some of it unused. // // The per-shard slice is rounded up, so the shards together are never // stricter than what was asked for. With small caps that rounding dominates: // MaxEntries of 2 across 16 shards is one entry per shard, or sixteen in // total. Keep the cap comfortably larger than the shard count. Shards int // Policy selects the eviction strategy. Defaults to LRU. Policy Policy // CleanupInterval is how often a background goroutine drops expired entries. // Zero picks an interval from the configured TTLs. Expired entries are also // dropped lazily on lookup, but until they are swept they still count // against the budget. CleanupInterval time.Duration // DisableCleanup runs the cache without a background goroutine. Expiry then // happens only on lookup and on eviction. DisableCleanup bool // OnEvict, if set, is called for every entry that leaves the cache without // being explicitly deleted. Negative entries are reported with the zero // value. It runs outside the shard lock, on the goroutine that caused the // removal, so it must not block. OnEvict func(key K, value V, reason EvictReason) // DisableStats skips the counters behind Stats. DisableStats bool }
Options configures a cache. The zero value is a valid, unbounded, never expiring cache; every field below is optional.
type Policy ¶
type Policy uint8
Policy decides what happens when a shard runs over its budget.
const ( // LRU evicts the least recently used entries until the shard fits again. // Keeping that order costs a write lock on every read. LRU Policy = iota // ClearOnFull drops the whole shard except the entry that overflowed it. // Reads then need only a read lock, which is worth more than precise // eviction when access order is flat and refilling is cheap relative to // the bookkeeping. ClearOnFull )
type Stats ¶
type Stats struct {
Hits int64 // lookups that returned a value
Misses int64 // lookups that found nothing
Negatives int64 // lookups that found a cached "does not exist"
Evictions int64 // entries dropped to stay inside the budget
Expirations int64 // entries dropped because their TTL ran out
Replacements int64 // entries overwritten by a later Set
Rejections int64 // Set calls refused with ErrTooLarge
Entries int // entries currently held, expired-but-not-yet-swept included
Bytes int64 // sum of the costs of those entries
}
Stats is a snapshot of the cache counters. Counters are cumulative since the cache was created; Entries and Bytes are instantaneous.