Files
Andrey Bocharnikov 4f622fb238 feat(cache): add multi-level cache
* l1 biggache, l2 keydb
* extracted from status-im/eth-rpc-proxy to reuse in another projects

fixes status-im/eth-rpc-proxy#106
2026-02-05 22:40:32 +04:00

230 lines
5.9 KiB
Go

package multi
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"go.uber.org/mock/gomock"
"github.com/status-im/proxy-common/cache"
"github.com/status-im/proxy-common/cache/mock"
"github.com/status-im/proxy-common/models"
)
func TestNewMultiCache(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
cache1 := mock.NewMockCache(ctrl)
cache2 := mock.NewMockCache(ctrl)
caches := []cache.Cache{cache1, cache2}
multiCache := NewMultiCache(caches, true)
assert.NotNil(t, multiCache)
mc := multiCache.(*MultiCache)
assert.Equal(t, 2, len(mc.caches))
assert.Equal(t, cache1, mc.caches[0])
assert.Equal(t, cache2, mc.caches[1])
}
func TestMultiCache_Get_FirstCacheHit(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
cache1 := mock.NewMockCache(ctrl)
cache2 := mock.NewMockCache(ctrl)
caches := []cache.Cache{cache1, cache2}
multiCache := NewMultiCache(caches, true)
expectedEntry := &models.CacheEntry{
Data: []byte("test-value"),
CreatedAt: time.Now().Unix(),
StaleAt: time.Now().Unix() + 60,
ExpiresAt: time.Now().Unix() + 120,
}
cache1.EXPECT().Get("test-key").Return(expectedEntry, true).Times(1)
// cache2.Get should not be called since cache1 has the value
entry, found := multiCache.Get("test-key")
assert.True(t, found)
assert.Equal(t, expectedEntry, entry)
}
func TestMultiCache_Get_SecondCacheHit(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
cache1 := mock.NewMockCache(ctrl)
cache2 := mock.NewMockCache(ctrl)
caches := []cache.Cache{cache1, cache2}
multiCache := NewMultiCache(caches, true)
expectedEntry := &models.CacheEntry{
Data: []byte("test-value"),
CreatedAt: time.Now().Unix(),
StaleAt: time.Now().Unix() + 60,
ExpiresAt: time.Now().Unix() + 120,
}
cache1.EXPECT().Get("test-key").Return(nil, false).Times(1)
cache2.EXPECT().Get("test-key").Return(expectedEntry, true).Times(1)
// Expect propagation to cache1
cache1.EXPECT().Set("test-key", expectedEntry.Data, gomock.Any()).Times(1)
entry, found := multiCache.Get("test-key")
assert.True(t, found)
assert.Equal(t, expectedEntry, entry)
}
func TestMultiCache_Get_AllCachesMiss(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
cache1 := mock.NewMockCache(ctrl)
cache2 := mock.NewMockCache(ctrl)
caches := []cache.Cache{cache1, cache2}
multiCache := NewMultiCache(caches, true)
cache1.EXPECT().Get("test-key").Return(nil, false).Times(1)
cache2.EXPECT().Get("test-key").Return(nil, false).Times(1)
entry, found := multiCache.Get("test-key")
assert.False(t, found)
assert.Nil(t, entry)
}
func TestMultiCache_Get_NoCaches(t *testing.T) {
multiCache := NewMultiCache([]cache.Cache{}, true)
entry, found := multiCache.Get("test-key")
assert.False(t, found)
assert.Nil(t, entry)
}
func TestMultiCache_GetStale_FirstCacheHit(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
cache1 := mock.NewMockCache(ctrl)
cache2 := mock.NewMockCache(ctrl)
caches := []cache.Cache{cache1, cache2}
multiCache := NewMultiCache(caches, true)
expectedEntry := &models.CacheEntry{
Data: []byte("test-value"),
CreatedAt: time.Now().Unix(),
StaleAt: time.Now().Unix() - 30, // stale but not expired
ExpiresAt: time.Now().Unix() + 60,
}
cache1.EXPECT().GetStale("test-key").Return(expectedEntry, true).Times(1)
entry, found := multiCache.GetStale("test-key")
assert.True(t, found)
assert.Equal(t, expectedEntry, entry)
}
func TestMultiCache_GetStale_SecondCacheHit(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
cache1 := mock.NewMockCache(ctrl)
cache2 := mock.NewMockCache(ctrl)
caches := []cache.Cache{cache1, cache2}
multiCache := NewMultiCache(caches, true)
expectedEntry := &models.CacheEntry{
Data: []byte("test-value"),
CreatedAt: time.Now().Unix(),
StaleAt: time.Now().Unix() - 30, // stale but not expired
ExpiresAt: time.Now().Unix() + 60,
}
cache1.EXPECT().GetStale("test-key").Return(nil, false).Times(1)
cache2.EXPECT().GetStale("test-key").Return(expectedEntry, true).Times(1)
// Expect propagation to cache1
cache1.EXPECT().Set("test-key", expectedEntry.Data, gomock.Any()).Times(1)
entry, found := multiCache.GetStale("test-key")
assert.True(t, found)
assert.Equal(t, expectedEntry, entry)
}
func TestMultiCache_Set_AllCaches(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
cache1 := mock.NewMockCache(ctrl)
cache2 := mock.NewMockCache(ctrl)
caches := []cache.Cache{cache1, cache2}
multiCache := NewMultiCache(caches, true)
testVal := []byte("test-value")
testTTL := models.TTL{Fresh: 60 * time.Second, Stale: 30 * time.Second}
cache1.EXPECT().Set("test-key", testVal, testTTL).Times(1)
cache2.EXPECT().Set("test-key", testVal, testTTL).Times(1)
multiCache.Set("test-key", testVal, testTTL)
}
func TestMultiCache_Set_NoCaches(t *testing.T) {
multiCache := NewMultiCache([]cache.Cache{}, true)
testVal := []byte("test-value")
testTTL := models.TTL{Fresh: 60 * time.Second, Stale: 30 * time.Second}
// Should not panic
multiCache.Set("test-key", testVal, testTTL)
}
func TestMultiCache_Delete_AllCaches(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
cache1 := mock.NewMockCache(ctrl)
cache2 := mock.NewMockCache(ctrl)
caches := []cache.Cache{cache1, cache2}
multiCache := NewMultiCache(caches, true)
cache1.EXPECT().Delete("test-key").Times(1)
cache2.EXPECT().Delete("test-key").Times(1)
multiCache.Delete("test-key")
}
func TestMultiCache_Delete_NoCaches(t *testing.T) {
multiCache := NewMultiCache([]cache.Cache{}, true)
// Should not panic
multiCache.Delete("test-key")
}
func TestMultiCache_GetCacheCount(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
cache1 := mock.NewMockCache(ctrl)
cache2 := mock.NewMockCache(ctrl)
caches := []cache.Cache{cache1, cache2}
multiCache := NewMultiCache(caches, true)
mc := multiCache.(*MultiCache)
assert.Equal(t, 2, mc.GetCacheCount())
}