From 951d939cdb89c08c1237d521dcbe3b4564725524 Mon Sep 17 00:00:00 2001 From: Silas Davis <silas@monax.io> Date: Tue, 26 Jun 2018 14:43:20 +0100 Subject: [PATCH] Add last_block_info endpoint for healthcheck Also cleans up rpc/lib argument parsing Signed-off-by: Silas Davis <silas@monax.io> --- rpc/lib/rpc_test.go | 8 ++--- rpc/lib/server/handlers.go | 49 ++++++++++++++++--------------- rpc/lib/server/handlers_test.go | 7 ++--- rpc/lib/server/http_server.go | 2 +- rpc/lib/types/error_codes.go | 52 +++++++++++++++++++++++++++++++++ rpc/lib/types/types.go | 29 +++++++++++------- rpc/lib/types/types_test.go | 4 +-- rpc/result.go | 8 +++++ rpc/result_test.go | 17 +++++++++++ rpc/service.go | 34 +++++++++++++++++++++ rpc/tm/methods.go | 4 +++ 11 files changed, 169 insertions(+), 45 deletions(-) create mode 100644 rpc/lib/types/error_codes.go diff --git a/rpc/lib/rpc_test.go b/rpc/lib/rpc_test.go index 99941c88..1d21c21a 100644 --- a/rpc/lib/rpc_test.go +++ b/rpc/lib/rpc_test.go @@ -187,22 +187,22 @@ func testWithHTTPClient(t *testing.T, cl client.HTTPClient) { val := "acbd" got, err := echoViaHTTP(cl, val) require.Nil(t, err) - assert.Equal(t, got, val) + assert.Equal(t, val, got) val2 := randBytes(t) got2, err := echoBytesViaHTTP(cl, val2) require.Nil(t, err) - assert.Equal(t, got2, val2) + assert.Equal(t, val2, got2) val3 := cmn.HexBytes(randBytes(t)) got3, err := echoDataBytesViaHTTP(cl, val3) require.Nil(t, err) - assert.Equal(t, got3, val3) + assert.Equal(t, val3, got3) val4 := rand.Intn(10000) got4, err := echoIntViaHTTP(cl, val4) require.Nil(t, err) - assert.Equal(t, got4, val4) + assert.Equal(t, val4, got4) } func echoViaWS(cl *client.WSClient, val string) (string, error) { diff --git a/rpc/lib/server/handlers.go b/rpc/lib/server/handlers.go index 31892c0f..506425b7 100644 --- a/rpc/lib/server/handlers.go +++ b/rpc/lib/server/handlers.go @@ -311,37 +311,40 @@ func jsonStringToArg(ty reflect.Type, arg string) (reflect.Value, error) { } func nonJSONToArg(ty reflect.Type, arg string) (reflect.Value, error, bool) { - isQuotedString := strings.HasPrefix(arg, `"`) && strings.HasSuffix(arg, `"`) - isHexString := strings.HasPrefix(strings.ToLower(arg), "0x") expectingString := ty.Kind() == reflect.String - expectingByteSlice := ty.Kind() == reflect.Slice && ty.Elem().Kind() == reflect.Uint8 + expectingBytes := (ty.Kind() == reflect.Slice || ty.Kind() == reflect.Array) && ty.Elem().Kind() == reflect.Uint8 - if isHexString { - if !expectingString && !expectingByteSlice { - err := errors.Errorf("Got a hex string arg, but expected '%s'", - ty.Kind().String()) - return reflect.ValueOf(nil), err, false - } + isQuotedString := strings.HasPrefix(arg, `"`) && strings.HasSuffix(arg, `"`) - var value []byte - value, err := hex.DecodeString(arg[2:]) - if err != nil { - return reflect.ValueOf(nil), err, false - } - if ty.Kind() == reflect.String { - return reflect.ValueOf(string(value)), nil, true - } - return reflect.ValueOf([]byte(value)), nil, true + // Throw quoted strings at JSON parser later... because it always has... + if expectingString && !isQuotedString { + return reflect.ValueOf(arg), nil, true } - if isQuotedString && expectingByteSlice { - v := reflect.New(reflect.TypeOf("")) - err := json.Unmarshal([]byte(arg), v.Interface()) + if expectingBytes { + if isQuotedString { + rv := reflect.New(ty) + err := json.Unmarshal([]byte(arg), rv.Interface()) + if err != nil { + return reflect.ValueOf(nil), err, false + } + return rv.Elem(), nil, true + } + if strings.HasPrefix(strings.ToLower(arg), "0x") { + arg = arg[2:] + } + var value []byte + value, err := hex.DecodeString(arg) if err != nil { return reflect.ValueOf(nil), err, false } - v = v.Elem() - return reflect.ValueOf([]byte(v.String())), nil, true + if ty.Kind() == reflect.Array { + // Gives us an empty array of the right type + rv := reflect.New(ty).Elem() + reflect.Copy(rv, reflect.ValueOf(value)) + return rv, nil, true + } + return reflect.ValueOf(value), nil, true } return reflect.ValueOf(nil), nil, false diff --git a/rpc/lib/server/handlers_test.go b/rpc/lib/server/handlers_test.go index be66d56c..1c7ce385 100644 --- a/rpc/lib/server/handlers_test.go +++ b/rpc/lib/server/handlers_test.go @@ -37,8 +37,8 @@ func TestRPCParams(t *testing.T) { wantErr string }{ // bad - {`{"jsonrpc": "2.0", "id": "0"}`, "Method not found"}, - {`{"jsonrpc": "2.0", "method": "y", "id": "0"}`, "Method not found"}, + {`{"jsonrpc": "2.0", "id": "0"}`, "Method Not Found"}, + {`{"jsonrpc": "2.0", "method": "y", "id": "0"}`, "Method Not Found"}, {`{"method": "c", "id": "0", "params": a}`, "invalid character"}, {`{"method": "c", "id": "0", "params": ["a"]}`, "got 1"}, {`{"method": "c", "id": "0", "params": ["a", "b"]}`, "of type int"}, @@ -56,7 +56,6 @@ func TestRPCParams(t *testing.T) { mux.ServeHTTP(rec, req) res := rec.Result() // Always expecting back a JSONRPCResponse - assert.True(t, statusOK(res.StatusCode), "#%d: should always return 2XX", i) blob, err := ioutil.ReadAll(res.Body) if err != nil { t.Errorf("#%d: err reading body: %v", i, err) @@ -66,7 +65,7 @@ func TestRPCParams(t *testing.T) { recv := new(types.RPCResponse) assert.Nil(t, json.Unmarshal(blob, recv), "#%d: expecting successful parsing of an RPCResponse:\nblob: %s", i, blob) assert.NotEqual(t, recv, new(types.RPCResponse), "#%d: not expecting a blank RPCResponse", i) - + assert.Equal(t, recv.Error.HTTPStatusCode(), res.StatusCode, "#%d: status should match error code", i) if tt.wantErr == "" { assert.Nil(t, recv.Error, "#%d: not expecting an error", i) } else { diff --git a/rpc/lib/server/http_server.go b/rpc/lib/server/http_server.go index e0734304..e080ff26 100644 --- a/rpc/lib/server/http_server.go +++ b/rpc/lib/server/http_server.go @@ -46,7 +46,7 @@ func WriteRPCResponseHTTP(w http.ResponseWriter, res types.RPCResponse) { panic(err) } w.Header().Set("Content-Type", "application/json") - w.WriteHeader(200) + w.WriteHeader(res.Error.HTTPStatusCode()) w.Write(jsonBytes) // nolint: errcheck, gas } diff --git a/rpc/lib/types/error_codes.go b/rpc/lib/types/error_codes.go new file mode 100644 index 00000000..4ef91dd2 --- /dev/null +++ b/rpc/lib/types/error_codes.go @@ -0,0 +1,52 @@ +package types + +import ( + "net/http" + "strconv" +) + +// From JSONRPC 2.0 spec +type RPCErrorCode int + +const ( + RPCErrorCodeParseError RPCErrorCode = -32700 + RPCErrorCodeInvalidRequest RPCErrorCode = -32600 + RPCErrorCodeMethodNotFound RPCErrorCode = -32601 + RPCErrorCodeInvalidParams RPCErrorCode = -32602 + RPCErrorCodeInternalError RPCErrorCode = -32603 + RPCErrorCodeServerError RPCErrorCode = -32000 +) + +func (code RPCErrorCode) String() string { + switch code { + case RPCErrorCodeParseError: + return "Parse Error" + case RPCErrorCodeInvalidRequest: + return "Parse Error" + case RPCErrorCodeMethodNotFound: + return "Method Not Found" + case RPCErrorCodeInvalidParams: + return "Invalid Params" + case RPCErrorCodeInternalError: + return "Internal Error" + case RPCErrorCodeServerError: + return "Server Error" + default: + return strconv.FormatInt(int64(code), 10) + } +} + +func (code RPCErrorCode) HTTPStatusCode() int { + switch code { + case RPCErrorCodeInvalidRequest: + return http.StatusBadRequest + case RPCErrorCodeMethodNotFound: + return http.StatusMethodNotAllowed + default: + return http.StatusInternalServerError + } +} + +func (code RPCErrorCode) Error() string { + return code.String() +} diff --git a/rpc/lib/types/types.go b/rpc/lib/types/types.go index 42a7a219..8ff3e6c6 100644 --- a/rpc/lib/types/types.go +++ b/rpc/lib/types/types.go @@ -71,9 +71,9 @@ func ArrayToRequest(id string, method string, params []interface{}) (RPCRequest, // RESPONSE type RPCError struct { - Code int `json:"code"` - Message string `json:"message"` - Data string `json:"data,omitempty"` + Code RPCErrorCode `json:"code"` + Message string `json:"message"` + Data string `json:"data,omitempty"` } func (err RPCError) Error() string { @@ -84,6 +84,13 @@ func (err RPCError) Error() string { return fmt.Sprintf(baseFormat, err.Code, err.Message) } +func (err *RPCError) HTTPStatusCode() int { + if err == nil { + return 200 + } + return err.Code.HTTPStatusCode() +} + type RPCResponse struct { JSONRPC string `json:"jsonrpc"` ID string `json:"id"` @@ -106,11 +113,11 @@ func NewRPCSuccessResponse(id string, res interface{}) RPCResponse { return RPCResponse{JSONRPC: "2.0", ID: id, Result: rawMsg} } -func NewRPCErrorResponse(id string, code int, msg string, data string) RPCResponse { +func NewRPCErrorResponse(id string, code RPCErrorCode, data string) RPCResponse { return RPCResponse{ JSONRPC: "2.0", ID: id, - Error: &RPCError{Code: code, Message: msg, Data: data}, + Error: &RPCError{Code: code, Message: code.String(), Data: data}, } } @@ -122,27 +129,27 @@ func (resp RPCResponse) String() string { } func RPCParseError(id string, err error) RPCResponse { - return NewRPCErrorResponse(id, -32700, "Parse error. Invalid JSON", err.Error()) + return NewRPCErrorResponse(id, RPCErrorCodeParseError, err.Error()) } func RPCInvalidRequestError(id string, err error) RPCResponse { - return NewRPCErrorResponse(id, -32600, "Invalid Request", err.Error()) + return NewRPCErrorResponse(id, RPCErrorCodeInvalidRequest, err.Error()) } func RPCMethodNotFoundError(id string) RPCResponse { - return NewRPCErrorResponse(id, -32601, "Method not found", "") + return NewRPCErrorResponse(id, RPCErrorCodeMethodNotFound, "") } func RPCInvalidParamsError(id string, err error) RPCResponse { - return NewRPCErrorResponse(id, -32602, "Invalid params", err.Error()) + return NewRPCErrorResponse(id, RPCErrorCodeInvalidParams, err.Error()) } func RPCInternalError(id string, err error) RPCResponse { - return NewRPCErrorResponse(id, -32603, "Internal error", err.Error()) + return NewRPCErrorResponse(id, RPCErrorCodeInternalError, err.Error()) } func RPCServerError(id string, err error) RPCResponse { - return NewRPCErrorResponse(id, -32000, "Server error", err.Error()) + return NewRPCErrorResponse(id, RPCErrorCodeServerError, err.Error()) } //---------------------------------------- diff --git a/rpc/lib/types/types_test.go b/rpc/lib/types/types_test.go index 1227bbd1..dd0e0316 100644 --- a/rpc/lib/types/types_test.go +++ b/rpc/lib/types/types_test.go @@ -23,12 +23,12 @@ func TestResponses(t *testing.T) { d := RPCParseError("1", errors.New("Hello world")) e, _ := json.Marshal(d) - f := `{"jsonrpc":"2.0","id":"1","error":{"code":-32700,"message":"Parse error. Invalid JSON","data":"Hello world"}}` + f := `{"jsonrpc":"2.0","id":"1","error":{"code":-32700,"message":"Parse Error","data":"Hello world"}}` assert.Equal(string(f), string(e)) g := RPCMethodNotFoundError("2") h, _ := json.Marshal(g) - i := `{"jsonrpc":"2.0","id":"2","error":{"code":-32601,"message":"Method not found"}}` + i := `{"jsonrpc":"2.0","id":"2","error":{"code":-32601,"message":"Method Not Found"}}` assert.Equal(string(h), string(i)) } diff --git a/rpc/result.go b/rpc/result.go index 3159690e..44a61c1d 100644 --- a/rpc/result.go +++ b/rpc/result.go @@ -18,6 +18,8 @@ import ( "encoding/json" "fmt" + "time" + acm "github.com/hyperledger/burrow/account" "github.com/hyperledger/burrow/binary" "github.com/hyperledger/burrow/crypto" @@ -120,6 +122,12 @@ type ResultStatus struct { NodeVersion string } +type ResultLastBlockInfo struct { + LastBlockHeight uint64 + LastBlockTime time.Time + LastBlockHash binary.HexBytes +} + type ResultChainId struct { ChainName string ChainId string diff --git a/rpc/result_test.go b/rpc/result_test.go index 62ea5b51..ade8a615 100644 --- a/rpc/result_test.go +++ b/rpc/result_test.go @@ -18,7 +18,12 @@ import ( "encoding/json" "testing" + "time" + + "fmt" + acm "github.com/hyperledger/burrow/account" + "github.com/hyperledger/burrow/binary" "github.com/hyperledger/burrow/crypto" "github.com/hyperledger/burrow/execution" "github.com/hyperledger/burrow/txs" @@ -166,3 +171,15 @@ func TestResultDumpConsensusState(t *testing.T) { require.NoError(t, err) assert.Equal(t, string(bs), string(bsOut)) } + +func TestResultLastBlockInfo(t *testing.T) { + res := &ResultLastBlockInfo{ + LastBlockTime: time.Now(), + LastBlockHash: binary.HexBytes{3, 4, 5, 6}, + LastBlockHeight: 2343, + } + bs, err := json.Marshal(res) + require.NoError(t, err) + fmt.Println(string(bs)) + +} diff --git a/rpc/service.go b/rpc/service.go index 81afee60..0863a65a 100644 --- a/rpc/service.go +++ b/rpc/service.go @@ -18,6 +18,10 @@ import ( "context" "fmt" + "time" + + "encoding/json" + acm "github.com/hyperledger/burrow/account" "github.com/hyperledger/burrow/account/state" "github.com/hyperledger/burrow/binary" @@ -418,3 +422,33 @@ func (s *Service) GeneratePrivateAccount() (*ResultGeneratePrivateAccount, error PrivateAccount: acm.AsConcretePrivateAccount(privateAccount), }, nil } + +func (s *Service) LastBlockInfo(blockWithin string) (*ResultLastBlockInfo, error) { + res := &ResultLastBlockInfo{ + LastBlockHeight: s.blockchain.LastBlockHeight(), + LastBlockHash: s.blockchain.LastBlockHash(), + LastBlockTime: s.blockchain.LastBlockTime(), + } + if blockWithin == "" { + return res, nil + } + duration, err := time.ParseDuration(blockWithin) + if err != nil { + return nil, fmt.Errorf("could not parse blockWithin duration to determine whether to throw error: %v", err) + } + // Take neg abs in case caller is counting backwards (not we add later) + if duration > 0 { + duration = -duration + } + blockTimeThreshold := time.Now().Add(duration) + if res.LastBlockTime.After(blockTimeThreshold) { + // We've created blocks recently enough + return res, nil + } + resJSON, err := json.Marshal(res) + if err != nil { + resJSON = []byte("<error: could not marshal last block info>") + } + return nil, fmt.Errorf("no block committed within the last %s (cutoff: %s), last block info: %s", + blockWithin, blockTimeThreshold.Format(time.RFC3339), string(resJSON)) +} diff --git a/rpc/tm/methods.go b/rpc/tm/methods.go index 6e892df8..017cf29c 100644 --- a/rpc/tm/methods.go +++ b/rpc/tm/methods.go @@ -55,6 +55,9 @@ const ( // Private keys and signing GeneratePrivateAccount = "unsafe/gen_priv_account" SignTx = "unsafe/sign_tx" + + // Health check + LastBlockInfo = "last_block_info" ) const SubscriptionTimeout = 5 * time.Second @@ -175,6 +178,7 @@ func GetRoutes(service *rpc.Service, logger *logging.Logger) map[string]*server. // Private account GeneratePrivateAccount: server.NewRPCFunc(service.GeneratePrivateAccount, ""), + LastBlockInfo: server.NewRPCFunc(service.LastBlockInfo, "block_within"), } } -- GitLab