From dde2af06eb78a8af48236d32a6209299b647dd37 Mon Sep 17 00:00:00 2001 From: Daniel Gushchyan Date: Tue, 22 Nov 2022 11:55:30 -0800 Subject: [PATCH 1/5] docs on current cachekv implementation --- store/cachekv/README.md | 146 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100644 store/cachekv/README.md diff --git a/store/cachekv/README.md b/store/cachekv/README.md new file mode 100644 index 000000000000..2dcceb086463 --- /dev/null +++ b/store/cachekv/README.md @@ -0,0 +1,146 @@ +# CacheKVStore specification + +A `CacheKVStore` is cache wrapper for a `KVStore`. It extends the operations of the `KVStore` to work with a cache, allowing for reduced I/O operations and more efficient disposing of changes (e.g. after processing a failed transaction). + +## Types and Structs + +--- + +```go +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 +} +``` + +The Store struct wraps the underlying `KVStore` (referred to as `parent`) with additional data structures for implementing the cache. Mutex is needed as IAVL trees (the `KVStore` in application) are not safe for concurrent use. + +### `cache` + +The main mapping of key-value pairs stored in cache. This map contains both keys that are cached from read operations as well as ‘dirty’ keys which map to a value that is different than what is in the underlying `KVStore`. + +Values that are mapped to in the `cache` map are wrapped in a `cValue` struct, which contains the value and a boolean flag (`dirty`) representing whether the value differs from the `parent`. + +```go +type cValue struct { + value []byte + dirty bool +} +``` + +### `deleted` + +Key-value pairs that are to be deleted from the `parent`are stored in the `deleted` map. Keys are mapped to an empty struct to implement a set. + +### `unsortedCache` + +Similar to`deleted`, this is a set of keys that are dirty and will need to be updated in the `KVStore` upon a write. Keys are mapped to an empty struct to implement a set. + +### `sortedCache` + +A database that will be populated by the keys in `unsortedCache` during iteration over the cache. Keys are always inserted in sorted order. + +## CRUD Operations and Writing + +--- + +The `Set`, `Get`, and `Delete` functions all reference `setCacheValue()` , which is the only entrypoint to mutating the `cache` map. + +`setCacheValue()` is defined as follows: + +```go +func (store *Store) setCacheValue(key, value []byte, deleted bool, dirty bool) { + keyStr := conv.UnsafeBytesToStr(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{}{} + } +} +``` + +`setCacheValue()` inserts a key-value pair into the `cache` map. Two boolean parameters, `deleted` and `dirty`, are passed in to flag whether the inserted key should also be inserted into the `deleted` and `dirty` sets. + +### `Get` + +`Get` first attempts to return the value from the `cache` map. If the key does not exist in `cache`, gets the value from `store.parent` instead and calls `setCacheValue()` with `deleted=false` and `dirty=false`. + +### `Set` + +Calls `setCacheValue()` with `deleted=false` and `dirty=true`. + +### `Delete` + +A value being deleted from the `KVStore` is represented with a `nil` value in the `cache` map, and an insertion of the key into the `deleted` map. + +Calls `setCacheValue` with `deleted=true` and `dirty=true`. + +### `Write` + +Values in the `cache` are written to `parent` in ascending order of their keys. + +A slice of all dirty keys in the `cache` is made, then sorted in increasing order. These keys are then iterated over to update `parent`. + +If a key is marked for deletion (checked with `isDeleted()`), then the `parent.Delete` is called. Otherwise, `parent.Set` is called to update the underlying `KVStore` with the value in cache. + +## Iteration + +--- + +Efficient iteration over keys in the `KVStore` is important for efficiently generating Merkle range proofs. + +To iterate over the `CacheKVStore`, `iterate()` must iterate over a merged set of keys from the parent `KVStore` and cache. + +`[cacheMergeIterator](https://github.com/cosmos/cosmos-sdk/blob/d8391cb6796d770b02448bee70b865d824e43449/store/cachekv/mergeiterator.go)` implements functions to provide a single iterator with an input of iterators over`parent` and `cache`. This iterator iterates over keys from both iterators in a shared lexicological order, and overrides the value provided by the parent iterator if the same key is dirty or deleted in the cache. + +### Implementation Overview + +The iterators over the `parent` and `cache` are generated and passed into `cacheMergeIterator`, which is the iterator that is returned. Implementation of the `parent` iterator is up to the underlying `KVStore` . The rest of the implementation details here will cover the generation of the `cache` iterator. + +Generating the cache iterator can be decomposed into four parts: + +1. Finding all keys that exist in the range we are iterating over +2. Sorting this list of keys +3. Inserting these keys into the `sortedCache` and removing them from the `unsortedCache` +4. Returning an iterator over `sortedCache` with the desired range + +Currently, the implementation for the first two parts is split into two cases, depending on the size of the unsorted cache. The two cases are as follows. + +If the size of the `unsortedCache` is less than `minSortSize` (currently 1024), a linear time approach is taken to search over keys. + +```go +n := len(store.unsortedCache) +unsorted := make([]*kv.Pair, 0) + +if n < minSortSize { + for key := range store.unsortedCache { + if dbm.IsKeyInDomain(conv.UnsafeStrToBytes(key), start, end) { + cacheValue := store.cache[key] + unsorted = append(unsorted, &kv.Pair{Key: []byte(key), Value: cacheValue.value}) + } + } + store.clearUnsortedCacheSubset(unsorted, stateUnsorted) + return +} +``` + +Here, we iterate through all the keys in the `unsortedCache`, collecting them in a slice called `unsorted`. + +At this point, part 3. is achieved in`clearUnsortedCacheSubset()`. This function iterates through `unsorted`, removing each key from `unsortedCache` . Afterwards, the`unsorted` slice is sorted. Lastly, it iterates through the key-pairs in the now sorted slice, setting any key meant to be deleted to map to an arbitrary value (`[]byte{}`). + +In the case that the size of `unsortedCache` is larger than `minSortSize` , a linear time approach to finding keys within the desired range is too slow to use. Instead, a slice of all keys in `unsortedCache` is sorted, and binary search is used to find the beginning and ending indices of the desired range. This produces an already-sorted slice that is passed into the same `clearUnsortedCacheSubset()` function. An iota identifier (`sortedState`) is used to skip the sorting step in the function. + +Finally, part 4. is achieved with `memIterator`, which implements an iterator over the `sortedCache` items. + +As of [PR#12885](https://github.com/cosmos/cosmos-sdk/pull/12885), an optimization to the binary search case mitigates the overhead of sorting the entirety of the key set in `unsortedCache`. To avoid wasting the compute spent sorting, we should ensure that a reasonable amount of values are removed from `unsortedCache`. If the length of the range for iteration is less than `minSortedCache`, we widen the range of values for removal from `unsortedCache` to be up to `minSortedCache` in length. This amortizes the cost of processing elements across multiple calls. \ No newline at end of file From 3122e7dc46837c1495ae551732b2d05bc3c17334 Mon Sep 17 00:00:00 2001 From: Daniel Gushchyan Date: Tue, 22 Nov 2022 16:11:56 -0800 Subject: [PATCH 2/5] added suggested change, some minor edits --- store/cachekv/README.md | 49 +++++++++++++++++++++++------------------ 1 file changed, 27 insertions(+), 22 deletions(-) diff --git a/store/cachekv/README.md b/store/cachekv/README.md index 2dcceb086463..f3c2ec3a34f6 100644 --- a/store/cachekv/README.md +++ b/store/cachekv/README.md @@ -1,11 +1,20 @@ # CacheKVStore specification -A `CacheKVStore` is cache wrapper for a `KVStore`. It extends the operations of the `KVStore` to work with a cache, allowing for reduced I/O operations and more efficient disposing of changes (e.g. after processing a failed transaction). +A `CacheKVStore` is cache wrapper for a `KVStore`. It extends the operations of the `KVStore` to work with a cache, allowing for reduced I/O operations and more efficient disposing of changes (e.g. after processing a failed transaction). + +The core goals the CacheKVStore seeks to solve are: +* Buffer all writes to the parent store, so they can be dropped if they needs to be reverted +* Allow iteration over contiguous spans of keys +* Act as a cache, so we don't repeat I/O to disk for reads we've already done + * Note: We actually fail to achieve this for iteration right now + * Note: Need to consider this getting too large and dropping some cached reads +* Make subsequent reads, account for prior buffered writes +* Write all changes to the parent store + +We should revisit these goals with time (for instance its unclear that all disk writes need to be buffered to the end of the block), but this is the current status. ## Types and Structs ---- - ```go type Store struct { mtx sync.Mutex @@ -17,13 +26,13 @@ type Store struct { } ``` -The Store struct wraps the underlying `KVStore` (referred to as `parent`) with additional data structures for implementing the cache. Mutex is needed as IAVL trees (the `KVStore` in application) are not safe for concurrent use. +The Store struct wraps the underlying `KVStore` (referred to as `parent`) with additional data structures for implementing the cache. Mutex is used as IAVL trees (the `KVStore` in application) are not safe for concurrent use. ### `cache` The main mapping of key-value pairs stored in cache. This map contains both keys that are cached from read operations as well as ‘dirty’ keys which map to a value that is different than what is in the underlying `KVStore`. -Values that are mapped to in the `cache` map are wrapped in a `cValue` struct, which contains the value and a boolean flag (`dirty`) representing whether the value differs from the `parent`. +Values that are mapped to in `cache` are wrapped in a `cValue` struct, which contains the value and a boolean flag (`dirty`) representing whether the value differs from what's in `parent`. ```go type cValue struct { @@ -34,11 +43,11 @@ type cValue struct { ### `deleted` -Key-value pairs that are to be deleted from the `parent`are stored in the `deleted` map. Keys are mapped to an empty struct to implement a set. +Key-value pairs that are to be deleted from the `parent` are stored in the `deleted` map. Keys are mapped to an empty struct to implement a set. ### `unsortedCache` -Similar to`deleted`, this is a set of keys that are dirty and will need to be updated in the `KVStore` upon a write. Keys are mapped to an empty struct to implement a set. +Similar to `deleted`, this is a set of keys that are dirty and will need to be updated in the `KVStore` upon a write. Keys are mapped to an empty struct to implement a set. ### `sortedCache` @@ -46,9 +55,7 @@ A database that will be populated by the keys in `unsortedCache` during iteratio ## CRUD Operations and Writing ---- - -The `Set`, `Get`, and `Delete` functions all reference `setCacheValue()` , which is the only entrypoint to mutating the `cache` map. +The `Set`, `Get`, and `Delete` functions all reference `setCacheValue()` , which is the only entrypoint to mutating `cache`. `setCacheValue()` is defined as follows: @@ -70,11 +77,11 @@ func (store *Store) setCacheValue(key, value []byte, deleted bool, dirty bool) { } ``` -`setCacheValue()` inserts a key-value pair into the `cache` map. Two boolean parameters, `deleted` and `dirty`, are passed in to flag whether the inserted key should also be inserted into the `deleted` and `dirty` sets. +`setCacheValue()` inserts a key-value pair into `cache`. Two boolean parameters, `deleted` and `dirty`, are passed in to flag whether the inserted key should also be inserted into the `deleted` and `dirty` sets. ### `Get` -`Get` first attempts to return the value from the `cache` map. If the key does not exist in `cache`, gets the value from `store.parent` instead and calls `setCacheValue()` with `deleted=false` and `dirty=false`. +`Get` first attempts to return the value from the `cache` map. If the key does not exist in `cache`, `parent` is called instead. This value from parent is passed into cache with `deleted=false` and `dirty=false`. ### `Set` @@ -82,27 +89,25 @@ Calls `setCacheValue()` with `deleted=false` and `dirty=true`. ### `Delete` -A value being deleted from the `KVStore` is represented with a `nil` value in the `cache` map, and an insertion of the key into the `deleted` map. +A value being deleted from the `KVStore` is represented with a `nil` value in `cache`, and an insertion of the key into the `deleted` set. -Calls `setCacheValue` with `deleted=true` and `dirty=true`. +Calls `setCacheValue()` with `deleted=true` and `dirty=true`. ### `Write` Values in the `cache` are written to `parent` in ascending order of their keys. -A slice of all dirty keys in the `cache` is made, then sorted in increasing order. These keys are then iterated over to update `parent`. +A slice of all dirty keys in the `cache` is made, then sorted in increasing order. These keys are iterated over to update `parent`. -If a key is marked for deletion (checked with `isDeleted()`), then the `parent.Delete` is called. Otherwise, `parent.Set` is called to update the underlying `KVStore` with the value in cache. +If a key is marked for deletion (checked with `isDeleted()`), then the `parent.Delete()` is called. Otherwise, `parent.Set()` is called to update the underlying `KVStore` with the value in cache. ## Iteration ---- - -Efficient iteration over keys in the `KVStore` is important for efficiently generating Merkle range proofs. +Efficient iteration over keys in the `KVStore` is important for generating Merkle range proofs. Iteration over the `CacheKVStore` requires producing all key-value pairs from the underlying `KVStore` while taking into account updated values from the cache. -To iterate over the `CacheKVStore`, `iterate()` must iterate over a merged set of keys from the parent `KVStore` and cache. +In the current implementation, there is no guarantee that all values in the underlying `KVStore` have been cached. As a result, iteration is achieved by iterating through both `parent` and the cache (failing to actually benefit from caching). -`[cacheMergeIterator](https://github.com/cosmos/cosmos-sdk/blob/d8391cb6796d770b02448bee70b865d824e43449/store/cachekv/mergeiterator.go)` implements functions to provide a single iterator with an input of iterators over`parent` and `cache`. This iterator iterates over keys from both iterators in a shared lexicological order, and overrides the value provided by the parent iterator if the same key is dirty or deleted in the cache. +[cacheMergeIterator](https://github.com/cosmos/cosmos-sdk/blob/d8391cb6796d770b02448bee70b865d824e43449/store/cachekv/mergeiterator.go) implements functions to provide a single iterator with an input of iterators over `parent` and the cache. This iterator iterates over keys from both iterators in a shared lexicological order, and overrides the value provided by the parent iterator if the same key is dirty or deleted in the cache. ### Implementation Overview @@ -143,4 +148,4 @@ In the case that the size of `unsortedCache` is larger than `minSortSize` , a li Finally, part 4. is achieved with `memIterator`, which implements an iterator over the `sortedCache` items. -As of [PR#12885](https://github.com/cosmos/cosmos-sdk/pull/12885), an optimization to the binary search case mitigates the overhead of sorting the entirety of the key set in `unsortedCache`. To avoid wasting the compute spent sorting, we should ensure that a reasonable amount of values are removed from `unsortedCache`. If the length of the range for iteration is less than `minSortedCache`, we widen the range of values for removal from `unsortedCache` to be up to `minSortedCache` in length. This amortizes the cost of processing elements across multiple calls. \ No newline at end of file +As of [PR #12885](https://github.com/cosmos/cosmos-sdk/pull/12885), an optimization to the binary search case mitigates the overhead of sorting the entirety of the key set in `unsortedCache`. To avoid wasting the compute spent sorting, we should ensure that a reasonable amount of values are removed from `unsortedCache`. If the length of the range for iteration is less than `minSortedCache`, we widen the range of values for removal from `unsortedCache` to be up to `minSortedCache` in length. This amortizes the cost of processing elements across multiple calls. \ No newline at end of file From 6b1ab7e424730314db85c252698a3320272a314e Mon Sep 17 00:00:00 2001 From: Daniel Gushchyan Date: Tue, 22 Nov 2022 16:34:36 -0800 Subject: [PATCH 3/5] formatting tweaks --- store/cachekv/README.md | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/store/cachekv/README.md b/store/cachekv/README.md index f3c2ec3a34f6..033352a750aa 100644 --- a/store/cachekv/README.md +++ b/store/cachekv/README.md @@ -26,7 +26,7 @@ type Store struct { } ``` -The Store struct wraps the underlying `KVStore` (referred to as `parent`) with additional data structures for implementing the cache. Mutex is used as IAVL trees (the `KVStore` in application) are not safe for concurrent use. +The Store struct wraps the underlying `KVStore` (`parent`) with additional data structures for implementing the cache. Mutex is used as IAVL trees (the `KVStore` in application) are not safe for concurrent use. ### `cache` @@ -43,7 +43,7 @@ type cValue struct { ### `deleted` -Key-value pairs that are to be deleted from the `parent` are stored in the `deleted` map. Keys are mapped to an empty struct to implement a set. +Key-value pairs that are to be deleted from `parent` are stored in the `deleted` map. Keys are mapped to an empty struct to implement a set. ### `unsortedCache` @@ -81,7 +81,7 @@ func (store *Store) setCacheValue(key, value []byte, deleted bool, dirty bool) { ### `Get` -`Get` first attempts to return the value from the `cache` map. If the key does not exist in `cache`, `parent` is called instead. This value from parent is passed into cache with `deleted=false` and `dirty=false`. +`Get` first attempts to return the value from `cache`. If the key does not exist in `cache`, `parent.Get()` is called instead. This value from the parent is passed into `setCacheValue()` with `deleted=false` and `dirty=false`. ### `Set` @@ -95,34 +95,34 @@ Calls `setCacheValue()` with `deleted=true` and `dirty=true`. ### `Write` -Values in the `cache` are written to `parent` in ascending order of their keys. +Values in the cache are written to `parent` in ascending order of their keys. -A slice of all dirty keys in the `cache` is made, then sorted in increasing order. These keys are iterated over to update `parent`. +A slice of all dirty keys in `cache` is made, then sorted in increasing order. These keys are iterated over to update `parent`. -If a key is marked for deletion (checked with `isDeleted()`), then the `parent.Delete()` is called. Otherwise, `parent.Set()` is called to update the underlying `KVStore` with the value in cache. +If a key is marked for deletion (checked with `isDeleted()`), then `parent.Delete()` is called. Otherwise, `parent.Set()` is called to update the underlying `KVStore` with the value in cache. ## Iteration -Efficient iteration over keys in the `KVStore` is important for generating Merkle range proofs. Iteration over the `CacheKVStore` requires producing all key-value pairs from the underlying `KVStore` while taking into account updated values from the cache. +Efficient iteration over keys in `KVStore` is important for generating Merkle range proofs. Iteration over `CacheKVStore` requires producing all key-value pairs from the underlying `KVStore` while taking into account updated values from the cache. -In the current implementation, there is no guarantee that all values in the underlying `KVStore` have been cached. As a result, iteration is achieved by iterating through both `parent` and the cache (failing to actually benefit from caching). +In the current implementation, there is no guarantee that all values in `parent` have been cached. As a result, iteration is achieved by iterating through both `parent` and the cache (failing to actually benefit from caching). [cacheMergeIterator](https://github.com/cosmos/cosmos-sdk/blob/d8391cb6796d770b02448bee70b865d824e43449/store/cachekv/mergeiterator.go) implements functions to provide a single iterator with an input of iterators over `parent` and the cache. This iterator iterates over keys from both iterators in a shared lexicological order, and overrides the value provided by the parent iterator if the same key is dirty or deleted in the cache. ### Implementation Overview -The iterators over the `parent` and `cache` are generated and passed into `cacheMergeIterator`, which is the iterator that is returned. Implementation of the `parent` iterator is up to the underlying `KVStore` . The rest of the implementation details here will cover the generation of the `cache` iterator. +Iterators over `parent` and the cache are generated and passed into `cacheMergeIterator`, which returns a single iterator. Implementation of the `parent` iterator is up to the underlying `KVStore`. The rest of the implementation details here will cover the generation of the cache iterator. Generating the cache iterator can be decomposed into four parts: 1. Finding all keys that exist in the range we are iterating over 2. Sorting this list of keys -3. Inserting these keys into the `sortedCache` and removing them from the `unsortedCache` +3. Inserting these keys into `sortedCache` and removing them from `unsortedCache` 4. Returning an iterator over `sortedCache` with the desired range Currently, the implementation for the first two parts is split into two cases, depending on the size of the unsorted cache. The two cases are as follows. -If the size of the `unsortedCache` is less than `minSortSize` (currently 1024), a linear time approach is taken to search over keys. +If the size of `unsortedCache` is less than `minSortSize` (currently 1024), a linear time approach is taken to search over keys. ```go n := len(store.unsortedCache) @@ -140,12 +140,12 @@ if n < minSortSize { } ``` -Here, we iterate through all the keys in the `unsortedCache`, collecting them in a slice called `unsorted`. +Here, we iterate through all the keys in `unsortedCache`, collecting them in a slice called `unsorted`. -At this point, part 3. is achieved in`clearUnsortedCacheSubset()`. This function iterates through `unsorted`, removing each key from `unsortedCache` . Afterwards, the`unsorted` slice is sorted. Lastly, it iterates through the key-pairs in the now sorted slice, setting any key meant to be deleted to map to an arbitrary value (`[]byte{}`). +At this point, part 3. is achieved in `clearUnsortedCacheSubset()`. This function iterates through `unsorted`, removing each key from `unsortedCache`. Afterwards, `unsorted` is sorted. Lastly, it iterates through the key-pairs in the now sorted slice, setting any key meant to be deleted to map to an arbitrary value (`[]byte{}`). -In the case that the size of `unsortedCache` is larger than `minSortSize` , a linear time approach to finding keys within the desired range is too slow to use. Instead, a slice of all keys in `unsortedCache` is sorted, and binary search is used to find the beginning and ending indices of the desired range. This produces an already-sorted slice that is passed into the same `clearUnsortedCacheSubset()` function. An iota identifier (`sortedState`) is used to skip the sorting step in the function. +In the case that the size of `unsortedCache` is larger than `minSortSize`, a linear time approach to finding keys within the desired range is too slow to use. Instead, a slice of all keys in `unsortedCache` is sorted, and binary search is used to find the beginning and ending indices of the desired range. This produces an already-sorted slice that is passed into the same `clearUnsortedCacheSubset()` function. An iota identifier (`sortedState`) is used to skip the sorting step in the function. -Finally, part 4. is achieved with `memIterator`, which implements an iterator over the `sortedCache` items. +Finally, part 4. is achieved with `memIterator`, which implements an iterator over the items in `sortedCache`. As of [PR #12885](https://github.com/cosmos/cosmos-sdk/pull/12885), an optimization to the binary search case mitigates the overhead of sorting the entirety of the key set in `unsortedCache`. To avoid wasting the compute spent sorting, we should ensure that a reasonable amount of values are removed from `unsortedCache`. If the length of the range for iteration is less than `minSortedCache`, we widen the range of values for removal from `unsortedCache` to be up to `minSortedCache` in length. This amortizes the cost of processing elements across multiple calls. \ No newline at end of file From 6bcacbc3bd258a1662f50db73faba4f8ce17fbad Mon Sep 17 00:00:00 2001 From: Daniel Gushchyan Date: Wed, 30 Nov 2022 19:06:45 -0800 Subject: [PATCH 4/5] Added feedback from @thpani --- store/cachekv/README.md | 68 +++++++++++++++++------------------------ 1 file changed, 28 insertions(+), 40 deletions(-) diff --git a/store/cachekv/README.md b/store/cachekv/README.md index 033352a750aa..33eafa5d437e 100644 --- a/store/cachekv/README.md +++ b/store/cachekv/README.md @@ -1,17 +1,17 @@ # CacheKVStore specification -A `CacheKVStore` is cache wrapper for a `KVStore`. It extends the operations of the `KVStore` to work with a cache, allowing for reduced I/O operations and more efficient disposing of changes (e.g. after processing a failed transaction). - +A `CacheKVStore` is cache wrapper for a `KVStore`. It extends the operations of the `KVStore` to work with a write-back cache, allowing for reduced I/O operations and more efficient disposing of changes (e.g. after processing a failed transaction). + The core goals the CacheKVStore seeks to solve are: -* Buffer all writes to the parent store, so they can be dropped if they needs to be reverted +Buffer all writes to the parent store, so they can be dropped if they need to be reverted * Allow iteration over contiguous spans of keys -* Act as a cache, so we don't repeat I/O to disk for reads we've already done +* Act as a cache, improving access time for reads that have already been done (by replacing tree access with hashtable access, avoiding disk I/O) * Note: We actually fail to achieve this for iteration right now * Note: Need to consider this getting too large and dropping some cached reads -* Make subsequent reads, account for prior buffered writes -* Write all changes to the parent store +* Make subsequent reads account for prior buffered writes +* Write all buffered changes to the parent store -We should revisit these goals with time (for instance its unclear that all disk writes need to be buffered to the end of the block), but this is the current status. +We should revisit these goals with time (for instance it's unclear that all disk writes need to be buffered to the end of the block), but this is the current status. ## Types and Structs @@ -30,9 +30,9 @@ The Store struct wraps the underlying `KVStore` (`parent`) with additional data ### `cache` -The main mapping of key-value pairs stored in cache. This map contains both keys that are cached from read operations as well as ‘dirty’ keys which map to a value that is different than what is in the underlying `KVStore`. +The main mapping of key-value pairs stored in cache. This map contains both keys that are cached from read operations as well as ‘dirty’ keys which map to a value that is potentially different than what is in the underlying `KVStore`. -Values that are mapped to in `cache` are wrapped in a `cValue` struct, which contains the value and a boolean flag (`dirty`) representing whether the value differs from what's in `parent`. +Values that are mapped to in `cache` are wrapped in a `cValue` struct, which contains the value and a boolean flag (`dirty`) representing whether the value has been written since the last write-back to `parent`. ```go type cValue struct { @@ -47,55 +47,41 @@ Key-value pairs that are to be deleted from `parent` are stored in the `deleted` ### `unsortedCache` -Similar to `deleted`, this is a set of keys that are dirty and will need to be updated in the `KVStore` upon a write. Keys are mapped to an empty struct to implement a set. +Similar to `deleted`, this is a set of keys that are dirty and will need to be updated in the parent `KVStore` upon a write. Keys are mapped to an empty struct to implement a set. ### `sortedCache` -A database that will be populated by the keys in `unsortedCache` during iteration over the cache. Keys are always inserted in sorted order. +A database that will be populated by the keys in `unsortedCache` during iteration over the cache. The keys are always held in sorted order. ## CRUD Operations and Writing -The `Set`, `Get`, and `Delete` functions all reference `setCacheValue()` , which is the only entrypoint to mutating `cache`. +The `Set`, `Get`, and `Delete` functions all call `setCacheValue()`, which is the only entry point to mutating `cache` (besides `Write()`, which clears it). -`setCacheValue()` is defined as follows: - -```go -func (store *Store) setCacheValue(key, value []byte, deleted bool, dirty bool) { - keyStr := conv.UnsafeBytesToStr(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{}{} - } -} -``` - -`setCacheValue()` inserts a key-value pair into `cache`. Two boolean parameters, `deleted` and `dirty`, are passed in to flag whether the inserted key should also be inserted into the `deleted` and `dirty` sets. +`setCacheValue()` inserts a key-value pair into `cache`. Two boolean parameters, `deleted` and `dirty`, are passed in to flag whether the inserted key should also be inserted into the `deleted` and `dirty` sets. Keys will be removed from the `deleted` set if they are written to after being deleted. ### `Get` `Get` first attempts to return the value from `cache`. If the key does not exist in `cache`, `parent.Get()` is called instead. This value from the parent is passed into `setCacheValue()` with `deleted=false` and `dirty=false`. +### `Has` + +`Has` returns true if `Get` returns a non-nil value. As a result of calling `Get`, it may mutate the cache by caching the read. + ### `Set` +New values are written by setting or updating the value of a key in `cache`. `Set` does not write to `parent`. + Calls `setCacheValue()` with `deleted=false` and `dirty=true`. ### `Delete` -A value being deleted from the `KVStore` is represented with a `nil` value in `cache`, and an insertion of the key into the `deleted` set. +A value being deleted from the `KVStore` is represented with a `nil` value in `cache`, and an insertion of the key into the `deleted` set. `Delete` does not write to `parent`. Calls `setCacheValue()` with `deleted=true` and `dirty=true`. ### `Write` -Values in the cache are written to `parent` in ascending order of their keys. +Key-value pairs in the cache are written to `parent` in ascending order of their keys. A slice of all dirty keys in `cache` is made, then sorted in increasing order. These keys are iterated over to update `parent`. @@ -105,13 +91,15 @@ If a key is marked for deletion (checked with `isDeleted()`), then `parent.Delet Efficient iteration over keys in `KVStore` is important for generating Merkle range proofs. Iteration over `CacheKVStore` requires producing all key-value pairs from the underlying `KVStore` while taking into account updated values from the cache. -In the current implementation, there is no guarantee that all values in `parent` have been cached. As a result, iteration is achieved by iterating through both `parent` and the cache (failing to actually benefit from caching). +In the current implementation, there is no guarantee that all values in `parent` have been cached. As a result, iteration is achieved by interleaved iteration through both `parent` and the cache (failing to actually benefit from caching). -[cacheMergeIterator](https://github.com/cosmos/cosmos-sdk/blob/d8391cb6796d770b02448bee70b865d824e43449/store/cachekv/mergeiterator.go) implements functions to provide a single iterator with an input of iterators over `parent` and the cache. This iterator iterates over keys from both iterators in a shared lexicological order, and overrides the value provided by the parent iterator if the same key is dirty or deleted in the cache. +[cacheMergeIterator](https://github.com/cosmos/cosmos-sdk/blob/d8391cb6796d770b02448bee70b865d824e43449/store/cachekv/mergeiterator.go) implements functions to provide a single iterator with an input of iterators over `parent` and the cache. This iterator iterates over keys from both iterators in a shared lexicographic order, and overrides the value provided by the parent iterator if the same key is dirty or deleted in the cache. ### Implementation Overview -Iterators over `parent` and the cache are generated and passed into `cacheMergeIterator`, which returns a single iterator. Implementation of the `parent` iterator is up to the underlying `KVStore`. The rest of the implementation details here will cover the generation of the cache iterator. +Iterators over `parent` and the cache are generated and passed into `cacheMergeIterator`, which returns a single, interleaved iterator. Implementation of the `parent` iterator is up to the underlying `KVStore`. The remainder of this section covers the generation of the cache iterator. + +Recall that `unsortedCache` is an unordered set of dirty cache keys. Our goal is to construct an ordered iterator over cache keys that fall within the `start` and `end` bounds requested. Generating the cache iterator can be decomposed into four parts: @@ -140,9 +128,9 @@ if n < minSortSize { } ``` -Here, we iterate through all the keys in `unsortedCache`, collecting them in a slice called `unsorted`. +Here, we iterate through all the keys in `unsortedCache` (i.e., the dirty cache keys), collecting those within the requested range in an unsorted slice called `unsorted`. -At this point, part 3. is achieved in `clearUnsortedCacheSubset()`. This function iterates through `unsorted`, removing each key from `unsortedCache`. Afterwards, `unsorted` is sorted. Lastly, it iterates through the key-pairs in the now sorted slice, setting any key meant to be deleted to map to an arbitrary value (`[]byte{}`). +At this point, part 3. is achieved in `clearUnsortedCacheSubset()`. This function iterates through `unsorted`, removing each key from `unsortedCache`. Afterwards, `unsorted` is sorted. Lastly, it iterates through the now sorted slice, inserting key-value pairs into `sortedCache`. Any key marked for deletion is mapped to an arbitrary value (`[]byte{}`). In the case that the size of `unsortedCache` is larger than `minSortSize`, a linear time approach to finding keys within the desired range is too slow to use. Instead, a slice of all keys in `unsortedCache` is sorted, and binary search is used to find the beginning and ending indices of the desired range. This produces an already-sorted slice that is passed into the same `clearUnsortedCacheSubset()` function. An iota identifier (`sortedState`) is used to skip the sorting step in the function. From fdaade6bf1062bd1997e1723f70abf15f0933626 Mon Sep 17 00:00:00 2001 From: Marko Date: Wed, 28 Dec 2022 11:10:32 +0100 Subject: [PATCH 5/5] Update store/cachekv/README.md Co-authored-by: Aleksandr Bezobchuk --- store/cachekv/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/store/cachekv/README.md b/store/cachekv/README.md index 33eafa5d437e..66f0916dea4b 100644 --- a/store/cachekv/README.md +++ b/store/cachekv/README.md @@ -3,7 +3,8 @@ A `CacheKVStore` is cache wrapper for a `KVStore`. It extends the operations of the `KVStore` to work with a write-back cache, allowing for reduced I/O operations and more efficient disposing of changes (e.g. after processing a failed transaction). The core goals the CacheKVStore seeks to solve are: -Buffer all writes to the parent store, so they can be dropped if they need to be reverted + +* Buffer all writes to the parent store, so they can be dropped if they need to be reverted * Allow iteration over contiguous spans of keys * Act as a cache, improving access time for reads that have already been done (by replacing tree access with hashtable access, avoiding disk I/O) * Note: We actually fail to achieve this for iteration right now