github.com/hasnat/dolt/go@v0.0.0-20210628190320-9eb5d843fbb7/store/nbs/root_tracker_test.go (about)

     1  // Copyright 2019 Dolthub, Inc.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  //
    15  // This file incorporates work covered by the following copyright and
    16  // permission notice:
    17  //
    18  // Copyright 2016 Attic Labs, Inc. All rights reserved.
    19  // Licensed under the Apache License, version 2.0:
    20  // http://www.apache.org/licenses/LICENSE-2.0
    21  
    22  package nbs
    23  
    24  import (
    25  	"context"
    26  	"fmt"
    27  	"sync"
    28  	"testing"
    29  
    30  	"github.com/stretchr/testify/assert"
    31  	"github.com/stretchr/testify/require"
    32  
    33  	"github.com/dolthub/dolt/go/store/chunks"
    34  	"github.com/dolthub/dolt/go/store/constants"
    35  	"github.com/dolthub/dolt/go/store/hash"
    36  )
    37  
    38  func TestChunkStoreZeroValue(t *testing.T) {
    39  	assert := assert.New(t)
    40  	_, _, store := makeStoreWithFakes(t)
    41  	defer store.Close()
    42  
    43  	// No manifest file gets written until the first call to Commit(). Prior to that, Root() will simply return hash.Hash{}.
    44  	h, err := store.Root(context.Background())
    45  	require.NoError(t, err)
    46  	assert.Equal(hash.Hash{}, h)
    47  	assert.Equal(constants.NomsVersion, store.Version())
    48  }
    49  
    50  func TestChunkStoreVersion(t *testing.T) {
    51  	assert := assert.New(t)
    52  	_, _, store := makeStoreWithFakes(t)
    53  	defer store.Close()
    54  
    55  	assert.Equal(constants.NomsVersion, store.Version())
    56  	newRoot := hash.Of([]byte("new root"))
    57  	if assert.True(store.Commit(context.Background(), newRoot, hash.Hash{})) {
    58  		assert.Equal(constants.NomsVersion, store.Version())
    59  	}
    60  }
    61  
    62  func TestChunkStoreRebase(t *testing.T) {
    63  	assert := assert.New(t)
    64  	fm, p, store := makeStoreWithFakes(t)
    65  	defer store.Close()
    66  
    67  	h, err := store.Root(context.Background())
    68  	require.NoError(t, err)
    69  	assert.Equal(hash.Hash{}, h)
    70  	assert.Equal(constants.NomsVersion, store.Version())
    71  
    72  	// Simulate another process writing a manifest behind store's back.
    73  	newRoot, chunks, err := interloperWrite(fm, p, []byte("new root"), []byte("hello2"), []byte("goodbye2"), []byte("badbye2"))
    74  	require.NoError(t, err)
    75  
    76  	// state in store shouldn't change
    77  	h, err = store.Root(context.Background())
    78  	require.NoError(t, err)
    79  	assert.Equal(hash.Hash{}, h)
    80  	assert.Equal(constants.NomsVersion, store.Version())
    81  
    82  	err = store.Rebase(context.Background())
    83  	require.NoError(t, err)
    84  
    85  	// NOW it should
    86  	h, err = store.Root(context.Background())
    87  	require.NoError(t, err)
    88  	assert.Equal(newRoot, h)
    89  	assert.Equal(constants.NomsVersion, store.Version())
    90  	assertDataInStore(chunks, store, assert)
    91  }
    92  
    93  func TestChunkStoreCommit(t *testing.T) {
    94  	assert := assert.New(t)
    95  	_, _, store := makeStoreWithFakes(t)
    96  	defer store.Close()
    97  
    98  	h, err := store.Root(context.Background())
    99  	require.NoError(t, err)
   100  	assert.Equal(hash.Hash{}, h)
   101  
   102  	newRootChunk := chunks.NewChunk([]byte("new root"))
   103  	newRoot := newRootChunk.Hash()
   104  	err = store.Put(context.Background(), newRootChunk)
   105  	require.NoError(t, err)
   106  	success, err := store.Commit(context.Background(), newRoot, hash.Hash{})
   107  	require.NoError(t, err)
   108  	if assert.True(success) {
   109  		has, err := store.Has(context.Background(), newRoot)
   110  		require.NoError(t, err)
   111  		assert.True(has)
   112  		h, err := store.Root(context.Background())
   113  		require.NoError(t, err)
   114  		assert.Equal(newRoot, h)
   115  	}
   116  
   117  	secondRootChunk := chunks.NewChunk([]byte("newer root"))
   118  	secondRoot := secondRootChunk.Hash()
   119  	err = store.Put(context.Background(), secondRootChunk)
   120  	require.NoError(t, err)
   121  	success, err = store.Commit(context.Background(), secondRoot, newRoot)
   122  	require.NoError(t, err)
   123  	if assert.True(success) {
   124  		h, err := store.Root(context.Background())
   125  		require.NoError(t, err)
   126  		assert.Equal(secondRoot, h)
   127  		has, err := store.Has(context.Background(), newRoot)
   128  		require.NoError(t, err)
   129  		assert.True(has)
   130  		has, err = store.Has(context.Background(), secondRoot)
   131  		require.NoError(t, err)
   132  		assert.True(has)
   133  	}
   134  }
   135  
   136  func TestChunkStoreManifestAppearsAfterConstruction(t *testing.T) {
   137  	assert := assert.New(t)
   138  	fm, p, store := makeStoreWithFakes(t)
   139  	defer store.Close()
   140  
   141  	h, err := store.Root(context.Background())
   142  	require.NoError(t, err)
   143  	assert.Equal(hash.Hash{}, h)
   144  	assert.Equal(constants.NomsVersion, store.Version())
   145  
   146  	// Simulate another process writing a manifest behind store's back.
   147  	interloperWrite(fm, p, []byte("new root"), []byte("hello2"), []byte("goodbye2"), []byte("badbye2"))
   148  
   149  	// state in store shouldn't change
   150  	h, err = store.Root(context.Background())
   151  	require.NoError(t, err)
   152  	assert.Equal(hash.Hash{}, h)
   153  	assert.Equal(constants.NomsVersion, store.Version())
   154  }
   155  
   156  func TestChunkStoreManifestFirstWriteByOtherProcess(t *testing.T) {
   157  	assert := assert.New(t)
   158  	fm := &fakeManifest{}
   159  	mm := manifestManager{fm, newManifestCache(0), newManifestLocks()}
   160  	p := newFakeTablePersister()
   161  
   162  	// Simulate another process writing a manifest behind store's back.
   163  	newRoot, chunks, err := interloperWrite(fm, p, []byte("new root"), []byte("hello2"), []byte("goodbye2"), []byte("badbye2"))
   164  	require.NoError(t, err)
   165  
   166  	store, err := newNomsBlockStore(context.Background(), constants.Format718String, mm, p, inlineConjoiner{defaultMaxTables}, defaultMemTableSize)
   167  	require.NoError(t, err)
   168  	defer store.Close()
   169  
   170  	h, err := store.Root(context.Background())
   171  	require.NoError(t, err)
   172  	assert.Equal(newRoot, h)
   173  	assert.Equal(constants.NomsVersion, store.Version())
   174  	assertDataInStore(chunks, store, assert)
   175  }
   176  
   177  func TestChunkStoreCommitOptimisticLockFail(t *testing.T) {
   178  	assert := assert.New(t)
   179  	fm, p, store := makeStoreWithFakes(t)
   180  	defer store.Close()
   181  
   182  	// Simulate another process writing a manifest behind store's back.
   183  	newRoot, chunks, err := interloperWrite(fm, p, []byte("new root"), []byte("hello2"), []byte("goodbye2"), []byte("badbye2"))
   184  	require.NoError(t, err)
   185  
   186  	newRoot2 := hash.Of([]byte("new root 2"))
   187  	success, err := store.Commit(context.Background(), newRoot2, hash.Hash{})
   188  	require.NoError(t, err)
   189  	assert.False(success)
   190  	assertDataInStore(chunks, store, assert)
   191  	success, err = store.Commit(context.Background(), newRoot2, newRoot)
   192  	require.NoError(t, err)
   193  	assert.True(success)
   194  }
   195  
   196  func TestChunkStoreManifestPreemptiveOptimisticLockFail(t *testing.T) {
   197  	assert := assert.New(t)
   198  	fm := &fakeManifest{}
   199  	mm := manifestManager{fm, newManifestCache(defaultManifestCacheSize), newManifestLocks()}
   200  	p := newFakeTablePersister()
   201  	c := inlineConjoiner{defaultMaxTables}
   202  
   203  	store, err := newNomsBlockStore(context.Background(), constants.Format718String, mm, p, c, defaultMemTableSize)
   204  	require.NoError(t, err)
   205  	defer store.Close()
   206  
   207  	// Simulate another goroutine writing a manifest behind store's back.
   208  	interloper, err := newNomsBlockStore(context.Background(), constants.Format718String, mm, p, c, defaultMemTableSize)
   209  	require.NoError(t, err)
   210  	defer interloper.Close()
   211  
   212  	chunk := chunks.NewChunk([]byte("hello"))
   213  	err = interloper.Put(context.Background(), chunk)
   214  	require.NoError(t, err)
   215  	assert.True(interloper.Commit(context.Background(), chunk.Hash(), hash.Hash{}))
   216  
   217  	// Try to land a new chunk in store, which should fail AND not persist the contents of store.mt
   218  	chunk = chunks.NewChunk([]byte("goodbye"))
   219  	err = store.Put(context.Background(), chunk)
   220  	require.NoError(t, err)
   221  	assert.NotNil(store.mt)
   222  	assert.False(store.Commit(context.Background(), chunk.Hash(), hash.Hash{}))
   223  	assert.NotNil(store.mt)
   224  
   225  	h, err := store.Root(context.Background())
   226  	require.NoError(t, err)
   227  	success, err := store.Commit(context.Background(), chunk.Hash(), h)
   228  	require.NoError(t, err)
   229  	assert.True(success)
   230  	assert.Nil(store.mt)
   231  
   232  	h, err = store.Root(context.Background())
   233  	require.NoError(t, err)
   234  	assert.Equal(chunk.Hash(), h)
   235  	assert.Equal(constants.NomsVersion, store.Version())
   236  }
   237  
   238  func TestChunkStoreCommitLocksOutFetch(t *testing.T) {
   239  	assert := assert.New(t)
   240  	fm := &fakeManifest{name: "foo"}
   241  	upm := &updatePreemptManifest{manifest: fm}
   242  	mm := manifestManager{upm, newManifestCache(defaultManifestCacheSize), newManifestLocks()}
   243  	p := newFakeTablePersister()
   244  	c := inlineConjoiner{defaultMaxTables}
   245  
   246  	store, err := newNomsBlockStore(context.Background(), constants.Format718String, mm, p, c, defaultMemTableSize)
   247  	require.NoError(t, err)
   248  	defer store.Close()
   249  
   250  	// store.Commit() should lock out calls to mm.Fetch()
   251  	wg := sync.WaitGroup{}
   252  	fetched := manifestContents{}
   253  	upm.preUpdate = func() {
   254  		wg.Add(1)
   255  		go func() {
   256  			defer wg.Done()
   257  			var err error
   258  			_, fetched, err = mm.Fetch(context.Background(), nil)
   259  			require.NoError(t, err)
   260  		}()
   261  	}
   262  
   263  	rootChunk := chunks.NewChunk([]byte("new root"))
   264  	err = store.Put(context.Background(), rootChunk)
   265  	require.NoError(t, err)
   266  	h, err := store.Root(context.Background())
   267  	require.NoError(t, err)
   268  	success, err := store.Commit(context.Background(), rootChunk.Hash(), h)
   269  	require.NoError(t, err)
   270  	assert.True(success)
   271  
   272  	wg.Wait()
   273  	h, err = store.Root(context.Background())
   274  	require.NoError(t, err)
   275  	assert.Equal(h, fetched.root)
   276  }
   277  
   278  func TestChunkStoreSerializeCommits(t *testing.T) {
   279  	assert := assert.New(t)
   280  	fm := &fakeManifest{name: "foo"}
   281  	upm := &updatePreemptManifest{manifest: fm}
   282  	mc := newManifestCache(defaultManifestCacheSize)
   283  	l := newManifestLocks()
   284  	p := newFakeTablePersister()
   285  	c := inlineConjoiner{defaultMaxTables}
   286  
   287  	store, err := newNomsBlockStore(context.Background(), constants.Format718String, manifestManager{upm, mc, l}, p, c, defaultMemTableSize)
   288  	require.NoError(t, err)
   289  	defer store.Close()
   290  
   291  	storeChunk := chunks.NewChunk([]byte("store"))
   292  	interloperChunk := chunks.NewChunk([]byte("interloper"))
   293  	updateCount := 0
   294  
   295  	interloper, err := newNomsBlockStore(
   296  		context.Background(),
   297  		constants.Format718String,
   298  		manifestManager{
   299  			updatePreemptManifest{fm, func() { updateCount++ }}, mc, l,
   300  		},
   301  		p,
   302  		c,
   303  		defaultMemTableSize)
   304  	require.NoError(t, err)
   305  	defer interloper.Close()
   306  
   307  	wg := sync.WaitGroup{}
   308  	upm.preUpdate = func() {
   309  		wg.Add(1)
   310  		go func() {
   311  			defer wg.Done()
   312  			err := interloper.Put(context.Background(), interloperChunk)
   313  			require.NoError(t, err)
   314  			h, err := interloper.Root(context.Background())
   315  			require.NoError(t, err)
   316  			success, err := interloper.Commit(context.Background(), h, h)
   317  			require.NoError(t, err)
   318  			assert.True(success)
   319  		}()
   320  
   321  		updateCount++
   322  	}
   323  
   324  	err = store.Put(context.Background(), storeChunk)
   325  	require.NoError(t, err)
   326  	h, err := store.Root(context.Background())
   327  	require.NoError(t, err)
   328  	success, err := store.Commit(context.Background(), h, h)
   329  	require.NoError(t, err)
   330  	assert.True(success)
   331  
   332  	wg.Wait()
   333  	assert.Equal(2, updateCount)
   334  	assert.True(interloper.Has(context.Background(), storeChunk.Hash()))
   335  	assert.True(interloper.Has(context.Background(), interloperChunk.Hash()))
   336  }
   337  
   338  func makeStoreWithFakes(t *testing.T) (fm *fakeManifest, p tablePersister, store *NomsBlockStore) {
   339  	fm = &fakeManifest{}
   340  	mm := manifestManager{fm, newManifestCache(0), newManifestLocks()}
   341  	p = newFakeTablePersister()
   342  	store, err := newNomsBlockStore(context.Background(), constants.Format718String, mm, p, inlineConjoiner{defaultMaxTables}, 0)
   343  	require.NoError(t, err)
   344  	return
   345  }
   346  
   347  // Simulate another process writing a manifest behind store's back.
   348  func interloperWrite(fm *fakeManifest, p tablePersister, rootChunk []byte, chunks ...[]byte) (newRoot hash.Hash, persisted [][]byte, err error) {
   349  	newLock, newRoot := computeAddr([]byte("locker")), hash.Of(rootChunk)
   350  	persisted = append(chunks, rootChunk)
   351  
   352  	var src chunkSource
   353  	src, err = p.Persist(context.Background(), createMemTable(persisted), nil, &Stats{})
   354  
   355  	if err != nil {
   356  		return hash.Hash{}, nil, err
   357  	}
   358  
   359  	fm.set(constants.NomsVersion, newLock, newRoot, []tableSpec{{mustAddr(src.hash()), uint32(len(chunks))}}, nil)
   360  	return
   361  }
   362  
   363  func createMemTable(chunks [][]byte) *memTable {
   364  	mt := newMemTable(1 << 10)
   365  	for _, c := range chunks {
   366  		mt.addChunk(computeAddr(c), c)
   367  	}
   368  	return mt
   369  }
   370  
   371  func assertDataInStore(slices [][]byte, store chunks.ChunkStore, assert *assert.Assertions) {
   372  	for _, data := range slices {
   373  		ok, err := store.Has(context.Background(), chunks.NewChunk(data).Hash())
   374  		assert.NoError(err)
   375  		assert.True(ok)
   376  	}
   377  }
   378  
   379  // fakeManifest simulates a fileManifest without touching disk.
   380  type fakeManifest struct {
   381  	name     string
   382  	contents manifestContents
   383  	mu       sync.RWMutex
   384  }
   385  
   386  func (fm *fakeManifest) Name() string { return fm.name }
   387  
   388  // ParseIfExists returns any fake manifest data the caller has injected using
   389  // Update() or set(). It treats an empty |fm.lock| as a non-existent manifest.
   390  func (fm *fakeManifest) ParseIfExists(ctx context.Context, stats *Stats, readHook func() error) (bool, manifestContents, error) {
   391  	fm.mu.RLock()
   392  	defer fm.mu.RUnlock()
   393  	if fm.contents.lock != (addr{}) {
   394  		return true, fm.contents, nil
   395  	}
   396  
   397  	return false, manifestContents{}, nil
   398  }
   399  
   400  // Update checks whether |lastLock| == |fm.lock| and, if so, updates internal
   401  // fake manifest state as per the manifest.Update() contract: |fm.lock| is set
   402  // to |newLock|, |fm.root| is set to |newRoot|, and the contents of |specs|
   403  // replace |fm.tableSpecs|. If |lastLock| != |fm.lock|, then the update
   404  // fails. Regardless of success or failure, the current state is returned.
   405  func (fm *fakeManifest) Update(ctx context.Context, lastLock addr, newContents manifestContents, stats *Stats, writeHook func() error) (manifestContents, error) {
   406  	fm.mu.Lock()
   407  	defer fm.mu.Unlock()
   408  	if fm.contents.lock == lastLock {
   409  		fm.contents = manifestContents{newContents.vers, newContents.lock, newContents.root, addr(hash.Hash{}), nil, nil}
   410  		fm.contents.specs = make([]tableSpec, len(newContents.specs))
   411  		copy(fm.contents.specs, newContents.specs)
   412  		if newContents.appendix != nil && len(newContents.appendix) > 0 {
   413  			fm.contents.appendix = make([]tableSpec, len(newContents.appendix))
   414  			copy(fm.contents.appendix, newContents.appendix)
   415  		}
   416  	}
   417  	return fm.contents, nil
   418  }
   419  
   420  func (fm *fakeManifest) set(version string, lock addr, root hash.Hash, specs, appendix []tableSpec) {
   421  	fm.contents = manifestContents{version, lock, root, addr(hash.Hash{}), specs, appendix}
   422  }
   423  
   424  func newFakeTableSet() tableSet {
   425  	return tableSet{p: newFakeTablePersister(), rl: make(chan struct{}, 1)}
   426  }
   427  
   428  func newFakeTablePersister() tablePersister {
   429  	return fakeTablePersister{map[addr]tableReader{}, &sync.RWMutex{}}
   430  }
   431  
   432  type fakeTablePersister struct {
   433  	sources map[addr]tableReader
   434  	mu      *sync.RWMutex
   435  }
   436  
   437  var _ tablePersister = fakeTablePersister{}
   438  
   439  func (ftp fakeTablePersister) Persist(ctx context.Context, mt *memTable, haver chunkReader, stats *Stats) (chunkSource, error) {
   440  	if mustUint32(mt.count()) > 0 {
   441  		name, data, chunkCount, err := mt.write(haver, stats)
   442  
   443  		if err != nil {
   444  			return emptyChunkSource{}, err
   445  		}
   446  
   447  		if chunkCount > 0 {
   448  			ftp.mu.Lock()
   449  			defer ftp.mu.Unlock()
   450  			ti, err := parseTableIndex(data)
   451  
   452  			if err != nil {
   453  				return nil, err
   454  			}
   455  
   456  			ftp.sources[name] = newTableReader(ti, tableReaderAtFromBytes(data), fileBlockSize)
   457  			return chunkSourceAdapter{ftp.sources[name], name}, nil
   458  		}
   459  	}
   460  	return emptyChunkSource{}, nil
   461  }
   462  
   463  func (ftp fakeTablePersister) ConjoinAll(ctx context.Context, sources chunkSources, stats *Stats) (chunkSource, error) {
   464  	name, data, chunkCount, err := compactSourcesToBuffer(sources)
   465  
   466  	if err != nil {
   467  		return nil, err
   468  	}
   469  
   470  	if chunkCount > 0 {
   471  		ftp.mu.Lock()
   472  		defer ftp.mu.Unlock()
   473  		ti, err := parseTableIndex(data)
   474  
   475  		if err != nil {
   476  			return nil, err
   477  		}
   478  
   479  		ftp.sources[name] = newTableReader(ti, tableReaderAtFromBytes(data), fileBlockSize)
   480  		return chunkSourceAdapter{ftp.sources[name], name}, nil
   481  	}
   482  	return emptyChunkSource{}, nil
   483  }
   484  
   485  func compactSourcesToBuffer(sources chunkSources) (name addr, data []byte, chunkCount uint32, err error) {
   486  	totalData := uint64(0)
   487  	for _, src := range sources {
   488  		chunkCount += mustUint32(src.count())
   489  		totalData += mustUint64(src.uncompressedLen())
   490  	}
   491  	if chunkCount == 0 {
   492  		return
   493  	}
   494  
   495  	maxSize := maxTableSize(uint64(chunkCount), totalData)
   496  	buff := make([]byte, maxSize) // This can blow up RAM
   497  	tw := newTableWriter(buff, nil)
   498  	errString := ""
   499  
   500  	for _, src := range sources {
   501  		chunks := make(chan extractRecord)
   502  		go func() {
   503  			defer close(chunks)
   504  			err := src.extract(context.Background(), chunks)
   505  
   506  			if err != nil {
   507  				chunks <- extractRecord{a: mustAddr(src.hash()), err: err}
   508  			}
   509  		}()
   510  
   511  		for rec := range chunks {
   512  			if rec.err != nil {
   513  				errString += fmt.Sprintf("Failed to extract %s:\n %v\n******\n\n", rec.a, rec.err)
   514  				continue
   515  			}
   516  			tw.addChunk(rec.a, rec.data)
   517  		}
   518  	}
   519  
   520  	if errString != "" {
   521  		return addr{}, nil, 0, fmt.Errorf(errString)
   522  	}
   523  
   524  	tableSize, name, err := tw.finish()
   525  
   526  	if err != nil {
   527  		return addr{}, nil, 0, err
   528  	}
   529  
   530  	return name, buff[:tableSize], chunkCount, nil
   531  }
   532  
   533  func (ftp fakeTablePersister) Open(ctx context.Context, name addr, chunkCount uint32, stats *Stats) (chunkSource, error) {
   534  	ftp.mu.RLock()
   535  	defer ftp.mu.RUnlock()
   536  	return chunkSourceAdapter{ftp.sources[name], name}, nil
   537  }
   538  
   539  func (ftp fakeTablePersister) PruneTableFiles(_ context.Context, _ manifestContents) error {
   540  	return chunks.ErrUnsupportedOperation
   541  }