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