github.com/cosmos/cosmos-sdk@v0.50.10/docs/spec/store/interblock-cache.md (about) 1 # Inter-block Cache 2 3 * [Inter-block Cache](#inter-block-cache) 4 * [Synopsis](#synopsis) 5 * [Overview and basic concepts](#overview-and-basic-concepts) 6 * [Motivation](#motivation) 7 * [Definitions](#definitions) 8 * [System model and properties](#system-model-and-properties) 9 * [Assumptions](#assumptions) 10 * [Properties](#properties) 11 * [Thread safety](#thread-safety) 12 * [Crash recovery](#crash-recovery) 13 * [Iteration](#iteration) 14 * [Technical specification](#technical-specification) 15 * [General design](#general-design) 16 * [API](#api) 17 * [CommitKVCacheManager](#commitkvcachemanager) 18 * [CommitKVStoreCache](#commitkvstorecache) 19 * [Implementation details](#implementation-details) 20 * [History](#history) 21 * [Copyright](#copyright) 22 23 ## Synopsis 24 25 The inter-block cache is an in-memory cache storing (in-most-cases) immutable state that modules need to read in between blocks. When enabled, all sub-stores of a multi store, e.g., `rootmulti`, are wrapped. 26 27 ## Overview and basic concepts 28 29 ### Motivation 30 31 The goal of the inter-block cache is to allow SDK modules to have fast access to data that it is typically queried during the execution of every block. This is data that do not change often, e.g. module parameters. The inter-block cache wraps each `CommitKVStore` of a multi store such as `rootmulti` with a fixed size, write-through cache. Caches are not cleared after a block is committed, as opposed to other caching layers such as `cachekv`. 32 33 ### Definitions 34 35 * `Store key` uniquely identifies a store. 36 * `KVCache` is a `CommitKVStore` wrapped with a cache. 37 * `Cache manager` is a key component of the inter-block cache responsible for maintaining a map from `store keys` to `KVCaches`. 38 39 ## System model and properties 40 41 ### Assumptions 42 43 This specification assumes that there exists a cache implementation accessible to the inter-block cache feature. 44 45 > The implementation uses adaptive replacement cache (ARC), an enhancement over the standard last-recently-used (LRU) cache in that tracks both frequency and recency of use. 46 47 The inter-block cache requires that the cache implementation to provide methods to create a cache, add a key/value pair, remove a key/value pair and retrieve the value associated to a key. In this specification, we assume that a `Cache` feature offers this functionality through the following methods: 48 49 * `NewCache(size int)` creates a new cache with `size` capacity and returns it. 50 * `Get(key string)` attempts to retrieve a key/value pair from `Cache.` It returns `(value []byte, success bool)`. If `Cache` contains the key, it `value` contains the associated value and `success=true`. Otherwise, `success=false` and `value` should be ignored. 51 * `Add(key string, value []byte)` inserts a key/value pair into the `Cache`. 52 * `Remove(key string)` removes the key/value pair identified by `key` from `Cache`. 53 54 The specification also assumes that `CommitKVStore` offers the following API: 55 56 * `Get(key string)` attempts to retrieve a key/value pair from `CommitKVStore`. 57 * `Set(key, string, value []byte)` inserts a key/value pair into the `CommitKVStore`. 58 * `Delete(key string)` removes the key/value pair identified by `key` from `CommitKVStore`. 59 60 > Ideally, both `Cache` and `CommitKVStore` should be specified in a different document and referenced here. 61 62 ### Properties 63 64 #### Thread safety 65 66 Accessing the `cache manager` or a `KVCache` is not thread-safe: no method is guarded with a lock. 67 Note that this is true even if the cache implementation is thread-safe. 68 69 > For instance, assume that two `Set` operations are executed concurrently on the same key, each writing a different value. After both are executed, the cache and the underlying store may be inconsistent, each storing a different value under the same key. 70 71 #### Crash recovery 72 73 The inter-block cache transparently delegates `Commit()` to its aggregate `CommitKVStore`. If the 74 aggregate `CommitKVStore` supports atomic writes and use them to guarantee that the store is always in a consistent state in disk, the inter-block cache can be transparently moved to a consistent state when a failure occurs. 75 76 > Note that this is the case for `IAVLStore`, the preferred `CommitKVStore`. On commit, it calls `SaveVersion()` on the underlying `MutableTree`. `SaveVersion` writes to disk are atomic via batching. This means that only consistent versions of the store (the tree) are written to the disk. Thus, in case of a failure during a `SaveVersion` call, on recovery from disk, the version of the store will be consistent. 77 78 #### Iteration 79 80 Iteration over each wrapped store is supported via the embedded `CommitKVStore` interface. 81 82 ## Technical specification 83 84 ### General design 85 86 The inter-block cache feature is composed by two components: `CommitKVCacheManager` and `CommitKVCache`. 87 88 `CommitKVCacheManager` implements the cache manager. It maintains a mapping from a store key to a `KVStore`. 89 90 ```go 91 type CommitKVStoreCacheManager interface{ 92 cacheSize uint 93 caches map[string]CommitKVStore 94 } 95 ``` 96 97 `CommitKVStoreCache` implements a `KVStore`: a write-through cache that wraps a `CommitKVStore`. This means that deletes and writes always happen to both the cache and the underlying `CommitKVStore`. Reads on the other hand first hit the internal cache. During a cache miss, the read is delegated to the underlying `CommitKVStore` and cached. 98 99 ```go 100 type CommitKVStoreCache interface{ 101 store CommitKVStore 102 cache Cache 103 } 104 ``` 105 106 To enable inter-block cache on `rootmulti`, one needs to instantiate a `CommitKVCacheManager` and set it by calling `SetInterBlockCache()` before calling one of `LoadLatestVersion()`, `LoadLatestVersionAndUpgrade(...)`, `LoadVersionAndUpgrade(...)` and `LoadVersion(version)`. 107 108 ### API 109 110 #### CommitKVCacheManager 111 112 The method `NewCommitKVStoreCacheManager` creates a new cache manager and returns it. 113 114 | Name | Type | Description | 115 | ------------- | ---------|------- | 116 | size | integer | Determines the capacity of each of the KVCache maintained by the manager | 117 118 ```go 119 func NewCommitKVStoreCacheManager(size uint) CommitKVStoreCacheManager { 120 manager = CommitKVStoreCacheManager{size, make(map[string]CommitKVStore)} 121 return manager 122 } 123 ``` 124 125 `GetStoreCache` returns a cache from the CommitStoreCacheManager for a given store key. If no cache exists for the store key, then one is created and set. 126 127 | Name | Type | Description | 128 | ------------- | ---------|------- | 129 | manager | `CommitKVStoreCacheManager` | The cache manager | 130 | storeKey | string | The store key of the store being retrieved | 131 | store | `CommitKVStore` | The store that it is cached in case the manager does not have any in its map of caches | 132 133 ```go 134 func GetStoreCache( 135 manager CommitKVStoreCacheManager, 136 storeKey string, 137 store CommitKVStore) CommitKVStore { 138 139 if manager.caches.has(storeKey) { 140 return manager.caches.get(storeKey) 141 } else { 142 cache = CommitKVStoreCacheManager{store, manager.cacheSize} 143 manager.set(storeKey, cache) 144 return cache 145 } 146 } 147 ``` 148 149 `Unwrap` returns the underlying CommitKVStore for a given store key. 150 151 | Name | Type | Description | 152 | ------------- | ---------|------- | 153 | manager | `CommitKVStoreCacheManager` | The cache manager | 154 | storeKey | string | The store key of the store being unwrapped | 155 156 ```go 157 func Unwrap( 158 manager CommitKVStoreCacheManager, 159 storeKey string) CommitKVStore { 160 161 if manager.caches.has(storeKey) { 162 cache = manager.caches.get(storeKey) 163 return cache.store 164 } else { 165 return nil 166 } 167 } 168 ``` 169 170 `Reset` resets the manager's map of caches. 171 172 | Name | Type | Description | 173 | ------------- | ---------|------- | 174 | manager | `CommitKVStoreCacheManager` | The cache manager | 175 176 ```go 177 function Reset(manager CommitKVStoreCacheManager) { 178 179 for (let storeKey of manager.caches.keys()) { 180 manager.caches.delete(storeKey) 181 } 182 } 183 ``` 184 185 #### CommitKVStoreCache 186 187 `NewCommitKVStoreCache` creates a new `CommitKVStoreCache` and returns it. 188 189 | Name | Type | Description | 190 | ------------- | ---------|------- | 191 | store | CommitKVStore | The store to be cached | 192 | size | string | Determines the capacity of the cache being created | 193 194 ```go 195 func NewCommitKVStoreCache( 196 store CommitKVStore, 197 size uint) CommitKVStoreCache { 198 KVCache = CommitKVStoreCache{store, NewCache(size)} 199 return KVCache 200 } 201 ``` 202 203 `Get` retrieves a value by key. It first looks in the cache. If the key is not in the cache, the query is delegated to the underlying `CommitKVStore`. In the latter case, the key/value pair is cached. The method returns the value. 204 205 | Name | Type | Description | 206 | ------------- | ---------|------- | 207 | KVCache | `CommitKVStoreCache` | The `CommitKVStoreCache` from which the key/value pair is retrieved | 208 | key | string | Key of the key/value pair being retrieved | 209 210 ```go 211 func Get( 212 KVCache CommitKVStoreCache, 213 key string) []byte { 214 valueCache, success := KVCache.cache.Get(key) 215 if success { 216 // cache hit 217 return valueCache 218 } else { 219 // cache miss 220 valueStore = KVCache.store.Get(key) 221 KVCache.cache.Add(key, valueStore) 222 return valueStore 223 } 224 } 225 ``` 226 227 `Set` inserts a key/value pair into both the write-through cache and the underlying `CommitKVStore`. 228 229 | Name | Type | Description | 230 | ------------- | ---------|------- | 231 | KVCache | `CommitKVStoreCache` | The `CommitKVStoreCache` to which the key/value pair is inserted | 232 | key | string | Key of the key/value pair being inserted | 233 | value | []byte | Value of the key/value pair being inserted | 234 235 ```go 236 func Set( 237 KVCache CommitKVStoreCache, 238 key string, 239 value []byte) { 240 241 KVCache.cache.Add(key, value) 242 KVCache.store.Set(key, value) 243 } 244 ``` 245 246 `Delete` removes a key/value pair from both the write-through cache and the underlying `CommitKVStore`. 247 248 | Name | Type | Description | 249 | ------------- | ---------|------- | 250 | KVCache | `CommitKVStoreCache` | The `CommitKVStoreCache` from which the key/value pair is deleted | 251 | key | string | Key of the key/value pair being deleted | 252 253 ```go 254 func Delete( 255 KVCache CommitKVStoreCache, 256 key string) { 257 258 KVCache.cache.Remove(key) 259 KVCache.store.Delete(key) 260 } 261 ``` 262 263 `CacheWrap` wraps a `CommitKVStoreCache` with another caching layer (`CacheKV`). 264 265 > It is unclear whether there is a use case for `CacheWrap`. 266 267 | Name | Type | Description | 268 | ------------- | ---------|------- | 269 | KVCache | `CommitKVStoreCache` | The `CommitKVStoreCache` being wrapped | 270 271 ```go 272 func CacheWrap( 273 KVCache CommitKVStoreCache) { 274 275 return CacheKV.NewStore(KVCache) 276 } 277 ``` 278 279 ### Implementation details 280 281 The inter-block cache implementation uses a fixed-sized adaptive replacement cache (ARC) as cache. [The ARC implementation](https://github.com/hashicorp/golang-lru/blob/master/arc.go) is thread-safe. ARC is an enhancement over the standard LRU cache in that tracks both frequency and recency of use. This avoids a burst in access to new entries from evicting the frequently used older entries. It adds some additional tracking overhead to a standard LRU cache, computationally it is roughly `2x` the cost, and the extra memory overhead is linear with the size of the cache. The default cache size is `1000`. 282 283 ## History 284 285 Dec 20, 2022 - Initial draft finished and submitted as a PR 286 287 ## Copyright 288 289 All content herein is licensed under [Apache 2.0](https://www.apache.org/licenses/LICENSE-2.0).