From 75e5b17c9d9b82ff8083ddf22e06e49a2762f2df Mon Sep 17 00:00:00 2001 From: Uday Patil Date: Thu, 5 Oct 2023 21:18:11 -0500 Subject: [PATCH] [occ] Implement basic multiversion store (#322) ## 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 --- store/multiversion/store.go | 120 +++++++++++++++++++++++++++++++ store/multiversion/store_test.go | 54 ++++++++++++++ 2 files changed, 174 insertions(+) create mode 100644 store/multiversion/store.go create mode 100644 store/multiversion/store_test.go diff --git a/store/multiversion/store.go b/store/multiversion/store.go new file mode 100644 index 000000000..b52c6af1a --- /dev/null +++ b/store/multiversion/store.go @@ -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) diff --git a/store/multiversion/store_test.go b/store/multiversion/store_test.go new file mode 100644 index 000000000..91465c435 --- /dev/null +++ b/store/multiversion/store_test.go @@ -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"))) +}