Documentation
¶
Overview ¶
Package cistern is a two-level cache-aside library: an in-process L1 and an optional shared L2, with load coalescing, negative caching, and invalidation by key, by tag and by event.
Its requirements are in REQUIREMENTS.md, its threat model in docs/THREAT-MODEL.md, and its decisions in docs/adr.
A cache is never a source of truth. Every read path is fail-open: when L2 is unavailable, cistern degrades to L1 and the loader rather than failing the read (RNF-02).
Index ¶
- Constants
- Variables
- func NewGeneration() (uint64, error)
- type Cache
- func (c *Cache[K, V]) Close()
- func (c *Cache[K, V]) Delete(ctx context.Context, k K) error
- func (c *Cache[K, V]) Get(ctx context.Context, k K) (value V, ok bool, err error)
- func (c *Cache[K, V]) GetOrLoad(ctx context.Context, k K, load Loader[V], opts ...EntryOption) (V, error)
- func (c *Cache[K, V]) InvalidateTag(ctx context.Context, tag string) error
- func (c *Cache[K, V]) Set(ctx context.Context, k K, v V, opts ...EntryOption) error
- type CoalescedEvent
- type EntryOption
- type ErrorEvent
- type HitEvent
- type Hooks
- type InvalidateEvent
- type Level
- type LoadEvent
- type Loader
- type MissEvent
- type NoCache
- type Op
- type Option
- func WithBus(b bus.Bus) Option
- func WithCodec(cd codec.Codec) Option
- func WithHookKeys() Option
- func WithHooks(h Hooks) Option
- func WithJitter(f float64) Option
- func WithKeyHashing() Option
- func WithL1(s Store) Option
- func WithL1TTL(d time.Duration) Option
- func WithL2(s Store) Option
- func WithLoadTimeout(d time.Duration) Option
- func WithMaxValueBytes(n int) Option
- func WithNegativeTTL(d time.Duration) Option
- func WithTTL(d time.Duration) Option
- func WithTags[K comparable](tags func(K) []string) Option
- type Store
- type TagStore
Examples ¶
Constants ¶
const ( DefaultLoadTimeout = 10 * time.Second DefaultMaxValueBytes = 1 << 20 )
Defaults used when the corresponding option is not given.
const MaxKeyBytes = 256
MaxKeyBytes is the longest consumer key stored verbatim (ADR-0008). A longer key is refused with ErrInvalidKey unless the cache was built with WithKeyHashing.
Variables ¶
var ( // ErrNotFound reports that a value does not exist in the source of truth. // // A loader returns it (possibly wrapped) to say the key has no value; with // negative caching enabled, cistern remembers that absence for a short TTL // instead of calling the loader again (RF-06). A read that finds a // remembered absence returns it. A plain cache miss is not ErrNotFound. ErrNotFound = errors.New("cistern: not found") // ErrInvalidConfig reports a cache that cannot be built as configured — // for example an empty namespace (RS-01) or an L1 TTL longer than the L2 // TTL (RF-08). Constructors return it wrapped with the reason; they never // panic (RF-16). ErrInvalidConfig = errors.New("cistern: invalid configuration") // ErrValueTooLarge reports a value whose encoding exceeds the cache's // value limit (WithMaxValueBytes, RS-05). Set returns it; GetOrLoad // returns the loaded value without caching it instead. ErrValueTooLarge = errors.New("cistern: value too large") // ErrInvalidKey reports a consumer key that is empty, not valid UTF-8, // contains a control character, or is longer than MaxKeyBytes without // WithKeyHashing (RS-03, ADR-0008). It is the caller's bug, so every // operation returns it rather than treating it as a miss. ErrInvalidKey = errors.New("cistern: invalid key") // ErrUncacheable reports a value type or value marked NoCache (RS-04). ErrUncacheable = errors.New("cistern: value must not be cached") )
Functions ¶
func NewGeneration ¶
NewGeneration returns an unpredictable generation in [1, 2^62), the value a TagStore gives a counter it creates (ADR-0005). The range leaves room for more increments than will ever happen.
Types ¶
type Cache ¶
type Cache[K comparable, V any] struct { // contains filtered or unexported fields }
Cache is a typed cache-aside cache over one or two levels.
A Cache is safe for concurrent use. It never holds a value object: both levels store encoded bytes, and every hit decodes a fresh copy, so a caller that mutates what it received cannot change what the next caller receives (ADR-0014).
func New ¶
func New[K comparable, V any](namespace string, key func(K) string, opts ...Option) (*Cache[K, V], error)
New returns a Cache whose entries live under namespace.
The namespace names a domain, such as "tasks", and must match [a-z0-9._-]{1,64}; there is no cache without one (RS-01). key turns a K into the consumer key. When entries belong to a user or tenant, key must put the owner in it — "user:42:list" — because cistern cannot tell a key that omits the owner from one that does not need it (RS-02, ADR-0008).
At least one of WithL1 and WithL2, and WithTTL, are required. New returns an error wrapping ErrInvalidConfig for any invalid configuration.
Example ¶
The key function puts the owner first, so two users can never address the same entry (RS-02).
package main
import (
"context"
"fmt"
"time"
"github.com/JonasBorgesLM/cistern"
"github.com/JonasBorgesLM/cistern/memory"
)
type listKey struct {
UserID int
ListID int
}
type taskList struct {
Title string `json:"title"`
Tasks []string `json:"tasks"`
}
func main() {
l1, err := memory.New(memory.WithMaxEntries(10_000))
if err != nil {
panic(err)
}
lists, err := cistern.New[listKey, taskList]("tasks",
func(k listKey) string { return fmt.Sprintf("user:%d:list:%d", k.UserID, k.ListID) },
cistern.WithL1(l1),
cistern.WithTTL(30*time.Second),
cistern.WithJitter(0.1),
)
if err != nil {
panic(err)
}
ctx := context.Background()
err = lists.Set(ctx, listKey{UserID: 42, ListID: 1}, taskList{Title: "today", Tasks: []string{"review ADR-0005"}})
if err != nil {
panic(err)
}
own, ok, _ := lists.Get(ctx, listKey{UserID: 42, ListID: 1})
fmt.Println(own.Title, ok)
_, ok, _ = lists.Get(ctx, listKey{UserID: 43, ListID: 1})
fmt.Println("another user's list cached:", ok)
}
Output: today true another user's list cached: false
func (*Cache[K, V]) Close ¶
func (c *Cache[K, V]) Close()
Close stops the cache listening to its Bus. It is safe to call more than once, and on a cache without a Bus.
func (*Cache[K, V]) Delete ¶
Delete removes k from every configured level. L1 is always evicted, and an error from L2 is returned: a failed invalidation that went unreported would leave stale data on every replica until it expired (ADR-0002).
Example ¶
After writing the source of truth, invalidate rather than Set (ADR-0003).
package main
import (
"context"
"fmt"
"time"
"github.com/JonasBorgesLM/cistern"
"github.com/JonasBorgesLM/cistern/memory"
)
func main() {
l1, _ := memory.New()
cache, _ := cistern.New[string, string]("profiles", func(id string) string { return "user:" + id }, cistern.WithL1(l1), cistern.WithTTL(time.Minute))
ctx := context.Background()
_ = cache.Set(ctx, "42", "old display name")
// ... the profile is updated in the database ...
if err := cache.Delete(ctx, "42"); err != nil {
fmt.Println("invalidation failed; the cache may serve stale data:", err)
}
_, ok, _ := cache.Get(ctx, "42")
fmt.Println("cached after invalidation:", ok)
}
Output: cached after invalidation: false
func (*Cache[K, V]) Get ¶
Get returns the cached value for k, reading L1 before L2. ok reports a hit; a miss is not an error. A remembered absence (negative caching) returns ErrNotFound.
Reads fail open (ADR-0002): a level that errors, or an entry that does not decode, is treated as a miss. Apart from ErrNotFound, Get returns an error only for an invalid key (ErrInvalidKey) or when ctx is done.
func (*Cache[K, V]) GetOrLoad ¶
func (c *Cache[K, V]) GetOrLoad(ctx context.Context, k K, load Loader[V], opts ...EntryOption) (V, error)
GetOrLoad returns the cached value for k, or loads it with load, stores it and returns it. Concurrent calls for the same missing key share one load (RF-05); each caller still decodes its own copy.
Like Get, it fails open: a cache level that errors is a miss, and a value that cannot be stored is still returned. A loader error is returned as is and not cached, except ErrNotFound when negative caching is enabled (WithNegativeTTL), which is remembered and returned as ErrNotFound until it expires. A load that exceeds the load timeout returns an error wrapping context.DeadlineExceeded.
Example ¶
GetOrLoad is cache-aside in one call: a miss runs the loader and stores what it returns (RF-01, RF-05). With WithNegativeTTL, a loader's ErrNotFound is remembered too, so a missing key does not reach the source on every request (RF-06). TTL overrides the cache's TTL for one entry (RF-03).
package main
import (
"context"
"errors"
"fmt"
"time"
"github.com/JonasBorgesLM/cistern"
"github.com/JonasBorgesLM/cistern/memory"
)
func main() {
l1, _ := memory.New()
profiles, _ := cistern.New[string, string]("profiles", func(id string) string { return "user:" + id },
cistern.WithL1(l1), cistern.WithTTL(time.Minute), cistern.WithNegativeTTL(10*time.Second))
ctx := context.Background()
queries := 0
fromDB := func(id string) cistern.Loader[string] {
return func(context.Context) (string, error) {
queries++
if id == "42" {
return "Ada", nil
}
return "", cistern.ErrNotFound // the loader's way to say "no such row"
}
}
for range 2 {
name, _ := profiles.GetOrLoad(ctx, "42", fromDB("42"), cistern.TTL(5*time.Minute))
fmt.Println(name)
}
for range 2 {
_, err := profiles.GetOrLoad(ctx, "7", fromDB("7"))
fmt.Println(errors.Is(err, cistern.ErrNotFound))
}
fmt.Println("database queries:", queries)
}
Output: Ada Ada true true database queries: 2
func (*Cache[K, V]) InvalidateTag ¶
InvalidateTag retires every entry carrying tag, in every replica, without scanning (RF-10, ADR-0005): it advances the tag's generation counter, and entries that recorded an older generation read as misses from then on.
Like Delete, it fails loud (ADR-0002). It returns ErrInvalidConfig on a cache built without WithTags and ErrInvalidKey for an invalid tag.
Example ¶
Tags group entries that one write invalidates together (RF-10). The owner is in the tag as well as in the key, so invalidating one user's lists never touches another's (RS-02, ADR-0005).
package main
import (
"context"
"fmt"
"time"
"github.com/JonasBorgesLM/cistern"
"github.com/JonasBorgesLM/cistern/memory"
)
type listKey struct {
UserID int
ListID int
}
func main() {
l1, _ := memory.New()
lists, _ := cistern.New[listKey, string]("tasks",
func(k listKey) string { return fmt.Sprintf("user:%d:list:%d", k.UserID, k.ListID) },
cistern.WithL1(l1), cistern.WithTTL(time.Minute),
cistern.WithTags(func(k listKey) []string { return []string{fmt.Sprintf("user:%d:lists", k.UserID)} }))
ctx := context.Background()
_ = lists.Set(ctx, listKey{UserID: 42, ListID: 1}, "today")
_ = lists.Set(ctx, listKey{UserID: 42, ListID: 2}, "someday")
_ = lists.Set(ctx, listKey{UserID: 43, ListID: 1}, "groceries")
// ... user 42 creates a task in the database ...
if err := lists.InvalidateTag(ctx, "user:42:lists"); err != nil {
fmt.Println("invalidation failed; the cache may serve stale data:", err)
}
for _, k := range []listKey{{42, 1}, {42, 2}, {43, 1}} {
_, ok, _ := lists.Get(ctx, k)
fmt.Printf("user %d list %d cached: %v\n", k.UserID, k.ListID, ok)
}
}
Output: user 42 list 1 cached: false user 42 list 2 cached: false user 43 list 1 cached: true
func (*Cache[K, V]) Set ¶
func (c *Cache[K, V]) Set(ctx context.Context, k K, v V, opts ...EntryOption) error
Set stores v under k in every configured level, L2 first. Unlike a read, it fails loud (ADR-0002): an error from any level is returned, so a caller that asked to store a value knows when it was not stored.
After writing the source of truth, prefer Delete to Set: two concurrent Sets can reach the cache in the opposite order from the source, while two Deletes can only cost a miss (ADR-0003).
type CoalescedEvent ¶
CoalescedEvent describes a caller served by another caller's load.
type EntryOption ¶
type EntryOption func(*entryConfig)
EntryOption configures a single call.
func TTL ¶
func TTL(d time.Duration) EntryOption
TTL overrides the cache's TTL for one entry. It must be positive. With both levels configured it sets the L2 TTL; the L1 TTL is the smaller of it and the configured L1 TTL (ADR-0004). In GetOrLoad, the options of the call that starts a load are the ones that apply to what it stores.
type ErrorEvent ¶
type ErrorEvent struct {
Namespace string
Key string // empty unless WithHookKeys
Op Op
Level Level
Err error
}
ErrorEvent describes a swallowed error. Level is zero when the error is not tied to one level.
type HitEvent ¶
type HitEvent struct {
Namespace string
Key string // empty unless WithHookKeys
Level Level
Absent bool
}
HitEvent describes a served read. Absent reports a remembered absence.
type Hooks ¶
type Hooks struct {
// OnHit fires when a read is served, with the level that served it.
OnHit func(ctx context.Context, e HitEvent)
// OnMiss fires when a read finds nothing usable.
OnMiss func(ctx context.Context, e MissEvent)
// OnLoad fires when a loader returns.
OnLoad func(ctx context.Context, e LoadEvent)
// OnCoalesced fires for a caller served by another caller's load (RF-05).
OnCoalesced func(ctx context.Context, e CoalescedEvent)
// OnError fires for an error that fail-open swallowed (ADR-0002). Errors
// that are returned to the caller are not reported here.
OnError func(ctx context.Context, e ErrorEvent)
// OnInvalidate fires when a key or tag is invalidated, by this instance
// or, with a Bus, by another.
OnInvalidate func(ctx context.Context, e InvalidateEvent)
}
Hooks observe a cache (RF-15, ADR-0015). Every field may be nil.
Hooks are called synchronously and must not block: a hook that blocks is a read that blocks. A hook that panics is recovered and ignored. Events carry the namespace; the consumer key only with WithHookKeys, because keys usually embed a user id and hooks usually feed logs (RS-08).
type InvalidateEvent ¶
type InvalidateEvent struct {
Namespace string
Key string // empty unless WithHookKeys, for tag invalidations, and for a Remote one of a hashed key (only its hash travels)
Tag string
Remote bool
}
InvalidateEvent describes an invalidation: of a key (Tag is empty) or of a tag. Remote reports one that arrived from another instance over the Bus. Tags are always present — they name groups, not entries.
type Level ¶
type Level uint8
Level identifies a cache level in hook events.
The levels. The zero Level means an event is not tied to one level.
type LoadEvent ¶
type LoadEvent struct {
Namespace string
Key string // empty unless WithHookKeys
Duration time.Duration
Err error
}
LoadEvent describes a loader call.
type Loader ¶
Loader fetches a value from the source of truth. It returns ErrNotFound, possibly wrapped, when the value does not exist.
The context it receives carries the values of the caller that started the load but not its cancellation, and is bounded by the load timeout (ADR-0007).
type NoCache ¶
type NoCache interface {
NoCache()
}
NoCache marks a type whose values must never be cached (RS-04): secrets, tokens, sessions. A cache whose value type implements it — directly or through a pointer — cannot be built, and Set and GetOrLoad refuse such values behind an interface value type, with ErrUncacheable.
The mark is a guard rail, not a classifier: a secret inside a type that does not implement NoCache is not detected (docs/THREAT-MODEL.md, T-03).
type Op ¶
type Op string
Op names the operation whose error a fail-open read swallowed.
const ( OpRead Op = "read" // a level could not answer OpDecode Op = "decode" // an entry was malformed, from another codec, or did not decode (RS-06) OpPopulate Op = "populate" // a loaded value could not be stored OpBackfill Op = "backfill" // an L2 entry could not be copied into L1 OpEvent Op = "event" // a Bus event could not be applied locally )
The operations reported through OnError.
type Option ¶
type Option func(*config)
Option configures a Cache. Options are validated together by New, which returns an error wrapping ErrInvalidConfig rather than panicking (RF-16).
func WithBus ¶
WithBus connects the cache to other instances through b (RF-11, ADR-0006): Delete and InvalidateTag publish an event, and events from other instances evict the local L1 copy of a key, or retire a tag locally, at once instead of after the L1 TTL. The cache subscribes in New; call Close to stop.
The Bus is best-effort; a lost event costs at most one L1 TTL of staleness (RNF-11). A publish failure is returned by the invalidation that caused it.
Example ¶
Each replica has its own L1; a Bus carries invalidations between them, so a Delete on one drops the others' copies at once instead of after their L1 TTL (RF-11, RF-12, ADR-0006). Across processes the Bus is redisstore.NewBus; bus.NewLocal connects caches in one process.
package main
import (
"context"
"fmt"
"time"
"github.com/JonasBorgesLM/cistern"
"github.com/JonasBorgesLM/cistern/bus"
"github.com/JonasBorgesLM/cistern/memory"
)
func main() {
b := bus.NewLocal()
replica := func() *cistern.Cache[string, string] {
l1, _ := memory.New()
c, err := cistern.New[string, string]("profiles", func(id string) string { return "user:" + id },
cistern.WithL1(l1), cistern.WithTTL(time.Hour), cistern.WithBus(b))
if err != nil {
panic(err)
}
return c
}
a, c := replica(), replica()
defer a.Close() // ends the cache's subscription
defer c.Close()
ctx := context.Background()
_ = a.Set(ctx, "42", "Ada")
_ = c.Set(ctx, "42", "Ada")
// ... the profile changes; replica a handles the write ...
_ = a.Delete(ctx, "42")
_, ok, _ := c.Get(ctx, "42")
fmt.Println("replica c still cached:", ok)
}
Output: replica c still cached: false
func WithHookKeys ¶
func WithHookKeys() Option
WithHookKeys puts the consumer key in hook events. Without it events carry only the namespace, because keys usually embed a user id and hooks usually feed logs (RS-08).
func WithHooks ¶
WithHooks sets the hooks the cache reports to (RF-15, ADR-0015).
Example ¶
Hooks are where fail-open reads report what they swallowed (ADR-0015). Events carry the namespace; the key only with WithHookKeys (RS-08).
package main
import (
"context"
"fmt"
"time"
"github.com/JonasBorgesLM/cistern"
"github.com/JonasBorgesLM/cistern/memory"
)
func main() {
l1, _ := memory.New()
var misses int
cache, _ := cistern.New[string, string]("profiles", func(id string) string { return "user:" + id },
cistern.WithL1(l1), cistern.WithTTL(time.Minute),
cistern.WithHooks(cistern.Hooks{
OnMiss: func(_ context.Context, e cistern.MissEvent) { misses++ },
OnError: func(_ context.Context, e cistern.ErrorEvent) {
fmt.Println("swallowed:", e.Op, e.Level, e.Err)
},
}))
_, _, _ = cache.Get(context.Background(), "42")
fmt.Println("misses:", misses)
}
Output: misses: 1
func WithJitter ¶
WithJitter shortens each entry's TTL by a random fraction of up to f, so entries written together do not expire together (RF-04). f must be in [0, 1). Jitter never lengthens a TTL, and the same draw applies to both levels, so L1 still never outlives L2.
func WithKeyHashing ¶
func WithKeyHashing() Option
WithKeyHashing stores a consumer key longer than MaxKeyBytes as its SHA-256 instead of refusing it (RS-03, ADR-0008). Keys within the limit are never hashed, and every key is still checked for control characters and UTF-8.
func WithL1TTL ¶
WithL1TTL caps how long an entry lives in L1. It must be positive and no longer than the TTL (RF-08), and requires WithL1. It defaults to the TTL.
With more than one replica, it is also the worst-case staleness: a replica that misses an invalidation serves its copy until its L1 entry expires (ADR-0004, RNF-11).
func WithLoadTimeout ¶
WithLoadTimeout bounds how long a load may run (ADR-0007). It must be positive and defaults to DefaultLoadTimeout. When it expires, the callers waiting on the load get an error wrapping context.DeadlineExceeded and the next caller starts a fresh load, even if the loader never returns.
func WithMaxValueBytes ¶
WithMaxValueBytes bounds the encoded size of a value (RS-05). It must be positive and defaults to DefaultMaxValueBytes. Set fails with ErrValueTooLarge over it; GetOrLoad returns an oversized loaded value without caching it; a stored entry over it is read as a miss (ADR-0009).
func WithNegativeTTL ¶
WithNegativeTTL enables negative caching (RF-06): when a loader returns ErrNotFound, the absence is remembered for d instead of calling the loader again on every request for the missing key (T-11). d must be positive and no longer than the TTL; the L1 copy of an absence also obeys the L1 TTL.
func WithTTL ¶
WithTTL sets how long an entry lives, unless a call overrides it with TTL. It is required and must be positive. With both levels configured it is the L2 TTL.
func WithTags ¶
func WithTags[K comparable](tags func(K) []string) Option
WithTags declares how a key's tags are derived, which enables InvalidateTag (RF-10, ADR-0005): every entry records the generation of its tags, and a tag invalidated since reads as a miss. Reads and writes derive tags from the same function, so they can never disagree. At most 8 tags per key, each valid like a key; order and duplicates do not matter.
The cache's authoritative level — L2 if there is one, else L1 — must implement TagStore; New checks it.
type Store ¶
type Store interface {
// Get returns the value stored under key. ok is false when there is no
// live entry; that is not an error. An error means the store could not
// answer.
Get(ctx context.Context, key string) (value []byte, ok bool, err error)
// Set stores value under key, replacing any existing entry, for ttl.
// ttl must be positive.
Set(ctx context.Context, key string, value []byte, ttl time.Duration) error
// Delete removes the entry under key. Deleting a missing key is not an
// error.
Delete(ctx context.Context, key string) error
}
Store is one cache level: in-process (the memory package) or shared (the redisstore module). Both levels hold encoded bytes behind this one interface (ADR-0014), so any implementation can be checked against the same conformance suite (RF-14).
Keys are physical keys already built and validated by Cache (ADR-0008). Implementations must be safe for concurrent use, must honour ctx, must not retain the value slice passed to Set, and must not let a caller's changes to a slice returned by Get reach the stored entry.
type TagStore ¶
type TagStore interface {
Store
// GetTagged returns the value stored under key, as Get does, together with
// the current generation of each counter in genKeys, in order. A missing
// counter is created, with genTTL. Where the store allows, value and
// generations are read in one round trip.
GetTagged(ctx context.Context, key string, genKeys []string, genTTL time.Duration) (value []byte, ok bool, gens []uint64, err error)
// Bump advances each counter in genKeys to a value it has never held,
// creating a missing one as GetTagged does, and sets its TTL to genTTL.
Bump(ctx context.Context, genKeys []string, genTTL time.Duration) error
}
TagStore is a Store that also keeps tag generation counters, which tag invalidation needs from the cache's authoritative level: L2 when there is one, else L1 (RF-10, ADR-0005).
Counters are addressed by keys the Cache builds. A missing counter — never written, expired or evicted — must be created at an unpredictable value in [1, 2^62) (NewGeneration), never at a small or fixed one: an evicted counter that restarted low could repeat a generation an old entry recorded, and resurrect it (T-14).
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package bus carries invalidation events between cache instances (RF-11, ADR-0006).
|
Package bus carries invalidation events between cache instances (RF-11, ADR-0006). |
|
Package cisterntest is the conformance suite for cistern.Store implementations (RF-14).
|
Package cisterntest is the conformance suite for cistern.Store implementations (RF-14). |
|
Package codec defines how cistern turns values into the bytes it stores, and provides the JSON default (RF-13).
|
Package codec defines how cistern turns values into the bytes it stores, and provides the JSON default (RF-13). |
|
internal
|
|
|
envelope
Package envelope is the storage format of every cistern entry (ADR-0009, amended by ADR-0005): a header — version, flags, logical expiry, codec id, tag generations — followed by the encoded value.
|
Package envelope is the storage format of every cistern entry (ADR-0009, amended by ADR-0005): a header — version, flags, logical expiry, codec id, tag generations — followed by the encoded value. |
|
singleflight
Package singleflight coalesces concurrent loads of the same key into one call (RF-05), with the semantics ADR-0007 records: the load runs detached from any single caller, is bounded by a timeout after which the key is free again, and a panic reaches every caller still waiting.
|
Package singleflight coalesces concurrent loads of the same key into one call (RF-05), with the semantics ADR-0007 records: the load runs detached from any single caller, is bounded by a timeout after which the key is free again, and a panic reaches every caller still waiting. |
|
Package memory is cistern's in-process L1 Store: an LRU over encoded bytes, bounded by an entry count and a byte size, with a TTL per entry.
|
Package memory is cistern's in-process L1 Store: an LRU over encoded bytes, bounded by an entry count and a byte size, with a TTL per entry. |
|
redisstore
module
|