rpc: Add caching support (backport #9650) (#9666)

* rpc: Add caching support (#9650)

* Set cache control in the HTTP-RPC response header

* Add a simply cache policy to the RPC routes

* add a condition to check the RPC request has default height settings

* fix cherry pick error

* update pending log

* use options struct intead of single parameter

* refacor FuncOptions to functional options

* add functional options in WebSocket RPC function

* revert doc

* replace deprecated function call

* revise functional options

* remove unuse comment

* fix revised error

* adjust cache-control settings

* Update rpc/jsonrpc/server/http_json_handler.go

Co-authored-by: Thane Thomson <connect@thanethomson.com>

* linter: Fix false positive

Signed-off-by: Thane Thomson <connect@thanethomson.com>

* rpc: Separate cacheable and non-cacheable HTTP response writers

Allows us to roll this change out in a non-API-breaking way, since this
is an additive change.

Signed-off-by: Thane Thomson <connect@thanethomson.com>

* rpc: Ensure consistent caching strategy

Ensure a consistent caching strategy across both JSONRPC- and URI-based
requests.

This requires a bit of a refactor of the previous caching logic, which
is complicated a little by the complex reflection-based approach taken
in the Tendermint RPC.

Signed-off-by: Thane Thomson <connect@thanethomson.com>

* rpc: Add more tests for caching

Signed-off-by: Thane Thomson <connect@thanethomson.com>

* Update CHANGELOG_PENDING

Signed-off-by: Thane Thomson <connect@thanethomson.com>

* light: Sync routes config with RPC core

Signed-off-by: Thane Thomson <connect@thanethomson.com>

* rpc: Update OpenAPI docs

Signed-off-by: Thane Thomson <connect@thanethomson.com>

Signed-off-by: Thane Thomson <connect@thanethomson.com>
Co-authored-by: jayt106 <jaytseng106@gmail.com>
Co-authored-by: jay tseng <jay.tseng@crypto.com>
Co-authored-by: JayT106 <JayT106@users.noreply.github.com>
(cherry picked from commit 816c6bac00)

# Conflicts:
#	CHANGELOG_PENDING.md
#	light/proxy/routes.go
#	rpc/core/routes.go
#	rpc/openapi/openapi.yaml
#	test/fuzz/tests/rpc_jsonrpc_server_test.go

* Fix conflict in CHANGELOG_PENDING

Signed-off-by: Thane Thomson <connect@thanethomson.com>

* Resolve remaining conflicts

Signed-off-by: Thane Thomson <connect@thanethomson.com>

Signed-off-by: Thane Thomson <connect@thanethomson.com>
Co-authored-by: Thane Thomson <connect@thanethomson.com>
This commit is contained in:
mergify[bot]
2022-11-07 06:11:05 -05:00
committed by GitHub
parent e7b9ee7cef
commit 7917485bc7
11 changed files with 342 additions and 58 deletions

View File

@@ -19,5 +19,6 @@
### IMPROVEMENTS
- [p2p] \#9641 Add new Envelope type and associated methods for sending and receiving Envelopes instead of raw bytes.
This also adds new metrics, `tendermint_p2p_message_send_bytes_total` and `tendermint_p2p_message_receive_bytes_total`, that expose how many bytes of each message type have been sent.
- [rpc] \#9666 Enable caching of RPC responses (@JayT106)
### BUG FIXES

View File

@@ -21,20 +21,20 @@ func RPCRoutes(c *lrpc.Client) map[string]*rpcserver.RPCFunc {
"health": rpcserver.NewRPCFunc(makeHealthFunc(c), ""),
"status": rpcserver.NewRPCFunc(makeStatusFunc(c), ""),
"net_info": rpcserver.NewRPCFunc(makeNetInfoFunc(c), ""),
"blockchain": rpcserver.NewRPCFunc(makeBlockchainInfoFunc(c), "minHeight,maxHeight"),
"genesis": rpcserver.NewRPCFunc(makeGenesisFunc(c), ""),
"genesis_chunked": rpcserver.NewRPCFunc(makeGenesisChunkedFunc(c), ""),
"block": rpcserver.NewRPCFunc(makeBlockFunc(c), "height"),
"block_by_hash": rpcserver.NewRPCFunc(makeBlockByHashFunc(c), "hash"),
"block_results": rpcserver.NewRPCFunc(makeBlockResultsFunc(c), "height"),
"commit": rpcserver.NewRPCFunc(makeCommitFunc(c), "height"),
"tx": rpcserver.NewRPCFunc(makeTxFunc(c), "hash,prove"),
"blockchain": rpcserver.NewRPCFunc(makeBlockchainInfoFunc(c), "minHeight,maxHeight", rpcserver.Cacheable()),
"genesis": rpcserver.NewRPCFunc(makeGenesisFunc(c), "", rpcserver.Cacheable()),
"genesis_chunked": rpcserver.NewRPCFunc(makeGenesisChunkedFunc(c), "", rpcserver.Cacheable()),
"block": rpcserver.NewRPCFunc(makeBlockFunc(c), "height", rpcserver.Cacheable("height")),
"block_by_hash": rpcserver.NewRPCFunc(makeBlockByHashFunc(c), "hash", rpcserver.Cacheable()),
"block_results": rpcserver.NewRPCFunc(makeBlockResultsFunc(c), "height", rpcserver.Cacheable("height")),
"commit": rpcserver.NewRPCFunc(makeCommitFunc(c), "height", rpcserver.Cacheable("height")),
"tx": rpcserver.NewRPCFunc(makeTxFunc(c), "hash,prove", rpcserver.Cacheable()),
"tx_search": rpcserver.NewRPCFunc(makeTxSearchFunc(c), "query,prove,page,per_page,order_by"),
"block_search": rpcserver.NewRPCFunc(makeBlockSearchFunc(c), "query,page,per_page,order_by"),
"validators": rpcserver.NewRPCFunc(makeValidatorsFunc(c), "height,page,per_page"),
"validators": rpcserver.NewRPCFunc(makeValidatorsFunc(c), "height,page,per_page", rpcserver.Cacheable("height")),
"dump_consensus_state": rpcserver.NewRPCFunc(makeDumpConsensusStateFunc(c), ""),
"consensus_state": rpcserver.NewRPCFunc(makeConsensusStateFunc(c), ""),
"consensus_params": rpcserver.NewRPCFunc(makeConsensusParamsFunc(c), "height"),
"consensus_params": rpcserver.NewRPCFunc(makeConsensusParamsFunc(c), "height", rpcserver.Cacheable("height")),
"unconfirmed_txs": rpcserver.NewRPCFunc(makeUnconfirmedTxsFunc(c), "limit"),
"num_unconfirmed_txs": rpcserver.NewRPCFunc(makeNumUnconfirmedTxsFunc(c), ""),
@@ -45,7 +45,7 @@ func RPCRoutes(c *lrpc.Client) map[string]*rpcserver.RPCFunc {
// abci API
"abci_query": rpcserver.NewRPCFunc(makeABCIQueryFunc(c), "path,data,height,prove"),
"abci_info": rpcserver.NewRPCFunc(makeABCIInfoFunc(c), ""),
"abci_info": rpcserver.NewRPCFunc(makeABCIInfoFunc(c), "", rpcserver.Cacheable()),
// evidence API
"broadcast_evidence": rpcserver.NewRPCFunc(makeBroadcastEvidenceFunc(c), "evidence"),

View File

@@ -17,21 +17,21 @@ var Routes = map[string]*rpc.RPCFunc{
"health": rpc.NewRPCFunc(Health, ""),
"status": rpc.NewRPCFunc(Status, ""),
"net_info": rpc.NewRPCFunc(NetInfo, ""),
"blockchain": rpc.NewRPCFunc(BlockchainInfo, "minHeight,maxHeight"),
"genesis": rpc.NewRPCFunc(Genesis, ""),
"genesis_chunked": rpc.NewRPCFunc(GenesisChunked, "chunk"),
"block": rpc.NewRPCFunc(Block, "height"),
"block_by_hash": rpc.NewRPCFunc(BlockByHash, "hash"),
"block_results": rpc.NewRPCFunc(BlockResults, "height"),
"commit": rpc.NewRPCFunc(Commit, "height"),
"check_tx": rpc.NewRPCFunc(CheckTx, "tx"),
"tx": rpc.NewRPCFunc(Tx, "hash,prove"),
"blockchain": rpc.NewRPCFunc(BlockchainInfo, "minHeight,maxHeight", rpc.Cacheable()),
"genesis": rpc.NewRPCFunc(Genesis, "", rpc.Cacheable()),
"genesis_chunked": rpc.NewRPCFunc(GenesisChunked, "chunk", rpc.Cacheable()),
"block": rpc.NewRPCFunc(Block, "height", rpc.Cacheable("height")),
"block_by_hash": rpc.NewRPCFunc(BlockByHash, "hash", rpc.Cacheable()),
"block_results": rpc.NewRPCFunc(BlockResults, "height", rpc.Cacheable("height")),
"commit": rpc.NewRPCFunc(Commit, "height", rpc.Cacheable("height")),
"check_tx": rpc.NewRPCFunc(CheckTx, "tx", rpc.Cacheable()),
"tx": rpc.NewRPCFunc(Tx, "hash,prove", rpc.Cacheable()),
"tx_search": rpc.NewRPCFunc(TxSearch, "query,prove,page,per_page,order_by"),
"block_search": rpc.NewRPCFunc(BlockSearch, "query,page,per_page,order_by"),
"validators": rpc.NewRPCFunc(Validators, "height,page,per_page"),
"validators": rpc.NewRPCFunc(Validators, "height,page,per_page", rpc.Cacheable("height")),
"dump_consensus_state": rpc.NewRPCFunc(DumpConsensusState, ""),
"consensus_state": rpc.NewRPCFunc(ConsensusState, ""),
"consensus_params": rpc.NewRPCFunc(ConsensusParams, "height"),
"consensus_params": rpc.NewRPCFunc(ConsensusParams, "height", rpc.Cacheable("height")),
"unconfirmed_txs": rpc.NewRPCFunc(UnconfirmedTxs, "limit"),
"num_unconfirmed_txs": rpc.NewRPCFunc(NumUnconfirmedTxs, ""),
@@ -42,7 +42,7 @@ var Routes = map[string]*rpc.RPCFunc{
// abci API
"abci_query": rpc.NewRPCFunc(ABCIQuery, "path,data,height,prove"),
"abci_info": rpc.NewRPCFunc(ABCIInfo, ""),
"abci_info": rpc.NewRPCFunc(ABCIInfo, "", rpc.Cacheable()),
// evidence API
"broadcast_evidence": rpc.NewRPCFunc(BroadcastEvidence, "evidence"),

View File

@@ -7,8 +7,10 @@ import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"os"
"os/exec"
"strings"
"testing"
"time"
@@ -37,9 +39,7 @@ const (
testVal = "acbd"
)
var (
ctx = context.Background()
)
var ctx = context.Background()
type ResultEcho struct {
Value string `json:"value"`
@@ -57,6 +57,10 @@ type ResultEchoDataBytes struct {
Value tmbytes.HexBytes `json:"value"`
}
type ResultEchoWithDefault struct {
Value int `json:"value"`
}
// Define some routes
var Routes = map[string]*server.RPCFunc{
"echo": server.NewRPCFunc(EchoResult, "arg"),
@@ -64,6 +68,7 @@ var Routes = map[string]*server.RPCFunc{
"echo_bytes": server.NewRPCFunc(EchoBytesResult, "arg"),
"echo_data_bytes": server.NewRPCFunc(EchoDataBytesResult, "arg"),
"echo_int": server.NewRPCFunc(EchoIntResult, "arg"),
"echo_default": server.NewRPCFunc(EchoWithDefault, "arg", server.Cacheable("arg")),
}
func EchoResult(ctx *types.Context, v string) (*ResultEcho, error) {
@@ -86,6 +91,14 @@ func EchoDataBytesResult(ctx *types.Context, v tmbytes.HexBytes) (*ResultEchoDat
return &ResultEchoDataBytes{v}, nil
}
func EchoWithDefault(ctx *types.Context, v *int) (*ResultEchoWithDefault, error) {
val := -1
if v != nil {
val = *v
}
return &ResultEchoWithDefault{val}, nil
}
func TestMain(m *testing.M) {
setup()
code := m.Run()
@@ -199,26 +212,47 @@ func echoDataBytesViaHTTP(cl client.Caller, bytes tmbytes.HexBytes) (tmbytes.Hex
return result.Value, nil
}
func echoWithDefaultViaHTTP(cl client.Caller, v *int) (int, error) {
params := map[string]interface{}{}
if v != nil {
params["arg"] = *v
}
result := new(ResultEchoWithDefault)
if _, err := cl.Call(ctx, "echo_default", params, result); err != nil {
return 0, err
}
return result.Value, nil
}
func testWithHTTPClient(t *testing.T, cl client.HTTPClient) {
val := testVal
got, err := echoViaHTTP(cl, val)
require.Nil(t, err)
require.NoError(t, err)
assert.Equal(t, got, val)
val2 := randBytes(t)
got2, err := echoBytesViaHTTP(cl, val2)
require.Nil(t, err)
require.NoError(t, err)
assert.Equal(t, got2, val2)
val3 := tmbytes.HexBytes(randBytes(t))
got3, err := echoDataBytesViaHTTP(cl, val3)
require.Nil(t, err)
require.NoError(t, err)
assert.Equal(t, got3, val3)
val4 := tmrand.Intn(10000)
got4, err := echoIntViaHTTP(cl, val4)
require.Nil(t, err)
require.NoError(t, err)
assert.Equal(t, got4, val4)
got5, err := echoWithDefaultViaHTTP(cl, nil)
require.NoError(t, err)
assert.Equal(t, got5, -1)
val6 := tmrand.Intn(10000)
got6, err := echoWithDefaultViaHTTP(cl, &val6)
require.NoError(t, err)
assert.Equal(t, got6, val6)
}
func echoViaWS(cl *client.WSClient, val string) (string, error) {
@@ -233,7 +267,6 @@ func echoViaWS(cl *client.WSClient, val string) (string, error) {
msg := <-cl.ResponsesCh
if msg.Error != nil {
return "", err
}
result := new(ResultEcho)
err = json.Unmarshal(msg.Result, result)
@@ -255,7 +288,6 @@ func echoBytesViaWS(cl *client.WSClient, bytes []byte) ([]byte, error) {
msg := <-cl.ResponsesCh
if msg.Error != nil {
return []byte{}, msg.Error
}
result := new(ResultEchoBytes)
err = json.Unmarshal(msg.Result, result)
@@ -399,6 +431,74 @@ func TestWSClientPingPong(t *testing.T) {
time.Sleep(6 * time.Second)
}
func TestJSONRPCCaching(t *testing.T) {
httpAddr := strings.Replace(tcpAddr, "tcp://", "http://", 1)
cl, err := client.DefaultHTTPClient(httpAddr)
require.NoError(t, err)
// Not supplying the arg should result in not caching
params := make(map[string]interface{})
req, err := types.MapToRequest(types.JSONRPCIntID(1000), "echo_default", params)
require.NoError(t, err)
res1, err := rawJSONRPCRequest(t, cl, httpAddr, req)
defer func() { _ = res1.Body.Close() }()
require.NoError(t, err)
assert.Equal(t, "", res1.Header.Get("Cache-control"))
// Supplying the arg should result in caching
params["arg"] = tmrand.Intn(10000)
req, err = types.MapToRequest(types.JSONRPCIntID(1001), "echo_default", params)
require.NoError(t, err)
res2, err := rawJSONRPCRequest(t, cl, httpAddr, req)
defer func() { _ = res2.Body.Close() }()
require.NoError(t, err)
assert.Equal(t, "public, max-age=86400", res2.Header.Get("Cache-control"))
}
func rawJSONRPCRequest(t *testing.T, cl *http.Client, url string, req interface{}) (*http.Response, error) {
reqBytes, err := json.Marshal(req)
require.NoError(t, err)
reqBuf := bytes.NewBuffer(reqBytes)
httpReq, err := http.NewRequest(http.MethodPost, url, reqBuf)
require.NoError(t, err)
httpReq.Header.Set("Content-type", "application/json")
return cl.Do(httpReq)
}
func TestURICaching(t *testing.T) {
httpAddr := strings.Replace(tcpAddr, "tcp://", "http://", 1)
cl, err := client.DefaultHTTPClient(httpAddr)
require.NoError(t, err)
// Not supplying the arg should result in not caching
args := url.Values{}
res1, err := rawURIRequest(t, cl, httpAddr+"/echo_default", args)
defer func() { _ = res1.Body.Close() }()
require.NoError(t, err)
assert.Equal(t, "", res1.Header.Get("Cache-control"))
// Supplying the arg should result in caching
args.Set("arg", fmt.Sprintf("%d", tmrand.Intn(10000)))
res2, err := rawURIRequest(t, cl, httpAddr+"/echo_default", args)
defer func() { _ = res2.Body.Close() }()
require.NoError(t, err)
assert.Equal(t, "public, max-age=86400", res2.Header.Get("Cache-control"))
}
func rawURIRequest(t *testing.T, cl *http.Client, url string, args url.Values) (*http.Response, error) {
req, err := http.NewRequest(http.MethodPost, url, strings.NewReader(args.Encode()))
require.NoError(t, err)
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
return cl.Do(req)
}
func randBytes(t *testing.T) []byte {
n := tmrand.Intn(10) + 2
buf := make([]byte, n)

View File

@@ -55,6 +55,11 @@ func makeJSONRPCHandler(funcMap map[string]*RPCFunc, logger log.Logger) http.Han
requests = []types.RPCRequest{request}
}
// Set the default response cache to true unless
// 1. Any RPC request error.
// 2. Any RPC request doesn't allow to be cached.
// 3. Any RPC request has the height argument and the value is 0 (the default).
cache := true
for _, request := range requests {
request := request
@@ -72,11 +77,13 @@ func makeJSONRPCHandler(funcMap map[string]*RPCFunc, logger log.Logger) http.Han
responses,
types.RPCInvalidRequestError(request.ID, fmt.Errorf("path %s is invalid", r.URL.Path)),
)
cache = false
continue
}
rpcFunc, ok := funcMap[request.Method]
if !ok || rpcFunc.ws {
if !ok || (rpcFunc.ws) {
responses = append(responses, types.RPCMethodNotFoundError(request.ID))
cache = false
continue
}
ctx := &types.Context{JSONReq: &request, HTTPReq: r}
@@ -88,11 +95,16 @@ func makeJSONRPCHandler(funcMap map[string]*RPCFunc, logger log.Logger) http.Han
responses,
types.RPCInvalidParamsError(request.ID, fmt.Errorf("error converting json params to arguments: %w", err)),
)
cache = false
continue
}
args = append(args, fnArgs...)
}
if cache && !rpcFunc.cacheableWithArgs(args) {
cache = false
}
returns := rpcFunc.f.Call(args)
result, err := unreflectResult(returns)
if err != nil {
@@ -103,7 +115,13 @@ func makeJSONRPCHandler(funcMap map[string]*RPCFunc, logger log.Logger) http.Han
}
if len(responses) > 0 {
if wErr := WriteRPCResponseHTTP(w, responses...); wErr != nil {
var wErr error
if cache {
wErr = WriteCacheableRPCResponseHTTP(w, responses...)
} else {
wErr = WriteRPCResponseHTTP(w, responses...)
}
if wErr != nil {
logger.Error("failed to write responses", "res", responses, "err", wErr)
}
}

View File

@@ -19,6 +19,7 @@ import (
func testMux() *http.ServeMux {
funcMap := map[string]*RPCFunc{
"c": NewRPCFunc(func(ctx *types.Context, s string, i int) (string, error) { return "foo", nil }, "s,i"),
"block": NewRPCFunc(func(ctx *types.Context, h int) (string, error) { return "block", nil }, "height", Cacheable("height")),
}
mux := http.NewServeMux()
buf := new(bytes.Buffer)
@@ -227,3 +228,52 @@ func TestUnknownRPCPath(t *testing.T) {
require.Equal(t, http.StatusNotFound, res.StatusCode, "should always return 404")
res.Body.Close()
}
func TestRPCResponseCache(t *testing.T) {
mux := testMux()
body := strings.NewReader(`{"jsonrpc": "2.0","method":"block","id": 0, "params": ["1"]}`)
req, _ := http.NewRequest("Get", "http://localhost/", body)
rec := httptest.NewRecorder()
mux.ServeHTTP(rec, req)
res := rec.Result()
// Always expecting back a JSONRPCResponse
require.True(t, statusOK(res.StatusCode), "should always return 2XX")
require.Equal(t, "public, max-age=86400", res.Header.Get("Cache-control"))
_, err := io.ReadAll(res.Body)
res.Body.Close()
require.Nil(t, err, "reading from the body should not give back an error")
// send a request with default height.
body = strings.NewReader(`{"jsonrpc": "2.0","method":"block","id": 0, "params": ["0"]}`)
req, _ = http.NewRequest("Get", "http://localhost/", body)
rec = httptest.NewRecorder()
mux.ServeHTTP(rec, req)
res = rec.Result()
// Always expecting back a JSONRPCResponse
require.True(t, statusOK(res.StatusCode), "should always return 2XX")
require.Equal(t, "", res.Header.Get("Cache-control"))
_, err = io.ReadAll(res.Body)
res.Body.Close()
require.Nil(t, err, "reading from the body should not give back an error")
// send a request with default height, but as empty set of parameters.
body = strings.NewReader(`{"jsonrpc": "2.0","method":"block","id": 0, "params": []}`)
req, _ = http.NewRequest("Get", "http://localhost/", body)
rec = httptest.NewRecorder()
mux.ServeHTTP(rec, req)
res = rec.Result()
// Always expecting back a JSONRPCResponse
require.True(t, statusOK(res.StatusCode), "should always return 2XX")
require.Equal(t, "", res.Header.Get("Cache-control"))
_, err = io.ReadAll(res.Body)
res.Body.Close()
require.Nil(t, err, "reading from the body should not give back an error")
}

View File

@@ -117,6 +117,22 @@ func WriteRPCResponseHTTPError(
// WriteRPCResponseHTTP marshals res as JSON (with indent) and writes it to w.
func WriteRPCResponseHTTP(w http.ResponseWriter, res ...types.RPCResponse) error {
return writeRPCResponseHTTP(w, []httpHeader{}, res...)
}
// WriteCacheableRPCResponseHTTP marshals res as JSON (with indent) and writes
// it to w. Adds cache-control to the response header and sets the expiry to
// one day.
func WriteCacheableRPCResponseHTTP(w http.ResponseWriter, res ...types.RPCResponse) error {
return writeRPCResponseHTTP(w, []httpHeader{{"Cache-Control", "public, max-age=86400"}}, res...)
}
type httpHeader struct {
name string
value string
}
func writeRPCResponseHTTP(w http.ResponseWriter, headers []httpHeader, res ...types.RPCResponse) error {
var v interface{}
if len(res) == 1 {
v = res[0]
@@ -129,6 +145,9 @@ func WriteRPCResponseHTTP(w http.ResponseWriter, res ...types.RPCResponse) error
return fmt.Errorf("json marshal: %w", err)
}
w.Header().Set("Content-Type", "application/json")
for _, header := range headers {
w.Header().Set(header.name, header.value)
}
w.WriteHeader(200)
_, err = w.Write(jsonBytes)
return err
@@ -166,7 +185,6 @@ func RecoverAndLogHandler(handler http.Handler, logger log.Logger) http.Handler
// Without this, Chrome & Firefox were retrying aborted ajax requests,
// at least to my localhost.
if e := recover(); e != nil {
// If RPCResponse
if res, ok := e.(types.RPCResponse); ok {
if wErr := WriteRPCResponseHTTP(rww, res); wErr != nil {

View File

@@ -112,7 +112,7 @@ func TestWriteRPCResponseHTTP(t *testing.T) {
// one argument
w := httptest.NewRecorder()
err := WriteRPCResponseHTTP(w, types.NewRPCSuccessResponse(id, &sampleResult{"hello"}))
err := WriteCacheableRPCResponseHTTP(w, types.NewRPCSuccessResponse(id, &sampleResult{"hello"}))
require.NoError(t, err)
resp := w.Result()
body, err := io.ReadAll(resp.Body)
@@ -120,6 +120,7 @@ func TestWriteRPCResponseHTTP(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, 200, resp.StatusCode)
assert.Equal(t, "application/json", resp.Header.Get("Content-Type"))
assert.Equal(t, "public, max-age=86400", resp.Header.Get("Cache-control"))
assert.Equal(t, `{
"jsonrpc": "2.0",
"id": -1,

View File

@@ -63,7 +63,14 @@ func makeHTTPHandler(rpcFunc *RPCFunc, logger log.Logger) func(http.ResponseWrit
}
return
}
if err := WriteRPCResponseHTTP(w, types.NewRPCSuccessResponse(dummyID, result)); err != nil {
resp := types.NewRPCSuccessResponse(dummyID, result)
if rpcFunc.cacheableWithArgs(args) {
err = WriteCacheableRPCResponseHTTP(w, resp)
} else {
err = WriteRPCResponseHTTP(w, resp)
}
if err != nil {
logger.Error("failed to write response", "res", result, "err", err)
return
}

View File

@@ -23,7 +23,30 @@ func RegisterRPCFuncs(mux *http.ServeMux, funcMap map[string]*RPCFunc, logger lo
mux.HandleFunc("/", handleInvalidJSONRPCPaths(makeJSONRPCHandler(funcMap, logger)))
}
// Function introspection
type Option func(*RPCFunc)
// Cacheable enables returning a cache control header from RPC functions to
// which it is applied.
//
// `noCacheDefArgs` is a list of argument names that, if omitted or set to
// their defaults when calling the RPC function, will skip the response
// caching.
func Cacheable(noCacheDefArgs ...string) Option {
return func(r *RPCFunc) {
r.cacheable = true
r.noCacheDefArgs = make(map[string]interface{})
for _, arg := range noCacheDefArgs {
r.noCacheDefArgs[arg] = nil
}
}
}
// Ws enables WebSocket communication.
func Ws() Option {
return func(r *RPCFunc) {
r.ws = true
}
}
// RPCFunc contains the introspected type information for a function
type RPCFunc struct {
@@ -31,32 +54,65 @@ type RPCFunc struct {
args []reflect.Type // type of each function arg
returns []reflect.Type // type of each return arg
argNames []string // name of each argument
ws bool // websocket only
cacheable bool // enable cache control
ws bool // enable websocket communication
noCacheDefArgs map[string]interface{} // a lookup table of args that, if not supplied or are set to default values, cause us to not cache
}
// NewRPCFunc wraps a function for introspection.
// f is the function, args are comma separated argument names
func NewRPCFunc(f interface{}, args string) *RPCFunc {
return newRPCFunc(f, args, false)
func NewRPCFunc(f interface{}, args string, options ...Option) *RPCFunc {
return newRPCFunc(f, args, options...)
}
// NewWSRPCFunc wraps a function for introspection and use in the websockets.
func NewWSRPCFunc(f interface{}, args string) *RPCFunc {
return newRPCFunc(f, args, true)
func NewWSRPCFunc(f interface{}, args string, options ...Option) *RPCFunc {
options = append(options, Ws())
return newRPCFunc(f, args, options...)
}
func newRPCFunc(f interface{}, args string, ws bool) *RPCFunc {
// cacheableWithArgs returns whether or not a call to this function is cacheable,
// given the specified arguments.
func (f *RPCFunc) cacheableWithArgs(args []reflect.Value) bool {
if !f.cacheable {
return false
}
// Skip the context variable common to all RPC functions
for i := 1; i < len(f.args); i++ {
// f.argNames does not include the context variable
argName := f.argNames[i-1]
if _, hasDefault := f.noCacheDefArgs[argName]; hasDefault {
// Argument with default value was not supplied
if i >= len(args) {
return false
}
// Argument with default value is set to its zero value
if args[i].IsZero() {
return false
}
}
}
return true
}
func newRPCFunc(f interface{}, args string, options ...Option) *RPCFunc {
var argNames []string
if args != "" {
argNames = strings.Split(args, ",")
}
return &RPCFunc{
r := &RPCFunc{
f: reflect.ValueOf(f),
args: funcArgTypes(f),
returns: funcReturnTypes(f),
argNames: argNames,
ws: ws,
}
for _, opt := range options {
opt(r)
}
return r
}
// return a function's argument types

View File

@@ -216,6 +216,9 @@ paths:
Please refer to
https://docs.tendermint.com/v0.34/tendermint-core/using-tendermint.html#formatting
for formatting/encoding rules.
Upon success, the `Cache-Control` header will be set with the default
maximum age.
parameters:
- in: query
name: tx
@@ -623,9 +626,12 @@ paths:
tags:
- Info
description: |
Get block headers for minHeight <= height maxHeight.
Get block headers for minHeight <= height <= maxHeight.
At most 20 items will be returned.
Upon success, the `Cache-Control` header will be set with the default
maximum age.
responses:
"200":
description: Block headers, returned in descending order (highest first).
@@ -655,6 +661,9 @@ paths:
- Info
description: |
Get Block.
If the `height` field is set to a non-default value, upon success, the
`Cache-Control` header will be set with the default maximum age.
responses:
"200":
description: Block informations.
@@ -684,6 +693,9 @@ paths:
- Info
description: |
Get Block By Hash.
Upon success, the `Cache-Control` header will be set with the default
maximum age.
responses:
"200":
description: Block informations.
@@ -704,7 +716,7 @@ paths:
parameters:
- in: query
name: height
description: height to return. If no height is provided, it will fetch informations regarding the latest block.
description: height to return. If no height is provided, it will fetch information regarding the latest block.
schema:
type: integer
default: 0
@@ -714,6 +726,9 @@ paths:
description: |
Get block_results. When the `discard_abci_responses` storage flag is
enabled, this endpoint will return an error.
If the `height` field is set to a non-default value, upon success, the
`Cache-Control` header will be set with the default maximum age.
responses:
"200":
description: Block results.
@@ -743,6 +758,9 @@ paths:
- Info
description: |
Get Commit.
If the `height` field is set to a non-default value, upon success, the
`Cache-Control` header will be set with the default maximum age.
responses:
"200":
description: |
@@ -791,6 +809,9 @@ paths:
- Info
description: |
Get Validators. Validators are sorted by voting power.
If the `height` field is set to a non-default value, upon success, the
`Cache-Control` header will be set with the default maximum age.
responses:
"200":
description: Commit results.
@@ -812,6 +833,9 @@ paths:
- Info
description: |
Get genesis.
Upon success, the `Cache-Control` header will be set with the default
maximum age.
responses:
"200":
description: Genesis results.
@@ -890,6 +914,9 @@ paths:
- Info
description: |
Get consensus parameters.
If the `height` field is set to a non-default value, upon success, the
`Cache-Control` header will be set with the default maximum age.
responses:
"200":
description: consensus parameters results.
@@ -1080,14 +1107,14 @@ paths:
parameters:
- in: query
name: hash
description: transaction Hash to retrive
description: hash of transaction to retrieve
required: true
schema:
type: string
example: "0xD70952032620CC4E2737EB8AC379806359D8E0B17B0488F627997A0B043ABDED"
- in: query
name: prove
description: Include proofs of the transactions inclusion in the block
description: Include proofs of the transaction's inclusion in the block
required: false
schema:
type: boolean
@@ -1096,7 +1123,10 @@ paths:
tags:
- Info
description: |
Get a trasasction
Get a transaction
Upon success, the `Cache-Control` header will be set with the default
maximum age.
responses:
"200":
description: Get a transaction`
@@ -1112,12 +1142,15 @@ paths:
$ref: "#/components/schemas/ErrorResponse"
/abci_info:
get:
summary: Get some info about the application.
summary: Get info about the application.
operationId: abci_info
tags:
- ABCI
description: |
Get some info about the application.
Get info about the application.
Upon success, the `Cache-Control` header will be set with the default
maximum age.
responses:
"200":
description: Get some info about the application.