-
Notifications
You must be signed in to change notification settings - Fork 69
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Multiversion Item Implementation and Tests (#318)
## Describe your changes and provide context Add multiversion store data structures file, and implement the multiversioned item ## Testing performed to validate your change Added unit tests to verify behavior
- Loading branch information
Showing
2 changed files
with
352 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,160 @@ | ||
package multiversion | ||
|
||
import ( | ||
"sync" | ||
|
||
"github.com/cosmos/cosmos-sdk/store/types" | ||
"github.com/google/btree" | ||
) | ||
|
||
const ( | ||
// The approximate number of items and children per B-tree node. Tuned with benchmarks. | ||
multiVersionBTreeDegree = 2 // should be equivalent to a binary search tree TODO: benchmark this | ||
) | ||
|
||
type MultiVersionValue interface { | ||
GetLatest() (value MultiVersionValueItem, found bool) | ||
GetLatestBeforeIndex(index int) (value MultiVersionValueItem, found bool) | ||
Set(index int, value []byte) | ||
SetEstimate(index int) | ||
Delete(index int) | ||
} | ||
|
||
type MultiVersionValueItem interface { | ||
IsDeleted() bool | ||
IsEstimate() bool | ||
Value() []byte | ||
Index() int | ||
} | ||
|
||
type multiVersionItem struct { | ||
valueTree *btree.BTree // contains versions values written to this key | ||
mtx sync.RWMutex // manages read + write accesses | ||
} | ||
|
||
var _ MultiVersionValue = (*multiVersionItem)(nil) | ||
|
||
func NewMultiVersionItem() *multiVersionItem { | ||
return &multiVersionItem{ | ||
valueTree: btree.New(multiVersionBTreeDegree), | ||
} | ||
} | ||
|
||
// GetLatest returns the latest written value to the btree, and returns a boolean indicating whether it was found. | ||
// | ||
// A `nil` value along with `found=true` indicates a deletion that has occurred and the underlying parent store doesn't need to be hit. | ||
func (item *multiVersionItem) GetLatest() (MultiVersionValueItem, bool) { | ||
item.mtx.RLock() | ||
defer item.mtx.RUnlock() | ||
|
||
bTreeItem := item.valueTree.Max() | ||
if bTreeItem == nil { | ||
return nil, false | ||
} | ||
valueItem := bTreeItem.(*valueItem) | ||
return valueItem, true | ||
} | ||
|
||
// GetLatest returns the latest written value to the btree prior to the index passed in, and returns a boolean indicating whether it was found. | ||
// | ||
// A `nil` value along with `found=true` indicates a deletion that has occurred and the underlying parent store doesn't need to be hit. | ||
func (item *multiVersionItem) GetLatestBeforeIndex(index int) (MultiVersionValueItem, bool) { | ||
item.mtx.RLock() | ||
defer item.mtx.RUnlock() | ||
|
||
// we want to find the value at the index that is LESS than the current index | ||
pivot := NewDeletedItem(index - 1) | ||
|
||
var vItem *valueItem | ||
var found bool | ||
// start from pivot which contains our current index, and return on first item we hit. | ||
// This will ensure we get the latest indexed value relative to our current index | ||
item.valueTree.DescendLessOrEqual(pivot, func(bTreeItem btree.Item) bool { | ||
vItem = bTreeItem.(*valueItem) | ||
found = true | ||
return false | ||
}) | ||
return vItem, found | ||
} | ||
|
||
func (item *multiVersionItem) Set(index int, value []byte) { | ||
types.AssertValidValue(value) | ||
item.mtx.Lock() | ||
defer item.mtx.Unlock() | ||
|
||
valueItem := NewValueItem(index, value) | ||
item.valueTree.ReplaceOrInsert(valueItem) | ||
} | ||
|
||
func (item *multiVersionItem) Delete(index int) { | ||
item.mtx.Lock() | ||
defer item.mtx.Unlock() | ||
|
||
deletedItem := NewDeletedItem(index) | ||
item.valueTree.ReplaceOrInsert(deletedItem) | ||
} | ||
|
||
func (item *multiVersionItem) SetEstimate(index int) { | ||
item.mtx.Lock() | ||
defer item.mtx.Unlock() | ||
|
||
estimateItem := NewEstimateItem(index) | ||
item.valueTree.ReplaceOrInsert(estimateItem) | ||
} | ||
|
||
type valueItem struct { | ||
index int | ||
value []byte | ||
estimate bool | ||
} | ||
|
||
var _ MultiVersionValueItem = (*valueItem)(nil) | ||
|
||
// Index implements MultiVersionValueItem. | ||
func (v *valueItem) Index() int { | ||
return v.index | ||
} | ||
|
||
// IsDeleted implements MultiVersionValueItem. | ||
func (v *valueItem) IsDeleted() bool { | ||
return v.value == nil && !v.estimate | ||
} | ||
|
||
// IsEstimate implements MultiVersionValueItem. | ||
func (v *valueItem) IsEstimate() bool { | ||
return v.estimate | ||
} | ||
|
||
// Value implements MultiVersionValueItem. | ||
func (v *valueItem) Value() []byte { | ||
return v.value | ||
} | ||
|
||
// implement Less for btree.Item for valueItem | ||
func (i *valueItem) Less(other btree.Item) bool { | ||
return i.index < other.(*valueItem).index | ||
} | ||
|
||
func NewValueItem(index int, value []byte) *valueItem { | ||
return &valueItem{ | ||
index: index, | ||
value: value, | ||
estimate: false, | ||
} | ||
} | ||
|
||
func NewEstimateItem(index int) *valueItem { | ||
return &valueItem{ | ||
index: index, | ||
value: nil, | ||
estimate: true, | ||
} | ||
} | ||
|
||
func NewDeletedItem(index int) *valueItem { | ||
return &valueItem{ | ||
index: index, | ||
value: nil, | ||
estimate: false, | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,192 @@ | ||
package multiversion_test | ||
|
||
import ( | ||
"testing" | ||
|
||
mv "github.com/cosmos/cosmos-sdk/store/multiversion" | ||
"github.com/stretchr/testify/require" | ||
) | ||
|
||
func TestMultiversionItemGetLatest(t *testing.T) { | ||
mvItem := mv.NewMultiVersionItem() | ||
// We have no value, should get found == false and a nil value | ||
value, found := mvItem.GetLatest() | ||
require.False(t, found) | ||
require.Nil(t, value) | ||
|
||
// assert that we find a value after it's set | ||
one := []byte("one") | ||
mvItem.Set(1, one) | ||
value, found = mvItem.GetLatest() | ||
require.True(t, found) | ||
require.Equal(t, one, value.Value()) | ||
|
||
// assert that we STILL get the "one" value since it is the latest | ||
zero := []byte("zero") | ||
mvItem.Set(0, zero) | ||
value, found = mvItem.GetLatest() | ||
require.True(t, found) | ||
require.Equal(t, one, value.Value()) | ||
|
||
// we should see a deletion as the latest now, aka nil value and found == true | ||
mvItem.Delete(2) | ||
value, found = mvItem.GetLatest() | ||
require.True(t, found) | ||
require.True(t, value.IsDeleted()) | ||
require.Nil(t, value.Value()) | ||
|
||
// Overwrite the deleted value with some data | ||
two := []byte("two") | ||
mvItem.Set(2, two) | ||
value, found = mvItem.GetLatest() | ||
require.True(t, found) | ||
require.Equal(t, two, value.Value()) | ||
} | ||
|
||
func TestMultiversionItemGetByIndex(t *testing.T) { | ||
mvItem := mv.NewMultiVersionItem() | ||
// We have no value, should get found == false and a nil value | ||
value, found := mvItem.GetLatestBeforeIndex(9) | ||
require.False(t, found) | ||
require.Nil(t, value) | ||
|
||
// assert that we find a value after it's set | ||
one := []byte("one") | ||
mvItem.Set(1, one) | ||
// should not be found because we specifically search "LESS THAN" | ||
value, found = mvItem.GetLatestBeforeIndex(1) | ||
require.False(t, found) | ||
require.Nil(t, value) | ||
// querying from "two" should be found | ||
value, found = mvItem.GetLatestBeforeIndex(2) | ||
require.True(t, found) | ||
require.Equal(t, one, value.Value()) | ||
|
||
// verify that querying for an earlier index returns nil | ||
value, found = mvItem.GetLatestBeforeIndex(0) | ||
require.False(t, found) | ||
require.Nil(t, value) | ||
|
||
// assert that we STILL get the "one" value when querying with a later index | ||
zero := []byte("zero") | ||
mvItem.Set(0, zero) | ||
// verify that querying for zero should ALWAYS return nil | ||
value, found = mvItem.GetLatestBeforeIndex(0) | ||
require.False(t, found) | ||
require.Nil(t, value) | ||
|
||
value, found = mvItem.GetLatestBeforeIndex(2) | ||
require.True(t, found) | ||
require.Equal(t, one, value.Value()) | ||
// verify we get zero when querying with index 1 | ||
value, found = mvItem.GetLatestBeforeIndex(1) | ||
require.True(t, found) | ||
require.Equal(t, zero, value.Value()) | ||
|
||
// we should see a deletion as the latest now, aka nil value and found == true, but index 4 still returns `one` | ||
mvItem.Delete(4) | ||
value, found = mvItem.GetLatestBeforeIndex(4) | ||
require.True(t, found) | ||
require.Equal(t, one, value.Value()) | ||
// should get deletion item for a later index | ||
value, found = mvItem.GetLatestBeforeIndex(5) | ||
require.True(t, found) | ||
require.True(t, value.IsDeleted()) | ||
|
||
// verify that we still read the proper underlying item for an older index | ||
value, found = mvItem.GetLatestBeforeIndex(3) | ||
require.True(t, found) | ||
require.Equal(t, one, value.Value()) | ||
|
||
// Overwrite the deleted value with some data and verify we read it properly | ||
four := []byte("four") | ||
mvItem.Set(4, four) | ||
// also reads the four | ||
value, found = mvItem.GetLatestBeforeIndex(6) | ||
require.True(t, found) | ||
require.Equal(t, four, value.Value()) | ||
// still reads the `one` | ||
value, found = mvItem.GetLatestBeforeIndex(4) | ||
require.True(t, found) | ||
require.Equal(t, one, value.Value()) | ||
} | ||
|
||
func TestMultiversionItemEstimate(t *testing.T) { | ||
mvItem := mv.NewMultiVersionItem() | ||
// We have no value, should get found == false and a nil value | ||
value, found := mvItem.GetLatestBeforeIndex(9) | ||
require.False(t, found) | ||
require.Nil(t, value) | ||
|
||
// assert that we find a value after it's set | ||
one := []byte("one") | ||
mvItem.Set(1, one) | ||
// should not be found because we specifically search "LESS THAN" | ||
value, found = mvItem.GetLatestBeforeIndex(1) | ||
require.False(t, found) | ||
require.Nil(t, value) | ||
// querying from "two" should be found | ||
value, found = mvItem.GetLatestBeforeIndex(2) | ||
require.True(t, found) | ||
require.False(t, value.IsEstimate()) | ||
require.Equal(t, one, value.Value()) | ||
// set as estimate | ||
mvItem.SetEstimate(1) | ||
// should not be found because we specifically search "LESS THAN" | ||
value, found = mvItem.GetLatestBeforeIndex(1) | ||
require.False(t, found) | ||
require.Nil(t, value) | ||
// querying from "two" should be found as ESTIMATE | ||
value, found = mvItem.GetLatestBeforeIndex(2) | ||
require.True(t, found) | ||
require.True(t, value.IsEstimate()) | ||
|
||
// verify that querying for an earlier index returns nil | ||
value, found = mvItem.GetLatestBeforeIndex(0) | ||
require.False(t, found) | ||
require.Nil(t, value) | ||
|
||
// assert that we STILL get the "one" value when querying with a later index | ||
zero := []byte("zero") | ||
mvItem.Set(0, zero) | ||
// verify that querying for zero should ALWAYS return nil | ||
value, found = mvItem.GetLatestBeforeIndex(0) | ||
require.False(t, found) | ||
require.Nil(t, value) | ||
|
||
value, found = mvItem.GetLatestBeforeIndex(2) | ||
require.True(t, found) | ||
require.True(t, value.IsEstimate()) | ||
// verify we get zero when querying with index 1 | ||
value, found = mvItem.GetLatestBeforeIndex(1) | ||
require.True(t, found) | ||
require.Equal(t, zero, value.Value()) | ||
// reset one to no longer be an estiamte | ||
mvItem.Set(1, one) | ||
// we should see a deletion as the latest now, aka nil value and found == true, but index 4 still returns `one` | ||
mvItem.Delete(4) | ||
value, found = mvItem.GetLatestBeforeIndex(4) | ||
require.True(t, found) | ||
require.Equal(t, one, value.Value()) | ||
// should get deletion item for a later index | ||
value, found = mvItem.GetLatestBeforeIndex(5) | ||
require.True(t, found) | ||
require.True(t, value.IsDeleted()) | ||
|
||
// verify that we still read the proper underlying item for an older index | ||
value, found = mvItem.GetLatestBeforeIndex(3) | ||
require.True(t, found) | ||
require.Equal(t, one, value.Value()) | ||
|
||
// Overwrite the deleted value with an estimate and verify we read it properly | ||
mvItem.SetEstimate(4) | ||
// also reads the four | ||
value, found = mvItem.GetLatestBeforeIndex(6) | ||
require.True(t, found) | ||
require.True(t, value.IsEstimate()) | ||
require.False(t, value.IsDeleted()) | ||
// still reads the `one` | ||
value, found = mvItem.GetLatestBeforeIndex(4) | ||
require.True(t, found) | ||
require.Equal(t, one, value.Value()) | ||
} |