feat(stf/branch): simplify merged iterator (#22131)
Co-authored-by: marbar3778 <marbar3778@yahoo.com>
This commit is contained in:
parent
ffa74d17cb
commit
99b4858a95
@ -5,6 +5,7 @@ go 1.23
|
||||
// server v2 integration
|
||||
replace (
|
||||
cosmossdk.io/api => ../../api
|
||||
cosmossdk.io/core/testing => ../../core/testing
|
||||
cosmossdk.io/server/v2/appmanager => ../../server/v2/appmanager
|
||||
cosmossdk.io/server/v2/stf => ../../server/v2/stf
|
||||
cosmossdk.io/store/v2 => ../../store/v2
|
||||
@ -30,7 +31,7 @@ require (
|
||||
require (
|
||||
buf.build/gen/go/cometbft/cometbft/protocolbuffers/go v1.35.1-20240701160653-fedbb9acfd2f.1 // indirect
|
||||
buf.build/gen/go/cosmos/gogo-proto/protocolbuffers/go v1.35.1-20240130113600-88ef6483f90f.1 // indirect
|
||||
cosmossdk.io/core/testing v0.0.0-20240923163230-04da382a9f29 // indirect
|
||||
cosmossdk.io/core/testing v0.0.0 // indirect
|
||||
cosmossdk.io/errors/v2 v2.0.0-20240731132947-df72853b3ca5 // indirect
|
||||
github.com/DataDog/zstd v1.5.5 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
|
||||
@ -4,8 +4,6 @@ buf.build/gen/go/cosmos/gogo-proto/protocolbuffers/go v1.35.1-20240130113600-88e
|
||||
buf.build/gen/go/cosmos/gogo-proto/protocolbuffers/go v1.35.1-20240130113600-88ef6483f90f.1/go.mod h1:zqi/LZjZhyvjCMTEVIwAf5VRlkLduuCfqmZxgoormq0=
|
||||
cosmossdk.io/core v1.0.0-alpha.5 h1:McjYXAQ6XcT20v2uHyH7PhoWH8V+mebzfVFqT3GinsI=
|
||||
cosmossdk.io/core v1.0.0-alpha.5/go.mod h1:3u9cWq1FAVtiiCrDPpo4LhR+9V6k/ycSG4/Y/tREWCY=
|
||||
cosmossdk.io/core/testing v0.0.0-20240923163230-04da382a9f29 h1:NxxUo0GMJUbIuVg0R70e3cbn9eFTEuMr7ev1AFvypdY=
|
||||
cosmossdk.io/core/testing v0.0.0-20240923163230-04da382a9f29/go.mod h1:8s2tPeJtSiQuoyPmr2Ag7meikonISO4Fv4MoO8+ORrs=
|
||||
cosmossdk.io/depinject v1.1.0 h1:wLan7LG35VM7Yo6ov0jId3RHWCGRhe8E8bsuARorl5E=
|
||||
cosmossdk.io/depinject v1.1.0/go.mod h1:kkI5H9jCGHeKeYWXTqYdruogYrEeWvBQCw1Pj4/eCFI=
|
||||
cosmossdk.io/errors/v2 v2.0.0-20240731132947-df72853b3ca5 h1:IQNdY2kB+k+1OM2DvqFG1+UgeU1JzZrWtwuWzI3ZfwA=
|
||||
|
||||
@ -4,6 +4,7 @@ go 1.23.1
|
||||
|
||||
replace (
|
||||
cosmossdk.io/api => ../../../api
|
||||
cosmossdk.io/core/testing => ../../../core/testing
|
||||
cosmossdk.io/server/v2 => ../
|
||||
cosmossdk.io/server/v2/appmanager => ../appmanager
|
||||
cosmossdk.io/server/v2/stf => ../stf
|
||||
@ -43,7 +44,7 @@ require (
|
||||
require (
|
||||
buf.build/gen/go/cometbft/cometbft/protocolbuffers/go v1.35.1-20240701160653-fedbb9acfd2f.1 // indirect
|
||||
buf.build/gen/go/cosmos/gogo-proto/protocolbuffers/go v1.35.1-20240130113600-88ef6483f90f.1 // indirect
|
||||
cosmossdk.io/core/testing v0.0.0-20240923163230-04da382a9f29 // indirect
|
||||
cosmossdk.io/core/testing v0.0.0 // indirect
|
||||
cosmossdk.io/depinject v1.1.0 // indirect
|
||||
cosmossdk.io/errors v1.0.1 // indirect
|
||||
cosmossdk.io/math v1.3.0 // indirect
|
||||
|
||||
@ -8,8 +8,6 @@ cosmossdk.io/collections v0.4.0 h1:PFmwj2W8szgpD5nOd8GWH6AbYNi1f2J6akWXJ7P5t9s=
|
||||
cosmossdk.io/collections v0.4.0/go.mod h1:oa5lUING2dP+gdDquow+QjlF45eL1t4TJDypgGd+tv0=
|
||||
cosmossdk.io/core v1.0.0-alpha.5 h1:McjYXAQ6XcT20v2uHyH7PhoWH8V+mebzfVFqT3GinsI=
|
||||
cosmossdk.io/core v1.0.0-alpha.5/go.mod h1:3u9cWq1FAVtiiCrDPpo4LhR+9V6k/ycSG4/Y/tREWCY=
|
||||
cosmossdk.io/core/testing v0.0.0-20240923163230-04da382a9f29 h1:NxxUo0GMJUbIuVg0R70e3cbn9eFTEuMr7ev1AFvypdY=
|
||||
cosmossdk.io/core/testing v0.0.0-20240923163230-04da382a9f29/go.mod h1:8s2tPeJtSiQuoyPmr2Ag7meikonISO4Fv4MoO8+ORrs=
|
||||
cosmossdk.io/depinject v1.1.0 h1:wLan7LG35VM7Yo6ov0jId3RHWCGRhe8E8bsuARorl5E=
|
||||
cosmossdk.io/depinject v1.1.0/go.mod h1:kkI5H9jCGHeKeYWXTqYdruogYrEeWvBQCw1Pj4/eCFI=
|
||||
cosmossdk.io/errors v1.0.1 h1:bzu+Kcr0kS/1DuPBtUFdWjzLqyUuCiyHjyJB6srBV/0=
|
||||
|
||||
83
server/v2/stf/branch/bench_test.go
Normal file
83
server/v2/stf/branch/bench_test.go
Normal file
@ -0,0 +1,83 @@
|
||||
package branch
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"cosmossdk.io/core/store"
|
||||
coretesting "cosmossdk.io/core/testing"
|
||||
)
|
||||
|
||||
var (
|
||||
stackSizes = []int{1, 10, 100}
|
||||
elemsInStack = 10
|
||||
)
|
||||
|
||||
func Benchmark_CacheStack_Set(b *testing.B) {
|
||||
for _, stackSize := range stackSizes {
|
||||
b.Run(fmt.Sprintf("StackSize%d", stackSize), func(b *testing.B) {
|
||||
bs := makeBranchStack(b, stackSize)
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = bs.Set([]byte{0}, []byte{0})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Benchmark_Get(b *testing.B) {
|
||||
for _, stackSize := range stackSizes {
|
||||
b.Run(fmt.Sprintf("StackSize%d", stackSize), func(b *testing.B) {
|
||||
bs := makeBranchStack(b, stackSize)
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, _ = bs.Get([]byte{0})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Benchmark_Iterate(b *testing.B) {
|
||||
var keySink, valueSink any
|
||||
|
||||
for _, stackSize := range stackSizes {
|
||||
b.Run(fmt.Sprintf("StackSize%d", stackSize), func(b *testing.B) {
|
||||
bs := makeBranchStack(b, stackSize)
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
iter, _ := bs.Iterator(nil, nil)
|
||||
for iter.Valid() {
|
||||
keySink = iter.Key()
|
||||
valueSink = iter.Value()
|
||||
iter.Next()
|
||||
}
|
||||
_ = iter.Close()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
_ = keySink
|
||||
_ = valueSink
|
||||
}
|
||||
|
||||
// makeBranchStack creates a branch stack of the given size and initializes it with unique key-value pairs.
|
||||
func makeBranchStack(b *testing.B, stackSize int) Store[store.KVStore] {
|
||||
parent := coretesting.NewMemKV()
|
||||
branch := NewStore[store.KVStore](parent)
|
||||
for i := 1; i < stackSize; i++ {
|
||||
branch = NewStore[store.KVStore](branch)
|
||||
for j := 0; j < elemsInStack; j++ {
|
||||
// create unique keys by including the branch index.
|
||||
key := []byte{byte(i), byte(j)}
|
||||
value := []byte{byte(j)}
|
||||
err := branch.Set(key, value)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return branch
|
||||
}
|
||||
@ -15,7 +15,7 @@ const (
|
||||
|
||||
var errKeyEmpty = errors.New("key cannot be empty")
|
||||
|
||||
// changeSet implements the sorted cache for cachekv store,
|
||||
// changeSet implements the sorted tree for cachekv store,
|
||||
// we don't use MemDB here because cachekv is used extensively in sdk core path,
|
||||
// we need it to be as fast as possible, while `MemDB` is mainly used as a mocking db in unit tests.
|
||||
//
|
||||
|
||||
@ -7,229 +7,162 @@ import (
|
||||
corestore "cosmossdk.io/core/store"
|
||||
)
|
||||
|
||||
// mergedIterator merges a parent Iterator and a cache Iterator.
|
||||
// The cache iterator may return nil keys to signal that an item
|
||||
// had been deleted (but not deleted in the parent).
|
||||
// If the cache iterator has the same key as the parent, the
|
||||
// cache shadows (overrides) the parent.
|
||||
type mergedIterator struct {
|
||||
parent corestore.Iterator
|
||||
cache corestore.Iterator
|
||||
ascending bool
|
||||
var (
|
||||
errInvalidIterator = errors.New("invalid iterator")
|
||||
)
|
||||
|
||||
valid bool
|
||||
// mergedIterator merges a parent Iterator and a child Iterator.
|
||||
// The child iterator may contain items that shadow or override items in the parent iterator.
|
||||
// If the child iterator has the same key as the parent, the child's value takes precedence.
|
||||
// Deleted items in the child (indicated by nil values) are skipped.
|
||||
type mergedIterator[Parent, Child corestore.Iterator] struct {
|
||||
parent Parent // Iterator for the parent store
|
||||
child Child // Iterator for the child store
|
||||
ascending bool // Direction of iteration
|
||||
valid bool // Indicates if the iterator is in a valid state
|
||||
currKey []byte // Current key pointed by the iterator
|
||||
currValue []byte // Current value corresponding to currKey
|
||||
err error // Error encountered during iteration
|
||||
}
|
||||
|
||||
var _ corestore.Iterator = (*mergedIterator)(nil)
|
||||
// Ensure mergedIterator implements the corestore.Iterator interface.
|
||||
var _ corestore.Iterator = (*mergedIterator[corestore.Iterator, corestore.Iterator])(nil)
|
||||
|
||||
// mergeIterators merges two iterators.
|
||||
func mergeIterators(parent, cache corestore.Iterator, ascending bool) corestore.Iterator {
|
||||
iter := &mergedIterator{
|
||||
// mergeIterators creates a new merged iterator from parent and child iterators.
|
||||
// The 'ascending' parameter determines the direction of iteration.
|
||||
func mergeIterators[Parent, Child corestore.Iterator](parent Parent, child Child, ascending bool) *mergedIterator[Parent, Child] {
|
||||
iter := &mergedIterator[Parent, Child]{
|
||||
parent: parent,
|
||||
cache: cache,
|
||||
child: child,
|
||||
ascending: ascending,
|
||||
}
|
||||
|
||||
iter.valid = iter.skipUntilExistsOrInvalid()
|
||||
iter.advance() // Initialize the iterator by advancing to the first valid item
|
||||
return iter
|
||||
}
|
||||
|
||||
// Domain implements Iterator.
|
||||
// Returns parent domain because cache and parent domains are the same.
|
||||
func (iter *mergedIterator) Domain() (start, end []byte) {
|
||||
return iter.parent.Domain()
|
||||
// Domain returns the start and end range of the iterator.
|
||||
// It delegates to the parent iterator as both iterators share the same domain.
|
||||
func (i *mergedIterator[Parent, Child]) Domain() (start, end []byte) {
|
||||
return i.parent.Domain()
|
||||
}
|
||||
|
||||
// Valid implements Iterator.
|
||||
func (iter *mergedIterator) Valid() bool {
|
||||
return iter.valid
|
||||
// Valid checks if the iterator is in a valid state.
|
||||
// It returns true if the iterator has not reached the end.
|
||||
func (i *mergedIterator[Parent, Child]) Valid() bool {
|
||||
return i.valid
|
||||
}
|
||||
|
||||
// Next implements Iterator
|
||||
func (iter *mergedIterator) Next() {
|
||||
iter.assertValid()
|
||||
// Next advances the iterator to the next valid item.
|
||||
// It skips over deleted items (with nil values) and updates the current key and value.
|
||||
func (i *mergedIterator[Parent, Child]) Next() {
|
||||
if !i.valid {
|
||||
i.err = errInvalidIterator
|
||||
return
|
||||
}
|
||||
i.advance()
|
||||
}
|
||||
|
||||
switch {
|
||||
case !iter.parent.Valid():
|
||||
// If parent is invalid, get the next cache item.
|
||||
iter.cache.Next()
|
||||
case !iter.cache.Valid():
|
||||
// If cache is invalid, get the next parent item.
|
||||
iter.parent.Next()
|
||||
default:
|
||||
// Both are valid. Compare keys.
|
||||
keyP, keyC := iter.parent.Key(), iter.cache.Key()
|
||||
switch iter.compare(keyP, keyC) {
|
||||
case -1: // parent < cache
|
||||
iter.parent.Next()
|
||||
case 0: // parent == cache
|
||||
iter.parent.Next()
|
||||
iter.cache.Next()
|
||||
case 1: // parent > cache
|
||||
iter.cache.Next()
|
||||
// Key returns the current key pointed by the iterator.
|
||||
// If the iterator is invalid, it returns nil.
|
||||
func (i *mergedIterator[Parent, Child]) Key() []byte {
|
||||
if !i.valid {
|
||||
panic("called key on invalid iterator")
|
||||
}
|
||||
return i.currKey
|
||||
}
|
||||
|
||||
// Value returns the current value corresponding to the current key.
|
||||
// If the iterator is invalid, it returns nil.
|
||||
func (i *mergedIterator[Parent, Child]) Value() []byte {
|
||||
if !i.valid {
|
||||
panic("called value on invalid iterator")
|
||||
}
|
||||
return i.currValue
|
||||
}
|
||||
|
||||
// Close closes both the parent and child iterators.
|
||||
// It returns any error encountered during the closing of the iterators.
|
||||
func (i *mergedIterator[Parent, Child]) Close() (err error) {
|
||||
err = errors.Join(err, i.parent.Close())
|
||||
err = errors.Join(err, i.child.Close())
|
||||
i.valid = false
|
||||
return err
|
||||
}
|
||||
|
||||
// Error returns any error that occurred during iteration.
|
||||
// If the iterator is valid, it returns nil.
|
||||
func (i *mergedIterator[Parent, Child]) Error() error {
|
||||
return i.err
|
||||
}
|
||||
|
||||
// advance moves the iterator to the next valid (non-deleted) item.
|
||||
// It handles merging logic between the parent and child iterators.
|
||||
func (i *mergedIterator[Parent, Child]) advance() {
|
||||
for {
|
||||
// Check if both iterators have reached the end
|
||||
if !i.parent.Valid() && !i.child.Valid() {
|
||||
i.valid = false
|
||||
return
|
||||
}
|
||||
}
|
||||
iter.valid = iter.skipUntilExistsOrInvalid()
|
||||
}
|
||||
|
||||
// Key implements Iterator
|
||||
func (iter *mergedIterator) Key() []byte {
|
||||
iter.assertValid()
|
||||
var key, value []byte
|
||||
|
||||
// If parent is invalid, get the cache key.
|
||||
if !iter.parent.Valid() {
|
||||
return iter.cache.Key()
|
||||
}
|
||||
// If parent iterator is exhausted, use the child iterator
|
||||
if !i.parent.Valid() {
|
||||
key = i.child.Key()
|
||||
value = i.child.Value()
|
||||
i.child.Next()
|
||||
} else if !i.child.Valid() {
|
||||
// If child iterator is exhausted, use the parent iterator
|
||||
key = i.parent.Key()
|
||||
value = i.parent.Value()
|
||||
i.parent.Next()
|
||||
} else {
|
||||
// Both iterators are valid; compare keys
|
||||
keyP, keyC := i.parent.Key(), i.child.Key()
|
||||
switch cmp := i.compare(keyP, keyC); {
|
||||
case cmp < 0:
|
||||
// Parent key is less than child key
|
||||
key = keyP
|
||||
value = i.parent.Value()
|
||||
i.parent.Next()
|
||||
case cmp == 0:
|
||||
// Keys are equal; child overrides parent
|
||||
key = keyC
|
||||
value = i.child.Value()
|
||||
i.parent.Next()
|
||||
i.child.Next()
|
||||
case cmp > 0:
|
||||
// Child key is less than parent key
|
||||
key = keyC
|
||||
value = i.child.Value()
|
||||
i.child.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// If cache is invalid, get the parent key.
|
||||
if !iter.cache.Valid() {
|
||||
return iter.parent.Key()
|
||||
}
|
||||
// Skip deleted items (value is nil)
|
||||
if value == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Both are valid. Compare keys.
|
||||
keyP, keyC := iter.parent.Key(), iter.cache.Key()
|
||||
|
||||
cmp := iter.compare(keyP, keyC)
|
||||
switch cmp {
|
||||
case -1: // parent < cache
|
||||
return keyP
|
||||
case 0: // parent == cache
|
||||
return keyP
|
||||
case 1: // parent > cache
|
||||
return keyC
|
||||
default:
|
||||
panic("invalid compare result")
|
||||
// Update the current key and value, and mark iterator as valid
|
||||
i.currKey = key
|
||||
i.currValue = value
|
||||
i.valid = true
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Value implements Iterator
|
||||
func (iter *mergedIterator) Value() []byte {
|
||||
iter.assertValid()
|
||||
|
||||
// If parent is invalid, get the cache value.
|
||||
if !iter.parent.Valid() {
|
||||
return iter.cache.Value()
|
||||
}
|
||||
|
||||
// If cache is invalid, get the parent value.
|
||||
if !iter.cache.Valid() {
|
||||
return iter.parent.Value()
|
||||
}
|
||||
|
||||
// Both are valid. Compare keys.
|
||||
keyP, keyC := iter.parent.Key(), iter.cache.Key()
|
||||
|
||||
cmp := iter.compare(keyP, keyC)
|
||||
switch cmp {
|
||||
case -1: // parent < cache
|
||||
return iter.parent.Value()
|
||||
case 0: // parent == cache
|
||||
return iter.cache.Value()
|
||||
case 1: // parent > cache
|
||||
return iter.cache.Value()
|
||||
default:
|
||||
panic("invalid comparison result")
|
||||
}
|
||||
}
|
||||
|
||||
// Close implements Iterator
|
||||
func (iter *mergedIterator) Close() error {
|
||||
err1 := iter.cache.Close()
|
||||
if err := iter.parent.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return err1
|
||||
}
|
||||
|
||||
var errInvalidIterator = errors.New("invalid merged iterator")
|
||||
|
||||
// Error returns an error if the mergedIterator is invalid defined by the
|
||||
// Valid method.
|
||||
func (iter *mergedIterator) Error() error {
|
||||
if !iter.Valid() {
|
||||
return errInvalidIterator
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// If not valid, panics.
|
||||
// NOTE: May have side-effect of iterating over cache.
|
||||
func (iter *mergedIterator) assertValid() {
|
||||
if err := iter.Error(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Like bytes.Compare but opposite if not ascending.
|
||||
func (iter *mergedIterator) compare(a, b []byte) int {
|
||||
if iter.ascending {
|
||||
// compare compares two byte slices a and b.
|
||||
// It returns an integer comparing a and b:
|
||||
// - Negative if a < b
|
||||
// - Zero if a == b
|
||||
// - Positive if a > b
|
||||
//
|
||||
// The comparison respects the iterator's direction (ascending or descending).
|
||||
func (i *mergedIterator[Parent, Child]) compare(a, b []byte) int {
|
||||
if i.ascending {
|
||||
return bytes.Compare(a, b)
|
||||
}
|
||||
|
||||
return bytes.Compare(a, b) * -1
|
||||
}
|
||||
|
||||
// Skip all delete-items from the cache w/ `key < until`. After this function,
|
||||
// current cache item is a non-delete-item, or `until <= key`.
|
||||
// If the current cache item is not a delete item, does nothing.
|
||||
// If `until` is nil, there is no limit, and cache may end up invalid.
|
||||
// CONTRACT: cache is valid.
|
||||
func (iter *mergedIterator) skipCacheDeletes(until []byte) {
|
||||
for iter.cache.Valid() &&
|
||||
iter.cache.Value() == nil &&
|
||||
(until == nil || iter.compare(iter.cache.Key(), until) < 0) {
|
||||
iter.cache.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// Fast forwards cache (or parent+cache in case of deleted items) until current
|
||||
// item exists, or until iterator becomes invalid.
|
||||
// Returns whether the iterator is valid.
|
||||
func (iter *mergedIterator) skipUntilExistsOrInvalid() bool {
|
||||
for {
|
||||
// If parent is invalid, fast-forward cache.
|
||||
if !iter.parent.Valid() {
|
||||
iter.skipCacheDeletes(nil)
|
||||
return iter.cache.Valid()
|
||||
}
|
||||
// Parent is valid.
|
||||
|
||||
if !iter.cache.Valid() {
|
||||
return true
|
||||
}
|
||||
// Parent is valid, cache is valid.
|
||||
|
||||
// Compare parent and cache.
|
||||
keyP := iter.parent.Key()
|
||||
keyC := iter.cache.Key()
|
||||
|
||||
switch iter.compare(keyP, keyC) {
|
||||
case -1: // parent < cache.
|
||||
return true
|
||||
|
||||
case 0: // parent == cache.
|
||||
// Skip over if cache item is a delete.
|
||||
valueC := iter.cache.Value()
|
||||
if valueC == nil {
|
||||
iter.parent.Next()
|
||||
iter.cache.Next()
|
||||
|
||||
continue
|
||||
}
|
||||
// Cache is not a delete.
|
||||
|
||||
return true // cache exists.
|
||||
case 1: // cache < parent
|
||||
// Skip over if cache item is a delete.
|
||||
valueC := iter.cache.Value()
|
||||
if valueC == nil {
|
||||
iter.skipCacheDeletes(keyP)
|
||||
continue
|
||||
}
|
||||
// Cache is not a delete.
|
||||
return true // cache exists.
|
||||
}
|
||||
}
|
||||
return bytes.Compare(b, a)
|
||||
}
|
||||
|
||||
@ -7,6 +7,52 @@ import (
|
||||
corestore "cosmossdk.io/core/store"
|
||||
)
|
||||
|
||||
func TestMergedIterator_Validity(t *testing.T) {
|
||||
panics := func(f func()) {
|
||||
defer func() {
|
||||
r := recover()
|
||||
if r == nil {
|
||||
t.Error("panic expected")
|
||||
}
|
||||
}()
|
||||
|
||||
f()
|
||||
}
|
||||
|
||||
t.Run("panics when calling key on invalid iter", func(t *testing.T) {
|
||||
parent, err := newMemState().Iterator(nil, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cache, err := newMemState().Iterator(nil, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
it := mergeIterators(parent, cache, true)
|
||||
panics(func() {
|
||||
it.Key()
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("panics when calling value on invalid iter", func(t *testing.T) {
|
||||
parent, err := newMemState().Iterator(nil, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cache, err := newMemState().Iterator(nil, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
it := mergeIterators(parent, cache, true)
|
||||
|
||||
panics(func() {
|
||||
it.Value()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestMergedIterator_Next(t *testing.T) {
|
||||
specs := map[string]struct {
|
||||
setup func() corestore.Iterator
|
||||
@ -19,7 +65,7 @@ func TestMergedIterator_Next(t *testing.T) {
|
||||
return mergeIterators(must(parent.Iterator(nil, nil)), must(cache.Iterator(nil, nil)), true)
|
||||
},
|
||||
},
|
||||
"parent iterator has one item, cache is empty": {
|
||||
"parent iterator has one item, child is empty": {
|
||||
setup: func() corestore.Iterator {
|
||||
parent := newMemState()
|
||||
if err := parent.Set([]byte("k1"), []byte("1")); err != nil {
|
||||
@ -30,7 +76,7 @@ func TestMergedIterator_Next(t *testing.T) {
|
||||
},
|
||||
exp: [][2]string{{"k1", "1"}},
|
||||
},
|
||||
"cache has one item, parent is empty": {
|
||||
"child has one item, parent is empty": {
|
||||
setup: func() corestore.Iterator {
|
||||
parent := newMemState()
|
||||
cache := newMemState()
|
||||
@ -41,21 +87,21 @@ func TestMergedIterator_Next(t *testing.T) {
|
||||
},
|
||||
exp: [][2]string{{"k1", "1"}},
|
||||
},
|
||||
"both iterators have same key, cache preferred": {
|
||||
"both iterators have same key, child preferred": {
|
||||
setup: func() corestore.Iterator {
|
||||
parent := newMemState()
|
||||
if err := parent.Set([]byte("k1"), []byte("parent-val")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cache := newMemState()
|
||||
if err := cache.Set([]byte("k1"), []byte("cache-val")); err != nil {
|
||||
if err := cache.Set([]byte("k1"), []byte("child-val")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return mergeIterators(must(parent.Iterator(nil, nil)), must(cache.Iterator(nil, nil)), true)
|
||||
},
|
||||
exp: [][2]string{{"k1", "cache-val"}},
|
||||
exp: [][2]string{{"k1", "child-val"}},
|
||||
},
|
||||
"both iterators have same key, but cache value is nil": {
|
||||
"both iterators have same key, but child value is nil": {
|
||||
setup: func() corestore.Iterator {
|
||||
parent := newMemState()
|
||||
if err := parent.Set([]byte("k1"), []byte("1")); err != nil {
|
||||
@ -68,7 +114,7 @@ func TestMergedIterator_Next(t *testing.T) {
|
||||
return mergeIterators(must(parent.Iterator(nil, nil)), must(cache.Iterator(nil, nil)), true)
|
||||
},
|
||||
},
|
||||
"parent and cache are ascending": {
|
||||
"parent and child are ascending": {
|
||||
setup: func() corestore.Iterator {
|
||||
parent := newMemState()
|
||||
if err := parent.Set([]byte("k2"), []byte("v2")); err != nil {
|
||||
@ -88,7 +134,7 @@ func TestMergedIterator_Next(t *testing.T) {
|
||||
},
|
||||
exp: [][2]string{{"k1", "v1"}, {"k2", "v2"}, {"k3", "v3"}, {"k4", "v4"}},
|
||||
},
|
||||
"parent and cache are descending": {
|
||||
"parent and child are descending": {
|
||||
setup: func() corestore.Iterator {
|
||||
parent := newMemState()
|
||||
if err := parent.Set([]byte("k3"), []byte("v3")); err != nil {
|
||||
|
||||
@ -8,9 +8,9 @@ import (
|
||||
|
||||
var _ store.Writer = (*Store[store.Reader])(nil)
|
||||
|
||||
// Store wraps an in-memory cache around an underlying types.KVStore.
|
||||
// Store wraps an in-memory child around an underlying types.KVStore.
|
||||
type Store[T store.Reader] struct {
|
||||
changeSet changeSet // always ascending sorted
|
||||
changeSet changeSet // ordered changeset.
|
||||
parent T
|
||||
}
|
||||
|
||||
@ -24,11 +24,18 @@ func NewStore[T store.Reader](parent T) Store[T] {
|
||||
|
||||
// Get implements types.KVStore.
|
||||
func (s Store[T]) Get(key []byte) (value []byte, err error) {
|
||||
// if found in memory cache, immediately return.
|
||||
value, found := s.changeSet.get(key)
|
||||
if found {
|
||||
return
|
||||
}
|
||||
return s.parent.Get(key)
|
||||
// after we get it from parent store, we cache it.
|
||||
// if it is not found in parent store, we still cache it as nil.
|
||||
value, err = s.parent.Get(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
// Set implements types.KVStore.
|
||||
@ -36,7 +43,6 @@ func (s Store[T]) Set(key, value []byte) error {
|
||||
if value == nil {
|
||||
return errors.New("cannot set a nil value")
|
||||
}
|
||||
|
||||
s.changeSet.set(key, value)
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@ go 1.23
|
||||
|
||||
require (
|
||||
cosmossdk.io/core v1.0.0-alpha.5
|
||||
cosmossdk.io/core/testing v0.0.0
|
||||
cosmossdk.io/schema v0.3.0
|
||||
github.com/cosmos/gogoproto v1.7.0
|
||||
github.com/tidwall/btree v1.7.0
|
||||
@ -13,3 +14,5 @@ require (
|
||||
github.com/google/go-cmp v0.6.0 // indirect
|
||||
google.golang.org/protobuf v1.35.1 // indirect
|
||||
)
|
||||
|
||||
replace cosmossdk.io/core/testing => ../../../core/testing
|
||||
|
||||
@ -58,7 +58,7 @@ require (
|
||||
cloud.google.com/go/iam v1.1.13 // indirect
|
||||
cloud.google.com/go/storage v1.43.0 // indirect
|
||||
cosmossdk.io/collections v0.4.1-0.20241104084251-838f1557af0a // indirect
|
||||
cosmossdk.io/core/testing v0.0.0-20240923163230-04da382a9f29 // indirect
|
||||
cosmossdk.io/core/testing v0.0.0 // indirect
|
||||
cosmossdk.io/errors v1.0.1 // indirect
|
||||
cosmossdk.io/errors/v2 v2.0.0-20240731132947-df72853b3ca5 // indirect
|
||||
cosmossdk.io/schema v0.3.1-0.20241010135032-192601639cac // indirect
|
||||
@ -291,6 +291,7 @@ replace (
|
||||
// server v2 integration
|
||||
replace (
|
||||
cosmossdk.io/api => ../../api
|
||||
cosmossdk.io/core/testing => ../../core/testing
|
||||
cosmossdk.io/runtime/v2 => ../../runtime/v2
|
||||
cosmossdk.io/server/v2 => ../../server/v2
|
||||
cosmossdk.io/server/v2/appmanager => ../../server/v2/appmanager
|
||||
|
||||
@ -194,8 +194,6 @@ cloud.google.com/go/workflows v1.6.0/go.mod h1:6t9F5h/unJz41YqfBmqSASJSXccBLtD1V
|
||||
cloud.google.com/go/workflows v1.7.0/go.mod h1:JhSrZuVZWuiDfKEFxU0/F1PQjmpnpcoISEXH2bcHC3M=
|
||||
cosmossdk.io/core v1.0.0-alpha.5 h1:McjYXAQ6XcT20v2uHyH7PhoWH8V+mebzfVFqT3GinsI=
|
||||
cosmossdk.io/core v1.0.0-alpha.5/go.mod h1:3u9cWq1FAVtiiCrDPpo4LhR+9V6k/ycSG4/Y/tREWCY=
|
||||
cosmossdk.io/core/testing v0.0.0-20240923163230-04da382a9f29 h1:NxxUo0GMJUbIuVg0R70e3cbn9eFTEuMr7ev1AFvypdY=
|
||||
cosmossdk.io/core/testing v0.0.0-20240923163230-04da382a9f29/go.mod h1:8s2tPeJtSiQuoyPmr2Ag7meikonISO4Fv4MoO8+ORrs=
|
||||
cosmossdk.io/depinject v1.1.0 h1:wLan7LG35VM7Yo6ov0jId3RHWCGRhe8E8bsuARorl5E=
|
||||
cosmossdk.io/depinject v1.1.0/go.mod h1:kkI5H9jCGHeKeYWXTqYdruogYrEeWvBQCw1Pj4/eCFI=
|
||||
cosmossdk.io/errors v1.0.1 h1:bzu+Kcr0kS/1DuPBtUFdWjzLqyUuCiyHjyJB6srBV/0=
|
||||
|
||||
@ -32,7 +32,7 @@ require (
|
||||
)
|
||||
|
||||
require (
|
||||
cosmossdk.io/core/testing v0.0.0-20240923163230-04da382a9f29
|
||||
cosmossdk.io/core/testing v0.0.0
|
||||
cosmossdk.io/runtime/v2 v2.0.0-20240911143651-72620a577660
|
||||
cosmossdk.io/server/v2/stf v0.0.0-00010101000000-000000000000
|
||||
cosmossdk.io/store/v2 v2.0.0-00010101000000-000000000000
|
||||
@ -248,6 +248,7 @@ replace (
|
||||
cosmossdk.io/api => ../api
|
||||
cosmossdk.io/client/v2 => ../client/v2
|
||||
cosmossdk.io/collections => ../collections
|
||||
cosmossdk.io/core/testing => ../core/testing
|
||||
cosmossdk.io/indexer/postgres => ../indexer/postgres
|
||||
cosmossdk.io/runtime/v2 => ../runtime/v2
|
||||
cosmossdk.io/server/v2/appmanager => ../server/v2/appmanager
|
||||
|
||||
@ -194,8 +194,6 @@ cloud.google.com/go/workflows v1.6.0/go.mod h1:6t9F5h/unJz41YqfBmqSASJSXccBLtD1V
|
||||
cloud.google.com/go/workflows v1.7.0/go.mod h1:JhSrZuVZWuiDfKEFxU0/F1PQjmpnpcoISEXH2bcHC3M=
|
||||
cosmossdk.io/core v1.0.0-alpha.5 h1:McjYXAQ6XcT20v2uHyH7PhoWH8V+mebzfVFqT3GinsI=
|
||||
cosmossdk.io/core v1.0.0-alpha.5/go.mod h1:3u9cWq1FAVtiiCrDPpo4LhR+9V6k/ycSG4/Y/tREWCY=
|
||||
cosmossdk.io/core/testing v0.0.0-20240923163230-04da382a9f29 h1:NxxUo0GMJUbIuVg0R70e3cbn9eFTEuMr7ev1AFvypdY=
|
||||
cosmossdk.io/core/testing v0.0.0-20240923163230-04da382a9f29/go.mod h1:8s2tPeJtSiQuoyPmr2Ag7meikonISO4Fv4MoO8+ORrs=
|
||||
cosmossdk.io/depinject v1.1.0 h1:wLan7LG35VM7Yo6ov0jId3RHWCGRhe8E8bsuARorl5E=
|
||||
cosmossdk.io/depinject v1.1.0/go.mod h1:kkI5H9jCGHeKeYWXTqYdruogYrEeWvBQCw1Pj4/eCFI=
|
||||
cosmossdk.io/errors v1.0.1 h1:bzu+Kcr0kS/1DuPBtUFdWjzLqyUuCiyHjyJB6srBV/0=
|
||||
|
||||
Loading…
Reference in New Issue
Block a user