Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support in-place migration ordering #10614

Merged
merged 18 commits into from
Jan 5, 2022
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 54 additions & 37 deletions docs/core/upgrade.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,20 +22,6 @@ This document provides steps to use the In-Place Store Migrations upgrade method

Each module gets assigned a consensus version by the module developer. The consensus version serves as the breaking change version of the module. The Cosmos SDK keeps track of all module consensus versions in the x/upgrade `VersionMap` store. During an upgrade, the difference between the old `VersionMap` stored in state and the new `VersionMap` is calculated by the Cosmos SDK. For each identified difference, the module-specific migrations are run and the respective consensus version of each upgraded module is incremented.

## Genesis State

When starting a new chain, the consensus version of each module must be saved to state during the application's genesis. To save the consensus version, add the following line to the `InitChainer` method in `app.go`:

```diff
func (app *MyApp) InitChainer(ctx sdk.Context, req abci.RequestInitChain) abci.ResponseInitChain {
...
+ app.UpgradeKeeper.SetModuleVersionMap(ctx, app.mm.GetVersionMap())
...
}
```

This information is used by the Cosmos SDK to detect when modules with newer versions are introduced to the app.

### Consensus Version

The consensus version is defined on each app module by the module developer and serves as the breaking change version of the module. The consensus version informs the Cosmos SDK on which modules need to be upgraded. For example, if the bank module was version 2 and an upgrade introduces bank module 3, the Cosmos SDK upgrades the bank module and runs the "version 2 to 3" migration script.
Expand Down Expand Up @@ -64,20 +50,38 @@ Migrations are run inside of an `UpgradeHandler` using `app.mm.RunMigrations(ct

```go
cfg := module.NewConfigurator(...)
app.UpgradeKeeper.SetUpgradeHandler("my-plan", func(ctx sdk.Context, plan upgradetypes.Plan, vm module.VersionMap) (module.VersionMap, error) {
app.UpgradeKeeper.SetUpgradeHandler("my-plan", func(ctx sdk.Context, plan upgradetypes.Plan, fromVM module.VersionMap) (module.VersionMap, error) {

// ...
// do upgrade logic
// ...

// RunMigrations returns the VersionMap
// with the updated module ConsensusVersions
return app.mm.RunMigrations(ctx, vm)
orderedVersions = module.DefaultMigrationsOrder(fromVM)
robert-zaremba marked this conversation as resolved.
Show resolved Hide resolved
// returns a VersionMap with the updated module ConsensusVersions
return app.mm.RunMigrations(ctx, orderedVersions)
})
```

To learn more about configuring migration scripts for your modules, see the [Module Upgrade Guide](../building-modules/upgrade.md).

### Order Of Migrations

All migrations are run in (priority desc, alphabetical asc) based on the values in the `fromVM` map. By default all modules have the same priority = 100, except `x/auth` which is run last and has priority = 99. The reason is state dependencies between x/auth and other modules (you can read more in [issue #10606](https://github.com/cosmos/cosmos-sdk/issues/10606)).

If you want to change the order of migration then you need to change module priority number. For example, you want to run `foo` last then you can use this code in `SetUpgradeHandler`:

```go
max := 0
for _, mv := range orderedVersions {
if mv.Priority > max {
max = mv.Priority
}
}
orderedVersions["foo"].Priority = max + 1
return app.mm.RunMigrations(ctx, cfg, orderedVersions)
})
```

## Adding New Modules During Upgrades

You can introduce entirely new modules to the application during an upgrade. New modules are recognized because they have not yet been registered in `x/upgrade`'s `VersionMap` store. In this case, `RunMigrations` calls the `InitGenesis` function from the corresponding module to set up its initial state.
Expand Down Expand Up @@ -105,7 +109,35 @@ if upgradeInfo.Name == "my-plan" && !app.UpgradeKeeper.IsSkipHeight(upgradeInfo.
}
```

## Overwriting Genesis Functions
## Genesis State

When starting a new chain, the consensus version of each module MUST be saved to state during the application's genesis. To save the consensus version, add the following line to the `InitChainer` method in `app.go`:

```diff
func (app *MyApp) InitChainer(ctx sdk.Context, req abci.RequestInitChain) abci.ResponseInitChain {
...
+ app.UpgradeKeeper.SetModuleVersionMap(ctx, app.mm.GetVersionMap())
...
}
```

This information is used by the Cosmos SDK to detect when modules with newer versions are introduced to the app.

For a new module `foo`, `InitGenesis` is called by the `RunMigration` only when there is a new module registered in the module manager and there is no `foo` entry in the `fromVM` registered in the state. Therefore, if you want to skip `InitGenesis` when a new module is added to the app, then you should set its module version in `fromVM` to the module package consensus version:

```go
app.UpgradeKeeper.SetUpgradeHandler("my-plan", func(ctx sdk.Context, plan upgradetypes.Plan, fromVM module.VersionMap) (module.VersionMap, error) {
// ...

// Set foo's version to the latest ConsensusVersion in the VersionMap.
// This will skip running InitGenesis on Foo
fromVM[foo.ModuleName] = foo.AppModule{}.ConsensusVersion()

return app.mm.RunMigrations(ctx, fromVM)
})
```

### Overwriting Genesis Functions

The Cosmos SDK offers modules that the application developer can import in their app. These modules often have an `InitGenesis` function already defined.

Expand All @@ -118,32 +150,17 @@ You MUST manually set the consensus version in the version map passed to the `Up
```go
import foo "github.com/my/module/foo"

app.UpgradeKeeper.SetUpgradeHandler("my-plan", func(ctx sdk.Context, plan upgradetypes.Plan, vm module.VersionMap) (module.VersionMap, error) {
app.UpgradeKeeper.SetUpgradeHandler("my-plan", func(ctx sdk.Context, plan upgradetypes.Plan, fromVM module.VersionMap) (module.VersionMap, error) {

// Register the consensus version in the version map
// to avoid the SDK from triggering the default
// InitGenesis function.
vm["foo"] = foo.AppModule{}.ConsensusVersion()
fromVM["foo"] = foo.AppModule{}.ConsensusVersion()

// Run custom InitGenesis for foo
app.mm["foo"].InitGenesis(ctx, app.appCodec, myCustomGenesisState)

return app.mm.RunMigrations(ctx, cfg, vm)
})
```

If you do not have a custom genesis function and want to skip the module's default genesis function, you can simply register the module with the version map in the `UpgradeHandler` as shown in the example:

```go
import foo "github.com/my/module/foo"

app.UpgradeKeeper.SetUpgradeHandler("my-plan", func(ctx sdk.Context, plan upgradetypes.Plan, vm module.VersionMap) (module.VersionMap, error) {

// Set foo's version to the latest ConsensusVersion in the VersionMap.
// This will skip running InitGenesis on Foo
vm["foo"] = foo.AppModule{}.ConsensusVersion()

return app.mm.RunMigrations(ctx, cfg, vm)
return app.mm.RunMigrations(ctx, cfg, fromVM)
})
```

Expand Down
4 changes: 4 additions & 0 deletions simapp/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,10 @@ func NewSimApp(
group.ModuleName,
)

if err := app.mm.SetOrderMigrations(module.DefaultMigrationsOrder(app.mm.Modules)...); err != nil {
panic(err)
}

app.mm.RegisterInvariants(&app.CrisisKeeper)
app.mm.RegisterRoutes(app.legacyRouter, app.QueryRouter(), encodingConfig.Amino)
app.configurator = module.NewConfigurator(app.appCodec, app.msgSvcRouter, app.GRPCQueryRouter())
Expand Down
6 changes: 3 additions & 3 deletions types/errors/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,10 +160,10 @@ var (
// Examples: not DB domain error, file writing etc...
ErrIO = Register(RootCodespace, 39, "Internal IO error")

// ErrAppConfig defines an error occurred if min-gas-prices field in BaseConfig is empty.
ErrAppConfig = Register(RootCodespace, 40, "error in app.toml")

// ErrPanic is only set when we recover from a panic, so we know to
// redact potentially sensitive system info
ErrPanic = errorsmod.ErrPanic

// ErrAppConfig defines an error occurred if min-gas-prices field in BaseConfig is empty.
ErrAppConfig = Register(RootCodespace, 40, "error in app.toml")
)
79 changes: 54 additions & 25 deletions types/module/module.go
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@ type Manager struct {
OrderExportGenesis []string
OrderBeginBlockers []string
OrderEndBlockers []string
OrderMigrations []string
}

// NewManager creates a new Manager object
Expand All @@ -253,28 +254,36 @@ func NewManager(modules ...AppModule) *Manager {

// SetOrderInitGenesis sets the order of init genesis calls
func (m *Manager) SetOrderInitGenesis(moduleNames ...string) {
m.checkForgottenModules("SetOrderInitGenesis", moduleNames)
m.assertNoForgottenModules("SetOrderInitGenesis", moduleNames)
m.OrderInitGenesis = moduleNames
}

// SetOrderExportGenesis sets the order of export genesis calls
func (m *Manager) SetOrderExportGenesis(moduleNames ...string) {
m.checkForgottenModules("SetOrderExportGenesis", moduleNames)
m.assertNoForgottenModules("SetOrderExportGenesis", moduleNames)
m.OrderExportGenesis = moduleNames
}

// SetOrderBeginBlockers sets the order of set begin-blocker calls
func (m *Manager) SetOrderBeginBlockers(moduleNames ...string) {
m.checkForgottenModules("SetOrderBeginBlockers", moduleNames)
m.assertNoForgottenModules("SetOrderBeginBlockers", moduleNames)
m.OrderBeginBlockers = moduleNames
}

// SetOrderEndBlockers sets the order of set end-blocker calls
func (m *Manager) SetOrderEndBlockers(moduleNames ...string) {
m.checkForgottenModules("SetOrderEndBlockers", moduleNames)
m.assertNoForgottenModules("SetOrderEndBlockers", moduleNames)
m.OrderEndBlockers = moduleNames
}

// SetOrderMigrations sets the order of migrations to be run. If not set
// then migrations will be run with an order defined in `DefaultMigrationsOrder`.
// Function will return error if the order is not complete (eg a module won't be defined).
func (m *Manager) SetOrderMigrations(moduleNames ...string) {
m.assertNoForgottenModules("SetOrderMigrations", moduleNames)
m.OrderMigrations = moduleNames
}

// RegisterInvariants registers all module invariants
func (m *Manager) RegisterInvariants(ir sdk.InvariantRegistry) {
for _, module := range m.Modules {
Expand Down Expand Up @@ -345,24 +354,29 @@ func (m *Manager) ExportGenesis(ctx sdk.Context, cdc codec.JSONCodec) map[string
return genesisData
}

// checkForgottenModules checks that we didn't forget any modules in the
// assertNoForgottenModules checks that we didn't forget any modules in the
// SetOrder* functions.
func (m *Manager) checkForgottenModules(setOrderFnName string, moduleNames []string) {
setOrderMap := map[string]struct{}{}
func (m *Manager) assertNoForgottenModules(setOrderFnName string, moduleNames []string) {
ms := make(map[string]bool)
for _, m := range moduleNames {
setOrderMap[m] = struct{}{}
ms[m] = true
}

if len(setOrderMap) != len(m.Modules) {
panic(fmt.Sprintf("got %d modules in the module manager, but %d modules in %s", len(m.Modules), len(setOrderMap), setOrderFnName))
var missing []string
for m := range m.Modules {
if !ms[m] {
missing = append(missing, m)
}
}
if len(missing) != 0 {
panic(fmt.Sprintf(
"%s: all modules must be defined when setting SetOrderMigrations, missing: %v", setOrderFnName, missing))
}
}

// MigrationHandler is the migration function that each module registers.
type MigrationHandler func(sdk.Context) error

// VersionMap is a map of moduleName -> version, where version denotes the
// version from which we should perform the migration for each module.
// VersionMap is a map of moduleName -> version
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the second part was not relevant to this type. VersionMap is just a map - it's also use as a return type in RunMigrations

type VersionMap map[string]uint64

// RunMigrations performs in-place store migrations for all modules. This
Expand All @@ -389,6 +403,10 @@ type VersionMap map[string]uint64
// `InitGenesis` on that module.
// - return the `updatedVM` to be persisted in the x/upgrade's store.
//
// Migrations are run in an alphabetical order, except x/auth which is run last. If you want
// to change the order then you should run migrations in multiple stages as described in
// docs/core/upgrade.md.
//
// As an app developer, if you wish to skip running InitGenesis for your new
// module "foo", you need to manually pass a `fromVM` argument to this function
// foo's module version set to its latest ConsensusVersion. That way, the diff
Expand Down Expand Up @@ -416,18 +434,8 @@ func (m Manager) RunMigrations(ctx sdk.Context, cfg Configurator, fromVM Version
return nil, sdkerrors.Wrapf(sdkerrors.ErrInvalidType, "expected %T, got %T", configurator{}, cfg)
}

updatedVM := make(VersionMap)
// for deterministic iteration order
// (as some migrations depend on other modules
// and the order of executing migrations matters)
// TODO: make the order user-configurable?
sortedModNames := make([]string, 0, len(m.Modules))
for key := range m.Modules {
sortedModNames = append(sortedModNames, key)
}
sort.Strings(sortedModNames)

for _, moduleName := range sortedModNames {
updatedVM := VersionMap{}
for _, moduleName := range m.OrderMigrations {
module := m.Modules[moduleName]
fromVersion, exists := fromVM[moduleName]
toVersion := module.ConsensusVersion()
Expand Down Expand Up @@ -514,3 +522,24 @@ func (m *Manager) GetVersionMap() VersionMap {

return vermap
}

// Returns a default migrations ordres: ascending alphabetical by module name,
robert-zaremba marked this conversation as resolved.
Show resolved Hide resolved
// except x/auth which will run last.
robert-zaremba marked this conversation as resolved.
Show resolved Hide resolved
// TODO: write tests
func DefaultMigrationsOrder(modules []string) []string {
const authName = "auth"
out := make([]string, 0, len(modules))
hasAuth := false
for _, m := range modules {
if m == authName {
hasAuth = true
} else {
out = append(out, m)
}
}
sort.Strings(out)
if hasAuth {
out = append(out, authName)
}
return out
}
37 changes: 37 additions & 0 deletions types/module/module_int_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package module

import (
"testing"

"github.com/stretchr/testify/suite"
)

func TestModuleIntSuite(t *testing.T) {
suite.Run(t, new(TestSuite))
}

type TestSuite struct {
suite.Suite
}

func (s TestSuite) TestAssertNoForgottenModules() {
m := Manager{
Modules: map[string]AppModule{"a": nil, "b": nil},
}
tcs := []struct {
name string
positive bool
modules []string
}{
{"same modules", true, []string{"a", "b"}},
{"more modules", true, []string{"a", "b", "c"}},
}

for _, tc := range tcs {
if tc.positive {
m.assertNoForgottenModules("x", tc.modules)
} else {
s.Panics(func() { m.assertNoForgottenModules("x", tc.modules) })
}
}
}