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

Fast Cache - Downgrade - reupgrade protection and other improvements #12

Merged
merged 27 commits into from
Feb 7, 2022
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
6350dc8
add leaf hash to fast node and unit test
p0mvn Jan 26, 2022
5f579c5
refactor get with index and get by index, fix migration in load versi…
p0mvn Jan 26, 2022
f6ebabb
use Get in GetVersioned of mutable tree
p0mvn Jan 27, 2022
89633f7
refactor non membership proof to use fast storage if available
p0mvn Jan 27, 2022
75d9f81
bench non-membership proof
p0mvn Jan 27, 2022
19ee7d3
fix bench tests to work with the new changes
p0mvn Jan 28, 2022
cbf2f41
add downgrade-reupgrade protection and unit test
p0mvn Jan 28, 2022
96208dc
remove leaf hash from fast node
p0mvn Jan 28, 2022
5217e71
resolve multithreading bug related to iterators not being closed
p0mvn Jan 28, 2022
ea072a6
clean up
p0mvn Jan 28, 2022
806b1a0
use correct tree in bench tests
p0mvn Jan 29, 2022
3f24e71
add cache to tree used to bench non membership proofs
p0mvn Jan 29, 2022
0cb480e
add benc tests for GetWithIndex and GetByIndex
p0mvn Jan 29, 2022
ee1ab84
revert GetWithIndex and GetByIndex
p0mvn Jan 29, 2022
2edb8cc
remove unused import
p0mvn Jan 29, 2022
9712e87
unit test re-upgrade protection and fix small issues
p0mvn Jan 30, 2022
eea8cfd
remove redundant setStorageVersion method
p0mvn Jan 31, 2022
cd0a61c
fix bug with appending to live stage version to storage version and n…
p0mvn Jan 31, 2022
8586d10
add comment for setFastStorageVersionToBatch
p0mvn Jan 31, 2022
c293938
refactor and improve unit tests for reupgrade protection
p0mvn Jan 31, 2022
eae9425
rename ndb's isFastStorageEnabled to hasUpgradedToFastStorage and add…
p0mvn Jan 31, 2022
2241204
comment out new implementation for GetNonMembershipProof
p0mvn Jan 31, 2022
31710b3
update comments in nodedb to reflect the difference between hasUpgrad…
p0mvn Jan 31, 2022
c0e4776
refactor nodedb tests
p0mvn Jan 31, 2022
51b6c2f
downgrade tendermint to 0.34.14 - osmosis's latest cosmos sdk does no…
p0mvn Feb 3, 2022
a7e7fb2
fix bug where fast storage was not enabled when version 0 was attempt…
p0mvn Feb 7, 2022
f78896b
implement unsaved fast iterator to be used in mutable tree (#16)
p0mvn Feb 7, 2022
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
31 changes: 27 additions & 4 deletions benchmarks/bench_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ func commitTree(b *testing.B, t *iavl.MutableTree) {

// queries random keys against live state. Keys are almost certainly not in the tree.
func runQueriesFast(b *testing.B, t *iavl.MutableTree, keyLen int) {
require.True(b, t.IsFastCacheEnabled())
for i := 0; i < b.N; i++ {
q := randBytes(keyLen)
t.Get(q)
Expand All @@ -67,6 +68,7 @@ func runQueriesFast(b *testing.B, t *iavl.MutableTree, keyLen int) {

// queries keys that are known to be in state
func runKnownQueriesFast(b *testing.B, t *iavl.MutableTree, keys [][]byte) {
require.True(b, t.IsFastCacheEnabled()) // to ensure fast storage is enabled
l := int32(len(keys))
for i := 0; i < b.N; i++ {
q := keys[rand.Int31n(l)]
Expand All @@ -75,22 +77,43 @@ func runKnownQueriesFast(b *testing.B, t *iavl.MutableTree, keys [][]byte) {
}

func runQueriesSlow(b *testing.B, t *iavl.MutableTree, keyLen int) {
b.StopTimer()
// Save version to get an old immutable tree to query against,
// Fast storage is not enabled on old tree versions, allowing us to bench the desired behavior.
_, version, err := t.SaveVersion()
require.NoError(b, err)

itree, err := t.GetImmutable(version - 1)
require.NoError(b, err)
require.False(b, itree.IsFastCacheEnabled()) // to ensure fast storage is not enabled

b.StartTimer()
for i := 0; i < b.N; i++ {
q := randBytes(keyLen)
t.GetWithIndex(q)
itree.GetWithIndex(q)
}
}

func runKnownQueriesSlow(b *testing.B, t *iavl.MutableTree, keys [][]byte) {
b.StopTimer()
// Save version to get an old immutable tree to query against,
// Fast storage is not enabled on old tree versions, allowing us to bench the desired behavior.
_, version, err := t.SaveVersion()
require.NoError(b, err)

itree, err := t.GetImmutable(version - 1)
require.NoError(b, err)
require.False(b, itree.IsFastCacheEnabled()) // to ensure fast storage is not enabled
b.StartTimer()
l := int32(len(keys))
for i := 0; i < b.N; i++ {
q := keys[rand.Int31n(l)]
t.GetWithIndex(q)
itree.GetWithIndex(q)
}
}

func runIterationFast(b *testing.B, t *iavl.MutableTree, expectedSize int) {
require.True(b, t.IsFastCacheEnabled()) // to ensure that fast iterator is returned.
require.True(b, t.IsFastCacheEnabled()) // to ensure fast storage is enabled
for i := 0; i < b.N; i++ {
itr := t.ImmutableTree.Iterator(nil, nil, false)
iterate(b, itr, expectedSize)
Expand All @@ -100,7 +123,7 @@ func runIterationFast(b *testing.B, t *iavl.MutableTree, expectedSize int) {

func runIterationSlow(b *testing.B, t *iavl.MutableTree, expectedSize int) {
for i := 0; i < b.N; i++ {
itr := iavl.NewIterator(nil, nil, false, t.ImmutableTree)
itr := iavl.NewIterator(nil, nil, false, t.ImmutableTree) // create slow iterator directly
iterate(b, itr, expectedSize)
itr.Close()
}
Expand Down
4 changes: 2 additions & 2 deletions export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,8 @@ func setupExportTreeRandom(t *testing.T) *ImmutableTree {
keySize = 16
valueSize = 16

versions = 32 // number of versions to generate
versionOps = 4096 // number of operations (create/update/delete) per version
versions = 8 // number of versions to generate
versionOps = 1024 // number of operations (create/update/delete) per version
updateRatio = 0.4 // ratio of updates out of all operations
deleteRatio = 0.2 // ratio of deletes out of all operations
)
Expand Down
5 changes: 2 additions & 3 deletions fast_iterator.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,12 +123,11 @@ func (iter *FastIterator) Next() {

// Close implements dbm.Iterator
func (iter *FastIterator) Close() error {
iter.fastIterator = nil
iter.valid = false

if iter.fastIterator != nil {
iter.err = iter.fastIterator.Close()
}
iter.valid = false
iter.fastIterator = nil
return iter.err
}

Expand Down
3 changes: 1 addition & 2 deletions fast_node.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ type FastNode struct {
key []byte
versionLastUpdatedAt int64
value []byte
// leafHash []byte // TODO: Look into if this would help with proof stuff.
}

// NewFastNode returns a new fast node from a value and version.
Expand All @@ -34,7 +33,7 @@ func DeserializeFastNode(key []byte, buf []byte) (*FastNode, error) {

val, _, cause := decodeBytes(buf)
if cause != nil {
return nil, errors.Wrap(cause, "decoding node.value")
return nil, errors.Wrap(cause, "decoding fastnode.value")
}

fastNode := &FastNode{
Expand Down
58 changes: 58 additions & 0 deletions fast_node_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package iavl

import (
"bytes"
"encoding/hex"
"testing"

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

func TestFastNode_encodedSize(t *testing.T) {
fastNode := &FastNode{
key: randBytes(10),
versionLastUpdatedAt: 1,
value: randBytes(20),
}

expectedSize := 1 + len(fastNode.value) + 1

require.Equal(t, expectedSize, fastNode.encodedSize())
}

func TestFastNode_encode_decode(t *testing.T) {
testcases := map[string]struct {
node *FastNode
expectHex string
expectError bool
}{
"nil": {nil, "", true},
"empty": {&FastNode{}, "0000", false},
"inner": {&FastNode{
key: []byte{0x4},
versionLastUpdatedAt: 1,
value: []byte{0x2},
}, "020102", false},
}
for name, tc := range testcases {
tc := tc
t.Run(name, func(t *testing.T) {
var buf bytes.Buffer
err := tc.node.writeBytes(&buf)
if tc.expectError {
require.Error(t, err)
return
}
require.NoError(t, err)
require.Equal(t, tc.expectHex, hex.EncodeToString(buf.Bytes()))

node, err := DeserializeFastNode(tc.node.key, buf.Bytes())
require.NoError(t, err)
// since value and leafHash are always decoded to []byte{} we augment the expected struct here
if tc.node.value == nil {
tc.node.value = []byte{}
}
require.Equal(t, tc.node, node)
})
}
}
5 changes: 3 additions & 2 deletions immutable_tree.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ func (t *ImmutableTree) Export() *Exporter {
//
// The index is the index in the list of leaf nodes sorted lexicographically by key. The leftmost leaf has index 0.
// It's neighbor has index 1 and so on.
func (t *ImmutableTree) GetWithIndex(key []byte) (index int64, value []byte) {
func (t *ImmutableTree) GetWithIndex(key []byte) (int64, []byte) {
if t.root == nil {
return 0, nil
}
Expand Down Expand Up @@ -201,6 +201,7 @@ func (t *ImmutableTree) GetByIndex(index int64) (key []byte, value []byte) {
if t.root == nil {
return nil, nil
}

return t.root.getByIndex(t, index)
}

Expand All @@ -212,7 +213,7 @@ func (t *ImmutableTree) Iterate(fn func(key []byte, value []byte) bool) bool {
}

itr := t.Iterator(nil, nil, true)

defer itr.Close()
for ; itr.Valid(); itr.Next() {
if fn(itr.Key(), itr.Value()) {
return true
Expand Down
31 changes: 15 additions & 16 deletions mutable_tree.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ func (t *MutableTree) Iterate(fn func(key []byte, value []byte) bool) (stopped b
sort.Strings(unsavedFastNodesToSort)

itr := t.ImmutableTree.Iterator(nil, nil, true)

defer itr.Close()
nextUnsavedIdx := 0
for itr.Valid() && nextUnsavedIdx < len(unsavedFastNodesToSort) {
diskKeyStr := string(itr.Key())
Expand Down Expand Up @@ -439,11 +439,6 @@ func (tree *MutableTree) LazyLoadVersion(targetVersion int64) (int64, error) {
tree.mtx.Lock()
defer tree.mtx.Unlock()

// Attempt to upgrade
if _, err := tree.enableFastStorageAndCommitIfNotEnabled(); err != nil {
return 0, err
}

tree.versions[targetVersion] = true

iTree := &ImmutableTree{
Expand All @@ -460,6 +455,11 @@ func (tree *MutableTree) LazyLoadVersion(targetVersion int64) (int64, error) {
tree.ImmutableTree = iTree
tree.lastSaved = iTree.clone()

// Attempt to upgrade
if _, err := tree.enableFastStorageAndCommitIfNotEnabled(); err != nil {
return 0, err
}

return targetVersion, nil
}

Expand All @@ -483,11 +483,6 @@ func (tree *MutableTree) LoadVersion(targetVersion int64) (int64, error) {
tree.mtx.Lock()
defer tree.mtx.Unlock()

// Attempt to upgrade
if _, err := tree.enableFastStorageAndCommitIfNotEnabled(); err != nil {
return 0, err
}

var latestRoot []byte
for version, r := range roots {
tree.versions[version] = true
Expand Down Expand Up @@ -524,6 +519,11 @@ func (tree *MutableTree) LoadVersion(targetVersion int64) (int64, error) {
tree.lastSaved = t.clone()
tree.allRootLoaded = true

// Attempt to upgrade
if _, err := tree.enableFastStorageAndCommitIfNotEnabled(); err != nil {
return 0, err
}

return latestVersion, nil
}

Expand Down Expand Up @@ -588,6 +588,7 @@ func (tree *MutableTree) enableFastStorageAndCommit() error {
}()

itr := tree.ImmutableTree.Iterator(nil, nil, true)
defer itr.Close()
for ; itr.Valid(); itr.Next() {
if err = tree.ndb.SaveFastNode(NewFastNode(itr.Key(), itr.Value(), tree.version)); err != nil {
return err
Expand Down Expand Up @@ -671,7 +672,7 @@ func (tree *MutableTree) GetVersioned(key []byte, version int64) []byte {
if err != nil {
return nil
}
_, value := t.GetWithIndex(key)
value := t.Get(key)
return value
}
return nil
Expand Down Expand Up @@ -760,10 +761,8 @@ func (tree *MutableTree) saveFastNodeVersion() error {
return err
}

if !tree.ndb.isFastStorageEnabled() {
Copy link
Member

@ValarDragon ValarDragon Jan 30, 2022

Choose a reason for hiding this comment

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

This is removed because its only called if tree.ndb.isFastStorageEnabled is true?

If so, can we add a comment stating this above the saveFastNodeVersion function?

Copy link
Member Author

Choose a reason for hiding this comment

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

The reason why this was removed is that now we are going with the assumption that fast cache storage will be upgraded to on all versions above 1.1.0 or fastStorageVersionValue. Therefore, we must make sure that we persist the latest live version on disk. Otherwise, re-upgrade protection would not function correctly.

In case there is any confusion, there are now 2 separate methods that check for fast storage to be enabled:

  • in nodeDB (hasUpgradedToFastStorage)
    • This method checks that the upgrade has happened. When the upgrade is successful, fast storage on disk must match the latest tree.
    • I renamed this method from isFastStorageEnabled to hasUpgradedToFastStorage and added a comment for clarity
  • in immutable tree (isFastStorageEnabled)
    • This one checks for 2 conditions:
      1. The tree is of the latest version
      2. hasUpgradedToFastStorage - this one is more of a sanity check since now we are assuming that the upgrade should persist forever unless manually downgraded.
    • also added a comment

Let me know if this makes sense

if err := tree.ndb.setStorageVersionBatch(fastStorageVersionValue); err != nil {
return err
}
if err := tree.ndb.setStorageVersionBatch(fastStorageVersionValue); err != nil {
return err
}

return nil
Expand Down
27 changes: 27 additions & 0 deletions nodedb.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (
"fmt"
"math"
"sort"
"strconv"
"strings"
"sync"

"github.com/pkg/errors"
Expand All @@ -18,6 +20,12 @@ const (
hashSize = sha256.Size
genesisVersion = 1
storageVersionKey = "storage_version"
// We store latest saved version together with storage version delimited by the constant below.
// This delimiter is valid only if fast storage is enabled (i.e. storageVersion >= fastStorageVersionValue).
// The latest saved version is needed for protection against downgrade and re-upgrade. In such a case, it would
// be possible to observe mismatch between the latest version state and the fast nodes on disk.
// Therefore, we would like to detect that and overwrite fast nodes on disk with the latest version state.
fastStorageVersionDelimiter = "-"
// Using semantic versioning: https://semver.org/
defaultStorageVersionValue = "1.0.0"
fastStorageVersionValue = "1.1.0"
Expand Down Expand Up @@ -211,6 +219,10 @@ func (ndb *nodeDB) SaveFastNode(node *FastNode) error {
}

func (ndb *nodeDB) setStorageVersion(newVersion string) error {
if newVersion >= fastStorageVersionValue {
ValarDragon marked this conversation as resolved.
Show resolved Hide resolved
newVersion = newVersion + fastStorageVersionDelimiter + strconv.Itoa(int(ndb.latestVersion))
Copy link
Member

Choose a reason for hiding this comment

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

won't we be infinitely appending to newVersion like this, if newVersion gets parsed from the serialized value later?

Copy link
Member

@ValarDragon ValarDragon Jan 30, 2022

Choose a reason for hiding this comment

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

I don't think any of the tests test repeated calls of parsing & setting but maybe i'm wrong

Copy link
Member Author

Choose a reason for hiding this comment

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

You are right, good catch.

I refactored this method completely and renamed it to setFastStorageVersionToBatch. Now, it does not take version as a parameter. Parses the storageVersion and updates it to fast, if needed, with the latest live tree version. Added more unit tests to make sure all code branches are covered

}

if err := ndb.db.Set(metadataKeyFormat.Key([]byte(storageVersionKey)), []byte(newVersion)); err != nil {
return err
}
Expand All @@ -219,6 +231,10 @@ func (ndb *nodeDB) setStorageVersion(newVersion string) error {
}

func (ndb *nodeDB) setStorageVersionBatch(newVersion string) error {
if newVersion >= fastStorageVersionValue {
newVersion = newVersion + fastStorageVersionDelimiter + strconv.Itoa(int(ndb.latestVersion))
}

if err := ndb.batch.Set(metadataKeyFormat.Key([]byte(storageVersionKey)), []byte(newVersion)); err != nil {
return err
}
Expand All @@ -234,6 +250,17 @@ func (ndb *nodeDB) isFastStorageEnabled() bool {
return ndb.getStorageVersion() >= fastStorageVersionValue
}

func (ndb *nodeDB) shouldForceFastStorageUpdate() bool {
versions := strings.Split(ndb.storageVersion, fastStorageVersionDelimiter)

if len(versions) == 2 {
if versions[1] != strconv.Itoa(int(ndb.getLatestVersion())) {
return true
}
}
return false
Copy link
Member

Choose a reason for hiding this comment

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

This should never happen under correct behavior right? Should we add a comment do a fmt.Println if this occurs?

Copy link
Member Author

Choose a reason for hiding this comment

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

This should happen when we have already upgraded to fast storage and it matches the latest state to avoid redundantly upgrading fast nodes when they are already live

Copy link
Member Author

Choose a reason for hiding this comment

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

This should also happen if we are on the default (non-fast) storage version. This method is independent from hasUpgradedToFastStorage.

To avoid confusion,

hasUpgradedToFastStorage - returns true if we have upgraded to fast storage, false if we are on default

shouldForceFastStorageUpdate - returns true if we have upgraded to fast storage but it does not match the live state, false otherwise

}

// SaveNode saves a FastNode to disk.
func (ndb *nodeDB) saveFastNodeUnlocked(node *FastNode) error {
if node.key == nil {
Expand Down
Loading