github.com/susy-go/susy-graviton@v0.0.0-20190614130430-36cddae42305/swarm/storage/localstore/localstore_test.go (about) 1 // Copyleft 2018 The susy-graviton Authors 2 // This file is part of the susy-graviton library. 3 // 4 // The susy-graviton library is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU Lesser General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // The susy-graviton library is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MSRCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU Lesser General Public License for more details. 13 // 14 // You should have received a copy of the GNU Lesser General Public License 15 // along with the susy-graviton library. If not, see <http://www.gnu.org/licenses/>. 16 17 package localstore 18 19 import ( 20 "bytes" 21 "fmt" 22 "io/ioutil" 23 "math/rand" 24 "os" 25 "sort" 26 "strconv" 27 "sync" 28 "testing" 29 "time" 30 31 ch "github.com/susy-go/susy-graviton/swarm/chunk" 32 "github.com/susy-go/susy-graviton/swarm/shed" 33 "github.com/susy-go/susy-graviton/swarm/storage" 34 "github.com/syndtr/goleveldb/leveldb" 35 ) 36 37 // TestDB validates if the chunk can be uploaded and 38 // correctly retrieved. 39 func TestDB(t *testing.T) { 40 db, cleanupFunc := newTestDB(t, nil) 41 defer cleanupFunc() 42 43 chunk := generateRandomChunk() 44 45 err := db.NewPutter(ModePutUpload).Put(chunk) 46 if err != nil { 47 t.Fatal(err) 48 } 49 50 got, err := db.NewGetter(ModeGetRequest).Get(chunk.Address()) 51 if err != nil { 52 t.Fatal(err) 53 } 54 55 if !bytes.Equal(got.Address(), chunk.Address()) { 56 t.Errorf("got address %x, want %x", got.Address(), chunk.Address()) 57 } 58 if !bytes.Equal(got.Data(), chunk.Data()) { 59 t.Errorf("got data %x, want %x", got.Data(), chunk.Data()) 60 } 61 } 62 63 // TestDB_updateGCSem tests maxParallelUpdateGC limit. 64 // This test temporary sets the limit to a low number, 65 // makes updateGC function execution time longer by 66 // setting a custom testHookUpdateGC function with a sleep 67 // and a count current and maximal number of goroutines. 68 func TestDB_updateGCSem(t *testing.T) { 69 updateGCSleep := time.Second 70 var count int 71 var max int 72 var mu sync.Mutex 73 defer setTestHookUpdateGC(func() { 74 mu.Lock() 75 // add to the count of current goroutines 76 count++ 77 if count > max { 78 // set maximal detected numbers of goroutines 79 max = count 80 } 81 mu.Unlock() 82 83 // wait for some time to ensure multiple parallel goroutines 84 time.Sleep(updateGCSleep) 85 86 mu.Lock() 87 count-- 88 mu.Unlock() 89 })() 90 91 defer func(m int) { maxParallelUpdateGC = m }(maxParallelUpdateGC) 92 maxParallelUpdateGC = 3 93 94 db, cleanupFunc := newTestDB(t, nil) 95 defer cleanupFunc() 96 97 chunk := generateRandomChunk() 98 99 err := db.NewPutter(ModePutUpload).Put(chunk) 100 if err != nil { 101 t.Fatal(err) 102 } 103 104 getter := db.NewGetter(ModeGetRequest) 105 106 // get more chunks then maxParallelUpdateGC 107 // in time shorter then updateGCSleep 108 for i := 0; i < 5; i++ { 109 _, err = getter.Get(chunk.Address()) 110 if err != nil { 111 t.Fatal(err) 112 } 113 } 114 115 if max != maxParallelUpdateGC { 116 t.Errorf("got max %v, want %v", max, maxParallelUpdateGC) 117 } 118 } 119 120 // BenchmarkNew measures the time that New function 121 // needs to initialize and count the number of key/value 122 // pairs in GC index. 123 // This benchmark generates a number of chunks, uploads them, 124 // sets them to synced state for them to enter the GC index, 125 // and measures the execution time of New function by creating 126 // new databases with the same data directory. 127 // 128 // This benchmark takes significant amount of time. 129 // 130 // Measurements on MacBook Pro (Retina, 15-inch, Mid 2014) show 131 // that New function executes around 1s for database with 1M chunks. 132 // 133 // # go test -benchmem -run=none github.com/susy-go/susy-graviton/swarm/storage/localstore -bench BenchmarkNew -v -timeout 20m 134 // goos: darwin 135 // goarch: amd64 136 // pkg: github.com/susy-go/susy-graviton/swarm/storage/localstore 137 // BenchmarkNew/1000-8 200 11672414 ns/op 9570960 B/op 10008 allocs/op 138 // BenchmarkNew/10000-8 100 14890609 ns/op 10490118 B/op 7759 allocs/op 139 // BenchmarkNew/100000-8 20 58334080 ns/op 17763157 B/op 22978 allocs/op 140 // BenchmarkNew/1000000-8 2 748595153 ns/op 45297404 B/op 253242 allocs/op 141 // PASS 142 func BenchmarkNew(b *testing.B) { 143 if testing.Short() { 144 b.Skip("skipping benchmark in short mode") 145 } 146 for _, count := range []int{ 147 1000, 148 10000, 149 100000, 150 1000000, 151 } { 152 b.Run(strconv.Itoa(count), func(b *testing.B) { 153 dir, err := ioutil.TempDir("", "localstore-new-benchmark") 154 if err != nil { 155 b.Fatal(err) 156 } 157 defer os.RemoveAll(dir) 158 baseKey := make([]byte, 32) 159 if _, err := rand.Read(baseKey); err != nil { 160 b.Fatal(err) 161 } 162 db, err := New(dir, baseKey, nil) 163 if err != nil { 164 b.Fatal(err) 165 } 166 uploader := db.NewPutter(ModePutUpload) 167 syncer := db.NewSetter(ModeSetSync) 168 for i := 0; i < count; i++ { 169 chunk := generateFakeRandomChunk() 170 err := uploader.Put(chunk) 171 if err != nil { 172 b.Fatal(err) 173 } 174 err = syncer.Set(chunk.Address()) 175 if err != nil { 176 b.Fatal(err) 177 } 178 } 179 err = db.Close() 180 if err != nil { 181 b.Fatal(err) 182 } 183 b.ResetTimer() 184 185 for n := 0; n < b.N; n++ { 186 b.StartTimer() 187 db, err := New(dir, baseKey, nil) 188 b.StopTimer() 189 190 if err != nil { 191 b.Fatal(err) 192 } 193 err = db.Close() 194 if err != nil { 195 b.Fatal(err) 196 } 197 } 198 }) 199 } 200 } 201 202 // newTestDB is a helper function that constructs a 203 // temporary database and returns a cleanup function that must 204 // be called to remove the data. 205 func newTestDB(t testing.TB, o *Options) (db *DB, cleanupFunc func()) { 206 t.Helper() 207 208 dir, err := ioutil.TempDir("", "localstore-test") 209 if err != nil { 210 t.Fatal(err) 211 } 212 cleanupFunc = func() { os.RemoveAll(dir) } 213 baseKey := make([]byte, 32) 214 if _, err := rand.Read(baseKey); err != nil { 215 t.Fatal(err) 216 } 217 db, err = New(dir, baseKey, o) 218 if err != nil { 219 cleanupFunc() 220 t.Fatal(err) 221 } 222 cleanupFunc = func() { 223 err := db.Close() 224 if err != nil { 225 t.Error(err) 226 } 227 os.RemoveAll(dir) 228 } 229 return db, cleanupFunc 230 } 231 232 // generateRandomChunk generates a valid Chunk with 233 // data size of default chunk size. 234 func generateRandomChunk() storage.Chunk { 235 return storage.GenerateRandomChunk(ch.DefaultSize) 236 } 237 238 func init() { 239 // needed for generateFakeRandomChunk 240 rand.Seed(time.Now().UnixNano()) 241 } 242 243 // generateFakeRandomChunk generates a Chunk that is not 244 // valid, but it contains a random key and a random value. 245 // This function is faster then storage.GenerateRandomChunk 246 // which generates a valid chunk. 247 // Some tests in this package do not need valid chunks, just 248 // random data, and their execution time can be decreased 249 // using this function. 250 func generateFakeRandomChunk() storage.Chunk { 251 data := make([]byte, ch.DefaultSize) 252 rand.Read(data) 253 key := make([]byte, 32) 254 rand.Read(key) 255 return storage.NewChunk(key, data) 256 } 257 258 // TestGenerateFakeRandomChunk validates that 259 // generateFakeRandomChunk returns random data by comparing 260 // two generated chunks. 261 func TestGenerateFakeRandomChunk(t *testing.T) { 262 c1 := generateFakeRandomChunk() 263 c2 := generateFakeRandomChunk() 264 addrLen := len(c1.Address()) 265 if addrLen != 32 { 266 t.Errorf("first chunk address length %v, want %v", addrLen, 32) 267 } 268 dataLen := len(c1.Data()) 269 if dataLen != ch.DefaultSize { 270 t.Errorf("first chunk data length %v, want %v", dataLen, ch.DefaultSize) 271 } 272 addrLen = len(c2.Address()) 273 if addrLen != 32 { 274 t.Errorf("second chunk address length %v, want %v", addrLen, 32) 275 } 276 dataLen = len(c2.Data()) 277 if dataLen != ch.DefaultSize { 278 t.Errorf("second chunk data length %v, want %v", dataLen, ch.DefaultSize) 279 } 280 if bytes.Equal(c1.Address(), c2.Address()) { 281 t.Error("fake chunks addresses do not differ") 282 } 283 if bytes.Equal(c1.Data(), c2.Data()) { 284 t.Error("fake chunks data bytes do not differ") 285 } 286 } 287 288 // newRetrieveIndexesTest returns a test function that validates if the right 289 // chunk values are in the retrieval indexes. 290 func newRetrieveIndexesTest(db *DB, chunk storage.Chunk, storeTimestamp, accessTimestamp int64) func(t *testing.T) { 291 return func(t *testing.T) { 292 item, err := db.retrievalDataIndex.Get(addressToItem(chunk.Address())) 293 if err != nil { 294 t.Fatal(err) 295 } 296 validateItem(t, item, chunk.Address(), chunk.Data(), storeTimestamp, 0) 297 298 // access index should not be set 299 wantErr := leveldb.ErrNotFound 300 item, err = db.retrievalAccessIndex.Get(addressToItem(chunk.Address())) 301 if err != wantErr { 302 t.Errorf("got error %v, want %v", err, wantErr) 303 } 304 } 305 } 306 307 // newRetrieveIndexesTestWithAccess returns a test function that validates if the right 308 // chunk values are in the retrieval indexes when access time must be stored. 309 func newRetrieveIndexesTestWithAccess(db *DB, chunk storage.Chunk, storeTimestamp, accessTimestamp int64) func(t *testing.T) { 310 return func(t *testing.T) { 311 item, err := db.retrievalDataIndex.Get(addressToItem(chunk.Address())) 312 if err != nil { 313 t.Fatal(err) 314 } 315 validateItem(t, item, chunk.Address(), chunk.Data(), storeTimestamp, 0) 316 317 if accessTimestamp > 0 { 318 item, err = db.retrievalAccessIndex.Get(addressToItem(chunk.Address())) 319 if err != nil { 320 t.Fatal(err) 321 } 322 validateItem(t, item, chunk.Address(), nil, 0, accessTimestamp) 323 } 324 } 325 } 326 327 // newPullIndexTest returns a test function that validates if the right 328 // chunk values are in the pull index. 329 func newPullIndexTest(db *DB, chunk storage.Chunk, storeTimestamp int64, wantError error) func(t *testing.T) { 330 return func(t *testing.T) { 331 item, err := db.pullIndex.Get(shed.Item{ 332 Address: chunk.Address(), 333 StoreTimestamp: storeTimestamp, 334 }) 335 if err != wantError { 336 t.Errorf("got error %v, want %v", err, wantError) 337 } 338 if err == nil { 339 validateItem(t, item, chunk.Address(), nil, storeTimestamp, 0) 340 } 341 } 342 } 343 344 // newPushIndexTest returns a test function that validates if the right 345 // chunk values are in the push index. 346 func newPushIndexTest(db *DB, chunk storage.Chunk, storeTimestamp int64, wantError error) func(t *testing.T) { 347 return func(t *testing.T) { 348 item, err := db.pushIndex.Get(shed.Item{ 349 Address: chunk.Address(), 350 StoreTimestamp: storeTimestamp, 351 }) 352 if err != wantError { 353 t.Errorf("got error %v, want %v", err, wantError) 354 } 355 if err == nil { 356 validateItem(t, item, chunk.Address(), nil, storeTimestamp, 0) 357 } 358 } 359 } 360 361 // newGCIndexTest returns a test function that validates if the right 362 // chunk values are in the push index. 363 func newGCIndexTest(db *DB, chunk storage.Chunk, storeTimestamp, accessTimestamp int64) func(t *testing.T) { 364 return func(t *testing.T) { 365 item, err := db.gcIndex.Get(shed.Item{ 366 Address: chunk.Address(), 367 StoreTimestamp: storeTimestamp, 368 AccessTimestamp: accessTimestamp, 369 }) 370 if err != nil { 371 t.Fatal(err) 372 } 373 validateItem(t, item, chunk.Address(), nil, storeTimestamp, accessTimestamp) 374 } 375 } 376 377 // newItemsCountTest returns a test function that validates if 378 // an index contains expected number of key/value pairs. 379 func newItemsCountTest(i shed.Index, want int) func(t *testing.T) { 380 return func(t *testing.T) { 381 var c int 382 err := i.Iterate(func(item shed.Item) (stop bool, err error) { 383 c++ 384 return 385 }, nil) 386 if err != nil { 387 t.Fatal(err) 388 } 389 if c != want { 390 t.Errorf("got %v items in index, want %v", c, want) 391 } 392 } 393 } 394 395 // newIndexGCSizeTest retruns a test function that validates if DB.gcSize 396 // value is the same as the number of items in DB.gcIndex. 397 func newIndexGCSizeTest(db *DB) func(t *testing.T) { 398 return func(t *testing.T) { 399 var want int64 400 err := db.gcIndex.Iterate(func(item shed.Item) (stop bool, err error) { 401 want++ 402 return 403 }, nil) 404 if err != nil { 405 t.Fatal(err) 406 } 407 got := db.getGCSize() 408 if got != want { 409 t.Errorf("got gc size %v, want %v", got, want) 410 } 411 } 412 } 413 414 // testIndexChunk embeds storageChunk with additional data that is stored 415 // in database. It is used for index values validations. 416 type testIndexChunk struct { 417 storage.Chunk 418 storeTimestamp int64 419 } 420 421 // testItemsOrder tests the order of chunks in the index. If sortFunc is not nil, 422 // chunks will be sorted with it before validation. 423 func testItemsOrder(t *testing.T, i shed.Index, chunks []testIndexChunk, sortFunc func(i, j int) (less bool)) { 424 newItemsCountTest(i, len(chunks))(t) 425 426 if sortFunc != nil { 427 sort.Slice(chunks, sortFunc) 428 } 429 430 var cursor int 431 err := i.Iterate(func(item shed.Item) (stop bool, err error) { 432 want := chunks[cursor].Address() 433 got := item.Address 434 if !bytes.Equal(got, want) { 435 return true, fmt.Errorf("got address %x at position %v, want %x", got, cursor, want) 436 } 437 cursor++ 438 return false, nil 439 }, nil) 440 if err != nil { 441 t.Fatal(err) 442 } 443 } 444 445 // validateItem is a helper function that checks Item values. 446 func validateItem(t *testing.T, item shed.Item, address, data []byte, storeTimestamp, accessTimestamp int64) { 447 t.Helper() 448 449 if !bytes.Equal(item.Address, address) { 450 t.Errorf("got item address %x, want %x", item.Address, address) 451 } 452 if !bytes.Equal(item.Data, data) { 453 t.Errorf("got item data %x, want %x", item.Data, data) 454 } 455 if item.StoreTimestamp != storeTimestamp { 456 t.Errorf("got item store timestamp %v, want %v", item.StoreTimestamp, storeTimestamp) 457 } 458 if item.AccessTimestamp != accessTimestamp { 459 t.Errorf("got item access timestamp %v, want %v", item.AccessTimestamp, accessTimestamp) 460 } 461 } 462 463 // setNow replaces now function and 464 // returns a function that will reset it to the 465 // value before the change. 466 func setNow(f func() int64) (reset func()) { 467 current := now 468 reset = func() { now = current } 469 now = f 470 return reset 471 } 472 473 // TestSetNow tests if setNow function changes now function 474 // correctly and if its reset function resets the original function. 475 func TestSetNow(t *testing.T) { 476 // set the current function after the test finishes 477 defer func(f func() int64) { now = f }(now) 478 479 // expected value for the unchanged function 480 var original int64 = 1 481 // expected value for the changed function 482 var changed int64 = 2 483 484 // define the original (unchanged) functions 485 now = func() int64 { 486 return original 487 } 488 489 // get the time 490 got := now() 491 492 // test if got variable is set correctly 493 if got != original { 494 t.Errorf("got now value %v, want %v", got, original) 495 } 496 497 // set the new function 498 reset := setNow(func() int64 { 499 return changed 500 }) 501 502 // get the time 503 got = now() 504 505 // test if got variable is set correctly to changed value 506 if got != changed { 507 t.Errorf("got hook value %v, want %v", got, changed) 508 } 509 510 // set the function to the original one 511 reset() 512 513 // get the time 514 got = now() 515 516 // test if got variable is set correctly to original value 517 if got != original { 518 t.Errorf("got hook value %v, want %v", got, original) 519 } 520 }