WIP Decoupled caching

This commit is contained in:
Icaro Motta 2024-09-13 19:51:24 -03:00
parent 9e943699f5
commit 86ad257585
No known key found for this signature in database
GPG Key ID: 009557D9D014DF07
4 changed files with 116 additions and 56 deletions

View File

@ -0,0 +1,67 @@
package market
import (
"sync"
"time"
)
type cacheItem[V any] struct {
value V
expiration time.Time
}
type Cache[K comparable, V any] struct {
data map[K]cacheItem[V]
lock sync.RWMutex
ttl time.Duration
fetcher func(key K) (V, error)
}
func NewCache[K comparable, V any](ttl time.Duration, fetchFn func(key K) (V, error)) *Cache[K, V] {
return &Cache[K, V]{
data: make(map[K]cacheItem[V]),
ttl: ttl,
fetcher: fetchFn,
}
}
func (c *Cache[K, V]) Get(key K, fresh bool) (V, error) {
if fresh {
return c.refresh(key, fresh)
}
c.lock.RLock()
item, exists := c.data[key]
c.lock.RUnlock()
if exists && time.Now().Before(item.expiration) {
return item.value, nil
}
return c.refresh(key, fresh)
}
func (c *Cache[K, V]) refresh(key K, fresh bool) (V, error) {
c.lock.Lock()
defer c.lock.Unlock()
if !fresh {
item, exists := c.data[key]
if exists && time.Now().Before(item.expiration) {
return item.value, nil
}
}
value, err := c.fetcher(key)
if err != nil {
var zero V
return zero, err
}
c.data[key] = cacheItem[V]{
value: value,
expiration: time.Now().Add(c.ttl),
}
return value, nil
}

View File

@ -2,6 +2,7 @@ package market
import (
"context"
"strings"
"sync"
"time"
@ -143,10 +144,29 @@ func (pm *Manager) FetchTokenMarketValues(symbols []string, currency string) (ma
}
marketValues := result.(map[string]thirdparty.TokenMarketValues)
pm.updateMarketCache(currency, marketValues)
return marketValues, nil
}
func (pm *Manager) MakeCacheForFetchTokenMarketValues(ttl time.Duration) *Cache[string, map[string]thirdparty.TokenMarketValues] {
parseKey := func(key string) (string, []string) {
keyParts := strings.Split(key, "-")
currency := keyParts[0]
tokenSymbols := keyParts[1:]
return currency, tokenSymbols
}
fetcher := func(key string) (map[string]thirdparty.TokenMarketValues, error) {
currency, tokenSymbols := parseKey(key)
return pm.FetchTokenMarketValues(tokenSymbols, currency)
}
return NewCache(ttl, fetcher)
}
func GenerateCacheKeyForFetchTokenMarketValues(currency string, tokenSymbols []string) string {
return currency + "-" + strings.Join(tokenSymbols, "-")
}
func (pm *Manager) GetCachedTokenMarketValues() MarketValuesPerCurrencyAndToken {
pm.marketCacheLock.RLock()
defer pm.marketCacheLock.RUnlock()

View File

@ -3,6 +3,7 @@ package market
import (
"errors"
"testing"
"time"
"github.com/golang/mock/gomock"
@ -184,7 +185,7 @@ func TestFetchTokenMarketValues(t *testing.T) {
require.Nil(t, marketValues)
}
func TestGetOrFetchTokenMarketValues(t *testing.T) {
func TestCachedFetchTokenMarketValues(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
@ -235,74 +236,41 @@ func TestGetOrFetchTokenMarketValues(t *testing.T) {
provider.EXPECT().ID().Return("MockMarketProvider").AnyTimes()
manager := setupMarketManager(t, []thirdparty.MarketDataProvider{provider})
ttl := 200 * time.Millisecond
cache := manager.MakeCacheForFetchTokenMarketValues(ttl)
cacheKey := GenerateCacheKeyForFetchTokenMarketValues(currency, symbols)
fresh := false
// Test: ensure errors are propagated
provider.EXPECT().FetchTokenMarketValues(symbols, currency).Return(nil, errors.New("error"))
marketValues, err := manager.GetOrFetchTokenMarketValues(symbols, currency, 0)
marketValues, err := cache.Get(cacheKey, fresh)
require.Error(t, err)
require.Nil(t, marketValues)
// Test: ensure token market values are retrieved
provider.EXPECT().FetchTokenMarketValues(symbols, currency).Return(initialTokenMarketValues, nil)
marketValues, err = manager.GetOrFetchTokenMarketValues(symbols, currency, 10)
marketValues, err = cache.Get(cacheKey, fresh)
require.NoError(t, err)
require.Equal(t, initialTokenMarketValues, marketValues)
// Test: ensure token market values are cached
provider.EXPECT().FetchTokenMarketValues(symbols, currency).Return(updatedTokenMarketValues, nil)
marketValues, err = manager.GetOrFetchTokenMarketValues(symbols, currency, 10)
provider.EXPECT().FetchTokenMarketValues(symbols, currency).Return(updatedTokenMarketValues, nil).MaxTimes(0)
marketValues, err = cache.Get(cacheKey, fresh)
require.NoError(t, err)
require.Equal(t, initialTokenMarketValues, marketValues)
// Test: ensure token market values are updated
marketValues, err = manager.GetOrFetchTokenMarketValues(symbols, currency, -1)
// Test: ensure token market values are updated when requesting fresh data
provider.EXPECT().FetchTokenMarketValues(symbols, currency).Return(updatedTokenMarketValues, nil).Times(1)
fresh = true
marketValues, err = cache.Get(cacheKey, fresh)
require.NoError(t, err)
require.Equal(t, updatedTokenMarketValues, marketValues)
}
func TestGetCachedTokenMarketValues(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
symbols := []string{"BTC", "ETH"}
currency := "EUR"
initialTokenMarketValues := map[string]thirdparty.TokenMarketValues{
"BTC": {
MKTCAP: 1000000000,
HIGHDAY: 1.23456,
LOWDAY: 1.00000,
CHANGEPCTHOUR: 0.1,
CHANGEPCTDAY: 0.2,
CHANGEPCT24HOUR: 0.3,
CHANGE24HOUR: 0.4,
},
"ETH": {
MKTCAP: 2000000000,
HIGHDAY: 4.56789,
LOWDAY: 4.00000,
CHANGEPCTHOUR: 0.5,
CHANGEPCTDAY: 0.6,
CHANGEPCT24HOUR: 0.7,
CHANGE24HOUR: 0.8,
},
}
provider := mock_thirdparty.NewMockMarketDataProvider(ctrl)
provider.EXPECT().ID().Return("MockMarketProvider").AnyTimes()
manager := setupMarketManager(t, []thirdparty.MarketDataProvider{provider})
// Test: ensure token market cache is empty
tokenMarketCache := manager.GetCachedTokenMarketValues()
require.Empty(t, tokenMarketCache)
// Test: ensure token market values are retrieved
provider.EXPECT().FetchTokenMarketValues(symbols, currency).Return(initialTokenMarketValues, nil)
marketValues, err := manager.GetOrFetchTokenMarketValues(symbols, currency, 10)
tokenMarketCache = manager.GetCachedTokenMarketValues()
// Test: stale data is ignored and the cache is refreshed
time.Sleep(ttl + time.Millisecond)
provider.EXPECT().FetchTokenMarketValues(symbols, currency).Return(initialTokenMarketValues, nil).Times(1)
fresh = false
marketValues, err = cache.Get(cacheKey, fresh)
require.NoError(t, err)
for _, token := range symbols {
tokenMarketValues := marketValues[token]
cachedTokenMarketValues := tokenMarketCache[currency][token]
require.Equal(t, cachedTokenMarketValues.MarketValues, tokenMarketValues)
}
require.Equal(t, initialTokenMarketValues, marketValues)
}

View File

@ -4,6 +4,7 @@ import (
"context"
"math"
"math/big"
"strings"
"sync"
"time"
@ -501,9 +502,13 @@ func (r *Reader) GetWalletToken(ctx context.Context, clients map[uint64]chain.Cl
})
group.Add(func(parent context.Context) error {
tokenMarketValues, err = r.marketManager.GetOrFetchTokenMarketValues(tokenSymbols, currency, 60)
ttl := 60 * time.Second
cache := r.marketManager.MakeCacheForFetchTokenMarketValues(ttl)
fresh := false
tokenMarketValues, err = cache.Get(market.GenerateCacheKeyForFetchTokenMarketValues(currency, tokenSymbols), fresh)
if err != nil {
log.Info("marketManager.GetOrFetchTokenMarketValues err", err)
log.Info("marketManager.FetchTokenMarketValues err", err)
}
return nil
})