From c95853db84b38466559e22b33aba3811800cfc0d Mon Sep 17 00:00:00 2001 From: Alex Ainscow Date: Tue, 9 Jun 2026 15:45:25 +0100 Subject: [PATCH] test/osd: cover truncate+write EC transaction behaviour Add unit-tests to recreate the problems found by the IO Sequencer in the previous commit. Some tests here rely on frameworks added in Umbrella and as such we are keeping them independent (in their own PRs). Signed-off-by: Alex Ainscow Signed-off-by: Matty Williams Assisted-by: IBM Bob:Claude/GPT --- src/test/osd/PGBackendTestFixture.cc | 247 +++++++++++++++++++- src/test/osd/PGBackendTestFixture.h | 37 +++ src/test/osd/TestBackendBasics.cc | 337 +++++++++++++++++++++++++++ src/test/osd/test_ec_transaction.cc | 59 +++++ 4 files changed, 678 insertions(+), 2 deletions(-) diff --git a/src/test/osd/PGBackendTestFixture.cc b/src/test/osd/PGBackendTestFixture.cc index b5aca3e2aa79..f3e9d24b55c4 100644 --- a/src/test/osd/PGBackendTestFixture.cc +++ b/src/test/osd/PGBackendTestFixture.cc @@ -716,6 +716,117 @@ int PGBackendTestFixture::write( return result; } +int PGBackendTestFixture::do_write_impl( + const std::string& obj_name, + uint64_t object_size, + std::optional truncate_size, + const std::vector>& writes) +{ + hobject_t hoid = make_test_object(obj_name); + PGTransactionUPtr pg_t = std::make_unique(); + + ObjectContextRef obc = get_object_context(hoid, true); + if (obc && !obc->obs.exists) { + obc->obs.oi.size = object_size; + } + pg_t->obc_map[hoid] = obc; + + // Track outstanding write + outstanding_writes[hoid]++; + + // Apply truncate if specified + if (truncate_size.has_value()) { + pg_t->truncate(hoid, truncate_size.value()); + } + + // Apply all writes + uint64_t new_size = truncate_size.value_or(object_size); + for (const auto& [offset, data] : writes) { + bufferlist bl; + bl.append(data); + pg_t->write(hoid, offset, bl.length(), bl); + new_size = std::max(new_size, offset + bl.length()); + } + + object_stat_sum_t delta_stats; + if (new_size > object_size) { + delta_stats.num_bytes = new_size - object_size; + } else if (new_size < object_size) { + delta_stats.num_bytes = -(int64_t)(object_size - new_size); + } else { + delta_stats.num_bytes = 0; + } + + // Prior version comes from the object's current version + eversion_t prior_version = obc->obs.oi.version; + eversion_t at_version = get_next_version(); + + // Build the NEW OI + object_info_t new_oi = obc->obs.oi; + new_oi.version = at_version; + new_oi.prior_version = prior_version; + new_oi.size = new_size; + + // Encode new OI into PGTransaction + { + bufferlist oi_bl; + new_oi.encode(oi_bl, + osdmap->get_features(CEPH_ENTITY_TYPE_OSD, nullptr)); + pg_t->setattr(hoid, OI_ATTR, oi_bl); + } + + // Update OBC obs to new state BEFORE submitting + obc->obs.oi = new_oi; + + std::vector log_entries; + pg_log_entry_t entry; + entry.op = pg_log_entry_t::MODIFY; + entry.soid = hoid; + entry.version = at_version; + entry.prior_version = prior_version; + log_entries.push_back(entry); + + // Create completion lambda for write-specific cleanup + auto write_complete = [this, hoid, obc, prior_version, object_size](int r) { + // Decrement outstanding writes counter + if (outstanding_writes[hoid] > 0) { + outstanding_writes[hoid]--; + if (outstanding_writes[hoid] == 0) { + outstanding_writes.erase(hoid); + } + } + + if (r != 0 && r != -EINPROGRESS) { + // Roll back OBC on failure + obc->obs.oi.version = prior_version; + obc->obs.oi.size = object_size; + obc->attr_cache.clear(); + outstanding_writes.erase(hoid); + } + }; + + return do_transaction_and_complete( + hoid, std::move(pg_t), delta_stats, at_version, std::move(log_entries), write_complete); +} + +int PGBackendTestFixture::write( + const std::string& obj_name, + uint64_t object_size, + std::optional truncate_size, + const std::vector>& writes) +{ + int primary_osd = osdmap->get_pg_acting_primary(pgid); + ceph_assert(primary_osd >= 0); + + int result = -1; + event_loop->schedule_transaction(primary_osd, [this, &result, obj_name, object_size, truncate_size, &writes]() { + result = do_write_impl(obj_name, object_size, truncate_size, writes); + }); + event_loop->run_until_idle(); + + return result; +} + int PGBackendTestFixture::read_object( const std::string& obj_name, uint64_t offset, @@ -779,6 +890,124 @@ int PGBackendTestFixture::read_object( } } +void PGBackendTestFixture::visualize_miscompare( + const std::string& obj_name, + const char* expected_buf, + const char* read_buf, + size_t size, + const std::string& phase) +{ + bool mismatch_found = false; + size_t first_mismatch = 0; + size_t last_mismatch = 0; + + // Find mismatches + for (size_t i = 0; i < size; i++) { + if (read_buf[i] != expected_buf[i]) { + if (!mismatch_found) { + first_mismatch = i; + mismatch_found = true; + } + last_mismatch = i; + } + } + + if (!mismatch_found) { + return; // No mismatches to visualize + } + + std::cout << "\n=== MISCOMPARE DETECTED in " << phase << " ===" << std::endl; + std::cout << "Object: " << obj_name << std::endl; + if (pool_type == EC) { + std::cout << "Config: k=" << k << " m=" << m << " stripe_unit=" << stripe_unit << std::endl; + } else { + std::cout << "Config: Replicated pool, size=" << num_replicas << std::endl; + } + std::cout << "Miscompare range: [" << first_mismatch << ", " << last_mismatch << "]" << std::endl; + std::cout << "Total mismatches: " << (last_mismatch - first_mismatch + 1) << " bytes" << std::endl; + + // Show detailed hex+ASCII dump around first mismatch + size_t dump_start = (first_mismatch > 64) ? (first_mismatch - 64) : 0; + size_t dump_end = std::min(last_mismatch + 64, size); + + std::cout << "\nHex+ASCII dump around first mismatch [" << dump_start << ", " << dump_end << "):" << std::endl; + std::cout << "Offset Hex ASCII" << std::endl; + + std::string last_line_hex; + std::string last_line_ascii; + int repeat_count = 0; + + for (size_t i = dump_start; i < dump_end; i += 16) { + // Build hex representation + std::ostringstream hex_stream; + for (size_t j = 0; j < 16 && (i + j) < dump_end; j++) { + unsigned char c = read_buf[i + j]; + bool mismatch = (c != expected_buf[i + j]); + if (mismatch) hex_stream << "\033[1;31m"; + hex_stream << std::setw(2) << std::setfill('0') << std::hex << (int)c; + if (mismatch) hex_stream << "\033[0m"; + hex_stream << " "; + } + std::string hex_line = hex_stream.str(); + + // Build ASCII representation + std::ostringstream ascii_stream; + for (size_t j = 0; j < 16 && (i + j) < dump_end; j++) { + char c = read_buf[i + j]; + bool mismatch = (c != expected_buf[i + j]); + if (mismatch) ascii_stream << "\033[1;31m"; + ascii_stream << (isprint(c) ? c : '.'); + if (mismatch) ascii_stream << "\033[0m"; + } + std::string ascii_line = ascii_stream.str(); + + // Check if this line is identical to the last line + if (hex_line == last_line_hex && ascii_line == last_line_ascii && i > dump_start) { + repeat_count++; + continue; + } + + // Print any accumulated repeats + if (repeat_count > 0) { + std::cout << " * " << repeat_count << " identical line(s) omitted *" << std::endl; + repeat_count = 0; + } + + // Print current line + std::cout << std::setw(8) << std::setfill('0') << std::hex << i << " " + << std::setw(48) << std::left << hex_line << " " << ascii_line + << std::dec << std::endl; + + last_line_hex = hex_line; + last_line_ascii = ascii_line; + } + + // Print any remaining repeats + if (repeat_count > 0) { + std::cout << " * " << repeat_count << " identical line(s) omitted *" << std::endl; + } + + // Show expected vs read comparison + std::cout << "\nExpected vs Read (first 128 bytes of miscompare):" << std::endl; + size_t compare_len = std::min(size_t(128), last_mismatch - first_mismatch + 1); + + std::cout << "Expected: "; + for (size_t i = 0; i < compare_len; i++) { + char c = expected_buf[first_mismatch + i]; + std::cout << (isprint(c) ? c : '.'); + } + std::cout << std::endl; + + std::cout << "Read: "; + for (size_t i = 0; i < compare_len; i++) { + char c = read_buf[first_mismatch + i]; + if (c != expected_buf[first_mismatch + i]) std::cout << "\033[1;31m"; + std::cout << (isprint(c) ? c : '.'); + if (c != expected_buf[first_mismatch + i]) std::cout << "\033[0m"; + } + std::cout << std::endl; +} + void PGBackendTestFixture::verify_object( const std::string& obj_name, const std::string& expected_data, @@ -792,8 +1021,22 @@ void PGBackendTestFixture::verify_object( EXPECT_EQ(read_data.length(), expected_data.length()) << "Read data length should match"; if (read_data.length() == expected_data.length()) { - std::string read_string(read_data.c_str(), read_data.length()); - EXPECT_EQ(read_string, expected_data) << "Data should match"; + const char* read_buf = read_data.c_str(); + const char* expected_buf = expected_data.c_str(); + + // Check for mismatches + bool has_mismatch = false; + for (size_t i = 0; i < expected_data.length(); i++) { + if (read_buf[i] != expected_buf[i]) { + has_mismatch = true; + break; + } + } + + if (has_mismatch) { + visualize_miscompare(obj_name, expected_buf, read_buf, expected_data.length(), "verify_object"); + FAIL() << "Data mismatch detected"; + } } } diff --git a/src/test/osd/PGBackendTestFixture.h b/src/test/osd/PGBackendTestFixture.h index 837f67f8f536..82fc6c87119b 100644 --- a/src/test/osd/PGBackendTestFixture.h +++ b/src/test/osd/PGBackendTestFixture.h @@ -355,6 +355,12 @@ public: uint64_t offset, const std::string& data, uint64_t object_size); + + int do_write_impl( + const std::string& obj_name, + uint64_t object_size, + std::optional truncate_size, + const std::vector>& writes); int do_write_attribute_impl( const std::string& obj_name, @@ -374,6 +380,21 @@ public: const std::string& data, uint64_t object_size); + /** + * Write operation with optional truncate and multiple writes in a single transaction. + * + * @param obj_name Name of the object + * @param object_size Current size of the object + * @param truncate_size Optional truncate size (nullopt means no truncate) + * @param writes Vector of {offset, data} pairs to write + * @return Result code (0 on success, negative on error) + */ + int write( + const std::string& obj_name, + uint64_t object_size, + std::optional truncate_size, + const std::vector>& writes); + int read_object( const std::string& obj_name, uint64_t offset, @@ -394,6 +415,22 @@ public: * @param offset Offset to read from (default: 0) * @param context_msg Optional context message to append to assertion messages */ + /** + * Visualize data miscompare with hex+ASCII dump and line compression. + * + * @param obj_name Name of the object being compared + * @param expected_buf Expected data buffer + * @param read_buf Actual read data buffer + * @param size Size of both buffers + * @param phase Description of when the comparison occurred (e.g., "After shard 1 failure") + */ + void visualize_miscompare( + const std::string& obj_name, + const char* expected_buf, + const char* read_buf, + size_t size, + const std::string& phase); + void verify_object( const std::string& obj_name, const std::string& expected_data, diff --git a/src/test/osd/TestBackendBasics.cc b/src/test/osd/TestBackendBasics.cc index 36d367fa076c..2eaad6937658 100644 --- a/src/test/osd/TestBackendBasics.cc +++ b/src/test/osd/TestBackendBasics.cc @@ -384,6 +384,343 @@ TEST_P(TestBackendBasics, DirectRead) { } } +// --------------------------------------------------------------------------- +// TestBackendBasics: TruncateAndWrite +// --------------------------------------------------------------------------- + +/** + * TruncateAndWrite - test truncate to 0 followed by writes in a single transaction. + * + * This test verifies the behavior described in the failing test_ec_transaction test + * "truncate_then_write_one_shard" at a higher level using the full backend. + * + * The test: + * 1. Creates a 20k object + * 2. In one transaction, truncates to 0, then writes at: + * - chunk_size~chunk_size (e.g., 4k~4k for k=4,m=2,su=4k) + * - (chunk_size * (k+1))~chunk_size (e.g., 20k~4k) + * 3. Reads back and verifies the resulting 16k object + * + * This exercises the EC transaction planning logic for truncate+write operations + * and ensures data integrity across the operation. + */ +TEST_P(TestBackendBasics, TruncateAndWrite) { + const auto& param = GetParam().write_read; + const auto& backend_config = GetParam().backend; + + // Skip test for non-EC backends - truncate behavior is different + if (backend_config.pool_type != EC) { + GTEST_SKIP() << "TruncateAndWrite test only applies to EC backends"; + } + + std::string obj_name = "test_truncate_write_" + backend_config.label + "_" + param.label; + + // Step 1: Create a 20k object + const size_t initial_size = (2 * k + 1) * stripe_unit; + std::string initial_data(initial_size, 'X'); + + int result = create_and_write(obj_name, initial_data); + EXPECT_EQ(result, 0) << param.label << " initial write should complete successfully"; + verify_object(obj_name, initial_data, 0, initial_size); + + // Step 2: In one transaction, truncate to 0 and write at two offsets + uint64_t first_write_offset = stripe_unit; + uint64_t second_write_offset = stripe_unit * (k + 1); + uint64_t final_size = second_write_offset + stripe_unit; + + result = write( + obj_name, + initial_size, + 0, // truncate to 0 + { + {first_write_offset, std::string(stripe_unit, 'A')}, + {second_write_offset, std::string(stripe_unit, 'B')} + } + ); + + EXPECT_EQ(result, 0) << param.label << " truncate+write transaction should complete successfully"; + + // Step 3: Build expected data and verify + std::string expected_data(final_size, '\0'); + for (size_t i = first_write_offset; i < first_write_offset + stripe_unit; i++) { + expected_data[i] = 'A'; + } + for (size_t i = second_write_offset; i < second_write_offset + stripe_unit; i++) { + expected_data[i] = 'B'; + } + + verify_object(obj_name, expected_data, 0, final_size); + + // Clean up + auto* primary_listener = get_primary_listener(); + if (primary_listener) { + primary_listener->sent_messages.clear(); + } +} + +// --------------------------------------------------------------------------- +// TestBackendBasics: TruncateExpandAndWrite +// --------------------------------------------------------------------------- + +/** + * TruncateExpandAndWrite - test truncate-expand followed by write in a single transaction. + * + * This test verifies the behavior when an object is expanded via truncate and then + * written to in the same transaction. This is different from TruncateAndWrite which + * truncates to 0 (shrinking the object). + * + * The test: + * 1. Creates an 8k object + * 2. In one transaction, truncates to 22k (expanding), then writes 2k at offset 2k + * 3. Reads back and verifies the resulting object + * + * Expected result is an object with: + * - [0, 2k): original 'X' data (preserved) + * - [2k, 4k): 'A' characters (new write) + * - [4k, 8k): original 'X' data (preserved) + * - [8k, 22k): zeros (expanded region from truncate) + * + * This exercises the EC transaction planning logic for truncate-expand+write operations + * and ensures data integrity across the operation. + */ +TEST_P(TestBackendBasics, TruncateExpandAndWrite) { + const auto& param = GetParam().write_read; + const auto& backend_config = GetParam().backend; + + // Skip test for non-EC backends - truncate behavior is different + if (backend_config.pool_type != EC) { + GTEST_SKIP() << "TruncateExpandAndWrite test only applies to EC backends"; + } + + std::string obj_name = "test_truncate_expand_write_" + backend_config.label + "_" + param.label; + + // Step 1: Create an 8k object (2 * stripe_unit for k=4) + const size_t initial_size = 2 * stripe_unit; // 8k + std::string initial_data(initial_size, 'X'); + + int result = create_and_write(obj_name, initial_data); + EXPECT_EQ(result, 0) << param.label << " initial write should complete successfully"; + verify_object(obj_name, initial_data, 0, initial_size); + + // Step 2: In one transaction, truncate to 22k and write 2k at offset 2k + const uint64_t truncate_size = (k + 1) * stripe_unit + stripe_unit / 2; // 22k for k=4 + const uint64_t write_offset = stripe_unit / 2; // 2k + const uint64_t write_size = stripe_unit / 2; // 2k + + result = write( + obj_name, + initial_size, + truncate_size, // truncate to 22k (expand from 8k) + { + {write_offset, std::string(write_size, 'A')} + } + ); + + EXPECT_EQ(result, 0) << param.label << " truncate+write transaction should complete successfully"; + + // Step 3: Build expected data and verify + std::string expected_data(truncate_size, '\0'); + + // Region [0, 2k): original 'X' data preserved + for (size_t i = 0; i < write_offset; i++) { + expected_data[i] = 'X'; + } + + // Region [2k, 4k): new 'A' data from write + for (size_t i = write_offset; i < write_offset + write_size; i++) { + expected_data[i] = 'A'; + } + + // Region [4k, 8k): original 'X' data preserved + for (size_t i = write_offset + write_size; i < initial_size; i++) { + expected_data[i] = 'X'; + } + + // Region [8k, 22k): zeros (expanded region) - already initialized to '\0' + + verify_object(obj_name, expected_data, 0, truncate_size); + + // Clean up + auto* primary_listener = get_primary_listener(); + if (primary_listener) { + primary_listener->sent_messages.clear(); + } +} + +// --------------------------------------------------------------------------- +// TestBackendBasics: TruncateToChunkSizeAndWrite +// --------------------------------------------------------------------------- + +/** + * TruncateToChunkSizeAndWrite - test truncate to chunk_size followed by writes in a single transaction. + * + * This is a variant of TruncateAndWrite that truncates to chunk_size (4k) instead of 0. + * This tests a different code path in the EC transaction planning logic. + * + * The test: + * 1. Creates a (2*k+1)*chunk_size object (e.g., 36k for k=4) + * 2. In one transaction, truncates to chunk_size (4k), then writes at: + * - chunk_size~chunk_size (e.g., 4k~4k for k=4,m=2,su=4k) + * - (chunk_size * (k+1))~chunk_size (e.g., 20k~4k) + * 3. Reads back and verifies the resulting object + * + * Expected result is an object with: + * - [0, chunk_size): original data (preserved by truncate to 4k) + * - [chunk_size, 2*chunk_size): 'A' characters (first write) + * - [2*chunk_size, (k+1)*chunk_size): zeros (sparse region) + * - [(k+1)*chunk_size, (k+2)*chunk_size): 'B' characters (second write) + */ +TEST_P(TestBackendBasics, TruncateToChunkSizeAndWrite) { + const auto& param = GetParam().write_read; + const auto& backend_config = GetParam().backend; + + // Skip test for non-EC backends - truncate behavior is different + if (backend_config.pool_type != EC) { + GTEST_SKIP() << "TruncateToChunkSizeAndWrite test only applies to EC backends"; + } + + std::string obj_name = "test_truncate4k_write_" + backend_config.label + "_" + param.label; + + // Step 1: Create object which will be shrunk + const size_t initial_size = (2 * k + 1) * stripe_unit; + std::string initial_data(initial_size, 'X'); + + int result = create_and_write(obj_name, initial_data); + EXPECT_EQ(result, 0) << param.label << " initial write should complete successfully"; + verify_object(obj_name, initial_data, 0, initial_size); + + // Step 2: In one transaction, truncate to chunk_size and write at two offsets + uint64_t first_write_offset = stripe_unit; + uint64_t second_write_offset = stripe_unit * (k + 1); + uint64_t final_size = second_write_offset + stripe_unit; + + result = write( + obj_name, + initial_size, + stripe_unit, // truncate to chunk_size (4k) + { + {first_write_offset, std::string(stripe_unit, 'A')}, + {second_write_offset, std::string(stripe_unit, 'B')} + } + ); + + EXPECT_EQ(result, 0) << param.label << " truncate+write transaction should complete successfully"; + + // Step 3: Build expected data and verify + std::string expected_data(final_size, '\0'); + // Preserved region [0, chunk_size) with original 'X' data + for (size_t i = 0; i < stripe_unit; i++) { + expected_data[i] = 'X'; + } + // 'A' region at [chunk_size, 2*chunk_size) + for (size_t i = first_write_offset; i < first_write_offset + stripe_unit; i++) { + expected_data[i] = 'A'; + } + // 'B' region at [(k+1)*chunk_size, (k+2)*chunk_size) + for (size_t i = second_write_offset; i < second_write_offset + stripe_unit; i++) { + expected_data[i] = 'B'; + } + + verify_object(obj_name, expected_data, 0, final_size); + + // Clean up + auto* primary_listener = get_primary_listener(); + if (primary_listener) { + primary_listener->sent_messages.clear(); + } +} + + +// --------------------------------------------------------------------------- +// TestBackendBasics: TruncateToChunkSizeAndWrite +// --------------------------------------------------------------------------- + +/** + * TruncateToChunkSizeAndWrite - test truncate to chunk_size followed by writes in a single transaction. + * + * This is a variant of TruncateAndWrite that truncates to chunk_size (4k) instead of 0. + * This tests a different code path in the EC transaction planning logic. + * + * The test: + * 1. Creates a 20k object + * 2. In one transaction, truncates to chunk_size (4k), then writes at: + * - chunk_size~chunk_size (e.g., 4k~4k for k=4,m=2,su=4k) + * - (chunk_size * (k+1))~chunk_size (e.g., 20k~4k) + * 3. Reads back and verifies the resulting object + * + * Expected result is an object with: + * - [0, chunk_size): original data (preserved by truncate to 4k) + * - [chunk_size, 2*chunk_size): 'A' characters (first write) + * - [2*chunk_size, (k+1)*chunk_size): zeros (sparse region) + * - [(k+1)*chunk_size, (k+2)*chunk_size): 'B' characters (second write) + */ +TEST_P(TestBackendBasics, TruncateToChunkSizeAndWriteToSameSize) { + const auto& param = GetParam().write_read; + const auto& backend_config = GetParam().backend; + + // Skip test for non-EC backends - truncate behavior is different + if (backend_config.pool_type != EC) { + GTEST_SKIP() << "TruncateToChunkSizeAndWrite test only applies to EC backends"; + } + + std::string obj_name = "test_truncate4k_write_" + backend_config.label + "_" + param.label; + + // Step 1: Create object which will be shrunk + const size_t initial_size = (k + 2) * stripe_unit; + std::string initial_data(initial_size, 'X'); + + int result = create_and_write(obj_name, initial_data); + EXPECT_EQ(result, 0) << param.label << " initial write should complete successfully"; + verify_object(obj_name, initial_data, 0, initial_size); + + // Step 2: In one transaction, truncate to chunk_size and write at two offsets + uint64_t first_write_offset = stripe_unit; + uint64_t second_write_offset = stripe_unit * (k + 1); + uint64_t final_size = second_write_offset + stripe_unit; + + result = write( + obj_name, + initial_size, + stripe_unit, // truncate to chunk_size (4k) + { + {first_write_offset, std::string(stripe_unit, 'A')}, + {second_write_offset, std::string(stripe_unit, 'B')} + } + ); + + EXPECT_EQ(result, 0) << param.label << " truncate+write transaction should complete successfully"; + + // Step 3: Build expected data and verify + std::string expected_data(final_size, '\0'); + // Preserved region [0, chunk_size) with original 'X' data + for (size_t i = 0; i < stripe_unit; i++) { + expected_data[i] = 'X'; + } + // 'A' region at [chunk_size, 2*chunk_size) + for (size_t i = first_write_offset; i < first_write_offset + stripe_unit; i++) { + expected_data[i] = 'A'; + } + // 'B' region at [(k+1)*chunk_size, (k+2)*chunk_size) + for (size_t i = second_write_offset; i < second_write_offset + stripe_unit; i++) { + expected_data[i] = 'B'; + } + + verify_object(obj_name, expected_data, 0, final_size); + + // Step 4: Fail shard 1 (which received the write) and verify object can still be read + // For optimized EC, shard 1 is a data shard that received part of the write + simulate_multiple_osd_failures({1}); + + // Verify the object can still be read correctly via EC reconstruction + verify_object(obj_name, expected_data, 0, final_size); + + // Clean up + auto* primary_listener = get_primary_listener(); + if (primary_listener) { + primary_listener->sent_messages.clear(); + } +} + // --------------------------------------------------------------------------- // Backend configurations and size parameters // --------------------------------------------------------------------------- diff --git a/src/test/osd/test_ec_transaction.cc b/src/test/osd/test_ec_transaction.cc index 37ec6a467cc2..a5335dc28afe 100644 --- a/src/test/osd/test_ec_transaction.cc +++ b/src/test/osd/test_ec_transaction.cc @@ -503,5 +503,64 @@ TEST(ectransaction, truncate_to_stripe) { // Truncating to a whole shard - no writes needed. ECUtil::shard_extent_set_t ref_write(sinfo.get_k_plus_m()); + ASSERT_EQ(ref_write, plan.will_write); +} + +TEST(ectransaction, truncate_then_write_one_shard) { + hobject_t h; + PGTransaction::ObjectOperation op; + bufferlist a, b; + + // Simulate a sparsify operation that overwrites an existing object with data at + // specific offsets, creating a sparse pattern. + // + // Initial object is 20k, with zeros at 0~4k, 8k~4k, 16k~4k + // + // The sparsify operation writes at offsets 4k~4k and 12k~4k. + op.truncate = std::pair(0, 0); + + // First write at offset 4096, length 4KB (0~4096) + a.append_zero(4096); + op.buffer_updates.insert(4096, a.length(), PGTransaction::ObjectOperation::BufferUpdate::Write{a, 0}); + + // Second write at offset 12288 (12KB), length 4KB + b.append_zero(4096); + op.buffer_updates.insert(12288, b.length(), PGTransaction::ObjectOperation::BufferUpdate::Write{b, 0}); + + pg_pool_t pool; + pool.set_flag(pg_pool_t::FLAG_EC_OPTIMIZATIONS); + + // EC configuration: k=2, m=1, chunk_size=4096 (matching FastEC profile) + ECUtil::stripe_info_t sinfo(2, 1, 8192, &pool, std::vector(0)); + + // Set current object size to 16384 (16KB) - the object exists with this size + object_info_t oi; + oi.size = 16384; + + shard_id_set shards; + shards.insert_range(shard_id_t(0), 3); // k=2 + m=1 = 3 shards + + ECTransaction::WritePlanObj plan( + h, + op, + sinfo, + shards, + shards, + false, + 20480, // current_size + oi, + std::nullopt, + 0); + + generic_derr << "plan " << plan << dendl; + + // With truncate 0, we're starting fresh - no reads should be required + ASSERT_FALSE(plan.to_read); + + // Truncates are handled by the transaction generation. + ECUtil::shard_extent_set_t ref_write(sinfo.get_k_plus_m()); + ref_write[shard_id_t(1)].insert(0, 8192); + ref_write[shard_id_t(2)].insert(0, 8192); + ASSERT_EQ(ref_write, plan.will_write); } \ No newline at end of file -- 2.47.3