]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
librbd: generic object list snapshot request
authorJason Dillaman <dillaman@redhat.com>
Tue, 1 Sep 2020 20:46:59 +0000 (16:46 -0400)
committerJason Dillaman <dillaman@redhat.com>
Mon, 21 Sep 2020 11:51:55 +0000 (07:51 -0400)
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 <dillaman@redhat.com>
src/librbd/CMakeLists.txt
src/librbd/io/ObjectRequest.cc
src/librbd/io/ObjectRequest.h
src/librbd/io/SimpleSchedulerObjectDispatch.cc
src/librbd/io/Types.cc [new file with mode: 0644]
src/librbd/io/Types.h
src/test/librados_test_stub/NeoradosTestStub.cc
src/test/librbd/io/test_mock_ObjectRequest.cc

index 735ead7896da1af13b391399f62628bbfa7883d7..480e2c79d48c1aa99de50653f51d50bf46e0c66f 100644 (file)
@@ -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
index d24997b198c502f39e3e5c6e71255c30e0b26c5b..ac6d1b992432cbca41e2594edee635bbc8206578 100644 (file)
@@ -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 <typename S, typename D>
+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 <typename I>
@@ -726,6 +741,228 @@ int ObjectCompareAndWriteRequest<I>::filter_write_result(int r) const {
   return r;
 }
 
+template <typename I>
+ObjectListSnapsRequest<I>::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<I>(
+      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 <typename I>
+void ObjectListSnapsRequest<I>::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 <typename I>
+void ObjectListSnapsRequest<I>::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 <typename I>
+void ObjectListSnapsRequest<I>::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<uint64_t> 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<uint64_t> 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<uint64_t> object_interval;
+        object_interval.insert(object_extent.first, object_extent.second);
+
+        // clip diff to current object extent
+        interval_set<uint64_t> diff_interval;
+        diff_interval.intersection_of(object_interval, diff);
+
+        interval_set<uint64_t> 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 <typename I>
+void ObjectListSnapsRequest<I>::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<librbd::ImageCtx>;
 template class librbd::io::ObjectDiscardRequest<librbd::ImageCtx>;
 template class librbd::io::ObjectWriteSameRequest<librbd::ImageCtx>;
 template class librbd::io::ObjectCompareAndWriteRequest<librbd::ImageCtx>;
+template class librbd::io::ObjectListSnapsRequest<librbd::ImageCtx>;
index 2c7d410c64d59f890320a8f16fb4c45c0084508c..b9173224028472450437b31d24df97a3a0d65357 100644 (file)
@@ -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 <typename ImageCtxT = ImageCtx>
+class ObjectListSnapsRequest : public ObjectRequest<ImageCtxT> {
+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<librbd::ImageCtx>;
 extern template class librbd::io::ObjectDiscardRequest<librbd::ImageCtx>;
 extern template class librbd::io::ObjectWriteSameRequest<librbd::ImageCtx>;
 extern template class librbd::io::ObjectCompareAndWriteRequest<librbd::ImageCtx>;
+extern template class librbd::io::ObjectListSnapsRequest<librbd::ImageCtx>;
 
 #endif // CEPH_LIBRBD_IO_OBJECT_REQUEST_H
index 03fd4139f2bf92b2c8a4f30e7aa97d9548bb6c52..f16167957dbe4a20b95da2d75d6e8b7657bf5dc7 100644 (file)
@@ -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 (file)
index 0000000..128d725
--- /dev/null
@@ -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 <iostream>
+
+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
index 6193e2f5a09875b396bddb26fd8d5db47eec9e37..9d03b332152097e5f199364bb142ad1fca73de6f 100644 (file)
@@ -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 <iosfwd>
 #include <map>
 #include <vector>
 
@@ -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<uint64_t> SnapIds;
+
+typedef std::pair<librados::snap_t, librados::snap_t> WriteReadSnapIds;
+extern const WriteReadSnapIds INITIAL_WRITE_READ_SNAP_IDS;
+
+typedef std::map<WriteReadSnapIds,
+                 interval_map<uint64_t,
+                              SnapshotExtent,
+                              SnapshotExtentSplitMerge>> SnapshotDelta;
+
 using striper::LightweightBufferExtents;
 using striper::LightweightObjectExtent;
 using striper::LightweightObjectExtents;
index 25db52b494d5a023100bcf7272e0e33a1e1e7103..c91a0aad4edc8410390ae1af88e1bb1f6a1609a5 100644 (file)
@@ -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<librados::TestObjectOperationImpl**>(&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<librados::TestObjectOperationImpl**>(&impl);
   o->ops.push_back(std::bind(
index 76072fcdfd62aa3e953724d54e06a6fa5053c7b8..e6262c2a57809d004307269821bdb3b7dc060142 100644 (file)
@@ -110,6 +110,7 @@ struct TestMockIoObjectRequest : public TestMockFixture {
   typedef ObjectDiscardRequest<librbd::MockTestImageCtx> MockObjectDiscardRequest;
   typedef ObjectWriteSameRequest<librbd::MockTestImageCtx> MockObjectWriteSameRequest;
   typedef ObjectCompareAndWriteRequest<librbd::MockTestImageCtx> MockObjectCompareAndWriteRequest;
+  typedef ObjectListSnapsRequest<librbd::MockTestImageCtx> MockObjectListSnapsRequest;
   typedef AbstractObjectWriteRequest<librbd::MockTestImageCtx> MockAbstractObjectWriteRequest;
   typedef CopyupRequest<librbd::MockTestImageCtx> 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<std::pair<uint64_t,uint64_t>>{
+    {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<std::pair<uint64_t,uint64_t>>{
+    {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<std::pair<uint64_t,uint64_t>>{
+    {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