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 ( import (
"context" "context"
"strings"
"sync" "sync"
"time" "time"
@ -143,10 +144,29 @@ func (pm *Manager) FetchTokenMarketValues(symbols []string, currency string) (ma
} }
marketValues := result.(map[string]thirdparty.TokenMarketValues) marketValues := result.(map[string]thirdparty.TokenMarketValues)
pm.updateMarketCache(currency, marketValues)
return marketValues, nil 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 { func (pm *Manager) GetCachedTokenMarketValues() MarketValuesPerCurrencyAndToken {
pm.marketCacheLock.RLock() pm.marketCacheLock.RLock()
defer pm.marketCacheLock.RUnlock() defer pm.marketCacheLock.RUnlock()

View File

@ -3,6 +3,7 @@ package market
import ( import (
"errors" "errors"
"testing" "testing"
"time"
"github.com/golang/mock/gomock" "github.com/golang/mock/gomock"
@ -184,7 +185,7 @@ func TestFetchTokenMarketValues(t *testing.T) {
require.Nil(t, marketValues) require.Nil(t, marketValues)
} }
func TestGetOrFetchTokenMarketValues(t *testing.T) { func TestCachedFetchTokenMarketValues(t *testing.T) {
ctrl := gomock.NewController(t) ctrl := gomock.NewController(t)
defer ctrl.Finish() defer ctrl.Finish()
@ -235,74 +236,41 @@ func TestGetOrFetchTokenMarketValues(t *testing.T) {
provider.EXPECT().ID().Return("MockMarketProvider").AnyTimes() provider.EXPECT().ID().Return("MockMarketProvider").AnyTimes()
manager := setupMarketManager(t, []thirdparty.MarketDataProvider{provider}) 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 // Test: ensure errors are propagated
provider.EXPECT().FetchTokenMarketValues(symbols, currency).Return(nil, errors.New("error")) 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.Error(t, err)
require.Nil(t, marketValues) require.Nil(t, marketValues)
// Test: ensure token market values are retrieved // Test: ensure token market values are retrieved
provider.EXPECT().FetchTokenMarketValues(symbols, currency).Return(initialTokenMarketValues, nil) 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.NoError(t, err)
require.Equal(t, initialTokenMarketValues, marketValues) require.Equal(t, initialTokenMarketValues, marketValues)
// Test: ensure token market values are cached // Test: ensure token market values are cached
provider.EXPECT().FetchTokenMarketValues(symbols, currency).Return(updatedTokenMarketValues, nil) provider.EXPECT().FetchTokenMarketValues(symbols, currency).Return(updatedTokenMarketValues, nil).MaxTimes(0)
marketValues, err = manager.GetOrFetchTokenMarketValues(symbols, currency, 10) marketValues, err = cache.Get(cacheKey, fresh)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, initialTokenMarketValues, marketValues) require.Equal(t, initialTokenMarketValues, marketValues)
// Test: ensure token market values are updated // Test: ensure token market values are updated when requesting fresh data
marketValues, err = manager.GetOrFetchTokenMarketValues(symbols, currency, -1) provider.EXPECT().FetchTokenMarketValues(symbols, currency).Return(updatedTokenMarketValues, nil).Times(1)
fresh = true
marketValues, err = cache.Get(cacheKey, fresh)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, updatedTokenMarketValues, marketValues) require.Equal(t, updatedTokenMarketValues, marketValues)
}
func TestGetCachedTokenMarketValues(t *testing.T) { // Test: stale data is ignored and the cache is refreshed
ctrl := gomock.NewController(t) time.Sleep(ttl + time.Millisecond)
defer ctrl.Finish() provider.EXPECT().FetchTokenMarketValues(symbols, currency).Return(initialTokenMarketValues, nil).Times(1)
fresh = false
symbols := []string{"BTC", "ETH"} marketValues, err = cache.Get(cacheKey, fresh)
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()
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, initialTokenMarketValues, marketValues)
for _, token := range symbols {
tokenMarketValues := marketValues[token]
cachedTokenMarketValues := tokenMarketCache[currency][token]
require.Equal(t, cachedTokenMarketValues.MarketValues, tokenMarketValues)
}
} }

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"math" "math"
"math/big" "math/big"
"strings"
"sync" "sync"
"time" "time"
@ -501,9 +502,13 @@ func (r *Reader) GetWalletToken(ctx context.Context, clients map[uint64]chain.Cl
}) })
group.Add(func(parent context.Context) error { 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 { if err != nil {
log.Info("marketManager.GetOrFetchTokenMarketValues err", err) log.Info("marketManager.FetchTokenMarketValues err", err)
} }
return nil return nil
}) })