Skip to content

Commit 4c4ea8a

Browse files
authored
Merge pull request #1145 from c9s/bhwu/add-market-in-mem-cache
FEATURE: add market info in-mem cache
2 parents 92b8652 + 3f7e617 commit 4c4ea8a

File tree

2 files changed

+200
-4
lines changed

2 files changed

+200
-4
lines changed

pkg/cache/cache.go

+97-4
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,74 @@ import (
88
"os"
99
"path"
1010
"reflect"
11+
"sync"
1112
"time"
1213

1314
"github.com/pkg/errors"
1415
log "github.com/sirupsen/logrus"
1516

1617
"github.com/c9s/bbgo/pkg/types"
18+
"github.com/c9s/bbgo/pkg/util"
19+
"github.com/c9s/bbgo/pkg/util/backoff"
1720
)
1821

19-
type DataFetcher func() (interface{}, error)
22+
const memCacheExpiry = 5 * time.Minute
23+
const fileCacheExpiry = 24 * time.Hour
24+
25+
var globalMarketMemCache *marketMemCache = newMarketMemCache()
26+
27+
type marketMemCache struct {
28+
sync.Mutex
29+
markets map[string]marketMapWithTime
30+
}
31+
32+
type marketMapWithTime struct {
33+
updatedAt time.Time
34+
markets types.MarketMap
35+
}
36+
37+
func newMarketMemCache() *marketMemCache {
38+
cache := &marketMemCache{
39+
markets: make(map[string]marketMapWithTime),
40+
}
41+
return cache
42+
}
43+
44+
func (c *marketMemCache) IsOutdated(exName string) bool {
45+
c.Lock()
46+
defer c.Unlock()
47+
48+
data, ok := c.markets[exName]
49+
return !ok || time.Since(data.updatedAt) > memCacheExpiry
50+
}
51+
52+
func (c *marketMemCache) Set(exName string, markets types.MarketMap) {
53+
c.Lock()
54+
defer c.Unlock()
55+
56+
c.markets[exName] = marketMapWithTime{
57+
updatedAt: time.Now(),
58+
markets: markets,
59+
}
60+
}
61+
62+
func (c *marketMemCache) Get(exName string) (types.MarketMap, bool) {
63+
c.Lock()
64+
defer c.Unlock()
65+
66+
markets, ok := c.markets[exName]
67+
if !ok {
68+
return nil, false
69+
}
2070

21-
const cacheExpiry = 24 * time.Hour
71+
copied := types.MarketMap{}
72+
for key, val := range markets.markets {
73+
copied[key] = val
74+
}
75+
return copied, true
76+
}
77+
78+
type DataFetcher func() (interface{}, error)
2279

2380
// WithCache let you use the cache with the given cache key, variable reference and your data fetcher,
2481
// The key must be an unique ID.
@@ -29,7 +86,7 @@ func WithCache(key string, obj interface{}, fetcher DataFetcher) error {
2986
cacheFile := path.Join(cacheDir, key+".json")
3087

3188
stat, err := os.Stat(cacheFile)
32-
if os.IsNotExist(err) || (stat != nil && time.Since(stat.ModTime()) > cacheExpiry) {
89+
if os.IsNotExist(err) || (stat != nil && time.Since(stat.ModTime()) > fileCacheExpiry) {
3390
log.Debugf("cache %s not found or cache expired, executing fetcher callback to get the data", cacheFile)
3491

3592
data, err := fetcher()
@@ -70,6 +127,42 @@ func WithCache(key string, obj interface{}, fetcher DataFetcher) error {
70127
}
71128

72129
func LoadExchangeMarketsWithCache(ctx context.Context, ex types.Exchange) (markets types.MarketMap, err error) {
130+
inMem, ok := util.GetEnvVarBool("USE_MARKETS_CACHE_IN_MEMORY")
131+
if ok && inMem {
132+
return loadMarketsFromMem(ctx, ex)
133+
}
134+
135+
// fallback to use files as cache
136+
return loadMarketsFromFile(ctx, ex)
137+
}
138+
139+
// loadMarketsFromMem is useful for one process to run multiple bbgos in different go routines.
140+
func loadMarketsFromMem(ctx context.Context, ex types.Exchange) (markets types.MarketMap, _ error) {
141+
exName := ex.Name().String()
142+
if globalMarketMemCache.IsOutdated(exName) {
143+
op := func() error {
144+
rst, err2 := ex.QueryMarkets(ctx)
145+
if err2 != nil {
146+
return err2
147+
}
148+
149+
markets = rst
150+
globalMarketMemCache.Set(exName, rst)
151+
return nil
152+
}
153+
154+
if err := backoff.RetryGeneral(ctx, op); err != nil {
155+
return nil, err
156+
}
157+
158+
return markets, nil
159+
}
160+
161+
rst, _ := globalMarketMemCache.Get(exName)
162+
return rst, nil
163+
}
164+
165+
func loadMarketsFromFile(ctx context.Context, ex types.Exchange) (markets types.MarketMap, err error) {
73166
key := fmt.Sprintf("%s-markets", ex.Name())
74167
if futureExchange, implemented := ex.(types.FuturesExchange); implemented {
75168
settings := futureExchange.GetFuturesSettings()
@@ -82,4 +175,4 @@ func LoadExchangeMarketsWithCache(ctx context.Context, ex types.Exchange) (marke
82175
return ex.QueryMarkets(ctx)
83176
})
84177
return markets, err
85-
}
178+
}

pkg/cache/cache_test.go

+103
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
package cache
2+
3+
import (
4+
"context"
5+
"errors"
6+
"testing"
7+
"time"
8+
9+
"github.com/golang/mock/gomock"
10+
"github.com/stretchr/testify/assert"
11+
12+
"github.com/c9s/bbgo/pkg/types"
13+
"github.com/c9s/bbgo/pkg/types/mocks"
14+
)
15+
16+
func Test_newMarketMemCache(t *testing.T) {
17+
cache := newMarketMemCache()
18+
assert.NotNil(t, cache)
19+
assert.NotNil(t, cache.markets)
20+
}
21+
22+
func Test_marketMemCache_GetSet(t *testing.T) {
23+
cache := newMarketMemCache()
24+
cache.Set("max", types.MarketMap{
25+
"btctwd": types.Market{
26+
Symbol: "btctwd",
27+
LocalSymbol: "btctwd",
28+
},
29+
"ethtwd": types.Market{
30+
Symbol: "ethtwd",
31+
LocalSymbol: "ethtwd",
32+
},
33+
})
34+
markets, ok := cache.Get("max")
35+
assert.True(t, ok)
36+
37+
btctwd, ok := markets["btctwd"]
38+
assert.True(t, ok)
39+
ethtwd, ok := markets["ethtwd"]
40+
assert.True(t, ok)
41+
assert.Equal(t, types.Market{
42+
Symbol: "btctwd",
43+
LocalSymbol: "btctwd",
44+
}, btctwd)
45+
assert.Equal(t, types.Market{
46+
Symbol: "ethtwd",
47+
LocalSymbol: "ethtwd",
48+
}, ethtwd)
49+
50+
_, ok = cache.Get("binance")
51+
assert.False(t, ok)
52+
53+
expired := cache.IsOutdated("max")
54+
assert.False(t, expired)
55+
56+
detailed := cache.markets["max"]
57+
detailed.updatedAt = time.Now().Add(-2 * memCacheExpiry)
58+
cache.markets["max"] = detailed
59+
expired = cache.IsOutdated("max")
60+
assert.True(t, expired)
61+
62+
expired = cache.IsOutdated("binance")
63+
assert.True(t, expired)
64+
}
65+
66+
func Test_loadMarketsFromMem(t *testing.T) {
67+
mockCtrl := gomock.NewController(t)
68+
defer mockCtrl.Finish()
69+
70+
mockEx := mocks.NewMockExchange(mockCtrl)
71+
mockEx.EXPECT().Name().Return(types.ExchangeName("max")).AnyTimes()
72+
mockEx.EXPECT().QueryMarkets(gomock.Any()).Return(nil, errors.New("faked")).Times(1)
73+
mockEx.EXPECT().QueryMarkets(gomock.Any()).Return(types.MarketMap{
74+
"btctwd": types.Market{
75+
Symbol: "btctwd",
76+
LocalSymbol: "btctwd",
77+
},
78+
"ethtwd": types.Market{
79+
Symbol: "ethtwd",
80+
LocalSymbol: "ethtwd",
81+
},
82+
}, nil).Times(1)
83+
84+
for i := 0; i < 10; i++ {
85+
markets, err := loadMarketsFromMem(context.Background(), mockEx)
86+
assert.NoError(t, err)
87+
88+
btctwd, ok := markets["btctwd"]
89+
assert.True(t, ok)
90+
ethtwd, ok := markets["ethtwd"]
91+
assert.True(t, ok)
92+
assert.Equal(t, types.Market{
93+
Symbol: "btctwd",
94+
LocalSymbol: "btctwd",
95+
}, btctwd)
96+
assert.Equal(t, types.Market{
97+
Symbol: "ethtwd",
98+
LocalSymbol: "ethtwd",
99+
}, ethtwd)
100+
}
101+
102+
globalMarketMemCache = newMarketMemCache() // reset the global cache
103+
}

0 commit comments

Comments
 (0)