diff --git a/go.mod b/go.mod index 570ea3f95bbd..8c8f740d6459 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/99designs/keyring v1.1.6 github.com/armon/go-metrics v0.3.11 github.com/bgentry/speakeasy v0.1.0 - github.com/btcsuite/btcd v0.22.1 + github.com/btcsuite/btcd v0.22.3 github.com/coinbase/rosetta-sdk-go v0.7.0 github.com/confio/ics23/go v0.7.0 github.com/cosmos/btcutil v1.0.4 @@ -45,7 +45,8 @@ require ( github.com/tendermint/go-amino v0.16.0 github.com/tendermint/tendermint v0.34.21 github.com/tendermint/tm-db v0.6.6 - golang.org/x/crypto v0.1.0 + github.com/tidwall/btree v1.6.0 + golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f google.golang.org/grpc v1.53.0 google.golang.org/protobuf v1.28.1 diff --git a/go.sum b/go.sum index ef1da197b1fe..842bc98b3985 100644 --- a/go.sum +++ b/go.sum @@ -108,8 +108,8 @@ github.com/btcsuite/btcd v0.0.0-20190115013929-ed77733ec07d/go.mod h1:d3C0AkH6BR github.com/btcsuite/btcd v0.0.0-20190315201642-aa6e0f35703c/go.mod h1:DrZx5ec/dmnfpw9KyYoQyYo7d0KEvTkk/5M/vbZjAr8= github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= github.com/btcsuite/btcd v0.21.0-beta/go.mod h1:ZSWyehm27aAuS9bvkATT+Xte3hjHZ+MRgMY/8NJ7K94= -github.com/btcsuite/btcd v0.22.1 h1:CnwP9LM/M9xuRrGSCGeMVs9iv09uMqwsVX7EeIpgV2c= -github.com/btcsuite/btcd v0.22.1/go.mod h1:wqgTSL29+50LRkmOVknEdmt8ZojIzhuWvgu/iptuN7Y= +github.com/btcsuite/btcd v0.22.3 h1:kYNaWFvOw6xvqP0vR20RP1Zq1DVMBxEO8QN5d1/EfNg= +github.com/btcsuite/btcd v0.22.3/go.mod h1:wqgTSL29+50LRkmOVknEdmt8ZojIzhuWvgu/iptuN7Y= github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 h1:q0rUy8C/TYNBQS1+CGKw68tLOFYSNEs0TFnxxnS9+4U= github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= github.com/btcsuite/btcutil v0.0.0-20180706230648-ab6388e0c60a/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= @@ -771,6 +771,8 @@ github.com/tendermint/go-amino v0.16.0 h1:GyhmgQKvqF82e2oZeuMSp9JTN0N09emoSZlb2l github.com/tendermint/go-amino v0.16.0/go.mod h1:TQU0M1i/ImAo+tYpZi73AU3V/dKeCoMC9Sphe2ZwGME= github.com/tendermint/tendermint v0.34.21 h1:UiGGnBFHVrZhoQVQ7EfwSOLuCtarqCSsRf8VrklqB7s= github.com/tendermint/tendermint v0.34.21/go.mod h1:XDvfg6U7grcFTDx7VkzxnhazQ/bspGJAn4DZ6DcLLjQ= +github.com/tidwall/btree v1.6.0 h1:LDZfKfQIBHGHWSwckhXI0RPSXzlo+KYdjK7FWSqOzzg= +github.com/tidwall/btree v1.6.0/go.mod h1:twD9XRA5jj9VUQGELzDO4HPQTNJsoWWfYEL+EUQ2cKY= github.com/tidwall/gjson v1.6.7/go.mod h1:zeFuBCIqD4sN/gmqBzZ4j7Jd6UcA2Fc56x7QFsv+8fI= github.com/tidwall/match v1.0.3/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/pretty v1.0.2/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= @@ -842,8 +844,8 @@ golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU= -golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= +golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e h1:T8NU3HyQ8ClP4SEE+KbFlg6n0NhuTsN4MyznaarGsZM= +golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= diff --git a/store/cachekv/internal/memcache.go b/store/cachekv/internal/memcache.go new file mode 100644 index 000000000000..392237464a55 --- /dev/null +++ b/store/cachekv/internal/memcache.go @@ -0,0 +1,95 @@ +package internal + +import ( + "bytes" + "errors" + + "github.com/tidwall/btree" +) + +var errKeyEmpty = errors.New("key cannot be empty") + +const ( + // The approximate number of items and children per B-tree node. Tuned with benchmarks. + // copied from memdb. + bTreeDegree = 32 +) + +// MemCache implements the in-memory cache 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. +// +// We choose tidwall/btree over google/btree here because it provides API to implement step iterator directly. +type MemCache struct { + tree *btree.BTreeG[item] +} + +// NewMemCache creates a wrapper around a btree.BTreeG object. +func NewMemCache() MemCache { + return MemCache{ + tree: btree.NewBTreeGOptions(byKeys, btree.Options{ + Degree: bTreeDegree, + NoLocks: false, + }), + } +} + +// Set set a cache entry, dirty means it's newly set, +// `nil` value means a deletion. +func (bt MemCache) Set(key, value []byte, dirty bool) { + bt.tree.Set(item{key: key, value: value, dirty: dirty}) +} + +// Get returns (value, found) +func (bt MemCache) Get(key []byte) ([]byte, bool) { + i, found := bt.tree.Get(item{key: key}) + if !found { + return nil, false + } + return i.value, true +} + +// ScanDirtyItems iterate over the dirty entries. +func (bt MemCache) ScanDirtyItems(fn func(key, value []byte)) { + bt.tree.Scan(func(item item) bool { + if item.dirty { + fn(item.key, item.value) + } + return true + }) +} + +// Copy the cache. This is a copy-on-write operation and is very fast because +// it only performs a shadowed copy. +func (bt MemCache) Copy() MemCache { + return MemCache{tree: bt.tree.IsoCopy()} +} + +// Iterator iterates on a isolated view of the cache, not affected by future modifications. +func (bt MemCache) Iterator(start, end []byte) *memIterator { + if (start != nil && len(start) == 0) || (end != nil && len(end) == 0) { + panic(errKeyEmpty) + } + return NewMemIterator(start, end, bt.Copy(), true) +} + +// ReverseIterator iterates on a isolated view of the cache, not affected by future modifications. +func (bt MemCache) ReverseIterator(start, end []byte) *memIterator { + if (start != nil && len(start) == 0) || (end != nil && len(end) == 0) { + panic(errKeyEmpty) + } + return NewMemIterator(start, end, bt.Copy(), false) +} + +// item represents a cached key-value pair and the entry of the cache btree. +// If dirty is true, it indicates the cached value is newly set, maybe different from the underlying value. +type item struct { + key []byte + value []byte + dirty bool +} + +// byKeys compares the items by key +func byKeys(a, b item) bool { + return bytes.Compare(a.key, b.key) == -1 +} diff --git a/store/cachekv/internal/memcache_test.go b/store/cachekv/internal/memcache_test.go new file mode 100644 index 000000000000..f020517c6481 --- /dev/null +++ b/store/cachekv/internal/memcache_test.go @@ -0,0 +1,170 @@ +package internal + +import ( + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/require" +) + +func TestGetSetDelete(t *testing.T) { + db := NewMemCache() + + // A nonexistent key should return nil. + value, found := db.Get([]byte("a")) + require.Nil(t, value) + require.False(t, found) + + // Set and get a value. + db.Set([]byte("a"), []byte{0x01}, true) + db.Set([]byte("b"), []byte{0x02}, false) + value, found = db.Get([]byte("a")) + require.Equal(t, []byte{0x01}, value) + require.True(t, found) + + value, found = db.Get([]byte("b")) + require.Equal(t, []byte{0x02}, value) + require.True(t, found) + + var dirties [][]byte + db.ScanDirtyItems(func(k, v []byte) { + dirties = append(dirties, k) + }) + require.Equal(t, [][]byte{[]byte("a")}, dirties) +} + +func TestDBIterator(t *testing.T) { + db := NewMemCache() + + for i := 0; i < 10; i++ { + if i != 6 { // but skip 6. + db.Set(int642Bytes(int64(i)), []byte{}, false) + } + } + + // Blank iterator keys should panic + require.Panics(t, func() { + db.ReverseIterator([]byte{}, nil) + }) + require.Panics(t, func() { + db.ReverseIterator(nil, []byte{}) + }) + + itr := db.Iterator(nil, nil) + verifyIterator(t, itr, []int64{0, 1, 2, 3, 4, 5, 7, 8, 9}, "forward iterator") + + ritr := db.ReverseIterator(nil, nil) + verifyIterator(t, ritr, []int64{9, 8, 7, 5, 4, 3, 2, 1, 0}, "reverse iterator") + + itr = db.Iterator(nil, int642Bytes(0)) + verifyIterator(t, itr, []int64(nil), "forward iterator to 0") + + ritr = db.ReverseIterator(int642Bytes(10), nil) + verifyIterator(t, ritr, []int64(nil), "reverse iterator from 10 (ex)") + + itr = db.Iterator(int642Bytes(0), nil) + verifyIterator(t, itr, []int64{0, 1, 2, 3, 4, 5, 7, 8, 9}, "forward iterator from 0") + + itr = db.Iterator(int642Bytes(1), nil) + verifyIterator(t, itr, []int64{1, 2, 3, 4, 5, 7, 8, 9}, "forward iterator from 1") + + ritr = db.ReverseIterator(nil, int642Bytes(10)) + verifyIterator(t, ritr, + []int64{9, 8, 7, 5, 4, 3, 2, 1, 0}, "reverse iterator from 10 (ex)") + + ritr = db.ReverseIterator(nil, int642Bytes(9)) + verifyIterator(t, ritr, + []int64{8, 7, 5, 4, 3, 2, 1, 0}, "reverse iterator from 9 (ex)") + + ritr = db.ReverseIterator(nil, int642Bytes(8)) + verifyIterator(t, ritr, + []int64{7, 5, 4, 3, 2, 1, 0}, "reverse iterator from 8 (ex)") + + itr = db.Iterator(int642Bytes(5), int642Bytes(6)) + verifyIterator(t, itr, []int64{5}, "forward iterator from 5 to 6") + + itr = db.Iterator(int642Bytes(5), int642Bytes(7)) + verifyIterator(t, itr, []int64{5}, "forward iterator from 5 to 7") + + itr = db.Iterator(int642Bytes(5), int642Bytes(8)) + verifyIterator(t, itr, []int64{5, 7}, "forward iterator from 5 to 8") + + itr = db.Iterator(int642Bytes(6), int642Bytes(7)) + verifyIterator(t, itr, []int64(nil), "forward iterator from 6 to 7") + + itr = db.Iterator(int642Bytes(6), int642Bytes(8)) + verifyIterator(t, itr, []int64{7}, "forward iterator from 6 to 8") + + itr = db.Iterator(int642Bytes(7), int642Bytes(8)) + verifyIterator(t, itr, []int64{7}, "forward iterator from 7 to 8") + + ritr = db.ReverseIterator(int642Bytes(4), int642Bytes(5)) + verifyIterator(t, ritr, []int64{4}, "reverse iterator from 5 (ex) to 4") + + ritr = db.ReverseIterator(int642Bytes(4), int642Bytes(6)) + verifyIterator(t, ritr, + []int64{5, 4}, "reverse iterator from 6 (ex) to 4") + + ritr = db.ReverseIterator(int642Bytes(4), int642Bytes(7)) + verifyIterator(t, ritr, + []int64{5, 4}, "reverse iterator from 7 (ex) to 4") + + ritr = db.ReverseIterator(int642Bytes(5), int642Bytes(6)) + verifyIterator(t, ritr, []int64{5}, "reverse iterator from 6 (ex) to 5") + + ritr = db.ReverseIterator(int642Bytes(5), int642Bytes(7)) + verifyIterator(t, ritr, []int64{5}, "reverse iterator from 7 (ex) to 5") + + ritr = db.ReverseIterator(int642Bytes(6), int642Bytes(7)) + verifyIterator(t, ritr, + []int64(nil), "reverse iterator from 7 (ex) to 6") + + ritr = db.ReverseIterator(int642Bytes(10), nil) + verifyIterator(t, ritr, []int64(nil), "reverse iterator to 10") + + ritr = db.ReverseIterator(int642Bytes(6), nil) + verifyIterator(t, ritr, []int64{9, 8, 7}, "reverse iterator to 6") + + ritr = db.ReverseIterator(int642Bytes(5), nil) + verifyIterator(t, ritr, []int64{9, 8, 7, 5}, "reverse iterator to 5") + + ritr = db.ReverseIterator(int642Bytes(8), int642Bytes(9)) + verifyIterator(t, ritr, []int64{8}, "reverse iterator from 9 (ex) to 8") + + ritr = db.ReverseIterator(int642Bytes(2), int642Bytes(4)) + verifyIterator(t, ritr, + []int64{3, 2}, "reverse iterator from 4 (ex) to 2") + + ritr = db.ReverseIterator(int642Bytes(4), int642Bytes(2)) + verifyIterator(t, ritr, + []int64(nil), "reverse iterator from 2 (ex) to 4") + + // Ensure that the iterators don't panic with an empty database. + db2 := NewMemCache() + + itr = db2.Iterator(nil, nil) + verifyIterator(t, itr, nil, "forward iterator with empty db") + + ritr = db2.ReverseIterator(nil, nil) + verifyIterator(t, ritr, nil, "reverse iterator with empty db") +} + +func verifyIterator(t *testing.T, itr *memIterator, expected []int64, msg string) { + i := 0 + for itr.Valid() { + key := itr.Key() + require.Equal(t, expected[i], bytes2Int64(key), "iterator: %d mismatches", i) + itr.Next() + i++ + } + require.Equal(t, i, len(expected), "expected to have fully iterated over all the elements in iter") + require.NoError(t, itr.Close()) +} + +func int642Bytes(i int64) []byte { + return sdk.Uint64ToBigEndian(uint64(i)) +} + +func bytes2Int64(buf []byte) int64 { + return int64(sdk.BigEndianToUint64(buf)) +} diff --git a/store/cachekv/internal/memiterator.go b/store/cachekv/internal/memiterator.go new file mode 100644 index 000000000000..3d5b654e9770 --- /dev/null +++ b/store/cachekv/internal/memiterator.go @@ -0,0 +1,119 @@ +package internal + +import ( + "bytes" + "errors" + + "github.com/cosmos/cosmos-sdk/store/types" + "github.com/tidwall/btree" +) + +var _ types.Iterator = (*memIterator)(nil) + +// memIterator iterates over iterKVCache items. +// if key is nil, means it was deleted. +// Implements Iterator. +type memIterator struct { + iter btree.IterG[item] + + start []byte + end []byte + ascending bool + valid bool +} + +func NewMemIterator(start, end []byte, items MemCache, ascending bool) *memIterator { + iter := items.tree.Iter() + var valid bool + if ascending { + if start != nil { + valid = iter.Seek(item{key: start}) + } else { + valid = iter.First() + } + } else { + if end != nil { + valid = iter.Seek(item{key: end}) + if !valid { + valid = iter.Last() + } else { + // end is exclusive + valid = iter.Prev() + } + } else { + valid = iter.Last() + } + } + + mi := &memIterator{ + iter: iter, + start: start, + end: end, + ascending: ascending, + valid: valid, + } + + if mi.valid { + mi.valid = mi.keyInRange(mi.Key()) + } + + return mi +} + +func (mi *memIterator) Domain() (start []byte, end []byte) { + return mi.start, mi.end +} + +func (mi *memIterator) Close() error { + mi.iter.Release() + return nil +} + +func (mi *memIterator) Error() error { + if !mi.Valid() { + return errors.New("invalid memIterator") + } + return nil +} + +func (mi *memIterator) Valid() bool { + return mi.valid +} + +func (mi *memIterator) Next() { + mi.assertValid() + + if mi.ascending { + mi.valid = mi.iter.Next() + } else { + mi.valid = mi.iter.Prev() + } + + if mi.valid { + mi.valid = mi.keyInRange(mi.Key()) + } +} + +func (mi *memIterator) keyInRange(key []byte) bool { + if mi.ascending && mi.end != nil && bytes.Compare(key, mi.end) >= 0 { + return false + } + if !mi.ascending && mi.start != nil && bytes.Compare(key, mi.start) < 0 { + return false + } + return true +} + +func (mi *memIterator) Key() []byte { + return mi.iter.Item().key +} + +func (mi *memIterator) Value() []byte { + return mi.iter.Item().value +} + +func (mi *memIterator) assertValid() { + if err := mi.Error(); err != nil { + panic(err) + } +} diff --git a/store/cachekv/mergeiterator.go b/store/cachekv/internal/mergeiterator.go similarity index 82% rename from store/cachekv/mergeiterator.go rename to store/cachekv/internal/mergeiterator.go index 25dfac803329..4186a178a863 100644 --- a/store/cachekv/mergeiterator.go +++ b/store/cachekv/internal/mergeiterator.go @@ -1,4 +1,4 @@ -package cachekv +package internal import ( "bytes" @@ -18,79 +18,63 @@ type cacheMergeIterator struct { parent types.Iterator cache types.Iterator ascending bool + + valid bool } var _ types.Iterator = (*cacheMergeIterator)(nil) -func newCacheMergeIterator(parent, cache types.Iterator, ascending bool) *cacheMergeIterator { +func NewCacheMergeIterator(parent, cache types.Iterator, ascending bool) *cacheMergeIterator { iter := &cacheMergeIterator{ parent: parent, cache: cache, ascending: ascending, } + iter.valid = iter.skipUntilExistsOrInvalid() return iter } // Domain implements Iterator. -// If the domains are different, returns the union. +// Returns parent domain because cache and parent domains are the same. func (iter *cacheMergeIterator) Domain() (start, end []byte) { - startP, endP := iter.parent.Domain() - startC, endC := iter.cache.Domain() - - if iter.compare(startP, startC) < 0 { - start = startP - } else { - start = startC - } - - if iter.compare(endP, endC) < 0 { - end = endC - } else { - end = endP - } - - return start, end + return iter.parent.Domain() } // Valid implements Iterator. func (iter *cacheMergeIterator) Valid() bool { - return iter.skipUntilExistsOrInvalid() + return iter.valid } // Next implements Iterator func (iter *cacheMergeIterator) Next() { - iter.skipUntilExistsOrInvalid() iter.assertValid() - // If parent is invalid, get the next cache item. - if !iter.parent.Valid() { + switch { + case !iter.parent.Valid(): + // If parent is invalid, get the next cache item. iter.cache.Next() - return - } - - // If cache is invalid, get the next parent item. - if !iter.cache.Valid() { - iter.parent.Next() - return - } - - // 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 + case !iter.cache.Valid(): + // If cache is invalid, get the next parent item. iter.parent.Next() - iter.cache.Next() - case 1: // parent > cache - iter.cache.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() + } } + iter.valid = iter.skipUntilExistsOrInvalid() } // Key implements Iterator func (iter *cacheMergeIterator) Key() []byte { - iter.skipUntilExistsOrInvalid() iter.assertValid() // If parent is invalid, get the cache key. @@ -121,7 +105,6 @@ func (iter *cacheMergeIterator) Key() []byte { // Value implements Iterator func (iter *cacheMergeIterator) Value() []byte { - iter.skipUntilExistsOrInvalid() iter.assertValid() // If parent is invalid, get the cache value. @@ -152,11 +135,12 @@ func (iter *cacheMergeIterator) Value() []byte { // Close implements Iterator func (iter *cacheMergeIterator) Close() error { + err1 := iter.cache.Close() if err := iter.parent.Close(); err != nil { return err } - return iter.cache.Close() + return err1 } // Error returns an error if the cacheMergeIterator is invalid defined by the diff --git a/store/cachekv/memiterator.go b/store/cachekv/memiterator.go deleted file mode 100644 index 04df40ff56aa..000000000000 --- a/store/cachekv/memiterator.go +++ /dev/null @@ -1,56 +0,0 @@ -package cachekv - -import ( - "bytes" - - dbm "github.com/tendermint/tm-db" - - "github.com/cosmos/cosmos-sdk/store/types" -) - -// Iterates over iterKVCache items. -// if key is nil, means it was deleted. -// Implements Iterator. -type memIterator struct { - types.Iterator - - lastKey []byte - deleted map[string]struct{} -} - -func newMemIterator(start, end []byte, items *dbm.MemDB, deleted map[string]struct{}, ascending bool) *memIterator { - var iter types.Iterator - var err error - - if ascending { - iter, err = items.Iterator(start, end) - } else { - iter, err = items.ReverseIterator(start, end) - } - - if err != nil { - panic(err) - } - - return &memIterator{ - Iterator: iter, - - lastKey: nil, - deleted: deleted, - } -} - -func (mi *memIterator) Value() []byte { - key := mi.Iterator.Key() - // We need to handle the case where deleted is modified and includes our current key - // We handle this by maintaining a lastKey object in the iterator. - // If the current key is the same as the last key (and last key is not nil / the start) - // then we are calling value on the same thing as last time. - // Therefore we don't check the mi.deleted to see if this key is included in there. - reCallingOnOldLastKey := (mi.lastKey != nil) && bytes.Equal(key, mi.lastKey) - if _, ok := mi.deleted[string(key)]; ok && !reCallingOnOldLastKey { - return nil - } - mi.lastKey = key - return mi.Iterator.Value() -} diff --git a/store/cachekv/store.go b/store/cachekv/store.go index f599a01e8533..e412af1e6277 100644 --- a/store/cachekv/store.go +++ b/store/cachekv/store.go @@ -1,39 +1,21 @@ package cachekv import ( - "bytes" + "github.com/cosmos/cosmos-sdk/store/listenkv" "io" - "reflect" - "sort" - "sync" - "time" - "unsafe" - - dbm "github.com/tendermint/tm-db" - "github.com/cosmos/cosmos-sdk/internal/conv" - "github.com/cosmos/cosmos-sdk/store/listenkv" + "github.com/cosmos/cosmos-sdk/store/cachekv/internal" "github.com/cosmos/cosmos-sdk/store/tracekv" "github.com/cosmos/cosmos-sdk/store/types" - "github.com/cosmos/cosmos-sdk/telemetry" - "github.com/cosmos/cosmos-sdk/types/kv" ) -// If value is nil but deleted is false, it means the parent doesn't have the -// key. (No need to delete upon Write()) -type cValue struct { - value []byte - dirty bool -} - // Store wraps an in-memory cache around an underlying types.KVStore. +// If a cached value is nil but deleted is defined for the corresponding key, +// it means the parent doesn't have the key. (No need to delete upon Write()) type Store struct { - mtx sync.Mutex - cache map[string]*cValue - deleted map[string]struct{} - unsortedCache map[string]struct{} - sortedCache *dbm.MemDB // always ascending sorted - parent types.KVStore + // always ascending sorted + cache internal.MemCache + parent types.KVStore } var _ types.CacheKVStore = (*Store)(nil) @@ -41,11 +23,8 @@ var _ types.CacheKVStore = (*Store)(nil) // NewStore creates a new Store object func NewStore(parent types.KVStore) *Store { return &Store{ - cache: make(map[string]*cValue), - deleted: make(map[string]struct{}), - unsortedCache: make(map[string]struct{}), - sortedCache: dbm.NewMemDB(), - parent: parent, + cache: internal.NewMemCache(), + parent: parent, } } @@ -55,32 +34,24 @@ func (store *Store) GetStoreType() types.StoreType { } // Get implements types.KVStore. -func (store *Store) Get(key []byte) (value []byte) { - store.mtx.Lock() - defer store.mtx.Unlock() - +func (store *Store) Get(key []byte) []byte { types.AssertValidKey(key) - cacheValue, ok := store.cache[conv.UnsafeBytesToStr(key)] - if !ok { - value = store.parent.Get(key) - store.setCacheValue(key, value, false, false) - } else { - value = cacheValue.value + if value, found := store.cache.Get(key); found { + return value } + value := store.parent.Get(key) + store.setCacheValue(key, value, false) return value } // Set implements types.KVStore. func (store *Store) Set(key []byte, value []byte) { - store.mtx.Lock() - defer store.mtx.Unlock() - types.AssertValidKey(key) types.AssertValidValue(value) - store.setCacheValue(key, value, false, true) + store.setCacheValue(key, value, true) } // Has implements types.KVStore. @@ -91,64 +62,21 @@ func (store *Store) Has(key []byte) bool { // Delete implements types.KVStore. func (store *Store) Delete(key []byte) { - store.mtx.Lock() - defer store.mtx.Unlock() - defer telemetry.MeasureSince(time.Now(), "store", "cachekv", "delete") - types.AssertValidKey(key) - store.setCacheValue(key, nil, true, true) + store.setCacheValue(key, nil, true) } // Implements Cachetypes.KVStore. func (store *Store) Write() { - store.mtx.Lock() - defer store.mtx.Unlock() - defer telemetry.MeasureSince(time.Now(), "store", "cachekv", "write") - - // We need a copy of all of the keys. - // Not the best, but probably not a bottleneck depending. - keys := make([]string, 0, len(store.cache)) - - for key, dbValue := range store.cache { - if dbValue.dirty { - keys = append(keys, key) - } - } - - sort.Strings(keys) - - // TODO: Consider allowing usage of Batch, which would allow the write to - // at least happen atomically. - for _, key := range keys { - if store.isDeleted(key) { - // We use []byte(key) instead of conv.UnsafeStrToBytes because we cannot - // be sure if the underlying store might do a save with the byteslice or - // not. Once we get confirmation that .Delete is guaranteed not to - // save the byteslice, then we can assume only a read-only copy is sufficient. - store.parent.Delete([]byte(key)) - continue + store.cache.ScanDirtyItems(func(key, value []byte) { + if value == nil { + store.parent.Delete(key) + } else { + store.parent.Set(key, value) } + }) - cacheValue := store.cache[key] - if cacheValue.value != nil { - // It already exists in the parent, hence delete it. - store.parent.Set([]byte(key), cacheValue.value) - } - } - - // Clear the cache using the map clearing idiom - // and not allocating fresh objects. - // Please see https://bencher.orijtech.com/perfclinic/mapclearing/ - for key := range store.cache { - delete(store.cache, key) - } - for key := range store.deleted { - delete(store.deleted, key) - } - for key := range store.unsortedCache { - delete(store.unsortedCache, key) - } - store.sortedCache = dbm.NewMemDB() + store.cache = internal.NewMemCache() } // CacheWrap implements CacheWrapper. @@ -161,246 +89,32 @@ func (store *Store) CacheWrapWithTrace(w io.Writer, tc types.TraceContext) types return NewStore(tracekv.NewStore(store, w, tc)) } -// CacheWrapWithListeners implements the CacheWrapper interface. -func (store *Store) CacheWrapWithListeners(storeKey types.StoreKey, listeners []types.WriteListener) types.CacheWrap { - return NewStore(listenkv.NewStore(store, storeKey, listeners)) -} - //---------------------------------------- // Iteration // Iterator implements types.KVStore. func (store *Store) Iterator(start, end []byte) types.Iterator { - return store.iterator(start, end, true) + return internal.NewCacheMergeIterator( + store.parent.Iterator(start, end), + store.cache.Iterator(start, end), + true, + ) } // ReverseIterator implements types.KVStore. func (store *Store) ReverseIterator(start, end []byte) types.Iterator { - return store.iterator(start, end, false) -} - -func (store *Store) iterator(start, end []byte, ascending bool) types.Iterator { - store.mtx.Lock() - defer store.mtx.Unlock() - - var parent, cache types.Iterator - - if ascending { - parent = store.parent.Iterator(start, end) - } else { - parent = store.parent.ReverseIterator(start, end) - } - - store.dirtyItems(start, end) - cache = newMemIterator(start, end, store.sortedCache, store.deleted, ascending) - - return newCacheMergeIterator(parent, cache, ascending) -} - -func findStartIndex(strL []string, startQ string) int { - // Modified binary search to find the very first element in >=startQ. - if len(strL) == 0 { - return -1 - } - - var left, right, mid int - right = len(strL) - 1 - for left <= right { - mid = (left + right) >> 1 - midStr := strL[mid] - if midStr == startQ { - // Handle condition where there might be multiple values equal to startQ. - // We are looking for the very first value < midStL, that i+1 will be the first - // element >= midStr. - for i := mid - 1; i >= 0; i-- { - if strL[i] != midStr { - return i + 1 - } - } - return 0 - } - if midStr < startQ { - left = mid + 1 - } else { // midStrL > startQ - right = mid - 1 - } - } - if left >= 0 && left < len(strL) && strL[left] >= startQ { - return left - } - return -1 + return internal.NewCacheMergeIterator( + store.parent.ReverseIterator(start, end), + store.cache.ReverseIterator(start, end), + false, + ) } -func findEndIndex(strL []string, endQ string) int { - if len(strL) == 0 { - return -1 - } - - // Modified binary search to find the very first element > 1 - midStr := strL[mid] - if midStr == endQ { - // Handle condition where there might be multiple values equal to startQ. - // We are looking for the very first value < midStL, that i+1 will be the first - // element >= midStr. - for i := mid - 1; i >= 0; i-- { - if strL[i] < midStr { - return i + 1 - } - } - return 0 - } - if midStr < endQ { - left = mid + 1 - } else { // midStrL > startQ - right = mid - 1 - } - } - - // Binary search failed, now let's find a value less than endQ. - for i := right; i >= 0; i-- { - if strL[i] < endQ { - return i - } - } - - return -1 -} - -//nolint -func findStartEndIndex(strL []string, startStr, endStr string) (int, int) { - // Now find the values within the domain - // [start, end) - startIndex := findStartIndex(strL, startStr) - endIndex := findEndIndex(strL, endStr) - - if endIndex < 0 { - endIndex = len(strL) - 1 - } - if startIndex < 0 { - startIndex = 0 - } - return startIndex, endIndex -} - -type sortState int - -const ( - stateUnsorted sortState = iota - stateAlreadySorted -) - -// strToByte is meant to make a zero allocation conversion -// from string -> []byte to speed up operations, it is not meant -// to be used generally, but for a specific pattern to check for available -// keys within a domain. -func strToByte(s string) []byte { - var b []byte - hdr := (*reflect.SliceHeader)(unsafe.Pointer(&b)) - hdr.Cap = len(s) - hdr.Len = len(s) - hdr.Data = (*reflect.StringHeader)(unsafe.Pointer(&s)).Data - return b -} - -// byteSliceToStr is meant to make a zero allocation conversion -// from []byte -> string to speed up operations, it is not meant -// to be used generally, but for a specific pattern to delete keys -// from a map. -func byteSliceToStr(b []byte) string { - hdr := (*reflect.StringHeader)(unsafe.Pointer(&b)) - return *(*string)(unsafe.Pointer(hdr)) -} - -// Constructs a slice of dirty items, to use w/ memIterator. -func (store *Store) dirtyItems(start, end []byte) { - startStr, endStr := conv.UnsafeBytesToStr(start), conv.UnsafeBytesToStr(end) - if startStr > endStr { - // Nothing to do here. - return - } - - n := len(store.unsortedCache) - unsorted := make([]*kv.Pair, 0) - // If the unsortedCache is too big, its costs too much to determine - // whats in the subset we are concerned about. - // If you are interleaving iterator calls with writes, this can easily become an - // O(N^2) overhead. - // Even without that, too many range checks eventually becomes more expensive - // than just not having the cache. - if n >= 256 { - for key := range store.unsortedCache { - cacheValue := store.cache[key] - keyBz := strToByte(key) - unsorted = append(unsorted, &kv.Pair{Key: keyBz, Value: cacheValue.value}) - } - } else { - // else do a linear scan to determine if the unsorted pairs are in the pool. - for key := range store.unsortedCache { - keyBz := strToByte(key) - if dbm.IsKeyInDomain(keyBz, start, end) { - cacheValue := store.cache[key] - unsorted = append(unsorted, &kv.Pair{Key: keyBz, Value: cacheValue.value}) - } - } - } - store.clearUnsortedCacheSubset(unsorted) -} - -func (store *Store) clearUnsortedCacheSubset(unsorted []*kv.Pair) { - n := len(store.unsortedCache) - if len(unsorted) == n { // This pattern allows the Go compiler to emit the map clearing idiom for the entire map. - for key := range store.unsortedCache { - delete(store.unsortedCache, key) - } - store.unsortedCache = make(map[string]struct{}, 300) - } else { // Otherwise, normally delete the unsorted keys from the map. - for _, kv := range unsorted { - delete(store.unsortedCache, byteSliceToStr(kv.Key)) - } - } - sort.Slice(unsorted, func(i, j int) bool { - return bytes.Compare(unsorted[i].Key, unsorted[j].Key) < 0 - }) - - for _, item := range unsorted { - if item.Value == nil { - // deleted element, tracked by store.deleted - // setting arbitrary value - store.sortedCache.Set(item.Key, []byte{}) - continue - } - err := store.sortedCache.Set(item.Key, item.Value) - if err != nil { - panic(err) - } - } -} - -//---------------------------------------- -// etc - // Only entrypoint to mutate store.cache. -func (store *Store) setCacheValue(key, value []byte, deleted bool, dirty bool) { - keyStr := byteSliceToStr(key) - store.cache[keyStr] = &cValue{ - value: value, - dirty: dirty, - } - if deleted { - store.deleted[keyStr] = struct{}{} - } else { - delete(store.deleted, keyStr) - } - if dirty { - store.unsortedCache[keyStr] = struct{}{} - } +func (store *Store) setCacheValue(key, value []byte, dirty bool) { + store.cache.Set(key, value, dirty) } -func (store *Store) isDeleted(key string) bool { - _, ok := store.deleted[key] - return ok +func (store *Store) CacheWrapWithListeners(storeKey types.StoreKey, listeners []types.WriteListener) types.CacheWrap { + return NewStore(listenkv.NewStore(store, storeKey, listeners)) }