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