From: Jason Dillaman Date: Tue, 1 Sep 2020 20:46:59 +0000 (-0400) Subject: librbd: generic object list snapshot request X-Git-Tag: v16.1.0~961^2~14 X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=11923e234efc053aef0789c665925ca2ca59589b;p=ceph.git librbd: generic object list snapshot request List the snapshots on a given RADOS object and convert it to a more generic set of per-snapshot data and zeroed extents. Signed-off-by: Jason Dillaman --- diff --git a/src/librbd/CMakeLists.txt b/src/librbd/CMakeLists.txt index 735ead7896d..480e2c79d48 100644 --- a/src/librbd/CMakeLists.txt +++ b/src/librbd/CMakeLists.txt @@ -95,6 +95,7 @@ set(librbd_internal_srcs io/ReadResult.cc io/RefreshImageDispatch.cc io/SimpleSchedulerObjectDispatch.cc + io/Types.cc io/Utils.cc io/WriteBlockImageDispatch.cc journal/CreateRequest.cc diff --git a/src/librbd/io/ObjectRequest.cc b/src/librbd/io/ObjectRequest.cc index d24997b198c..ac6d1b99243 100644 --- a/src/librbd/io/ObjectRequest.cc +++ b/src/librbd/io/ObjectRequest.cc @@ -10,7 +10,7 @@ #include "include/err.h" #include "include/neorados/RADOS.hpp" #include "osd/osd_types.h" - +#include "librados/snap_set_diff.h" #include "librbd/AsioEngine.h" #include "librbd/ExclusiveLock.h" #include "librbd/ImageCtx.h" @@ -48,6 +48,21 @@ inline bool is_copy_on_read(I *ictx, const IOContext& io_context) { ictx->exclusive_lock->is_lock_owner())); } +template +void convert_snap_set(const S& src_snap_set, + D* dst_snap_set) { + dst_snap_set->seq = src_snap_set.seq; + dst_snap_set->clones.reserve(src_snap_set.clones.size()); + for (auto& src_clone : src_snap_set.clones) { + dst_snap_set->clones.emplace_back(); + auto& dst_clone = dst_snap_set->clones.back(); + dst_clone.cloneid = src_clone.cloneid; + dst_clone.snaps = src_clone.snaps; + dst_clone.overlap = src_clone.overlap; + dst_clone.size = src_clone.size; + } +} + } // anonymous namespace template @@ -726,6 +741,228 @@ int ObjectCompareAndWriteRequest::filter_write_result(int r) const { return r; } +template +ObjectListSnapsRequest::ObjectListSnapsRequest( + I *ictx, uint64_t objectno, Extents&& object_extents, SnapIds&& snap_ids, + int list_snaps_flags, const ZTracer::Trace &parent_trace, + SnapshotDelta* snapshot_delta, Context *completion) + : ObjectRequest( + ictx, objectno, ictx->duplicate_data_io_context(), "snap_list", + parent_trace, completion), + m_object_extents(std::move(object_extents)), + m_snap_ids(std::move(snap_ids)), m_list_snaps_flags(list_snaps_flags), + m_snapshot_delta(snapshot_delta) { + this->m_io_context->read_snap(CEPH_SNAPDIR); +} + +template +void ObjectListSnapsRequest::send() { + I *image_ctx = this->m_ictx; + ldout(image_ctx->cct, 20) << dendl; + + if (m_snap_ids.size() < 2) { + lderr(image_ctx->cct) << "invalid snap ids: " << m_snap_ids << dendl; + this->async_finish(-EINVAL); + return; + } + + list_snaps(); +} + +template +void ObjectListSnapsRequest::list_snaps() { + I *image_ctx = this->m_ictx; + ldout(image_ctx->cct, 20) << dendl; + + neorados::ReadOp read_op; + read_op.list_snaps(&m_snap_set, &m_ec); + + image_ctx->rados_api.execute( + {data_object_name(this->m_ictx, this->m_object_no)}, + *this->m_io_context, std::move(read_op), nullptr, + librbd::asio::util::get_callback_adapter( + [this](int r) { handle_list_snaps(r); }), nullptr, + (this->m_trace.valid() ? this->m_trace.get_info() : nullptr)); +} + +template +void ObjectListSnapsRequest::handle_list_snaps(int r) { + I *image_ctx = this->m_ictx; + auto cct = image_ctx->cct; + + if (r >= 0) { + r = -m_ec.value(); + } + + ldout(cct, 20) << "r=" << r << dendl; + + m_snapshot_delta->clear(); + auto& snapshot_delta = *m_snapshot_delta; + + if (r == -ENOENT) { + // the object does not exist -- mark the missing extents + zero_initial_extent(true); + this->finish(0); + return; + } else if (r < 0) { + lderr(cct) << "failed to retrieve object snapshot list: " << cpp_strerror(r) + << dendl; + this->finish(r); + return; + } + + // helper function requires the librados legacy data structure + librados::snap_set_t snap_set; + convert_snap_set(m_snap_set, &snap_set); + + ceph_assert(!m_snap_ids.empty()); + librados::snap_t start_snap_id = 0; + librados::snap_t first_snap_id = *m_snap_ids.begin(); + librados::snap_t last_snap_id = *m_snap_ids.rbegin(); + + // loop through all expected snapshots and build interval sets for + // data and zeroed ranges for each snapshot + bool prev_exists = false; + uint64_t prev_end_size = 0; + bool whiteout_detected = false; + for (auto end_snap_id : m_snap_ids) { + if (start_snap_id == end_snap_id) { + continue; + } else if (end_snap_id > last_snap_id) { + break; + } + + interval_set diff; + uint64_t end_size; + bool exists; + librados::snap_t clone_end_snap_id; + bool read_whole_object; + calc_snap_set_diff(cct, snap_set, start_snap_id, + end_snap_id, &diff, &end_size, &exists, + &clone_end_snap_id, &read_whole_object); + + if (read_whole_object) { + ldout(cct, 1) << "need to read full object" << dendl; + diff.insert(0, image_ctx->layout.object_size); + end_size = image_ctx->layout.object_size; + clone_end_snap_id = end_snap_id; + } else if (!exists) { + end_size = 0; + } else if (exists && end_size == 0 && start_snap_id == 0) { + ldout(cct, 20) << "whiteout detected" << dendl; + whiteout_detected = true; + } + + if (exists) { + // reads should be issued against the newest (existing) snapshot within + // the associated snapshot object clone. writes should be issued + // against the oldest snapshot in the snap_map. + ceph_assert(clone_end_snap_id >= end_snap_id); + if (clone_end_snap_id > last_snap_id) { + // do not read past the copy point snapshot + clone_end_snap_id = last_snap_id; + } + } + + ldout(cct, 20) << "start_snap_id=" << start_snap_id << ", " + << "end_snap_id=" << end_snap_id << ", " + << "clone_end_snap_id=" << clone_end_snap_id << ", " + << "diff=" << diff << ", " + << "end_size=" << end_size << ", " + << "exists=" << exists << dendl; + if (end_snap_id <= first_snap_id) { + // don't include deltas from the starting snapshots, but we iterate over + // it to track its existence and size + ldout(cct, 20) << "skipping prior snapshots" << dendl; + } else if (exists || prev_exists || !diff.empty()) { + // clip diff to size of object (in case it was truncated) + if (exists && end_size < prev_end_size) { + interval_set trunc; + trunc.insert(end_size, prev_end_size - end_size); + trunc.intersection_of(diff); + diff.subtract(trunc); + ldout(cct, 20) << "clearing truncate diff: " << trunc << dendl; + } + + for (auto& object_extent : m_object_extents) { + interval_set object_interval; + object_interval.insert(object_extent.first, object_extent.second); + + // clip diff to current object extent + interval_set diff_interval; + diff_interval.intersection_of(object_interval, diff); + + interval_set zero_interval; + if (end_size < prev_end_size) { + // insert zeroed object extent from truncation + auto zero_length = prev_end_size - end_size; + zero_interval.insert(end_size, zero_length); + } + + if (exists) { + ldout(cct, 20) << "object_extent=" << object_extent.first << "~" + << object_extent.second << ", " + << "data_interval=" << diff_interval << dendl; + for (auto& interval : diff_interval) { + snapshot_delta[{end_snap_id, clone_end_snap_id}].insert( + interval.first, interval.second, + SnapshotExtent(SNAPSHOT_EXTENT_STATE_DATA, interval.second)); + } + } else { + zero_interval.union_of(diff_interval); + } + + zero_interval.intersection_of(object_interval); + if (!zero_interval.empty()) { + ldout(cct, 20) << "object_extent=" << object_extent.first << "~" + << object_extent.second << " " + << "zero_interval=" << zero_interval << dendl; + for (auto& interval : zero_interval) { + snapshot_delta[{end_snap_id, end_snap_id}].insert( + interval.first, interval.second, + SnapshotExtent(SNAPSHOT_EXTENT_STATE_ZEROED, interval.second)); + } + } + } + } + + prev_end_size = end_size; + prev_exists = exists; + start_snap_id = end_snap_id; + } + + bool snapshot_delta_empty = snapshot_delta.empty(); + if (whiteout_detected || snapshot_delta_empty) { + zero_initial_extent(false); + } + + ldout(cct, 20) << "snapshot_delta=" << snapshot_delta << dendl; + + this->finish(0); +} + +template +void ObjectListSnapsRequest::zero_initial_extent(bool dne) { + I *image_ctx = this->m_ictx; + auto cct = image_ctx->cct; + + ceph_assert(!m_snap_ids.empty()); + librados::snap_t snap_id_start = *m_snap_ids.begin(); + + // the object does not exist -- mark the missing extents + if (snap_id_start == 0) { + for (auto [object_offset, object_length] : m_object_extents) { + ldout(cct, 20) << "zeroing initial extent " << object_offset << "~" + << object_length << dendl; + (*m_snapshot_delta)[INITIAL_WRITE_READ_SNAP_IDS].insert( + object_offset, object_length, + SnapshotExtent( + (dne ? SNAPSHOT_EXTENT_STATE_DNE : SNAPSHOT_EXTENT_STATE_ZEROED), + object_length)); + } + } +} + } // namespace io } // namespace librbd @@ -736,3 +973,4 @@ template class librbd::io::ObjectWriteRequest; template class librbd::io::ObjectDiscardRequest; template class librbd::io::ObjectWriteSameRequest; template class librbd::io::ObjectCompareAndWriteRequest; +template class librbd::io::ObjectListSnapsRequest; diff --git a/src/librbd/io/ObjectRequest.h b/src/librbd/io/ObjectRequest.h index 2c7d410c64d..b9173224028 100644 --- a/src/librbd/io/ObjectRequest.h +++ b/src/librbd/io/ObjectRequest.h @@ -6,6 +6,7 @@ #include "include/int_types.h" #include "include/buffer.h" +#include "include/neorados/RADOS.hpp" #include "include/rados/librados.hpp" #include "common/zipkin_trace.h" #include "librbd/ObjectMap.h" @@ -439,6 +440,48 @@ private: int m_op_flags; }; +template +class ObjectListSnapsRequest : public ObjectRequest { +public: + static ObjectListSnapsRequest* create( + ImageCtxT *ictx, uint64_t objectno, Extents&& object_extents, + SnapIds&& snap_ids, int list_snaps_flags, + const ZTracer::Trace &parent_trace, SnapshotDelta* snapshot_delta, + Context *completion) { + return new ObjectListSnapsRequest(ictx, objectno, + std::move(object_extents), + std::move(snap_ids), list_snaps_flags, + parent_trace, snapshot_delta, completion); + } + + ObjectListSnapsRequest( + ImageCtxT *ictx, uint64_t objectno, Extents&& object_extents, + SnapIds&& snap_ids, int list_snaps_flags, + const ZTracer::Trace &parent_trace, SnapshotDelta* snapshot_delta, + Context *completion); + + void send() override; + + const char *get_op_type() const override { + return "snap_list"; + } + +private: + Extents m_object_extents; + SnapIds m_snap_ids; + int m_list_snaps_flags; + SnapshotDelta* m_snapshot_delta; + + neorados::SnapSet m_snap_set; + boost::system::error_code m_ec; + + void list_snaps(); + void handle_list_snaps(int r); + + void zero_initial_extent(bool dne); + +}; + } // namespace io } // namespace librbd @@ -449,5 +492,6 @@ extern template class librbd::io::ObjectWriteRequest; extern template class librbd::io::ObjectDiscardRequest; extern template class librbd::io::ObjectWriteSameRequest; extern template class librbd::io::ObjectCompareAndWriteRequest; +extern template class librbd::io::ObjectListSnapsRequest; #endif // CEPH_LIBRBD_IO_OBJECT_REQUEST_H diff --git a/src/librbd/io/SimpleSchedulerObjectDispatch.cc b/src/librbd/io/SimpleSchedulerObjectDispatch.cc index 03fd4139f2b..f16167957db 100644 --- a/src/librbd/io/SimpleSchedulerObjectDispatch.cc +++ b/src/librbd/io/SimpleSchedulerObjectDispatch.cc @@ -3,6 +3,7 @@ #include "librbd/io/SimpleSchedulerObjectDispatch.h" #include "include/neorados/RADOS.hpp" +#include "common/ceph_time.h" #include "common/Timer.h" #include "common/errno.h" #include "librbd/AsioEngine.h" @@ -27,6 +28,7 @@ namespace librbd { namespace io { using namespace boost::accumulators; +using ceph::operator<<; using librbd::util::data_object_name; static const int LATENCY_STATS_WINDOW_SIZE = 10; diff --git a/src/librbd/io/Types.cc b/src/librbd/io/Types.cc new file mode 100644 index 00000000000..128d725ade3 --- /dev/null +++ b/src/librbd/io/Types.cc @@ -0,0 +1,35 @@ +// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*- +// vim: ts=8 sw=2 smarttab + +#include "librbd/io/Types.h" +#include + +namespace librbd { +namespace io { + +const WriteReadSnapIds INITIAL_WRITE_READ_SNAP_IDS{0, 0}; + +std::ostream& operator<<(std::ostream& os, SnapshotExtentState state) { + switch (state) { + case SNAPSHOT_EXTENT_STATE_ZEROED: + os << "zeroed"; + break; + case SNAPSHOT_EXTENT_STATE_DATA: + os << "data"; + break; + default: + ceph_abort(); + break; + } + return os; +} + +std::ostream& operator<<(std::ostream& os, const SnapshotExtent& se) { + os << "[" + << "state=" << se.state << ", " + << "length=" << se.length << "]"; + return os; +} + +} // namespace io +} // namespace librbd diff --git a/src/librbd/io/Types.h b/src/librbd/io/Types.h index 6193e2f5a09..9d03b332152 100644 --- a/src/librbd/io/Types.h +++ b/src/librbd/io/Types.h @@ -5,7 +5,10 @@ #define CEPH_LIBRBD_IO_TYPES_H #include "include/int_types.h" +#include "include/rados/rados_types.hpp" +#include "common/interval_map.h" #include "osdc/StriperTypes.h" +#include #include #include @@ -117,6 +120,65 @@ enum { OBJECT_DISPATCH_FLAG_WILL_RETRY_ON_ERROR = 1UL << 1 }; +enum SnapshotExtentState { + SNAPSHOT_EXTENT_STATE_DNE, /* does not exist */ + SNAPSHOT_EXTENT_STATE_ZEROED, + SNAPSHOT_EXTENT_STATE_DATA +}; + +std::ostream& operator<<(std::ostream& os, SnapshotExtentState state); + +struct SnapshotExtent { + SnapshotExtentState state; + size_t length; + + SnapshotExtent(SnapshotExtentState state, size_t length) + : state(state), length(length) { + } + + operator SnapshotExtentState() const { + return state; + } + + bool operator==(const SnapshotExtent& rhs) const { + return state == rhs.state && length == rhs.length; + } +}; + +std::ostream& operator<<(std::ostream& os, const SnapshotExtent& state); + +struct SnapshotExtentSplitMerge { + SnapshotExtent split(uint64_t offset, uint64_t length, + SnapshotExtent &se) const { + return SnapshotExtent(se.state, se.length); + } + + bool can_merge(const SnapshotExtent& left, + const SnapshotExtent& right) const { + return left.state == right.state; + } + + SnapshotExtent merge(SnapshotExtent&& left, SnapshotExtent&& right) const { + SnapshotExtent se(left); + se.length += right.length; + return se; + } + + uint64_t length(const SnapshotExtent& se) const { + return se.length; + } +}; + +typedef std::vector SnapIds; + +typedef std::pair WriteReadSnapIds; +extern const WriteReadSnapIds INITIAL_WRITE_READ_SNAP_IDS; + +typedef std::map> SnapshotDelta; + using striper::LightweightBufferExtents; using striper::LightweightObjectExtent; using striper::LightweightObjectExtents; diff --git a/src/test/librados_test_stub/NeoradosTestStub.cc b/src/test/librados_test_stub/NeoradosTestStub.cc index 25db52b494d..c91a0aad4ed 100644 --- a/src/test/librados_test_stub/NeoradosTestStub.cc +++ b/src/test/librados_test_stub/NeoradosTestStub.cc @@ -393,6 +393,36 @@ void ReadOp::sparse_read(uint64_t off, uint64_t len, o->ops.push_back(op); } +void ReadOp::list_snaps(SnapSet* snaps, bs::error_code* ec) { + auto o = *reinterpret_cast(&impl); + librados::ObjectOperationTestImpl op = + [snaps] + (librados::TestIoCtxImpl* io_ctx, const std::string& oid, bufferlist*, + uint64_t, const SnapContext&) mutable -> int { + librados::snap_set_t snap_set; + int r = io_ctx->list_snaps(oid, &snap_set); + if (r >= 0 && snaps != nullptr) { + *snaps = {}; + snaps->seq = snap_set.seq; + snaps->clones.reserve(snap_set.clones.size()); + for (auto& clone : snap_set.clones) { + neorados::CloneInfo clone_info; + clone_info.cloneid = clone.cloneid; + clone_info.snaps = clone.snaps; + clone_info.overlap = clone.overlap; + clone_info.size = clone.size; + snaps->clones.push_back(clone_info); + } + } + return r; + }; + if (ec != NULL) { + op = std::bind(save_operation_ec, + std::bind(op, _1, _2, _3, _4, _5), ec); + } + o->ops.push_back(op); +} + void WriteOp::create(bool exclusive) { auto o = *reinterpret_cast(&impl); o->ops.push_back(std::bind( diff --git a/src/test/librbd/io/test_mock_ObjectRequest.cc b/src/test/librbd/io/test_mock_ObjectRequest.cc index 76072fcdfd6..e6262c2a578 100644 --- a/src/test/librbd/io/test_mock_ObjectRequest.cc +++ b/src/test/librbd/io/test_mock_ObjectRequest.cc @@ -110,6 +110,7 @@ struct TestMockIoObjectRequest : public TestMockFixture { typedef ObjectDiscardRequest MockObjectDiscardRequest; typedef ObjectWriteSameRequest MockObjectWriteSameRequest; typedef ObjectCompareAndWriteRequest MockObjectCompareAndWriteRequest; + typedef ObjectListSnapsRequest MockObjectListSnapsRequest; typedef AbstractObjectWriteRequest MockAbstractObjectWriteRequest; typedef CopyupRequest MockCopyupRequest; typedef util::Mock MockUtils; @@ -334,6 +335,20 @@ struct TestMockIoObjectRequest : public TestMockFixture { expect.WillOnce(DoDefault()); } } + + void expect_list_snaps(MockTestImageCtx &mock_image_ctx, + const librados::snap_set_t& snap_set, int r) { + auto io_context = *mock_image_ctx.get_data_io_context(); + io_context.read_snap(CEPH_SNAPDIR); + auto& mock_io_ctx = librados::get_mock_io_ctx(mock_image_ctx.rados_api, + io_context); + EXPECT_CALL(mock_io_ctx, list_snaps(_, _)) + .WillOnce(WithArg<1>(Invoke( + [snap_set, r](librados::snap_set_t* out_snap_set) { + *out_snap_set = snap_set; + return r; + }))); + } }; TEST_F(TestMockIoObjectRequest, Read) { @@ -1592,6 +1607,139 @@ TEST_F(TestMockIoObjectRequest, ObjectMapError) { ASSERT_EQ(-EBLOCKLISTED, ctx.wait()); } +TEST_F(TestMockIoObjectRequest, ListSnaps) { + librbd::ImageCtx *ictx; + ASSERT_EQ(0, open_image(m_image_name, &ictx)); + + MockTestImageCtx mock_image_ctx(*ictx); + mock_image_ctx.snaps = {3, 4, 5, 6, 7}; + + librados::snap_set_t snap_set; + snap_set.seq = 6; + librados::clone_info_t clone_info; + + clone_info.cloneid = 3; + clone_info.snaps = {3}; + clone_info.overlap = std::vector>{ + {0, 4194304}}; + clone_info.size = 4194304; + snap_set.clones.push_back(clone_info); + + clone_info.cloneid = 4; + clone_info.snaps = {4}; + clone_info.overlap = std::vector>{ + {278528, 4096}, {442368, 4096}, {1859584, 4096}, {2224128, 4096}, + {2756608, 4096}, {3227648, 4096}, {3739648, 4096}, {3903488, 4096}}; + clone_info.size = 4194304; + snap_set.clones.push_back(clone_info); + + clone_info.cloneid = 6; + clone_info.snaps = {5, 6}; + clone_info.overlap = std::vector>{ + {425984, 4096}, {440320, 1024}, {1925120, 4096}, {2125824, 4096}, + {2215936, 5120}, {3067904, 4096}}; + clone_info.size = 3072000; + snap_set.clones.push_back(clone_info); + + clone_info.cloneid = CEPH_NOSNAP; + clone_info.snaps = {}; + clone_info.overlap = {}; + clone_info.size = 4194304; + snap_set.clones.push_back(clone_info); + + expect_list_snaps(mock_image_ctx, snap_set, 0); + + SnapshotDelta snapshot_delta; + C_SaferCond ctx; + auto req = MockObjectListSnapsRequest::create( + &mock_image_ctx, 0, + {{440320, 1024}, {2122728, 1024}, {2220032, 2048}, {3072000, 4096}}, + {3, 4, 5, 6, 7, CEPH_NOSNAP}, 0, {}, &snapshot_delta, &ctx); + req->send(); + ASSERT_EQ(0, ctx.wait()); + + SnapshotDelta expected_snapshot_delta; + expected_snapshot_delta[{5,6}].insert( + 440320, 1024, {SNAPSHOT_EXTENT_STATE_DATA, 1024}); + expected_snapshot_delta[{5,6}].insert( + 2122728, 1024, {SNAPSHOT_EXTENT_STATE_DATA, 1024}); + expected_snapshot_delta[{5,6}].insert( + 2220032, 2048, {SNAPSHOT_EXTENT_STATE_DATA, 2048}); + expected_snapshot_delta[{7,CEPH_NOSNAP}].insert( + 2122728, 1024, {SNAPSHOT_EXTENT_STATE_DATA, 1024}); + expected_snapshot_delta[{7,CEPH_NOSNAP}].insert( + 2221056, 1024, {SNAPSHOT_EXTENT_STATE_DATA, 1024}); + expected_snapshot_delta[{7,CEPH_NOSNAP}].insert( + 3072000, 4096, {SNAPSHOT_EXTENT_STATE_DATA, 4096}); + expected_snapshot_delta[{5,5}].insert( + 3072000, 4096, {SNAPSHOT_EXTENT_STATE_ZEROED, 4096}); + ASSERT_EQ(expected_snapshot_delta, snapshot_delta); +} + +TEST_F(TestMockIoObjectRequest, ListSnapsDNE) { + librbd::ImageCtx *ictx; + ASSERT_EQ(0, open_image(m_image_name, &ictx)); + + MockTestImageCtx mock_image_ctx(*ictx); + + expect_list_snaps(mock_image_ctx, {}, -ENOENT); + + SnapshotDelta snapshot_delta; + C_SaferCond ctx; + auto req = MockObjectListSnapsRequest::create( + &mock_image_ctx, 0, + {{440320, 1024}}, + {0, CEPH_NOSNAP}, 0, {}, &snapshot_delta, &ctx); + req->send(); + ASSERT_EQ(0, ctx.wait()); + + SnapshotDelta expected_snapshot_delta; + expected_snapshot_delta[{0,0}].insert( + 440320, 1024, {SNAPSHOT_EXTENT_STATE_DNE, 1024}); + ASSERT_EQ(expected_snapshot_delta, snapshot_delta); +} + +TEST_F(TestMockIoObjectRequest, ListSnapsEmpty) { + librbd::ImageCtx *ictx; + ASSERT_EQ(0, open_image(m_image_name, &ictx)); + + MockTestImageCtx mock_image_ctx(*ictx); + + expect_list_snaps(mock_image_ctx, {}, 0); + + SnapshotDelta snapshot_delta; + C_SaferCond ctx; + auto req = MockObjectListSnapsRequest::create( + &mock_image_ctx, 0, + {{440320, 1024}}, + {0, CEPH_NOSNAP}, 0, {}, &snapshot_delta, &ctx); + req->send(); + ASSERT_EQ(0, ctx.wait()); + + SnapshotDelta expected_snapshot_delta; + expected_snapshot_delta[{0,0}].insert( + 440320, 1024, {SNAPSHOT_EXTENT_STATE_ZEROED, 1024}); + ASSERT_EQ(expected_snapshot_delta, snapshot_delta); +} + +TEST_F(TestMockIoObjectRequest, ListSnapsError) { + librbd::ImageCtx *ictx; + ASSERT_EQ(0, open_image(m_image_name, &ictx)); + + MockTestImageCtx mock_image_ctx(*ictx); + + expect_list_snaps(mock_image_ctx, {}, -EPERM); + + SnapshotDelta snapshot_delta; + C_SaferCond ctx; + auto req = MockObjectListSnapsRequest::create( + &mock_image_ctx, 0, + {{440320, 1024}, {2122728, 1024}, {2220032, 2048}, {3072000, 4096}}, + {3, CEPH_NOSNAP}, 0, {}, &snapshot_delta, &ctx); + req->send(); + ASSERT_EQ(-EPERM, ctx.wait()); +} + } // namespace io } // namespace librbd