diff --git a/CHANGELOG.md b/CHANGELOG.md index f055e6efe7..9de82073f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -63,6 +63,7 @@ Ref: https://keepachangelog.com/en/1.0.0/ - (ante) [#1717](https://github.com/evmos/ethermint/pull/1717) Reuse sender recovery result. - (cli) [#242](https://github.com/crypto-org-chain/ethermint/pull/242) Integrate tendermint bootstrap cmd. - (cli) [#246](https://github.com/crypto-org-chain/ethermint/pull/246) Call app.Close to cleanup resource on graceful shutdown. +* (cli) [#288](https://github.com/crypto-org-chain/ethermint/pull/288) make abci handshake shutdown gracefully. ## [v0.21.0] - 2023-01-26 diff --git a/go.mod b/go.mod index 75d5d07dbd..146edab7d1 100644 --- a/go.mod +++ b/go.mod @@ -40,6 +40,7 @@ require ( github.com/tidwall/sjson v1.2.5 github.com/tyler-smith/go-bip39 v1.1.0 golang.org/x/net v0.12.0 + golang.org/x/sync v0.2.0 golang.org/x/text v0.12.0 google.golang.org/genproto/googleapis/api v0.0.0-20230629202037-9506855d4529 google.golang.org/grpc v1.56.2 @@ -197,7 +198,6 @@ require ( golang.org/x/crypto v0.11.0 // indirect golang.org/x/exp v0.0.0-20230711153332-06a737ee72cb // indirect golang.org/x/oauth2 v0.8.0 // indirect - golang.org/x/sync v0.2.0 // indirect golang.org/x/sys v0.11.0 // indirect golang.org/x/term v0.10.0 // indirect golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect @@ -217,8 +217,7 @@ require ( replace ( // use cosmos keyring github.com/99designs/keyring => github.com/cosmos/keyring v1.1.7-0.20210622111912-ef00f8ac3d76 - github.com/ChainSafe/go-schnorrkel => github.com/ChainSafe/go-schnorrkel v0.0.0-20200405005733-88cbf1b4c40d - github.com/cometbft/cometbft => github.com/cometbft/cometbft v0.37.2 + github.com/cometbft/cometbft => github.com/cometbft/cometbft v0.37.2-0.20230905115601-790d57e1748f github.com/cometbft/cometbft-db => github.com/crypto-org-chain/cometbft-db v0.0.0-20230412133340-ac70df4b45f6 github.com/cosmos/cosmos-sdk => github.com/crypto-org-chain/cosmos-sdk v0.46.0-beta2.0.20230905040840-b3af5590283b // Fix upstream GHSA-h395-qcrw-5vmq vulnerability. diff --git a/go.sum b/go.sum index a567d7f350..d1ba1c07bc 100644 --- a/go.sum +++ b/go.sum @@ -209,8 +209,8 @@ filippo.io/edwards25519 v1.0.0/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5E github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/ChainSafe/go-schnorrkel v0.0.0-20200405005733-88cbf1b4c40d h1:nalkkPQcITbvhmL4+C4cKA87NW0tfm3Kl9VXRoPywFg= -github.com/ChainSafe/go-schnorrkel v0.0.0-20200405005733-88cbf1b4c40d/go.mod h1:URdX5+vg25ts3aCh8H5IFZybJYKWhJHYMTnf+ULtoC4= +github.com/ChainSafe/go-schnorrkel v1.0.0 h1:3aDA67lAykLaG1y3AOjs88dMxC88PgUuHRrLeDnvGIM= +github.com/ChainSafe/go-schnorrkel v1.0.0/go.mod h1:dpzHYVxLZcp8pjlV+O+UR8K0Hp/z7vcchBSbMBEhCw4= github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ= github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= @@ -340,8 +340,8 @@ github.com/cockroachdb/redact v1.1.5/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZ github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= github.com/coinbase/rosetta-sdk-go/types v1.0.0 h1:jpVIwLcPoOeCR6o1tU+Xv7r5bMONNbHU7MuEHboiFuA= github.com/coinbase/rosetta-sdk-go/types v1.0.0/go.mod h1:eq7W2TMRH22GTW0N0beDnN931DW0/WOI1R2sdHNHG4c= -github.com/cometbft/cometbft v0.37.2 h1:XB0yyHGT0lwmJlFmM4+rsRnczPlHoAKFX6K8Zgc2/Jc= -github.com/cometbft/cometbft v0.37.2/go.mod h1:Y2MMMN//O5K4YKd8ze4r9jmk4Y7h0ajqILXbH5JQFVs= +github.com/cometbft/cometbft v0.37.2-0.20230905115601-790d57e1748f h1:1v8Q8r5RJhYzxgLY9CpkIQ1dsOpOn5gNLka01kA6C7s= +github.com/cometbft/cometbft v0.37.2-0.20230905115601-790d57e1748f/go.mod h1:gC4iJBO09opzl1pLiPl4fYWn8rRrfO713rRM5tZn5E4= github.com/confio/ics23/go v0.9.0 h1:cWs+wdbS2KRPZezoaaj+qBleXgUk5WOQFMP3CQFGTr4= github.com/confio/ics23/go v0.9.0/go.mod h1:4LPZ2NYqnYIVRklaozjNR1FScgDJ2s5Xrp+e/mYVRak= github.com/containerd/continuity v0.3.0 h1:nisirsYROK15TAMVukJOUyGJjz4BNQJBVsNvAXZJ/eg= diff --git a/gomod2nix.toml b/gomod2nix.toml index 471d580f3d..3b5b6060d1 100644 --- a/gomod2nix.toml +++ b/gomod2nix.toml @@ -48,9 +48,8 @@ schema = 3 hash = "sha256-oCalyOZWegRgKHZ1GvYHNkrMKh51j8cOE/K4yBPM5Oc=" replaced = "github.com/cosmos/keyring" [mod."github.com/ChainSafe/go-schnorrkel"] - version = "v0.0.0-20200405005733-88cbf1b4c40d" - hash = "sha256-i8RXZemJGlSjBT35oPm0SawFiBoIU5Pkq5xp4n/rzCY=" - replaced = "github.com/ChainSafe/go-schnorrkel" + version = "v1.0.0" + hash = "sha256-atJ2waz124m0DVHjol8v3NfCLKidU3fYu5AgzT9xCBA=" [mod."github.com/DataDog/zstd"] version = "v1.4.5" hash = "sha256-WFHcU2EFRIIc1wSc0jN2VPeJzBMcZTIN5LFNBFk+NAY=" @@ -121,8 +120,8 @@ schema = 3 version = "v1.0.0" hash = "sha256-z/0E0NiEGo7zxM7d94ImgUf8P0/KG6hbP9T4Vuym4p0=" [mod."github.com/cometbft/cometbft"] - version = "v0.37.2" - hash = "sha256-lldrYWvcwV6x9uSJWwycf5OCaNkLU6K5g25IbNnMyVE=" + version = "v0.37.2-0.20230905115601-790d57e1748f" + hash = "sha256-xWrKY2za+6szbr4FAGUcZKTkP2xgdbCccnqt/RCON8M=" replaced = "github.com/cometbft/cometbft" [mod."github.com/cometbft/cometbft-db"] version = "v0.0.0-20230412133340-ac70df4b45f6" diff --git a/server/start.go b/server/start.go index ec907c2ffa..6dcf90cbab 100644 --- a/server/start.go +++ b/server/start.go @@ -29,6 +29,7 @@ import ( "github.com/cosmos/cosmos-sdk/codec" "github.com/cosmos/cosmos-sdk/crypto/keyring" "github.com/cosmos/cosmos-sdk/telemetry" + "golang.org/x/sync/errgroup" "github.com/spf13/cobra" @@ -139,7 +140,9 @@ which accepts a path for the resulting pprof file. withTM, _ := cmd.Flags().GetBool(srvflags.WithTendermint) if !withTM { serverCtx.Logger.Info("starting ABCI without Tendermint") - return startStandAlone(serverCtx, opts) + return wrapCPUProfile(serverCtx, func() error { + return startStandAlone(serverCtx, opts) + }) } serverCtx.Logger.Info("Unlocking keyring") @@ -156,7 +159,9 @@ which accepts a path for the resulting pprof file. serverCtx.Logger.Info("starting ABCI with Tendermint") // amino is needed here for backwards compatibility of REST routes - err = startInProcess(serverCtx, clientCtx, opts) + err = wrapCPUProfile(serverCtx, func() error { + return startInProcess(serverCtx, clientCtx, opts) + }) errCode, ok := err.(server.ErrorCode) if !ok { return err @@ -249,6 +254,11 @@ func startStandAlone(ctx *server.Context, opts StartOptions) error { } app := opts.AppCreator(ctx.Logger, db, traceWriter, ctx.Viper) + defer func() { + if err := app.Close(); err != nil { + ctx.Logger.Error("close application failed", "error", err.Error()) + } + }() config, err := config.GetConfig(ctx.Viper) if err != nil { @@ -282,10 +292,6 @@ func startStandAlone(ctx *server.Context, opts StartOptions) error { if err = svr.Stop(); err != nil { tmos.Exit(err.Error()) } - - if err := app.Close(); err != nil { - tmos.Exit(err.Error()) - } }() // Wait for SIGINT or SIGTERM signal @@ -293,38 +299,13 @@ func startStandAlone(ctx *server.Context, opts StartOptions) error { } // legacyAminoCdc is used for the legacy REST API -func startInProcess(ctx *server.Context, clientCtx client.Context, opts StartOptions) (err error) { - cfg := ctx.Config +func startInProcess(svrCtx *server.Context, clientCtx client.Context, opts StartOptions) (err error) { + cfg := svrCtx.Config home := cfg.RootDir - logger := ctx.Logger - - if cpuProfile := ctx.Viper.GetString(srvflags.CPUProfile); cpuProfile != "" { - fp, err := ethdebug.ExpandHome(cpuProfile) - if err != nil { - ctx.Logger.Debug("failed to get filepath for the CPU profile file", "error", err.Error()) - return err - } - - f, err := os.Create(fp) - if err != nil { - return err - } - - ctx.Logger.Info("starting CPU profiler", "profile", cpuProfile) - if err := pprof.StartCPUProfile(f); err != nil { - return err - } - - defer func() { - ctx.Logger.Info("stopping CPU profiler", "profile", cpuProfile) - pprof.StopCPUProfile() - if err := f.Close(); err != nil { - logger.Error("failed to close CPU profiler file", "error", err.Error()) - } - }() - } + logger := svrCtx.Logger - db, err := opts.DBOpener(ctx.Viper, home, server.GetAppDBBackend(ctx.Viper)) + traceWriterFile := svrCtx.Viper.GetString(srvflags.TraceStore) + db, err := opts.DBOpener(svrCtx.Viper, home, server.GetAppDBBackend(svrCtx.Viper)) if err != nil { logger.Error("failed to open DB", "error", err.Error()) return err @@ -332,18 +313,17 @@ func startInProcess(ctx *server.Context, clientCtx client.Context, opts StartOpt defer func() { if err := db.Close(); err != nil { - ctx.Logger.With("error", err).Error("error closing db") + logger.With("error", err).Error("error closing db") } }() - traceWriterFile := ctx.Viper.GetString(srvflags.TraceStore) traceWriter, err := openTraceWriter(traceWriterFile) if err != nil { logger.Error("failed to open trace writer", "error", err.Error()) return err } - config, err := config.GetConfig(ctx.Viper) + config, err := config.GetConfig(svrCtx.Viper) if err != nil { logger.Error("failed to get server config", "error", err.Error()) return err @@ -354,7 +334,12 @@ func startInProcess(ctx *server.Context, clientCtx client.Context, opts StartOpt return err } - app := opts.AppCreator(ctx.Logger, db, traceWriter, ctx.Viper) + app := opts.AppCreator(svrCtx.Logger, db, traceWriter, svrCtx.Viper) + defer func() { + if err := app.Close(); err != nil { + logger.Error("close application failed", "error", err.Error()) + } + }() nodeKey, err := p2p.LoadOrGenNodeKey(cfg.NodeKeyFile()) if err != nil { @@ -362,11 +347,17 @@ func startInProcess(ctx *server.Context, clientCtx client.Context, opts StartOpt return err } + ctx, cancelFn := context.WithCancel(context.Background()) + g, ctx := errgroup.WithContext(ctx) + + // listen for quit signals so the calling parent process can gracefully exit + ListenForQuitSignals(g, true, cancelFn, svrCtx.Logger) + genDocProvider := node.DefaultGenesisDocProviderFunc(cfg) var ( tmNode *node.Node - gRPCOnly = ctx.Viper.GetBool(srvflags.GRPCOnly) + gRPCOnly = svrCtx.Viper.GetBool(srvflags.GRPCOnly) ) if gRPCOnly { @@ -376,7 +367,8 @@ func startInProcess(ctx *server.Context, clientCtx client.Context, opts StartOpt } else { logger.Info("starting node with ABCI Tendermint in-process") - tmNode, err = node.NewNode( + tmNode, err = node.NewNodeWithContext( + ctx, cfg, pvm.LoadOrGenFilePV(cfg.PrivValidatorKeyFile(), cfg.PrivValidatorStateFile()), nodeKey, @@ -384,7 +376,7 @@ func startInProcess(ctx *server.Context, clientCtx client.Context, opts StartOpt genDocProvider, node.DefaultDBProvider, node.DefaultMetricsProvider(cfg.Instrumentation), - ctx.Logger.With("server", "node"), + logger.With("server", "node"), ) if err != nil { logger.Error("failed init node", "error", err.Error()) @@ -399,7 +391,6 @@ func startInProcess(ctx *server.Context, clientCtx client.Context, opts StartOpt defer func() { if tmNode.IsRunning() { _ = tmNode.Stop() - _ = app.Close() } }() } @@ -422,19 +413,19 @@ func startInProcess(ctx *server.Context, clientCtx client.Context, opts StartOpt // Enable metrics if JSONRPC is enabled and --metrics is passed // Flag not added in config to avoid user enabling in config without passing in CLI - if config.JSONRPC.Enable && ctx.Viper.GetBool(srvflags.JSONRPCEnableMetrics) { + if config.JSONRPC.Enable && svrCtx.Viper.GetBool(srvflags.JSONRPCEnableMetrics) { ethmetricsexp.Setup(config.JSONRPC.MetricsAddress) } var idxer ethermint.EVMTxIndexer if config.JSONRPC.EnableIndexer { - idxDB, err := OpenIndexerDB(home, server.GetAppDBBackend(ctx.Viper)) + idxDB, err := OpenIndexerDB(home, server.GetAppDBBackend(svrCtx.Viper)) if err != nil { logger.Error("failed to open evm indexer DB", "error", err.Error()) return err } - idxLogger := ctx.Logger.With("indexer", "evm") + idxLogger := logger.With("indexer", "evm") idxer = indexer.NewKVIndexer(idxDB, idxLogger, clientCtx) indexerService := NewEVMIndexerService(idxer, clientCtx.Client.(rpcclient.Client)) indexerService.SetLogger(idxLogger) @@ -498,13 +489,13 @@ func startInProcess(ctx *server.Context, clientCtx client.Context, opts StartOpt } clientCtx = clientCtx.WithGRPCClient(grpcClient) - ctx.Logger.Debug("gRPC client assigned to client context", "address", grpcAddress) + svrCtx.Logger.Debug("gRPC client assigned to client context", "address", grpcAddress) } } var apiSrv *api.Server if config.API.Enable { - apiSrv = api.New(clientCtx, ctx.Logger.With("server", "api")) + apiSrv = api.New(clientCtx, svrCtx.Logger.With("server", "api")) app.RegisterAPIRoutes(apiSrv, config.API) if config.Telemetry.Enabled { @@ -541,7 +532,7 @@ func startInProcess(ctx *server.Context, clientCtx client.Context, opts StartOpt if config.GRPCWeb.Enable { grpcWebSrv, err = servergrpc.StartGRPCWeb(grpcSrv, config.Config) if err != nil { - ctx.Logger.Error("failed to start grpc-web http server", "error", err.Error()) + logger.Error("failed to start grpc-web http server", "error", err.Error()) return err } @@ -568,7 +559,7 @@ func startInProcess(ctx *server.Context, clientCtx client.Context, opts StartOpt tmEndpoint := "/websocket" tmRPCAddr := cfg.RPC.ListenAddress - httpSrv, httpSrvDone, err = StartJSONRPC(ctx, clientCtx, tmRPCAddr, tmEndpoint, &config, idxer) + httpSrv, httpSrvDone, err = StartJSONRPC(svrCtx, clientCtx, tmRPCAddr, tmEndpoint, &config, idxer) if err != nil { return err } @@ -591,7 +582,7 @@ func startInProcess(ctx *server.Context, clientCtx client.Context, opts StartOpt // we do not need to start Rosetta or handle any Tendermint related processes. if gRPCOnly { // wait for signal capture and gracefully return - return server.WaitForQuitSignals() + return g.Wait() } var rosettaSrv crgserver.Server @@ -606,14 +597,14 @@ func startInProcess(ctx *server.Context, clientCtx client.Context, opts StartOpt minGasPrices, err := sdk.ParseDecCoins(config.MinGasPrices) if err != nil { - ctx.Logger.Error("failed to parse minimum-gas-prices", "error", err.Error()) + logger.Error("failed to parse minimum-gas-prices", "error", err.Error()) return err } conf := &rosetta.Config{ Blockchain: config.Rosetta.Blockchain, Network: config.Rosetta.Network, - TendermintRPC: ctx.Config.RPC.ListenAddress, + TendermintRPC: svrCtx.Config.RPC.ListenAddress, GRPCEndpoint: config.GRPC.Address, Addr: config.Rosetta.Address, Retries: config.Rosetta.Retries, @@ -643,8 +634,8 @@ func startInProcess(ctx *server.Context, clientCtx client.Context, opts StartOpt case <-time.After(types.ServerStartTime): // assume server started successfully } } - // Wait for SIGINT or SIGTERM signal - return server.WaitForQuitSignals() + + return g.Wait() } func openDB(_ types.AppOptions, rootDir string, backendType dbm.BackendType) (dbm.DB, error) { @@ -677,3 +668,33 @@ func startTelemetry(cfg config.Config) (*telemetry.Metrics, error) { } return telemetry.New(cfg.Telemetry) } + +// wrapCPUProfile runs callback in a goroutine, then wait for quit signals. +func wrapCPUProfile(ctx *server.Context, callback func() error) error { + if cpuProfile := ctx.Viper.GetString(srvflags.CPUProfile); cpuProfile != "" { + fp, err := ethdebug.ExpandHome(cpuProfile) + if err != nil { + ctx.Logger.Debug("failed to get filepath for the CPU profile file", "error", err.Error()) + return err + } + f, err := os.Create(fp) + if err != nil { + return err + } + + ctx.Logger.Info("starting CPU profiler", "profile", cpuProfile) + if err := pprof.StartCPUProfile(f); err != nil { + return err + } + + defer func() { + ctx.Logger.Info("stopping CPU profiler", "profile", cpuProfile) + pprof.StopCPUProfile() + if err := f.Close(); err != nil { + ctx.Logger.Info("failed to close cpu-profile file", "profile", cpuProfile, "err", err.Error()) + } + }() + } + + return callback() +} diff --git a/server/util.go b/server/util.go index 44c4ca2977..e5d3d5e386 100644 --- a/server/util.go +++ b/server/util.go @@ -16,8 +16,12 @@ package server import ( + "context" "net" "net/http" + "os" + "os/signal" + "syscall" "time" "github.com/evmos/ethermint/server/config" @@ -25,6 +29,7 @@ import ( "github.com/improbable-eng/grpc-web/go/grpcweb" "github.com/spf13/cobra" "golang.org/x/net/netutil" + "golang.org/x/sync/errgroup" sdkserver "github.com/cosmos/cosmos-sdk/server" "github.com/cosmos/cosmos-sdk/server/types" @@ -139,3 +144,30 @@ func Listen(addr string, config *config.Config) (net.Listener, error) { } return ln, err } + +// ListenForQuitSignals listens for SIGINT and SIGTERM. When a signal is received, +// the cleanup function is called, indicating the caller can gracefully exit or +// return. +// +// Note, the blocking behavior of this depends on the block argument. +// The caller must ensure the corresponding context derived from the cancelFn is used correctly. +func ListenForQuitSignals(g *errgroup.Group, block bool, cancelFn context.CancelFunc, logger tmlog.Logger) { + sigCh := make(chan os.Signal, 1) + signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) + + f := func() { + sig := <-sigCh + cancelFn() + + logger.Info("caught signal", "signal", sig.String()) + } + + if block { + g.Go(func() error { + f() + return nil + }) + } else { + go f() + } +}