Skip to content

Commit 1f2b775

Browse files
committed
test(replication): add rdma blob error-path coverage
Add regression tests for incomplete RDMA BLOB bodies and BLOB size mismatches. These cases leave partial BLOB files on disk but must not complete the LOG_ENTRY message, so no WAL entry can reference the incomplete BLOB. Also cover multiple BLOB LOG_ENTRY messages in one RDMA receiver stream.
1 parent baddafb commit 1f2b775

2 files changed

Lines changed: 168 additions & 0 deletions

File tree

test/limestone/replication/rdma_log_entries_parser_test.cpp

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#include "replication/rdma_log_entries_parser.h"
22

33
#include <boost/filesystem.hpp>
4+
#include <arpa/inet.h>
45
#include <fstream>
56
#include <iterator>
67
#include <string>
@@ -536,4 +537,96 @@ TEST(rdma_log_entries_parser_test, partial_blob_body_keeps_reading_until_remaini
536537
boost::filesystem::remove_all(receiver_location);
537538
}
538539

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+
539632
} // namespace limestone::testing

test/limestone/replication/rdma_log_entries_receiver_test.cpp

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,81 @@ TEST(rdma_log_entries_receiver_test, streams_blob_message_split_across_inputs) {
183183
boost::filesystem::remove_all(receiver_location);
184184
}
185185

186+
TEST(rdma_log_entries_receiver_test, parses_multiple_blob_messages_in_one_session) {
187+
// A persistent session may send more than one LOG_ENTRY message containing
188+
// BLOBs. Feed two serialized BLOB messages as one RDMA byte stream, split
189+
// across arbitrary consume() boundaries, and verify that both messages and
190+
// both replica BLOB files are completed independently.
191+
static constexpr const char* sender_location = "/tmp/rdma_log_entries_receiver_multi_blob_sender_test";
192+
static constexpr const char* receiver_location = "/tmp/rdma_log_entries_receiver_multi_blob_receiver_test";
193+
boost::filesystem::remove_all(sender_location);
194+
boost::filesystem::remove_all(receiver_location);
195+
196+
limestone::api::configuration sender_conf{};
197+
sender_conf.set_data_location(sender_location);
198+
limestone::api::datastore_test sender_datastore{sender_conf};
199+
200+
limestone::api::configuration receiver_conf{};
201+
receiver_conf.set_data_location(receiver_location);
202+
limestone::api::datastore_test receiver_datastore{receiver_conf};
203+
204+
constexpr limestone::api::blob_id_type first_blob_id = 9877;
205+
constexpr limestone::api::blob_id_type second_blob_id = 9878;
206+
std::string const first_blob_body = "first-session-blob";
207+
std::string const second_blob_body = "second-session-blob";
208+
209+
auto first_sender_blob_path = sender_datastore.get_blob_file(first_blob_id).path();
210+
boost::filesystem::create_directories(first_sender_blob_path.parent_path());
211+
std::ofstream(first_sender_blob_path.string(), std::ios::binary) << first_blob_body;
212+
213+
auto second_sender_blob_path = sender_datastore.get_blob_file(second_blob_id).path();
214+
boost::filesystem::create_directories(second_sender_blob_path.parent_path());
215+
std::ofstream(second_sender_blob_path.string(), std::ios::binary) << second_blob_body;
216+
217+
message_log_entries first{1202};
218+
first.set_session_begin_flag(true);
219+
first.add_normal_with_blob(3, "blob-key-1", "blob-value-1", {4, 5}, {first_blob_id});
220+
221+
message_log_entries second{1202};
222+
second.set_session_end_flag(true);
223+
second.add_normal_with_blob(4, "blob-key-2", "blob-value-2", {4, 6}, {second_blob_id});
224+
225+
std::string bytes = serialize_message_with_blobs(first, sender_datastore)
226+
+ serialize_message_with_blobs(second, sender_datastore);
227+
228+
rdma_log_entries_receiver receiver{receiver_datastore};
229+
std::size_t consumed = 0;
230+
while (consumed < bytes.size()) {
231+
std::size_t chunk = consumed % 2 == 0 ? 7u : 3u;
232+
chunk = std::min(chunk, bytes.size() - consumed);
233+
consumed += receiver.consume(std::string_view{bytes}.substr(consumed, chunk));
234+
}
235+
236+
ASSERT_EQ(receiver.message_count(), 2u);
237+
238+
auto first_received = receiver.take_message();
239+
EXPECT_EQ(first_received->get_epoch_id(), 1202);
240+
EXPECT_TRUE(first_received->has_session_begin_flag());
241+
ASSERT_EQ(first_received->get_entries().size(), 1u);
242+
EXPECT_EQ(first_received->get_entries()[0].blob_ids,
243+
std::vector<limestone::api::blob_id_type>{first_blob_id});
244+
245+
auto second_received = receiver.take_message();
246+
EXPECT_EQ(second_received->get_epoch_id(), 1202);
247+
EXPECT_TRUE(second_received->has_session_end_flag());
248+
ASSERT_EQ(second_received->get_entries().size(), 1u);
249+
EXPECT_EQ(second_received->get_entries()[0].blob_ids,
250+
std::vector<limestone::api::blob_id_type>{second_blob_id});
251+
252+
auto first_receiver_blob_path = receiver_datastore.get_blob_file(first_blob_id).path();
253+
auto second_receiver_blob_path = receiver_datastore.get_blob_file(second_blob_id).path();
254+
EXPECT_EQ(read_file(first_receiver_blob_path), first_blob_body);
255+
EXPECT_EQ(read_file(second_receiver_blob_path), second_blob_body);
256+
257+
boost::filesystem::remove_all(sender_location);
258+
boost::filesystem::remove_all(receiver_location);
259+
}
260+
186261
TEST(rdma_log_entries_receiver_test, leaves_partial_next_message_in_progress_after_completed_message) {
187262
// If the input ends after the next message type byte, the previous message
188263
// should still be available while the receiver remembers that a new message

0 commit comments

Comments
 (0)