Skip to content

Commit

Permalink
Multiversion Item Implementation and Tests (#318)
Browse files Browse the repository at this point in the history
## 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
udpatil committed Jan 2, 2024
1 parent 56cb827 commit e110f6c
Show file tree
Hide file tree
Showing 2 changed files with 352 additions and 0 deletions.
160 changes: 160 additions & 0 deletions store/multiversion/data_structures.go
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,
}
}
192 changes: 192 additions & 0 deletions store/multiversion/data_structures_test.go
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())
}

0 comments on commit e110f6c

Please sign in to comment.