WIP Decoupled caching
This commit is contained in:
parent
9e943699f5
commit
86ad257585
|
@ -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
|
||||
}
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
})
|
||||
|
|
Loading…
Reference in New Issue