github.com/aigarnetwork/aigar@v0.0.0-20191115204914-d59a6eb70f8e/core/rawdb/freezer_table_test.go (about) 1 // Copyright 2018 The go-ethereum Authors 2 // Copyright 2019 The go-aigar Authors 3 // This file is part of the go-aigar library. 4 // 5 // The go-aigar library is free software: you can redistribute it and/or modify 6 // it under the terms of the GNU Lesser General Public License as published by 7 // the Free Software Foundation, either version 3 of the License, or 8 // (at your option) any later version. 9 // 10 // The go-aigar library is distributed in the hope that it will be useful, 11 // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 // GNU Lesser General Public License for more details. 14 // 15 // You should have received a copy of the GNU Lesser General Public License 16 // along with the go-aigar library. If not, see <http://www.gnu.org/licenses/>. 17 18 package rawdb 19 20 import ( 21 "bytes" 22 "fmt" 23 "math/rand" 24 "os" 25 "path/filepath" 26 "testing" 27 "time" 28 29 "github.com/AigarNetwork/aigar/metrics" 30 ) 31 32 func init() { 33 rand.Seed(time.Now().Unix()) 34 } 35 36 // Gets a chunk of data, filled with 'b' 37 func getChunk(size int, b int) []byte { 38 data := make([]byte, size) 39 for i := range data { 40 data[i] = byte(b) 41 } 42 return data 43 } 44 45 func print(t *testing.T, f *freezerTable, item uint64) { 46 a, err := f.Retrieve(item) 47 if err != nil { 48 t.Fatal(err) 49 } 50 t.Logf("db[%d] = %x\n", item, a) 51 } 52 53 // TestFreezerBasics test initializing a freezertable from scratch, writing to the table, 54 // and reading it back. 55 func TestFreezerBasics(t *testing.T) { 56 t.Parallel() 57 // set cutoff at 50 bytes 58 f, err := newCustomTable(os.TempDir(), 59 fmt.Sprintf("unittest-%d", rand.Uint64()), 60 metrics.NewMeter(), metrics.NewMeter(), metrics.NewGauge(), 50, true) 61 if err != nil { 62 t.Fatal(err) 63 } 64 defer f.Close() 65 // Write 15 bytes 255 times, results in 85 files 66 for x := 0; x < 255; x++ { 67 data := getChunk(15, x) 68 f.Append(uint64(x), data) 69 } 70 71 //print(t, f, 0) 72 //print(t, f, 1) 73 //print(t, f, 2) 74 // 75 //db[0] = 000000000000000000000000000000 76 //db[1] = 010101010101010101010101010101 77 //db[2] = 020202020202020202020202020202 78 79 for y := 0; y < 255; y++ { 80 exp := getChunk(15, y) 81 got, err := f.Retrieve(uint64(y)) 82 if err != nil { 83 t.Fatal(err) 84 } 85 if !bytes.Equal(got, exp) { 86 t.Fatalf("test %d, got \n%x != \n%x", y, got, exp) 87 } 88 } 89 // Check that we cannot read too far 90 _, err = f.Retrieve(uint64(255)) 91 if err != errOutOfBounds { 92 t.Fatal(err) 93 } 94 } 95 96 // TestFreezerBasicsClosing tests same as TestFreezerBasics, but also closes and reopens the freezer between 97 // every operation 98 func TestFreezerBasicsClosing(t *testing.T) { 99 t.Parallel() 100 // set cutoff at 50 bytes 101 var ( 102 fname = fmt.Sprintf("basics-close-%d", rand.Uint64()) 103 rm, wm, sg = metrics.NewMeter(), metrics.NewMeter(), metrics.NewGauge() 104 f *freezerTable 105 err error 106 ) 107 f, err = newCustomTable(os.TempDir(), fname, rm, wm, sg, 50, true) 108 if err != nil { 109 t.Fatal(err) 110 } 111 // Write 15 bytes 255 times, results in 85 files 112 for x := 0; x < 255; x++ { 113 data := getChunk(15, x) 114 f.Append(uint64(x), data) 115 f.Close() 116 f, err = newCustomTable(os.TempDir(), fname, rm, wm, sg, 50, true) 117 if err != nil { 118 t.Fatal(err) 119 } 120 } 121 defer f.Close() 122 123 for y := 0; y < 255; y++ { 124 exp := getChunk(15, y) 125 got, err := f.Retrieve(uint64(y)) 126 if err != nil { 127 t.Fatal(err) 128 } 129 if !bytes.Equal(got, exp) { 130 t.Fatalf("test %d, got \n%x != \n%x", y, got, exp) 131 } 132 f.Close() 133 f, err = newCustomTable(os.TempDir(), fname, rm, wm, sg, 50, true) 134 if err != nil { 135 t.Fatal(err) 136 } 137 } 138 } 139 140 // TestFreezerRepairDanglingHead tests that we can recover if index entries are removed 141 func TestFreezerRepairDanglingHead(t *testing.T) { 142 t.Parallel() 143 rm, wm, sg := metrics.NewMeter(), metrics.NewMeter(), metrics.NewGauge() 144 fname := fmt.Sprintf("dangling_headtest-%d", rand.Uint64()) 145 146 { // Fill table 147 f, err := newCustomTable(os.TempDir(), fname, rm, wm, sg, 50, true) 148 if err != nil { 149 t.Fatal(err) 150 } 151 // Write 15 bytes 255 times 152 for x := 0; x < 255; x++ { 153 data := getChunk(15, x) 154 f.Append(uint64(x), data) 155 } 156 // The last item should be there 157 if _, err = f.Retrieve(0xfe); err != nil { 158 t.Fatal(err) 159 } 160 f.Close() 161 } 162 // open the index 163 idxFile, err := os.OpenFile(filepath.Join(os.TempDir(), fmt.Sprintf("%s.ridx", fname)), os.O_RDWR, 0644) 164 if err != nil { 165 t.Fatalf("Failed to open index file: %v", err) 166 } 167 // Remove 4 bytes 168 stat, err := idxFile.Stat() 169 if err != nil { 170 t.Fatalf("Failed to stat index file: %v", err) 171 } 172 idxFile.Truncate(stat.Size() - 4) 173 idxFile.Close() 174 // Now open it again 175 { 176 f, err := newCustomTable(os.TempDir(), fname, rm, wm, sg, 50, true) 177 if err != nil { 178 t.Fatal(err) 179 } 180 // The last item should be missing 181 if _, err = f.Retrieve(0xff); err == nil { 182 t.Errorf("Expected error for missing index entry") 183 } 184 // The one before should still be there 185 if _, err = f.Retrieve(0xfd); err != nil { 186 t.Fatalf("Expected no error, got %v", err) 187 } 188 } 189 } 190 191 // TestFreezerRepairDanglingHeadLarge tests that we can recover if very many index entries are removed 192 func TestFreezerRepairDanglingHeadLarge(t *testing.T) { 193 t.Parallel() 194 rm, wm, sg := metrics.NewMeter(), metrics.NewMeter(), metrics.NewGauge() 195 fname := fmt.Sprintf("dangling_headtest-%d", rand.Uint64()) 196 197 { // Fill a table and close it 198 f, err := newCustomTable(os.TempDir(), fname, rm, wm, sg, 50, true) 199 if err != nil { 200 t.Fatal(err) 201 } 202 // Write 15 bytes 255 times 203 for x := 0; x < 0xff; x++ { 204 data := getChunk(15, x) 205 f.Append(uint64(x), data) 206 } 207 // The last item should be there 208 if _, err = f.Retrieve(f.items - 1); err == nil { 209 if err != nil { 210 t.Fatal(err) 211 } 212 } 213 f.Close() 214 } 215 // open the index 216 idxFile, err := os.OpenFile(filepath.Join(os.TempDir(), fmt.Sprintf("%s.ridx", fname)), os.O_RDWR, 0644) 217 if err != nil { 218 t.Fatalf("Failed to open index file: %v", err) 219 } 220 // Remove everything but the first item, and leave data unaligned 221 // 0-indexEntry, 1-indexEntry, corrupt-indexEntry 222 idxFile.Truncate(indexEntrySize + indexEntrySize + indexEntrySize/2) 223 idxFile.Close() 224 // Now open it again 225 { 226 f, err := newCustomTable(os.TempDir(), fname, rm, wm, sg, 50, true) 227 if err != nil { 228 t.Fatal(err) 229 } 230 // The first item should be there 231 if _, err = f.Retrieve(0); err != nil { 232 t.Fatal(err) 233 } 234 // The second item should be missing 235 if _, err = f.Retrieve(1); err == nil { 236 t.Errorf("Expected error for missing index entry") 237 } 238 // We should now be able to store items again, from item = 1 239 for x := 1; x < 0xff; x++ { 240 data := getChunk(15, ^x) 241 f.Append(uint64(x), data) 242 } 243 f.Close() 244 } 245 // And if we open it, we should now be able to read all of them (new values) 246 { 247 f, _ := newCustomTable(os.TempDir(), fname, rm, wm, sg, 50, true) 248 for y := 1; y < 255; y++ { 249 exp := getChunk(15, ^y) 250 got, err := f.Retrieve(uint64(y)) 251 if err != nil { 252 t.Fatal(err) 253 } 254 if !bytes.Equal(got, exp) { 255 t.Fatalf("test %d, got \n%x != \n%x", y, got, exp) 256 } 257 } 258 } 259 } 260 261 // TestSnappyDetection tests that we fail to open a snappy database and vice versa 262 func TestSnappyDetection(t *testing.T) { 263 t.Parallel() 264 rm, wm, sg := metrics.NewMeter(), metrics.NewMeter(), metrics.NewGauge() 265 fname := fmt.Sprintf("snappytest-%d", rand.Uint64()) 266 // Open with snappy 267 { 268 f, err := newCustomTable(os.TempDir(), fname, rm, wm, sg, 50, true) 269 if err != nil { 270 t.Fatal(err) 271 } 272 // Write 15 bytes 255 times 273 for x := 0; x < 0xff; x++ { 274 data := getChunk(15, x) 275 f.Append(uint64(x), data) 276 } 277 f.Close() 278 } 279 // Open without snappy 280 { 281 f, err := newCustomTable(os.TempDir(), fname, rm, wm, sg, 50, false) 282 if err != nil { 283 t.Fatal(err) 284 } 285 if _, err = f.Retrieve(0); err == nil { 286 f.Close() 287 t.Fatalf("expected empty table") 288 } 289 } 290 291 // Open with snappy 292 { 293 f, err := newCustomTable(os.TempDir(), fname, rm, wm, sg, 50, true) 294 if err != nil { 295 t.Fatal(err) 296 } 297 // There should be 255 items 298 if _, err = f.Retrieve(0xfe); err != nil { 299 f.Close() 300 t.Fatalf("expected no error, got %v", err) 301 } 302 } 303 304 } 305 func assertFileSize(f string, size int64) error { 306 stat, err := os.Stat(f) 307 if err != nil { 308 return err 309 } 310 if stat.Size() != size { 311 return fmt.Errorf("error, expected size %d, got %d", size, stat.Size()) 312 } 313 return nil 314 315 } 316 317 // TestFreezerRepairDanglingIndex checks that if the index has more entries than there are data, 318 // the index is repaired 319 func TestFreezerRepairDanglingIndex(t *testing.T) { 320 t.Parallel() 321 rm, wm, sg := metrics.NewMeter(), metrics.NewMeter(), metrics.NewGauge() 322 fname := fmt.Sprintf("dangling_indextest-%d", rand.Uint64()) 323 324 { // Fill a table and close it 325 f, err := newCustomTable(os.TempDir(), fname, rm, wm, sg, 50, true) 326 if err != nil { 327 t.Fatal(err) 328 } 329 // Write 15 bytes 9 times : 150 bytes 330 for x := 0; x < 9; x++ { 331 data := getChunk(15, x) 332 f.Append(uint64(x), data) 333 } 334 // The last item should be there 335 if _, err = f.Retrieve(f.items - 1); err != nil { 336 f.Close() 337 t.Fatal(err) 338 } 339 f.Close() 340 // File sizes should be 45, 45, 45 : items[3, 3, 3) 341 } 342 // Crop third file 343 fileToCrop := filepath.Join(os.TempDir(), fmt.Sprintf("%s.0002.rdat", fname)) 344 // Truncate third file: 45 ,45, 20 345 { 346 if err := assertFileSize(fileToCrop, 45); err != nil { 347 t.Fatal(err) 348 } 349 file, err := os.OpenFile(fileToCrop, os.O_RDWR, 0644) 350 if err != nil { 351 t.Fatal(err) 352 } 353 file.Truncate(20) 354 file.Close() 355 } 356 // Open db it again 357 // It should restore the file(s) to 358 // 45, 45, 15 359 // with 3+3+1 items 360 { 361 f, err := newCustomTable(os.TempDir(), fname, rm, wm, sg, 50, true) 362 if err != nil { 363 t.Fatal(err) 364 } 365 if f.items != 7 { 366 f.Close() 367 t.Fatalf("expected %d items, got %d", 7, f.items) 368 } 369 if err := assertFileSize(fileToCrop, 15); err != nil { 370 t.Fatal(err) 371 } 372 } 373 } 374 375 func TestFreezerTruncate(t *testing.T) { 376 377 t.Parallel() 378 rm, wm, sg := metrics.NewMeter(), metrics.NewMeter(), metrics.NewGauge() 379 fname := fmt.Sprintf("truncation-%d", rand.Uint64()) 380 381 { // Fill table 382 f, err := newCustomTable(os.TempDir(), fname, rm, wm, sg, 50, true) 383 if err != nil { 384 t.Fatal(err) 385 } 386 // Write 15 bytes 30 times 387 for x := 0; x < 30; x++ { 388 data := getChunk(15, x) 389 f.Append(uint64(x), data) 390 } 391 // The last item should be there 392 if _, err = f.Retrieve(f.items - 1); err != nil { 393 t.Fatal(err) 394 } 395 f.Close() 396 } 397 // Reopen, truncate 398 { 399 f, err := newCustomTable(os.TempDir(), fname, rm, wm, sg, 50, true) 400 if err != nil { 401 t.Fatal(err) 402 } 403 defer f.Close() 404 f.truncate(10) // 150 bytes 405 if f.items != 10 { 406 t.Fatalf("expected %d items, got %d", 10, f.items) 407 } 408 // 45, 45, 45, 15 -- bytes should be 15 409 if f.headBytes != 15 { 410 t.Fatalf("expected %d bytes, got %d", 15, f.headBytes) 411 } 412 413 } 414 415 } 416 417 // TestFreezerRepairFirstFile tests a head file with the very first item only half-written. 418 // That will rewind the index, and _should_ truncate the head file 419 func TestFreezerRepairFirstFile(t *testing.T) { 420 t.Parallel() 421 rm, wm, sg := metrics.NewMeter(), metrics.NewMeter(), metrics.NewGauge() 422 fname := fmt.Sprintf("truncationfirst-%d", rand.Uint64()) 423 { // Fill table 424 f, err := newCustomTable(os.TempDir(), fname, rm, wm, sg, 50, true) 425 if err != nil { 426 t.Fatal(err) 427 } 428 // Write 80 bytes, splitting out into two files 429 f.Append(0, getChunk(40, 0xFF)) 430 f.Append(1, getChunk(40, 0xEE)) 431 // The last item should be there 432 if _, err = f.Retrieve(f.items - 1); err != nil { 433 t.Fatal(err) 434 } 435 f.Close() 436 } 437 // Truncate the file in half 438 fileToCrop := filepath.Join(os.TempDir(), fmt.Sprintf("%s.0001.rdat", fname)) 439 { 440 if err := assertFileSize(fileToCrop, 40); err != nil { 441 t.Fatal(err) 442 } 443 file, err := os.OpenFile(fileToCrop, os.O_RDWR, 0644) 444 if err != nil { 445 t.Fatal(err) 446 } 447 file.Truncate(20) 448 file.Close() 449 } 450 // Reopen 451 { 452 f, err := newCustomTable(os.TempDir(), fname, rm, wm, sg, 50, true) 453 if err != nil { 454 t.Fatal(err) 455 } 456 if f.items != 1 { 457 f.Close() 458 t.Fatalf("expected %d items, got %d", 0, f.items) 459 } 460 // Write 40 bytes 461 f.Append(1, getChunk(40, 0xDD)) 462 f.Close() 463 // Should have been truncated down to zero and then 40 written 464 if err := assertFileSize(fileToCrop, 40); err != nil { 465 t.Fatal(err) 466 } 467 } 468 } 469 470 // TestFreezerReadAndTruncate tests: 471 // - we have a table open 472 // - do some reads, so files are open in readonly 473 // - truncate so those files are 'removed' 474 // - check that we did not keep the rdonly file descriptors 475 func TestFreezerReadAndTruncate(t *testing.T) { 476 t.Parallel() 477 rm, wm, sg := metrics.NewMeter(), metrics.NewMeter(), metrics.NewGauge() 478 fname := fmt.Sprintf("read_truncate-%d", rand.Uint64()) 479 { // Fill table 480 f, err := newCustomTable(os.TempDir(), fname, rm, wm, sg, 50, true) 481 if err != nil { 482 t.Fatal(err) 483 } 484 // Write 15 bytes 30 times 485 for x := 0; x < 30; x++ { 486 data := getChunk(15, x) 487 f.Append(uint64(x), data) 488 } 489 // The last item should be there 490 if _, err = f.Retrieve(f.items - 1); err != nil { 491 t.Fatal(err) 492 } 493 f.Close() 494 } 495 // Reopen and read all files 496 { 497 f, err := newCustomTable(os.TempDir(), fname, rm, wm, sg, 50, true) 498 if err != nil { 499 t.Fatal(err) 500 } 501 if f.items != 30 { 502 f.Close() 503 t.Fatalf("expected %d items, got %d", 0, f.items) 504 } 505 for y := byte(0); y < 30; y++ { 506 f.Retrieve(uint64(y)) 507 } 508 // Now, truncate back to zero 509 f.truncate(0) 510 // Write the data again 511 for x := 0; x < 30; x++ { 512 data := getChunk(15, ^x) 513 if err := f.Append(uint64(x), data); err != nil { 514 t.Fatalf("error %v", err) 515 } 516 } 517 f.Close() 518 } 519 } 520 521 func TestOffset(t *testing.T) { 522 t.Parallel() 523 rm, wm, sg := metrics.NewMeter(), metrics.NewMeter(), metrics.NewGauge() 524 fname := fmt.Sprintf("offset-%d", rand.Uint64()) 525 { // Fill table 526 f, err := newCustomTable(os.TempDir(), fname, rm, wm, sg, 40, true) 527 if err != nil { 528 t.Fatal(err) 529 } 530 // Write 6 x 20 bytes, splitting out into three files 531 f.Append(0, getChunk(20, 0xFF)) 532 f.Append(1, getChunk(20, 0xEE)) 533 534 f.Append(2, getChunk(20, 0xdd)) 535 f.Append(3, getChunk(20, 0xcc)) 536 537 f.Append(4, getChunk(20, 0xbb)) 538 f.Append(5, getChunk(20, 0xaa)) 539 f.printIndex() 540 f.Close() 541 } 542 // Now crop it. 543 { 544 // delete files 0 and 1 545 for i := 0; i < 2; i++ { 546 p := filepath.Join(os.TempDir(), fmt.Sprintf("%v.%04d.rdat", fname, i)) 547 if err := os.Remove(p); err != nil { 548 t.Fatal(err) 549 } 550 } 551 // Read the index file 552 p := filepath.Join(os.TempDir(), fmt.Sprintf("%v.ridx", fname)) 553 indexFile, err := os.OpenFile(p, os.O_RDWR, 0644) 554 if err != nil { 555 t.Fatal(err) 556 } 557 indexBuf := make([]byte, 7*indexEntrySize) 558 indexFile.Read(indexBuf) 559 560 // Update the index file, so that we store 561 // [ file = 2, offset = 4 ] at index zero 562 563 tailId := uint32(2) // First file is 2 564 itemOffset := uint32(4) // We have removed four items 565 zeroIndex := indexEntry{ 566 offset: tailId, 567 filenum: itemOffset, 568 } 569 buf := zeroIndex.marshallBinary() 570 // Overwrite index zero 571 copy(indexBuf, buf) 572 // Remove the four next indices by overwriting 573 copy(indexBuf[indexEntrySize:], indexBuf[indexEntrySize*5:]) 574 indexFile.WriteAt(indexBuf, 0) 575 // Need to truncate the moved index items 576 indexFile.Truncate(indexEntrySize * (1 + 2)) 577 indexFile.Close() 578 579 } 580 // Now open again 581 { 582 f, err := newCustomTable(os.TempDir(), fname, rm, wm, sg, 40, true) 583 if err != nil { 584 t.Fatal(err) 585 } 586 f.printIndex() 587 // It should allow writing item 6 588 f.Append(6, getChunk(20, 0x99)) 589 590 // It should be fine to fetch 4,5,6 591 if got, err := f.Retrieve(4); err != nil { 592 t.Fatal(err) 593 } else if exp := getChunk(20, 0xbb); !bytes.Equal(got, exp) { 594 t.Fatalf("expected %x got %x", exp, got) 595 } 596 if got, err := f.Retrieve(5); err != nil { 597 t.Fatal(err) 598 } else if exp := getChunk(20, 0xaa); !bytes.Equal(got, exp) { 599 t.Fatalf("expected %x got %x", exp, got) 600 } 601 if got, err := f.Retrieve(6); err != nil { 602 t.Fatal(err) 603 } else if exp := getChunk(20, 0x99); !bytes.Equal(got, exp) { 604 t.Fatalf("expected %x got %x", exp, got) 605 } 606 607 // It should error at 0, 1,2,3 608 for i := 0; i < 4; i++ { 609 if _, err := f.Retrieve(uint64(i)); err == nil { 610 t.Fatal("expected err") 611 } 612 } 613 } 614 } 615 616 // TODO (?) 617 // - test that if we remove several head-files, aswell as data last data-file, 618 // the index is truncated accordingly 619 // Right now, the freezer would fail on these conditions: 620 // 1. have data files d0, d1, d2, d3 621 // 2. remove d2,d3 622 // 623 // However, all 'normal' failure modes arising due to failing to sync() or save a file should be 624 // handled already, and the case described above can only (?) happen if an external process/user 625 // deletes files from the filesystem.