mirror of
https://github.com/d0zingcat/gocryptotrader.git
synced 2026-05-23 23:16:49 +00:00
* gateio: websocket ob manager fix (cherry-pick me)
* fix(gateio): update websocket orderbook manager to support delay and deadline parameters
feat(subscriptions): mv ChannelKey type for subscription management from gateio to subscriptions
test(gateio): enhance tests for orderbook update manager and subscription keys
* ai: nits
* linter: fix
* Fix asset typo and add in case test
* cranktakular: nits
* cranktakular: nits after merge master
* bump time delay for cache
* fix bug where on error it never initiates another orderbook fetch
* lint: fix
* Update exchanges/gateio/ws_ob_update_manager.go
Co-authored-by: Scott <gloriousCode@users.noreply.github.com>
* glorious: nits
* linter: fix
* Update exchanges/gateio/gateio_wrapper_test.go
Co-authored-by: Gareth Kirwan <gbjkirwan@gmail.com>
* Update exchanges/gateio/gateio_wrapper.go
Co-authored-by: Gareth Kirwan <gbjkirwan@gmail.com>
* Update exchanges/gateio/gateio_wrapper_test.go
Co-authored-by: Gareth Kirwan <gbjkirwan@gmail.com>
* Update exchanges/gateio/ws_ob_update_manager.go
Co-authored-by: Gareth Kirwan <gbjkirwan@gmail.com>
* Update exchanges/gateio/ws_ob_update_manager.go
Co-authored-by: Gareth Kirwan <gbjkirwan@gmail.com>
* Update exchanges/gateio/ws_ob_update_manager.go
Co-authored-by: Gareth Kirwan <gbjkirwan@gmail.com>
* Update exchanges/gateio/ws_ob_update_manager_test.go
Co-authored-by: Gareth Kirwan <gbjkirwan@gmail.com>
* gk: nits
* Update exchanges/gateio/gateio_wrapper.go
Co-authored-by: Gareth Kirwan <gbjkirwan@gmail.com>
* Update exchanges/gateio/ws_ob_update_manager.go
Co-authored-by: Gareth Kirwan <gbjkirwan@gmail.com>
* gk: nits
* bossking: nits
* Update exchanges/gateio/ws_ob_update_manager_test.go
Co-authored-by: Gareth Kirwan <gbjkirwan@gmail.com>
* gk: nits
* apply patch
* rm error state as this was on the same thread as cacheStateQueuing and had the potential to drop messages, add tests.
* linter: fix
* mock live request
* misc: fix
* Update exchanges/gateio/ws_ob_update_manager.go
Co-authored-by: Gareth Kirwan <gbjkirwan@gmail.com>
* Update exchanges/gateio/ws_ob_update_manager.go
Co-authored-by: Gareth Kirwan <gbjkirwan@gmail.com>
* Update exchanges/gateio/ws_ob_update_manager.go
Co-authored-by: Gareth Kirwan <gbjkirwan@gmail.com>
* Update exchanges/gateio/ws_ob_update_manager.go
Co-authored-by: Gareth Kirwan <gbjkirwan@gmail.com>
* Update exchanges/gateio/ws_ob_update_manager.go
Co-authored-by: Gareth Kirwan <gbjkirwan@gmail.com>
* gk: nits
* field name from mtx -> m
* lint: fix
* race: check fix
* thrasher-: patch adams
---------
Co-authored-by: Ryan O'Hara-Reid <ryan.oharareid@thrasher.io>
Co-authored-by: Scott <gloriousCode@users.noreply.github.com>
Co-authored-by: Gareth Kirwan <gbjkirwan@gmail.com>
406 lines
14 KiB
Go
406 lines
14 KiB
Go
package gateio
|
|
|
|
import (
|
|
"context"
|
|
"math"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"github.com/thrasher-corp/gocryptotrader/currency"
|
|
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
|
|
"github.com/thrasher-corp/gocryptotrader/exchanges/kline"
|
|
"github.com/thrasher-corp/gocryptotrader/exchanges/orderbook"
|
|
"github.com/thrasher-corp/gocryptotrader/exchanges/subscription"
|
|
testexch "github.com/thrasher-corp/gocryptotrader/internal/testing/exchange"
|
|
)
|
|
|
|
func TestProcessOrderbookUpdate(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
m := newWsOBUpdateManager(time.Millisecond*200, time.Millisecond*200)
|
|
err := m.ProcessOrderbookUpdate(t.Context(), e, 1337, &orderbook.Update{})
|
|
assert.ErrorIs(t, err, currency.ErrCurrencyPairEmpty)
|
|
|
|
pair := currency.NewPair(currency.BABY, currency.BABYDOGE)
|
|
cache, err := m.LoadCache(pair, asset.USDTMarginedFutures)
|
|
require.NoError(t, err)
|
|
cache.m.Lock()
|
|
assert.Equal(t, cacheStateInitialised, cache.state)
|
|
cache.m.Unlock()
|
|
|
|
err = e.Websocket.Orderbook.LoadSnapshot(&orderbook.Book{
|
|
Exchange: e.Name,
|
|
Pair: pair,
|
|
Asset: asset.USDTMarginedFutures,
|
|
Bids: []orderbook.Level{{Price: 1, Amount: 1}},
|
|
Asks: []orderbook.Level{{Price: 1, Amount: 1}},
|
|
LastUpdated: time.Now(),
|
|
LastPushed: time.Now(),
|
|
LastUpdateID: 1336,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
err = m.ProcessOrderbookUpdate(t.Context(), e, 1337, &orderbook.Update{
|
|
UpdateID: 1338,
|
|
Pair: pair,
|
|
Asset: asset.USDTMarginedFutures,
|
|
AllowEmpty: true,
|
|
UpdateTime: time.Now(),
|
|
})
|
|
require.NoError(t, err)
|
|
cache.m.Lock()
|
|
assert.Equal(t, cacheStateQueuing, cache.state, "state should be queuing after first update")
|
|
cache.m.Unlock()
|
|
var wg1, wg2 sync.WaitGroup
|
|
wg1.Add(1)
|
|
wg2.Go(func() {
|
|
wg1.Done()
|
|
updatedID := <-cache.ch
|
|
assert.Equal(t, int64(1339), updatedID, "should ensure update was queued")
|
|
})
|
|
wg1.Wait()
|
|
err = m.ProcessOrderbookUpdate(t.Context(), e, 1337, &orderbook.Update{
|
|
UpdateID: 1339,
|
|
Pair: pair,
|
|
Asset: asset.USDTMarginedFutures,
|
|
AllowEmpty: true,
|
|
UpdateTime: time.Now(),
|
|
})
|
|
wg2.Wait()
|
|
|
|
require.NoError(t, err)
|
|
cache.m.Lock()
|
|
assert.Equal(t, cacheStateQueuing, cache.state)
|
|
cache.m.Unlock()
|
|
|
|
assert.Eventually(t, func() bool {
|
|
cache.m.Lock()
|
|
defer cache.m.Unlock()
|
|
return cache.state == cacheStateQueuing
|
|
}, time.Second, time.Millisecond*10, "sync should eventually fail as BABYBABYDOGE is not a supported pair an error state and forces everything to queue")
|
|
|
|
err = m.ProcessOrderbookUpdate(t.Context(), e, 1337, &orderbook.Update{
|
|
UpdateID: 1338,
|
|
Pair: pair,
|
|
Asset: asset.USDTMarginedFutures,
|
|
AllowEmpty: true,
|
|
UpdateTime: time.Now(),
|
|
})
|
|
require.NoError(t, err, "ProcessOrderbookUpdate must not error as an error state is recovered")
|
|
|
|
err = e.Websocket.Orderbook.LoadSnapshot(&orderbook.Book{
|
|
Exchange: e.Name,
|
|
Pair: pair,
|
|
Asset: asset.USDTMarginedFutures,
|
|
Bids: []orderbook.Level{{Price: 1, Amount: 1}},
|
|
Asks: []orderbook.Level{{Price: 1, Amount: 1}},
|
|
LastUpdated: time.Now(),
|
|
LastPushed: time.Now(),
|
|
LastUpdateID: 1336,
|
|
})
|
|
require.NoError(t, err, "LoadSnapshot must not error while ensuring successful processing")
|
|
cache.m.Lock()
|
|
cache.state = cacheStateSynced
|
|
cache.m.Unlock()
|
|
err = m.ProcessOrderbookUpdate(t.Context(), e, 1337, &orderbook.Update{
|
|
UpdateID: 1338,
|
|
Pair: pair,
|
|
Asset: asset.USDTMarginedFutures,
|
|
AllowEmpty: true,
|
|
UpdateTime: time.Now(),
|
|
})
|
|
require.NoError(t, err, "ProcessOrderbookUpdate must not error while ensuring successful processing")
|
|
|
|
cache.m.Lock()
|
|
cache.state = 100
|
|
cache.m.Unlock()
|
|
err = m.ProcessOrderbookUpdate(t.Context(), e, 1337, &orderbook.Update{
|
|
UpdateID: 1339,
|
|
Pair: pair,
|
|
Asset: asset.USDTMarginedFutures,
|
|
AllowEmpty: true,
|
|
UpdateTime: time.Now(),
|
|
})
|
|
require.ErrorIs(t, err, errUnhandledCacheState, "ProcessOrderbookUpdate must error due to unhandled state")
|
|
}
|
|
|
|
func TestLoadCache(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
m := newWsOBUpdateManager(0, 0)
|
|
_, err := m.LoadCache(currency.EMPTYPAIR, 1336)
|
|
require.ErrorIs(t, err, currency.ErrCurrencyPairEmpty)
|
|
|
|
_, err = m.LoadCache(currency.NewBTCUSDT(), 1336)
|
|
require.ErrorIs(t, err, asset.ErrInvalidAsset)
|
|
|
|
cache, err := m.LoadCache(currency.NewBTCUSDT(), asset.USDTMarginedFutures)
|
|
require.NoError(t, err, "LoadCache must not error")
|
|
assert.NotNil(t, cache)
|
|
assert.Len(t, m.lookup, 1)
|
|
cache.m.Lock()
|
|
assert.Equal(t, cacheStateInitialised, cache.state, "state should be initialised after first load")
|
|
cache.m.Unlock()
|
|
|
|
cache2, err := m.LoadCache(currency.NewBTCUSDT(), asset.USDTMarginedFutures)
|
|
require.NoError(t, err, "LoadCache must not error")
|
|
assert.Equal(t, cache, cache2, "should be the same cache instance")
|
|
}
|
|
|
|
func TestSyncOrderbook(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
e := new(Exchange)
|
|
require.NoError(t, testexch.Setup(e), "Setup must not error")
|
|
|
|
cache := &updateCache{}
|
|
pair := currency.NewPair(currency.ETH, currency.USDT)
|
|
err := cache.SyncOrderbook(t.Context(), e, pair, asset.Spot, 0, defaultWSOrderbookUpdateDeadline)
|
|
require.ErrorIs(t, err, subscription.ErrNotFound)
|
|
|
|
// Add dummy subscription so that it can be matched and a limit/level can be extracted for initial orderbook sync spot.
|
|
err = e.Websocket.AddSubscriptions(nil, &subscription.Subscription{Channel: subscription.OrderbookChannel, Interval: kline.HundredMilliseconds})
|
|
require.NoError(t, err)
|
|
|
|
ctxCancel, cancel := context.WithCancel(t.Context())
|
|
cancel()
|
|
err = cache.SyncOrderbook(ctxCancel, e, pair, asset.Spot, 0, defaultWSOrderbookUpdateDeadline)
|
|
require.ErrorIs(t, err, context.Canceled)
|
|
|
|
cache.updates = []pendingUpdate{{update: &orderbook.Update{Pair: pair, Asset: asset.Spot}}}
|
|
err = cache.SyncOrderbook(t.Context(), e, pair, asset.Spot, 0, 0)
|
|
require.ErrorIs(t, err, context.DeadlineExceeded)
|
|
|
|
cache.updates = []pendingUpdate{{update: &orderbook.Update{Pair: pair, Asset: asset.Spot}}}
|
|
err = cache.SyncOrderbook(t.Context(), e, pair, asset.Spot, 0, time.Second)
|
|
require.ErrorContains(t, err, context.DeadlineExceeded.Error())
|
|
|
|
err = e.Base.SetPairs([]currency.Pair{pair}, asset.Spot, true)
|
|
require.NoError(t, err)
|
|
cache.updates = []pendingUpdate{{update: &orderbook.Update{Pair: pair, Asset: asset.Spot, UpdateID: math.MaxInt64}}}
|
|
err = cache.SyncOrderbook(t.Context(), e, pair, asset.Spot, 0, time.Second)
|
|
require.ErrorIs(t, err, orderbook.ErrOrderbookInvalid)
|
|
|
|
err = e.Base.SetPairs([]currency.Pair{pair}, asset.USDTMarginedFutures, true)
|
|
require.NoError(t, err)
|
|
cache.updates = []pendingUpdate{{update: &orderbook.Update{Pair: pair, Asset: asset.USDTMarginedFutures, UpdateID: math.MaxInt64}}}
|
|
err = cache.SyncOrderbook(t.Context(), e, pair, asset.USDTMarginedFutures, 0, time.Second)
|
|
require.ErrorIs(t, err, orderbook.ErrOrderbookInvalid)
|
|
}
|
|
|
|
func TestWaitForUpdate(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cache := &updateCache{
|
|
updates: []pendingUpdate{
|
|
{update: &orderbook.Update{Pair: currency.NewBTCUSD(), Asset: asset.Spot, UpdateID: 1337, AllowEmpty: true, UpdateTime: time.Now()}},
|
|
},
|
|
}
|
|
|
|
err := cache.waitForUpdate(t.Context(), 1337)
|
|
require.NoError(t, err)
|
|
|
|
ctx, cancel := context.WithDeadline(t.Context(), time.Now())
|
|
defer cancel()
|
|
err = cache.waitForUpdate(ctx, 1338)
|
|
require.ErrorIs(t, err, context.DeadlineExceeded)
|
|
|
|
cache.ch = make(chan int64, 1) // Reset channel to avoid deadlock
|
|
var wg sync.WaitGroup
|
|
wg.Go(func() {
|
|
err = cache.waitForUpdate(t.Context(), 1338)
|
|
})
|
|
cache.ch <- 1338
|
|
wg.Wait()
|
|
assert.NoError(t, err)
|
|
}
|
|
|
|
func TestApplyPendingUpdates(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
e := new(Exchange)
|
|
require.NoError(t, testexch.Setup(e), "Setup must not error")
|
|
pair := currency.NewPair(currency.LTC, currency.USDT)
|
|
cache := &updateCache{updates: []pendingUpdate{{update: &orderbook.Update{Pair: pair, Asset: asset.USDTMarginedFutures}}}}
|
|
err := cache.applyPendingUpdates(e)
|
|
require.ErrorIs(t, err, orderbook.ErrDepthNotFound)
|
|
|
|
dummy := &orderbook.Book{
|
|
Exchange: e.Name,
|
|
Pair: pair,
|
|
Asset: asset.USDTMarginedFutures,
|
|
Bids: []orderbook.Level{{Price: 1, Amount: 1}},
|
|
Asks: []orderbook.Level{{Price: 1, Amount: 1}},
|
|
LastUpdated: time.Now(),
|
|
LastPushed: time.Now(),
|
|
LastUpdateID: 1335,
|
|
}
|
|
|
|
err = e.Websocket.Orderbook.LoadSnapshot(dummy)
|
|
require.NoError(t, err)
|
|
|
|
err = cache.applyPendingUpdates(e)
|
|
require.ErrorIs(t, err, errPendingUpdatesNotApplied)
|
|
|
|
cache.updates[0].firstUpdateID = 1337
|
|
cache.updates[0].update.UpdateID = 1338
|
|
err = cache.applyPendingUpdates(e)
|
|
require.ErrorIs(t, err, errOrderbookSnapshotOutdated)
|
|
|
|
cache.updates[0].firstUpdateID = 1336
|
|
cache.updates[0].update.UpdateID = 1338
|
|
err = cache.applyPendingUpdates(e)
|
|
require.ErrorIs(t, err, orderbook.ErrOrderbookInvalid)
|
|
|
|
err = e.Websocket.Orderbook.LoadSnapshot(dummy)
|
|
require.NoError(t, err)
|
|
|
|
cache.updates[0].update.AllowEmpty = true
|
|
cache.updates[0].update.UpdateTime = time.Now()
|
|
err = cache.applyPendingUpdates(e)
|
|
require.NoError(t, err)
|
|
|
|
cache.updates[0].firstUpdateID = 1339
|
|
cache.updates[0].update.UpdateID = 1342
|
|
cache.updates = append(cache.updates, pendingUpdate{
|
|
firstUpdateID: 1344,
|
|
update: &orderbook.Update{Pair: pair, Asset: asset.USDTMarginedFutures, UpdateID: 1345, AllowEmpty: true, UpdateTime: time.Now()},
|
|
})
|
|
err = cache.applyPendingUpdates(e)
|
|
require.ErrorIs(t, err, errOrderbookSnapshotOutdated)
|
|
}
|
|
|
|
func TestClearWithLock(t *testing.T) {
|
|
t.Parallel()
|
|
cache := &updateCache{updates: []pendingUpdate{{update: &orderbook.Update{}}}}
|
|
cache.clearWithLock()
|
|
require.Empty(t, cache.updates)
|
|
}
|
|
|
|
func TestClearNoLock(t *testing.T) {
|
|
t.Parallel()
|
|
cache := &updateCache{updates: []pendingUpdate{{update: &orderbook.Update{}}}}
|
|
cache.clearNoLock()
|
|
require.Empty(t, cache.updates)
|
|
}
|
|
|
|
func TestApplyUpdate(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
e := new(Exchange)
|
|
err := testexch.Setup(e)
|
|
require.NoError(t, err, "Setup must not error")
|
|
e.Name = "ApplyUpdateTest"
|
|
|
|
m := newWsOBUpdateManager(0, 0)
|
|
cache, err := m.LoadCache(currency.NewBTCUSDT(), asset.USDTMarginedFutures)
|
|
require.NoError(t, err, "LoadCache must not error")
|
|
|
|
cache.m.Lock()
|
|
err = m.applyUpdate(t.Context(), e, cache, 1, &orderbook.Update{
|
|
Pair: currency.NewBTCUSDT(),
|
|
Asset: asset.USDTMarginedFutures,
|
|
})
|
|
cache.m.Unlock()
|
|
require.ErrorIs(t, err, orderbook.ErrDepthNotFound, "applyUpdate must error when not initialised")
|
|
|
|
snapshot := &orderbook.Book{
|
|
Exchange: e.Name,
|
|
Pair: currency.NewBTCUSDT(),
|
|
Asset: asset.USDTMarginedFutures,
|
|
Bids: []orderbook.Level{{Price: 1, Amount: 1}},
|
|
Asks: []orderbook.Level{{Price: 1, Amount: 1}},
|
|
LastUpdated: time.Now(),
|
|
LastPushed: time.Now(),
|
|
LastUpdateID: 1336,
|
|
}
|
|
|
|
err = e.Websocket.Orderbook.LoadSnapshot(snapshot)
|
|
require.NoError(t, err)
|
|
|
|
cache.m.Lock()
|
|
err = m.applyUpdate(t.Context(), e, cache, 1, &orderbook.Update{
|
|
UpdateID: 1338,
|
|
Pair: currency.NewBTCUSDT(),
|
|
Asset: asset.USDTMarginedFutures,
|
|
})
|
|
cache.m.Unlock()
|
|
require.NoError(t, err, "applyUpdate must not error when desynced")
|
|
|
|
_, err = e.Websocket.Orderbook.LastUpdateID(currency.NewBTCUSDT(), asset.USDTMarginedFutures)
|
|
require.ErrorIs(t, err, orderbook.ErrOrderbookInvalid, "LastUpdateID must error after invalidateCache is called")
|
|
|
|
err = e.Websocket.Orderbook.LoadSnapshot(snapshot)
|
|
require.NoError(t, err)
|
|
|
|
cache.m.Lock()
|
|
err = m.applyUpdate(t.Context(), e, cache, 1337, &orderbook.Update{
|
|
UpdateID: 1339,
|
|
Pair: currency.NewBTCUSDT(),
|
|
Asset: asset.USDTMarginedFutures,
|
|
})
|
|
cache.m.Unlock()
|
|
require.NoError(t, err, "applyUpdate must not error when in sync but update failed to apply")
|
|
|
|
_, err = e.Websocket.Orderbook.LastUpdateID(currency.NewBTCUSDT(), asset.USDTMarginedFutures)
|
|
require.ErrorIs(t, err, orderbook.ErrOrderbookInvalid, "LastUpdateID must error after invalidateCache is called")
|
|
|
|
err = e.Websocket.Orderbook.LoadSnapshot(snapshot)
|
|
require.NoError(t, err)
|
|
|
|
cache.m.Lock()
|
|
err = m.applyUpdate(t.Context(), e, cache, 1337, &orderbook.Update{
|
|
UpdateID: 1338,
|
|
Pair: currency.NewBTCUSDT(),
|
|
Asset: asset.USDTMarginedFutures,
|
|
AllowEmpty: true,
|
|
})
|
|
cache.m.Unlock()
|
|
require.NoError(t, err, "applyUpdate must not error when in sync and update applied")
|
|
}
|
|
|
|
func TestOBManagerProcessOrderbookUpdateHTTPMocked(t *testing.T) {
|
|
t.Parallel()
|
|
e := new(Exchange)
|
|
require.NoError(t, testexch.Setup(e), "Setup must not error")
|
|
e.Name = "ManagerHTTPMocked"
|
|
err := testexch.MockHTTPInstance(e, "/api/v4/")
|
|
require.NoError(t, err, "MockHTTPInstance must not error")
|
|
|
|
// Add dummy subscription so that it can be matched and a limit/level can be extracted for initial orderbook sync spot.
|
|
err = e.Websocket.AddSubscriptions(nil, &subscription.Subscription{Channel: subscription.OrderbookChannel, Interval: kline.TwentyMilliseconds})
|
|
require.NoError(t, err)
|
|
|
|
m := newWsOBUpdateManager(0, defaultWSOrderbookUpdateDeadline)
|
|
err = m.ProcessOrderbookUpdate(t.Context(), e, 27596272446, &orderbook.Update{
|
|
UpdateID: 27596272447,
|
|
Pair: currency.NewBTCUSDT(),
|
|
Asset: asset.Spot,
|
|
AllowEmpty: true,
|
|
UpdateTime: time.Now(),
|
|
})
|
|
require.NoError(t, err, "ProcessOrderbookUpdate must not error")
|
|
|
|
// Wait for the background sync goroutine to complete and orderbook to be synced
|
|
require.Eventually(t, func() bool {
|
|
_, err := e.Websocket.Orderbook.LastUpdateID(currency.NewBTCUSDT(), asset.Spot)
|
|
return err == nil
|
|
}, time.Second*5, time.Millisecond*50, "orderbook must eventually be synced")
|
|
|
|
err = m.ProcessOrderbookUpdate(t.Context(), e, 27596272448, &orderbook.Update{
|
|
UpdateID: 27596272449,
|
|
Pair: currency.NewBTCUSDT(),
|
|
Asset: asset.Spot,
|
|
AllowEmpty: true,
|
|
UpdateTime: time.Now(),
|
|
})
|
|
require.NoError(t, err, "ProcessOrderbookUpdate must not error on synced orderbook")
|
|
|
|
id, err := e.Websocket.Orderbook.LastUpdateID(currency.NewBTCUSDT(), asset.Spot)
|
|
require.NoError(t, err, "LastUpdateID must not error")
|
|
assert.Equal(t, int64(27596272449), id, "LastUpdateID should be updated to orderbook.Update.UpdateID")
|
|
}
|