-
Notifications
You must be signed in to change notification settings - Fork 275
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
Only load to target version when target version doesn't equal to 0, hi… #102
Only load to target version when target version doesn't equal to 0, hi… #102
Conversation
…gher version won't be loaded
Hi @liamsi , could you take a look at this PR? Thank very much |
Thanks a lot for your pull request!
Can you describe the usecase you are looking at? Will have a closer look today! |
Yes... having a An example of a use case is when you want to implement state rollback to a specific height (c.f. Another use case is to provide atomicity of a commit at the software level without using a databse transaction. For example I first commit my IAVL state at version N that gives me a hash, I then store a mapping from that hash -> N, before finally saving my blockchain state (atomically) with a reference to that hash. If anything goes wrong my last committed blockchain state will point to a hash that points to version N-1 and I can just resume from that version. Any intermediate writes don't appear as inconsistent state because they are not committed and I will overwrite version N of the tree during my next commit (provided I am allowed to do so). It makes sense to be cautious about overwriting state that is meant to be immutable, but I think in this case given that we have However there is an issue with this in that suppose we rollback from version N to version M and then unload the tree (i.e. stop our node) at version L with M <= L < N. If we restart and call |
mutable_tree.go
Outdated
latestVersion := int64(0) | ||
var latestRoot []byte | ||
for version, r := range roots { | ||
tree.versions[version] = true | ||
tree.ndb.traversePrefix([]byte(rootPrefix), func(k, v []byte) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We ought to be able to avoid traversing the entire prefix here. nodedb should expose an traverseRange
which in turn can use db.Iterator([]byte(rootPrefix), ndb.rootKey(targetVersion))
since we know the exact end point.
Come to think of it - do we really need even need to maintain a set of versions that exist? The only place this set is used is to 'fail early' when trying to get a previous tree version. GetImmutable
will throw an error if the version is not found. I do not see why that isn't generally good enough. Then we could avoid this traversal completely (other than in loading the latest version). My feeling is loading/checking previous versions is not something that needs to be optimised with a cache - seems better just to check the database when necessary.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Comos-sdk use IAVL to implement its fundamental store. It implements prune strategy:
https://github.com/cosmos/cosmos-sdk/blob/2c6ed0fefc499410a7734ac93005f32a99b47b5b/store/iavlstore.go#L104
and
https://github.com/cosmos/cosmos-sdk/blob/2c6ed0fefc499410a7734ac93005f32a99b47b5b/store/iavlstore.go#L82
So the entire prefix here will not be too huge. But if some developers don't want to implement prune strategy here it is necessary to expose an traverseRange.
Thanks for your impressive comment.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I currently don't prune (for reference I'm using it here: https://github.com/hyperledger/burrow/blob/develop/execution/state.go).
I'm not terribly worried about the size - it does load fairly quickly.
But my afterthought is probably now my main point: I don't think we need the versions set at all, as in, I think we could remove this field: https://github.com/tendermint/iavl/blob/master/mutable_tree.go#L19
Then all we need to do is load targetVersion
. Later on rather than checking our versions set we can just handle this error gracefully: https://github.com/tendermint/iavl/blob/master/mutable_tree.go#L270. It means less loading up front - and I suspect most old versions never get accessed again.
For loading the latest version we'll need to traverse anyway.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would be happy to implement this in a subsequent PR though - what you have here is essential for my use case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm glad my pull request is helpful for your work. Thanks.
@liamsi |
Hey @HaoyangLiu @silasdavis. Thank you both for all the input and sorry for letting this slip. While the changes made look great and the use-cases you describe make perfect sense, we need to make sure that this doesn't break anything. I think this suggestion made by @silasdavis should maybe land in this PR too:
/cc @jlandrews @jaekwon |
@liamsi @silasdavis |
I think we should refactor getRoots() to return a slice of roots. As is, we still have getRoots() which is only used in once place, a test case (via .roots()), and we have 2 very similar iteration functions... so basically we have 3 functions that iterate over the roots. Then in I'd rename Just FYI I don't think there was a problem with the previous usage of getRoots(), it seems like the way we've been using it, random iteration wasn't a problem. But using an ordered slice is a bit simpler so... |
@liamsi makes a good point that this probably messes with orphan tracking. If we're going to overwrite, we need to also clean up everything related to versions after the version passed w/
And so on. I think the easiest way do deal with this is to delete all versions from the desired Given all of this, I think the best way would be to implement a func LoadVersionForOverwriting(version int64) {
LoadVersion(version)
DeleteVersionsFrom(version+1)
} |
Thanks for your input @jaekwon! BTW @jlandrews initially brought up the concerns about the orphan tracking. @HaoyangLiu thanks for the changes! Do you mind adding tests that document and test the new behaviour? Namely, that the versions are actually deleted (incl. the orphans)? Also, a test that covers the new flag for |
…fix/load_more_verson_than_target
@liamsi |
Will do during the weekend (mostly AFK till Saturday) |
return latestVersion, err | ||
} | ||
tree.deleteVersionsFrom(targetVersion+1) | ||
return targetVersion, nil |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
According to the comment it "returns the version number of the latest version found". Here we just return the argument passed by the caller targetVersion
? Either the comment needs to be fixed, or, we'd need to return latestVersion
instead (or make it more explicit that they are the same?).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry. It is my mistake. I will change the comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No worries! Thank you. But then: why do we even need to return this value if it is already known by the caller. It is not even read in the tests. It's a bit confusing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the targetVersion is greater than the lastest version, then the lastest version will be return. I have modified the test the check the return value.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks! It's still a bit confusing. So it actually returns the latest version together with an error. How is this useful? Usually, in go if you return an error the other return values aren't considered by the caller (usually nil or default values). But I see that this is what LoadVersion
already handles it like this. I think it would be useful if it returned a particular which the caller could check against. But this is probably orthogonal to this PR. No need to change this in this PR.
mutable_tree.go
Outdated
if version > lastestVersion { | ||
break | ||
} | ||
if version == 0 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about version < 0
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, this check is independent of the for loop (and hence, should live outside of the loop)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, version won't less than zero. If version is less than zero, an error will be return in an early position. But changing version == 0
to version <= 0
is much better.
tree_test.go
Outdated
count++ | ||
if count > maxLength { | ||
break | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nitpicking: use a classical for loop instead of break
to increase readability. for count:=1; count <= maxLength; count++ {}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good suggestion. I will modify the code.
tree_test.go
Outdated
@@ -13,6 +13,7 @@ import ( | |||
mathrand "math/rand" | |||
|
|||
cmn "github.com/tendermint/tendermint/libs/common" | |||
"strconv" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The order of imports should be:
- stdlib
- external packages
- internal packages
go-imports can automate this for you.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good suggestion. I have changed the code.
@liamsi |
I think it would be great if the tests also showed that the orphans were actually deleted. But this is also covered by tests calling |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This PR LGTM. I'd feel more comfortable with a second opinion by @jaekwon / @silasdavis / @jlandrews though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good to me too 👍
@ebuchman any reason didn't make it into 0.12.0? |
It did actually make it. It even made it into 0.11.1 which was a pre-release by Jae: https://github.com/tendermint/iavl/commits/v0.11.1 |
…i… (cosmos#102) * Only load to target verion when target version doesn't equal to 0, higher version won't be loaded * add LoadVersionOverwrite * Refactor code according to code review * Refactor comment and error handle * Add overwrite test
Currently, the versions in MutableTree contains all previously saved versions of the tree.
The LoadVersion of MutableTree will mark all existing version to be true. Suppose this situation, current tree version is 10, but I want to load to 9. Then versions[10] will be marked true too. However, I think this is not reasonable. Suppose I want to rewrite the latest version 10 to different value, if version[10] is true, then the new value can't be overwritten. But sometimes overwriting is necessary.
Besides, the above code is the current code for LoadVersion. It has another bug. The variable roots is a map. In golang, map iteration is random. As a result, maybe some lower versions can't be loaded. My PR also fixed this bug. The traversePrefix will return a ascending list.
@liamsi