diff --git a/cosmovisor/CHANGELOG.md b/cosmovisor/CHANGELOG.md index aa1aef2a0cd1..8f9089395f34 100644 --- a/cosmovisor/CHANGELOG.md +++ b/cosmovisor/CHANGELOG.md @@ -39,6 +39,7 @@ Ref: https://keepachangelog.com/en/1.0.0/ ### Features +* [\#12188](https://github.com/cosmos/cosmos-sdk/pull/12188) Add a `DAEMON_RESTART_DELAY` for allowing a node operator to define a delay between the node halt (for upgrade) and backup. * [\#11823](https://github.com/cosmos/cosmos-sdk/pull/11823) Refactor `cosmovisor` CLI to use `cobra`. * [\#11731](https://github.com/cosmos/cosmos-sdk/pull/11731) `cosmovisor version -o json` returns the cosmovisor version and the result of `simd --output json --long` in one JSON object. @@ -46,6 +47,10 @@ Ref: https://keepachangelog.com/en/1.0.0/ * [\#12005](https://github.com/cosmos/cosmos-sdk/pull/12005) Fix cosmovisor binary usage for pre-upgrade +### CLI Breaking Changes + +* [\#12188](https://github.com/cosmos/cosmos-sdk/pull/12188) Remove the possibility to set a time with only a number. `DAEMON_POLL_INTERVAL` env variable now only supports a duration (e.g. `100ms`, `30s`, `20m`). + ## v1.1.0 2022-10-02 ### Features diff --git a/cosmovisor/README.md b/cosmovisor/README.md index c0e50139b125..319817341d7d 100644 --- a/cosmovisor/README.md +++ b/cosmovisor/README.md @@ -74,7 +74,8 @@ All arguments passed to `cosmovisor run` will be passed to the application binar * `DAEMON_NAME` is the name of the binary itself (e.g. `gaiad`, `regend`, `simd`, etc.). * `DAEMON_ALLOW_DOWNLOAD_BINARIES` (*optional*), if set to `true`, will enable auto-downloading of new binaries (for security reasons, this is intended for full nodes rather than validators). By default, `cosmovisor` will not auto-download new binaries. * `DAEMON_RESTART_AFTER_UPGRADE` (*optional*, default = `true`), if `true`, restarts the subprocess with the same command-line arguments and flags (but with the new binary) after a successful upgrade. Otherwise (`false`), `cosmovisor` stops running after an upgrade and requires the system administrator to manually restart it. Note restart is only after the upgrade and does not auto-restart the subprocess after an error occurs. -* `DAEMON_POLL_INTERVAL` is the interval length for polling the upgrade plan file. The value can either be a number (in milliseconds) or a duration (e.g. `1s`). Default: 300 milliseconds. +* `DAEMON_RESTART_DELAY` (*optional*, default none), allow a node operator to define a delay between the node halt (for upgrade) and backup by the specified time. The value must be a duration (e.g. `1s`). +* `DAEMON_POLL_INTERVAL` (*optional*, default 300 milliseconds), is the interval length for polling the upgrade plan file. The value must be a duration (e.g. `1s`). * `DAEMON_BACKUP_DIR` option to set a custom backup directory. If not set, `DAEMON_HOME` is used. * `UNSAFE_SKIP_BACKUP` (defaults to `false`), if set to `true`, upgrades directly without performing a backup. Otherwise (`false`, default) backs up the data before trying the upgrade. The default value of false is useful and recommended in case of failures and when a backup needed to rollback. We recommend using the default backup option `UNSAFE_SKIP_BACKUP=false`. * `DAEMON_PREUPGRADE_MAX_RETRIES` (defaults to `0`). The maximum number of times to call `pre-upgrade` in the application after exit status of `31`. After the maximum number of retries, cosmovisor fails the upgrade. diff --git a/cosmovisor/args.go b/cosmovisor/args.go index c9a0d36a9279..af16586c9113 100644 --- a/cosmovisor/args.go +++ b/cosmovisor/args.go @@ -2,7 +2,6 @@ package cosmovisor import ( "encoding/json" - "errors" "fmt" "net/url" "os" @@ -23,6 +22,7 @@ const ( EnvName = "DAEMON_NAME" EnvDownloadBin = "DAEMON_ALLOW_DOWNLOAD_BINARIES" EnvRestartUpgrade = "DAEMON_RESTART_AFTER_UPGRADE" + EnvRestartDelay = "DAEMON_RESTART_DELAY" EnvSkipBackup = "UNSAFE_SKIP_BACKUP" EnvDataBackupPath = "DAEMON_DATA_BACKUP_DIR" EnvInterval = "DAEMON_POLL_INTERVAL" @@ -45,6 +45,7 @@ type Config struct { Name string AllowDownloadBinaries bool RestartAfterUpgrade bool + RestartDelay time.Duration PollInterval time.Duration UnsafeSkipBackup bool DataBackupPath string @@ -97,6 +98,13 @@ func (cfg *Config) SymLinkToGenesis() (string, error) { return cfg.GenesisBin(), nil } +// WaitRestartDelay will block and wait until the RestartDelay has elapsed. +func (cfg *Config) WaitRestartDelay() { + if cfg.RestartDelay > 0 { + time.Sleep(cfg.RestartDelay) + } +} + // CurrentBin is the path to the currently selected binary (genesis if no link is set) // This will resolve the symlink to the underlying directory to make it easier to debug func (cfg *Config) CurrentBin() (string, error) { @@ -152,23 +160,27 @@ func GetConfigFromEnv() (*Config, error) { interval := os.Getenv(EnvInterval) if interval != "" { - var intervalUInt uint64 - intervalUInt, err = strconv.ParseUint(interval, 10, 32) - if err == nil { - cfg.PollInterval = time.Millisecond * time.Duration(intervalUInt) + val, err := parseEnvDuration(interval) + if err != nil { + errs = append(errs, fmt.Errorf("invalid: %s: %w", EnvInterval, err)) } else { - cfg.PollInterval, err = time.ParseDuration(interval) - } - switch { - case err != nil: - errs = append(errs, fmt.Errorf("invalid %s: could not parse \"%s\" into either a duration or uint (milliseconds)", EnvInterval, interval)) - case cfg.PollInterval <= 0: - errs = append(errs, fmt.Errorf("invalid %s: must be greater than 0", EnvInterval)) + cfg.PollInterval = val } } else { cfg.PollInterval = 300 * time.Millisecond } + cfg.RestartDelay = 0 // default value but makes it explicit + restartDelay := os.Getenv(EnvRestartDelay) + if restartDelay != "" { + val, err := parseEnvDuration(restartDelay) + if err != nil { + errs = append(errs, fmt.Errorf("invalid: %s: %w", EnvRestartDelay, err)) + } else { + cfg.RestartDelay = val + } + } + envPreupgradeMaxRetriesVal := os.Getenv(EnvPreupgradeMaxRetries) if cfg.PreupgradeMaxRetries, err = strconv.Atoi(envPreupgradeMaxRetriesVal); err != nil && envPreupgradeMaxRetriesVal != "" { errs = append(errs, fmt.Errorf("%s could not be parsed to int: %w", EnvPreupgradeMaxRetries, err)) @@ -182,6 +194,19 @@ func GetConfigFromEnv() (*Config, error) { return cfg, nil } +func parseEnvDuration(input string) (time.Duration, error) { + duration, err := time.ParseDuration(input) + if err != nil { + return 0, fmt.Errorf("could not parse '%s' into a duration: %w", input, err) + } + + if duration <= 0 { + return 0, fmt.Errorf("must be greater than 0") + } + + return duration, nil +} + // LogConfigOrError logs either the config details or the error. func LogConfigOrError(logger *zerolog.Logger, cfg *Config, err error) { if cfg == nil && err == nil { @@ -201,15 +226,18 @@ func LogConfigOrError(logger *zerolog.Logger, cfg *Config, err error) { // and that Name is set func (cfg *Config) validate() []error { var errs []error + + // validate EnvName if cfg.Name == "" { - errs = append(errs, errors.New(EnvName+" is not set")) + errs = append(errs, fmt.Errorf("%s is not set", EnvName)) } + // validate EnvHome switch { case cfg.Home == "": - errs = append(errs, errors.New(EnvHome+" is not set")) + errs = append(errs, fmt.Errorf("%s is not set", EnvHome)) case !filepath.IsAbs(cfg.Home): - errs = append(errs, errors.New(EnvHome+" must be an absolute path")) + errs = append(errs, fmt.Errorf("%s must be an absolute path", EnvHome)) default: switch info, err := os.Stat(cfg.Root()); { case err != nil: @@ -223,7 +251,8 @@ func (cfg *Config) validate() []error { if cfg.UnsafeSkipBackup == true { return errs } - // if UnsafeSkipBackup is false, check if the DataBackupPath valid + + // if UnsafeSkipBackup is false, validate DataBackupPath switch { case cfg.DataBackupPath == "": errs = append(errs, fmt.Errorf("%s must not be empty", EnvDataBackupPath)) @@ -327,11 +356,13 @@ func (cfg Config) DetailString() string { {EnvName, cfg.Name}, {EnvDownloadBin, fmt.Sprintf("%t", cfg.AllowDownloadBinaries)}, {EnvRestartUpgrade, fmt.Sprintf("%t", cfg.RestartAfterUpgrade)}, + {EnvRestartDelay, fmt.Sprintf("%s", cfg.RestartDelay)}, {EnvInterval, fmt.Sprintf("%s", cfg.PollInterval)}, {EnvSkipBackup, fmt.Sprintf("%t", cfg.UnsafeSkipBackup)}, {EnvDataBackupPath, cfg.DataBackupPath}, {EnvPreupgradeMaxRetries, fmt.Sprintf("%d", cfg.PreupgradeMaxRetries)}, } + derivedEntries := []struct{ name, value string }{ {"Root Dir", cfg.Root()}, {"Upgrade Dir", cfg.BaseUpgradeDir()}, diff --git a/cosmovisor/args_test.go b/cosmovisor/args_test.go index 136440a63251..3ecb6f871b15 100644 --- a/cosmovisor/args_test.go +++ b/cosmovisor/args_test.go @@ -31,6 +31,7 @@ type cosmovisorEnv struct { Name string DownloadBin string RestartUpgrade string + RestartDelay string SkipBackup string DataBackupPath string Interval string @@ -44,6 +45,7 @@ func (c cosmovisorEnv) ToMap() map[string]string { EnvName: c.Name, EnvDownloadBin: c.DownloadBin, EnvRestartUpgrade: c.RestartUpgrade, + EnvRestartDelay: c.RestartDelay, EnvSkipBackup: c.SkipBackup, EnvDataBackupPath: c.DataBackupPath, EnvInterval: c.Interval, @@ -62,6 +64,8 @@ func (c *cosmovisorEnv) Set(envVar, envVal string) { c.DownloadBin = envVal case EnvRestartUpgrade: c.RestartUpgrade = envVal + case EnvRestartDelay: + c.RestartDelay = envVal case EnvSkipBackup: c.SkipBackup = envVal case EnvDataBackupPath: @@ -354,12 +358,13 @@ func (s *argsTestSuite) TestGetConfigFromEnv() { absPath, perr := filepath.Abs(relPath) s.Require().NoError(perr) - newConfig := func(home, name, dataBackupPath string, downloadBin, restartUpgrade, skipBackup bool, interval, preupgradeMaxRetries int) *Config { + newConfig := func(home, name string, downloadBin, restartUpgrade bool, restartDelay int, skipBackup bool, dataBackupPath string, interval int, preupgradeMaxRetries int) *Config { return &Config{ Home: home, Name: name, AllowDownloadBinaries: downloadBin, RestartAfterUpgrade: restartUpgrade, + RestartDelay: time.Millisecond * time.Duration(restartDelay), PollInterval: time.Millisecond * time.Duration(interval), UnsafeSkipBackup: skipBackup, DataBackupPath: dataBackupPath, @@ -373,160 +378,201 @@ func (s *argsTestSuite) TestGetConfigFromEnv() { expectedCfg *Config expectedErrCount int }{ - // EnvHome, EnvName, EnvDownloadBin, EnvRestartUpgrade, EnvSkipBackup, EnvDataBackupPath, EnvDataBackupPath, EnvInterval, EnvPreupgradeMaxRetries { - name: "all bad", - envVals: cosmovisorEnv{"", "", "bad", "bad", "bad", "", "bad", "bad"}, + name: "all bad", + envVals: cosmovisorEnv{ + Home: "", + Name: "", + DownloadBin: "bad", + RestartUpgrade: "bad", + RestartDelay: "bad", + SkipBackup: "bad", + DataBackupPath: "bad", + Interval: "bad", + PreupgradeMaxRetries: "bad", + }, expectedCfg: nil, - expectedErrCount: 8, + expectedErrCount: 9, }, { name: "all good", - envVals: cosmovisorEnv{absPath, "testname", "true", "false", "true", "", "303", "1"}, - expectedCfg: newConfig(absPath, "testname", absPath, true, false, true, 303, 1), + envVals: cosmovisorEnv{absPath, "testname", "true", "false", "600ms", "true", "", "303ms", "1"}, + expectedCfg: newConfig(absPath, "testname", true, false, 600, true, absPath, 303, 1), expectedErrCount: 0, }, { name: "nothing set", - envVals: cosmovisorEnv{"", "", "", "", "", "", "", ""}, + envVals: cosmovisorEnv{"", "", "", "", "", "", "", "", ""}, expectedCfg: nil, expectedErrCount: 3, }, // Note: Home and Name tests are done in TestValidate { name: "download bin bad", - envVals: cosmovisorEnv{absPath, "testname", "bad", "false", "true", "", "303", "1"}, + envVals: cosmovisorEnv{absPath, "testname", "bad", "false", "600ms", "true", "", "303ms", "1"}, expectedCfg: nil, expectedErrCount: 1, }, { name: "download bin not set", - envVals: cosmovisorEnv{absPath, "testname", "", "false", "true", "", "303", "1"}, - expectedCfg: newConfig(absPath, "testname", absPath, false, false, true, 303, 1), + envVals: cosmovisorEnv{absPath, "testname", "", "false", "600ms", "true", "", "303ms", "1"}, + expectedCfg: newConfig(absPath, "testname", false, false, 600, true, absPath, 303, 1), expectedErrCount: 0, }, { name: "download bin true", - envVals: cosmovisorEnv{absPath, "testname", "true", "false", "true", "", "303", "1"}, - expectedCfg: newConfig(absPath, "testname", absPath, true, false, true, 303, 1), + envVals: cosmovisorEnv{absPath, "testname", "true", "false", "600ms", "true", "", "303ms", "1"}, + expectedCfg: newConfig(absPath, "testname", true, false, 600, true, absPath, 303, 1), expectedErrCount: 0, }, { name: "download bin false", - envVals: cosmovisorEnv{absPath, "testname", "false", "false", "true", "", "303", "1"}, - expectedCfg: newConfig(absPath, "testname", absPath, false, false, true, 303, 1), + envVals: cosmovisorEnv{absPath, "testname", "false", "false", "600ms", "true", "", "303ms", "1"}, + expectedCfg: newConfig(absPath, "testname", false, false, 600, true, absPath, 303, 1), expectedErrCount: 0, }, - // EnvHome, EnvName, EnvDownloadBin, EnvRestartUpgrade, EnvSkipBackup, EnvDataBackupPath, EnvInterval, EnvPreupgradeMaxRetries { name: "restart upgrade bad", - envVals: cosmovisorEnv{absPath, "testname", "true", "bad", "true", "", "303", "1"}, + envVals: cosmovisorEnv{absPath, "testname", "true", "bad", "600ms", "true", "", "303ms", "1"}, expectedCfg: nil, expectedErrCount: 1, }, { name: "restart upgrade not set", - envVals: cosmovisorEnv{absPath, "testname", "true", "", "true", "", "303", "1"}, - expectedCfg: newConfig(absPath, "testname", absPath, true, true, true, 303, 1), + envVals: cosmovisorEnv{absPath, "testname", "true", "", "600ms", "true", "", "303ms", "1"}, + expectedCfg: newConfig(absPath, "testname", true, true, 600, true, absPath, 303, 1), expectedErrCount: 0, }, { name: "restart upgrade true", - envVals: cosmovisorEnv{absPath, "testname", "true", "true", "true", "", "303", "1"}, - expectedCfg: newConfig(absPath, "testname", absPath, true, true, true, 303, 1), + envVals: cosmovisorEnv{absPath, "testname", "true", "true", "600ms", "true", "", "303ms", "1"}, + expectedCfg: newConfig(absPath, "testname", true, true, 600, true, absPath, 303, 1), expectedErrCount: 0, }, { name: "restart upgrade true", - envVals: cosmovisorEnv{absPath, "testname", "true", "false", "true", "", "303", "1"}, - expectedCfg: newConfig(absPath, "testname", absPath, true, false, true, 303, 1), + envVals: cosmovisorEnv{absPath, "testname", "true", "false", "600ms", "true", "", "303ms", "1"}, + expectedCfg: newConfig(absPath, "testname", true, false, 600, true, absPath, 303, 1), expectedErrCount: 0, }, - // EnvHome, EnvName, EnvDownloadBin, EnvRestartUpgrade, EnvSkipBackup, EnvDataBackupPath, EnvInterval, EnvPreupgradeMaxRetries { name: "skip unsafe backups bad", - envVals: cosmovisorEnv{absPath, "testname", "true", "false", "bad", "", "303", "1"}, + envVals: cosmovisorEnv{absPath, "testname", "true", "false", "600ms", "bad", "", "303ms", "1"}, expectedCfg: nil, expectedErrCount: 1, }, { name: "skip unsafe backups not set", - envVals: cosmovisorEnv{absPath, "testname", "true", "false", "", "", "303", "1"}, - expectedCfg: newConfig(absPath, "testname", absPath, true, false, false, 303, 1), + envVals: cosmovisorEnv{absPath, "testname", "true", "false", "600ms", "", "", "303ms", "1"}, + expectedCfg: newConfig(absPath, "testname", true, false, 600, false, absPath, 303, 1), expectedErrCount: 0, }, { name: "skip unsafe backups true", - envVals: cosmovisorEnv{absPath, "testname", "true", "false", "true", "", "303", "1"}, - expectedCfg: newConfig(absPath, "testname", absPath, true, false, true, 303, 1), + envVals: cosmovisorEnv{absPath, "testname", "true", "false", "600ms", "true", "", "303ms", "1"}, + expectedCfg: newConfig(absPath, "testname", true, false, 600, true, absPath, 303, 1), expectedErrCount: 0, }, { name: "skip unsafe backups false", - envVals: cosmovisorEnv{absPath, "testname", "true", "false", "false", "", "303", "1"}, - expectedCfg: newConfig(absPath, "testname", absPath, true, false, false, 303, 1), + envVals: cosmovisorEnv{absPath, "testname", "true", "false", "600ms", "false", "", "303ms", "1"}, + expectedCfg: newConfig(absPath, "testname", true, false, 600, false, absPath, 303, 1), expectedErrCount: 0, }, - // EnvHome, EnvName, EnvDownloadBin, EnvRestartUpgrade, EnvSkipBackup, EnvDataBackupPath, EnvInterval, EnvPreupgradeMaxRetries { name: "poll interval bad", - envVals: cosmovisorEnv{absPath, "testname", "false", "false", "false", "", "bad", "1"}, + envVals: cosmovisorEnv{absPath, "testname", "false", "false", "600ms", "false", "", "bad", "1"}, expectedCfg: nil, expectedErrCount: 1, }, { name: "poll interval 0", - envVals: cosmovisorEnv{absPath, "testname", "false", "false", "false", "", "0", "1"}, + envVals: cosmovisorEnv{absPath, "testname", "false", "false", "600ms", "false", "", "0", "1"}, expectedCfg: nil, expectedErrCount: 1, }, { name: "poll interval not set", - envVals: cosmovisorEnv{absPath, "testname", "false", "false", "false", "", "", "1"}, - expectedCfg: newConfig(absPath, "testname", absPath, false, false, false, 300, 1), + envVals: cosmovisorEnv{absPath, "testname", "false", "false", "600ms", "false", "", "", "1"}, + expectedCfg: newConfig(absPath, "testname", false, false, 600, false, absPath, 300, 1), expectedErrCount: 0, }, { - name: "poll interval 987", - envVals: cosmovisorEnv{absPath, "testname", "false", "false", "false", "", "987", "1"}, - expectedCfg: newConfig(absPath, "testname", absPath, false, false, false, 987, 1), - expectedErrCount: 0, + name: "poll interval 600", + envVals: cosmovisorEnv{absPath, "testname", "false", "false", "600ms", "false", "", "600", "1"}, + expectedCfg: nil, + expectedErrCount: 1, }, { name: "poll interval 1s", - envVals: cosmovisorEnv{absPath, "testname", "false", "false", "false", "", "1s", "1"}, - expectedCfg: newConfig(absPath, "testname", absPath, false, false, false, 1000, 1), + envVals: cosmovisorEnv{absPath, "testname", "false", "false", "600ms", "false", "", "1s", "1"}, + expectedCfg: newConfig(absPath, "testname", false, false, 600, false, absPath, 1000, 1), expectedErrCount: 0, }, { name: "poll interval -3m", - envVals: cosmovisorEnv{absPath, "testname", "false", "false", "false", "", "-3m", "1"}, + envVals: cosmovisorEnv{absPath, "testname", "false", "false", "600ms", "false", "", "-3m", "1"}, + expectedCfg: nil, + expectedErrCount: 1, + }, + { + name: "restart delay bad", + envVals: cosmovisorEnv{absPath, "testname", "false", "false", "bad", "false", "", "303ms", "1"}, + expectedCfg: nil, + expectedErrCount: 1, + }, + { + name: "restart delay 0", + envVals: cosmovisorEnv{absPath, "testname", "false", "false", "0", "false", "", "303ms", "1"}, + expectedCfg: nil, + expectedErrCount: 1, + }, + { + name: "restart delay not set", + envVals: cosmovisorEnv{absPath, "testname", "false", "false", "", "false", "", "303ms", "1"}, + expectedCfg: newConfig(absPath, "testname", false, false, 0, false, absPath, 303, 1), + expectedErrCount: 0, + }, + { + name: "restart delay 600", + envVals: cosmovisorEnv{absPath, "testname", "false", "false", "600", "false", "", "300ms", "1"}, + expectedCfg: nil, + expectedErrCount: 1, + }, + { + name: "restart delay 1s", + envVals: cosmovisorEnv{absPath, "testname", "false", "false", "1s", "false", "", "303ms", "1"}, + expectedCfg: newConfig(absPath, "testname", false, false, 1000, false, absPath, 303, 1), + expectedErrCount: 0, + }, + { + name: "restart delay -3m", + envVals: cosmovisorEnv{absPath, "testname", "false", "false", "-3m", "false", "", "303ms", "1"}, expectedCfg: nil, expectedErrCount: 1, }, - // EnvHome, EnvName, EnvDownloadBin, EnvRestartUpgrade, EnvSkipBackup, EnvDataBackupPath, EnvInterval, EnvPreupgradeMaxRetries { name: "prepupgrade max retries bad", - envVals: cosmovisorEnv{absPath, "testname", "false", "false", "false", "", "406", "bad"}, + envVals: cosmovisorEnv{absPath, "testname", "false", "false", "600ms", "false", "", "406ms", "bad"}, expectedCfg: nil, expectedErrCount: 1, }, { name: "prepupgrade max retries 0", - envVals: cosmovisorEnv{absPath, "testname", "false", "false", "false", "", "406", "0"}, - expectedCfg: newConfig(absPath, "testname", absPath, false, false, false, 406, 0), + envVals: cosmovisorEnv{absPath, "testname", "false", "false", "600ms", "false", "", "406ms", "0"}, + expectedCfg: newConfig(absPath, "testname", false, false, 600, false, absPath, 406, 0), expectedErrCount: 0, }, { name: "prepupgrade max retries not set", - envVals: cosmovisorEnv{absPath, "testname", "false", "false", "false", "", "406", ""}, - expectedCfg: newConfig(absPath, "testname", absPath, false, false, false, 406, 0), + envVals: cosmovisorEnv{absPath, "testname", "false", "false", "600ms", "false", "", "406ms", ""}, + expectedCfg: newConfig(absPath, "testname", false, false, 600, false, absPath, 406, 0), expectedErrCount: 0, }, { name: "prepupgrade max retries 5", - envVals: cosmovisorEnv{absPath, "testname", "false", "false", "false", "", "406", "5"}, - expectedCfg: newConfig(absPath, "testname", absPath, false, false, false, 406, 5), + envVals: cosmovisorEnv{absPath, "testname", "false", "false", "600ms", "false", "", "406ms", "5"}, + expectedCfg: newConfig(absPath, "testname", false, false, 600, false, absPath, 406, 5), expectedErrCount: 0, }, } diff --git a/cosmovisor/process.go b/cosmovisor/process.go index 9a1e8aded69b..fe0df910cc8e 100644 --- a/cosmovisor/process.go +++ b/cosmovisor/process.go @@ -69,6 +69,8 @@ func (l Launcher) Run(args []string, stdout, stderr io.Writer) (bool, error) { } if !IsSkipUpgradeHeight(args, l.fw.currentInfo) { + l.cfg.WaitRestartDelay() + if err := l.doBackup(); err != nil { return false, err } diff --git a/cosmovisor/process_test.go b/cosmovisor/process_test.go index 623634277bb6..1beadecc7e78 100644 --- a/cosmovisor/process_test.go +++ b/cosmovisor/process_test.go @@ -6,6 +6,7 @@ package cosmovisor_test import ( "fmt" "testing" + "time" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" @@ -47,11 +48,9 @@ func (s *processTestSuite) TestLaunchProcess() { require.NoError(err) require.True(doUpgrade) require.Equal("", stderr.String()) - require.Equal(fmt.Sprintf("Genesis foo bar 1234 %s\nUPGRADE \"chain2\" NEEDED at height: 49: {}\n", upgradeFile), - stdout.String()) + require.Equal(fmt.Sprintf("Genesis foo bar 1234 %s\nUPGRADE \"chain2\" NEEDED at height: 49: {}\n", upgradeFile), stdout.String()) // ensure this is upgraded now and produces new output - currentBin, err = cfg.CurrentBin() require.NoError(err) @@ -70,6 +69,37 @@ func (s *processTestSuite) TestLaunchProcess() { require.Equal(cfg.UpgradeBin("chain2"), currentBin) } +func (s *processTestSuite) TestLaunchProcessWithRestartDelay() { + // binaries from testdata/validate directory + require := s.Require() + home := copyTestData(s.T(), "validate") + cfg := &cosmovisor.Config{Home: home, Name: "dummyd", RestartDelay: 5 * time.Second, PollInterval: 20, UnsafeSkipBackup: true} + logger := cosmovisor.NewLogger() + + // should run the genesis binary and produce expected output + stdout, stderr := NewBuffer(), NewBuffer() + currentBin, err := cfg.CurrentBin() + require.NoError(err) + require.Equal(cfg.GenesisBin(), currentBin) + + launcher, err := cosmovisor.NewLauncher(logger, cfg) + require.NoError(err) + + upgradeFile := cfg.UpgradeInfoFilePath() + + start := time.Now() + + doUpgrade, err := launcher.Run([]string{"foo", "bar", "1234", upgradeFile}, stdout, stderr) + require.NoError(err) + require.True(doUpgrade) + + // may not be the best way but the fastest way to check we meet the delay + // in addition to comparing both the runtime of this test and TestLaunchProcess in addition + if time.Since(start) < cfg.RestartDelay { + require.FailNow("restart delay not met") + } +} + // TestLaunchProcess will try running the script a few times and watch upgrades work properly // and args are passed through func (s *processTestSuite) TestLaunchProcessWithDownloads() {