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 }