|
1 | 1 | #include "replication/rdma_log_entries_parser.h" |
2 | 2 |
|
3 | 3 | #include <boost/filesystem.hpp> |
| 4 | +#include <arpa/inet.h> |
4 | 5 | #include <fstream> |
5 | 6 | #include <iterator> |
6 | 7 | #include <string> |
@@ -536,4 +537,96 @@ TEST(rdma_log_entries_parser_test, partial_blob_body_keeps_reading_until_remaini |
536 | 537 | boost::filesystem::remove_all(receiver_location); |
537 | 538 | } |
538 | 539 |
|
| 540 | +TEST(rdma_log_entries_parser_test, incomplete_blob_body_keeps_partial_file_and_message_incomplete) { |
| 541 | + // Simulate a transfer that stops before all BLOB body bytes arrive. The |
| 542 | + // parser may leave the partially written BLOB file on disk, but the |
| 543 | + // LOG_ENTRY message must stay incomplete so no WAL entry can reference it. |
| 544 | + static constexpr const char* sender_location = "/tmp/rdma_log_entries_parser_incomplete_sender_test"; |
| 545 | + static constexpr const char* receiver_location = "/tmp/rdma_log_entries_parser_incomplete_receiver_test"; |
| 546 | + boost::filesystem::remove_all(sender_location); |
| 547 | + boost::filesystem::remove_all(receiver_location); |
| 548 | + |
| 549 | + limestone::api::configuration sender_conf{}; |
| 550 | + sender_conf.set_data_location(sender_location); |
| 551 | + limestone::api::datastore_test sender_datastore{sender_conf}; |
| 552 | + |
| 553 | + limestone::api::configuration receiver_conf{}; |
| 554 | + receiver_conf.set_data_location(receiver_location); |
| 555 | + limestone::api::datastore_test receiver_datastore{receiver_conf}; |
| 556 | + |
| 557 | + constexpr limestone::api::blob_id_type blob_id = 6001; |
| 558 | + std::string const blob_body = "incomplete-blob-body"; |
| 559 | + auto sender_blob_path = sender_datastore.get_blob_file(blob_id).path(); |
| 560 | + boost::filesystem::create_directories(sender_blob_path.parent_path()); |
| 561 | + std::ofstream(sender_blob_path.string(), std::ios::binary) << blob_body; |
| 562 | + |
| 563 | + message_log_entries original{905}; |
| 564 | + original.add_normal_with_blob(3, "key", "value", {4, 5}, {blob_id}); |
| 565 | + |
| 566 | + blob_socket_io io("", sender_datastore); |
| 567 | + original.send_body(io); |
| 568 | + std::string body = io.get_out_string(); |
| 569 | + |
| 570 | + auto receiver_blob_path = receiver_datastore.get_blob_file(blob_id).path(); |
| 571 | + auto const truncated_size = body.size() - 3; |
| 572 | + |
| 573 | + rdma_log_entries_parser parser{receiver_datastore}; |
| 574 | + EXPECT_EQ(parser.consume(std::string_view{body}.substr(0, truncated_size)), truncated_size); |
| 575 | + EXPECT_FALSE(parser.complete()); |
| 576 | + EXPECT_EQ(parser.get_status(), rdma_log_entries_parser::status::reading); |
| 577 | + EXPECT_TRUE(boost::filesystem::exists(receiver_blob_path)); |
| 578 | + EXPECT_THROW([[maybe_unused]] auto msg = parser.take_message(), std::logic_error); |
| 579 | + |
| 580 | + boost::filesystem::remove_all(sender_location); |
| 581 | + boost::filesystem::remove_all(receiver_location); |
| 582 | +} |
| 583 | + |
| 584 | +TEST(rdma_log_entries_parser_test, blob_size_mismatch_larger_than_payload_leaves_partial_file_and_incomplete_message) { |
| 585 | + // Corrupt the encoded BLOB size so it is larger than the bytes actually |
| 586 | + // present in the payload. This models a size mismatch / missing trailing |
| 587 | + // chunk: the parser must keep waiting instead of completing the message |
| 588 | + // with a reference to an incomplete BLOB. |
| 589 | + static constexpr const char* sender_location = "/tmp/rdma_log_entries_parser_size_mismatch_sender_test"; |
| 590 | + static constexpr const char* receiver_location = "/tmp/rdma_log_entries_parser_size_mismatch_receiver_test"; |
| 591 | + boost::filesystem::remove_all(sender_location); |
| 592 | + boost::filesystem::remove_all(receiver_location); |
| 593 | + |
| 594 | + limestone::api::configuration sender_conf{}; |
| 595 | + sender_conf.set_data_location(sender_location); |
| 596 | + limestone::api::datastore_test sender_datastore{sender_conf}; |
| 597 | + |
| 598 | + limestone::api::configuration receiver_conf{}; |
| 599 | + receiver_conf.set_data_location(receiver_location); |
| 600 | + limestone::api::datastore_test receiver_datastore{receiver_conf}; |
| 601 | + |
| 602 | + constexpr limestone::api::blob_id_type blob_id = 6002; |
| 603 | + std::string const blob_body = "blob-size-mismatch"; |
| 604 | + auto sender_blob_path = sender_datastore.get_blob_file(blob_id).path(); |
| 605 | + boost::filesystem::create_directories(sender_blob_path.parent_path()); |
| 606 | + std::ofstream(sender_blob_path.string(), std::ios::binary) << blob_body; |
| 607 | + |
| 608 | + message_log_entries original{906}; |
| 609 | + original.add_normal_with_blob(3, "key", "value", {4, 5}, {blob_id}); |
| 610 | + |
| 611 | + blob_socket_io io("", sender_datastore); |
| 612 | + original.send_body(io); |
| 613 | + std::string body = io.get_out_string(); |
| 614 | + |
| 615 | + auto const blob_size_offset = body.size() - 1 - blob_body.size() - sizeof(std::uint32_t); |
| 616 | + auto encoded_size = htonl(static_cast<std::uint32_t>(blob_body.size() + 5)); |
| 617 | + std::memcpy(body.data() + static_cast<std::ptrdiff_t>(blob_size_offset), &encoded_size, sizeof(encoded_size)); |
| 618 | + |
| 619 | + auto receiver_blob_path = receiver_datastore.get_blob_file(blob_id).path(); |
| 620 | + |
| 621 | + rdma_log_entries_parser parser{receiver_datastore}; |
| 622 | + EXPECT_EQ(parser.consume(body), body.size()); |
| 623 | + EXPECT_FALSE(parser.complete()); |
| 624 | + EXPECT_EQ(parser.get_status(), rdma_log_entries_parser::status::reading); |
| 625 | + EXPECT_TRUE(boost::filesystem::exists(receiver_blob_path)); |
| 626 | + EXPECT_THROW([[maybe_unused]] auto msg = parser.take_message(), std::logic_error); |
| 627 | + |
| 628 | + boost::filesystem::remove_all(sender_location); |
| 629 | + boost::filesystem::remove_all(receiver_location); |
| 630 | +} |
| 631 | + |
539 | 632 | } // namespace limestone::testing |
0 commit comments