exchanges/websocket: Implement subscription configuration (#1394)

* Websockets: Move Subscription to its own package

This allows the small type to be imported from both `config` and from
`stream` without an import cycle, so we don't have to repeat ourselves

* Subs: Renamed Currency to Pair

This was being mis-used through much of the code, and since we're
already touching everything, we might as well fix it

* Websockets: Add Subscription configuration

* Binance: Add subscription configuration

* Kucoin: Subscription configuration

* Simplify GenerateDefaultSubs
* Improve TestGenSubs coverage
* Test Candle Sub generation
* Support Candle intervals
* Full responsibility for formatting Channel name on GenerateDefaultSubs
  OR consumer of Subscribe
* Simplify generatePayloads as a result
* Fix test coverage of asset types in processMarketSnapshot

* Exchanges: Abstract ParallelChanOp

* Tests: Generic ws mock instances

* Kucoin: Fix intermittent conflict in test currs

Use isolated test instance for `TestGetOpenInterest`.

`TestGetOpenInterest` would occassionally change pairs before
GenerateDefault Subs.
This commit is contained in:
Gareth Kirwan
2024-01-24 05:54:07 +01:00
committed by GitHub
parent 301551ac20
commit e007f69f7c
67 changed files with 3705 additions and 3167 deletions

View File

@@ -20,8 +20,8 @@ import (
"github.com/stretchr/testify/assert"
"github.com/thrasher-corp/gocryptotrader/config"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/protocol"
"github.com/thrasher-corp/gocryptotrader/exchanges/subscription"
)
const (
@@ -73,10 +73,10 @@ var defaultSetup = &WebsocketSetup{
DefaultURL: "testDefaultURL",
RunningURL: "wss://testRunningURL",
Connector: func() error { return nil },
Subscriber: func(_ []ChannelSubscription) error { return nil },
Unsubscriber: func(_ []ChannelSubscription) error { return nil },
GenerateSubscriptions: func() ([]ChannelSubscription, error) {
return []ChannelSubscription{
Subscriber: func(_ []subscription.Subscription) error { return nil },
Unsubscriber: func(_ []subscription.Subscription) error { return nil },
GenerateSubscriptions: func() ([]subscription.Subscription, error) {
return []subscription.Subscription{
{Channel: "TestSub"},
{Channel: "TestSub2", Key: "purple"},
{Channel: "TestSub3", Key: testSubKey{"mauve"}},
@@ -156,20 +156,20 @@ func TestSetup(t *testing.T) {
t.Fatalf("received: '%v' but expected: '%v'", err, errWebsocketSubscriberUnset)
}
websocketSetup.Subscriber = func([]ChannelSubscription) error { return nil }
websocketSetup.Subscriber = func([]subscription.Subscription) error { return nil }
websocketSetup.Features.Unsubscribe = true
err = w.Setup(websocketSetup)
if !errors.Is(err, errWebsocketUnsubscriberUnset) {
t.Fatalf("received: '%v' but expected: '%v'", err, errWebsocketUnsubscriberUnset)
}
websocketSetup.Unsubscriber = func([]ChannelSubscription) error { return nil }
websocketSetup.Unsubscriber = func([]subscription.Subscription) error { return nil }
err = w.Setup(websocketSetup)
if !errors.Is(err, errWebsocketSubscriptionsGeneratorUnset) {
t.Fatalf("received: '%v' but expected: '%v'", err, errWebsocketSubscriptionsGeneratorUnset)
}
websocketSetup.GenerateSubscriptions = func() ([]ChannelSubscription, error) { return nil, nil }
websocketSetup.GenerateSubscriptions = func() ([]subscription.Subscription, error) { return nil, nil }
err = w.Setup(websocketSetup)
if !errors.Is(err, errDefaultURLIsEmpty) {
t.Fatalf("received: '%v' but expected: '%v'", err, errDefaultURLIsEmpty)
@@ -504,11 +504,11 @@ func TestSubscribeUnsubscribe(t *testing.T) {
ws := *New()
assert.NoError(t, ws.Setup(defaultSetup), "WS Setup should not error")
fnSub := func(subs []ChannelSubscription) error {
fnSub := func(subs []subscription.Subscription) error {
ws.AddSuccessfulSubscriptions(subs...)
return nil
}
fnUnsub := func(unsubs []ChannelSubscription) error {
fnUnsub := func(unsubs []subscription.Subscription) error {
ws.RemoveSubscriptions(unsubs...)
return nil
}
@@ -522,7 +522,7 @@ func TestSubscribeUnsubscribe(t *testing.T) {
assert.Nil(t, ws.GetSubscription(42), "GetSubscription on empty internal map should return")
assert.NoError(t, ws.SubscribeToChannels(subs), "Basic Subscribing should not error")
assert.Len(t, ws.GetSubscriptions(), 4, "Should have 4 subscriptions")
byDefKey := ws.GetSubscription(DefaultChannelKey{Channel: "TestSub"})
byDefKey := ws.GetSubscription(subscription.DefaultKey{Channel: "TestSub"})
if assert.NotNil(t, byDefKey, "GetSubscription by default key should find a channel") {
assert.Equal(t, "TestSub", byDefKey.Channel, "GetSubscription by default key should return a pointer a copy of the right channel")
assert.NotSame(t, byDefKey, ws.subscriptions["TestSub"], "GetSubscription returns a fresh pointer")
@@ -556,18 +556,18 @@ func TestResubscribe(t *testing.T) {
err = ws.Setup(defaultSetup)
assert.NoError(t, err, "WS Setup should not error")
fnSub := func(subs []ChannelSubscription) error {
fnSub := func(subs []subscription.Subscription) error {
ws.AddSuccessfulSubscriptions(subs...)
return nil
}
fnUnsub := func(unsubs []ChannelSubscription) error {
fnUnsub := func(unsubs []subscription.Subscription) error {
ws.RemoveSubscriptions(unsubs...)
return nil
}
ws.Subscriber = fnSub
ws.Unsubscriber = fnUnsub
channel := []ChannelSubscription{{Channel: "resubTest"}}
channel := []subscription.Subscription{{Channel: "resubTest"}}
assert.ErrorIs(t, ws.ResubscribeToChannel(&channel[0]), ErrSubscriptionNotFound, "Resubscribe should error when channel isn't subscribed yet")
assert.NoError(t, ws.SubscribeToChannels(channel), "Subscribe should not error")
@@ -579,25 +579,25 @@ func TestSubscriptionState(t *testing.T) {
t.Parallel()
ws := New()
c := &ChannelSubscription{Key: 42, Channel: "Gophers", State: ChannelSubscribing}
assert.ErrorIs(t, ws.SetSubscriptionState(c, ChannelUnsubscribing), ErrSubscriptionNotFound, "Setting an imaginary sub should error")
c := &subscription.Subscription{Key: 42, Channel: "Gophers", State: subscription.SubscribingState}
assert.ErrorIs(t, ws.SetSubscriptionState(c, subscription.UnsubscribingState), ErrSubscriptionNotFound, "Setting an imaginary sub should error")
assert.NoError(t, ws.AddSubscription(c), "Adding first subscription should not error")
found := ws.GetSubscription(42)
assert.NotNil(t, found, "Should find the subscription")
assert.Equal(t, ChannelSubscribing, found.State, "Subscription should be Subscribing")
assert.Equal(t, subscription.SubscribingState, found.State, "Subscription should be Subscribing")
assert.ErrorIs(t, ws.AddSubscription(c), ErrSubscribedAlready, "Adding an already existing sub should error")
assert.ErrorIs(t, ws.SetSubscriptionState(c, ChannelSubscribing), ErrChannelInStateAlready, "Setting Same state should error")
assert.ErrorIs(t, ws.SetSubscriptionState(c, ChannelUnsubscribing+1), errInvalidChannelState, "Setting an invalid state should error")
assert.ErrorIs(t, ws.SetSubscriptionState(c, subscription.SubscribingState), ErrChannelInStateAlready, "Setting Same state should error")
assert.ErrorIs(t, ws.SetSubscriptionState(c, subscription.UnsubscribingState+1), errInvalidChannelState, "Setting an invalid state should error")
ws.AddSuccessfulSubscriptions(*c)
found = ws.GetSubscription(42)
assert.NotNil(t, found, "Should find the subscription")
assert.Equal(t, found.State, ChannelSubscribed, "Subscription should be subscribed state")
assert.Equal(t, found.State, subscription.SubscribedState, "Subscription should be subscribed state")
assert.NoError(t, ws.SetSubscriptionState(c, ChannelUnsubscribing), "Setting Unsub state should not error")
assert.NoError(t, ws.SetSubscriptionState(c, subscription.UnsubscribingState), "Setting Unsub state should not error")
found = ws.GetSubscription(42)
assert.Equal(t, found.State, ChannelUnsubscribing, "Subscription should be unsubscribing state")
assert.Equal(t, found.State, subscription.UnsubscribingState, "Subscription should be unsubscribing state")
}
// TestRemoveSubscriptions tests removing a subscription
@@ -605,7 +605,7 @@ func TestRemoveSubscriptions(t *testing.T) {
t.Parallel()
ws := New()
c := &ChannelSubscription{Key: 42, Channel: "Unite!"}
c := &subscription.Subscription{Key: 42, Channel: "Unite!"}
assert.NoError(t, ws.AddSubscription(c), "Adding first subscription should not error")
assert.NotNil(t, ws.GetSubscription(42), "Added subscription should be findable")
@@ -668,35 +668,6 @@ func TestGetSubscriptions(t *testing.T) {
assert.Equal(t, "hello3", w.GetSubscriptions()[0].Channel, "GetSubscriptions should return the correct channel details")
}
// TestEnsureKeyed logic test
func TestEnsureKeyed(t *testing.T) {
t.Parallel()
c := ChannelSubscription{
Channel: "candles",
Asset: asset.Spot,
Currency: currency.NewPair(currency.BTC, currency.USDT),
}
k1, ok := c.EnsureKeyed().(DefaultChannelKey)
if assert.True(t, ok, "EnsureKeyed should return a DefaultChannelKey") {
assert.Exactly(t, k1, c.Key, "EnsureKeyed should set the same key")
assert.Equal(t, k1.Channel, c.Channel, "DefaultChannelKey channel should be correct")
assert.Equal(t, k1.Asset, c.Asset, "DefaultChannelKey asset should be correct")
assert.Equal(t, k1.Currency, c.Currency, "DefaultChannelKey currency should be correct")
}
type platypus string
c = ChannelSubscription{
Key: platypus("Gerald"),
Channel: "orderbook",
Asset: asset.Margin,
Currency: currency.NewPair(currency.ETH, currency.USDC),
}
k2, ok := c.EnsureKeyed().(platypus)
if assert.True(t, ok, "EnsureKeyed should return a platypus") {
assert.Exactly(t, k2, c.Key, "EnsureKeyed should set the same key")
assert.EqualValues(t, "Gerald", k2, "key should have the correct value")
}
}
// TestSetCanUseAuthenticatedEndpoints logic test
func TestSetCanUseAuthenticatedEndpoints(t *testing.T) {
t.Parallel()
@@ -1076,7 +1047,7 @@ func TestGetChannelDifference(t *testing.T) {
t.Parallel()
web := Websocket{}
newChans := []ChannelSubscription{
newChans := []subscription.Subscription{
{
Channel: "Test1",
},
@@ -1093,7 +1064,7 @@ func TestGetChannelDifference(t *testing.T) {
web.AddSuccessfulSubscriptions(subs...)
flushedSubs := []ChannelSubscription{
flushedSubs := []subscription.Subscription{
{
Channel: "Test2",
},
@@ -1103,7 +1074,7 @@ func TestGetChannelDifference(t *testing.T) {
assert.Len(t, subs, 0, "Should get the correct number of subs")
assert.Len(t, unsubs, 2, "Should get the correct number of unsubs")
flushedSubs = []ChannelSubscription{
flushedSubs = []subscription.Subscription{
{
Channel: "Test2",
},
@@ -1126,23 +1097,23 @@ func TestGetChannelDifference(t *testing.T) {
// GenSubs defines a theoretical exchange with pair management
type GenSubs struct {
EnabledPairs currency.Pairs
subscribos []ChannelSubscription
unsubscribos []ChannelSubscription
subscribos []subscription.Subscription
unsubscribos []subscription.Subscription
}
// generateSubs default subs created from the enabled pairs list
func (g *GenSubs) generateSubs() ([]ChannelSubscription, error) {
superduperchannelsubs := make([]ChannelSubscription, len(g.EnabledPairs))
func (g *GenSubs) generateSubs() ([]subscription.Subscription, error) {
superduperchannelsubs := make([]subscription.Subscription, len(g.EnabledPairs))
for i := range g.EnabledPairs {
superduperchannelsubs[i] = ChannelSubscription{
Channel: "TEST:" + strconv.FormatInt(int64(i), 10),
Currency: g.EnabledPairs[i],
superduperchannelsubs[i] = subscription.Subscription{
Channel: "TEST:" + strconv.FormatInt(int64(i), 10),
Pair: g.EnabledPairs[i],
}
}
return superduperchannelsubs, nil
}
func (g *GenSubs) SUBME(subs []ChannelSubscription) error {
func (g *GenSubs) SUBME(subs []subscription.Subscription) error {
if len(subs) == 0 {
return errors.New("WOW")
}
@@ -1150,7 +1121,7 @@ func (g *GenSubs) SUBME(subs []ChannelSubscription) error {
return nil
}
func (g *GenSubs) UNSUBME(unsubs []ChannelSubscription) error {
func (g *GenSubs) UNSUBME(unsubs []subscription.Subscription) error {
if len(unsubs) == 0 {
return errors.New("WOW")
}
@@ -1197,19 +1168,19 @@ func TestFlushChannels(t *testing.T) {
// this to an unconnected state
}
problemFunc := func() ([]ChannelSubscription, error) {
problemFunc := func() ([]subscription.Subscription, error) {
return nil, errors.New("problems")
}
noSub := func() ([]ChannelSubscription, error) {
noSub := func() ([]subscription.Subscription, error) {
return nil, nil
}
// Disable pair and flush system
newgen.EnabledPairs = []currency.Pair{
currency.NewPair(currency.BTC, currency.AUD)}
web.GenerateSubs = func() ([]ChannelSubscription, error) {
return []ChannelSubscription{{Channel: "test"}}, nil
web.GenerateSubs = func() ([]subscription.Subscription, error) {
return []subscription.Subscription{{Channel: "test"}}, nil
}
err = web.FlushChannels()
if err != nil {
@@ -1256,14 +1227,14 @@ func TestFlushChannels(t *testing.T) {
web.subscriptionMutex.Lock()
web.subscriptions = subscriptionMap{
41: {
Key: 41,
Channel: "match channel",
Currency: currency.NewPair(currency.BTC, currency.AUD),
Key: 41,
Channel: "match channel",
Pair: currency.NewPair(currency.BTC, currency.AUD),
},
42: {
Key: 42,
Channel: "unsub channel",
Currency: currency.NewPair(currency.THETA, currency.USDT),
Key: 42,
Channel: "unsub channel",
Pair: currency.NewPair(currency.THETA, currency.USDT),
},
}
web.subscriptionMutex.Unlock()
@@ -1309,10 +1280,10 @@ func TestEnable(t *testing.T) {
connector: connect,
Wg: new(sync.WaitGroup),
ShutdownC: make(chan struct{}),
GenerateSubs: func() ([]ChannelSubscription, error) {
return []ChannelSubscription{{Channel: "test"}}, nil
GenerateSubs: func() ([]subscription.Subscription, error) {
return []subscription.Subscription{{Channel: "test"}}, nil
},
Subscriber: func(cs []ChannelSubscription) error { return nil },
Subscriber: func(cs []subscription.Subscription) error { return nil },
}
err := web.Enable()
@@ -1466,7 +1437,7 @@ func TestCheckSubscriptions(t *testing.T) {
ws.MaxSubscriptionsPerConnection = 1
err = ws.checkSubscriptions([]ChannelSubscription{{}, {}})
err = ws.checkSubscriptions([]subscription.Subscription{{}, {}})
if !errors.Is(err, errSubscriptionsExceedsLimit) {
t.Fatalf("received: %v, but expected: %v", err, errSubscriptionsExceedsLimit)
}
@@ -1474,12 +1445,12 @@ func TestCheckSubscriptions(t *testing.T) {
ws.MaxSubscriptionsPerConnection = 2
ws.subscriptions = subscriptionMap{42: {Key: 42, Channel: "test"}}
err = ws.checkSubscriptions([]ChannelSubscription{{Key: 42, Channel: "test"}})
err = ws.checkSubscriptions([]subscription.Subscription{{Key: 42, Channel: "test"}})
if !errors.Is(err, errChannelAlreadySubscribed) {
t.Fatalf("received: %v, but expected: %v", err, errChannelAlreadySubscribed)
}
err = ws.checkSubscriptions([]ChannelSubscription{{}})
err = ws.checkSubscriptions([]subscription.Subscription{{}})
if !errors.Is(err, nil) {
t.Fatalf("received: %v, but expected: %v", err, nil)
}