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

Regen network/multistore upgrades #5500

Merged

Conversation

anilcse
Copy link
Collaborator

@anilcse anilcse commented Jan 9, 2020

Closes: regen-network#7

Description


For contributor use:

  • Targeted PR against correct branch (see CONTRIBUTING.md)
  • Linked to Github issue with discussion and accepted design OR link to spec that describes this work.
  • Code follows the module structure standards.
  • Wrote unit and integration tests
  • Updated relevant documentation (docs/) or specification (x/<module>/spec/)
  • Added relevant godoc comments.
  • Added a relevant changelog entry to the Unreleased section in CHANGELOG.md
  • Re-reviewed Files changed in the Github PR explorer

For admin use:

  • Added appropriate labels to PR (ex. WIP, R4R, docs, etc)
  • Reviewers assigned
  • Squashed all commits, uses message "Merge pull request #XYZ: [title]" (coding standards)

@lgtm-com
Copy link

lgtm-com bot commented Jan 9, 2020

This pull request introduces 1 alert when merging 62ff38f into 869531c - view on LGTM.com

new alerts:

  • 1 for Useless assignment to local variable

x/upgrade/internal/types/storeloader.go Outdated Show resolved Hide resolved
x/upgrade/internal/types/storeloader_test.go Outdated Show resolved Hide resolved
x/upgrade/abci_test.go Outdated Show resolved Hide resolved
x/upgrade/internal/types/storeloader.go Outdated Show resolved Hide resolved
x/upgrade/internal/keeper/keeper.go Outdated Show resolved Hide resolved
@lgtm-com
Copy link

lgtm-com bot commented Jan 10, 2020

This pull request introduces 1 alert when merging 3a71d54 into 3df3887 - view on LGTM.com

new alerts:

  • 1 for Useless assignment to local variable

@lgtm-com
Copy link

lgtm-com bot commented Jan 13, 2020

This pull request introduces 1 alert when merging 28f7bca into d0a5b9b - view on LGTM.com

new alerts:

  • 1 for Useless assignment to local variable

Copy link
Contributor

@ethanfrey ethanfrey left a comment

Choose a reason for hiding this comment

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

A number of minor cleanup comments. However, two major design questions:

  1. I see the two loaders: StoreLoaderWithUpgrade and UpgradeableStoreLoader. And a bit confused about usage. UpgradeableStoreLoader requires loading the migration paths from disks, which I believe is now a deprecated approach. StoreLoaderWithUpgrade seems to do what the PR requests, but is marked as only for testing. The usage should be clarified.

The other issue I have is how the upgrade handler is supposed to set this. I think @aaronc needs to answer this (ideally showing a code sample that compiles). From the issue:

Change the upgrade.Keeper SetUpgradeHandlermethod to:SetUpgradeHandler(name string, upgradeHandler types.UpgradeHandler, storeUpgrades []storytypes.StoreUpgrades)`
so that whenever store key renames/deletions are needed they can be registered with the upgrade handler

Add a hook to the upgrade module that configures BaseApp and the multi-store before starting the ABCI app. The multi-store upgrades should only be performed when there is an upgrade-info.json present and the version of the store matches the upgrade height in the json file. This will prevent store upgrades from happening too early.

Clearly the argument is still not added to the SetHandler method. However, I see no clean way to add such a callback. I doubt BaseApp is ever supposed to be passed into a module, and not sure about import cycles and such. Also, the issue is that this info must be available without any calls to the KVStore. (Thus the original design to load from json file). If there is zero or one SetHandler registered, then we can use the data (or nil) as the pending store upgrades. And expose some call GetPendingStoreUpgrades that just inspects the map set up by SetHandler. BUT, if there are multiple SetHandler calls in the app, we cannot check the db to see which is pending to properly implement this.

I would recommend NOT doing this in SetHandler, and just adding this directly in app.go. In fact app.go must be updated for each upgrade, and is in the new binary, so it can store this info.

So, when preparing for an upgrade, we would have two lines:

# prepare the next update upgrade handler
upgradeKeeper.SetHandler("next-update", migrationFunc)
# only if we want to do upgrades in next update 
storeLoader := upgrade.StoreLoaderWithUpgrade(storeUpgradesForNextUpdate)
app.SetStoreLoader(storeLoader)

simapp/app.go Outdated
@@ -196,7 +196,7 @@ func NewSimApp(
app.CrisisKeeper = crisis.NewKeeper(
app.subspaces[crisis.ModuleName], invCheckPeriod, app.SupplyKeeper, auth.FeeCollectorName,
)
app.UpgradeKeeper = upgrade.NewKeeper(skipUpgradeHeights, keys[upgrade.StoreKey], app.cdc)
app.UpgradeKeeper = upgrade.NewKeeper(skipUpgradeHeights, keys[upgrade.StoreKey], app.cdc, DefaultNodeHome)
Copy link
Contributor

Choose a reason for hiding this comment

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

This is just the default home. What happens when running with a different home flag (--home=/foo/bar)? I think you need to read the home path with viper, like I did in wasmd: https://github.com/cosmwasm/wasmd/blob/master/app/app.go#L213-L215

If you have a better way to get this info, let me know and we can fix both binaries. But this was the best way I could think of to get the home dir dynamically

Copy link
Member

Choose a reason for hiding this comment

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

@ethanfrey that is up to the specific application. You can see how that is handled in gaia

Copy link
Contributor

Choose a reason for hiding this comment

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

Actually the whole home processing stuff is burried in the client libs in cosmos-sdk not in the app. The default is set by app, but how we override with --home or --home-client is standardized

return err
}

var upgradeInfo store.UpgradeInfo
Copy link
Contributor

Choose a reason for hiding this comment

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

So UpgradeInfo includes StoreUpgrades. Where do we set those? I never see those set by searching the diff (but maybe I missed it)

Copy link
Member

Choose a reason for hiding this comment

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

The idea I had was for just Plan to get serialized to json. I think that's likely how it should work. The store upgrades wouldn't get serialized to disk here.

x/upgrade/internal/keeper/keeper.go Outdated Show resolved Hide resolved

// StoreLoaderWithUpgrade is used to prepare baseapp with a fixed StoreLoader
// pattern. This is useful in test cases, or with custom upgrade loading logic.
func StoreLoaderWithUpgrade(storeUpgrades *storetypes.StoreUpgrades) baseapp.StoreLoader {
Copy link
Contributor

Choose a reason for hiding this comment

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

If this is called in app.go (which I think it is), then we should do the viper stuff there, and just pass this as a second argument. You need to update app.go to use viper.GetString(flags.FlagHome) instead of DefaultNodeHome when creating the upgrade keeper as well, so let's just use that value and pass it in here as well.

Copy link
Member

Choose a reason for hiding this comment

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

keeping viper calls out of the modules and pushing them up into the cobra cli code is preferred.

}

// Check if the current commit version and upgrade height matches
if (len(upgrades.StoreUpgrades.Renamed) > 0 || len(upgrades.StoreUpgrades.Deleted) > 0) &&
Copy link
Contributor

Choose a reason for hiding this comment

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

This looks like similar logic (but cleaned up) from the original upgrade handler.

Big issue here, is that we now never write the store upgrades from the pre-upgrade binary (design was that they would be in the proposal), but use them from the post-upgrade binary (written in code). I understand the reason behind this change, however, here we check value from file.

I would actually remove the StoreUpgrades field form UpgradeInfo as it is confusing and not used elsewhere in the code (I think). And you should be checking eg. len(storeUpgrades.Renamed) here. I see you do use the proper storeUpgrades as an argument to ms.LoadLatestVersionAndUpgrade, so it seems that you just missed to upgrade the if statement.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah,sorry missed this. Changed in next commit

//
// This is useful for in place migrations when a store key is renamed between
// two versions of the software.
func UpgradeableStoreLoader(upgradeInfoPath string) baseapp.StoreLoader {
Copy link
Contributor

Choose a reason for hiding this comment

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

This is an implementation of the older technique, right?
The code looks correct to me, but I am a bit confused when and where we would use one or the other.

Copy link
Contributor

Choose a reason for hiding this comment

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

This was to address store changes saved to disk, but as that is deprecated I'll remove the one which takes path


upgradeInfoFilePath := filepath.Join(homeDir, "upgrade-info.json")

data := []byte(`{"height": 0, "store_upgrades": {"renamed":[{"old_key": "bnk", "new_key": "bank"}]}}`)
Copy link
Contributor

Choose a reason for hiding this comment

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

I assume to use a non-zero height? We want to use the same height on BeginBlock, right? They are supposed to check equality.

If this is eg. height=2, then we mount as below and show that no migrations happen (even with the migration handler installed). Then commit eg. 2 blocks, so the iavl tree is now stored at height 2. Then mount the store again (with committed height =2) and validate the stores were renamed properly.

A bit more complicated, but important to ensure the checks for "only at height" work out properly

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, will clean this up

return err
}

var upgradeInfo store.UpgradeInfo
Copy link
Member

Choose a reason for hiding this comment

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

The idea I had was for just Plan to get serialized to json. I think that's likely how it should work. The store upgrades wouldn't get serialized to disk here.

@aaronc
Copy link
Member

aaronc commented Jan 30, 2020

So, when preparing for an upgrade, we would have two lines:

# prepare the next update upgrade handler
upgradeKeeper.SetHandler("next-update", migrationFunc)
# only if we want to do upgrades in next update 
storeLoader := upgrade.StoreLoaderWithUpgrade(storeUpgradesForNextUpdate)
app.SetStoreLoader(storeLoader)

Since this is a pretty infrequent operation, I think something that puts the burden in app.go is okay. Basically somewhere, maybe even just in app.go there needs to be some logic that:

  1. reads upgrade height and upgrade name from upgrade-info.json
  2. checks if there are are store upgrades for this upgrade name
  3. checks if the current store version is equal to the upgrade height in upgrade-info.json
  4. if so, apply the store upgrades in initialization

So this could be as simple as something like:

upgradeName, upgradeHeight := upgradeKeeper.ReadUpgradeInfoFromDisk()
if upgradeName == "myUpgrade" {
  // configure store loader that checks if version == upgradeHeight and applies store upgrades
  app.SetStoreLoader(UpgradeStoreLoader(upgradeHeight, storeUpgrades)
}

This could also be more self-contained if we added a store upgrades parameter to SetHandler:

upgradeKeeper.SetHandler("next-update", migrationFunc, storeUpgrades)
app.SetStoreLoader(upgradeKeeper.UpgradeStoreLoader())

Happy to get on a quick call to talk this through if that would help.

@lgtm-com
Copy link

lgtm-com bot commented Jan 31, 2020

This pull request introduces 1 alert when merging 3cc4be0 into 6890feb - view on LGTM.com

new alerts:

  • 1 for Useless assignment to local variable

Copy link
Member

@aaronc aaronc left a comment

Choose a reason for hiding this comment

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

utACK

@anilcse
Copy link
Collaborator Author

anilcse commented Feb 21, 2020

@alexanderbez can you check this PR

Copy link
Contributor

@alexanderbez alexanderbez left a comment

Choose a reason for hiding this comment

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

Looks good @anilcse! I left some minor feedback on the PR. Take a look :)

simapp/app.go Outdated
@@ -189,7 +192,7 @@ func NewSimApp(
app.CrisisKeeper = crisis.NewKeeper(
app.subspaces[crisis.ModuleName], invCheckPeriod, app.SupplyKeeper, auth.FeeCollectorName,
)
app.UpgradeKeeper = upgrade.NewKeeper(skipUpgradeHeights, keys[upgrade.StoreKey], app.cdc)
app.UpgradeKeeper = upgrade.NewKeeper(skipUpgradeHeights, keys[upgrade.StoreKey], app.cdc, viper.GetString(flags.FlagHome))
Copy link
Contributor

Choose a reason for hiding this comment

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

The viper dep should not be used here. We need to figure out a way to provide the home value app constructor instead.

Something we need to address in another PR is really cleaning up the app constructor. We should use variadic option functions maybe.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This might help: #5548

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, we should address that issue. But I would suggest doing it separately to keep this PR as lean as possible. For the time being, simply have the SimApp constructor take a new parameter or even introduce variadic options.

@@ -44,6 +44,13 @@ type StoreUpgrades struct {
Deleted []string `json:"deleted"`
}

// UpgradeInfo defines height and name of the upgrade
// to ensure multistore upgrades happens only at matching height
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
// to ensure multistore upgrades happens only at matching height
// to ensure multistore upgrades happen only at matching height.

Comment on lines 42 to 43
// Write upgrade info to disk
// UpgradeStoreLoader uses this info to perform or skip store migrations
Copy link
Contributor

Choose a reason for hiding this comment

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

// Write the upgrade info to disk. The UpgradeStoreLoader uses this info to perform or skip
// store migrations.

x/upgrade/abci_test.go Show resolved Hide resolved
@@ -130,3 +140,66 @@ func (k Keeper) ApplyUpgrade(ctx sdk.Context, plan types.Plan) {
func (k Keeper) IsSkipHeight(height int64) bool {
return k.skipUpgradeHeights[height]
}

// DumpUpgradeInfoToDisk adds plan height to upgrade-info.json
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
// DumpUpgradeInfoToDisk adds plan height to upgrade-info.json
// DumpUpgradeInfoToDisk writes upgrade information to UpgradeInfoFileName.

func (k Keeper) GetUpgradeInfoPath() (string, error) {
upgradeInfoFileDir := path.Join(k.GetHomeDir(), "data")

if _, err := os.Stat(upgradeInfoFileDir); os.IsNotExist(err) {
Copy link
Contributor

Choose a reason for hiding this comment

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

import 	tmos "github.com/tendermint/tendermint/libs/os"

Use tmos.EnsureDir here instead of duplicating code 👍

}

// GetHomeDir returns the height at which the given upgrade was executed
func (k Keeper) GetHomeDir() string {
Copy link
Contributor

Choose a reason for hiding this comment

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

Does this need to be exposed/used anywhere outside the keeper?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Not really. I will change that


var upgradeInfo store.UpgradeInfo
json.Unmarshal(data, &upgradeInfo)
return upgradeInfo.Name, upgradeInfo.Height
Copy link
Contributor

Choose a reason for hiding this comment

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

You're returning all the values from the UpgradeInfo type. You might as well just return the entire UpgradeInfo struct which will cleanup the method signature 👍

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Updated the required changes

x/upgrade/spec/01_concepts.md Show resolved Hide resolved
@@ -56,6 +56,17 @@ During each `EndBlock` execution, the `x/upgrade` module checks if there exists
`Handler` is executed. If the `Plan` is expected to execute but no `Handler` is registered
or if the binary was upgraded too early, the node will gracefully panic and exit.

## StoreLoader
The `x/upgrade` module also facilitates store migrations as part of the upgrade. The
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we certainly need way more detail and explanation here. This is a core component of the upgrade procedure. I'd elaborate and explain concepts and procedures such as when and how a file is written to disk. What the file contains and why. How the file is used to migrate state....etc.

Copy link
Contributor

@alexanderbez alexanderbez left a comment

Choose a reason for hiding this comment

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

ACK -- just need to resolve the merge conflict ;)

…ork/multistore-upgrades

# Conflicts:
#	x/upgrade/keeper/keeper.go
…network/cosmos-sdk into regen-network/multistore-upgrades
@anilcse
Copy link
Collaborator Author

anilcse commented Feb 27, 2020

ACK -- just need to resolve the merge conflict ;)

Updated

@alexanderbez alexanderbez merged commit 0b74491 into cosmos:master Feb 28, 2020
@ryanchristo ryanchristo deleted the regen-network/multistore-upgrades branch December 12, 2022 18:16
larry0x pushed a commit to larry0x/cosmos-sdk that referenced this pull request May 22, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Support multi-store upgrades that need to happen outside of BeginBlock
7 participants