github.com/hechain20/hechain@v0.0.0-20220316014945-b544036ba106/core/ledger/kvledger/txmgmt/statedb/statecouchdb/commit_handling.go (about) 1 /* 2 Copyright hechain. All Rights Reserved. 3 4 SPDX-License-Identifier: Apache-2.0 5 */ 6 7 package statecouchdb 8 9 import ( 10 "fmt" 11 "math" 12 "sync" 13 14 "github.com/hechain20/hechain/core/ledger/kvledger/txmgmt/statedb" 15 "github.com/pkg/errors" 16 ) 17 18 type committer struct { 19 db *couchDatabase 20 batchUpdateMap map[string]*batchableDocument 21 namespace string 22 cacheKVs cacheKVs 23 cacheEnabled bool 24 } 25 26 func (c *committer) addToCacheUpdate(kv *keyValue) { 27 if !c.cacheEnabled { 28 return 29 } 30 31 if kv.Value == nil { 32 // nil value denotes a delete operation 33 c.cacheKVs[kv.key] = nil 34 return 35 } 36 37 c.cacheKVs[kv.key] = &CacheValue{ 38 Version: kv.Version.ToBytes(), 39 Value: kv.Value, 40 Metadata: kv.Metadata, 41 AdditionalInfo: []byte(kv.revision), 42 } 43 } 44 45 func (c *committer) updateRevisionInCacheUpdate(key, rev string) { 46 if !c.cacheEnabled { 47 return 48 } 49 cv := c.cacheKVs[key] 50 if cv == nil { 51 // nil value denotes a delete 52 return 53 } 54 cv.AdditionalInfo = []byte(rev) 55 } 56 57 // buildCommitters builds committers per namespace. Each committer transforms the 58 // given batch in the form of underlying db and keep it in memory. 59 func (vdb *VersionedDB) buildCommitters(updates *statedb.UpdateBatch) ([]*committer, error) { 60 namespaces := updates.GetUpdatedNamespaces() 61 62 // for each namespace, we build multiple committers (based on maxBatchSize per namespace) 63 var wg sync.WaitGroup 64 nsCommittersChan := make(chan []*committer, len(namespaces)) 65 defer close(nsCommittersChan) 66 errsChan := make(chan error, len(namespaces)) 67 defer close(errsChan) 68 69 // for each namespace, we build committers in parallel. This is because, 70 // the committer building process requires fetching of missing revisions 71 // that in turn, we want to do in parallel 72 for _, ns := range namespaces { 73 nsUpdates := updates.GetUpdates(ns) 74 wg.Add(1) 75 go func(ns string) { 76 defer wg.Done() 77 committers, err := vdb.buildCommittersForNs(ns, nsUpdates) 78 if err != nil { 79 errsChan <- err 80 return 81 } 82 nsCommittersChan <- committers 83 }(ns) 84 } 85 wg.Wait() 86 87 // collect all committers 88 var allCommitters []*committer 89 select { 90 case err := <-errsChan: 91 return nil, errors.WithStack(err) 92 default: 93 for i := 0; i < len(namespaces); i++ { 94 allCommitters = append(allCommitters, <-nsCommittersChan...) 95 } 96 } 97 98 return allCommitters, nil 99 } 100 101 func (vdb *VersionedDB) buildCommittersForNs(ns string, nsUpdates map[string]*statedb.VersionedValue) ([]*committer, error) { 102 db, err := vdb.getNamespaceDBHandle(ns) 103 if err != nil { 104 return nil, err 105 } 106 // for each namespace, build mutiple committers based on the maxBatchSize 107 maxBatchSize := db.couchInstance.maxBatchUpdateSize() 108 numCommitters := 1 109 if maxBatchSize > 0 { 110 numCommitters = int(math.Ceil(float64(len(nsUpdates)) / float64(maxBatchSize))) 111 } 112 committers := make([]*committer, numCommitters) 113 114 cacheEnabled := vdb.cache.enabled(ns) 115 116 for i := 0; i < numCommitters; i++ { 117 committers[i] = &committer{ 118 db: db, 119 batchUpdateMap: make(map[string]*batchableDocument), 120 namespace: ns, 121 cacheKVs: make(cacheKVs), 122 cacheEnabled: cacheEnabled, 123 } 124 } 125 126 // for each committer, create a couchDoc per key-value pair present in the update batch 127 // which are associated with the committer's namespace. 128 revisions, err := vdb.getRevisions(ns, nsUpdates) 129 if err != nil { 130 return nil, err 131 } 132 133 i := 0 134 for key, vv := range nsUpdates { 135 kv := &keyValue{key: key, revision: revisions[key], VersionedValue: vv} 136 couchDoc, err := keyValToCouchDoc(kv) 137 if err != nil { 138 return nil, err 139 } 140 committers[i].batchUpdateMap[key] = &batchableDocument{CouchDoc: *couchDoc, Deleted: vv.Value == nil} 141 committers[i].addToCacheUpdate(kv) 142 if maxBatchSize > 0 && len(committers[i].batchUpdateMap) == maxBatchSize { 143 i++ 144 } 145 } 146 return committers, nil 147 } 148 149 func (vdb *VersionedDB) executeCommitter(committers []*committer) error { 150 errsChan := make(chan error, len(committers)) 151 defer close(errsChan) 152 var wg sync.WaitGroup 153 wg.Add(len(committers)) 154 155 for _, c := range committers { 156 go func(c *committer) { 157 defer wg.Done() 158 if err := c.commitUpdates(); err != nil { 159 errsChan <- err 160 } 161 }(c) 162 } 163 wg.Wait() 164 165 select { 166 case err := <-errsChan: 167 return errors.WithStack(err) 168 default: 169 return nil 170 } 171 } 172 173 // commitUpdates commits the given updates to couchdb 174 func (c *committer) commitUpdates() error { 175 docs := []*couchDoc{} 176 for _, update := range c.batchUpdateMap { 177 docs = append(docs, &update.CouchDoc) 178 } 179 180 // Do the bulk update into couchdb. Note that this will do retries if the entire bulk update fails or times out 181 responses, err := c.db.batchUpdateDocuments(docs) 182 if err != nil { 183 return err 184 } 185 186 // IF INDIVIDUAL DOCUMENTS IN THE BULK UPDATE DID NOT SUCCEED, TRY THEM INDIVIDUALLY 187 // iterate through the response from CouchDB by document 188 for _, resp := range responses { 189 // If the document returned an error, retry the individual document 190 if resp.Ok { 191 c.updateRevisionInCacheUpdate(resp.ID, resp.Rev) 192 continue 193 } 194 doc := c.batchUpdateMap[resp.ID] 195 196 var err error 197 // Remove the "_rev" from the JSON before saving 198 // this will allow the CouchDB retry logic to retry revisions without encountering 199 // a mismatch between the "If-Match" and the "_rev" tag in the JSON 200 if doc.CouchDoc.jsonValue != nil { 201 err = removeJSONRevision(&doc.CouchDoc.jsonValue) 202 if err != nil { 203 return err 204 } 205 } 206 // Check to see if the document was added to the batch as a delete type document 207 if doc.Deleted { 208 logger.Warningf("CouchDB batch document delete encountered an problem. Retrying delete for document ID:%s", resp.ID) 209 // If this is a deleted document, then retry the delete 210 // If the delete fails due to a document not being found (404 error), 211 // the document has already been deleted and the DeleteDoc will not return an error 212 err = c.db.deleteDoc(resp.ID, "") 213 } else { 214 logger.Warningf("CouchDB batch document update encountered an problem. Reason:%s, Retrying update for document ID:%s", resp.Reason, resp.ID) 215 // Save the individual document to couchdb 216 // Note that this will do retries as needed 217 var revision string 218 revision, err = c.db.saveDoc(resp.ID, "", &doc.CouchDoc) 219 c.updateRevisionInCacheUpdate(resp.ID, revision) 220 } 221 222 // If the single document update or delete returns an error, then throw the error 223 if err != nil { 224 errorString := fmt.Sprintf("error saving document ID: %v. Error: %s, Reason: %s", 225 resp.ID, resp.Error, resp.Reason) 226 227 logger.Errorf(errorString) 228 return errors.WithMessage(err, errorString) 229 } 230 } 231 return nil 232 } 233 234 func (vdb *VersionedDB) getRevisions(ns string, nsUpdates map[string]*statedb.VersionedValue) (map[string]string, error) { 235 revisions := make(map[string]string) 236 nsRevs := vdb.committedDataCache.revs[ns] 237 238 var missingKeys []string 239 var ok bool 240 for key := range nsUpdates { 241 if revisions[key], ok = nsRevs[key]; !ok { 242 missingKeys = append(missingKeys, key) 243 } 244 } 245 246 if len(missingKeys) == 0 { 247 // all revisions were present in the committedDataCache 248 return revisions, nil 249 } 250 251 missingKeys, err := vdb.addMissingRevisionsFromCache(ns, missingKeys, revisions) 252 if err != nil { 253 return nil, err 254 } 255 256 if len(missingKeys) == 0 { 257 // remaining revisions were present in the state cache 258 return revisions, nil 259 } 260 261 // don't update the cache for missing entries as 262 // revisions are going to get changed after the commit 263 if err := vdb.addMissingRevisionsFromDB(ns, missingKeys, revisions); err != nil { 264 return nil, err 265 } 266 return revisions, nil 267 } 268 269 func (vdb *VersionedDB) addMissingRevisionsFromCache(ns string, keys []string, revs map[string]string) ([]string, error) { 270 if !vdb.cache.enabled(ns) { 271 return keys, nil 272 } 273 274 var missingKeys []string 275 for _, k := range keys { 276 cv, err := vdb.cache.getState(vdb.chainName, ns, k) 277 if err != nil { 278 return nil, err 279 } 280 if cv == nil { 281 missingKeys = append(missingKeys, k) 282 continue 283 } 284 revs[k] = string(cv.AdditionalInfo) 285 } 286 return missingKeys, nil 287 } 288 289 func (vdb *VersionedDB) addMissingRevisionsFromDB(ns string, missingKeys []string, revisions map[string]string) error { 290 db, err := vdb.getNamespaceDBHandle(ns) 291 if err != nil { 292 return err 293 } 294 295 logger.Debugf("Pulling revisions for the [%d] keys for namsespace [%s] that were not part of the readset", len(missingKeys), db.dbName) 296 retrievedMetadata, err := retrieveNsMetadata(db, missingKeys) 297 if err != nil { 298 return err 299 } 300 for _, metadata := range retrievedMetadata { 301 revisions[metadata.ID] = metadata.Rev 302 } 303 304 return nil 305 } 306 307 // batchableDocument defines a document for a batch 308 type batchableDocument struct { 309 CouchDoc couchDoc 310 Deleted bool 311 }