github.com/jbendotnet/noms@v0.0.0-20190904222105-c43e4293ea92/go/datas/database_common.go (about)

     1  // Copyright 2016 Attic Labs, Inc. All rights reserved.
     2  // Licensed under the Apache License, version 2.0:
     3  // http://www.apache.org/licenses/LICENSE-2.0
     4  
     5  package datas
     6  
     7  import (
     8  	"errors"
     9  	"fmt"
    10  
    11  	"github.com/attic-labs/noms/go/chunks"
    12  	"github.com/attic-labs/noms/go/d"
    13  	"github.com/attic-labs/noms/go/hash"
    14  	"github.com/attic-labs/noms/go/merge"
    15  	"github.com/attic-labs/noms/go/types"
    16  	"github.com/attic-labs/noms/go/util/random"
    17  )
    18  
    19  type database struct {
    20  	*types.ValueStore
    21  	rt rootTracker
    22  }
    23  
    24  var (
    25  	ErrOptimisticLockFailed = errors.New("Optimistic lock failed on database Root update")
    26  	ErrMergeNeeded          = errors.New("Dataset head is not ancestor of commit")
    27  )
    28  
    29  // rootTracker is a narrowing of the ChunkStore interface, to keep Database disciplined about working directly with Chunks
    30  type rootTracker interface {
    31  	Rebase()
    32  	Root() hash.Hash
    33  	Commit(current, last hash.Hash) bool
    34  }
    35  
    36  func newDatabase(cs chunks.ChunkStore) *database {
    37  	vs := types.NewValueStore(cs)
    38  	if _, ok := cs.(*httpChunkStore); ok {
    39  		vs.SetEnforceCompleteness(false)
    40  	}
    41  
    42  	return &database{
    43  		ValueStore: vs, // ValueStore is responsible for closing |cs|
    44  		rt:         vs,
    45  	}
    46  }
    47  
    48  func (db *database) chunkStore() chunks.ChunkStore {
    49  	return db.ChunkStore()
    50  }
    51  
    52  func (db *database) Stats() interface{} {
    53  	return db.ChunkStore().Stats()
    54  }
    55  
    56  func (db *database) StatsSummary() string {
    57  	return db.ChunkStore().StatsSummary()
    58  }
    59  
    60  func (db *database) Flush() {
    61  	// TODO: This is a pretty ghetto hack - do better.
    62  	// See: https://github.com/attic-labs/noms/issues/3530
    63  	ds := db.GetDataset(fmt.Sprintf("-/flush/%s", random.Id()))
    64  	r := db.WriteValue(types.Bool(true))
    65  	ds, err := db.CommitValue(ds, r)
    66  	d.PanicIfError(err)
    67  	_, err = db.Delete(ds)
    68  	d.PanicIfError(err)
    69  }
    70  
    71  func (db *database) Datasets() types.Map {
    72  	rootHash := db.rt.Root()
    73  	if rootHash.IsEmpty() {
    74  		return types.NewMap(db)
    75  	}
    76  
    77  	return db.ReadValue(rootHash).(types.Map)
    78  }
    79  
    80  func (db *database) GetDataset(datasetID string) Dataset {
    81  	if !DatasetFullRe.MatchString(datasetID) {
    82  		d.Panic("Invalid dataset ID: %s", datasetID)
    83  	}
    84  	var head types.Value
    85  	if r, ok := db.Datasets().MaybeGet(types.String(datasetID)); ok {
    86  		head = r.(types.Ref).TargetValue(db)
    87  	}
    88  
    89  	return newDataset(db, datasetID, head)
    90  }
    91  
    92  func (db *database) Rebase() {
    93  	db.rt.Rebase()
    94  }
    95  
    96  func (db *database) Close() error {
    97  	return db.ValueStore.Close()
    98  }
    99  
   100  func (db *database) SetHead(ds Dataset, newHeadRef types.Ref) (Dataset, error) {
   101  	return db.doHeadUpdate(ds, func(ds Dataset) error { return db.doSetHead(ds, newHeadRef) })
   102  }
   103  
   104  func (db *database) doSetHead(ds Dataset, newHeadRef types.Ref) error {
   105  	if currentHeadRef, ok := ds.MaybeHeadRef(); ok && newHeadRef.Equals(currentHeadRef) {
   106  		return nil
   107  	}
   108  	commit := db.validateRefAsCommit(newHeadRef)
   109  
   110  	currentRootHash, currentDatasets := db.rt.Root(), db.Datasets()
   111  	commitRef := db.WriteValue(commit) // will be orphaned if the tryCommitChunks() below fails
   112  
   113  	currentDatasets = currentDatasets.Edit().Set(types.String(ds.ID()), types.ToRefOfValue(commitRef)).Map()
   114  	return db.tryCommitChunks(currentDatasets, currentRootHash)
   115  }
   116  
   117  func (db *database) FastForward(ds Dataset, newHeadRef types.Ref) (Dataset, error) {
   118  	return db.doHeadUpdate(ds, func(ds Dataset) error { return db.doFastForward(ds, newHeadRef) })
   119  }
   120  
   121  func (db *database) doFastForward(ds Dataset, newHeadRef types.Ref) error {
   122  	currentHeadRef, ok := ds.MaybeHeadRef()
   123  	if ok && newHeadRef.Equals(currentHeadRef) {
   124  		return nil
   125  	}
   126  
   127  	if ok && newHeadRef.Height() <= currentHeadRef.Height() {
   128  		return ErrMergeNeeded
   129  	}
   130  
   131  	commit := db.validateRefAsCommit(newHeadRef)
   132  	return db.doCommit(ds.ID(), commit, nil)
   133  }
   134  
   135  func (db *database) Commit(ds Dataset, v types.Value, opts CommitOptions) (Dataset, error) {
   136  	return db.doHeadUpdate(
   137  		ds,
   138  		func(ds Dataset) error { return db.doCommit(ds.ID(), buildNewCommit(ds, v, opts), opts.Policy) },
   139  	)
   140  }
   141  
   142  func (db *database) CommitValue(ds Dataset, v types.Value) (Dataset, error) {
   143  	return db.Commit(ds, v, CommitOptions{})
   144  }
   145  
   146  // doCommit manages concurrent access the single logical piece of mutable state: the current Root. doCommit is optimistic in that it is attempting to update head making the assumption that currentRootHash is the hash of the current head. The call to Commit below will return an 'ErrOptimisticLockFailed' error if that assumption fails (e.g. because of a race with another writer) and the entire algorithm must be tried again. This method will also fail and return an 'ErrMergeNeeded' error if the |commit| is not a descendent of the current dataset head
   147  func (db *database) doCommit(datasetID string, commit types.Struct, mergePolicy merge.Policy) error {
   148  	if !IsCommit(commit) {
   149  		d.Panic("Can't commit a non-Commit struct to dataset %s", datasetID)
   150  	}
   151  
   152  	// This could loop forever, given enough simultaneous committers. BUG 2565
   153  	var err error
   154  	for err = ErrOptimisticLockFailed; err == ErrOptimisticLockFailed; {
   155  		currentRootHash, currentDatasets := db.rt.Root(), db.Datasets()
   156  		commitRef := db.WriteValue(commit) // will be orphaned if the tryCommitChunks() below fails
   157  
   158  		// If there's nothing in the DB yet, skip all this logic.
   159  		if !currentRootHash.IsEmpty() {
   160  			r, hasHead := currentDatasets.MaybeGet(types.String(datasetID))
   161  
   162  			// First commit in dataset is always fast-forward, so go through all this iff there's already a Head for datasetID.
   163  			if hasHead {
   164  				head := r.(types.Ref).TargetValue(db)
   165  				currentHeadRef := types.NewRef(head)
   166  				ancestorRef, found := FindCommonAncestor(commitRef, currentHeadRef, db)
   167  				if !found {
   168  					return ErrMergeNeeded
   169  				}
   170  
   171  				// This covers all cases where currentHeadRef is not an ancestor of commit, including the following edge cases:
   172  				//   - commit is a duplicate of currentHead.
   173  				//   - we hit an ErrOptimisticLockFailed and looped back around because some other process changed the Head out from under us.
   174  				if currentHeadRef.TargetHash() != ancestorRef.TargetHash() || currentHeadRef.TargetHash() == commitRef.TargetHash() {
   175  					if mergePolicy == nil {
   176  						return ErrMergeNeeded
   177  					}
   178  
   179  					ancestor, currentHead := db.validateRefAsCommit(ancestorRef), db.validateRefAsCommit(currentHeadRef)
   180  					merged, err := mergePolicy(commit.Get(ValueField), currentHead.Get(ValueField), ancestor.Get(ValueField), db, nil)
   181  					if err != nil {
   182  						return err
   183  					}
   184  					commitRef = db.WriteValue(NewCommit(merged, types.NewSet(db, commitRef, currentHeadRef), types.EmptyStruct))
   185  				}
   186  			}
   187  		}
   188  		currentDatasets = currentDatasets.Edit().Set(types.String(datasetID), types.ToRefOfValue(commitRef)).Map()
   189  		err = db.tryCommitChunks(currentDatasets, currentRootHash)
   190  	}
   191  	return err
   192  }
   193  
   194  func (db *database) Delete(ds Dataset) (Dataset, error) {
   195  	return db.doHeadUpdate(ds, func(ds Dataset) error { return db.doDelete(ds.ID()) })
   196  }
   197  
   198  // doDelete manages concurrent access the single logical piece of mutable state: the current Root. doDelete is optimistic in that it is attempting to update head making the assumption that currentRootHash is the hash of the current head. The call to Commit below will return an 'ErrOptimisticLockFailed' error if that assumption fails (e.g. because of a race with another writer) and the entire algorithm must be tried again.
   199  func (db *database) doDelete(datasetIDstr string) error {
   200  	datasetID := types.String(datasetIDstr)
   201  	currentRootHash, currentDatasets := db.rt.Root(), db.Datasets()
   202  	var initialHead types.Ref
   203  	if r, hasHead := currentDatasets.MaybeGet(datasetID); !hasHead {
   204  		return nil
   205  	} else {
   206  		initialHead = r.(types.Ref)
   207  	}
   208  
   209  	var err error
   210  	for {
   211  		currentDatasets = currentDatasets.Edit().Remove(datasetID).Map()
   212  		err = db.tryCommitChunks(currentDatasets, currentRootHash)
   213  		if err != ErrOptimisticLockFailed {
   214  			break
   215  		}
   216  		// If the optimistic lock failed because someone changed the Head of datasetID, then return ErrMergeNeeded. If it failed because someone changed a different Dataset, we should try again.
   217  		currentRootHash, currentDatasets = db.rt.Root(), db.Datasets()
   218  		if r, hasHead := currentDatasets.MaybeGet(datasetID); !hasHead || (hasHead && !initialHead.Equals(r)) {
   219  			err = ErrMergeNeeded
   220  			break
   221  		}
   222  	}
   223  	return err
   224  }
   225  
   226  func (db *database) tryCommitChunks(currentDatasets types.Map, currentRootHash hash.Hash) (err error) {
   227  	newRootHash := db.WriteValue(currentDatasets).TargetHash()
   228  
   229  	if !db.rt.Commit(newRootHash, currentRootHash) {
   230  		err = ErrOptimisticLockFailed
   231  	}
   232  	return
   233  }
   234  
   235  func (db *database) validateRefAsCommit(r types.Ref) types.Struct {
   236  	v := db.ReadValue(r.TargetHash())
   237  
   238  	if v == nil {
   239  		panic(r.TargetHash().String() + " not found")
   240  	}
   241  	if !IsCommit(v) {
   242  		panic("Not a commit: " + types.EncodedValueMaxLines(v, 10) + "  ...\n")
   243  	}
   244  	return v.(types.Struct)
   245  }
   246  
   247  func buildNewCommit(ds Dataset, v types.Value, opts CommitOptions) types.Struct {
   248  	parents := opts.Parents
   249  	if (parents == types.Set{}) {
   250  		parents = types.NewSet(ds.Database())
   251  		if headRef, ok := ds.MaybeHeadRef(); ok {
   252  			parents = parents.Edit().Insert(headRef).Set()
   253  		}
   254  	}
   255  
   256  	meta := opts.Meta
   257  	if meta.IsZeroValue() {
   258  		meta = types.EmptyStruct
   259  	}
   260  	return NewCommit(v, parents, meta)
   261  }
   262  
   263  func (db *database) doHeadUpdate(ds Dataset, updateFunc func(ds Dataset) error) (Dataset, error) {
   264  	err := updateFunc(ds)
   265  	return db.GetDataset(ds.ID()), err
   266  }