exchanges/websocket: update websocket rate limiting to use requester rate limiting functionality (#1578)

* exchanges/websocket: update websocket rate limiting to use requester rate limiting functionality.

* glorious: nits

* rm unsused

* updoo

* glorious: purgerino

* reduce duplicate code

* thrasher: engrish

---------

Co-authored-by: shazbert <ryan.oharareid@thrasher.io>
This commit is contained in:
Ryan O'Hara-Reid
2024-09-02 16:43:05 +10:00
committed by GitHub
parent 7c9e6518f3
commit cb6b3421a7
43 changed files with 142 additions and 130 deletions

View File

@@ -10,8 +10,6 @@ import (
"github.com/thrasher-corp/gocryptotrader/types"
)
const wsRateLimitMilliseconds = 250
// withdrawals status codes description
const (
EmailSent = iota

View File

@@ -258,7 +258,7 @@ func (b *Binance) Setup(exch *config.Exchange) error {
return b.Websocket.SetupNewConnection(stream.ConnectionSetup{
ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout,
ResponseMaxLimit: exch.WebsocketResponseMaxLimit,
RateLimit: wsRateLimitMilliseconds,
RateLimit: request.NewWeightedRateLimitByDuration(250 * time.Millisecond),
})
}

View File

@@ -29,8 +29,6 @@ var (
BinanceRequestParamsOrderLimitMarker = RequestParamsOrderType("LIMIT_MAKER")
)
const wsRateLimitMilliseconds = 300
// crypto withdrawals status codes description
const (
EmailSent = iota

View File

@@ -576,7 +576,7 @@ func (bi *Binanceus) Subscribe(channelsToSubscribe subscription.List) error {
for i := range channelsToSubscribe {
payload.Params = append(payload.Params, channelsToSubscribe[i].Channel)
if i%50 == 0 && i != 0 {
err := bi.Websocket.Conn.SendJSONMessage(payload)
err := bi.Websocket.Conn.SendJSONMessage(context.TODO(), payload)
if err != nil {
return err
}
@@ -584,7 +584,7 @@ func (bi *Binanceus) Subscribe(channelsToSubscribe subscription.List) error {
}
}
if len(payload.Params) > 0 {
err := bi.Websocket.Conn.SendJSONMessage(payload)
err := bi.Websocket.Conn.SendJSONMessage(context.TODO(), payload)
if err != nil {
return err
}
@@ -600,7 +600,7 @@ func (bi *Binanceus) Unsubscribe(channelsToUnsubscribe subscription.List) error
for i := range channelsToUnsubscribe {
payload.Params = append(payload.Params, channelsToUnsubscribe[i].Channel)
if i%50 == 0 && i != 0 {
err := bi.Websocket.Conn.SendJSONMessage(payload)
err := bi.Websocket.Conn.SendJSONMessage(context.TODO(), payload)
if err != nil {
return err
}
@@ -608,7 +608,7 @@ func (bi *Binanceus) Unsubscribe(channelsToUnsubscribe subscription.List) error
}
}
if len(payload.Params) > 0 {
err := bi.Websocket.Conn.SendJSONMessage(payload)
err := bi.Websocket.Conn.SendJSONMessage(context.TODO(), payload)
if err != nil {
return err
}

View File

@@ -188,7 +188,7 @@ func (bi *Binanceus) Setup(exch *config.Exchange) error {
return bi.Websocket.SetupNewConnection(stream.ConnectionSetup{
ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout,
ResponseMaxLimit: exch.WebsocketResponseMaxLimit,
RateLimit: wsRateLimitMilliseconds,
RateLimit: request.NewWeightedRateLimitByDuration(300 * time.Millisecond),
})
}

View File

@@ -1710,7 +1710,7 @@ func (b *Bitfinex) GenerateDefaultSubscriptions() (subscription.List, error) {
// ConfigureWS to send checksums and sequence numbers
func (b *Bitfinex) ConfigureWS() error {
return b.Websocket.Conn.SendJSONMessage(map[string]interface{}{
return b.Websocket.Conn.SendJSONMessage(context.TODO(), map[string]interface{}{
"event": "conf",
"flags": bitfinexChecksumFlag + bitfinexWsSequenceFlag,
})
@@ -1914,7 +1914,7 @@ func (b *Bitfinex) WsSendAuth(ctx context.Context) error {
AuthNonce: nonce,
DeadManSwitch: 0,
}
err = b.Websocket.AuthConn.SendJSONMessage(request)
err = b.Websocket.AuthConn.SendJSONMessage(ctx, request)
if err != nil {
b.Websocket.SetCanUseAuthenticatedEndpoints(false)
return err
@@ -2028,7 +2028,7 @@ func (b *Bitfinex) WsCancelMultiOrders(orderIDs []int64) error {
OrderID: orderIDs,
}
request := makeRequestInterface(wsCancelMultipleOrders, cancel)
return b.Websocket.AuthConn.SendJSONMessage(request)
return b.Websocket.AuthConn.SendJSONMessage(context.TODO(), request)
}
// WsCancelOrder authenticated cancel order request
@@ -2079,13 +2079,13 @@ func (b *Bitfinex) WsCancelOrder(orderID int64) error {
func (b *Bitfinex) WsCancelAllOrders() error {
cancelAll := WsCancelAllOrdersRequest{All: 1}
request := makeRequestInterface(wsCancelMultipleOrders, cancelAll)
return b.Websocket.AuthConn.SendJSONMessage(request)
return b.Websocket.AuthConn.SendJSONMessage(context.TODO(), request)
}
// WsNewOffer authenticated new offer request
func (b *Bitfinex) WsNewOffer(data *WsNewOfferRequest) error {
request := makeRequestInterface(wsFundingOfferNew, data)
return b.Websocket.AuthConn.SendJSONMessage(request)
return b.Websocket.AuthConn.SendJSONMessage(context.TODO(), request)
}
// WsCancelOffer authenticated cancel offer request

View File

@@ -1,6 +1,7 @@
package bithumb
import (
"context"
"encoding/json"
"fmt"
"net/http"
@@ -203,7 +204,7 @@ func (b *Bithumb) Subscribe(channelsToSubscribe subscription.List) error {
if s.Channel == "ticker" {
req.TickTypes = wsDefaultTickTypes
}
err := b.Websocket.Conn.SendJSONMessage(req)
err := b.Websocket.Conn.SendJSONMessage(context.TODO(), req)
if err == nil {
err = b.Websocket.AddSuccessfulSubscriptions(s)
}

View File

@@ -32,8 +32,6 @@ import (
"github.com/thrasher-corp/gocryptotrader/portfolio/withdraw"
)
const wsRateLimitMillisecond = 1000
var errNotEnoughPairs = errors.New("at least one currency is required to fetch order history")
// SetDefaults sets the basic defaults for Bithumb
@@ -172,7 +170,7 @@ func (b *Bithumb) Setup(exch *config.Exchange) error {
return b.Websocket.SetupNewConnection(stream.ConnectionSetup{
ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout,
ResponseMaxLimit: exch.WebsocketResponseMaxLimit,
RateLimit: wsRateLimitMillisecond,
RateLimit: request.NewWeightedRateLimitByDuration(time.Second),
})
}

View File

@@ -599,7 +599,7 @@ func (b *Bitmex) Subscribe(subs subscription.List) error {
req.Arguments = append(req.Arguments, cName+":"+p.String())
}
}
err := b.Websocket.Conn.SendJSONMessage(req)
err := b.Websocket.Conn.SendJSONMessage(context.TODO(), req)
if err == nil {
err = b.Websocket.AddSuccessfulSubscriptions(subs...)
}
@@ -618,7 +618,7 @@ func (b *Bitmex) Unsubscribe(subs subscription.List) error {
req.Arguments = append(req.Arguments, cName+":"+p.String())
}
}
err := b.Websocket.Conn.SendJSONMessage(req)
err := b.Websocket.Conn.SendJSONMessage(context.TODO(), req)
if err == nil {
err = b.Websocket.RemoveSubscriptions(subs...)
}
@@ -655,7 +655,7 @@ func (b *Bitmex) websocketSendAuth(ctx context.Context) error {
sendAuth.Command = "authKeyExpires"
sendAuth.Arguments = append(sendAuth.Arguments, creds.Key, timestamp,
signature)
err = b.Websocket.Conn.SendJSONMessage(sendAuth)
err = b.Websocket.Conn.SendJSONMessage(ctx, sendAuth)
if err != nil {
b.Websocket.SetCanUseAuthenticatedEndpoints(false)
return err

View File

@@ -292,7 +292,7 @@ func (b *Bitstamp) Subscribe(channelsToSubscribe subscription.List) error {
req.Data.Channel = "private-" + req.Data.Channel + "-" + strconv.Itoa(int(auth.UserID))
req.Data.Auth = auth.Token
}
err := b.Websocket.Conn.SendJSONMessage(req)
err := b.Websocket.Conn.SendJSONMessage(context.TODO(), req)
if err == nil {
err = b.Websocket.AddSuccessfulSubscriptions(s)
}
@@ -314,7 +314,7 @@ func (b *Bitstamp) Unsubscribe(channelsToUnsubscribe subscription.List) error {
Channel: s.Channel,
},
}
err := b.Websocket.Conn.SendJSONMessage(req)
err := b.Websocket.Conn.SendJSONMessage(context.TODO(), req)
if err == nil {
err = b.Websocket.RemoveSubscriptions(s)
}

View File

@@ -374,7 +374,7 @@ func (b *BTCMarkets) Subscribe(subs subscription.List) error {
r.Channels = []string{s.Channel}
r.MarketIDs = s.Pairs.Strings()
err := b.Websocket.Conn.SendJSONMessage(r)
err := b.Websocket.Conn.SendJSONMessage(context.TODO(), r)
if err == nil {
err = b.Websocket.AddSuccessfulSubscriptions(s)
}
@@ -414,7 +414,7 @@ func (b *BTCMarkets) Unsubscribe(subs subscription.List) error {
MarketIDs: s.Pairs.Strings(),
}
err := b.Websocket.Conn.SendJSONMessage(req)
err := b.Websocket.Conn.SendJSONMessage(context.TODO(), req)
if err == nil {
err = b.Websocket.RemoveSubscriptions(s)
}

View File

@@ -78,7 +78,7 @@ func (b *BTSE) WsAuthenticate(ctx context.Context) error {
Operation: "authKeyExpires",
Arguments: []string{creds.Key, nonce, sign},
}
return b.Websocket.Conn.SendJSONMessage(req)
return b.Websocket.Conn.SendJSONMessage(ctx, req)
}
func stringToOrderStatus(status string) (order.Status, error) {
@@ -392,7 +392,7 @@ func (b *BTSE) Subscribe(channelsToSubscribe subscription.List) error {
for i := range channelsToSubscribe {
sub.Arguments = append(sub.Arguments, channelsToSubscribe[i].Channel)
}
err := b.Websocket.Conn.SendJSONMessage(sub)
err := b.Websocket.Conn.SendJSONMessage(context.TODO(), sub)
if err == nil {
err = b.Websocket.AddSuccessfulSubscriptions(channelsToSubscribe...)
}
@@ -407,7 +407,7 @@ func (b *BTSE) Unsubscribe(channelsToUnsubscribe subscription.List) error {
unSub.Arguments = append(unSub.Arguments,
channelsToUnsubscribe[i].Channel)
}
err := b.Websocket.Conn.SendJSONMessage(unSub)
err := b.Websocket.Conn.SendJSONMessage(context.TODO(), unSub)
if err == nil {
err = b.Websocket.RemoveSubscriptions(channelsToUnsubscribe...)
}

View File

@@ -1,6 +1,7 @@
package bybit
import (
"context"
"net/http"
"github.com/gorilla/websocket"
@@ -71,7 +72,7 @@ func (by *Bybit) handleInversePayloadSubscription(operation string, channelSubsc
for a := range payloads {
// The options connection does not send the subscription request id back with the subscription notification payload
// therefore the code doesn't wait for the response to check whether the subscription is successful or not.
err = by.Websocket.Conn.SendJSONMessage(payloads[a])
err = by.Websocket.Conn.SendJSONMessage(context.TODO(), payloads[a])
if err != nil {
return err
}

View File

@@ -90,7 +90,7 @@ func (by *Bybit) handleLinearPayloadSubscription(operation string, channelSubscr
for a := range payloads {
// The options connection does not send the subscription request id back with the subscription notification payload
// therefore the code doesn't wait for the response to check whether the subscription is successful or not.
err = by.Websocket.Conn.SendJSONMessage(payloads[a])
err = by.Websocket.Conn.SendJSONMessage(context.TODO(), payloads[a])
if err != nil {
return err
}

View File

@@ -1,6 +1,7 @@
package bybit
import (
"context"
"encoding/json"
"net/http"
"strconv"
@@ -78,7 +79,7 @@ func (by *Bybit) handleOptionsPayloadSubscription(operation string, channelSubsc
for a := range payloads {
// The options connection does not send the subscription request id back with the subscription notification payload
// therefore the code doesn't wait for the response to check whether the subscription is successful or not.
err = by.Websocket.Conn.SendJSONMessage(payloads[a])
err = by.Websocket.Conn.SendJSONMessage(context.TODO(), payloads[a])
if err != nil {
return err
}

View File

@@ -77,7 +77,7 @@ func GetRateLimit() request.RateLimitDefinitions {
amendOrderEPL: request.NewRateLimitWithWeight(time.Second, 10, 10),
cancelOrderEPL: request.NewRateLimitWithWeight(time.Second, 10, 10),
cancelSpotEPL: request.NewRateLimitWithWeight(time.Second, 20, 20),
cancelAllEPL: request.NewRateLimitWithWeight(time.Second, 1, 1),
cancelAllEPL: request.NewWeightedRateLimitByDuration(time.Second),
cancelAllSpotEPL: request.NewRateLimitWithWeight(time.Second, 20, 20),
createBatchOrderEPL: request.NewRateLimitWithWeight(time.Second, 10, 10),
amendBatchOrderEPL: request.NewRateLimitWithWeight(time.Second, 10, 10),
@@ -109,7 +109,7 @@ func GetRateLimit() request.RateLimitDefinitions {
interTransferEPL: request.NewRateLimitWithWeight(time.Minute, 20, 1),
saveTransferSubMemberEPL: request.NewRateLimitWithWeight(time.Minute, 20, 1),
universalTransferEPL: request.NewRateLimitWithWeight(time.Second, 5, 5),
createWithdrawalEPL: request.NewRateLimitWithWeight(time.Second, 1, 1),
createWithdrawalEPL: request.NewWeightedRateLimitByDuration(time.Second),
cancelWithdrawalEPL: request.NewRateLimitWithWeight(time.Minute, 60, 1),
userCreateSubMemberEPL: request.NewRateLimitWithWeight(time.Second, 5, 5),
userCreateSubAPIKeyEPL: request.NewRateLimitWithWeight(time.Second, 5, 5),

View File

@@ -423,7 +423,7 @@ func (c *CoinbasePro) Subscribe(subs subscription.List) error {
r.Channels = append(r.Channels, s.Channel)
}
}
err := c.Websocket.Conn.SendJSONMessage(r)
err := c.Websocket.Conn.SendJSONMessage(context.TODO(), r)
if err == nil {
err = c.Websocket.AddSuccessfulSubscriptions(subs...)
}
@@ -459,7 +459,7 @@ func (c *CoinbasePro) Unsubscribe(subs subscription.List) error {
ProductIDs: s.Pairs.Strings(),
})
}
err := c.Websocket.Conn.SendJSONMessage(r)
err := c.Websocket.Conn.SendJSONMessage(context.TODO(), r)
if err == nil {
err = c.Websocket.RemoveSubscriptions(subs...)
}

View File

@@ -43,7 +43,6 @@ const (
coinutStatusOK = "OK"
coinutMaxNonce = 16777215 // See https://github.com/coinut/api/wiki/Websocket-API#nonce
wsRateLimitInMilliseconds = 33
)
var errLookupInstrumentID = errors.New("unable to lookup instrument ID")

View File

@@ -618,7 +618,7 @@ func (c *COINUT) Subscribe(subs subscription.List) error {
Subscribe: true,
Nonce: getNonce(),
}
err = c.Websocket.Conn.SendJSONMessage(subscribe)
err = c.Websocket.Conn.SendJSONMessage(context.TODO(), subscribe)
if err == nil {
err = c.Websocket.AddSuccessfulSubscriptions(s)
}

View File

@@ -151,7 +151,7 @@ func (c *COINUT) Setup(exch *config.Exchange) error {
return c.Websocket.SetupNewConnection(stream.ConnectionSetup{
ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout,
ResponseMaxLimit: exch.WebsocketResponseMaxLimit,
RateLimit: wsRateLimitInMilliseconds,
RateLimit: request.NewWeightedRateLimitByDuration(33 * time.Millisecond),
})
}

View File

@@ -113,7 +113,7 @@ func (d *Deribit) WsConnect() error {
d.Websocket.SetCanUseAuthenticatedEndpoints(false)
}
}
return d.Websocket.Conn.SendJSONMessage(setHeartBeatMessage)
return d.Websocket.Conn.SendJSONMessage(context.TODO(), setHeartBeatMessage)
}
func (d *Deribit) wsLogin(ctx context.Context) error {
@@ -187,7 +187,7 @@ func (d *Deribit) wsHandleData(respRaw []byte) error {
return fmt.Errorf("%s - err %s could not parse websocket data: %s", d.Name, err, respRaw)
}
if response.Method == "heartbeat" {
return d.Websocket.Conn.SendJSONMessage(pingMessage)
return d.Websocket.Conn.SendJSONMessage(context.TODO(), pingMessage)
}
if response.ID > 2 {
if !d.Websocket.Match.IncomingWithData(response.ID, respRaw) {

View File

@@ -30,7 +30,7 @@ import (
const (
gateioWebsocketEndpoint = "wss://api.gateio.ws/ws/v4/"
gateioWebsocketRateLimit = 120
gateioWebsocketRateLimit = 120 * time.Millisecond
spotPingChannel = "spot.ping"
spotPongChannel = "spot.pong"

View File

@@ -227,7 +227,7 @@ func (g *Gateio) Setup(exch *config.Exchange) error {
}
return g.Websocket.SetupNewConnection(stream.ConnectionSetup{
URL: gateioWebsocketEndpoint,
RateLimit: gateioWebsocketRateLimit,
RateLimit: request.NewWeightedRateLimitByDuration(gateioWebsocketRateLimit),
ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout,
ResponseMaxLimit: exch.WebsocketResponseMaxLimit,
BespokeGenerateMessageID: g.GenerateWebsocketMessageID,

View File

@@ -16,6 +16,7 @@ import (
"github.com/thrasher-corp/gocryptotrader/exchanges/account"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/kline"
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
"github.com/thrasher-corp/gocryptotrader/exchanges/stream"
"github.com/thrasher-corp/gocryptotrader/exchanges/subscription"
"github.com/thrasher-corp/gocryptotrader/log"
@@ -63,7 +64,7 @@ func (g *Gateio) WsDeliveryFuturesConnect() error {
}
err = g.Websocket.SetupNewConnection(stream.ConnectionSetup{
URL: deliveryRealBTCTradingURL,
RateLimit: gateioWebsocketRateLimit,
RateLimit: request.NewWeightedRateLimitByDuration(gateioWebsocketRateLimit),
ResponseCheckTimeout: g.Config.WebsocketResponseCheckTimeout,
ResponseMaxLimit: g.Config.WebsocketResponseMaxLimit,
Authenticated: true,

View File

@@ -19,6 +19,7 @@ import (
"github.com/thrasher-corp/gocryptotrader/exchanges/kline"
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
"github.com/thrasher-corp/gocryptotrader/exchanges/orderbook"
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
"github.com/thrasher-corp/gocryptotrader/exchanges/stream"
"github.com/thrasher-corp/gocryptotrader/exchanges/subscription"
"github.com/thrasher-corp/gocryptotrader/exchanges/ticker"
@@ -82,7 +83,7 @@ func (g *Gateio) WsFuturesConnect() error {
err = g.Websocket.SetupNewConnection(stream.ConnectionSetup{
URL: futuresWebsocketBtcURL,
RateLimit: gateioWebsocketRateLimit,
RateLimit: request.NewWeightedRateLimitByDuration(gateioWebsocketRateLimit),
ResponseCheckTimeout: g.Config.WebsocketResponseCheckTimeout,
ResponseMaxLimit: g.Config.WebsocketResponseMaxLimit,
Authenticated: true,

View File

@@ -112,7 +112,7 @@ func (g *Gemini) manageSubs(subs subscription.List, op wsSubOp) error {
})
}
if err := g.Websocket.Conn.SendJSONMessage(req); err != nil {
if err := g.Websocket.Conn.SendJSONMessage(context.TODO(), req); err != nil {
return err
}

View File

@@ -27,7 +27,6 @@ import (
const (
hitbtcWebsocketAddress = "wss://api.hitbtc.com/api/2/ws"
rpcVersion = "2.0"
rateLimit = 20
errAuthFailed = 1002
)
@@ -524,7 +523,7 @@ func (h *HitBTC) Subscribe(channelsToSubscribe subscription.List) error {
r.Params.Limit = 100
}
err := h.Websocket.Conn.SendJSONMessage(r)
err := h.Websocket.Conn.SendJSONMessage(context.TODO(), r)
if err == nil {
err = h.Websocket.AddSuccessfulSubscriptions(s)
}
@@ -560,7 +559,7 @@ func (h *HitBTC) Unsubscribe(subs subscription.List) error {
r.Params.Limit = 100
}
err := h.Websocket.Conn.SendJSONMessage(r)
err := h.Websocket.Conn.SendJSONMessage(context.TODO(), r)
if err == nil {
err = h.Websocket.RemoveSubscriptions(s)
}
@@ -600,7 +599,7 @@ func (h *HitBTC) wsLogin(ctx context.Context) error {
ID: h.Websocket.Conn.GenerateMessageID(false),
}
err = h.Websocket.Conn.SendJSONMessage(request)
err = h.Websocket.Conn.SendJSONMessage(context.TODO(), request)
if err != nil {
h.Websocket.SetCanUseAuthenticatedEndpoints(false)
return err

View File

@@ -169,7 +169,7 @@ func (h *HitBTC) Setup(exch *config.Exchange) error {
}
return h.Websocket.SetupNewConnection(stream.ConnectionSetup{
RateLimit: rateLimit,
RateLimit: request.NewWeightedRateLimitByDuration(20 * time.Millisecond),
ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout,
ResponseMaxLimit: exch.WebsocketResponseMaxLimit,
})

View File

@@ -53,7 +53,6 @@ const (
authOp = "auth"
loginDelay = 50 * time.Millisecond
rateLimit = 20
)
// Instantiates a communications channel between websocket connections
@@ -225,7 +224,7 @@ func (h *HUOBI) wsHandleData(respRaw []byte) error {
OP: "pong",
TS: init.TS,
}
err := h.Websocket.AuthConn.SendJSONMessage(authPing)
err := h.Websocket.AuthConn.SendJSONMessage(context.TODO(), authPing)
if err != nil {
log.Errorln(log.ExchangeSys, err)
}
@@ -445,7 +444,7 @@ func (h *HUOBI) wsHandleData(respRaw []byte) error {
}
func (h *HUOBI) sendPingResponse(pong int64) {
err := h.Websocket.Conn.SendJSONMessage(WsPong{Pong: pong})
err := h.Websocket.Conn.SendJSONMessage(context.TODO(), WsPong{Pong: pong})
if err != nil {
log.Errorln(log.ExchangeSys, err)
}
@@ -565,7 +564,7 @@ func (h *HUOBI) Subscribe(channelsToSubscribe subscription.List) error {
wsAccountsOrdersEndPoint+channelsToSubscribe[i].Channel,
channelsToSubscribe[i].Channel)
} else {
err = h.Websocket.Conn.SendJSONMessage(WsRequest{
err = h.Websocket.Conn.SendJSONMessage(context.TODO(), WsRequest{
Subscribe: channelsToSubscribe[i].Channel,
})
}
@@ -599,7 +598,7 @@ func (h *HUOBI) Unsubscribe(channelsToUnsubscribe subscription.List) error {
wsAccountsOrdersEndPoint+channelsToUnsubscribe[i].Channel,
channelsToUnsubscribe[i].Channel)
} else {
err = h.Websocket.Conn.SendJSONMessage(WsRequest{
err = h.Websocket.Conn.SendJSONMessage(context.TODO(), WsRequest{
Unsubscribe: channelsToUnsubscribe[i].Channel,
})
}
@@ -648,7 +647,7 @@ func (h *HUOBI) wsLogin(ctx context.Context) error {
return err
}
request.Signature = crypto.Base64Encode(hmac)
err = h.Websocket.AuthConn.SendJSONMessage(request)
err = h.Websocket.AuthConn.SendJSONMessage(context.TODO(), request)
if err != nil {
h.Websocket.SetCanUseAuthenticatedEndpoints(false)
return err
@@ -673,7 +672,7 @@ func (h *HUOBI) wsAuthenticatedSubscribe(creds *account.Credentials, operation,
return err
}
request.Signature = crypto.Base64Encode(hmac)
return h.Websocket.AuthConn.SendJSONMessage(request)
return h.Websocket.AuthConn.SendJSONMessage(context.TODO(), request)
}
func (h *HUOBI) wsGetAccountsList(ctx context.Context) (*WsAuthenticatedAccountsListResponse, error) {

View File

@@ -220,7 +220,7 @@ func (h *HUOBI) Setup(exch *config.Exchange) error {
}
err = h.Websocket.SetupNewConnection(stream.ConnectionSetup{
RateLimit: rateLimit,
RateLimit: request.NewWeightedRateLimitByDuration(20 * time.Millisecond),
ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout,
ResponseMaxLimit: exch.WebsocketResponseMaxLimit,
})
@@ -229,7 +229,7 @@ func (h *HUOBI) Setup(exch *config.Exchange) error {
}
return h.Websocket.SetupNewConnection(stream.ConnectionSetup{
RateLimit: rateLimit,
RateLimit: request.NewWeightedRateLimitByDuration(20 * time.Millisecond),
ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout,
ResponseMaxLimit: exch.WebsocketResponseMaxLimit,
URL: wsAccountsOrdersURL,

View File

@@ -52,7 +52,6 @@ const (
krakenWsAddOrderStatus = "addOrderStatus"
krakenWsCancelOrderStatus = "cancelOrderStatus"
krakenWsCancelAllOrderStatus = "cancelAllStatus"
krakenWsRateLimit = 50
krakenWsPingDelay = time.Second * 27
krakenWsOrderbookDepth = 1000
)

View File

@@ -232,7 +232,7 @@ func (k *Kraken) Setup(exch *config.Exchange) error {
}
err = k.Websocket.SetupNewConnection(stream.ConnectionSetup{
RateLimit: krakenWsRateLimit,
RateLimit: request.NewWeightedRateLimitByDuration(50 * time.Millisecond),
ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout,
ResponseMaxLimit: exch.WebsocketResponseMaxLimit,
URL: krakenWSURL,
@@ -242,7 +242,7 @@ func (k *Kraken) Setup(exch *config.Exchange) error {
}
return k.Websocket.SetupNewConnection(stream.ConnectionSetup{
RateLimit: krakenWsRateLimit,
RateLimit: request.NewWeightedRateLimitByDuration(50 * time.Millisecond),
ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout,
ResponseMaxLimit: exch.WebsocketResponseMaxLimit,
URL: krakenAuthWSURL,

View File

@@ -217,7 +217,7 @@ func (ku *Kucoin) Setup(exch *config.Exchange) error {
return ku.Websocket.SetupNewConnection(stream.ConnectionSetup{
ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout,
ResponseMaxLimit: exch.WebsocketResponseMaxLimit,
RateLimit: 500,
RateLimit: request.NewRateLimitWithWeight(time.Second, 2, 1),
})
}

View File

@@ -292,7 +292,7 @@ func (ok *Okx) WsAuth(ctx context.Context, dialer *websocket.Dialer) error {
},
},
}
err = ok.Websocket.AuthConn.SendJSONMessage(request)
err = ok.Websocket.AuthConn.SendJSONMessage(ctx, request)
if err != nil {
return err
}
@@ -481,7 +481,7 @@ func (ok *Okx) handleSubscription(operation string, subscriptions subscription.L
if len(authChunk) > maxConnByteLen {
authRequests.Arguments = authRequests.Arguments[:len(authRequests.Arguments)-1]
i--
err = ok.Websocket.AuthConn.SendJSONMessage(authRequests)
err = ok.Websocket.AuthConn.SendJSONMessage(context.TODO(), authRequests)
if err != nil {
return err
}
@@ -505,7 +505,7 @@ func (ok *Okx) handleSubscription(operation string, subscriptions subscription.L
}
if len(chunk) > maxConnByteLen {
i--
err = ok.Websocket.Conn.SendJSONMessage(request)
err = ok.Websocket.Conn.SendJSONMessage(context.TODO(), request)
if err != nil {
return err
}
@@ -525,13 +525,13 @@ func (ok *Okx) handleSubscription(operation string, subscriptions subscription.L
}
if len(request.Arguments) > 0 {
if err := ok.Websocket.Conn.SendJSONMessage(request); err != nil {
if err := ok.Websocket.Conn.SendJSONMessage(context.TODO(), request); err != nil {
return err
}
}
if len(authRequests.Arguments) > 0 && ok.Websocket.CanUseAuthenticatedEndpoints() {
if err := ok.Websocket.AuthConn.SendJSONMessage(authRequests); err != nil {
if err := ok.Websocket.AuthConn.SendJSONMessage(context.TODO(), authRequests); err != nil {
return err
}
}
@@ -1369,7 +1369,7 @@ func (ok *Okx) WsPlaceOrder(arg *PlaceOrderRequestParam) (*OrderData, error) {
Arguments: []PlaceOrderRequestParam{*arg},
Operation: okxOpOrder,
}
err = ok.Websocket.AuthConn.SendJSONMessage(input)
err = ok.Websocket.AuthConn.SendJSONMessage(context.TODO(), input)
if err != nil {
return nil, err
}
@@ -1428,7 +1428,7 @@ func (ok *Okx) WsPlaceMultipleOrder(args []PlaceOrderRequestParam) ([]OrderData,
Arguments: args,
Operation: okxOpBatchOrders,
}
err = ok.Websocket.AuthConn.SendJSONMessage(input)
err = ok.Websocket.AuthConn.SendJSONMessage(context.TODO(), input)
if err != nil {
return nil, err
}
@@ -1498,7 +1498,7 @@ func (ok *Okx) WsCancelOrder(arg CancelOrderRequestParam) (*OrderData, error) {
Arguments: []CancelOrderRequestParam{arg},
Operation: okxOpCancelOrder,
}
err = ok.Websocket.AuthConn.SendJSONMessage(input)
err = ok.Websocket.AuthConn.SendJSONMessage(context.TODO(), input)
if err != nil {
return nil, err
}
@@ -1558,7 +1558,7 @@ func (ok *Okx) WsCancelMultipleOrder(args []CancelOrderRequestParam) ([]OrderDat
Arguments: args,
Operation: okxOpBatchCancelOrders,
}
err = ok.Websocket.AuthConn.SendJSONMessage(input)
err = ok.Websocket.AuthConn.SendJSONMessage(context.TODO(), input)
if err != nil {
return nil, err
}
@@ -1634,7 +1634,7 @@ func (ok *Okx) WsAmendOrder(arg *AmendOrderRequestParams) (*OrderData, error) {
Operation: okxOpAmendOrder,
Arguments: []AmendOrderRequestParams{*arg},
}
err = ok.Websocket.AuthConn.SendJSONMessage(input)
err = ok.Websocket.AuthConn.SendJSONMessage(context.TODO(), input)
if err != nil {
return nil, err
}
@@ -1696,7 +1696,7 @@ func (ok *Okx) WsAmendMultipleOrders(args []AmendOrderRequestParams) ([]OrderDat
Operation: okxOpBatchAmendOrders,
Arguments: args,
}
err = ok.Websocket.AuthConn.SendJSONMessage(input)
err = ok.Websocket.AuthConn.SendJSONMessage(context.TODO(), input)
if err != nil {
return nil, err
}
@@ -1849,7 +1849,7 @@ func (ok *Okx) wsChannelSubscription(operation, channel string, assetType asset.
}
ok.WsRequestSemaphore <- 1
defer func() { <-ok.WsRequestSemaphore }()
return ok.Websocket.Conn.SendJSONMessage(input)
return ok.Websocket.Conn.SendJSONMessage(context.TODO(), input)
}
// Private Channel Websocket methods
@@ -1915,7 +1915,7 @@ func (ok *Okx) wsAuthChannelSubscription(operation, channel string, assetType as
}
ok.WsRequestSemaphore <- 1
defer func() { <-ok.WsRequestSemaphore }()
return ok.Websocket.AuthConn.SendJSONMessage(input)
return ok.Websocket.AuthConn.SendJSONMessage(context.TODO(), input)
}
// WsAccountSubscription retrieve account information. Data will be pushed when triggered by

View File

@@ -227,7 +227,7 @@ func (ok *Okx) Setup(exch *config.Exchange) error {
URL: okxAPIWebsocketPublicURL,
ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout,
ResponseMaxLimit: okxWebsocketResponseMaxLimit,
RateLimit: 500,
RateLimit: request.NewRateLimitWithWeight(time.Second, 2, 1),
}); err != nil {
return err
}
@@ -237,7 +237,7 @@ func (ok *Okx) Setup(exch *config.Exchange) error {
ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout,
ResponseMaxLimit: okxWebsocketResponseMaxLimit,
Authenticated: true,
RateLimit: 500,
RateLimit: request.NewRateLimitWithWeight(time.Second, 2, 1),
})
}

View File

@@ -604,7 +604,7 @@ func (p *Poloniex) manageSubs(subs subscription.List, op wsOp) error {
}
req.Channel = s.Pairs[0].String()
}
err = p.Websocket.Conn.SendJSONMessage(req)
err = p.Websocket.Conn.SendJSONMessage(context.TODO(), req)
}
if err == nil {
if op == wsSubscribeOp {
@@ -635,7 +635,7 @@ func (p *Poloniex) wsSendAuthorisedCommand(secret, key string, op wsOp) error {
Key: key,
Payload: nonce,
}
return p.Websocket.Conn.SendJSONMessage(request)
return p.Websocket.Conn.SendJSONMessage(context.TODO(), request)
}
func (p *Poloniex) processAccountMarginPosition(notification []interface{}) error {

View File

@@ -80,6 +80,12 @@ func NewRateLimitWithWeight(interval time.Duration, actions int, weight Weight)
return GetRateLimiterWithWeight(NewRateLimit(interval, actions), weight)
}
// NewWeightedRateLimitByDuration creates a new RateLimit based of time
// interval. This equates to 1 action per interval. The weight is set to 1.
func NewWeightedRateLimitByDuration(interval time.Duration) *RateLimiterWithWeight {
return NewRateLimitWithWeight(interval, 1, 1)
}
// GetRateLimiterWithWeight couples a rate limiter with a weight count into an
// accepted defined rate limiter with weight struct
func GetRateLimiterWithWeight(l *rate.Limiter, weight Weight) *RateLimiterWithWeight {
@@ -107,12 +113,24 @@ func (r *Requester) InitiateRateLimit(ctx context.Context, e EndpointLimit) erro
rateLimiter := r.limiter[e]
err := RateLimit(ctx, rateLimiter)
if err != nil {
return fmt.Errorf("cannot rate limit request %w for endpoint %d", err, e)
}
return nil
}
// RateLimit is a function that will rate limit a request based on the rate
// limiter provided. It will return an error if the context is cancelled or
// deadline exceeded.
func RateLimit(ctx context.Context, rateLimiter *RateLimiterWithWeight) error {
if rateLimiter == nil {
return fmt.Errorf("cannot rate limit request %w for endpoint %d", errSpecificRateLimiterIsNil, e)
return errSpecificRateLimiterIsNil
}
if rateLimiter.Weight <= 0 {
return fmt.Errorf("cannot rate limit request %w for endpoint %d", errInvalidWeightCount, e)
return errInvalidWeightCount
}
var finalDelay time.Duration

View File

@@ -31,8 +31,8 @@ var serverLimit *RateLimiterWithWeight
func TestMain(m *testing.M) {
serverLimitInterval := time.Millisecond * 500
serverLimit = NewRateLimitWithWeight(serverLimitInterval, 1, 1)
serverLimitRetry := NewRateLimitWithWeight(serverLimitInterval, 1, 1)
serverLimit = NewWeightedRateLimitByDuration(serverLimitInterval)
serverLimitRetry := NewWeightedRateLimitByDuration(serverLimitInterval)
sm := http.NewServeMux()
sm.HandleFunc("/", func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "application/json")
@@ -201,7 +201,7 @@ func TestCheckRequest(t *testing.T) {
}
var globalshell = RateLimitDefinitions{
Auth: NewRateLimitWithWeight(time.Millisecond*600, 1, 1),
Auth: NewWeightedRateLimitByDuration(time.Millisecond * 600),
UnAuth: NewRateLimitWithWeight(time.Second*1, 100, 1)}
func TestDoRequest(t *testing.T) {

View File

@@ -9,13 +9,14 @@ import (
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
)
// Connection defines a streaming services connection
type Connection interface {
Dial(*websocket.Dialer, http.Header) error
ReadMessage() Response
SendJSONMessage(any) error
SendJSONMessage(ctx context.Context, payload any) error
SetupPingHandler(PingHandler)
// GenerateMessageID generates a message ID for the individual connection.
// If a bespoke function is set (by using SetupNewConnection) it will use
@@ -24,7 +25,7 @@ type Connection interface {
GenerateMessageID(highPrecision bool) int64
SendMessageReturnResponse(ctx context.Context, signature any, request any) ([]byte, error)
SendMessageReturnResponses(ctx context.Context, signature any, request any, expected int) ([][]byte, error)
SendRawMessage(messageType int, message []byte) error
SendRawMessage(ctx context.Context, messageType int, message []byte) error
SetURL(string)
SetProxy(string)
GetURL() string
@@ -41,7 +42,7 @@ type Response struct {
type ConnectionSetup struct {
ResponseCheckTimeout time.Duration
ResponseMaxLimit time.Duration
RateLimit int64
RateLimit *request.RateLimiterWithWeight
URL string
Authenticated bool
ConnectionLevelReporter Reporter

View File

@@ -207,7 +207,7 @@ func (w *Websocket) SetupNewConnection(c ConnectionSetup) error {
if c.ResponseCheckTimeout == 0 &&
c.ResponseMaxLimit == 0 &&
c.RateLimit == 0 &&
c.RateLimit == nil &&
c.URL == "" &&
c.ConnectionLevelReporter == nil &&
c.BespokeGenerateMessageID == nil {

View File

@@ -17,6 +17,7 @@ import (
"time"
"github.com/gorilla/websocket"
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
"github.com/thrasher-corp/gocryptotrader/log"
)
@@ -54,51 +55,46 @@ func (w *WebsocketConnection) Dial(dialer *websocket.Dialer, headers http.Header
}
// SendJSONMessage sends a JSON encoded message over the connection
func (w *WebsocketConnection) SendJSONMessage(data interface{}) error {
if !w.IsConnected() {
return fmt.Errorf("%s websocket connection: cannot send message to a disconnected websocket", w.ExchangeName)
}
w.writeControl.Lock()
defer w.writeControl.Unlock()
if w.Verbose {
if msg, err := json.Marshal(data); err == nil { // WriteJSON will error for us anyway
log.Debugf(log.WebsocketMgr, "%s websocket connection: sending message: %s\n", w.ExchangeName, msg)
func (w *WebsocketConnection) SendJSONMessage(ctx context.Context, data interface{}) error {
return w.writeToConn(ctx, func() error {
if w.Verbose {
if msg, err := json.Marshal(data); err == nil { // WriteJSON will error for us anyway
log.Debugf(log.WebsocketMgr, "%s websocket connection: sending message: %s\n", w.ExchangeName, msg)
}
}
}
if w.RateLimit > 0 {
time.Sleep(time.Duration(w.RateLimit) * time.Millisecond)
if !w.IsConnected() {
return fmt.Errorf("%v websocket connection: cannot send message to a disconnected websocket", w.ExchangeName)
}
}
return w.Connection.WriteJSON(data)
return w.Connection.WriteJSON(data)
})
}
// SendRawMessage sends a message over the connection without JSON encoding it
func (w *WebsocketConnection) SendRawMessage(messageType int, message []byte) error {
func (w *WebsocketConnection) SendRawMessage(ctx context.Context, messageType int, message []byte) error {
return w.writeToConn(ctx, func() error {
if w.Verbose {
log.Debugf(log.WebsocketMgr, "%v websocket connection: sending message [%s]\n", w.ExchangeName, message)
}
return w.Connection.WriteMessage(messageType, message)
})
}
func (w *WebsocketConnection) writeToConn(ctx context.Context, writeConn func() error) error {
if !w.IsConnected() {
return fmt.Errorf("%v websocket connection: cannot send message to a disconnected websocket", w.ExchangeName)
}
w.writeControl.Lock()
defer w.writeControl.Unlock()
if w.Verbose {
log.Debugf(log.WebsocketMgr, "%v websocket connection: sending message [%s]\n", w.ExchangeName, message)
}
if w.RateLimit > 0 {
time.Sleep(time.Duration(w.RateLimit) * time.Millisecond)
if !w.IsConnected() {
return fmt.Errorf("%v websocket connection: cannot send message to a disconnected websocket", w.ExchangeName)
if w.RateLimit != nil {
err := request.RateLimit(ctx, w.RateLimit)
if err != nil {
return fmt.Errorf("%s websocket connection: rate limit error: %w", w.ExchangeName, err)
}
}
// This lock acts as a rolling gate to prevent WriteMessage panics. Acquire after rate limit check.
w.writeControl.Lock()
defer w.writeControl.Unlock()
// NOTE: Secondary check to ensure the connection is still active after
// semacquire and potential rate limit.
if !w.IsConnected() {
return fmt.Errorf("%v websocket connection: cannot send message to a disconnected websocket", w.ExchangeName)
}
return w.Connection.WriteMessage(messageType, message)
return writeConn()
}
// SetupPingHandler will automatically send ping or pong messages based on
@@ -129,7 +125,7 @@ func (w *WebsocketConnection) SetupPingHandler(handler PingHandler) {
ticker.Stop()
return
case <-ticker.C:
err := w.SendRawMessage(handler.MessageType, handler.Message)
err := w.SendRawMessage(context.TODO(), handler.MessageType, handler.Message)
if err != nil {
log.Errorf(log.WebsocketMgr,
"%v websocket connection: ping handler failed to send message [%s]",
@@ -303,7 +299,7 @@ func (w *WebsocketConnection) SendMessageReturnResponses(ctx context.Context, si
}
start := time.Now()
err = w.SendRawMessage(websocket.TextMessage, outbound)
err = w.SendRawMessage(ctx, websocket.TextMessage, outbound)
if err != nil {
return nil, err
}

View File

@@ -24,6 +24,7 @@ import (
"github.com/thrasher-corp/gocryptotrader/config"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/exchanges/protocol"
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
"github.com/thrasher-corp/gocryptotrader/exchanges/subscription"
)
@@ -629,7 +630,7 @@ func TestDial(t *testing.T) {
ExchangeName: "test1",
Verbose: true,
URL: websocketTestURL,
RateLimit: 10,
RateLimit: request.NewWeightedRateLimitByDuration(10 * time.Millisecond),
ResponseMaxLimit: 7000000000,
},
},
@@ -677,7 +678,7 @@ func TestSendMessage(t *testing.T) {
ExchangeName: "test1",
Verbose: true,
URL: websocketTestURL,
RateLimit: 10,
RateLimit: request.NewWeightedRateLimitByDuration(10 * time.Millisecond),
ResponseMaxLimit: 7000000000,
},
},
@@ -713,11 +714,11 @@ func TestSendMessage(t *testing.T) {
}
t.Fatal(err)
}
err = testData.WC.SendJSONMessage(Ping)
err = testData.WC.SendJSONMessage(context.Background(), Ping)
if err != nil {
t.Error(err)
}
err = testData.WC.SendRawMessage(websocket.TextMessage, []byte(Ping))
err = testData.WC.SendRawMessage(context.Background(), websocket.TextMessage, []byte(Ping))
if err != nil {
t.Error(err)
}

View File

@@ -9,6 +9,7 @@ import (
"github.com/thrasher-corp/gocryptotrader/config"
"github.com/thrasher-corp/gocryptotrader/exchanges/fill"
"github.com/thrasher-corp/gocryptotrader/exchanges/protocol"
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
"github.com/thrasher-corp/gocryptotrader/exchanges/stream/buffer"
"github.com/thrasher-corp/gocryptotrader/exchanges/subscription"
"github.com/thrasher-corp/gocryptotrader/exchanges/trade"
@@ -132,7 +133,7 @@ type WebsocketConnection struct {
// writes methods
writeControl sync.Mutex
RateLimit int64
RateLimit *request.RateLimiterWithWeight
ExchangeName string
URL string
ProxyURL string