Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

stream: shift websocket reader and dialer code from wrapper to stream package #1493

Closed
wants to merge 12 commits into from

Conversation

shazbert
Copy link
Collaborator

@shazbert shazbert commented Mar 7, 2024

PR Description

Opened as draft for initial inspection and critique. This attempts to reduce code duplication for websocket wrappers and gets it another step forward for multi-connection handling.

  • Define websocket processor handlers per individual connection - This will eventually be bespoke connections only handling specific assets and or constrained subs.
  • Define bootstrap function for bespoke connection startup code per individual connection
  • Shift connection dialing and session handling away from wrappers
  • Shift websocket reader routines generation to stream package
  • Remove proxy field websocket connection as its handled 1 proxy per session anyway
  • Adds basic (non-tls) mock server and mock proxy for websocket testing
  • Remove superfluous running URL fields as connections can differ
  • Adds new currency error ErrCurrencyPairRequired
  • Split SetWebsocketURL into two functions SetWebsocketURL && SetWebsocketAuthURL
  • Split GetWebsocketURL into two functions GetWebsocketURL && GetWebsocketAuthURL
  • Removes unused websocket field DefaultURL
  • Removes unexported methods setState, setEnabled, setDataMonitorRunning, dataMonitorRunning, checkAndSetMonitorRunning, setTrafficMonitorRunning, setConnectionMonitorRunning and use the field methods directly.
  • Defered reader.Close in parseBinaryResponse() so as to make sure if io.ReadAll fails it will still close.

Websocket_Connection

  • Remove setConnectedStatus and IsConnected methods in favour of field methods.

Fixes # (issue)

Type of change

Please delete options that are not relevant and add an x in [] as item is complete.

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update

How has this been tested

Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration and
also consider improving test coverage whilst working on a certain feature or package.

  • go test ./... -race
  • golangci-lint run
  • Test X

Checklist

  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation and regenerated documentation via the documentation tool
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally and on Github Actions with my changes
  • Any dependent changes have been merged and published in downstream modules

@shazbert shazbert added help wanted review me This pull request is ready for review labels Mar 7, 2024
@shazbert shazbert requested a review from gloriousCode March 7, 2024 04:31
@shazbert shazbert self-assigned this Mar 7, 2024
Copy link
Collaborator

@gloriousCode gloriousCode left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the concepts. Will need some more work, but defining functions at the Connection Setup level is nice. The removal of needing duplicate ReadMessage functions is nice

Handler: ok.WsHandleData,
Bootstrap: ok.WsUnAuthBootstrap,
ReadBufferSize: 8192,
WriteBufferSize: 8192,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a comment consideration for subscription limit being passed in. Would something overlooking the subscriptions handle subscription count? Or can a connection itself understand that you're trying to oversubscribe?
I understand that the next step is about subscriptions, so I understand it won't be addressed here, but putting out the consideration

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah that is something that I want to think about implementing after shifting subscription handling over to individual connections. So we can hit a max ceiling and start generating new connections to handle everything.

Comment on lines 318 to 320
if w.GenerateSubs == nil {
return errors.New("generate subscriptions function not set")
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be much sooner, otherwise everything gets up and running, but it still errors out

// InitialiseConnection sets up a websocket connection
func (w *Websocket) InitialiseConnection(conn Connection, bootstrap func(Connection) error, handler func([]byte) error) error {
dialer := *websocket.DefaultDialer
if w.proxyAddr != nil {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With proxyAddr now being a direct url, and that SetProxy can be called at any time via RPC, I think this will require mutexs

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I un exported it and only call it in Websocket Connect method which is locked down.

Comment on lines 280 to 289
if w.connector != nil {
err := w.connector()
if err != nil {
w.setState(disconnected)
return fmt.Errorf("%v Error connecting %w", w.exchangeName, err)
}
}

if w.Conn != nil {
err := w.InitialiseConnection(w.Conn, w.UnAuthBootstrap, w.UnAuthHandler)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You will definitely need to expand on these details for differentiating these two ways of setting up connections. At present it is quite confusion which one is what and why. No using "new" 😄
Docs/commentary required about the changes youre introducing

}
}

if w.Conn != nil {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This setup doesn't work. Non-new websockets will panic here as w.Conn is defined, but the functions below are not

panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x2 addr=0x0 pc=0x104adf5a8]

goroutine 120 [running]:
github.com/thrasher-corp/gocryptotrader/exchanges/stream.(*Websocket).listen(0x140001ae3c0, {0x1058830d8, 0x140008e0100}, 0x0)
        /Users/scottg/go/src/github.com/thrasher-corp/gocryptotrader/exchanges/stream/websocket.go:992 +0x78
created by github.com/thrasher-corp/gocryptotrader/exchanges/stream.(*Websocket).InitialiseConnection in goroutine 146
        /Users/scottg/go/src/github.com/thrasher-corp/gocryptotrader/exchanges/stream/websocket.go:1022 +0x1a8

This is for Kraken, it shouldn'tbe hitting this new code for Kraken, right? No errors are thrown from nil functions in InistialiseConnection

// Authenticated stream connection
AuthConn Connection
AuthConn Connection
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Down the line, I would love for AuthConn to be removed. An auth connection is just another connection. At present, any connection issues for unauth/auth affect the other and it doesn't make much sense for the direction we're heading with multiple websocket connection types

Copy link
Collaborator

@gbjk gbjk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work 🎉
Subjective comments mostly

I think it moves us in the right direction, definitely.
I'm mindful of what our overall websockets structure will look like, and the current mixing of naming stream, websocket and connection, but I think this improves, generally.

return w.Shutdown()
}
return nil
}

// GetWebsocketURL returns the running websocket URL
func (w *Websocket) GetWebsocketURL() string {
return w.runningURL
func (w *Websocket) GetWebsocketURL(auth bool) string {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two issues with this:

  • I don't think we should go further down the "websocket is a websocket but maybe actually 2 and one of them is auth" route.
  • I don't think there's any cleaner API here in passing a boolean than having 2 methods

So in both cases I vote for GetWebsocketAuthURL() which has convenience handling for AuthConn is nil.

@@ -677,76 +680,70 @@ func (w *Websocket) CanUseAuthenticatedWebsocketForWrapper() bool {

// SetWebsocketURL sets websocket URL and can refresh underlying connections
func (w *Websocket) SetWebsocketURL(url string, auth, reconnect bool) error {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Back to front-review-comments 🤦
See my comment below on GetWebsocketURL.
I actually vote for 2 methods on this too, even though it changes the API.

Trying to think about how this pattern would look like for a router, but I'm about a week away from putting that up on the drawing board. Still mulling it over.

func (w *Websocket) IsInitialised() bool {
return w.state.Load() != uninitialised
}
func (w *Websocket) IsInitialised() bool { return w.state.Load() != uninitialised }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a note on all of these one-linifications:
I've often considered them non-idiomatic, since I've never seen them in the core lib or written by any of the trend setters.
I also find them difficult to parse by scanning, because the content indentation varies if it has a comment, which public funcs need to have.

So I guess I'm saying there should be a space after 644 to make checkAndSetMonitorRunning body parsable, and that 649-677 should be multi-liners.

This is subjective. But if you were on the fence or debating it, that's my taste on it.

Copy link
Collaborator

@gloriousCode gloriousCode Mar 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't enjoy them either, if that helps sway you Shazbert 😄 Doesn't help readability, doesn't really save much space. If any changes to the func occur, it has to change anyway

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@@ -1688,7 +1688,7 @@ func (b *Bitfinex) CancelMultipleOrdersV2(ctx context.Context, orderID, clientOr
cancelledOrder.AuxLimitPrice = f
}
}
cancelledOrders[y] = cancelledOrder
cancelledOrders = append(cancelledOrders, cancelledOrder)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this fix something?
In any case should be in it's own commit, and if it's a fix PR'd separately.
If it's fixing something, should have a test to prove it was broken before too 😄

@@ -208,7 +208,7 @@ func TestSetClientProxyAddress(t *testing.T) {
t.Error("SetClientProxyAddress parsed invalid URL")
}

if newBase.Websocket.GetProxyAddress() != "" {
if newBase.Websocket.GetProxyAddress().String() != "" {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Think that this would work with assert.NotEmpty
And we should be explicit about why we're expecting this, e.g. "SetClientProxyAddress should not set ProxyAddress on error"

exchanges/stream/websocket.go Outdated Show resolved Hide resolved
Comment on lines 113 to 176
mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
upgrader := websocket.Upgrader{}
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Fatalf("Failed to upgrade connection to WebSocket: %v", err)
}
go func() { // Echo the message back to the client for future use.
defer conn.Close()
_, msg, err := conn.ReadMessage()
if err != nil {
log.Fatalf("Failed to read message from WebSocket connection: %v", err)
}
err = conn.WriteMessage(websocket.TextMessage, msg)
if err != nil {
log.Fatalf("Failed to write message to WebSocket connection: %v", err)
}
}()
}))
mockServerURL += strings.Split(mockServer.URL, "//")[1]

mockProxy = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodConnect {
w.WriteHeader(http.StatusOK)
return
}

upgrader := websocket.Upgrader{}
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
http.Error(w, "Failed to upgrade connection to WebSocket", http.StatusInternalServerError)
return
}
actualServerConn, _, err := websocket.DefaultDialer.Dial("ws://"+r.Host, nil)
if err != nil {
err = conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseInternalServerErr, "Failed to dial actual server from proxy"))
log.Printf("Failed to dial actual server from proxy: %v", err)
conn.Close()
return
}
go func() {
defer conn.Close()
for {
messageType, message, err := conn.ReadMessage()
if err != nil {
return
}
if err := actualServerConn.WriteMessage(messageType, message); err != nil {
return
}
}
}()
go func() {
defer actualServerConn.Close()
for {
messageType, message, err := actualServerConn.ReadMessage()
if err != nil {
return
}
if err := conn.WriteMessage(messageType, message); err != nil {
return
}
}
}()
}))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be a func. Or 2.

ExchangeName: "test3",
Verbose: true,
URL: websocketTestURL,
// ProxyURL: proxyURL,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

☝️ Feels like maybe these were leaked, so just highlighting.

@@ -699,7 +789,7 @@ func TestSendMessage(t *testing.T) {
testData := &testCases[i]
t.Run(testData.WC.ExchangeName, func(t *testing.T) {
t.Parallel()
if testData.WC.ProxyURL != "" && !useProxyTests {
if /*testData.WC.ProxyURL != "" &&*/ !useProxyTests {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

☝️ Feels like maybe these were leaked, so just highlighting.

ExchangeConfig *config.Exchange
DefaultURL string
ExchangeConfig *config.Exchange
// DefaultURL string
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

☝️ Feels like maybe these were leaked, so just highlighting.

Copy link

codecov bot commented Mar 13, 2024

Codecov Report

Attention: Patch coverage is 61.06557% with 95 lines in your changes are missing coverage. Please review.

Project coverage is 37.78%. Comparing base (d679a76) to head (5af9fea).
Report is 3 commits behind head on master.

❗ Current head 5af9fea differs from pull request most recent head 8e9bf16. Consider uploading reports for the commit 8e9bf16 to get more accurate results

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##           master    #1493      +/-   ##
==========================================
+ Coverage   35.89%   37.78%   +1.88%     
==========================================
  Files         411      411              
  Lines      177595   147708   -29887     
==========================================
- Hits        63752    55815    -7937     
+ Misses     106058    84092   -21966     
- Partials     7785     7801      +16     
Files Coverage Δ
currency/code.go 89.17% <ø> (+1.12%) ⬆️
exchanges/binance/binance_wrapper.go 41.09% <100.00%> (+2.87%) ⬆️
exchanges/binance/type_convert.go 60.75% <ø> (+7.15%) ⬆️
exchanges/binanceus/binanceus_wrapper.go 43.88% <100.00%> (+3.80%) ⬆️
exchanges/bitfinex/bitfinex_wrapper.go 40.54% <100.00%> (+3.97%) ⬆️
exchanges/bithumb/bithumb.go 41.42% <100.00%> (+2.14%) ⬆️
exchanges/bithumb/bithumb_wrapper.go 40.86% <100.00%> (+3.18%) ⬆️
exchanges/bitmex/bitmex_wrapper.go 47.60% <100.00%> (+2.39%) ⬆️
exchanges/bitstamp/bitstamp_wrapper.go 57.97% <100.00%> (+5.79%) ⬆️
exchanges/btcmarkets/btcmarkets_wrapper.go 33.41% <100.00%> (+2.87%) ⬆️
... and 26 more

... and 351 files with indirect coverage changes

w.setConnectedStatus(false)
return w.Connection.UnderlyingConn().Close()
w.connected.Store(false)
return w.Connection.NetConn().Close()
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UnderlyingConn was deprecated so its now NetConn()

@shazbert shazbert marked this pull request as ready for review March 13, 2024 03:52
@shazbert shazbert requested a review from gloriousCode April 30, 2024 22:01
@shazbert shazbert added blocked and removed review me This pull request is ready for review labels Jul 2, 2024
@shazbert
Copy link
Collaborator Author

Closing in favour of #1580.

@shazbert shazbert closed this Jul 16, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants