From c10bf9cdb2ef0588db57d667a29250bb34e3cbf9 Mon Sep 17 00:00:00 2001 From: Benjamin Bengfort Date: Mon, 12 Jan 2026 14:23:21 -0600 Subject: [PATCH 1/4] In memory and Ristretto Caches --- cache.go | 6 +- go.mod | 4 + go.sum | 8 ++ inmem.go | 6 +- inmem_bench_test.go | 90 +++++++++++++++++++++ inmem_test.go | 50 ++++++++++++ ristretto/config.go | 128 ++++++++++++++++++++++++++++++ ristretto/ristretto.go | 85 ++++++++++++++++++++ ristretto/ristretto_bench_test.go | 115 +++++++++++++++++++++++++++ ristretto/ristretto_test.go | 62 +++++++++++++++ 10 files changed, 549 insertions(+), 5 deletions(-) create mode 100644 inmem_bench_test.go create mode 100644 inmem_test.go create mode 100644 ristretto/config.go create mode 100644 ristretto/ristretto.go create mode 100644 ristretto/ristretto_bench_test.go create mode 100644 ristretto/ristretto_test.go diff --git a/cache.go b/cache.go index 32aada4..7c1c414 100644 --- a/cache.go +++ b/cache.go @@ -17,8 +17,8 @@ type Cache interface { // Put stores the []byte representation of a response in the cache with a key. Put(string, []byte) - // Rm removes the cached response associated with the key. - Rm(string) + // Del removes the cached response associated with the key. + Del(string) } // CachedResponse returns the cached http.Response for the request if present and nil @@ -54,7 +54,7 @@ func cacheKey(req *http.Request) string { return req.Method + " " + req.URL.String() } -// cacheKeyWithHeaders returns the cach key for a request and includes the specified +// cacheKeyWithHeaders returns the cache key for a request and includes the specified // headers in their canonical form. This allows you to differentiate cache entries // based on header values such as Authorization or custom headers. func cacheKeyWithHeaders(req *http.Request, headers []string) string { diff --git a/go.mod b/go.mod index e6e48a0..e127278 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,11 @@ go 1.25.1 require github.com/stretchr/testify v1.11.1 require ( + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/dgraph-io/ristretto/v2 v2.3.0 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + golang.org/x/sys v0.35.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index c4c1710..dfa3385 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,17 @@ +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgraph-io/ristretto/v2 v2.3.0 h1:qTQ38m7oIyd4GAed/QkUZyPFNMnvVWyazGXRwvOt5zk= +github.com/dgraph-io/ristretto/v2 v2.3.0/go.mod h1:gpoRV3VzrEY1a9dWAYV6T1U7YzfgttXdd/ZzL1s9OZM= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= +golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/inmem.go b/inmem.go index d4ba291..61efdfc 100644 --- a/inmem.go +++ b/inmem.go @@ -10,6 +10,8 @@ type InMemoryCache struct { store map[string][]byte } +var _ Cache = (*InMemoryCache)(nil) + // Get the []byte representation of the response and true if present. func (c *InMemoryCache) Get(key string) (val []byte, ok bool) { c.RLock() @@ -28,8 +30,8 @@ func (c *InMemoryCache) Put(key string, val []byte) { c.Unlock() } -// Rm removes the cached response associated with the key. -func (c *InMemoryCache) Rm(key string) { +// Del removes the cached response associated with the key. +func (c *InMemoryCache) Del(key string) { c.Lock() delete(c.store, key) c.Unlock() diff --git a/inmem_bench_test.go b/inmem_bench_test.go new file mode 100644 index 0000000..2f9a9aa --- /dev/null +++ b/inmem_bench_test.go @@ -0,0 +1,90 @@ +package httpcache_test + +import ( + "testing" + + "go.rtnl.ai/httpcache" +) + +func benchmarkGet(size int) func(b *testing.B) { + return func(b *testing.B) { + cache := &httpcache.InMemoryCache{} + value := make([]byte, size) + + // Prepopulate the cache + for i := 0; i < 128; i++ { + key := string(rune('a' + i)) + cache.Put(key, value) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + cache.Get(string(rune('a' + i%192))) + } + } +} + +func BenchmarkInMemoryCacheGet(b *testing.B) { + b.Run("Small", benchmarkGet(512)) + b.Run("Realistic", benchmarkGet(2048)) + b.Run("Large", benchmarkGet(5.243e+6)) +} + +func benchmarkPut(size int) func(b *testing.B) { + return func(b *testing.B) { + cache := &httpcache.InMemoryCache{} + value := make([]byte, size) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + cache.Put(string(rune('a'+i%192)), value) + } + } +} + +func BenchmarkInMemoryCachePut(b *testing.B) { + b.Run("Small", benchmarkPut(512)) + b.Run("Realistic", benchmarkPut(2048)) + b.Run("Large", benchmarkPut(5.243e+6)) +} + +// Benchmark mixed operations +func BenchmarkInMemoryCacheMixed(b *testing.B) { + cache := &httpcache.InMemoryCache{} + value := make([]byte, 1024) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + key := string(rune('a' + i%128)) + switch i % 3 { + case 0: + cache.Put(key, value) + case 1: + cache.Get(key) + case 2: + cache.Del(key) + } + } +} + +// Benchmark concurrent mixed operations +func BenchmarkInMemoryCacheParallelMixed(b *testing.B) { + cache := &httpcache.InMemoryCache{} + value := make([]byte, 1024) + + b.RunParallel(func(pb *testing.PB) { + i := 0 + for pb.Next() { + key := string(rune('a' + i%128)) + switch i % 3 { + case 0: + cache.Put(key, value) + case 1: + cache.Get(key) + case 2: + cache.Del(key) + } + i++ + } + }) +} diff --git a/inmem_test.go b/inmem_test.go new file mode 100644 index 0000000..ac42f14 --- /dev/null +++ b/inmem_test.go @@ -0,0 +1,50 @@ +package httpcache_test + +import ( + "math/rand/v2" + "sync" + "testing" + + "github.com/stretchr/testify/require" + "go.rtnl.ai/httpcache" +) + +func TestInMemoryCache(t *testing.T) { + cache := &httpcache.InMemoryCache{} + cache.Put("foo", []byte("bar")) + + val, ok := cache.Get("foo") + require.True(t, ok) + require.Equal(t, []byte("bar"), val) + + cache.Del("foo") + _, ok = cache.Get("foo") + require.False(t, ok) +} + +func TestInMemoryRace(t *testing.T) { + // Ensures no race conditions occur during concurrent access. + cache := &httpcache.InMemoryCache{} + value := make([]byte, 2048) + + var wg sync.WaitGroup + for i := 0; i < 16; i++ { + wg.Add(1) + go func() { + defer wg.Done() + for j := 0; j < 512; j++ { + k := rand.IntN(64) + key := string(rune('a' + k%16)) + switch k % 3 { + case 0: + cache.Put(key, value) + case 1: + cache.Get(key) + case 2: + cache.Del(key) + } + } + }() + } + wg.Wait() +} diff --git a/ristretto/config.go b/ristretto/config.go new file mode 100644 index 0000000..6ba75cf --- /dev/null +++ b/ristretto/config.go @@ -0,0 +1,128 @@ +package ristretto + +import "github.com/dgraph-io/ristretto/v2" + +// Config is copied from ristretto.Config and uses the httpcache key and value types. +// It allows users to configure the Ristretto cache used by the Ristretto-backed with +// the documentation stored in httpcache rather than ristretto. +type Config struct { + // NumCounters determines the number of counters (keys) to keep that hold + // access frequency information. It's generally a good idea to have more + // counters than the max cache capacity, as this will improve eviction + // accuracy and subsequent hit ratios. + // + // For example, if you expect your cache to hold 1,000,000 items when full, + // NumCounters should be 10,000,000 (10x). Each counter takes up roughly + // 3 bytes (4 bits for each counter * 4 copies plus about a byte per + // counter for the bloom filter). Note that the number of counters is + // internally rounded up to the nearest power of 2, so the space usage + // may be a little larger than 3 bytes * NumCounters. + // + // We've seen good performance in setting this to 10x the number of items + // you expect to keep in the cache when full. + NumCounters int64 + + // MaxCost is how eviction decisions are made. For example, if MaxCost is + // 100 and a new item with a cost of 1 increases total cache cost to 101, + // 1 item will be evicted. + // + // MaxCost can be considered as the cache capacity, in whatever units you + // choose to use. + // + // For example, if you want the cache to have a max capacity of 100MB, you + // would set MaxCost to 100,000,000 and pass an item's number of bytes as + // the `cost` parameter for calls to Set. If new items are accepted, the + // eviction process will take care of making room for the new item and not + // overflowing the MaxCost value. + // + // MaxCost could be anything as long as it matches how you're using the cost + // values when calling Set. + MaxCost int64 + + // BufferItems determines the size of Get buffers. + // + // Unless you have a rare use case, using `64` as the BufferItems value + // results in good performance. + // + // If for some reason you see Get performance decreasing with lots of + // contention (you shouldn't), try increasing this value in increments of 64. + // This is a fine-tuning mechanism and you probably won't have to touch this. + BufferItems int64 + + // Metrics is true when you want variety of stats about the cache. + // There is some overhead to keeping statistics, so you should only set this + // flag to true when testing or throughput performance isn't a major factor. + Metrics bool + + // OnEvict is called for every eviction with the evicted item. + OnEvict func(item *ristretto.Item[[]byte]) + + // OnReject is called for every rejection done via the policy. + OnReject func(item *ristretto.Item[[]byte]) + + // OnExit is called whenever a value is removed from cache. This can be + // used to do manual memory deallocation. Would also be called on eviction + // as well as on rejection of the value. + OnExit func(val []byte) + + // ShouldUpdate is called when a value already exists in cache and is being updated. + // If ShouldUpdate returns true, the cache continues with the update (Set). If the + // function returns false, no changes are made in the cache. If the value doesn't + // already exist, the cache continue with setting that value for the given key. + // + // In this function, you can check whether the new value is valid. For example, if + // your value has timestamp assosicated with it, you could check whether the new + // value has the latest timestamp, preventing you from setting an older value. + ShouldUpdate func(cur, prev []byte) bool + + // KeyToHash function is used to customize the key hashing algorithm. + // Each key will be hashed using the provided function. If keyToHash value + // is not set, the default keyToHash function is used. + // + // Ristretto has a variety of defaults depending on the underlying interface type + // https://github.com/dgraph-io/ristretto/blob/main/z/z.go#L19-L41). + // + // Note that if you want 128bit hashes you should use the both the values + // in the return of the function. If you want to use 64bit hashes, you can + // just return the first uint64 and return 0 for the second uint64. + KeyToHash func(key string) (uint64, uint64) + + // Cost evaluates a value and outputs a corresponding cost. This function is ran + // after Set is called for a new item or an item is updated with a cost param of 0. + // + // Cost is an optional function you can pass to the Config in order to evaluate + // item cost at runtime, and only when the Set call isn't going to be dropped. This + // is useful if calculating item cost is particularly expensive and you don't want to + // waste time on items that will be dropped anyways. + // + // To signal to Ristretto that you'd like to use this Cost function: + // 1. Set the Cost field to a non-nil function. + // 2. When calling Set for new items or item updates, use a `cost` of 0. + Cost func(value []byte) int64 + + // IgnoreInternalCost set to true indicates to the cache that the cost of + // internally storing the value should be ignored. This is useful when the + // cost passed to set is not using bytes as units. Keep in mind that setting + // this to true will increase the memory usage. + IgnoreInternalCost bool + + // TtlTickerDurationInSec sets the value of time ticker for cleanup keys on TTL expiry. + TtlTickerDurationInSec int64 +} + +func (c *Config) convert() *ristretto.Config[string, []byte] { + return &ristretto.Config[string, []byte]{ + NumCounters: c.NumCounters, + MaxCost: c.MaxCost, + BufferItems: c.BufferItems, + Metrics: c.Metrics, + OnEvict: c.OnEvict, + OnReject: c.OnReject, + OnExit: c.OnExit, + ShouldUpdate: c.ShouldUpdate, + KeyToHash: c.KeyToHash, + Cost: c.Cost, + IgnoreInternalCost: c.IgnoreInternalCost, + TtlTickerDurationInSec: c.TtlTickerDurationInSec, + } +} diff --git a/ristretto/ristretto.go b/ristretto/ristretto.go new file mode 100644 index 0000000..c81b551 --- /dev/null +++ b/ristretto/ristretto.go @@ -0,0 +1,85 @@ +/* +Package ristretto provides a fast, concurrent implementation of httpcache.Cache built +with a focus on performance and correctness using the github.com/dgraph-io/ristretto +library as the underlying storage. + +This backend is suitable for applications that need to cache millions of entries in high +throughput environments with hundreds of threads accessing the cache concurrently. + +Example Usage: + + cache := ristretto.New(&ristretto.Config{ + NumCounters: 1e7, // number of keys to track frequency of (10M). + MaxCost: 1 << 30, // maximum cost of cache (1GB). + BufferItems: 64, // number of keys per Get buffer. + }) + + transport := httpcache.NewTransport(cache) + client := transport.Client() + + // Later ... + cache.Close() +*/ +package ristretto + +import ( + "io" + + "github.com/dgraph-io/ristretto/v2" + "go.rtnl.ai/httpcache" +) + +type Cache struct { + cache *ristretto.Cache[string, []byte] +} + +var _ httpcache.Cache = (*Cache)(nil) +var _ io.Closer = (*Cache)(nil) + +// Create a new Ristretto-backed httpcache.Cache with the specified configuration. +func New(config *Config) (_ *Cache, err error) { + cache := &Cache{} + if cache.cache, err = ristretto.NewCache(config.convert()); err != nil { + return nil, err + } + + return cache, nil +} + +// Get returns the value (if any) and a boolean representing whether the value was found +// or not. The value can be nil and the boolean can be true at the same time. Get will +// not return expired items. +func (c *Cache) Get(key string) ([]byte, bool) { + return c.cache.Get(key) +} + +// Put attempts to add the key-value item to the cache. If the cache has reached the +// maximum size, it may evict other items to make room for the new item. Put does not +// set an explicity cost for the item; instead, it relies on the Cost function defined +// in the Config to determine the cost of the item. If using a dynamic Cost function, +// it is possible that the item may be dropped and not cached rather than evicting other +// higher value items. +// +// Be careful when modifying the value byte slice after calling Put, calling `append` +// may update the underlying array pointer which will not be reflected in the cache. +func (c *Cache) Put(key string, value []byte) { + c.cache.Set(key, value, 0) +} + +// Del deletes the key-value item from the cache if it exists. +func (c *Cache) Del(key string) { + c.cache.Del(key) +} + +// Close stops all goroutines and closes all channels. +// Implements io.Closer. +func (c *Cache) Close() error { + c.cache.Close() + return nil +} + +// Wait blocks until all buffered writes have been applied. +// This ensures a call to Put() will be visible to future calls to Get(). +func (c *Cache) Wait() { + c.cache.Wait() +} diff --git a/ristretto/ristretto_bench_test.go b/ristretto/ristretto_bench_test.go new file mode 100644 index 0000000..0180c79 --- /dev/null +++ b/ristretto/ristretto_bench_test.go @@ -0,0 +1,115 @@ +package ristretto_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + "go.rtnl.ai/httpcache/ristretto" +) + +func benchmarkGet(size int) func(b *testing.B) { + return func(b *testing.B) { + cache, err := ristretto.New(&ristretto.Config{ + NumCounters: 1e7, // number of keys to track frequency of (10M). + MaxCost: 1 << 30, // maximum cost of cache (1GB). + BufferItems: 64, // number of keys per Get buffer. + }) + + require.NoError(b, err) + value := make([]byte, size) + + // Prepopulate the cache + for i := 0; i < 128; i++ { + key := string(rune('a' + i)) + cache.Put(key, value) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + cache.Get(string(rune('a' + i%192))) + } + } +} + +func BenchmarkRistrettoCacheGet(b *testing.B) { + b.Run("Small", benchmarkGet(512)) + b.Run("Realistic", benchmarkGet(2048)) + b.Run("Large", benchmarkGet(5.243e+6)) +} + +func benchmarkPut(size int) func(b *testing.B) { + return func(b *testing.B) { + cache, err := ristretto.New(&ristretto.Config{ + NumCounters: 1e7, // number of keys to track frequency of (10M). + MaxCost: 1 << 30, // maximum cost of cache (1GB). + BufferItems: 64, // number of keys per Get buffer. + }) + + require.NoError(b, err) + value := make([]byte, size) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + cache.Put(string(rune('a'+i%192)), value) + } + } +} + +func BenchmarkRistrettoCachePut(b *testing.B) { + b.Run("Small", benchmarkPut(512)) + b.Run("Realistic", benchmarkPut(2048)) + b.Run("Large", benchmarkPut(5.243e+6)) +} + +// Benchmark mixed operations +func BenchmarkRistrettoCacheMixed(b *testing.B) { + cache, err := ristretto.New(&ristretto.Config{ + NumCounters: 1e7, // number of keys to track frequency of (10M). + MaxCost: 1 << 30, // maximum cost of cache (1GB). + BufferItems: 64, // number of keys per Get buffer. + }) + + require.NoError(b, err) + value := make([]byte, 1024) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + key := string(rune('a' + i%128)) + switch i % 3 { + case 0: + cache.Put(key, value) + case 1: + cache.Get(key) + case 2: + cache.Del(key) + } + } +} + +// Benchmark concurrent mixed operations +func BenchmarkRistrettoCacheParallelMixed(b *testing.B) { + cache, err := ristretto.New(&ristretto.Config{ + NumCounters: 1e7, // number of keys to track frequency of (10M). + MaxCost: 1 << 30, // maximum cost of cache (1GB). + BufferItems: 64, // number of keys per Get buffer. + }) + + require.NoError(b, err) + value := make([]byte, 1024) + + b.RunParallel(func(pb *testing.PB) { + i := 0 + for pb.Next() { + key := string(rune('a' + i%128)) + switch i % 3 { + case 0: + cache.Put(key, value) + case 1: + cache.Get(key) + case 2: + cache.Del(key) + } + i++ + } + }) +} diff --git a/ristretto/ristretto_test.go b/ristretto/ristretto_test.go new file mode 100644 index 0000000..c9d636c --- /dev/null +++ b/ristretto/ristretto_test.go @@ -0,0 +1,62 @@ +package ristretto_test + +import ( + "math/rand/v2" + "sync" + "testing" + + "github.com/stretchr/testify/require" + "go.rtnl.ai/httpcache/ristretto" +) + +func TestRistrettoCache(t *testing.T) { + cache, err := ristretto.New(&ristretto.Config{ + NumCounters: 1e7, // number of keys to track frequency of (10M). + MaxCost: 1 << 30, // maximum cost of cache (1GB). + BufferItems: 64, // number of keys per Get buffer. + }) + require.NoError(t, err) + + cache.Put("foo", []byte("bar")) + cache.Wait() + + val, ok := cache.Get("foo") + require.True(t, ok) + require.Equal(t, []byte("bar"), val) + + cache.Del("foo") + _, ok = cache.Get("foo") + require.False(t, ok) +} + +func TestRistrettoRace(t *testing.T) { + // Ensures no race conditions occur during concurrent access. + cache, err := ristretto.New(&ristretto.Config{ + NumCounters: 1e7, // number of keys to track frequency of (10M). + MaxCost: 1 << 30, // maximum cost of cache (1GB). + BufferItems: 64, // number of keys per Get buffer. + }) + require.NoError(t, err) + value := make([]byte, 2048) + + var wg sync.WaitGroup + for i := 0; i < 16; i++ { + wg.Add(1) + go func() { + defer wg.Done() + for j := 0; j < 512; j++ { + k := rand.IntN(64) + key := string(rune('a' + k%16)) + switch k % 3 { + case 0: + cache.Put(key, value) + case 1: + cache.Get(key) + case 2: + cache.Del(key) + } + } + }() + } + wg.Wait() +} From fca1ec4e221078c85ad7deac3765bae0214d25e1 Mon Sep 17 00:00:00 2001 From: Benjamin Bengfort Date: Mon, 12 Jan 2026 14:57:27 -0600 Subject: [PATCH 2/4] update go.mod --- go.mod | 6 ++++-- go.sum | 2 ++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index e127278..aadd1ab 100644 --- a/go.mod +++ b/go.mod @@ -2,12 +2,14 @@ module go.rtnl.ai/httpcache go 1.25.1 -require github.com/stretchr/testify v1.11.1 +require ( + github.com/dgraph-io/ristretto/v2 v2.3.0 + github.com/stretchr/testify v1.11.1 +) require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/dgraph-io/ristretto/v2 v2.3.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect golang.org/x/sys v0.35.0 // indirect diff --git a/go.sum b/go.sum index dfa3385..ae7c996 100644 --- a/go.sum +++ b/go.sum @@ -4,6 +4,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgraph-io/ristretto/v2 v2.3.0 h1:qTQ38m7oIyd4GAed/QkUZyPFNMnvVWyazGXRwvOt5zk= github.com/dgraph-io/ristretto/v2 v2.3.0/go.mod h1:gpoRV3VzrEY1a9dWAYV6T1U7YzfgttXdd/ZzL1s9OZM= +github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da h1:aIftn67I1fkbMa512G+w+Pxci9hJPB8oMnkcP3iZF38= +github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= From 01a6e4ae98473f8e19559c14e5e46d4950116c7b Mon Sep 17 00:00:00 2001 From: Benjamin Bengfort Date: Mon, 12 Jan 2026 14:57:57 -0600 Subject: [PATCH 3/4] Update ristretto/ristretto.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Benjamin Bengfort --- ristretto/ristretto.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ristretto/ristretto.go b/ristretto/ristretto.go index c81b551..313a9f7 100644 --- a/ristretto/ristretto.go +++ b/ristretto/ristretto.go @@ -55,7 +55,7 @@ func (c *Cache) Get(key string) ([]byte, bool) { // Put attempts to add the key-value item to the cache. If the cache has reached the // maximum size, it may evict other items to make room for the new item. Put does not -// set an explicity cost for the item; instead, it relies on the Cost function defined +// set an explicitly cost for the item; instead, it relies on the Cost function defined // in the Config to determine the cost of the item. If using a dynamic Cost function, // it is possible that the item may be dropped and not cached rather than evicting other // higher value items. From 7dd3de8959c3e0258f8fe4831265294a46d2eb43 Mon Sep 17 00:00:00 2001 From: Benjamin Bengfort Date: Mon, 12 Jan 2026 14:58:21 -0600 Subject: [PATCH 4/4] Update ristretto/config.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Benjamin Bengfort --- ristretto/config.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ristretto/config.go b/ristretto/config.go index 6ba75cf..a0a2dec 100644 --- a/ristretto/config.go +++ b/ristretto/config.go @@ -71,7 +71,7 @@ type Config struct { // already exist, the cache continue with setting that value for the given key. // // In this function, you can check whether the new value is valid. For example, if - // your value has timestamp assosicated with it, you could check whether the new + // your value has timestamp associated with it, you could check whether the new // value has the latest timestamp, preventing you from setting an older value. ShouldUpdate func(cur, prev []byte) bool