websocket: Remove GenerateMessageID (#2008)

* Exchanges: Remove example BespokeGenerateMessageID

* Okx: Replace conn.RequestIDGenerator with MesssageID

Continued overall direction to remove the closed-loop of e => conn => e
roundtrip for message ids

* Exchanges: Add MessageSequence

This method removes the either/or nature of message id generation.
We don't tie the message ids to connections, or to anything.
Consumers just call whichever they want, or even combine them as they
want.
Anything more complicated will need a separate installation anyway

* GateIO: Split usage of MessageID and MessageSequence

* Binance: Switch to UUID message IDs

* Kraken: Switch to e.MessageSequence

* Kucoin: Switch to MessageID

* HitBTC: Switch to UUIDv7 for ws message ID

* Bybit: Switch to UUIDv7 for ws message ID

* Bitfinex: Switch to UUIDv7 and MessageSequence

Tested CID - It accepts 53 bits only for an int, so MessageSequence
makes sense. Can't use MessageID

* Websocket: Remove now unused MessageID function

Moved all MessageID usage into funcs and onto base methods, to remove
the closed loop of message IDs

* Docs: Update guidance for message signatures
This commit is contained in:
Gareth Kirwan
2025-10-24 07:14:24 +07:00
committed by GitHub
parent 0f70cfd8b6
commit bda9bbec66
33 changed files with 137 additions and 224 deletions

View File

@@ -127,7 +127,6 @@ func (e *Exchange) Setup(exch *config.Exchange) error {
Unsubscriber: e.SpotUnsubscribe,
GenerateSubscriptions: e.GenerateDefaultSubscriptionsSpot,
Connector: e.WsConnectSpot,
BespokeGenerateMessageID: e.GenerateWebsocketMessageID,
}); err != nil {
return err
}
@@ -143,7 +142,6 @@ func (e *Exchange) Setup(exch *config.Exchange) error {
Unsubscriber: e.FuturesUnsubscribe,
GenerateSubscriptions: func() (subscription.List, error) { return e.GenerateFuturesDefaultSubscriptions(currency.USDT) },
Connector: e.WsFuturesConnect,
BespokeGenerateMessageID: e.GenerateWebsocketMessageID,
}); err != nil {
return err
}

View File

@@ -5,11 +5,9 @@ import (
"compress/flate"
"compress/gzip"
"context"
"crypto/rand"
"errors"
"fmt"
"io"
"math/big"
"net"
"net/http"
"net/url"
@@ -38,10 +36,6 @@ type Connection interface {
Dial(context.Context, *gws.Dialer, http.Header) error
ReadMessage() Response
SetupPingHandler(request.EndpointLimit, PingHandler)
// GenerateMessageID generates a message ID for the individual connection. If a bespoke function is set
// (by using SetupNewConnection) it will use that, otherwise it will use the defaultGenerateMessageID function
// defined in websocket_connection.go.
GenerateMessageID(highPrecision bool) int64
// SendMessageReturnResponse will send a WS message to the connection and wait for response
SendMessageReturnResponse(ctx context.Context, epl request.EndpointLimit, signature, request any) ([]byte, error)
// SendMessageReturnResponses will send a WS message to the connection and wait for N responses
@@ -94,10 +88,6 @@ type ConnectionSetup struct {
// received from the exchange's websocket server. This function should
// handle the incoming message and pass it to the appropriate data handler.
Handler func(ctx context.Context, conn Connection, incoming []byte) error
// RequestIDGenerator is a function that returns a unique message ID.
// This is useful for when an exchange connection requires a unique or
// structured message ID for each message sent.
RequestIDGenerator func() int64
// Authenticate will be called to authenticate the connection
Authenticate func(ctx context.Context, conn Connection) error
// MessageFilter defines the criteria used to match messages to a specific connection.
@@ -135,7 +125,6 @@ type connection struct {
ResponseMaxLimit time.Duration
Traffic chan struct{}
readMessageErrors chan error
requestIDGenerator func() int64
}
// Dial sets proxy urls and then connects to the websocket
@@ -337,33 +326,6 @@ func (c *connection) parseBinaryResponse(resp []byte) ([]byte, error) {
return standardMessage, reader.Close()
}
// GenerateMessageID generates a message ID for the individual connection.
// If a bespoke function is set (by using SetupNewConnection) it will use that,
// otherwise it will use the defaultGenerateMessageID function.
func (c *connection) GenerateMessageID(highPrec bool) int64 {
if c.requestIDGenerator != nil {
return c.requestIDGenerator()
}
return c.defaultGenerateMessageID(highPrec)
}
// defaultGenerateMessageID generates the default message ID
func (c *connection) defaultGenerateMessageID(highPrec bool) int64 {
var minValue int64 = 1e8
var maxValue int64 = 2e8
if highPrec {
maxValue = 2e12
minValue = 1e12
}
// utilization of hard coded positive numbers and default crypto/rand
// io.reader will panic on error instead of returning
randomNumber, err := rand.Int(rand.Reader, big.NewInt(maxValue-minValue+1))
if err != nil {
panic(err)
}
return randomNumber.Int64() + minValue
}
// Shutdown shuts down and closes specific connection
func (c *connection) Shutdown() error {
if err := common.NilGuard(c, c.Connection); err != nil {

View File

@@ -305,7 +305,7 @@ func (m *Manager) SetupNewConnection(c *ConnectionSetup) error {
return err
}
if c.ResponseCheckTimeout == 0 && c.ResponseMaxLimit == 0 && c.RateLimit == nil && c.URL == "" && c.ConnectionLevelReporter == nil && c.RequestIDGenerator == nil {
if c.ResponseCheckTimeout == 0 && c.ResponseMaxLimit == 0 && c.RateLimit == nil && c.URL == "" && c.ConnectionLevelReporter == nil {
return fmt.Errorf("%w: %w", errConnSetup, errExchangeConfigEmpty)
}
@@ -401,7 +401,6 @@ func (m *Manager) getConnectionFromSetup(c *ConnectionSetup) *connection {
Match: match,
RateLimit: c.RateLimit,
Reporter: c.ConnectionLevelReporter,
requestIDGenerator: c.RequestIDGenerator,
RateLimitDefinitions: m.rateLimitDefinitions,
}
}

View File

@@ -564,7 +564,7 @@ func TestSendMessageReturnResponse(t *testing.T) {
Subscription: testRequestData{
Name: "ticker",
},
RequestID: wc.GenerateMessageID(false),
RequestID: 12345,
}
_, err = wc.SendMessageReturnResponse(t.Context(), request.Unset, req.RequestID, req)
@@ -758,37 +758,6 @@ func TestCanUseAuthenticatedWebsocketForWrapper(t *testing.T) {
assert.True(t, ws.CanUseAuthenticatedWebsocketForWrapper(), "CanUseAuthenticatedWebsocketForWrapper should return true")
}
func TestGenerateMessageID(t *testing.T) {
t.Parallel()
wc := connection{}
const spins = 1000
ids := make([]int64, spins)
for i := range spins {
id := wc.GenerateMessageID(true)
assert.NotContains(t, ids, id, "GenerateMessageID should not generate the same ID twice")
ids[i] = id
}
wc.requestIDGenerator = func() int64 { return 42 }
assert.EqualValues(t, 42, wc.GenerateMessageID(true), "GenerateMessageID should use bespokeGenerateMessageID")
}
// 7002502 166.7 ns/op 48 B/op 3 allocs/op
func BenchmarkGenerateMessageID_High(b *testing.B) {
wc := connection{}
for b.Loop() {
_ = wc.GenerateMessageID(true)
}
}
// 6536250 186.1 ns/op 48 B/op 3 allocs/op
func BenchmarkGenerateMessageID_Low(b *testing.B) {
wc := connection{}
for b.Loop() {
_ = wc.GenerateMessageID(false)
}
}
func TestCheckWebsocketURL(t *testing.T) {
err := checkWebsocketURL("")
assert.ErrorIs(t, err, errInvalidWebsocketURL, "checkWebsocketURL should error correctly on empty string")
@@ -1135,7 +1104,7 @@ func TestLatency(t *testing.T) {
Event: "subscribe",
Pairs: []string{currency.NewPairWithDelimiter("XBT", "USD", "/").String()},
Subscription: testRequestData{Name: "ticker"},
RequestID: wc.GenerateMessageID(false),
RequestID: 12346,
}
_, err = wc.SendMessageReturnResponse(t.Context(), request.Unset, req.RequestID, req)