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  }