github.com/dolthub/dolt/go@v0.40.5-0.20240520175717-68db7794bea6/store/nbs/file_manifest_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  	"os"
    27  	"os/exec"
    28  	"path/filepath"
    29  	"runtime"
    30  	"strings"
    31  	"testing"
    32  
    33  	"github.com/stretchr/testify/assert"
    34  	"github.com/stretchr/testify/require"
    35  
    36  	"github.com/dolthub/dolt/go/libraries/utils/file"
    37  	"github.com/dolthub/dolt/go/store/constants"
    38  	"github.com/dolthub/dolt/go/store/hash"
    39  )
    40  
    41  func makeFileManifestTempDir(t *testing.T) fileManifest {
    42  	dir, err := os.MkdirTemp("", "")
    43  	require.NoError(t, err)
    44  	fm, err := getFileManifest(context.Background(), dir, asyncFlush)
    45  	require.NoError(t, err)
    46  	return fm.(fileManifest)
    47  }
    48  
    49  func TestFileManifestLoadIfExists(t *testing.T) {
    50  	assert := assert.New(t)
    51  	fm := makeFileManifestTempDir(t)
    52  	defer file.RemoveAll(fm.dir)
    53  	stats := &Stats{}
    54  
    55  	exists, upstream, err := fm.ParseIfExists(context.Background(), stats, nil)
    56  	require.NoError(t, err)
    57  	assert.False(exists)
    58  
    59  	// Simulate another process writing a manifest (with an old Noms version).
    60  	jerk := computeAddr([]byte("jerk"))
    61  	newRoot := hash.Of([]byte("new root"))
    62  	tableName := hash.Of([]byte("table1"))
    63  	gcGen := hash.Hash{}
    64  	m := strings.Join([]string{StorageVersion, "0", jerk.String(), newRoot.String(), gcGen.String(), tableName.String(), "0"}, ":")
    65  	err = clobberManifest(fm.dir, m)
    66  	require.NoError(t, err)
    67  
    68  	// ParseIfExists should now reflect the manifest written above.
    69  	exists, upstream, err = fm.ParseIfExists(context.Background(), stats, nil)
    70  	require.NoError(t, err)
    71  	assert.True(exists)
    72  	assert.Equal("0", upstream.nbfVers)
    73  	assert.Equal(jerk, upstream.lock)
    74  	assert.Equal(newRoot, upstream.root)
    75  	if assert.Len(upstream.specs, 1) {
    76  		assert.Equal(tableName.String(), upstream.specs[0].name.String())
    77  		assert.Equal(uint32(0), upstream.specs[0].chunkCount)
    78  	}
    79  }
    80  
    81  func TestFileManifestUpdateWontClobberOldVersion(t *testing.T) {
    82  	assert := assert.New(t)
    83  	fm := makeFileManifestTempDir(t)
    84  	defer file.RemoveAll(fm.dir)
    85  	stats := &Stats{}
    86  
    87  	// Simulate another process having already put old Noms data in dir/.
    88  	m := strings.Join([]string{StorageVersion, "0", hash.Hash{}.String(), hash.Hash{}.String(), hash.Hash{}.String()}, ":")
    89  	err := clobberManifest(fm.dir, m)
    90  	require.NoError(t, err)
    91  
    92  	_, err = fm.Update(context.Background(), hash.Hash{}, manifestContents{}, stats, nil)
    93  	assert.Error(err)
    94  }
    95  
    96  func TestFileManifestUpdateEmpty(t *testing.T) {
    97  	assert := assert.New(t)
    98  	fm := makeFileManifestTempDir(t)
    99  	defer file.RemoveAll(fm.dir)
   100  	stats := &Stats{}
   101  
   102  	l := computeAddr([]byte{0x01})
   103  	upstream, err := fm.Update(context.Background(), hash.Hash{}, manifestContents{nbfVers: constants.FormatLD1String, lock: l}, stats, nil)
   104  	require.NoError(t, err)
   105  	assert.Equal(l, upstream.lock)
   106  	assert.True(upstream.root.IsEmpty())
   107  	assert.Empty(upstream.specs)
   108  
   109  	fm2, err := getFileManifest(context.Background(), fm.dir, asyncFlush) // Open existent, but empty manifest
   110  	require.NoError(t, err)
   111  	exists, upstream, err := fm2.ParseIfExists(context.Background(), stats, nil)
   112  	require.NoError(t, err)
   113  	assert.True(exists)
   114  	assert.Equal(l, upstream.lock)
   115  	assert.True(upstream.root.IsEmpty())
   116  	assert.Empty(upstream.specs)
   117  
   118  	l2 := computeAddr([]byte{0x02})
   119  	upstream, err = fm2.Update(context.Background(), l, manifestContents{nbfVers: constants.FormatLD1String, lock: l2}, stats, nil)
   120  	require.NoError(t, err)
   121  	assert.Equal(l2, upstream.lock)
   122  	assert.True(upstream.root.IsEmpty())
   123  	assert.Empty(upstream.specs)
   124  }
   125  
   126  func TestFileManifestUpdate(t *testing.T) {
   127  	assert := assert.New(t)
   128  	fm := makeFileManifestTempDir(t)
   129  	defer file.RemoveAll(fm.dir)
   130  	stats := &Stats{}
   131  
   132  	// First, test winning the race against another process.
   133  	contents := manifestContents{
   134  		nbfVers: constants.FormatLD1String,
   135  		lock:    computeAddr([]byte("locker")),
   136  		root:    hash.Of([]byte("new root")),
   137  		specs:   []tableSpec{{computeAddr([]byte("a")), 3}},
   138  	}
   139  	upstream, err := fm.Update(context.Background(), hash.Hash{}, contents, stats, func() error {
   140  		// This should fail to get the lock, and therefore _not_ clobber the manifest. So the Update should succeed.
   141  		lock := computeAddr([]byte("nolock"))
   142  		newRoot2 := hash.Of([]byte("noroot"))
   143  		gcGen := hash.Hash{}
   144  		m := strings.Join([]string{StorageVersion, constants.FormatLD1String, lock.String(), newRoot2.String(), gcGen.String()}, ":")
   145  		b, err := tryClobberManifest(fm.dir, m)
   146  		require.NoError(t, err, string(b))
   147  		return nil
   148  	})
   149  	require.NoError(t, err)
   150  	assert.Equal(contents.lock, upstream.lock)
   151  	assert.Equal(contents.root, upstream.root)
   152  	assert.Equal(contents.specs, upstream.specs)
   153  
   154  	// Now, test the case where the optimistic lock fails, and someone else updated the root since last we checked.
   155  	contents2 := manifestContents{lock: computeAddr([]byte("locker 2")), root: hash.Of([]byte("new root 2")), nbfVers: constants.FormatLD1String}
   156  	upstream, err = fm.Update(context.Background(), hash.Hash{}, contents2, stats, nil)
   157  	require.NoError(t, err)
   158  	assert.Equal(contents.lock, upstream.lock)
   159  	assert.Equal(contents.root, upstream.root)
   160  	assert.Equal(contents.specs, upstream.specs)
   161  	upstream, err = fm.Update(context.Background(), upstream.lock, contents2, stats, nil)
   162  	require.NoError(t, err)
   163  	assert.Equal(contents2.lock, upstream.lock)
   164  	assert.Equal(contents2.root, upstream.root)
   165  	assert.Empty(upstream.specs)
   166  
   167  	// Now, test the case where the optimistic lock fails because someone else updated only the tables since last we checked
   168  	jerkLock := computeAddr([]byte("jerk"))
   169  	tableName := computeAddr([]byte("table1"))
   170  	gcGen := hash.Hash{}
   171  	m := strings.Join([]string{StorageVersion, constants.FormatLD1String, jerkLock.String(), contents2.root.String(), gcGen.String(), tableName.String(), "1"}, ":")
   172  	err = clobberManifest(fm.dir, m)
   173  	require.NoError(t, err)
   174  
   175  	contents3 := manifestContents{lock: computeAddr([]byte("locker 3")), root: hash.Of([]byte("new root 3")), nbfVers: constants.FormatLD1String}
   176  	upstream, err = fm.Update(context.Background(), upstream.lock, contents3, stats, nil)
   177  	require.NoError(t, err)
   178  	assert.Equal(jerkLock, upstream.lock)
   179  	assert.Equal(contents2.root, upstream.root)
   180  	assert.Equal([]tableSpec{{tableName, 1}}, upstream.specs)
   181  }
   182  
   183  // tryClobberManifest simulates another process trying to access dir/manifestFileName concurrently. To avoid deadlock, it does a non-blocking lock of dir/lockFileName. If it can get the lock, it clobbers the manifest.
   184  func tryClobberManifest(dir, contents string) ([]byte, error) {
   185  	return runClobber(dir, contents)
   186  }
   187  
   188  // clobberManifest simulates another process writing dir/manifestFileName concurrently. It ignores the lock file, so it's up to the caller to ensure correctness.
   189  func clobberManifest(dir, contents string) error {
   190  	if err := os.WriteFile(filepath.Join(dir, lockFileName), nil, 0666); err != nil {
   191  		return err
   192  	}
   193  	return os.WriteFile(filepath.Join(dir, manifestFileName), []byte(contents), 0666)
   194  }
   195  
   196  func runClobber(dir, contents string) ([]byte, error) {
   197  	_, filename, _, _ := runtime.Caller(1)
   198  	clobber := filepath.Join(filepath.Dir(filename), "test/manifest_clobber.go")
   199  	mkPath := func(f string) string {
   200  		return filepath.Join(dir, f)
   201  	}
   202  
   203  	c := exec.Command("go", "run", clobber, mkPath(lockFileName), mkPath(manifestFileName), contents)
   204  	return c.CombinedOutput()
   205  }