Add gRPC server & reflection (#6463)

* Add gRPC proxy

* Make GRPC disabled by default

* WIP on integration tests

* WIP on integration tests

* Start setting up in process tests

* Start setting up in process tests

* Make it compile

* Add start server to network util

* Add Println

* Use go routine

* Fix scopelint

* Move to proxy_test

* Add response type cache

* Remove proxy

* Tweaks

* Use channel to handle error

* Use error chan

* Update server/start.go

Co-authored-by: Federico Kunze <31522760+fedekunze@users.noreply.github.com>

* Use %w

* Add sdk.Context

* Add comments

* Fix lint

* Add header and tests

* Address comments

* Factorize some code

* Fix lint

* Add height and prove in req metadata

* Add reflection test

* Fix lint

* Put grpc test in server/grpc

* Update baseapp/grpcserver.go

* Update baseapp/grpcserver.go

* Remove proof header

Co-authored-by: Amaury Martiny <amaury.martiny@protonmail.com>
Co-authored-by: Federico Kunze <31522760+fedekunze@users.noreply.github.com>
Co-authored-by: Alexander Bezobchuk <alexanderbez@users.noreply.github.com>
Co-authored-by: SaReN <sahithnarahari@gmail.com>
This commit is contained in:
Aaron Craelius
2020-07-27 17:57:15 +00:00
committed by GitHub
co-authored by Federico Kunze Amaury Martiny Alexander Bezobchuk SaReN
parent 20488b4f8e
commit e9534b0935
17 changed files with 384 additions and 70 deletions
+18
View File
@@ -75,6 +75,15 @@ type APIConfig struct {
// Ref: https://github.com/cosmos/cosmos-sdk/issues/6420
}
// GRPCConfig defines configuration for the gRPC server.
type GRPCConfig struct {
// Enable defines if the gRPC server should be enabled.
Enable bool `mapstructure:"enable"`
// Address defines the API server to listen on
Address string `mapstructure:"address"`
}
// Config defines the server's top level configuration
type Config struct {
BaseConfig `mapstructure:",squash"`
@@ -82,6 +91,7 @@ type Config struct {
// Telemetry defines the application telemetry configuration
Telemetry telemetry.Config `mapstructure:"telemetry"`
API APIConfig `mapstructure:"api"`
GRPC GRPCConfig `mapstructure:"grpc"`
}
// SetMinGasPrices sets the validator's minimum gas prices.
@@ -134,6 +144,10 @@ func DefaultConfig() *Config {
RPCReadTimeout: 10,
RPCMaxBodyBytes: 1000000,
},
GRPC: GRPCConfig{
Enable: false,
Address: "0.0.0.0:9090",
},
}
}
@@ -177,5 +191,9 @@ func GetConfig(v *viper.Viper) Config {
RPCMaxBodyBytes: v.GetUint("api.rpc-max-body-bytes"),
EnableUnsafeCORS: v.GetBool("api.enabled-unsafe-cors"),
},
GRPC: GRPCConfig{
Enable: v.GetBool("grpc.enable"),
Address: v.GetString("grpc.address"),
},
}
}
-35
View File
@@ -1,50 +1,15 @@
package server
import (
"encoding/json"
"io"
"os"
"path/filepath"
abci "github.com/tendermint/tendermint/abci/types"
"github.com/tendermint/tendermint/libs/log"
tmtypes "github.com/tendermint/tendermint/types"
dbm "github.com/tendermint/tm-db"
"github.com/cosmos/cosmos-sdk/server/api"
sdk "github.com/cosmos/cosmos-sdk/types"
)
type (
// AppOptions defines an interface that is passed into an application
// constructor, typically used to set BaseApp options that are either supplied
// via config file or through CLI arguments/flags. The underlying implementation
// is defined by the server package and is typically implemented via a Viper
// literal defined on the server Context. Note, casting Get calls may not yield
// the expected types and could result in type assertion errors. It is recommend
// to either use the cast package or perform manual conversion for safety.
AppOptions interface {
Get(string) interface{}
}
// Application defines an application interface that wraps abci.Application.
// The interface defines the necessary contracts to be implemented in order
// to fully bootstrap and start an application.
Application interface {
abci.Application
RegisterAPIRoutes(*api.Server)
}
// AppCreator is a function that allows us to lazily initialize an
// application using various configurations.
AppCreator func(log.Logger, dbm.DB, io.Writer, AppOptions) Application
// AppExporter is a function that dumps all app state to
// JSON-serializable structure and returns the current validator set.
AppExporter func(log.Logger, dbm.DB, io.Writer, int64, bool, []string) (json.RawMessage, []tmtypes.GenesisValidator, *abci.ConsensusParams, error)
)
func openDB(rootDir string) (dbm.DB, error) {
dataDir := filepath.Join(rootDir, "data")
db, err := sdk.NewLevelDB("application", dataDir)
+2 -1
View File
@@ -13,6 +13,7 @@ import (
"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/client/flags"
"github.com/cosmos/cosmos-sdk/codec"
"github.com/cosmos/cosmos-sdk/server/types"
sdk "github.com/cosmos/cosmos-sdk/types"
)
@@ -23,7 +24,7 @@ const (
)
// ExportCmd dumps app state to JSON.
func ExportCmd(appExporter AppExporter, defaultNodeHome string) *cobra.Command {
func ExportCmd(appExporter types.AppExporter, defaultNodeHome string) *cobra.Command {
cmd := &cobra.Command{
Use: "export",
Short: "Export state to JSON",
+47
View File
@@ -0,0 +1,47 @@
package grpc
import (
"fmt"
"net"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/reflection"
"github.com/cosmos/cosmos-sdk/server/types"
)
const (
// GRPCBlockHeightHeader is the gRPC header for block height.
GRPCBlockHeightHeader = "x-cosmos-block-height"
)
// StartGRPCServer starts a gRPC server on the given address.
func StartGRPCServer(app types.Application, address string) (*grpc.Server, error) {
grpcSrv := grpc.NewServer()
app.RegisterGRPCServer(grpcSrv)
// Reflection allows external clients to see what services and methods
// the gRPC server exposes.
reflection.Register(grpcSrv)
listener, err := net.Listen("tcp", address)
if err != nil {
return nil, err
}
errCh := make(chan error)
go func() {
err = grpcSrv.Serve(listener)
if err != nil {
errCh <- fmt.Errorf("failed to serve: %w", err)
}
}()
select {
case err := <-errCh:
return nil, err
case <-time.After(5 * time.Second): // assume server started successfully
return grpcSrv, nil
}
}
+101
View File
@@ -0,0 +1,101 @@
package grpc_test
import (
"context"
"fmt"
"testing"
"github.com/stretchr/testify/suite"
"google.golang.org/grpc"
"google.golang.org/grpc/metadata"
servergrpc "github.com/cosmos/cosmos-sdk/server/grpc"
"github.com/cosmos/cosmos-sdk/testutil/network"
"github.com/cosmos/cosmos-sdk/testutil/testdata"
sdk "github.com/cosmos/cosmos-sdk/types"
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
rpb "google.golang.org/grpc/reflection/grpc_reflection_v1alpha"
)
type IntegrationTestSuite struct {
suite.Suite
network *network.Network
}
func (s *IntegrationTestSuite) SetupSuite() {
s.T().Log("setting up integration test suite")
s.network = network.New(s.T(), network.DefaultConfig())
s.Require().NotNil(s.network)
_, err := s.network.WaitForHeight(2)
s.Require().NoError(err)
}
func (s *IntegrationTestSuite) TearDownSuite() {
s.T().Log("tearing down integration test suite")
s.network.Cleanup()
}
func (s *IntegrationTestSuite) TestGRPC() {
val0 := s.network.Validators[0]
conn, err := grpc.Dial(
val0.AppConfig.GRPC.Address,
grpc.WithInsecure(), // Or else we get "no transport security set"
)
s.Require().NoError(err)
// gRPC query to test service should work
testClient := testdata.NewTestServiceClient(conn)
testRes, err := testClient.Echo(context.Background(), &testdata.EchoRequest{Message: "hello"})
s.Require().NoError(err)
s.Require().Equal("hello", testRes.Message)
// gRPC query to bank service should work
denom := fmt.Sprintf("%stoken", val0.Moniker)
bankClient := banktypes.NewQueryClient(conn)
var header metadata.MD
bankRes, err := bankClient.Balance(
context.Background(),
&banktypes.QueryBalanceRequest{Address: val0.Address, Denom: denom},
grpc.Header(&header), // Also fetch grpc header
)
s.Require().NoError(err)
s.Require().Equal(
sdk.NewCoin(denom, s.network.Config.AccountTokens),
*bankRes.GetBalance(),
)
blockHeight := header.Get(servergrpc.GRPCBlockHeightHeader)
s.Require().Equal([]string{"2"}, blockHeight)
// Request metadata should work
bankRes, err = bankClient.Balance(
metadata.AppendToOutgoingContext(context.Background(), servergrpc.GRPCBlockHeightHeader, "1"), // Add metadata to request
&banktypes.QueryBalanceRequest{Address: val0.Address, Denom: denom},
grpc.Header(&header),
)
blockHeight = header.Get(servergrpc.GRPCBlockHeightHeader)
s.Require().Equal([]string{"1"}, blockHeight)
// Test server reflection
reflectClient := rpb.NewServerReflectionClient(conn)
stream, err := reflectClient.ServerReflectionInfo(context.Background(), grpc.WaitForReady(true))
s.Require().NoError(err)
s.Require().NoError(stream.Send(&rpb.ServerReflectionRequest{
MessageRequest: &rpb.ServerReflectionRequest_ListServices{},
}))
res, err := stream.Recv()
s.Require().NoError(err)
services := res.GetListServicesResponse().Service
servicesMap := make(map[string]bool)
for _, s := range services {
servicesMap[s.Name] = true
}
// Make sure the following services are present
s.Require().True(servicesMap["cosmos.bank.Query"])
}
func TestIntegrationTestSuite(t *testing.T) {
suite.Run(t, new(IntegrationTestSuite))
}
+7 -6
View File
@@ -6,22 +6,23 @@ import (
"github.com/spf13/cast"
"github.com/cosmos/cosmos-sdk/server/types"
"github.com/cosmos/cosmos-sdk/store"
"github.com/cosmos/cosmos-sdk/store/types"
storetypes "github.com/cosmos/cosmos-sdk/store/types"
)
// GetPruningOptionsFromFlags parses command flags and returns the correct
// PruningOptions. If a pruning strategy is provided, that will be parsed and
// returned, otherwise, it is assumed custom pruning options are provided.
func GetPruningOptionsFromFlags(appOpts AppOptions) (types.PruningOptions, error) {
func GetPruningOptionsFromFlags(appOpts types.AppOptions) (storetypes.PruningOptions, error) {
strategy := strings.ToLower(cast.ToString(appOpts.Get(FlagPruning)))
switch strategy {
case types.PruningOptionDefault, types.PruningOptionNothing, types.PruningOptionEverything:
return types.NewPruningOptionsFromString(strategy), nil
case storetypes.PruningOptionDefault, storetypes.PruningOptionNothing, storetypes.PruningOptionEverything:
return storetypes.NewPruningOptionsFromString(strategy), nil
case types.PruningOptionCustom:
opts := types.NewPruningOptions(
case storetypes.PruningOptionCustom:
opts := storetypes.NewPruningOptions(
cast.ToUint64(appOpts.Get(FlagPruningKeepRecent)),
cast.ToUint64(appOpts.Get(FlagPruningKeepEvery)),
cast.ToUint64(appOpts.Get(FlagPruningInterval)),
+18 -3
View File
@@ -17,12 +17,15 @@ import (
pvm "github.com/tendermint/tendermint/privval"
"github.com/tendermint/tendermint/proxy"
"github.com/tendermint/tendermint/rpc/client/local"
"google.golang.org/grpc"
"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/client/flags"
"github.com/cosmos/cosmos-sdk/codec"
"github.com/cosmos/cosmos-sdk/server/api"
"github.com/cosmos/cosmos-sdk/server/config"
servergrpc "github.com/cosmos/cosmos-sdk/server/grpc"
"github.com/cosmos/cosmos-sdk/server/types"
storetypes "github.com/cosmos/cosmos-sdk/store/types"
)
@@ -49,7 +52,7 @@ const (
// StartCmd runs the service passed in, either stand-alone or in-process with
// Tendermint.
func StartCmd(appCreator AppCreator, defaultNodeHome string) *cobra.Command {
func StartCmd(appCreator types.AppCreator, defaultNodeHome string) *cobra.Command {
cmd := &cobra.Command{
Use: "start",
Short: "Run the full node",
@@ -125,7 +128,7 @@ which accepts a path for the resulting pprof file.
return cmd
}
func startStandAlone(ctx *Context, appCreator AppCreator) error {
func startStandAlone(ctx *Context, appCreator types.AppCreator) error {
addr := ctx.Viper.GetString(flagAddress)
transport := ctx.Viper.GetString(flagTransport)
home := ctx.Viper.GetString(flags.FlagHome)
@@ -165,7 +168,7 @@ func startStandAlone(ctx *Context, appCreator AppCreator) error {
select {}
}
func startInProcess(ctx *Context, cdc codec.JSONMarshaler, appCreator AppCreator) error {
func startInProcess(ctx *Context, cdc codec.JSONMarshaler, appCreator types.AppCreator) error {
cfg := ctx.Config
home := cfg.RootDir
@@ -239,6 +242,14 @@ func startInProcess(ctx *Context, cdc codec.JSONMarshaler, appCreator AppCreator
}
}
var grpcSrv *grpc.Server
if config.GRPC.Enable {
grpcSrv, err = servergrpc.StartGRPCServer(app, config.GRPC.Address)
if err != nil {
return err
}
}
var cpuProfileCleanup func()
if cpuProfile := ctx.Viper.GetString(flagCPUProfile); cpuProfile != "" {
@@ -272,6 +283,10 @@ func startInProcess(ctx *Context, cdc codec.JSONMarshaler, appCreator AppCreator
_ = apiSrv.Close()
}
if grpcSrv != nil {
grpcSrv.Stop()
}
ctx.Logger.Info("exiting...")
})
+48
View File
@@ -0,0 +1,48 @@
package types
import (
"encoding/json"
"io"
"github.com/gogo/protobuf/grpc"
abci "github.com/tendermint/tendermint/abci/types"
"github.com/tendermint/tendermint/libs/log"
tmtypes "github.com/tendermint/tendermint/types"
dbm "github.com/tendermint/tm-db"
"github.com/cosmos/cosmos-sdk/server/api"
)
type (
// AppOptions defines an interface that is passed into an application
// constructor, typically used to set BaseApp options that are either supplied
// via config file or through CLI arguments/flags. The underlying implementation
// is defined by the server package and is typically implemented via a Viper
// literal defined on the server Context. Note, casting Get calls may not yield
// the expected types and could result in type assertion errors. It is recommend
// to either use the cast package or perform manual conversion for safety.
AppOptions interface {
Get(string) interface{}
}
// Application defines an application interface that wraps abci.Application.
// The interface defines the necessary contracts to be implemented in order
// to fully bootstrap and start an application.
Application interface {
abci.Application
RegisterAPIRoutes(*api.Server)
// RegisterGRPCServer registers gRPC services directly with the gRPC
// server.
RegisterGRPCServer(grpc.Server)
}
// AppCreator is a function that allows us to lazily initialize an
// application using various configurations.
AppCreator func(log.Logger, dbm.DB, io.Writer, AppOptions) Application
// AppExporter is a function that dumps all app state to
// JSON-serializable structure and returns the current validator set.
AppExporter func(log.Logger, dbm.DB, io.Writer, int64, bool, []string) (json.RawMessage, []tmtypes.GenesisValidator, *abci.ConsensusParams, error)
)
+2 -1
View File
@@ -23,6 +23,7 @@ import (
"github.com/cosmos/cosmos-sdk/client/flags"
"github.com/cosmos/cosmos-sdk/codec"
"github.com/cosmos/cosmos-sdk/server/config"
"github.com/cosmos/cosmos-sdk/server/types"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/version"
)
@@ -167,7 +168,7 @@ func interceptConfigs(ctx *Context, rootViper *viper.Viper) (*tmcfg.Config, erro
}
// add server commands
func AddCommands(rootCmd *cobra.Command, appCreator AppCreator, appExport AppExporter) {
func AddCommands(rootCmd *cobra.Command, appCreator types.AppCreator, appExport types.AppExporter) {
tendermintCmd := &cobra.Command{
Use: "tendermint",
Short: "Tendermint subcommands",