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

rpcserver: add batched request support (json 2.0) #841

Merged
merged 1 commit into from
Feb 13, 2018

Conversation

dnldd
Copy link
Member

@dnldd dnldd commented Sep 8, 2017

No description provided.

@dnldd
Copy link
Member Author

dnldd commented Sep 10, 2017

@davecgh PR should be good now.

@chappjc
Copy link
Member

chappjc commented Sep 13, 2017

Does this apply only to HTTP requests or also to websockets?

@dnldd
Copy link
Member Author

dnldd commented Sep 13, 2017

@chappjc applies to websockets as well.

rpcserver.go Outdated
jsonErr = rpcInvalidError("limited user not " +
"authorized for this method")
// determine if the request is single or batched
reqType := reflect.TypeOf(request)
Copy link
Member

Choose a reason for hiding this comment

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

instead of reflect, use switch/case:

switch request.(type) {
case map[string]interface{}:
    ...
}

@dnldd
Copy link
Member Author

dnldd commented Sep 13, 2017

@dajohi updated the PR per your review.

rpcserver.go Outdated
// determine if the request is single or batched
switch request.(type) {
case map[string]interface{}:
{
Copy link
Member

Choose a reason for hiding this comment

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

can you drop the scope and save on indentation?

Copy link
Member Author

Choose a reason for hiding this comment

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

sure thing, on it.

@dnldd
Copy link
Member Author

dnldd commented Sep 21, 2017

@dajohi PR updated.

rpcserver.go Outdated
switch request.(type) {
case map[string]interface{}:
request := request.(map[string]interface{})
req, err := dcrjson.NewRequest(request["id"].(interface{}), request["method"].(string), request["params"].([]interface{}))
Copy link
Member

Choose a reason for hiding this comment

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

please wrap at 80 columns

rpcserver.go Outdated
batchedReply.WriteByte('[')
for idx, entry := range reqs {
entry := entry.(map[string]interface{})
req, err := dcrjson.NewRequest(entry["id"].(interface{}), entry["method"].(string), entry["params"].([]interface{}))
Copy link
Member

Choose a reason for hiding this comment

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

please wrap at 80 columns

rpcwebsocket.go Outdated
switch request.(type) {
case map[string]interface{}:
data := request.(map[string]interface{})
req, err := dcrjson.NewRequest(data["id"].(interface{}), data["method"].(string), data["params"].([]interface{}))
Copy link
Member

Choose a reason for hiding this comment

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

please wrap at 80 columns

rpcwebsocket.go Outdated
var replies [][]byte
for _, entry := range batch {
entry := entry.(map[string]interface{})
req, err := dcrjson.NewRequest(entry["id"].(interface{}), entry["method"].(string), entry["params"].([]interface{}))
Copy link
Member

Choose a reason for hiding this comment

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

please wrap at 80 columns

@dnldd
Copy link
Member Author

dnldd commented Sep 22, 2017

@dajohi PR updated.

dajohi
dajohi previously requested changes Sep 29, 2017
Copy link
Member

@dajohi dajohi left a comment

Choose a reason for hiding this comment

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

Should be the last of my nitpicking

Looks good! 👍

rpcserver.go Outdated
jsonErr = rpcInvalidError("limited user not " +
"authorized for this method")
// determine if the request is single or batched
switch request.(type) {
Copy link
Member

Choose a reason for hiding this comment

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

Could you add a default case that does something like

default:
rpcslog.Debugf("unhandled request due to unknown type: %T", request)

rpcwebsocket.go Outdated
if cmd.err != nil {
if !c.authenticated {
// Determine if the request is single request or a batched request
switch request.(type) {
Copy link
Member

Choose a reason for hiding this comment

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

Could you add a default case that does something like

default:
rpcslog.Debugf("unhandled request due to unknown type: %T", request)

Copy link
Member Author

Choose a reason for hiding this comment

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

sure, on it.

@dnldd
Copy link
Member Author

dnldd commented Sep 29, 2017

@dajohi PR updated.

Copy link
Member

@davecgh davecgh left a comment

Choose a reason for hiding this comment

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

Unfortunately, this can't go in as is. There are a lot of issues such as failing to check error returns, the ability to crash it due to naked type casts, inefficiencies due to improper use of interfaces instead of unmarshalling into the appropriate dcrjson.Result type, and not following the spec for malformed requests.

rpcwebsocket.go Outdated
// QueueNotification which implements a queue via notificationQueueHandler to
// ensure sending notifications from other subsystems can't block. Ultimately,
// all messages are sent via the outHandler.
// wsClient provides an abstraction for handling a websocket client.
Copy link
Member

Choose a reason for hiding this comment

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

This comment is not wrapped at 80 cols properly.

It should be:

// wsClient provides an abstraction for handling a websocket client.  The
// overall data flow is split into 3 main goroutines. A websocket manager is
// used to allow things such as broadcasting requested notifications to all
// connected websocket clients. Inbound messages are read via the inHandler
// goroutine and generally dispatched to their own handler. There are two
// outbound message types - one for responding to client requests and another
// for async notifications. Responses to client requests use SendMessage which
// employs a buffered channel thereby limiting the number of outstanding
// requests that can be made.  Notifications are sent via QueueNotification
// which implements a queue via notificationQueueHandler to ensure sending
// notifications from other subsystems can't block.  Ultimately, all messages
// are sent via the outHandler.

Good editors are capable of doing this wrapping for you if they're configured properly.

Copy link
Member Author

Choose a reason for hiding this comment

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

will address the issues pointed out, thanks.

rpcserver.go Outdated
var result interface{}
var request dcrjson.Request
var msg []byte
var request interface{}
Copy link
Member

@davecgh davecgh Oct 15, 2017

Choose a reason for hiding this comment

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

To clarify a bit on what I mean about interfaces, notice the code here is doing the unmarshal into an interface. Instead, it would be more efficient and safer to look at the byte stream in order to determine whether it should unmarshal into a Request or a []Request in order to be properly typed.

For example:

// At package scope...
var batchedRequestPrefix = []byte("[{")
...

if bytes.HasPrefix(body, batchedRequestPrefix) {
	/* attempt unmarshal to []Request */
} else {
	/* attempt unmarshal to Request */
}

EDIT: For a bit more of an update on this, upon looking at the specification again, I see that each individual request within the batch requires an individual response unless the entire JSON fails to parse. That means the initial unmarshal will indeed need to go into a []interface, however, every element within that array should be unmarshalled into a Request instead of manually trying to do naked type casts.

Here are the three relevant examples from the spec:

rpc call Batch, invalid JSON:

--> [
  {"jsonrpc": "2.0", "method": "sum", "params": [1,2,4], "id": "1"},
  {"jsonrpc": "2.0", "method"
]
<-- {"jsonrpc": "2.0", "error": {"code": -32700, "message": "Parse error"}, "id": null}

rpc call with invalid Batch, but valid JSON:

--> [1,2,3]
<-- [
  {"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request"}, "id": null},
  {"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request"}, "id": null},
  {"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request"}, "id": null}
]

rpc call with mixed valid and invalid requests, but all valid JSON:

--> [
        {"jsonrpc": "2.0", "method": "sum", "params": [1,2,4], "id": "1"},
        {"jsonrpc": "2.0", "method": "notify_hello", "params": [7]},
        {"jsonrpc": "2.0", "method": "subtract", "params": [42,23], "id": "2"},
        {"foo": "boo"},
        {"jsonrpc": "2.0", "method": "foo.get", "params": {"name": "myself"}, "id": "5"},
        {"jsonrpc": "2.0", "method": "get_data", "id": "9"} 
    ]
<-- [
        {"jsonrpc": "2.0", "result": 7, "id": "1"},
        {"jsonrpc": "2.0", "result": 19, "id": "2"},
        {"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request"}, "id": null},
        {"jsonrpc": "2.0", "error": {"code": -32601, "message": "Method not found"}, "id": "5"},
        {"jsonrpc": "2.0", "result": ["hello", 5], "id": "9"}
    ]

rpcserver.go Outdated
batchedReply.WriteByte('[')
for idx, entry := range reqs {
entry := entry.(map[string]interface{})
req, err := dcrjson.NewRequest(entry["id"].(interface{}),
Copy link
Member

@davecgh davecgh Oct 15, 2017

Choose a reason for hiding this comment

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

This is an example of what I meant by naked type casts that can crash.

Notice what would happen if you fed this code requests like:

[{"jsonrpc":1.0", "bogus":"foo"}] or [{"jsonrpc":1.0", "id":"1", params:"foo"}]

@dnldd dnldd force-pushed the json2 branch 5 times, most recently from ba4f6ae to 79ec1b9 Compare October 19, 2017 23:22
@dnldd
Copy link
Member Author

dnldd commented Oct 19, 2017

@davecgh I've updated the http post implementation, the websocket impl. will be up in a couple of days. Would like a review for the http post impl. in the mean time when you're free, thanks.

@dnldd
Copy link
Member Author

dnldd commented Oct 21, 2017

updated, should be good now.

@dajohi dajohi dismissed their stale review November 9, 2017 12:29

issues addressed

Copy link
Member

@davecgh davecgh left a comment

Choose a reason for hiding this comment

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

This is producing some incorrect results in some negative paths. See the following code:

package main

import (
	"bytes"
	"crypto/tls"
	"crypto/x509"
	"fmt"
	"io/ioutil"
	"net/http"
	"path/filepath"

	"github.com/decred/dcrd/dcrutil"
)

func main() {
	const (
		url     = "https://127.0.0.1:19109"
		rpcUser = "yourrpcuser"
		rpcPass = "yourrpcpass"
	)

	// populate request set
	reqs := []string{
		`{}`,
		`[]`,
		`[1]`,
		`[1,2,3]`,
		`{"foo": "boo"}`,                              // should be an invalid request
		`{"jsonrpc": "1.0", "foo": "boo", "id": "1"}`,
		`{"jsonrpc": "1.0", "method": "getblockcount", "params": [], "id": "1"}`,
		`{"jsonrpc": "1.0", "method": "getblockcount", "params": "a", "id": "1"}`, // should be invalid since params is neither an array nor a json object.
		`[
			{"jsonrpc": "2.0", "method": "getblockcount", "params": [], "id": "1"},
			{"jsonrpc": "2.0", "method": "decodescript", "params": ["ac"]},
			{"jsonrpc": "2.0", "method": "getbestblockhash", "params": [], "id": "2"},
			{"foo": "boo"},
			{"jsonrpc": "2.0", "method": "getblockcount", "id": "9"} 
		]`, // should produce invalid request for the `{"foo": "boo"}`.
	}

	// Connect to local dcrd RPC server using websockets.
	dcrdHomeDir := dcrutil.AppDataDir("dcrd", false)
	certs, err := ioutil.ReadFile(filepath.Join(dcrdHomeDir, "rpc.cert"))
	if err != nil {
		fmt.Println(err)
		return
	}

	pool := x509.NewCertPool()
	pool.AppendCertsFromPEM(certs)
	client := http.Client{
		Transport: &http.Transport{
			TLSClientConfig: &tls.Config{
				RootCAs: pool,
			},
		},
	}

	for _, jsonReq := range reqs {
		bodyReader := bytes.NewReader([]byte(jsonReq))
		httpReq, err := http.NewRequest("POST", url, bodyReader)
		if err != nil {
			fmt.Println(err)
			return
		}
		httpReq.Close = true
		httpReq.Header.Set("Content-Type", "application/json")
		httpReq.SetBasicAuth(rpcUser, rpcPass)
		resp, err := client.Do(httpReq)
		if err != nil {
			fmt.Println("request:", jsonReq, "response:", err)
			return
		}

		respBytes, err := ioutil.ReadAll(resp.Body)
		resp.Body.Close()
		if err != nil {
			fmt.Println(err)
			return
		}

		fmt.Println("request:", jsonReq, "response:", string(respBytes))
	}
}

Output:

request: {} response: 
request: [] response: {"jsonrpc":"1.0","result":null,"error":{"code":-32600,"message":"Invalid request: empty batch"},"id":null}
request: [1] response: [{"jsonrpc":"1.0","result":null,"error":{"code":-32600,"message":"Invalid request: json: cannot unmarshal number into Go value of type dcrjson.Request"},"id":null}]
request: [1,2,3] response: [{"jsonrpc":"1.0","result":null,"error":{"code":-32600,"message":"Invalid request: json: cannot unmarshal number into Go value of type dcrjson.Request"},"id":null},{"jsonrpc":"1.0","result":null,"error":{"code":-32600,"message":"Invalid request: json: cannot unmarshal number into Go value of type dcrjson.Request"},"id":null},{"jsonrpc":"1.0","result":null,"error":{"code":-32600,"message":"Invalid request: json: cannot unmarshal number into Go value of type dcrjson.Request"},"id":null}]
request: {"foo": "boo"} response: 
request: {"jsonrpc": "1.0", "foo": "boo", "id": "1"} response: {"jsonrpc":"1.0","result":null,"error":{"code":-32601,"message":"Method not found"},"id":"1"}
request: {"jsonrpc": "1.0", "method": "getblockcount", "params": [], "id": "1"} response: {"jsonrpc":"1.0","result":227411,"error":null,"id":"1"}
request: {"jsonrpc": "1.0", "method": "getblockcount", "params": "a", "id": "1"} response: {"jsonrpc":"1.0","result":227411,"error":null,"id":"1"}
request: [
	{"jsonrpc": "2.0", "method": "getblockcount", "params": [], "id": "1"},
	{"jsonrpc": "2.0", "method": "decodescript", "params": ["ac"]},
	{"jsonrpc": "2.0", "method": "getbestblockhash", "params": [], "id": "2"},
	{"foo": "boo"},
	{"jsonrpc": "2.0", "method": "getblockcount", "id": "9"} 
] response: [{"jsonrpc":"2.0","result":227411,"error":null,"id":"1"},{"jsonrpc":"2.0","result":"0000000003563140b1bd52599ac1abca98900bce048e9c3ba9ca72eba948f7d6","error":null,"id":"2"},{"jsonrpc":"2.0","result":227411,"error":null,"id":"9"}]

// Response is the general form of a JSON-RPC response. The type of the Result
// field varies from one command to the next, so it is implemented as an
// interface. The ID field has to be a pointer for Go to put a null in it when
// Response is the general form of a JSON-RPC 1.0 response. The type of the
Copy link
Member

Choose a reason for hiding this comment

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

Why the addition of 1.0? This applies to both versions.

@davecgh
Copy link
Member

davecgh commented Jan 29, 2018

The final {"jsonrpc": "2.0", "method": "getblockcount", "id": "9"} in the batch is incorrectly failing. That should be valid because it doesn't specify params at all which is acceptable.

@dnldd dnldd force-pushed the json2 branch 2 times, most recently from 3a2040f to 5b4cafd Compare January 30, 2018 00:08
// type with the NewCmd or New<Foo>Cmd functions and call the MarshalCmd
// function with that command to generate the marshalled JSON-RPC request.
func NewRequest(id interface{}, method string, params []interface{}) (*Request, error) {
func (request *Request) UnmarshalJSON(b []byte) error {
Copy link
Member

Choose a reason for hiding this comment

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

Missing function comment.

rpcserver.go Outdated
}

// processRequest determines incoming request type (single or batched),
// parses it and returns a response.
Copy link
Member

Choose a reason for hiding this comment

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

s/response/marshalled response/

rpcwebsocket.go Outdated
if err != nil {
rpcsLog.Errorf("Failed to marshal parse failure "+
"reply: %v", err)
cmd := parseCmd(&req)
Copy link
Member

Choose a reason for hiding this comment

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

Seems like this should retain the previous functionality of checking for req.ID == nil first to avoid the extra overhead of parsing notifications. I agree that this is correct behavior due to the addition of the check for nil in the parseCmd function, but there is non reason to even create the struct and churn GC for notifications.

rpcwebsocket.go Outdated
Code: dcrjson.ErrRPCInvalidParams.Code,
Message: "limited user not authorized for this method",
rpcsLog.Debugf("Received command <%s> from %s", cmd.method, c.addr)
// Check auth. The client is immediately disconnected if the
Copy link
Member

Choose a reason for hiding this comment

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

Needs a blank line before this to separate the logical blocks.

@dnldd dnldd force-pushed the json2 branch 2 times, most recently from a819423 to 949b22b Compare February 1, 2018 11:46
@jakesylvestre
Copy link

Is there an example of using this on an http post?

@davecgh
Copy link
Member

davecgh commented Dec 4, 2019

It's the same as a regular request except you create an array of of them:

e.g.

$ curl --silent --cacert "~/.dcrd/rpc.cert" --user "USER:PASS" --data-binary '[{"jsonrpc": "1.0", "id":1, "method": "getinfo", "params": []},{"jsonrpc": "1.0", "id":2, "method": "getblockcount", "params": []}]' https://127.0.0.1:9109
[{"jsonrpc":"1.0","result":{"version":1060000,"protocolversion":7,"blocks":402789,"timeoffset":38,"connections":8,"proxy":"","difficulty":27376557113.1791,"testnet":false,"relayfee":0.0001,"errors":""},"error":null,"id":1},{"jsonrpc":"1.0","result":402789,"error":null,"id":2}]

@jakesylvestre
Copy link

Got it. @davecgh, this was tremendously helpful in putting together the PR mentioned above. By the way, you might find this bulk query builder helpful for this project.

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.

5 participants