Skip to content

Commit

Permalink
[occ] Implement basic multiversion store (#322)
Browse files Browse the repository at this point in the history
## Describe your changes and provide context
This implements the multiversion with basic functionality, but still
needs additional work to implement the iterator functionality and/or
persisting readsets for validation

## Testing performed to validate your change
Added unit tests for basic multiversion store
  • Loading branch information
udpatil committed Jan 31, 2024
1 parent 5e5b4ce commit cda8c31
Show file tree
Hide file tree
Showing 2 changed files with 174 additions and 0 deletions.
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)
}

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")))
}

0 comments on commit cda8c31

Please sign in to comment.