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 }