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