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

rpc: implement full bi-directional communication #18471

Merged
merged 5 commits into from
Feb 4, 2019

Conversation

fjl
Copy link
Contributor

@fjl fjl commented Jan 17, 2019

New APIs added:

client.RegisterName(namespace, service) // makes service available to server
client.Notify(ctx, method, args...)     // sends a notification
ClientFromContext(ctx)                  // to get a client in handler method

This is essentially a rewrite of the server-side code. JSON-RPC
processing code is now the same on both server and client side. Many
minor issues were fixed in the process and there is a new test suite for
JSON-RPC spec compliance (and non-compliance in some cases).

List of behavior changes:

  • Method handlers are now called with a per-request context instead of a
    per-connection context. The context is canceled right after the method
    returns.
  • Subscription error channels are always closed when the connection
    ends. There is no need to also wait on the Notifier's Closed channel
    to detect whether the subscription has ended.
  • Client now omits "params" instead of sending "params": null when there
    are no arguments to a call. The previous behavior was not compliant
    with the spec. The server still accepts "params": null.
  • Floating point numbers are allowed as "id". The spec doesn't allow
    them, but we handle request "id" as json.RawMessage and guarantee that
    the same number will be sent back.
  • Logging is improved significantly. There is now a message at DEBUG
    level for each RPC call served.

Supersedes #18075 (this PR implements #18075 (comment))
Supersedes #18078 (suitableCallbacks is much simplifed)

Copy link
Contributor

@holiman holiman left a comment

Choose a reason for hiding this comment

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

Looks really good, nice work! I'm going to test it out a bit more

)

const (
jsonrpcVersion = "2.0"
vsn = "2.0"
Copy link
Contributor

Choose a reason for hiding this comment

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

Three letter abbreviation of version, really? Please use version or leave it as it was previously

Copy link
Contributor Author

@fjl fjl Jan 18, 2019

Choose a reason for hiding this comment

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

IMHO the constant name has to be shorter than "2.0", otherwise it's not
worth defining it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I will change it to version.

@fjl
Copy link
Contributor Author

fjl commented Jan 18, 2019

Looks like the last commit finallly fixed the websocket issue.

@holiman
Copy link
Contributor

holiman commented Jan 23, 2019

Finally support for pi in id

curl -X POST --data '{"jsonrpc":"2.0","method":"web3_clientVersion","params":[],"id":3.14159}' -H "Content-type:application/json" http://localhost:8545
{"jsonrpc":"2.0","id":3.14159,"result":"Geth/v1.8.22-unstable-eddff1b2/linux-amd64/go1.11.2"}

Also, regarding parse errors, this still seems to work:

> eth.getUncle("0xasdfasdfasdfasdfasdfasdfadsasdfasdfasdfasdfasdfadsfasdffasdfasdf")
Error: invalid argument 0: json: cannot unmarshal invalid hex string into Go value of type common.Hash

But basically, these broken json requests change (master below)

$ echo '{"jsonrpc":"2.0","method":"web3_clientVersion,"params":[],"id":3.14}' | nc  -U  ~/.ethereum/geth.ipc
{"jsonrpc":"2.0","error":{"code":-32600,"message":"invalid character 'p' after object key:value pair"}}
$ echo '{BAZONK "jsonrpc":"2.0","method":"web3_clientVersion,"params":[],"id":3.14}' | nc  -U  ~/.ethereum/geth.ipc
{"jsonrpc":"2.0","error":{"code":-32600,"message":"invalid character 'B' looking for beginning of object key string"}}

This PR:

$ echo '{"jsonrpc":"2.0","method":"web3_clientVersion,"params":[],"id":3.14}' | nc  -U  ~/.ethereum/geth.ipc
$ echo '{BAZONK "jsonrpc":"2.0","method":"web3_clientVersion,"params":[],"id":3.14}' | nc  -U  ~/.ethereum/geth.ipc

I'd reckon that most errors are not on the json format level, but rather on the layer above that, so I think that's ok, if not ideal.

@holiman
Copy link
Contributor

holiman commented Jan 23, 2019

This is odd. The first one is against master, the second against this PR:

[user@work go-ethereum]$ echo '{"jsonrpc":"2.0","method":"web3_clientVersion","params":[""],"id":3.14}' | nc  -U  ~/.ethereum/geth.ipc
{"jsonrpc":"2.0","id":3.14,"result":"Geth/v1.9.0-unstable-ae857e74/linux-amd64/go1.11.2"}
[user@work go-ethereum]$ echo '{"jsonrpc":"2.0","method":"web3_clientVersion","params":[""],"id":3.14}' | nc  -U  ~/.ethereum/geth.ipc

It doesn't allow [""]. But after removing that:

[user@work go-ethereum]$ echo '{"jsonrpc":"2.0","method":"web3_clientVersion","params":[],"id":3.14}' | nc  -U  ~/.ethereum/geth.ipc
[user@work go-ethereum]$ 

There's still no response?
Logs:

TRACE[01-23|15:01:20.151] Accepted RPC connection                  conn=@
DEBUG[01-23|15:01:20.151] RPC connection read error                err=EOF
DEBUG[01-23|15:01:20.151] Served web3_clientVersion                reqid=4.14 t=23.715µs

@holiman
Copy link
Contributor

holiman commented Jan 23, 2019

Ah, there's something racey here..

[user@work go-ethereum]$ echo -n '{"jsonrpc":"2.0","method":"web3_clientVersion","id":4.14}' | nc  -U  ~/.ethereum/geth.ipc
[user@work go-ethereum]$ echo -n '{"jsonrpc":"2.0","method":"web3_clientVersion","id":4.14}' | nc  -U  ~/.ethereum/geth.ipc
[user@work go-ethereum]$ echo -n '{"jsonrpc":"2.0","method":"web3_clientVersion","id":4.14}' | nc  -U  ~/.ethereum/geth.ipc
[user@work go-ethereum]$ echo -n '{"jsonrpc":"2.0","method":"web3_clientVersion","id":4.14}' | nc  -U  ~/.ethereum/geth.ipc
[user@work go-ethereum]$ echo -n '{"jsonrpc":"2.0","method":"web3_clientVersion","id":4.14}' | nc  -U  ~/.ethereum/geth.ipc
{"jsonrpc":"2.0","id":4.14,"result":"Geth/v1.8.22-unstable-eddff1b2/linux-amd64/go1.11.2"}

@holiman
Copy link
Contributor

holiman commented Jan 23, 2019

There appears to be two separate clients reading from the same source, and one of them reads the message, the other hits an EOF which triggers immediate shutdown when it send the error to the errc

@holiman
Copy link
Contributor

holiman commented Jan 23, 2019

Hm, the one from inproc comes from here: https://github.com/ethereum/go-ethereum/blob/master/cmd/geth/main.go#L303

@holiman
Copy link
Contributor

holiman commented Jan 23, 2019

Oddly, even if I remove that whole chunk of self-attach code there's still some race. The only real difference is that I don't notice it until later, when the request comes in (if I leave the chunk in, it becomes triggered earlier)

However, it appears that ServeCodec somehow spins up two cliens, or alternatively invokes the dispatcher twice, not quite sure.

@holiman
Copy link
Contributor

holiman commented Jan 23, 2019

Ok, I think I found it. In client.go, there's a loop that reads data.

func (c *Client) read(codec ServerCodec) {
	for {
		log.Info("client starting read", "client.isHTTP", c.isHTTP, "client", c)
		msgs, batch, err := codec.Read()
		log.Info("client read data", "msgs", len(msgs), "err", err)
		if err != nil {
			c.readErr <- err
			return
		}
		c.readOp <- readOp{msgs, batch}
		log.Info("client delivered data", "batch", batch)
	}
}

yields

INFO [01-23|20:44:17.823] client starting read                     client.isHTTP=false client="&{idgen:0x858ce0 isHTTP:false services:0xc004723980 idCounter:0 reconnectFunc:<nil> writeConn:0xc004bf3f90 close:0xc005c3e2a0 closing:0xc005c3e300 didClose:0xc005c3e360 reconnected:0xc005c3e3c0 readOp:0xc005c3e420 readErr:0xc005c3e480 reqInit:0xc005c3e4e0 reqSent:0xc005c4e600 reqTimeout:0xc005c3e540}"
INFO [01-23|20:44:17.828] client read data                         msgs=1 err=nil
INFO [01-23|20:44:17.829] client delivered data                    batch=false
INFO [01-23|20:44:17.829] client starting read                     client.isHTTP=false client="&{idgen:0x858ce0 isHTTP:false services:0xc004723980 idCounter:0 reconnectFunc:<nil> writeConn:0xc004bf3f90 close:0xc005c3e2a0 closing:0xc005c3e300 didClose:0xc005c3e360 reconnected:0xc005c3e3c0 readOp:0xc005c3e420 readErr:0xc005c3e480 reqInit:0xc005c3e4e0 reqSent:0xc005c4e600 reqTimeout:0xc005c3e540}"
INFO [01-23|20:44:17.829] client read data                         msgs=0 err=EOF

So the client first reads the json data, and delivers over c.readOp. Then it immediately reads again, but now the remote side is finished, and there's an EOF. The eof is passed via c.readErr <- err, which immediately closes the conn.

Not sure about the correct way to fix that...

@holiman
Copy link
Contributor

holiman commented Jan 23, 2019

This fixes the problem above, but the fix is not complete. It doesn't handle batch messages, and I'm not sure how it deals with subscriptions.

diff --git a/rpc/client.go b/rpc/client.go
index 6ca5b3c6c..9fc327a94 100644
--- a/rpc/client.go
+++ b/rpc/client.go
@@ -22,6 +22,7 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
+	"io"
 	"net/url"
 	"reflect"
 	"strconv"
@@ -533,10 +534,9 @@ func (c *Client) dispatch(codec ServerCodec) {
 		}
 		close(c.didClose)
 	}()
-
 	// Spawn the initial read loop.
 	go c.read(codec)
-
+	responseDoneCh := make(chan (bool), 1)
 	for {
 		select {
 		case <-c.close:
@@ -547,10 +547,16 @@ func (c *Client) dispatch(codec ServerCodec) {
 			if op.batch {
 				conn.handler.handleBatch(op.msgs)
 			} else {
-				conn.handler.handleMsg(op.msgs[0])
+				conn.handler.handleMsg(op.msgs[0], responseDoneCh)
 			}
 
 		case err := <-c.readErr:
+			if err == io.EOF {
+				// No more reading, but might not want to close it immediately, in case
+				// we are still processing and wants to write data.
+				<- responseDoneCh
+
+			}
 			conn.handler.log.Debug("RPC connection read error", "err", err)
 			conn.close(err, lastOp)
 			reading = false
diff --git a/rpc/handler.go b/rpc/handler.go
index bc03ef25f..44a6c2d73 100644
--- a/rpc/handler.go
+++ b/rpc/handler.go
@@ -131,7 +131,7 @@ func (h *handler) handleBatch(msgs []*jsonrpcMessage) {
 }
 
 // handleMsg handles a single message.
-func (h *handler) handleMsg(msg *jsonrpcMessage) {
+func (h *handler) handleMsg(msg *jsonrpcMessage, done chan(bool)) {
 	if ok := h.handleImmediate(msg); ok {
 		return
 	}
@@ -144,6 +144,9 @@ func (h *handler) handleMsg(msg *jsonrpcMessage) {
 		for _, n := range cp.notifiers {
 			n.activate()
 		}
+		if done != nil{
+			done <- true
+		}
 	})
 }
 
diff --git a/rpc/server.go b/rpc/server.go
index 5a92847f2..75b564743 100644
--- a/rpc/server.go
+++ b/rpc/server.go
@@ -111,7 +111,7 @@ func (s *Server) serveSingleRequest(ctx context.Context, codec ServerCodec) {
 	if batch {
 		h.handleBatch(reqs)
 	} else {
-		h.handleMsg(reqs[0])
+		h.handleMsg(reqs[0], nil)
 	}
 }
 

@fjl
Copy link
Contributor Author

fjl commented Jan 24, 2019

Thank you for the investigation! I'll fix it.

@fjl
Copy link
Contributor Author

fjl commented Jan 24, 2019

Fix committed. The issue with nc is that it's pedantic enough to half-close the connection after writing the request. This triggers the read error. handler already waits for responses on close because of the wait group. The fix was just to close the handler before closing the connection. This might have adverse effects for slow connections because it will block up the client until the deadline arrives.

@fjl
Copy link
Contributor Author

fjl commented Jan 24, 2019

@holiman please try your example again, somehow it doesn't work for me. I said it's fixed because the unit test I added passes now but maybe it's different with unix sockets and real-life nc.

@fjl
Copy link
Contributor Author

fjl commented Jan 24, 2019

I'm still thinking about adding the parse errors back in.

@holiman
Copy link
Contributor

holiman commented Jan 25, 2019

Yup, the fix works for me. If you don't add parse errors back in, I'd suggest to instead bump the error message from DEBUG to INFO.

DEBUG[01-25|10:03:21.254] RPC connection read error                err="invalid character 'x' looking for beginning of value"

Otherwise, people will just have a hard time debugging their cross-platform woes, and they'll bug us about it.

Copy link
Contributor

@holiman holiman left a comment

Choose a reason for hiding this comment

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

Looks good to me, and my works fine in my (non-exhaustive) testing
We should not merge this until after the Constantinople release is out, and it needs to live for a whle on master before we put it into a release.

fjl added 5 commits February 4, 2019 12:38
New APIs added:

    client.RegisterName(namespace, service) // makes service available to server
    client.Notify(ctx, method, args...)     // sends a notification
    ClientFromContext(ctx)                  // to get a client in handler method

This is essentially a rewrite of the server-side code. JSON-RPC
processing code is now the same on both server and client side. Many
minor issues were fixed in the process and there is a new test suite for
JSON-RPC spec compliance (and non-compliance in some cases).

List of behavior changes:

- Method handlers are now called with a per-request context instead of a
  per-connection context. The context is canceled right after the method
  returns.
- Subscription error channels are always closed when the connection
  ends. There is no need to also wait on the Notifier's Closed channel
  to detect whether the subscription has ended.
- Parse errors just close the connection instead of reporting an error
  back to the client, except on HTTP connections. I'm not happy about
  this change, but it was non-trivial to implement this within the
  concurrency model used in Client.
- Client now omits "params" instead of sending "params": null when there
  are no arguments to a call. The previous behavior was not compliant
  with the spec. The server still accepts "params": null.
- Floating point numbers are allowed as "id". The spec doesn't allow
  them, but we handle request "id" as json.RawMessage and guarantee that
  the same number will be sent back.
- Logging is improved significantly. There is now a message at DEBUG
  level for each RPC call served.
Read operations could be dropped after reconnect because the new reader
goroutine would be started before the previous one exited. Start it later.

For short-lived connections performing a single call, the result would
sometimes be dropped because we closed the connection before waiting for
handler shutdown. Reverse shutdown order to fix this problem.
@fjl
Copy link
Contributor Author

fjl commented Feb 4, 2019

rebased and parse errors are back

@fjl fjl merged commit 245f314 into ethereum:master Feb 4, 2019
@shazow
Copy link
Contributor

shazow commented May 14, 2019

In case it's interesting to anyone involved in this, I also wrote a bidirectional jsonrpc2 implementation in Vipnode last August:

Let me know if there's desire to collaborate in the future.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants