github.com/ndau/noms@v1.0.5/go/nbs/root_tracker_test.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 nbs
     6  
     7  import (
     8  	"fmt"
     9  	"sync"
    10  	"testing"
    11  
    12  	"github.com/ndau/noms/go/chunks"
    13  	"github.com/ndau/noms/go/constants"
    14  	"github.com/ndau/noms/go/hash"
    15  	"github.com/stretchr/testify/assert"
    16  )
    17  
    18  func TestChunkStoreZeroValue(t *testing.T) {
    19  	assert := assert.New(t)
    20  	_, _, store := makeStoreWithFakes(t)
    21  	defer store.Close()
    22  
    23  	// No manifest file gets written until the first call to Commit(). Prior to that, Root() will simply return hash.Hash{}.
    24  	assert.Equal(hash.Hash{}, store.Root())
    25  	assert.Equal(constants.NomsVersion, store.Version())
    26  }
    27  
    28  func TestChunkStoreVersion(t *testing.T) {
    29  	assert := assert.New(t)
    30  	_, _, store := makeStoreWithFakes(t)
    31  	defer store.Close()
    32  
    33  	assert.Equal(constants.NomsVersion, store.Version())
    34  	newRoot := hash.Of([]byte("new root"))
    35  	if assert.True(store.Commit(newRoot, hash.Hash{})) {
    36  		assert.Equal(constants.NomsVersion, store.Version())
    37  	}
    38  }
    39  
    40  func TestChunkStoreRebase(t *testing.T) {
    41  	assert := assert.New(t)
    42  	fm, p, store := makeStoreWithFakes(t)
    43  	defer store.Close()
    44  
    45  	assert.Equal(hash.Hash{}, store.Root())
    46  	assert.Equal(constants.NomsVersion, store.Version())
    47  
    48  	// Simulate another process writing a manifest behind store's back.
    49  	newRoot, chunks := interloperWrite(fm, p, []byte("new root"), []byte("hello2"), []byte("goodbye2"), []byte("badbye2"))
    50  
    51  	// state in store shouldn't change
    52  	assert.Equal(hash.Hash{}, store.Root())
    53  	assert.Equal(constants.NomsVersion, store.Version())
    54  
    55  	store.Rebase()
    56  
    57  	// NOW it should
    58  	assert.Equal(newRoot, store.Root())
    59  	assert.Equal(constants.NomsVersion, store.Version())
    60  	assertDataInStore(chunks, store, assert)
    61  }
    62  
    63  func TestChunkStoreCommit(t *testing.T) {
    64  	assert := assert.New(t)
    65  	_, _, store := makeStoreWithFakes(t)
    66  	defer store.Close()
    67  
    68  	assert.Equal(hash.Hash{}, store.Root())
    69  
    70  	newRootChunk := chunks.NewChunk([]byte("new root"))
    71  	newRoot := newRootChunk.Hash()
    72  	store.Put(newRootChunk)
    73  	if assert.True(store.Commit(newRoot, hash.Hash{})) {
    74  		assert.True(store.Has(newRoot))
    75  		assert.Equal(newRoot, store.Root())
    76  	}
    77  
    78  	secondRootChunk := chunks.NewChunk([]byte("newer root"))
    79  	secondRoot := secondRootChunk.Hash()
    80  	store.Put(secondRootChunk)
    81  	if assert.True(store.Commit(secondRoot, newRoot)) {
    82  		assert.Equal(secondRoot, store.Root())
    83  		assert.True(store.Has(newRoot))
    84  		assert.True(store.Has(secondRoot))
    85  	}
    86  }
    87  
    88  func TestChunkStoreManifestAppearsAfterConstruction(t *testing.T) {
    89  	assert := assert.New(t)
    90  	fm, p, store := makeStoreWithFakes(t)
    91  	defer store.Close()
    92  
    93  	assert.Equal(hash.Hash{}, store.Root())
    94  	assert.Equal(constants.NomsVersion, store.Version())
    95  
    96  	// Simulate another process writing a manifest behind store's back.
    97  	interloperWrite(fm, p, []byte("new root"), []byte("hello2"), []byte("goodbye2"), []byte("badbye2"))
    98  
    99  	// state in store shouldn't change
   100  	assert.Equal(hash.Hash{}, store.Root())
   101  	assert.Equal(constants.NomsVersion, store.Version())
   102  }
   103  
   104  func TestChunkStoreManifestFirstWriteByOtherProcess(t *testing.T) {
   105  	assert := assert.New(t)
   106  	fm := &fakeManifest{}
   107  	mm := manifestManager{fm, newManifestCache(0), newManifestLocks()}
   108  	p := newFakeTablePersister()
   109  
   110  	// Simulate another process writing a manifest behind store's back.
   111  	newRoot, chunks := interloperWrite(fm, p, []byte("new root"), []byte("hello2"), []byte("goodbye2"), []byte("badbye2"))
   112  
   113  	store := newNomsBlockStore(mm, p, inlineConjoiner{defaultMaxTables}, defaultMemTableSize)
   114  	defer store.Close()
   115  
   116  	assert.Equal(newRoot, store.Root())
   117  	assert.Equal(constants.NomsVersion, store.Version())
   118  	assertDataInStore(chunks, store, assert)
   119  }
   120  
   121  func TestChunkStoreCommitOptimisticLockFail(t *testing.T) {
   122  	assert := assert.New(t)
   123  	fm, p, store := makeStoreWithFakes(t)
   124  	defer store.Close()
   125  
   126  	// Simulate another process writing a manifest behind store's back.
   127  	newRoot, chunks := interloperWrite(fm, p, []byte("new root"), []byte("hello2"), []byte("goodbye2"), []byte("badbye2"))
   128  
   129  	newRoot2 := hash.Of([]byte("new root 2"))
   130  	assert.False(store.Commit(newRoot2, hash.Hash{}))
   131  	assertDataInStore(chunks, store, assert)
   132  	assert.True(store.Commit(newRoot2, newRoot))
   133  }
   134  
   135  func TestChunkStoreManifestPreemptiveOptimisticLockFail(t *testing.T) {
   136  	assert := assert.New(t)
   137  	fm := &fakeManifest{}
   138  	mm := manifestManager{fm, newManifestCache(defaultManifestCacheSize), newManifestLocks()}
   139  	p := newFakeTablePersister()
   140  	c := inlineConjoiner{defaultMaxTables}
   141  
   142  	store := newNomsBlockStore(mm, p, c, defaultMemTableSize)
   143  	defer store.Close()
   144  
   145  	// Simulate another goroutine writing a manifest behind store's back.
   146  	interloper := newNomsBlockStore(mm, p, c, defaultMemTableSize)
   147  	defer interloper.Close()
   148  
   149  	chunk := chunks.NewChunk([]byte("hello"))
   150  	interloper.Put(chunk)
   151  	assert.True(interloper.Commit(chunk.Hash(), hash.Hash{}))
   152  
   153  	// Try to land a new chunk in store, which should fail AND not persist the contents of store.mt
   154  	chunk = chunks.NewChunk([]byte("goodbye"))
   155  	store.Put(chunk)
   156  	assert.NotNil(store.mt)
   157  	assert.False(store.Commit(chunk.Hash(), hash.Hash{}))
   158  	assert.NotNil(store.mt)
   159  
   160  	assert.True(store.Commit(chunk.Hash(), store.Root()))
   161  	assert.Nil(store.mt)
   162  	assert.Equal(chunk.Hash(), store.Root())
   163  	assert.Equal(constants.NomsVersion, store.Version())
   164  }
   165  
   166  func TestChunkStoreCommitLocksOutFetch(t *testing.T) {
   167  	assert := assert.New(t)
   168  	fm := &fakeManifest{name: "foo"}
   169  	upm := &updatePreemptManifest{manifest: fm}
   170  	mm := manifestManager{upm, newManifestCache(defaultManifestCacheSize), newManifestLocks()}
   171  	p := newFakeTablePersister()
   172  	c := inlineConjoiner{defaultMaxTables}
   173  
   174  	store := newNomsBlockStore(mm, p, c, defaultMemTableSize)
   175  	defer store.Close()
   176  
   177  	// store.Commit() should lock out calls to mm.Fetch()
   178  	wg := sync.WaitGroup{}
   179  	fetched := manifestContents{}
   180  	upm.preUpdate = func() {
   181  		wg.Add(1)
   182  		go func() {
   183  			defer wg.Done()
   184  			_, fetched = mm.Fetch(nil)
   185  		}()
   186  	}
   187  
   188  	rootChunk := chunks.NewChunk([]byte("new root"))
   189  	store.Put(rootChunk)
   190  	assert.True(store.Commit(rootChunk.Hash(), store.Root()))
   191  
   192  	wg.Wait()
   193  	assert.Equal(store.Root(), fetched.root)
   194  }
   195  
   196  func TestChunkStoreSerializeCommits(t *testing.T) {
   197  	assert := assert.New(t)
   198  	fm := &fakeManifest{name: "foo"}
   199  	upm := &updatePreemptManifest{manifest: fm}
   200  	mc := newManifestCache(defaultManifestCacheSize)
   201  	l := newManifestLocks()
   202  	p := newFakeTablePersister()
   203  	c := inlineConjoiner{defaultMaxTables}
   204  
   205  	store := newNomsBlockStore(manifestManager{upm, mc, l}, p, c, defaultMemTableSize)
   206  	defer store.Close()
   207  
   208  	storeChunk := chunks.NewChunk([]byte("store"))
   209  	interloperChunk := chunks.NewChunk([]byte("interloper"))
   210  	updateCount := 0
   211  
   212  	interloper := newNomsBlockStore(
   213  		manifestManager{
   214  			updatePreemptManifest{fm, func() { updateCount++ }}, mc, l,
   215  		},
   216  		p,
   217  		c,
   218  		defaultMemTableSize)
   219  	defer interloper.Close()
   220  
   221  	wg := sync.WaitGroup{}
   222  	upm.preUpdate = func() {
   223  		wg.Add(1)
   224  		go func() {
   225  			defer wg.Done()
   226  			interloper.Put(interloperChunk)
   227  			assert.True(interloper.Commit(interloper.Root(), interloper.Root()))
   228  		}()
   229  
   230  		updateCount++
   231  	}
   232  
   233  	store.Put(storeChunk)
   234  	assert.True(store.Commit(store.Root(), store.Root()))
   235  
   236  	wg.Wait()
   237  	assert.Equal(2, updateCount)
   238  	assert.True(interloper.Has(storeChunk.Hash()))
   239  	assert.True(interloper.Has(interloperChunk.Hash()))
   240  }
   241  
   242  func makeStoreWithFakes(t *testing.T) (fm *fakeManifest, p tablePersister, store *NomsBlockStore) {
   243  	fm = &fakeManifest{}
   244  	mm := manifestManager{fm, newManifestCache(0), newManifestLocks()}
   245  	p = newFakeTablePersister()
   246  	store = newNomsBlockStore(mm, p, inlineConjoiner{defaultMaxTables}, 0)
   247  	return
   248  }
   249  
   250  // Simulate another process writing a manifest behind store's back.
   251  func interloperWrite(fm *fakeManifest, p tablePersister, rootChunk []byte, chunks ...[]byte) (newRoot hash.Hash, persisted [][]byte) {
   252  	newLock, newRoot := computeAddr([]byte("locker")), hash.Of(rootChunk)
   253  	persisted = append(chunks, rootChunk)
   254  	src := p.Persist(createMemTable(persisted), nil, &Stats{})
   255  	fm.set(constants.NomsVersion, newLock, newRoot, []tableSpec{{src.hash(), uint32(len(chunks))}})
   256  	return
   257  }
   258  
   259  func createMemTable(chunks [][]byte) *memTable {
   260  	mt := newMemTable(1 << 10)
   261  	for _, c := range chunks {
   262  		mt.addChunk(computeAddr(c), c)
   263  	}
   264  	return mt
   265  }
   266  
   267  func assertDataInStore(slices [][]byte, store chunks.ChunkStore, assert *assert.Assertions) {
   268  	for _, data := range slices {
   269  		assert.True(store.Has(chunks.NewChunk(data).Hash()))
   270  	}
   271  }
   272  
   273  // fakeManifest simulates a fileManifest without touching disk.
   274  type fakeManifest struct {
   275  	name     string
   276  	contents manifestContents
   277  	mu       sync.RWMutex
   278  }
   279  
   280  func (fm *fakeManifest) Name() string { return fm.name }
   281  
   282  // ParseIfExists returns any fake manifest data the caller has injected using
   283  // Update() or set(). It treats an empty |fm.lock| as a non-existent manifest.
   284  func (fm *fakeManifest) ParseIfExists(stats *Stats, readHook func()) (exists bool, contents manifestContents) {
   285  	fm.mu.RLock()
   286  	defer fm.mu.RUnlock()
   287  	if fm.contents.lock != (addr{}) {
   288  		return true, fm.contents
   289  	}
   290  	return false, manifestContents{}
   291  }
   292  
   293  // Update checks whether |lastLock| == |fm.lock| and, if so, updates internal
   294  // fake manifest state as per the manifest.Update() contract: |fm.lock| is set
   295  // to |newLock|, |fm.root| is set to |newRoot|, and the contents of |specs|
   296  // replace |fm.tableSpecs|. If |lastLock| != |fm.lock|, then the update
   297  // fails. Regardless of success or failure, the current state is returned.
   298  func (fm *fakeManifest) Update(lastLock addr, newContents manifestContents, stats *Stats, writeHook func()) manifestContents {
   299  	fm.mu.Lock()
   300  	defer fm.mu.Unlock()
   301  	if fm.contents.lock == lastLock {
   302  		fm.contents = manifestContents{newContents.vers, newContents.lock, newContents.root, nil}
   303  		fm.contents.specs = make([]tableSpec, len(newContents.specs))
   304  		copy(fm.contents.specs, newContents.specs)
   305  	}
   306  	return fm.contents
   307  }
   308  
   309  func (fm *fakeManifest) set(version string, lock addr, root hash.Hash, specs []tableSpec) {
   310  	fm.contents = manifestContents{version, lock, root, specs}
   311  }
   312  
   313  func newFakeTableSet() tableSet {
   314  	return tableSet{p: newFakeTablePersister(), rl: make(chan struct{}, 1)}
   315  }
   316  
   317  func newFakeTablePersister() tablePersister {
   318  	return fakeTablePersister{map[addr]tableReader{}, &sync.RWMutex{}}
   319  }
   320  
   321  type fakeTablePersister struct {
   322  	sources map[addr]tableReader
   323  	mu      *sync.RWMutex
   324  }
   325  
   326  func (ftp fakeTablePersister) Persist(mt *memTable, haver chunkReader, stats *Stats) chunkSource {
   327  	if mt.count() > 0 {
   328  		name, data, chunkCount := mt.write(haver, stats)
   329  		if chunkCount > 0 {
   330  			ftp.mu.Lock()
   331  			defer ftp.mu.Unlock()
   332  			ftp.sources[name] = newTableReader(parseTableIndex(data), tableReaderAtFromBytes(data), fileBlockSize)
   333  			return chunkSourceAdapter{ftp.sources[name], name}
   334  		}
   335  	}
   336  	return emptyChunkSource{}
   337  }
   338  
   339  func (ftp fakeTablePersister) ConjoinAll(sources chunkSources, stats *Stats) chunkSource {
   340  	name, data, chunkCount := compactSourcesToBuffer(sources)
   341  	if chunkCount > 0 {
   342  		ftp.mu.Lock()
   343  		defer ftp.mu.Unlock()
   344  		ftp.sources[name] = newTableReader(parseTableIndex(data), tableReaderAtFromBytes(data), fileBlockSize)
   345  		return chunkSourceAdapter{ftp.sources[name], name}
   346  	}
   347  	return emptyChunkSource{}
   348  }
   349  
   350  func compactSourcesToBuffer(sources chunkSources) (name addr, data []byte, chunkCount uint32) {
   351  	totalData := uint64(0)
   352  	for _, src := range sources {
   353  		chunkCount += src.count()
   354  		totalData += src.uncompressedLen()
   355  	}
   356  	if chunkCount == 0 {
   357  		return
   358  	}
   359  
   360  	maxSize := maxTableSize(uint64(chunkCount), totalData)
   361  	buff := make([]byte, maxSize) // This can blow up RAM
   362  	tw := newTableWriter(buff, nil)
   363  	errString := ""
   364  
   365  	for _, src := range sources {
   366  		chunks := make(chan extractRecord)
   367  		go func() {
   368  			defer close(chunks)
   369  			defer func() {
   370  				if r := recover(); r != nil {
   371  					chunks <- extractRecord{a: src.hash(), err: r}
   372  				}
   373  			}()
   374  			src.extract(chunks)
   375  		}()
   376  		for rec := range chunks {
   377  			if rec.err != nil {
   378  				errString += fmt.Sprintf("Failed to extract %s:\n %v\n******\n\n", rec.a, rec.err)
   379  				continue
   380  			}
   381  			tw.addChunk(rec.a, rec.data)
   382  		}
   383  	}
   384  
   385  	if errString != "" {
   386  		panic(fmt.Errorf(errString))
   387  	}
   388  	tableSize, name := tw.finish()
   389  	return name, buff[:tableSize], chunkCount
   390  }
   391  
   392  func (ftp fakeTablePersister) Open(name addr, chunkCount uint32, stats *Stats) chunkSource {
   393  	ftp.mu.RLock()
   394  	defer ftp.mu.RUnlock()
   395  	return chunkSourceAdapter{ftp.sources[name], name}
   396  }
   397  
   398  type chunkSourceAdapter struct {
   399  	tableReader
   400  	h addr
   401  }
   402  
   403  func (csa chunkSourceAdapter) hash() addr {
   404  	return csa.h
   405  }
   406  
   407  func (csa chunkSourceAdapter) index() tableIndex {
   408  	return csa.tableIndex
   409  }