Documentation
¶
Overview ¶
Package sanecache is a small in-memory cache that is predictable before it is fast: writes are synchronous, a value that cannot fit is refused with an error, budgets are in bytes, "does not exist" is a first-class answer, TTLs carry jitter so that keys warmed together do not expire together, and a cold key is loaded once however many callers ask for it.
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]) All() iter.Seq2[K, V]
- 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]) GetManyOrLoad(ctx context.Context, keys []K) (map[K]V, error)
- func (c *Cache[K, V]) GetOrLoad(ctx context.Context, key K) (V, error)
- func (c *Cache[K, V]) GetOrLoadFunc(ctx context.Context, key K, load func(context.Context) (V, error)) (V, error)
- func (c *Cache[K, V]) GetOrLoadFuncWithCost(ctx context.Context, key K, load func(context.Context) (V, int64, error)) (V, error)
- 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]) SetWithCost(key K, value V, cost int64) error
- func (c *Cache[K, V]) Stats() Stats
- func (c *Cache[K, V]) ViewStats() map[string]ViewStats
- type EvictReason
- type Options
- type Policy
- type Stats
- type Status
- type View
- func (v *View[T]) All() iter.Seq2[string, T]
- func (v *View[T]) Delete(key string) bool
- func (v *View[T]) Get(key string) (T, bool)
- func (v *View[T]) GetManyOrLoad(ctx context.Context, keys []string) (map[string]T, error)
- func (v *View[T]) GetOrLoad(ctx context.Context, key string) (T, error)
- func (v *View[T]) GetOrLoadFunc(ctx context.Context, key string, load func(context.Context) (T, error)) (T, error)
- func (v *View[T]) GetOrLoadFuncWithCost(ctx context.Context, key string, load func(context.Context) (T, int64, error)) (T, error)
- func (v *View[T]) Lookup(key string) (T, Status)
- func (v *View[T]) Name() string
- func (v *View[T]) Set(key string, value T) error
- func (v *View[T]) SetNegative(key string) error
- func (v *View[T]) SetNegativeTTL(key string, ttl time.Duration) error
- func (v *View[T]) SetTTL(key string, value T, ttl time.Duration) error
- func (v *View[T]) SetWithCost(key string, value T, cost int64) error
- func (v *View[T]) Stats() ViewStats
- type ViewOptions
- type ViewStats
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var ( // ErrTooLarge is returned by Set when the value's cost exceeds the budget of // its shard, so that it could never be cached. ErrTooLarge = errors.New("sanecache: value cost exceeds the shard budget") // ErrNegativeDisabled is returned by SetNegative without a NegativeTTL: a // negative entry would otherwise never expire. ErrNegativeDisabled = errors.New("sanecache: negative caching is disabled (Options.NegativeTTL is unset)") // ErrNotFound is how a loader says the upstream has no such key, and how // GetOrLoad reports it, from a load or a cached negative entry alike. A // loader may wrap it: it is tested with errors.Is. ErrNotFound = errors.New("sanecache: the upstream has no such key") // ErrDisabled is returned by the writes of a view opened on a nil cache. ErrDisabled = errors.New("sanecache: the view has no cache to write to") // ErrNoLoader is returned by GetOrLoad and GetManyOrLoad without a loader, // and by GetOrLoadFunc given a nil function. ErrNoLoader = errors.New("sanecache: loading requires a loader (Options.Loader, Options.BatchLoader or ViewOptions.Loader)") )
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 Cost: a programming mistake is better caught at construction than by a cache that silently misbehaves.
func (*Cache[K, V]) All ¶ added in v0.9.0
All iterates over the cached values, for dumping a cache or copying it into another. Each shard is copied under its read lock and yielded after, so the loop may use the cache, and it sees each shard as it was when reached rather than the whole cache at one moment. Negative and expired entries are skipped. Iterating counts no lookups, reorders nothing and starts no refresh.
Example ¶
All carries what a cache holds into another one, here with a larger budget, or out to wherever a warm start is read back from.
package main
import (
"fmt"
"github.com/andared/sanecache"
)
func main() {
old := sanecache.New(sanecache.Options[string, int]{MaxEntries: 100})
defer old.Close()
_ = old.Set("a", 1)
_ = old.Set("b", 2)
bigger := sanecache.New(sanecache.Options[string, int]{MaxEntries: 1000})
defer bigger.Close()
for k, v := range old.All() {
_ = bigger.Set(k, v)
}
v, _ := bigger.Get("b")
fmt.Println(bigger.Len(), v)
}
Output: 2 2
func (*Cache[K, V]) Clear ¶
func (c *Cache[K, V]) Clear()
Clear removes entries and invalidates outstanding loads, shard by shard. Concurrent new loads and writes can repopulate shards already cleared. Existing waiters still receive their load results. OnEvict is not called.
func (*Cache[K, V]) Close ¶
func (c *Cache[K, V]) Close()
Close stops the background goroutines. The cache stays usable afterwards: entries then expire only on lookup, and a cache configured with ClockGranularity goes back to reading the wall clock. Calling Close more than once is safe, and a cache that is simply dropped stops its goroutines too.
func (*Cache[K, V]) Delete ¶
Delete removes key and reports whether it was present. It also invalidates outstanding loads for key, even when no entry was present. Existing waiters still receive their load result, but it cannot be cached. 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]) GetManyOrLoad ¶ added in v0.6.0
GetManyOrLoad returns the cached values of keys, loading the rest in one Options.BatchLoader call, for an upstream that answers many keys at once.
Single flight and invalidation work per key, as in GetOrLoad: a key already loading elsewhere is waited for, and a GetOrLoad of a key in the batch waits for the batch. A key that does not exist upstream is absent from the result and is not an error. Duplicate keys are looked up once.
The error is the first failure in key order; the keys that failed are absent and the others present. If ctx ends first, the result holds what was at hand and the error is ctx.Err(), while the loads carry on for other waiters.
Without a BatchLoader the missing keys are loaded concurrently with Options.Loader; with neither, it returns ErrNoLoader. Keys due for a refresh are refreshed in a BatchLoader call of their own, not holding up this one.
Example ¶
Fetching many keys from an upstream that answers many at once: one call for whatever the cache does not already hold.
package main
import (
"context"
"fmt"
"sync/atomic"
"time"
"github.com/andared/sanecache"
)
type article struct {
ID string
Body string
}
func main() {
var upstreamCalls atomic.Int64
c := sanecache.New(sanecache.Options[string, *article]{
TTL: time.Minute,
NegativeTTL: 10 * time.Second,
BatchLoader: func(_ context.Context, ids []string) (map[string]*article, error) {
upstreamCalls.Add(1)
// One round trip, say SELECT ... WHERE id IN (...). A requested id
// missing from the answer is an id the upstream does not have.
found := make(map[string]*article, len(ids))
for _, id := range ids {
if id != "gone" {
found[id] = &article{ID: id, Body: "hello"}
}
}
return found, nil
},
})
defer c.Close()
if err := c.Set("a1", &article{ID: "a1", Body: "cached"}); err != nil {
fmt.Println("set:", err)
return
}
got, err := c.GetManyOrLoad(context.Background(), []string{"a1", "a2", "a3", "gone"})
if err != nil {
fmt.Println("load:", err)
return
}
fmt.Println("found:", len(got), got["a1"].Body, got["a2"].Body)
// The same batch again is answered from the cache, "gone" included.
_, _ = c.GetManyOrLoad(context.Background(), []string{"a1", "a2", "a3", "gone"})
fmt.Println("upstream calls:", upstreamCalls.Load())
}
Output: found: 3 cached hello upstream calls: 1
func (*Cache[K, V]) GetOrLoad ¶ added in v0.2.0
GetOrLoad returns the cached value, calling Options.Loader when there is none, or Options.BatchLoader with this one key when there is no Loader. Callers of a key whose load is running wait for it instead of starting their own.
A cached "does not exist" is reported as ErrNotFound. A loaded value is stored before this returns; one too large for the budget is still returned, and counted as a rejection. Invalidation or a successful write during the load keeps its result out of the cache, though its waiters still get it, and new callers start a new load. Other errors are returned as the loader produced them and are not cached.
Example ¶
A cold key is fetched once however many callers want it at the same moment.
package main
import (
"context"
"errors"
"fmt"
"sync"
"sync/atomic"
"time"
"github.com/andared/sanecache"
)
type article struct {
ID string
Body string
}
func main() {
var upstreamCalls atomic.Int64
c := sanecache.New(sanecache.Options[string, *article]{
TTL: time.Minute,
NegativeTTL: 10 * time.Second,
Loader: func(_ context.Context, id string) (*article, error) {
upstreamCalls.Add(1)
if id == "gone" {
// The upstream's "no such row" is translated once, here, and
// callers see the same error from the negative entry later.
return nil, sanecache.ErrNotFound
}
return &article{ID: id, Body: "hello"}, nil
},
})
defer c.Close()
var wg sync.WaitGroup
for range 10 {
wg.Add(1)
go func() {
defer wg.Done()
if _, err := c.GetOrLoad(context.Background(), "a1"); err != nil {
fmt.Println("load:", err)
}
}()
}
wg.Wait()
_, err := c.GetOrLoad(context.Background(), "gone")
fmt.Println("missing:", errors.Is(err, sanecache.ErrNotFound))
// Ten callers, one upstream call — plus the one that came back empty.
fmt.Println("upstream calls:", upstreamCalls.Load())
}
Output: missing: true upstream calls: 2
func (*Cache[K, V]) GetOrLoadFunc ¶ added in v0.7.0
func (c *Cache[K, V]) GetOrLoadFunc(ctx context.Context, key K, load func(context.Context) (V, error)) (V, error)
GetOrLoadFunc is GetOrLoad with the loader passed in, for upstream calls that need more than the key. A running load of the key, whoever started it, is waited for instead, so load must return what any caller's would for that key. Options.Loader is not needed; a nil load returns ErrNoLoader.
Example ¶
A search page is cached under a key that only summarises the query. The request itself travels with the call instead of being parsed back out of the key.
package main
import (
"context"
"fmt"
"sync/atomic"
"time"
"github.com/andared/sanecache"
)
func main() {
type query struct {
Text string
Page int
}
var upstreamCalls atomic.Int64
search := func(_ context.Context, q query) ([]string, error) {
upstreamCalls.Add(1)
return []string{fmt.Sprintf("%s, page %d", q.Text, q.Page)}, nil
}
c := sanecache.New(sanecache.Options[string, []string]{TTL: time.Minute})
defer c.Close()
q := query{Text: "sanecache", Page: 2}
key := fmt.Sprintf("%s|%d", q.Text, q.Page)
for range 3 {
results, err := c.GetOrLoadFunc(context.Background(), key, func(ctx context.Context) ([]string, error) {
return search(ctx, q)
})
if err != nil {
fmt.Println("search:", err)
}
fmt.Println(results[0])
}
fmt.Println("upstream calls:", upstreamCalls.Load())
}
Output: sanecache, page 2 sanecache, page 2 sanecache, page 2 upstream calls: 1
func (*Cache[K, V]) GetOrLoadFuncWithCost ¶ added in v0.8.0
func (c *Cache[K, V]) GetOrLoadFuncWithCost( ctx context.Context, key K, load func(context.Context) (V, int64, error), ) (V, error)
GetOrLoadFuncWithCost is GetOrLoadFunc for a load that reports the cost of what it returns, which is used instead of Options.Cost, for values whose size is known only where they are produced. A negative cost counts as zero.
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. With RefreshAfter and a loader in Options, a value due for a refresh starts one.
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. A successful write prevents outstanding loads for the key from publishing over it. Rejected writes do not.
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. A successful write supersedes outstanding loads.
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]) SetWithCost ¶ added in v0.8.0
SetWithCost is Set with the value's cost given instead of taken from Options.Cost, for a value whose size is known only where it was produced. A negative cost counts as zero.
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 it never expires, which // suits a bounded cache or one written with SetTTL. TTL time.Duration // NegativeTTL enables SetNegative and sets how long a "does not exist" answer // lives. Keep it short: a missing object may appear at any moment. NegativeTTL time.Duration // Jitter spreads expiry by up to this percentage either way, so that keys // written together do not expire together. Must be 0..100. Jitter int // RefreshAfter reloads a value in the background once it is this old, so that // a key in steady use is replaced before it expires instead of making the next // caller wait. Zero turns it off. It must be shorter than TTL, which still // holds: a failed refresh is not retried, and the value lives out its TTL. // // The read that finds a value due returns it and starts its one refresh: // GetOrLoad, GetManyOrLoad and GetOrLoadFunc with their loaders, Get and // Lookup with Loader or BatchLoader. GetManyOrLoad refreshes the keys it finds // due in one BatchLoader call. A refresh is an ordinary load to single flight // and invalidation; its context carries the values of the read that started // it and is never cancelled. ErrNotFound replaces the value with a negative // entry when NegativeTTL is set. RefreshAfter time.Duration // MaxBytes is the budget in bytes, split evenly across shards. It requires // Cost, except for an interface V, such as the any of a cache used through // views that bring their own Cost: values written without a cost are then // charged as negative entries are, by the entry and its key. 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 resident size of a value in bytes, which is what MaxBytes // counts. A decoded struct commonly costs several times its JSON: measure it // rather than guess (see the README). Cost func(V) int64 // Loader fetches a value that is not cached, for GetOrLoad. Callers who ask // for a key while its load runs share that one call. // // An error wrapping ErrNotFound means the upstream has no such key, and is // cached as a negative entry when NegativeTTL is set. Other errors are // returned unchanged and not cached. // // The context carries the values of the caller that started the load, but is // cancelled only once every waiting caller has given up. A loader that ignores // it still caches its result, unless a Delete, a Clear or a successful write of // the key came after the load started. A loader must not GetOrLoad its own key. Loader func(ctx context.Context, key K) (V, error) // BatchLoader fetches several uncached keys in one call: the keys of // GetManyOrLoad nobody is loading yet, and the key of GetOrLoad when Loader is // not set. // // A requested key missing from the result does not exist upstream, as if a // Loader had returned ErrNotFound; keys not requested are ignored. An error // fails every key, and nothing from the call is cached. The context is // cancelled once no caller waits for any of its keys. BatchLoader func(ctx context.Context, keys []K) (map[K]V, error) // Shards splits the cache into independently locked parts, rounded up to a // power of two; zero and one both mean a single lock. Each shard gets an equal // slice of the budget, rounded up, so the budget becomes approximate. Keep it // well above the shard count: MaxEntries of 2 across 16 shards holds 16. Shards int // Policy selects the eviction strategy. Defaults to LRU. Policy Policy // CleanupInterval is how often a background goroutine drops expired entries. // Zero picks it from the TTLs. Lookups drop expired entries too, but until // then they count against the budget. CleanupInterval time.Duration // DisableCleanup runs without that goroutine: entries then expire only on // lookup and on eviction. DisableCleanup bool // ClockGranularity has a background goroutine read the clock this often, // instead of every lookup paying for it, which is about half of what a lookup // costs. Expiry is then accurate to one interval either way. Zero reads the // clock every time. TTL, NegativeTTL and RefreshAfter, and a view's, must be // at least 10 intervals, or expiry would be off by more than a tenth. It works // with DisableCleanup; Close returns lookups to the wall clock. ClockGranularity time.Duration // OnEvict is called for every entry that leaves the cache other than by Delete // or Clear, with the zero value for a negative entry. 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. Every field is optional: the zero value is an unbounded cache whose entries never expire.
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 // S3FIFO keeps a new entry in a small queue until it is read again, which // lets out keys read only once before they displace the ones in steady use, // and gives an entry in the main queue another round for each read. Reads // take only a read lock, as with ClearOnFull, and Set still always stores. // Near W-TinyLFU's hit rate when the working set does not fit (see the // README). S3FIFO )
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
// TypeMisses counts view lookups that found another type. Hits counts them
// too, since the cache had the key.
TypeMisses int64
Loads int64 // keys loaded by Loader or BatchLoader, successfully or not
// LoadNotFound counts loads that ended in ErrNotFound, keys missing from a
// batch answer included; LoadErrors does not.
LoadNotFound int64
LoadErrors int64 // loads that failed: any other error, or a panic
Batches int64 // BatchLoader calls; Loads/Batches is keys per call
Coalesced int64 // GetOrLoad calls spared a load by another caller's
// Refreshes counts finished refreshes, by key. Loads does not include them:
// it is what misses cost.
Refreshes int64
// RefreshErrors counts refreshes that failed or panicked, not ErrNotFound.
RefreshErrors int64
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, cumulative except Entries and Bytes.
type Status ¶
type Status uint8
Status is the outcome of a lookup.
type View ¶ added in v0.2.0
type View[T any] struct { // contains filtered or unexported fields }
View is a typed window onto a Cache[string, any] that holds several value types under one byte budget: one cache per type would mean splitting the memory between types up front. Each view fixes a type, namespaces its keys and counts its own hits. It costs a string join per operation, which allocates once name and key exceed 32 bytes.
func NewView ¶ added in v0.2.0
NewView opens a view named o.Name onto c. It panics on invalid options, as New does.
A nil c switches caching off, so that code can keep calling GetOrLoad: the loader runs on every call, still shared by concurrent callers, and nothing is remembered. Lookups miss, Delete reports false, and writes fail with ErrDisabled.
Example ¶
Several value types under one byte budget, which is the only kind of budget that does not need dividing up in advance.
package main
import (
"fmt"
"time"
"github.com/andared/sanecache"
)
type article struct {
ID string
Body string
}
type season struct {
Num int
}
func main() {
c := sanecache.New(sanecache.Options[string, any]{
TTL: 10 * time.Minute,
MaxBytes: 64 << 20,
// The fallback for views that do not bring their own Cost. With several
// types sharing a budget, this is where the type switch would go.
Cost: func(any) int64 { return 256 },
})
defer c.Close()
articles := sanecache.NewView(c, sanecache.ViewOptions[*article]{
Name: "article",
Cost: func(a *article) int64 { return int64(len(a.Body)) * 3 },
})
seasons := sanecache.NewView(c, sanecache.ViewOptions[*season]{
Name: "season",
TTL: time.Hour, // seasons change less often than articles do
})
_ = articles.Set("1", &article{ID: "1", Body: "hello"})
_ = seasons.Set("1", &season{Num: 7})
a, _ := articles.Get("1")
s, _ := seasons.Get("1")
fmt.Println(a.Body, s.Num)
// The same key in two views is two entries: the view name is part of it.
fmt.Println("entries:", c.Len(), "bytes:", c.Bytes())
}
Output: hello 7 entries: 2 bytes: 271
func (*View[T]) All ¶ added in v0.9.0
All is Cache.All for this view's values, by the caller's keys. It walks the whole cache, skipping the entries of other views and values of another type.
func (*View[T]) Delete ¶ added in v0.2.0
Delete is Cache.Delete for this view's key, invalidating its loads in any view or the cache.
func (*View[T]) Get ¶ added in v0.2.0
Get returns the cached value. As with Cache.Get, a cached "does not exist" answer reports false; use Lookup to tell it from a miss.
func (*View[T]) GetManyOrLoad ¶ added in v0.9.0
GetManyOrLoad is Cache.GetManyOrLoad for this view, with ViewOptions.BatchLoader, or with Loader key by key when there is none. With neither it returns ErrNoLoader.
Example ¶
A view's batch loader is asked for the caller's ids, without the view's prefix.
package main
import (
"context"
"fmt"
"time"
"github.com/andared/sanecache"
)
type season struct {
Num int
}
func main() {
c := sanecache.New(sanecache.Options[string, any]{TTL: time.Minute})
defer c.Close()
seasons := sanecache.NewView(c, sanecache.ViewOptions[*season]{
Name: "season",
BatchLoader: func(_ context.Context, ids []string) (map[string]*season, error) {
fmt.Println("asked for", len(ids))
found := make(map[string]*season, len(ids))
for i, id := range ids {
found[id] = &season{Num: i + 1}
}
return found, nil
},
})
_ = seasons.Set("1", &season{Num: 1})
got, err := seasons.GetManyOrLoad(context.Background(), []string{"1", "2", "3"})
if err != nil {
fmt.Println(err)
return
}
fmt.Println("found:", len(got), "batches:", seasons.Stats().Batches)
}
Output: asked for 2 found: 3 batches: 1
func (*View[T]) GetOrLoad ¶ added in v0.3.0
GetOrLoad is Cache.GetOrLoad with ViewOptions.Loader, storing with this view's Cost and TTLs; an entry of another type is a miss the load replaces. Without a view loader it returns ErrNoLoader, even for a cached key.
Example ¶
package main
import (
"context"
"errors"
"fmt"
"time"
"github.com/andared/sanecache"
)
type article struct {
ID string
Body string
}
func main() {
// A cache of any needs no Cost when each view brings its own.
c := sanecache.New(sanecache.Options[string, any]{MaxBytes: 1024})
defer c.Close()
articles := sanecache.NewView(c, sanecache.ViewOptions[*article]{
Name: "article",
Cost: func(a *article) int64 { return int64(len(a.Body)) * 3 },
NegativeTTL: time.Minute,
Loader: func(_ context.Context, id string) (*article, error) {
if id == "gone" {
return nil, sanecache.ErrNotFound
}
return &article{ID: id, Body: "hello"}, nil
},
})
a, err := articles.GetOrLoad(context.Background(), "1")
if err != nil {
fmt.Println(err)
return
}
fmt.Println(a.ID, a.Body)
_, err = articles.GetOrLoad(context.Background(), "gone")
fmt.Println("missing:", errors.Is(err, sanecache.ErrNotFound))
fmt.Println("loads:", articles.Stats().Loads, "total:", c.Stats().Loads)
}
Output: 1 hello missing: true loads: 2 total: 2
func (*View[T]) GetOrLoadFunc ¶ added in v0.7.0
func (v *View[T]) GetOrLoadFunc(ctx context.Context, key string, load func(context.Context) (T, error)) (T, error)
GetOrLoadFunc is Cache.GetOrLoadFunc for this view, sharing loads with its GetOrLoad. A nil load returns ErrNoLoader.
func (*View[T]) GetOrLoadFuncWithCost ¶ added in v0.8.0
func (v *View[T]) GetOrLoadFuncWithCost( ctx context.Context, key string, load func(context.Context) (T, int64, error), ) (T, error)
GetOrLoadFuncWithCost is Cache.GetOrLoadFuncWithCost for this view.
func (*View[T]) Lookup ¶ added in v0.2.0
Lookup returns the cached value and how the view answered. An entry of another type is a miss, counted as a TypeMiss. With RefreshAfter and a Loader, a value due for a refresh starts one.
func (*View[T]) Name ¶ added in v0.2.0
Name returns the view's name, the prefix of its keys in the cache.
func (*View[T]) SetNegative ¶ added in v0.2.0
SetNegative records that the upstream reports no such key, for the view's NegativeTTL.
func (*View[T]) SetNegativeTTL ¶ added in v0.2.0
SetNegativeTTL is SetNegative with an explicit lifetime.
func (*View[T]) SetTTL ¶ added in v0.2.0
SetTTL caches value under key for ttl; zero means it never expires.
func (*View[T]) SetWithCost ¶ added in v0.8.0
SetWithCost is Cache.SetWithCost for this view, with the view's TTL.
type ViewOptions ¶ added in v0.2.0
type ViewOptions[T any] struct { // Name namespaces the view's keys: it stores under Name + ":" + key. It must // not be empty or contain ":". Name string // Loader fetches an uncached value by the caller's key, without the prefix, // under Options.Loader's rules. Loads coalesce within one View instance, not // across views of the same name or with the cache's own loads. A loader must // not load its own key through this view. Loader func(context.Context, string) (T, error) // BatchLoader is Options.BatchLoader for this view, by the caller's keys: // GetManyOrLoad's missing keys in one call, and GetOrLoad's key when Loader // is not set. Batches coalesce with this View instance's loads, as Loader's do. BatchLoader func(context.Context, []string) (map[string]T, error) // Cost is Options.Cost for this view's values, sparing the cache's Cost a type // switch. Unset, the cache's Cost is used. Cost func(T) int64 // TTL is how long this view's values stay valid; zero takes the cache's. The // sweep interval follows the cache's TTLs, so with a much shorter TTL here, // set Options.CleanupInterval. TTL time.Duration // NegativeTTL is how long this view's "does not exist" answers live; zero // takes the cache's. NegativeTTL time.Duration // RefreshAfter is Options.RefreshAfter for this view, with its own loaders. // It must be shorter than the view's TTL. Zero means no refresh: the cache's // RefreshAfter is not inherited and never applies to a view's values. RefreshAfter time.Duration }
ViewOptions configures a view. Only Name is required.
type ViewStats ¶ added in v0.2.0
type ViewStats struct {
Hits int64 // lookups that returned a value of this view's type
Misses int64 // lookups that found nothing
Negatives int64 // lookups that found a cached "does not exist"
// TypeMisses counts lookups that found another type: a bug detector, since
// only two views of one name or writes straight to the cache cause them.
TypeMisses int64
Loads int64 // keys loaded by Loader or BatchLoader, successfully or not
LoadNotFound int64 // completed loader calls that returned ErrNotFound
LoadErrors int64 // completed loader calls that failed otherwise, or panicked
Batches int64 // BatchLoader calls
Coalesced int64 // calls spared a load by another caller
Refreshes int64 // completed refreshes started by RefreshAfter
RefreshErrors int64 // refreshes that failed or panicked
}
ViewStats is a snapshot of the counters of one view name.