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 (
|
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()
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
})
|
})
|
||||||
|
|
Loading…
Reference in New Issue