|
14 | 14 |
|
15 | 15 | #include <gtest/gtest.h> |
16 | 16 |
|
| 17 | +#include <map> |
17 | 18 | #include <memory> |
| 19 | +#include <stdexcept> |
18 | 20 | #include <string_view> |
19 | 21 |
|
20 | 22 | namespace { |
@@ -511,6 +513,166 @@ TEST_F(DBIteratorTest, TombstoneAfterValues) { |
511 | 513 | EXPECT_FALSE(it->valid()); |
512 | 514 | } |
513 | 515 |
|
| 516 | +// An iterator over an in-memory map that can be primed to throw on the |
| 517 | +// next mutating operation. Used to model the cloud_io failure path where |
| 518 | +// a deeper iterator (e.g. a two-level iterator's data block) raises an |
| 519 | +// exception mid-operation, as observed in the AT reactor-1 SIGSEGV. |
| 520 | +class throwing_iterator : public lsm::internal::iterator { |
| 521 | +public: |
| 522 | + explicit throwing_iterator(std::map<lsm::internal::key, iobuf> data) |
| 523 | + : _data(std::move(data)) |
| 524 | + , _it(_data.end()) {} |
| 525 | + |
| 526 | + // Arm the iterator to fail the next mutating call exactly once. |
| 527 | + void fail_next() { _fail_pending = true; } |
| 528 | + |
| 529 | + bool valid() const override { return _it != _data.end(); } |
| 530 | + |
| 531 | + ss::future<> seek_to_first() override { |
| 532 | + if (consume_failure()) { |
| 533 | + return make_failure(); |
| 534 | + } |
| 535 | + _it = _data.begin(); |
| 536 | + return ss::now(); |
| 537 | + } |
| 538 | + |
| 539 | + ss::future<> seek_to_last() override { |
| 540 | + if (consume_failure()) { |
| 541 | + return make_failure(); |
| 542 | + } |
| 543 | + _it = _data.empty() ? _data.end() : std::prev(_data.end()); |
| 544 | + return ss::now(); |
| 545 | + } |
| 546 | + |
| 547 | + ss::future<> seek(lsm::internal::key_view target) override { |
| 548 | + if (consume_failure()) { |
| 549 | + return make_failure(); |
| 550 | + } |
| 551 | + _it = _data.lower_bound(lsm::internal::key(target)); |
| 552 | + return ss::now(); |
| 553 | + } |
| 554 | + |
| 555 | + ss::future<> next() override { |
| 556 | + if (consume_failure()) { |
| 557 | + return make_failure(); |
| 558 | + } |
| 559 | + if (_it != _data.end()) { |
| 560 | + ++_it; |
| 561 | + } |
| 562 | + return ss::now(); |
| 563 | + } |
| 564 | + |
| 565 | + ss::future<> prev() override { |
| 566 | + if (consume_failure()) { |
| 567 | + return make_failure(); |
| 568 | + } |
| 569 | + if (_it == _data.begin()) { |
| 570 | + _it = _data.end(); |
| 571 | + } else if (_it != _data.end()) { |
| 572 | + --_it; |
| 573 | + } else if (!_data.empty()) { |
| 574 | + _it = std::prev(_data.end()); |
| 575 | + } |
| 576 | + return ss::now(); |
| 577 | + } |
| 578 | + |
| 579 | + lsm::internal::key_view key() override { return _it->first; } |
| 580 | + iobuf value() override { return _it->second.copy(); } |
| 581 | + |
| 582 | +private: |
| 583 | + bool consume_failure() { |
| 584 | + if (_fail_pending) { |
| 585 | + _fail_pending = false; |
| 586 | + return true; |
| 587 | + } |
| 588 | + return false; |
| 589 | + } |
| 590 | + static ss::future<> make_failure() { |
| 591 | + return ss::make_exception_future<>( |
| 592 | + std::runtime_error("simulated cloud_io download failure")); |
| 593 | + } |
| 594 | + |
| 595 | + std::map<lsm::internal::key, iobuf> _data; |
| 596 | + std::map<lsm::internal::key, iobuf>::iterator _it; |
| 597 | + bool _fail_pending = false; |
| 598 | +}; |
| 599 | + |
| 600 | +class DBIteratorExceptionSafetyTest : public testing::Test { |
| 601 | +public: |
| 602 | + void SetUp() override { |
| 603 | + _options = ss::make_lw_shared<lsm::internal::options>(); |
| 604 | + std::map<lsm::internal::key, iobuf> data; |
| 605 | + for (auto k : {"a", "b", "c"}) { |
| 606 | + auto ikey = lsm::internal::key::encode( |
| 607 | + {.key = lsm::user_key_view{k}, |
| 608 | + .seqno = 100_seqno, |
| 609 | + .type = lsm::internal::value_type::value}); |
| 610 | + data.emplace(std::move(ikey), iobuf::from(k)); |
| 611 | + } |
| 612 | + auto inner = std::make_unique<throwing_iterator>(std::move(data)); |
| 613 | + _inner = inner.get(); |
| 614 | + _it = lsm::db::create_db_iterator( |
| 615 | + std::move(inner), |
| 616 | + lsm::internal::sequence_number::max(), |
| 617 | + _options, |
| 618 | + [](lsm::internal::key_view) { return ss::now(); }); |
| 619 | + } |
| 620 | + |
| 621 | +protected: |
| 622 | + ss::lw_shared_ptr<lsm::internal::options> _options; |
| 623 | + throwing_iterator* _inner = nullptr; |
| 624 | + std::unique_ptr<lsm::internal::iterator> _it; |
| 625 | +}; |
| 626 | + |
| 627 | +// If seek throws partway through, valid() must report false. Previously |
| 628 | +// _valid retained whatever it had been before the call, allowing a stale |
| 629 | +// "true" to escape and a downstream key() to dereference a half-loaded |
| 630 | +// underlying iterator (the AT reactor-1 SIGSEGV). |
| 631 | +TEST_F(DBIteratorExceptionSafetyTest, SeekThrowLeavesIteratorInvalid) { |
| 632 | + _it->seek("a"_seek_key).get(); |
| 633 | + ASSERT_TRUE(_it->valid()); |
| 634 | + |
| 635 | + _inner->fail_next(); |
| 636 | + EXPECT_THROW(_it->seek("b"_seek_key).get(), std::runtime_error); |
| 637 | + EXPECT_FALSE(_it->valid()); |
| 638 | +} |
| 639 | + |
| 640 | +TEST_F(DBIteratorExceptionSafetyTest, SeekToFirstThrowLeavesIteratorInvalid) { |
| 641 | + _it->seek_to_first().get(); |
| 642 | + ASSERT_TRUE(_it->valid()); |
| 643 | + |
| 644 | + _inner->fail_next(); |
| 645 | + EXPECT_THROW(_it->seek_to_first().get(), std::runtime_error); |
| 646 | + EXPECT_FALSE(_it->valid()); |
| 647 | +} |
| 648 | + |
| 649 | +TEST_F(DBIteratorExceptionSafetyTest, SeekToLastThrowLeavesIteratorInvalid) { |
| 650 | + _it->seek_to_last().get(); |
| 651 | + ASSERT_TRUE(_it->valid()); |
| 652 | + |
| 653 | + _inner->fail_next(); |
| 654 | + EXPECT_THROW(_it->seek_to_last().get(), std::runtime_error); |
| 655 | + EXPECT_FALSE(_it->valid()); |
| 656 | +} |
| 657 | + |
| 658 | +TEST_F(DBIteratorExceptionSafetyTest, NextThrowLeavesIteratorInvalid) { |
| 659 | + _it->seek_to_first().get(); |
| 660 | + ASSERT_TRUE(_it->valid()); |
| 661 | + |
| 662 | + _inner->fail_next(); |
| 663 | + EXPECT_THROW(_it->next().get(), std::runtime_error); |
| 664 | + EXPECT_FALSE(_it->valid()); |
| 665 | +} |
| 666 | + |
| 667 | +TEST_F(DBIteratorExceptionSafetyTest, PrevThrowLeavesIteratorInvalid) { |
| 668 | + _it->seek_to_last().get(); |
| 669 | + ASSERT_TRUE(_it->valid()); |
| 670 | + |
| 671 | + _inner->fail_next(); |
| 672 | + EXPECT_THROW(_it->prev().get(), std::runtime_error); |
| 673 | + EXPECT_FALSE(_it->valid()); |
| 674 | +} |
| 675 | + |
514 | 676 | TEST_F(DBIteratorTest, MultipleVersionsWithSnapshot) { |
515 | 677 | add_value("key", "v1", 100_seqno); |
516 | 678 | add_value("key", "v2", 200_seqno); |
|
0 commit comments