From 1a306fde6efaaf32c7a92aab80fc560b0fa18775 Mon Sep 17 00:00:00 2001 From: Philip Offtermatt <57488781+p-offtermatt@users.noreply.github.com> Date: Thu, 19 Oct 2023 10:45:42 +0200 Subject: [PATCH] feat(server): start grpc server and register services when starting in standalone mode (v0.47) (#18109) Co-authored-by: Marko --- CHANGELOG.md | 4 + docs/docs/run-node/01-run-node.md | 7 + server/start.go | 212 +++++++++++++++++++----------- 3 files changed, 148 insertions(+), 75 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3bd9c4f94e6..b496daa0309 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,10 @@ Ref: https://keepachangelog.com/en/1.0.0/ ## [Unreleased] +### Features + +* (server) [#18110](https://github.com/cosmos/cosmos-sdk/pull/18110) Start gRPC & API server in standalone mode + ### Improvements * (baseapp) [#17954](https://github.com/cosmos/cosmos-sdk/issues/17954) Add `Mempool()` method on `BaseApp` to allow access to the mempool. diff --git a/docs/docs/run-node/01-run-node.md b/docs/docs/run-node/01-run-node.md index 1b28043e017..ec67cdcbaaf 100644 --- a/docs/docs/run-node/01-run-node.md +++ b/docs/docs/run-node/01-run-node.md @@ -136,6 +136,13 @@ The previous command allow you to run a single node. This is enough for the next The naive way would be to run the same commands again in separate terminal windows. This is possible, however in the Cosmos SDK, we leverage the power of [Docker Compose](https://docs.docker.com/compose/) to run a localnet. If you need inspiration on how to set up your own localnet with Docker Compose, you can have a look at the Cosmos SDK's [`docker-compose.yml`](https://github.com/cosmos/cosmos-sdk/blob/v0.47.0-rc1/docker-compose.yml). +### Standalone App/CometBFT + +By default, the Cosmos SDK runs CometBFT in-process with the application +If you want to run the application and CometBFT in separate processes, +start the application with the `--with-comet=false` flag +and set `rpc.laddr` in `config.toml` to the CometBFT node's RPC address. + ## Logging Logging provides a way to see what is going on with a node. By default the info level is set. This is a global level and all info logs will be outputted to the terminal. If you would like to filter specific logs to the terminal instead of all, then setting `module:log_level` is how this can work. diff --git a/server/start.go b/server/start.go index 061b736cf6a..31deb30745d 100644 --- a/server/start.go +++ b/server/start.go @@ -35,6 +35,8 @@ import ( "github.com/cosmos/cosmos-sdk/telemetry" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/mempool" + + rpchttp "github.com/cometbft/cometbft/rpc/client/http" ) const ( @@ -142,7 +144,7 @@ is performed. Note, when enabled, gRPC will also be automatically enabled. if !withTM { serverCtx.Logger.Info("starting ABCI without Tendermint") return wrapCPUProfile(serverCtx, func() error { - return startStandAlone(serverCtx, appCreator) + return startStandAlone(serverCtx, clientCtx, appCreator) }) } @@ -206,7 +208,7 @@ is performed. Note, when enabled, gRPC will also be automatically enabled. return cmd } -func startStandAlone(ctx *Context, appCreator types.AppCreator) error { +func startStandAlone(ctx *Context, clientCtx client.Context, appCreator types.AppCreator) error { addr := ctx.Viper.GetString(flagAddress) transport := ctx.Viper.GetString(flagTransport) home := ctx.Viper.GetString(flags.FlagHome) @@ -229,11 +231,64 @@ func startStandAlone(ctx *Context, appCreator types.AppCreator) error { return err } - _, err = startTelemetry(config) + // Add the tx service to the gRPC router. We only need to register this + // service if API or gRPC is enabled, and avoid doing so in the general + // case, because it spawns a new local CometBFT RPC client. + if config.API.Enable || config.GRPC.Enable { + // create tendermint client + // assumes the rpc listen address is where tendermint has its rpc server + rpcclient, err := rpchttp.New(ctx.Config.RPC.ListenAddress, "/websocket") + if err != nil { + return err + } + // re-assign for making the client available below + // do not use := to avoid shadowing clientCtx + clientCtx = clientCtx.WithClient(rpcclient) + // use the clientCtx provided to start command + app.RegisterTxService(clientCtx) + app.RegisterTendermintService(clientCtx) + app.RegisterNodeService(clientCtx) + } + + metrics, err := startTelemetry(config) if err != nil { return err } + cfg := ctx.Config + + genDocProvider := node.DefaultGenesisDocProviderFunc(cfg) + + apiSrv, err := startAPIserver(config, genDocProvider, clientCtx, home, ctx, app, metrics) + if err != nil { + return err + } + + var ( + grpcSrv *grpc.Server + grpcWebSrv *http.Server + ) + + if config.GRPC.Enable { + grpcSrv, err = servergrpc.StartGRPCServer(clientCtx, app, config.GRPC) + if err != nil { + return err + } + defer grpcSrv.Stop() + if config.GRPCWeb.Enable { + grpcWebSrv, err = servergrpc.StartGRPCWeb(grpcSrv, config) + if err != nil { + ctx.Logger.Error("failed to start grpc-web http server: ", err) + return err + } + defer func() { + if err := grpcWebSrv.Close(); err != nil { + ctx.Logger.Error("failed to close grpc-web http server: ", err) + } + }() + } + } + svr, err := server.NewServer(addr, transport, app) if err != nil { return fmt.Errorf("error creating listener: %v", err) @@ -244,18 +299,16 @@ func startStandAlone(ctx *Context, appCreator types.AppCreator) error { err = svr.Start() if err != nil { fmt.Println(err.Error()) - os.Exit(1) + return err } defer func() { - if err = svr.Stop(); err != nil { - fmt.Println(err.Error()) - os.Exit(1) - } + _ = svr.Stop() + + _ = app.Close() - if err = app.Close(); err != nil { - fmt.Println(err.Error()) - os.Exit(1) + if apiSrv != nil { + _ = apiSrv.Close() } }() @@ -354,70 +407,9 @@ func startInProcess(ctx *Context, clientCtx client.Context, appCreator types.App return err } - var apiSrv *api.Server - if config.API.Enable { - genDoc, err := genDocProvider() - if err != nil { - return err - } - - clientCtx := clientCtx.WithHomeDir(home).WithChainID(genDoc.ChainID) - - if config.GRPC.Enable { - _, port, err := net.SplitHostPort(config.GRPC.Address) - if err != nil { - return err - } - - maxSendMsgSize := config.GRPC.MaxSendMsgSize - if maxSendMsgSize == 0 { - maxSendMsgSize = serverconfig.DefaultGRPCMaxSendMsgSize - } - - maxRecvMsgSize := config.GRPC.MaxRecvMsgSize - if maxRecvMsgSize == 0 { - maxRecvMsgSize = serverconfig.DefaultGRPCMaxRecvMsgSize - } - - grpcAddress := fmt.Sprintf("127.0.0.1:%s", port) - - // If grpc is enabled, configure grpc client for grpc gateway. - grpcClient, err := grpc.Dial( - grpcAddress, - grpc.WithTransportCredentials(insecure.NewCredentials()), - grpc.WithDefaultCallOptions( - grpc.ForceCodec(codec.NewProtoCodec(clientCtx.InterfaceRegistry).GRPCCodec()), - grpc.MaxCallRecvMsgSize(maxRecvMsgSize), - grpc.MaxCallSendMsgSize(maxSendMsgSize), - ), - ) - if err != nil { - return err - } - - clientCtx = clientCtx.WithGRPCClient(grpcClient) - ctx.Logger.Debug("grpc client assigned to client context", "target", grpcAddress) - } - - apiSrv = api.New(clientCtx, ctx.Logger.With("module", "api-server")) - app.RegisterAPIRoutes(apiSrv, config.API) - if config.Telemetry.Enabled { - apiSrv.SetTelemetry(metrics) - } - errCh := make(chan error) - - go func() { - if err := apiSrv.Start(config); err != nil { - errCh <- err - } - }() - - select { - case err := <-errCh: - return err - - case <-time.After(types.ServerStartTime): // assume server started successfully - } + apiSrv, err := startAPIserver(config, genDocProvider, clientCtx, home, ctx, app, metrics) + if err != nil { + return err } var ( @@ -523,6 +515,76 @@ func startInProcess(ctx *Context, clientCtx client.Context, appCreator types.App return WaitForQuitSignals() } +func startAPIserver(config serverconfig.Config, genDocProvider node.GenesisDocProvider, clientCtx client.Context, home string, ctx *Context, app types.Application, metrics *telemetry.Metrics) (*api.Server, error) { + // If grpc is enabled, configure grpc client for grpc gateway. + // assume server started successfully + var apiSrv *api.Server + if config.API.Enable { + genDoc, err := genDocProvider() + if err != nil { + return nil, err + } + + clientCtx := clientCtx.WithHomeDir(home).WithChainID(genDoc.ChainID) + + if config.GRPC.Enable { + _, port, err := net.SplitHostPort(config.GRPC.Address) + if err != nil { + return nil, err + } + + maxSendMsgSize := config.GRPC.MaxSendMsgSize + if maxSendMsgSize == 0 { + maxSendMsgSize = serverconfig.DefaultGRPCMaxSendMsgSize + } + + maxRecvMsgSize := config.GRPC.MaxRecvMsgSize + if maxRecvMsgSize == 0 { + maxRecvMsgSize = serverconfig.DefaultGRPCMaxRecvMsgSize + } + + grpcAddress := fmt.Sprintf("127.0.0.1:%s", port) + + grpcClient, err := grpc.Dial( + grpcAddress, + grpc.WithTransportCredentials(insecure.NewCredentials()), + grpc.WithDefaultCallOptions( + grpc.ForceCodec(codec.NewProtoCodec(clientCtx.InterfaceRegistry).GRPCCodec()), + grpc.MaxCallRecvMsgSize(maxRecvMsgSize), + grpc.MaxCallSendMsgSize(maxSendMsgSize), + ), + ) + if err != nil { + return nil, err + } + + clientCtx = clientCtx.WithGRPCClient(grpcClient) + ctx.Logger.Debug("grpc client assigned to client context", "target", grpcAddress) + } + + apiSrv = api.New(clientCtx, ctx.Logger.With("module", "api-server")) + app.RegisterAPIRoutes(apiSrv, config.API) + if config.Telemetry.Enabled { + apiSrv.SetTelemetry(metrics) + } + errCh := make(chan error) + + go func() { + if err := apiSrv.Start(config); err != nil { + errCh <- err + } + }() + + select { + case err := <-errCh: + return nil, err + + case <-time.After(types.ServerStartTime): + } + } + return apiSrv, nil +} + func startTelemetry(cfg serverconfig.Config) (*telemetry.Metrics, error) { if !cfg.Telemetry.Enabled { return nil, nil