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

[occ] Implement basic multiversion store #322

Merged
merged 6 commits into from
Oct 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
120 changes: 120 additions & 0 deletions store/multiversion/store.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package multiversion

import (
"sync"
)

type MultiVersionStore interface {
GetLatest(key []byte) (value MultiVersionValueItem)
GetLatestBeforeIndex(index int, key []byte) (value MultiVersionValueItem)
Set(index int, incarnation int, key []byte, value []byte)
SetEstimate(index int, incarnation int, key []byte)
Delete(index int, incarnation int, key []byte)
Has(index int, key []byte) bool
// TODO: do we want to add helper functions for validations with readsets / applying writesets ?
}

type Store struct {
mtx sync.RWMutex
// map that stores the key -> MultiVersionValue mapping for accessing from a given key
multiVersionMap map[string]MultiVersionValue
// TODO: do we need to add something here to persist readsets for later validation
// TODO: we need to support iterators as well similar to how cachekv does it
// TODO: do we need secondary indexing on index -> keys - this way if we need to abort we can replace those keys with ESTIMATE values? - maybe this just means storing writeset
}

func NewMultiVersionStore() *Store {
return &Store{
multiVersionMap: make(map[string]MultiVersionValue),
}
}

// GetLatest implements MultiVersionStore.
func (s *Store) GetLatest(key []byte) (value MultiVersionValueItem) {
s.mtx.RLock()
defer s.mtx.RUnlock()

keyString := string(key)
// if the key doesn't exist in the overall map, return nil
if _, ok := s.multiVersionMap[keyString]; !ok {
return nil
}
val, found := s.multiVersionMap[keyString].GetLatest()
if !found {
return nil // this shouldn't be possible
}
return val
}

// GetLatestBeforeIndex implements MultiVersionStore.
func (s *Store) GetLatestBeforeIndex(index int, key []byte) (value MultiVersionValueItem) {
s.mtx.RLock()
defer s.mtx.RUnlock()

keyString := string(key)
// if the key doesn't exist in the overall map, return nil
if _, ok := s.multiVersionMap[keyString]; !ok {
return nil
}
val, found := s.multiVersionMap[keyString].GetLatestBeforeIndex(index)
// otherwise, we may have found a value for that key, but its not written before the index passed in
if !found {
return nil
}
// found a value prior to the passed in index, return that value (could be estimate OR deleted, but it is a definitive value)
return val
}

// Has implements MultiVersionStore. It checks if the key exists in the multiversion store at or before the specified index.
func (s *Store) Has(index int, key []byte) bool {
s.mtx.RLock()
defer s.mtx.RUnlock()

keyString := string(key)
if _, ok := s.multiVersionMap[keyString]; !ok {
return false // this is okay because the caller of this will THEN need to access the parent store to verify that the key doesnt exist there
}
_, found := s.multiVersionMap[keyString].GetLatestBeforeIndex(index)
return found
}

// This function will try to intialize the multiversion item if it doesn't exist for a key specified by byte array
// NOTE: this should be used within an acquired mutex lock
func (s *Store) tryInitMultiVersionItem(keyString string) {
if _, ok := s.multiVersionMap[keyString]; !ok {
multiVersionValue := NewMultiVersionItem()
s.multiVersionMap[keyString] = multiVersionValue
}
}

// Set implements MultiVersionStore.
func (s *Store) Set(index int, incarnation int, key []byte, value []byte) {
s.mtx.Lock()
defer s.mtx.Unlock()

keyString := string(key)
s.tryInitMultiVersionItem(keyString)
s.multiVersionMap[keyString].Set(index, incarnation, value)
}

// SetEstimate implements MultiVersionStore.
func (s *Store) SetEstimate(index int, incarnation int, key []byte) {
s.mtx.Lock()
defer s.mtx.Unlock()

keyString := string(key)
s.tryInitMultiVersionItem(keyString)
s.multiVersionMap[keyString].SetEstimate(index, incarnation)
}

// Delete implements MultiVersionStore.
func (s *Store) Delete(index int, incarnation int, key []byte) {
s.mtx.Lock()
defer s.mtx.Unlock()

keyString := string(key)
s.tryInitMultiVersionItem(keyString)
s.multiVersionMap[keyString].Delete(index, incarnation)
Copy link
Contributor

Choose a reason for hiding this comment

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

does something ever get removed from the multiVersionMap? (maybe when the last existing index/incarnation is deleted?)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

no, because even "deletions" are tracked explicitly, as soon as we introduce an item for a key, that key will remain in the multiversion store map for the remainder of the block, because a later transaction may need to access that data. Once all transactions have completed processing, we can discard the multiversion map after committing to store

}

var _ MultiVersionStore = (*Store)(nil)
54 changes: 54 additions & 0 deletions store/multiversion/store_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package multiversion_test

import (
"testing"

"github.com/cosmos/cosmos-sdk/store/multiversion"
"github.com/stretchr/testify/require"
)

func TestMultiVersionStore(t *testing.T) {
store := multiversion.NewMultiVersionStore()

// Test Set and GetLatest
store.Set(1, 1, []byte("key1"), []byte("value1"))
store.Set(2, 1, []byte("key1"), []byte("value2"))
store.Set(3, 1, []byte("key2"), []byte("value3"))
require.Equal(t, []byte("value2"), store.GetLatest([]byte("key1")).Value())
require.Equal(t, []byte("value3"), store.GetLatest([]byte("key2")).Value())

// Test SetEstimate
store.SetEstimate(4, 1, []byte("key1"))
require.True(t, store.GetLatest([]byte("key1")).IsEstimate())

// Test Delete
store.Delete(5, 1, []byte("key1"))
require.True(t, store.GetLatest([]byte("key1")).IsDeleted())

// Test GetLatestBeforeIndex
store.Set(6, 1, []byte("key1"), []byte("value4"))
require.True(t, store.GetLatestBeforeIndex(5, []byte("key1")).IsEstimate())
require.Equal(t, []byte("value4"), store.GetLatestBeforeIndex(7, []byte("key1")).Value())

// Test Has
require.True(t, store.Has(2, []byte("key1")))
require.False(t, store.Has(0, []byte("key1")))
require.False(t, store.Has(5, []byte("key4")))
}

func TestMultiVersionStoreHasLaterValue(t *testing.T) {
store := multiversion.NewMultiVersionStore()

store.Set(5, 1, []byte("key1"), []byte("value2"))

require.Nil(t, store.GetLatestBeforeIndex(4, []byte("key1")))
require.Equal(t, []byte("value2"), store.GetLatestBeforeIndex(6, []byte("key1")).Value())
}

func TestMultiVersionStoreKeyDNE(t *testing.T) {
store := multiversion.NewMultiVersionStore()

require.Nil(t, store.GetLatest([]byte("key1")))
require.Nil(t, store.GetLatestBeforeIndex(0, []byte("key1")))
require.False(t, store.Has(0, []byte("key1")))
}