From: Matty Williams Date: Thu, 11 Jun 2026 10:59:48 +0000 (+0100) Subject: test: Add TruncateWrite ops to IO Sequencer and Sequences using them X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=26f32cbab37b0c840d5328d34bc9ba6772d217b6;p=ceph.git test: Add TruncateWrite ops to IO Sequencer and Sequences using them current io exerciser sequences can model standalone truncates and writes, but cannot generate a single transaction that truncates an object and then writes sparse regions. Add SingleTruncateWriteOp, DoubleTruncateWriteOp, and TripleTruncateWriteOp, wire them through IoSequence, ObjectModel, and RadosIo, and extend EC sequences/tests so truncate+write transactions can be reproduced and validated. Signed-off-by: Matty Williams Assisted-by: IBM Bob:Claude/GPT --- diff --git a/src/common/io_exerciser/EcIoSequence.cc b/src/common/io_exerciser/EcIoSequence.cc index 2c61a7b381c1..a15287515662 100644 --- a/src/common/io_exerciser/EcIoSequence.cc +++ b/src/common/io_exerciser/EcIoSequence.cc @@ -45,6 +45,10 @@ std::unique_ptr EcIoSequence::generate_sequence( case Sequence::SEQUENCE_SEQ13: [[fallthrough]]; case Sequence::SEQUENCE_SEQ14: + [[fallthrough]]; + case Sequence::SEQUENCE_SEQ15: + [[fallthrough]]; + case Sequence::SEQUENCE_SEQ16: return std::make_unique(obj_size_range, seed, sequence, km, mappinglayers, check_consistency); case Sequence::SEQUENCE_SEQ10: diff --git a/src/common/io_exerciser/IoOp.cc b/src/common/io_exerciser/IoOp.cc index 11c1b3ca4c3d..2d75ab1ed4dd 100644 --- a/src/common/io_exerciser/IoOp.cc +++ b/src/common/io_exerciser/IoOp.cc @@ -24,6 +24,9 @@ using DoubleFailedWriteOp = ceph::io_exerciser::DoubleFailedWriteOp; using TripleFailedWriteOp = ceph::io_exerciser::TripleFailedWriteOp; using SingleAppendOp = ceph::io_exerciser::SingleAppendOp; using TruncateOp = ceph::io_exerciser::TruncateOp; +using SingleTruncateWriteOp = ceph::io_exerciser::SingleTruncateWriteOp; +using DoubleTruncateWriteOp = ceph::io_exerciser::DoubleTruncateWriteOp; +using TripleTruncateWriteOp = ceph::io_exerciser::TripleTruncateWriteOp; namespace { std::string value_to_string(uint64_t v) { @@ -287,6 +290,68 @@ std::string TruncateOp::to_string(uint64_t block_size) const { return "Truncate (size=" + value_to_string(size * block_size) + ")"; } +template +ceph::io_exerciser::TruncateWriteOp::TruncateWriteOp( + uint64_t size, std::array&& offset, + std::array&& length) + : TestOp(), size(size), offset(offset), length(length) {} + +template +std::string ceph::io_exerciser::TruncateWriteOp::to_string( + uint64_t block_size) const { + std::string result = "TruncateWrite (size=" + value_to_string(size * block_size); + for (int i = 0; i < N; i++) { + result += ", offset" + (N > 1 ? std::to_string(i + 1) : "") + "=" + + value_to_string(offset[i] * block_size) + ", length" + + (N > 1 ? std::to_string(i + 1) : "") + "=" + + value_to_string(length[i] * block_size); + } + result += ")"; + return result; +} + +SingleTruncateWriteOp::SingleTruncateWriteOp(uint64_t size, uint64_t offset, + uint64_t length) + : TruncateWriteOp(size, {offset}, {length}) {} + +std::unique_ptr SingleTruncateWriteOp::generate( + uint64_t size, uint64_t offset, uint64_t length) { + return std::make_unique(size, offset, length); +} + +DoubleTruncateWriteOp::DoubleTruncateWriteOp(uint64_t size, uint64_t offset1, + uint64_t length1, uint64_t offset2, + uint64_t length2) + : TruncateWriteOp(size, {offset1, offset2}, + {length1, length2}) {} + +std::unique_ptr DoubleTruncateWriteOp::generate( + uint64_t size, uint64_t offset1, uint64_t length1, uint64_t offset2, + uint64_t length2) { + return std::make_unique(size, offset1, length1, + offset2, length2); +} + +TripleTruncateWriteOp::TripleTruncateWriteOp(uint64_t size, uint64_t offset1, + uint64_t length1, uint64_t offset2, + uint64_t length2, uint64_t offset3, + uint64_t length3) + : TruncateWriteOp( + size, {offset1, offset2, offset3}, {length1, length2, length3}) {} + +std::unique_ptr TripleTruncateWriteOp::generate( + uint64_t size, uint64_t offset1, uint64_t length1, uint64_t offset2, + uint64_t length2, uint64_t offset3, uint64_t length3) { + return std::make_unique(size, offset1, length1, + offset2, length2, offset3, + length3); +} + +// Explicit template instantiations +template class ceph::io_exerciser::TruncateWriteOp; +template class ceph::io_exerciser::TruncateWriteOp; +template class ceph::io_exerciser::TruncateWriteOp; + SingleFailedWriteOp::SingleFailedWriteOp(uint64_t offset, uint64_t length) : ReadWriteOp({offset}, {length}) {} diff --git a/src/common/io_exerciser/IoOp.h b/src/common/io_exerciser/IoOp.h index 603cbcb7e2dd..0c69fc04d17c 100644 --- a/src/common/io_exerciser/IoOp.h +++ b/src/common/io_exerciser/IoOp.h @@ -175,6 +175,49 @@ class TruncateOp : public TestOp { uint64_t size; }; +template +class TruncateWriteOp : public TestOp { + public: + uint64_t size; + std::array offset; + std::array length; + + protected: + TruncateWriteOp(uint64_t size, + std::array&& offset, + std::array&& length); + std::string to_string(uint64_t block_size) const override; +}; + +class SingleTruncateWriteOp : public TruncateWriteOp { + public: + SingleTruncateWriteOp(uint64_t size, uint64_t offset, uint64_t length); + static std::unique_ptr generate(uint64_t size, + uint64_t offset, + uint64_t length); +}; + +class DoubleTruncateWriteOp : public TruncateWriteOp { + public: + DoubleTruncateWriteOp(uint64_t size, uint64_t offset1, uint64_t length1, + uint64_t offset2, uint64_t length2); + static std::unique_ptr generate(uint64_t size, + uint64_t offset1, + uint64_t length1, + uint64_t offset2, + uint64_t length2); +}; + +class TripleTruncateWriteOp : public TruncateWriteOp { + public: + TripleTruncateWriteOp(uint64_t size, uint64_t offset1, uint64_t length1, + uint64_t offset2, uint64_t length2, uint64_t offset3, + uint64_t length3); + static std::unique_ptr generate( + uint64_t size, uint64_t offset1, uint64_t length1, uint64_t offset2, + uint64_t length2, uint64_t offset3, uint64_t length3); +}; + class SingleFailedWriteOp : public ReadWriteOp { public: SingleFailedWriteOp(uint64_t offset, uint64_t length); diff --git a/src/common/io_exerciser/IoSequence.cc b/src/common/io_exerciser/IoSequence.cc index 2634b9a38a76..1b1bebfabb1a 100644 --- a/src/common/io_exerciser/IoSequence.cc +++ b/src/common/io_exerciser/IoSequence.cc @@ -59,6 +59,9 @@ std::ostream& ceph::io_exerciser::operator<<(std::ostream& os, case Sequence::SEQUENCE_SEQ15: os << "SEQUENCE_SEQ15"; break; + case Sequence::SEQUENCE_SEQ16: + os << "SEQUENCE_SEQ16"; + break; case Sequence::SEQUENCE_END: os << "SEQUENCE_END"; break; @@ -108,6 +111,8 @@ std::unique_ptr IoSequence::generate_sequence( return std::make_unique(obj_size_range, seed, check_consistency); case Sequence::SEQUENCE_SEQ15: return std::make_unique(obj_size_range, seed, check_consistency); + case Sequence::SEQUENCE_SEQ16: + return std::make_unique(obj_size_range, seed, check_consistency); default: break; } @@ -855,8 +860,82 @@ std::unique_ptr ceph::io_exerciser::Seq15::_next() { [[fallthrough]]; default: done = true; + remove = true; break; } return r; } + + +// Seq16: Multiple TruncateWrite operations with Consistency checks +// Each operation performs: +// 1. Truncate to a random size between 0 and current object size +// 2. Write a single block (size 1) at a random offset within the object +// 3. Consistency check +ceph::io_exerciser::Seq16::Seq16(std::pair obj_size_range, int seed, bool check_consistency) + : IoSequence(obj_size_range, seed, check_consistency), + truncate_size(0), + write_offset(0), + operation_count(0), + total_operations(20), + truncate_write_done(false), + consistency_done(false) { + select_random_object_size(); + setup_next_operation(); +} + +Sequence ceph::io_exerciser::Seq16::get_id() const { + return Sequence::SEQUENCE_SEQ16; +} + +std::string ceph::io_exerciser::Seq16::get_name() const { + return "Multiple TruncateWrite operations with varying sizes and Consistency checks"; +} + +void ceph::io_exerciser::Seq16::setup_next_operation() { + // Generate random truncate size between 0 and current object size + truncate_size = rng(obj_size + 1); + + // Generate random write offset within object size + // Ensure write stays within bounds (write_offset < obj_size for size 1 write) + write_offset = obj_size > 0 ? rng(obj_size) : 0; + + truncate_write_done = false; + consistency_done = false; +} + +std::unique_ptr ceph::io_exerciser::Seq16::_next() { + // Check if we're done with all operations + if (operation_count >= total_operations) { + done = true; + remove = true; + barrier = true; + return BarrierOp::generate(); + } + + // Perform TruncateWrite operation with a single write of size 1 + if (!truncate_write_done) { + truncate_write_done = true; + barrier = true; + return SingleTruncateWriteOp::generate(truncate_size, write_offset, 1); + } + + // Perform Consistency check + if (!consistency_done) { + consistency_done = true; + barrier = true; + + // Move to next operation + operation_count++; + if (operation_count < total_operations) { + setup_next_operation(); + } + + return ConsistencyOp::generate(); + } + + // Should not reach here + barrier = true; + return BarrierOp::generate(); +} diff --git a/src/common/io_exerciser/IoSequence.h b/src/common/io_exerciser/IoSequence.h index 13b7a54e7ffc..3c17bbeba958 100644 --- a/src/common/io_exerciser/IoSequence.h +++ b/src/common/io_exerciser/IoSequence.h @@ -48,6 +48,7 @@ enum class Sequence { SEQUENCE_SEQ13, SEQUENCE_SEQ14, SEQUENCE_SEQ15, + SEQUENCE_SEQ16, SEQUENCE_END, SEQUENCE_BEGIN = SEQUENCE_SEQ0 @@ -337,5 +338,24 @@ class Seq15 : public IoSequence { std::string get_name() const override; std::unique_ptr _next() override; }; + +class Seq16 : public IoSequence { + private: + uint64_t truncate_size; + uint64_t write_offset; + int operation_count; + int total_operations; + bool truncate_write_done; + bool consistency_done; + + void setup_next_operation(); + + public: + Seq16(std::pair obj_size_range, int seed, bool check_consistency); + + Sequence get_id() const override; + std::string get_name() const override; + std::unique_ptr _next() override; +}; } // namespace io_exerciser } // namespace ceph diff --git a/src/common/io_exerciser/ObjectModel.cc b/src/common/io_exerciser/ObjectModel.cc index cd9f741065a7..bc20d1a1b1ee 100644 --- a/src/common/io_exerciser/ObjectModel.cc +++ b/src/common/io_exerciser/ObjectModel.cc @@ -212,6 +212,94 @@ void ObjectModel::applyIoOp(IoOp& op) { verify_write_and_record_and_generate_seed(appendOp); } break; + case OpType::TruncateWrite: { + ceph_assert(primary_created); + ceph_assert(reads.empty()); + ceph_assert(writes.empty()); + SingleTruncateWriteOp& truncWriteOp = static_cast(op); + auto new_size = truncWriteOp.size; + auto old_size = primary_contents.size(); + bool expand = new_size > old_size; + primary_contents.resize(new_size); + if (expand) { + std::generate(std::execution::seq, primary_contents.begin() + old_size, + primary_contents.end(), generate_random); + } + // Now apply the write operations + for (int i = 0; i < 1; i++) { + ceph_assert(!reads.intersects(truncWriteOp.offset[i], truncWriteOp.length[i])); + ceph_assert(!writes.intersects(truncWriteOp.offset[i], truncWriteOp.length[i])); + writes.union_insert(truncWriteOp.offset[i], truncWriteOp.length[i]); + if (truncWriteOp.offset[i] + truncWriteOp.length[i] > primary_contents.size()) { + primary_contents.resize(truncWriteOp.offset[i] + truncWriteOp.length[i]); + } + std::generate(std::execution::seq, + std::next(primary_contents.begin(), truncWriteOp.offset[i]), + std::next(primary_contents.begin(), + truncWriteOp.offset[i] + truncWriteOp.length[i]), + generate_random); + } + num_io++; + } break; + case OpType::TruncateWrite2: { + ceph_assert(primary_created); + ceph_assert(reads.empty()); + ceph_assert(writes.empty()); + DoubleTruncateWriteOp& truncWriteOp = static_cast(op); + auto new_size = truncWriteOp.size; + auto old_size = primary_contents.size(); + bool expand = new_size > old_size; + primary_contents.resize(new_size); + if (expand) { + std::generate(std::execution::seq, primary_contents.begin() + old_size, + primary_contents.end(), generate_random); + } + // Now apply the write operations + for (int i = 0; i < 2; i++) { + ceph_assert(!reads.intersects(truncWriteOp.offset[i], truncWriteOp.length[i])); + ceph_assert(!writes.intersects(truncWriteOp.offset[i], truncWriteOp.length[i])); + writes.union_insert(truncWriteOp.offset[i], truncWriteOp.length[i]); + if (truncWriteOp.offset[i] + truncWriteOp.length[i] > primary_contents.size()) { + primary_contents.resize(truncWriteOp.offset[i] + truncWriteOp.length[i]); + } + std::generate(std::execution::seq, + std::next(primary_contents.begin(), truncWriteOp.offset[i]), + std::next(primary_contents.begin(), + truncWriteOp.offset[i] + truncWriteOp.length[i]), + generate_random); + } + num_io++; + } break; + case OpType::TruncateWrite3: { + ceph_assert(primary_created); + ceph_assert(reads.empty()); + ceph_assert(writes.empty()); + TripleTruncateWriteOp& truncWriteOp = static_cast(op); + auto new_size = truncWriteOp.size; + auto old_size = primary_contents.size(); + bool expand = new_size > old_size; + primary_contents.resize(new_size); + if (expand) { + std::generate(std::execution::seq, primary_contents.begin() + old_size, + primary_contents.end(), generate_random); + } + // Now apply the write operations + for (int i = 0; i < 3; i++) { + ceph_assert(!reads.intersects(truncWriteOp.offset[i], truncWriteOp.length[i])); + ceph_assert(!writes.intersects(truncWriteOp.offset[i], truncWriteOp.length[i])); + writes.union_insert(truncWriteOp.offset[i], truncWriteOp.length[i]); + if (truncWriteOp.offset[i] + truncWriteOp.length[i] > primary_contents.size()) { + primary_contents.resize(truncWriteOp.offset[i] + truncWriteOp.length[i]); + } + std::generate(std::execution::seq, + std::next(primary_contents.begin(), truncWriteOp.offset[i]), + std::next(primary_contents.begin(), + truncWriteOp.offset[i] + truncWriteOp.length[i]), + generate_random); + } + num_io++; + } break; + case OpType::FailedWrite: { ceph_assert(primary_created); SingleWriteOp& writeOp = static_cast(op); diff --git a/src/common/io_exerciser/OpType.h b/src/common/io_exerciser/OpType.h index af939614570e..9e2ba97fa635 100644 --- a/src/common/io_exerciser/OpType.h +++ b/src/common/io_exerciser/OpType.h @@ -27,6 +27,9 @@ enum class OpType { Write3, // Three writes in a single op Append, // Append Truncate, // Truncate + TruncateWrite, // Truncate + single write in a single op + TruncateWrite2, // Truncate + two writes in a single op + TruncateWrite3, // Truncate + three writes in a single op FailedWrite, // A write which should fail FailedWrite2, // Two writes in one op which should fail FailedWrite3, // Three writes in one op which should fail @@ -84,6 +87,12 @@ struct fmt::formatter { return fmt::format_to(ctx.out(), "Append"); case ceph::io_exerciser::OpType::Truncate: return fmt::format_to(ctx.out(), "Truncate"); + case ceph::io_exerciser::OpType::TruncateWrite: + return fmt::format_to(ctx.out(), "TruncateWrite"); + case ceph::io_exerciser::OpType::TruncateWrite2: + return fmt::format_to(ctx.out(), "TruncateWrite2"); + case ceph::io_exerciser::OpType::TruncateWrite3: + return fmt::format_to(ctx.out(), "TruncateWrite3"); case ceph::io_exerciser::OpType::FailedWrite: return fmt::format_to(ctx.out(), "FailedWrite"); case ceph::io_exerciser::OpType::FailedWrite2: diff --git a/src/common/io_exerciser/RadosIo.cc b/src/common/io_exerciser/RadosIo.cc index f98348ce3e8b..a8ea9b05a539 100644 --- a/src/common/io_exerciser/RadosIo.cc +++ b/src/common/io_exerciser/RadosIo.cc @@ -236,6 +236,13 @@ void RadosIo::applyIoOp(IoOp& op) { case OpType::FailedWrite3: applyReadWriteOp(op); break; + case OpType::TruncateWrite: + [[fallthrough]]; + case OpType::TruncateWrite2: + [[fallthrough]]; + case OpType::TruncateWrite3: + applyTruncateWriteOp(op); + break; case OpType::InjectReadError: [[fallthrough]]; case OpType::InjectWriteError: @@ -408,6 +415,59 @@ void RadosIo::applyReadWriteOp(IoOp& op) { } } +void RadosIo::applyTruncateWriteOp(IoOp& op) { + auto applyTruncateWriteOp = [this]( + TruncateWriteOp truncWriteOp) { + auto op_info = + std::make_shared>(truncWriteOp.offset, truncWriteOp.length); + librados::ObjectWriteOperation wop; + + // First, add the truncate operation + wop.truncate(truncWriteOp.size * block_size); + + // Then, add the write operations + for (int i = 0; i < N; i++) { + op_info->bufferlist[i] = + db->generate_data(truncWriteOp.offset[i], truncWriteOp.length[i]); + wop.write(truncWriteOp.offset[i] * block_size, + op_info->bufferlist[i]); + } + + auto write_cb = [this](boost::system::error_code ec, version_t ver) { + ceph_assert(ec == boost::system::errc::success); + finish_io(); + }; + librados::async_operate(asio.get_executor(), io, primary_oid, + std::move(wop), 0, nullptr, write_cb); + num_io++; + }; + + switch (op.getOpType()) { + case OpType::TruncateWrite: { + start_io(); + SingleTruncateWriteOp& truncWriteOp = static_cast(op); + applyTruncateWriteOp(truncWriteOp); + break; + } + case OpType::TruncateWrite2: { + start_io(); + DoubleTruncateWriteOp& truncWriteOp = static_cast(op); + applyTruncateWriteOp(truncWriteOp); + break; + } + case OpType::TruncateWrite3: { + start_io(); + TripleTruncateWriteOp& truncWriteOp = static_cast(op); + applyTruncateWriteOp(truncWriteOp); + break; + } + default: + ceph_abort_msg( + fmt::format("Unsupported TruncateWrite operation ({})", op.getOpType())); + break; + } +} + void RadosIo::applyInjectOp(IoOp& op) { bufferlist osdmap_outbl, inject_outbl; auto formatter = std::make_unique(false); diff --git a/src/common/io_exerciser/RadosIo.h b/src/common/io_exerciser/RadosIo.h index d79847dfa86b..248e13b81ef2 100644 --- a/src/common/io_exerciser/RadosIo.h +++ b/src/common/io_exerciser/RadosIo.h @@ -81,6 +81,7 @@ class RadosIo : public Model { private: void applyReadWriteOp(IoOp& op); + void applyTruncateWriteOp(IoOp& op); void applyInjectOp(IoOp& op); }; } // namespace io_exerciser diff --git a/src/test/osd/ceph_test_rados_io_sequence/ceph_test_rados_io_sequence.cc b/src/test/osd/ceph_test_rados_io_sequence/ceph_test_rados_io_sequence.cc index 0297312c6562..17d1a224f0e0 100644 --- a/src/test/osd/ceph_test_rados_io_sequence/ceph_test_rados_io_sequence.cc +++ b/src/test/osd/ceph_test_rados_io_sequence/ceph_test_rados_io_sequence.cc @@ -51,6 +51,9 @@ using DoubleWriteOp = ceph::io_exerciser::DoubleWriteOp; using TripleWriteOp = ceph::io_exerciser::TripleWriteOp; using SingleAppendOp = ceph::io_exerciser::SingleAppendOp; using TruncateOp = ceph::io_exerciser::TruncateOp; +using SingleTruncateWriteOp = ceph::io_exerciser::SingleTruncateWriteOp; +using DoubleTruncateWriteOp = ceph::io_exerciser::DoubleTruncateWriteOp; +using TripleTruncateWriteOp = ceph::io_exerciser::TripleTruncateWriteOp; using SingleFailedWriteOp = ceph::io_exerciser::SingleFailedWriteOp; using DoubleFailedWriteOp = ceph::io_exerciser::DoubleFailedWriteOp; using TripleFailedWriteOp = ceph::io_exerciser::TripleFailedWriteOp; @@ -1465,6 +1468,28 @@ bool ceph::io_sequence::tester::TestRunner::run_interactive_test() { ioop = SingleAppendOp::generate(length); } else if (op == "truncate") { ioop = TruncateOp::generate(get_numeric_token()); + } else if (op == "truncatewrite") { + uint64_t size = get_numeric_token(); + uint64_t offset = get_numeric_token(); + uint64_t length = get_numeric_token(); + ioop = SingleTruncateWriteOp::generate(size, offset, length); + } else if (op == "truncatewrite2") { + uint64_t size = get_numeric_token(); + uint64_t offset1 = get_numeric_token(); + uint64_t length1 = get_numeric_token(); + uint64_t offset2 = get_numeric_token(); + uint64_t length2 = get_numeric_token(); + ioop = DoubleTruncateWriteOp::generate(size, offset1, length1, offset2, length2); + } else if (op == "truncatewrite3") { + uint64_t size = get_numeric_token(); + uint64_t offset1 = get_numeric_token(); + uint64_t length1 = get_numeric_token(); + uint64_t offset2 = get_numeric_token(); + uint64_t length2 = get_numeric_token(); + uint64_t offset3 = get_numeric_token(); + uint64_t length3 = get_numeric_token(); + ioop = TripleTruncateWriteOp::generate(size, offset1, length1, offset2, length2, + offset3, length3); } else if (op == "failedwrite") { uint64_t offset = get_numeric_token(); uint64_t length = get_numeric_token();