Skip to content

Commit

Permalink
fix: add support for a ws client & batch processing over ws (gnolang#…
Browse files Browse the repository at this point in the history
…1498)

## Description

Let me start this PR description by explaining _what_ I wanted to
accomplish, so you are not discouraged when reading the file changes.

I wanted to:
- create a WS client from outside the repository (just like we can
create http clients)
- have the WS client support batch requests
- have the TM2 JSON-RPC server support batch requests / responses over
WS
- **not** have to rewrite core client / server logic and APIs

It might seem odd, reading the 3rd point and thinking this is not
supported. The truth is actually much more troubling.

Our JSON-RPC server (and client!) implementations are not great. HTTP
requests are handled and parsed in a completely different flow than WS
requests, even though the output of both should be identical (just over
different mediums). Lots of logic is coupled, making it hard to extract
and simplify. I've broken the WS / HTTP implementation multiple times
over the course of this PR, and all of the tests were _passing_, even
though I've critically broken the module at the time.
The client code for the JSON-RPC server requires a _response type_ (of
course, for Amino result parsing) to be given at the moment of calling,
which is not amazing considering our WS implementation is async, and
doesn't have these response types in the result parsing context (these
logic flows are handled by different threads).

What I ended up doing:
- added support for a WS client
- this was a bigger effort than expected; I extracted and simplified the
batching logic, but was still blocked by the lack of batch support in WS
request handling
- added batch support for the TM2 JSON-RPC server
- I basically mirrored the HTTP batch request handling (hint that these
should be identical flows)
- **BREAKING: completely overhauled our JSON-RPC client libraries and
actual instances (http / ws)**, for a couple of reasons:
- I tried to add support for all previously mentioned items, but it was
impossible with the architecture that was in place (`baseRPCClient`).
The slightly tweaked API (for creating HTTP / WS clients, and using
batches) is much simpler to use, and actually has error handling.
- We didn't have nearly enough coverage and good tests for the
functionality -- now we have a suite of E2E and unit tests that give
confidence.

I will start an effort in the near future for refactoring the JSON-RPC
server code from the ground up in a subsequent PR, this time with specs
and tests at the forefront.

### How to test out the websockets

To test out the WS responses, you can use a tool like
[websocat](https://github.com/vi/websocat).

1. start a local chain
2. run `websocat ws://127.0.0.1:26657/websocket` (this is the default
URL)
3. send a batch request:
```shell
[ { "id": 1, "jsonrpc": "2.0", "method": "status", "params": [] }, { "id": 2, "jsonrpc": "2.0", "method": "status", "params": [] } ]
```

### How to test out the updated client code

I created the following snippet for easily testing the functionality
updated in this PR:
- single HTTP / WS requests
- batch HTTP / WS requests

```go
package main

import (
	"context"
	"fmt"

	"github.com/gnolang/gno/tm2/pkg/bft/rpc/client"
	ctypes "github.com/gnolang/gno/tm2/pkg/bft/rpc/core/types"
)

func main() {
	// HTTP //
	fmt.Printf("\n--- HTTP CLIENT ---\n")

	// Open HTTP connection
	httpClient, err := client.NewHTTPClient("http://127.0.0.1:26657")
	if err != nil {
		panic("unable to start HTTP client")
	}

	// Get a single status
	status, err := httpClient.Status()
	if err != nil {
		fmt.Println("Unable to send single status (HTTP)!")

		panic(err)
	}

	fmt.Printf("\n\nHTTP - Single status: %v\n\n", status)

	// Get a batch of statuses
	httpBatch := httpClient.NewBatch()

	// Add 10 status requests
	for i := 0; i < 10; i++ {
		if err := httpBatch.Status(); err != nil {
			fmt.Println("Unable to add status request to HTTP batch!")

			panic(err)
		}
	}

	// Send the batch
	results, err := httpBatch.Send(context.Background())
	if err != nil {
		fmt.Println("Unable to send HTTP batch!")

		panic(err)
	}

	for index, resultRaw := range results {
		result, ok := resultRaw.(*ctypes.ResultStatus)
		if !ok {
			panic("Invalid status type in batch response!")
		}

		fmt.Printf("\nStatus %d from batch: %v\n", index, result)
	}

	// WS //
	fmt.Printf("\n--- WS CLIENT ---\n")

	// Open WS connection
	wsClient, err := client.NewWSClient("ws://127.0.0.1:26657/websocket")
	if err != nil {
		panic("unable to start WS client")
	}

	// Get a single status
	status, err = wsClient.Status()
	if err != nil {
		fmt.Println("Unable to send single status (WS)!")

		panic(err)
	}

	fmt.Printf("\n\nWS - Single status: %v\n\n", status)

	// Get a batch of statuses
	wsBatch := wsClient.NewBatch()

	// Add 10 status requests
	for i := 0; i < 10; i++ {
		if err := wsBatch.Status(); err != nil {
			fmt.Println("Unable to add status request to WS batch!")

			panic(err)
		}
	}

	// Send the batch
	results, err = wsBatch.Send(context.Background())
	if err != nil {
		fmt.Println("Unable to send WS batch!")

		panic(err)
	}

	for index, resultRaw := range results {
		result, ok := resultRaw.(*ctypes.ResultStatus)
		if !ok {
			panic("Invalid status type in batch response!")
		}

		fmt.Printf("\nStatus %d from batch: %v\n", index, result)
	}

	if err := wsClient.Close(); err != nil {
		fmt.Println("Unable to gracefully close WS client!")

		panic(err)
	}

	fmt.Printf("\n\nGreat success!\n\n")
}
```

cc @dongwon8247 

<details><summary>Contributors' checklist...</summary>

- [x] Added new tests, or not needed, or not feasible
- [x] Provided an example (e.g. screenshot) to aid review or the PR is
self-explanatory
- [x] Updated the official documentation or not needed
- [x] No breaking changes were made, or a `BREAKING CHANGE: xxx` message
was included in the description
- [ ] Added references to related issues and PRs
- [ ] Provided any useful hints for running manual tests
- [ ] Added new benchmarks to [generated
graphs](https://gnoland.github.io/benchmarks), if any. More info
[here](https://github.com/gnolang/gno/blob/master/.benchmarks/README.md).
</details>
  • Loading branch information
zivkovicmilos committed Apr 26, 2024
1 parent fdde3d0 commit 15ad779
Show file tree
Hide file tree
Showing 64 changed files with 4,460 additions and 4,160 deletions.
6 changes: 3 additions & 3 deletions .github/workflows/docker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ jobs:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Build and push
uses: docker/build-push-action@v3
with:
Expand All @@ -49,7 +49,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
target: [gnoland-slim, gnokey-slim, gno-slim, gnofaucet-slim, gnoweb-slim]
target: [ gnoland-slim, gnokey-slim, gno-slim, gnofaucet-slim, gnoweb-slim ]
steps:
- name: Checkout
uses: actions/checkout@v4
Expand All @@ -71,7 +71,7 @@ jobs:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Build and push
uses: docker/build-push-action@v3
with:
Expand Down
1 change: 1 addition & 0 deletions contribs/gnodev/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ require (
github.com/rivo/uniseg v0.4.3 // indirect
github.com/rogpeppe/go-internal v1.12.0 // indirect
github.com/rs/cors v1.10.1 // indirect
github.com/rs/xid v1.5.0 // indirect
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 // indirect
github.com/zondax/hid v0.9.2 // indirect
github.com/zondax/ledger-go v0.14.3 // indirect
Expand Down
2 changes: 2 additions & 0 deletions contribs/gnodev/go.sum

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions contribs/gnokeykc/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ require (
github.com/peterbourgon/ff/v3 v3.4.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rs/xid v1.5.0 // indirect
github.com/stretchr/testify v1.9.0 // indirect
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 // indirect
github.com/zondax/hid v0.9.2 // indirect
Expand Down
4 changes: 2 additions & 2 deletions contribs/gnokeykc/go.sum

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions docs/how-to-guides/connecting-from-go.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ A few things to note:
You can initialize the RPC Client used to connect to the Gno.land network with
the following line:
```go
rpc := rpcclient.NewHTTP("<gno_chain_endpoint>", "")
rpc := rpcclient.NewHTTP("<gno_chain_endpoint>")
```

A list of Gno.land network endpoints & chain IDs can be found in the [Gno RPC
Expand Down Expand Up @@ -138,7 +138,7 @@ func main() {
}

// Initialize the RPC client
rpc := rpcclient.NewHTTP("<gno.land_remote_endpoint>", "")
rpc := rpcclient.NewHTTP("<gno.land_remote_endpoint>")

// Initialize the gnoclient
client := gnoclient.Client{
Expand Down
6 changes: 3 additions & 3 deletions gno.land/pkg/gnoclient/example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ func Example_withDisk() {
}

remote := "127.0.0.1:26657"
rpcClient := rpcclient.NewHTTP(remote, "/websocket")
rpcClient, _ := rpcclient.NewHTTPClient(remote)

client := gnoclient.Client{
Signer: signer,
Expand All @@ -35,7 +35,7 @@ func Example_withInMemCrypto() {
signer, _ := gnoclient.SignerFromBip39(mnemo, chainID, bip39Passphrase, account, index)

remote := "127.0.0.1:26657"
rpcClient := rpcclient.NewHTTP(remote, "/websocket")
rpcClient, _ := rpcclient.NewHTTPClient(remote)

client := gnoclient.Client{
Signer: signer,
Expand All @@ -47,7 +47,7 @@ func Example_withInMemCrypto() {
// Example_readOnly demonstrates how to initialize a read-only gnoclient, which can only query.
func Example_readOnly() {
remote := "127.0.0.1:26657"
rpcClient := rpcclient.NewHTTP(remote, "/websocket")
rpcClient, _ := rpcclient.NewHTTPClient(remote)

client := gnoclient.Client{
RPCClient: rpcClient,
Expand Down
28 changes: 18 additions & 10 deletions gno.land/pkg/gnoclient/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ func TestCallSingle_Integration(t *testing.T) {

// Init Signer & RPCClient
signer := newInMemorySigner(t, "tendermint_test")
rpcClient := rpcclient.NewHTTP(remoteAddr, "/websocket")
rpcClient, err := rpcclient.NewHTTPClient(remoteAddr)
require.NoError(t, err)

// Setup Client
client := Client{
Expand Down Expand Up @@ -68,7 +69,8 @@ func TestCallMultiple_Integration(t *testing.T) {

// Init Signer & RPCClient
signer := newInMemorySigner(t, "tendermint_test")
rpcClient := rpcclient.NewHTTP(remoteAddr, "/websocket")
rpcClient, err := rpcclient.NewHTTPClient(remoteAddr)
require.NoError(t, err)

// Setup Client
client := Client{
Expand Down Expand Up @@ -119,7 +121,8 @@ func TestSendSingle_Integration(t *testing.T) {

// Init Signer & RPCClient
signer := newInMemorySigner(t, "tendermint_test")
rpcClient := rpcclient.NewHTTP(remoteAddr, "/websocket")
rpcClient, err := rpcclient.NewHTTPClient(remoteAddr)
require.NoError(t, err)

// Setup Client
client := Client{
Expand Down Expand Up @@ -167,7 +170,8 @@ func TestSendMultiple_Integration(t *testing.T) {

// Init Signer & RPCClient
signer := newInMemorySigner(t, "tendermint_test")
rpcClient := rpcclient.NewHTTP(remoteAddr, "/websocket")
rpcClient, err := rpcclient.NewHTTPClient(remoteAddr)
require.NoError(t, err)

// Setup Client
client := Client{
Expand Down Expand Up @@ -223,7 +227,8 @@ func TestRunSingle_Integration(t *testing.T) {

// Init Signer & RPCClient
signer := newInMemorySigner(t, "tendermint_test")
rpcClient := rpcclient.NewHTTP(remoteAddr, "/websocket")
rpcClient, err := rpcclient.NewHTTPClient(remoteAddr)
require.NoError(t, err)

client := Client{
Signer: signer,
Expand Down Expand Up @@ -281,7 +286,8 @@ func TestRunMultiple_Integration(t *testing.T) {

// Init Signer & RPCClient
signer := newInMemorySigner(t, "tendermint_test")
rpcClient := rpcclient.NewHTTP(remoteAddr, "/websocket")
rpcClient, err := rpcclient.NewHTTPClient(remoteAddr)
require.NoError(t, err)

client := Client{
Signer: signer,
Expand Down Expand Up @@ -361,7 +367,8 @@ func TestAddPackageSingle_Integration(t *testing.T) {

// Init Signer & RPCClient
signer := newInMemorySigner(t, "tendermint_test")
rpcClient := rpcclient.NewHTTP(remoteAddr, "/websocket")
rpcClient, err := rpcclient.NewHTTPClient(remoteAddr)
require.NoError(t, err)

// Setup Client
client := Client{
Expand Down Expand Up @@ -404,7 +411,7 @@ func Echo(str string) string {
}

// Execute AddPackage
_, err := client.AddPackage(baseCfg, msg)
_, err = client.AddPackage(baseCfg, msg)
assert.Nil(t, err)

// Check for deployed file on the node
Expand All @@ -429,7 +436,8 @@ func TestAddPackageMultiple_Integration(t *testing.T) {

// Init Signer & RPCClient
signer := newInMemorySigner(t, "tendermint_test")
rpcClient := rpcclient.NewHTTP(remoteAddr, "/websocket")
rpcClient, err := rpcclient.NewHTTPClient(remoteAddr)
require.NoError(t, err)

// Setup Client
client := Client{
Expand Down Expand Up @@ -495,7 +503,7 @@ func Hello(str string) string {
}

// Execute AddPackage
_, err := client.AddPackage(baseCfg, msg1, msg2)
_, err = client.AddPackage(baseCfg, msg1, msg2)
assert.Nil(t, err)

// Check Package #1
Expand Down
6 changes: 5 additions & 1 deletion gno.land/pkg/gnoweb/gnoweb.go
Original file line number Diff line number Diff line change
Expand Up @@ -421,7 +421,11 @@ func makeRequest(log *slog.Logger, cfg *Config, qpath string, data []byte) (res
// Prove: false, XXX
}
remote := cfg.RemoteAddr
cli := client.NewHTTP(remote, "/websocket")
cli, err := client.NewHTTPClient(remote)
if err != nil {
return nil, fmt.Errorf("unable to create HTTP client, %w", err)
}

qres, err := cli.ABCIQueryWithOptions(
qpath, data, opts2)
if err != nil {
Expand Down
6 changes: 5 additions & 1 deletion gnovm/pkg/gnomod/fetch.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@ func queryChain(remote string, qpath string, data []byte) (res *abci.ResponseQue
// Height: height, XXX
// Prove: false, XXX
}
cli := client.NewHTTP(remote, "/websocket")
cli, err := client.NewHTTPClient(remote)
if err != nil {
return nil, err
}

qres, err := cli.ABCIQueryWithOptions(qpath, data, opts2)
if err != nil {
return nil, err
Expand Down
16 changes: 7 additions & 9 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ go 1.21

require (
dario.cat/mergo v1.0.0
github.com/btcsuite/btcd/btcec/v2 v2.3.3
github.com/btcsuite/btcd/btcutil v1.1.5
github.com/cockroachdb/apd/v3 v3.2.1
github.com/cosmos/ledger-cosmos-go v0.13.3
Expand All @@ -26,6 +27,7 @@ require (
github.com/pmezard/go-difflib v1.0.0
github.com/rogpeppe/go-internal v1.12.0
github.com/rs/cors v1.10.1
github.com/rs/xid v1.5.0
github.com/stretchr/testify v1.9.0
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7
go.etcd.io/bbolt v1.3.9
Expand All @@ -49,29 +51,25 @@ require (

require (
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/gdamore/encoding v1.0.0 // indirect
github.com/go-logr/logr v1.4.1 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 // indirect
go.opentelemetry.io/otel/trace v1.25.0 // indirect
go.opentelemetry.io/proto/otlp v1.1.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240227224415-6ceb2ff114de // indirect
)

require (
github.com/btcsuite/btcd/btcec/v2 v2.3.3
github.com/gdamore/encoding v1.0.0 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/gorilla/securecookie v1.1.1 // indirect
github.com/gorilla/sessions v1.2.1 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/nxadm/tail v1.4.11 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/rivo/uniseg v0.4.3 // indirect
github.com/zondax/hid v0.9.2 // indirect
github.com/zondax/ledger-go v0.14.3 // indirect
go.opentelemetry.io/otel/trace v1.25.0 // indirect
go.opentelemetry.io/proto/otlp v1.1.0 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/sys v0.18.0 // indirect
golang.org/x/text v0.14.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240227224415-6ceb2ff114de // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda // indirect
google.golang.org/grpc v1.63.0 // indirect
)
2 changes: 2 additions & 0 deletions go.sum

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 15ad779

Please sign in to comment.