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 }