]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
librbd/object_map: don't assert if a snapshot doesn't exist 38455/head
authorJason Dillaman <dillaman@redhat.com>
Fri, 11 Dec 2020 01:11:34 +0000 (20:11 -0500)
committerJason Dillaman <dillaman@redhat.com>
Tue, 15 Dec 2020 19:21:37 +0000 (14:21 -0500)
When compute a fast-diff, if an snapshot no longer exists or fails
to load, ignore the error as long as it wasn't the start or end
snapshot.

Fixes: https://tracker.ceph.com/issues/48526
Signed-off-by: Jason Dillaman <dillaman@redhat.com>
(cherry picked from commit abab5f143b80ff876948307f00ab767bc1b6ba22)

Conflicts:
src/test/librbd/object_map/test_mock_DiffRequest.cc: exec() mock only takes 7 params

src/librbd/object_map/DiffRequest.cc
src/librbd/object_map/DiffRequest.h
src/test/librbd/CMakeLists.txt
src/test/librbd/object_map/test_mock_DiffRequest.cc [new file with mode: 0644]

index 1f80eb155cb2b335f8b3d67a3e804962b09625e0..1058288a6bac331c44533ca32a9760267a793917 100644 (file)
@@ -22,20 +22,34 @@ using util::create_rados_callback;
 
 template <typename I>
 void DiffRequest<I>::send() {
-  std::shared_lock image_locker{m_image_ctx->image_lock};
+  auto cct = m_image_ctx->cct;
 
-  m_diff_from_start = (m_snap_id_start == 0);
-  if (m_snap_id_start == 0) {
-    if (!m_image_ctx->snap_info.empty()) {
-      m_snap_id_start = m_image_ctx->snap_info.begin()->first;
-    } else {
-      m_snap_id_start = CEPH_NOSNAP;
-    }
+  if (m_snap_id_start == CEPH_NOSNAP || m_snap_id_start > m_snap_id_end) {
+    lderr(cct) << "invalid start/end snap ids: "
+               << "snap_id_start=" << m_snap_id_start << ", "
+               << "snap_id_end=" << m_snap_id_end << dendl;
+    finish(-EINVAL);
+    return;
+  } else if (m_snap_id_start == m_snap_id_end) {
+    // no delta between the same snapshot
+    finish(0);
+    return;
   }
 
   m_object_diff_state->clear();
-  m_current_snap_id = m_snap_id_start;
-  m_next_snap_id = m_snap_id_end;
+
+  // collect all the snap ids in the provided range (inclusive)
+  if (m_snap_id_start != 0) {
+    m_snap_ids.insert(m_snap_id_start);
+  }
+
+  std::shared_lock image_locker{m_image_ctx->image_lock};
+  auto snap_info_it = m_image_ctx->snap_info.upper_bound(m_snap_id_start);
+  auto snap_info_it_end = m_image_ctx->snap_info.lower_bound(m_snap_id_end);
+  for (; snap_info_it != snap_info_it_end; ++snap_info_it) {
+    m_snap_ids.insert(snap_info_it->first);
+  }
+  m_snap_ids.insert(m_snap_id_end);
 
   load_object_map(&image_locker);
 }
@@ -45,23 +59,19 @@ void DiffRequest<I>::load_object_map(
     std::shared_lock<ceph::shared_mutex>* image_locker) {
   ceph_assert(ceph_mutex_is_locked(m_image_ctx->image_lock));
 
-  auto cct = m_image_ctx->cct;
-  ldout(cct, 10) << dendl;
-
-  m_current_size = m_image_ctx->size;
-  if (m_current_snap_id != CEPH_NOSNAP) {
-    auto snap_it = m_image_ctx->snap_info.find(m_current_snap_id);
-    ceph_assert(snap_it != m_image_ctx->snap_info.end());
-    m_current_size = snap_it->second.size;
+  if (m_snap_ids.empty()) {
+    image_locker->unlock();
 
-    ++snap_it;
-    if (snap_it != m_image_ctx->snap_info.end()) {
-      m_next_snap_id = snap_it->first;
-    } else {
-      m_next_snap_id = CEPH_NOSNAP;
-    }
+    finish(0);
+    return;
   }
 
+  m_current_snap_id = *m_snap_ids.begin();
+  m_snap_ids.erase(m_current_snap_id);
+
+  auto cct = m_image_ctx->cct;
+  ldout(cct, 10) << "snap_id=" << m_current_snap_id << dendl;
+
   if ((m_image_ctx->features & RBD_FEATURE_FAST_DIFF) == 0) {
     image_locker->unlock();
 
@@ -70,6 +80,32 @@ void DiffRequest<I>::load_object_map(
     return;
   }
 
+  // ignore ENOENT with intermediate snapshots since deleted
+  // snaps will get merged with later snapshots
+  m_ignore_enoent = (m_current_snap_id != m_snap_id_start &&
+                     m_current_snap_id != m_snap_id_end);
+
+  if (m_current_snap_id == CEPH_NOSNAP) {
+    m_current_size = m_image_ctx->size;
+  } else {
+    auto snap_it = m_image_ctx->snap_info.find(m_current_snap_id);
+    if (snap_it == m_image_ctx->snap_info.end()) {
+      ldout(cct, 10) << "snapshot " << m_current_snap_id << " does not exist"
+                     << dendl;
+      if (!m_ignore_enoent) {
+        image_locker->unlock();
+
+        finish(-ENOENT);
+        return;
+      }
+
+      load_object_map(image_locker);
+      return;
+    }
+
+    m_current_size = snap_it->second.size;
+  }
+
   uint64_t flags = 0;
   int r = m_image_ctx->get_flags(m_current_snap_id, &flags);
   if (r < 0) {
@@ -80,9 +116,9 @@ void DiffRequest<I>::load_object_map(
     finish(r);
     return;
   }
-  if ((flags & RBD_FLAG_FAST_DIFF_INVALID) != 0) {
-    image_locker->unlock();
+  image_locker->unlock();
 
+  if ((flags & RBD_FLAG_FAST_DIFF_INVALID) != 0) {
     ldout(cct, 1) << "cannot perform fast diff on invalid object map"
                   << dendl;
     finish(-EINVAL);
@@ -115,7 +151,13 @@ void DiffRequest<I>::handle_load_object_map(int r) {
 
   std::string oid(ObjectMap<>::object_map_name(m_image_ctx->id,
                                                m_current_snap_id));
-  if (r < 0) {
+  if (r == -ENOENT && m_ignore_enoent) {
+    ldout(cct, 10) << "object map " << oid << " does not exist" << dendl;
+
+    std::shared_lock image_locker{m_image_ctx->image_lock};
+    load_object_map(&image_locker);
+    return;
+  } else if (r < 0) {
     lderr(cct) << "failed to load object map: " << oid << dendl;
     finish(r);
     return;
@@ -167,9 +209,10 @@ void DiffRequest<I>::handle_load_object_map(int r) {
   }
   ldout(cct, 20) << "computed overlap diffs" << dendl;
 
+  bool diff_from_start = (m_snap_id_start == 0);
   auto end_it = m_object_map.end();
   if (m_object_map.size() > m_prev_object_map.size() &&
-      (m_diff_from_start || m_prev_object_map_valid)) {
+      (diff_from_start || m_prev_object_map_valid)) {
     for (; it != end_it; ++it,++diff_it, ++i) {
       ldout(cct, 20) << "object state: " << i << " "
                      << "->" << static_cast<uint32_t>(*it) << dendl;
@@ -182,12 +225,6 @@ void DiffRequest<I>::handle_load_object_map(int r) {
   }
   ldout(cct, 20) << "computed resize diffs" << dendl;
 
-  if (m_current_snap_id == m_next_snap_id || m_next_snap_id > m_snap_id_end) {
-    finish(0);
-    return;
-  }
-
-  m_current_snap_id = m_next_snap_id;
   m_prev_object_map = m_object_map;
   m_prev_object_map_valid = true;
 
index e80a02d99e6f655083a03c7b193ed5a6bd57fa85..31a7a1bea25056fd51c101a86b049a2210892c7d 100644 (file)
@@ -8,6 +8,7 @@
 #include "common/bit_vector.hpp"
 #include "common/ceph_mutex.h"
 #include "librbd/object_map/Types.h"
+#include <set>
 
 struct Context;
 
@@ -60,9 +61,9 @@ private:
   BitVector<2>* m_object_diff_state;
   Context* m_on_finish;
 
-  bool m_diff_from_start = false;
+  std::set<uint64_t> m_snap_ids;
   uint64_t m_current_snap_id = 0;
-  uint64_t m_next_snap_id = 0;
+  bool m_ignore_enoent = false;
 
   uint64_t m_current_size = 0;
 
index b5581eb219cddb944a9685ec993df8acad92d1e3..53702ab79b696ad234398352ed6799d51c7808b8 100644 (file)
@@ -93,6 +93,7 @@ set(unittest_librbd_srcs
   mirror/snapshot/test_mock_UnlinkPeerRequest.cc
   mirror/snapshot/test_mock_Utils.cc
   mirror/test_mock_DisableRequest.cc
+  object_map/test_mock_DiffRequest.cc
   object_map/test_mock_InvalidateRequest.cc
   object_map/test_mock_LockRequest.cc
   object_map/test_mock_RefreshRequest.cc
diff --git a/src/test/librbd/object_map/test_mock_DiffRequest.cc b/src/test/librbd/object_map/test_mock_DiffRequest.cc
new file mode 100644 (file)
index 0000000..366bbb2
--- /dev/null
@@ -0,0 +1,490 @@
+// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*-
+// vim: ts=8 sw=2 smarttab
+
+#include "test/librbd/test_mock_fixture.h"
+#include "test/librbd/test_support.h"
+#include "test/librados_test_stub/MockTestMemIoCtxImpl.h"
+#include "include/rbd_types.h"
+#include "common/ceph_mutex.h"
+#include "librbd/object_map/DiffRequest.h"
+#include "gtest/gtest.h"
+#include "gmock/gmock.h"
+
+namespace librbd {
+namespace {
+
+struct MockTestImageCtx : public MockImageCtx {
+  MockTestImageCtx(ImageCtx &image_ctx) : MockImageCtx(image_ctx) {
+  }
+};
+
+} // anonymous namespace
+} // namespace librbd
+
+#include "librbd/object_map/DiffRequest.cc"
+
+using ::testing::_;
+using ::testing::Invoke;
+using ::testing::InSequence;
+using ::testing::StrEq;
+using ::testing::WithArg;
+
+namespace librbd {
+namespace object_map {
+
+class TestMockObjectMapDiffRequest : public TestMockFixture {
+public:
+  typedef DiffRequest<MockTestImageCtx> MockDiffRequest;
+
+  void SetUp() override {
+    TestMockFixture::SetUp();
+
+    ASSERT_EQ(0, open_image(m_image_name, &m_image_ctx));
+  }
+
+  void expect_get_flags(MockTestImageCtx& mock_image_ctx, uint64_t snap_id,
+                        int32_t flags, int r) {
+    EXPECT_CALL(mock_image_ctx, get_flags(snap_id, _))
+      .WillOnce(WithArg<1>(Invoke([flags, r](uint64_t *out_flags) {
+        *out_flags = flags;
+        return r;
+      })));
+  }
+
+  template <typename Lambda>
+  void expect_load_map(MockTestImageCtx& mock_image_ctx, uint64_t snap_id,
+                       const BitVector<2>& object_map, int r,
+                       Lambda&& lambda) {
+    std::string snap_oid(ObjectMap<>::object_map_name(mock_image_ctx.id,
+                                                      snap_id));
+    EXPECT_CALL(get_mock_io_ctx(mock_image_ctx.md_ctx),
+                exec(snap_oid, _, StrEq("rbd"), StrEq("object_map_load"), _,
+                     _, _))
+      .WillOnce(WithArg<5>(Invoke([object_map, r, lambda=std::move(lambda)]
+                                  (bufferlist* out_bl) {
+        lambda();
+
+        auto out_object_map{object_map};
+        out_object_map.set_crc_enabled(false);
+        encode(out_object_map, *out_bl);
+        return r;
+      })));
+  }
+
+  void expect_load_map(MockTestImageCtx& mock_image_ctx, uint64_t snap_id,
+                       const BitVector<2>& object_map, int r) {
+    expect_load_map(mock_image_ctx, snap_id, object_map, r, [](){});
+  }
+
+  librbd::ImageCtx* m_image_ctx = nullptr;
+  BitVector<2> m_object_diff_state;
+};
+
+TEST_F(TestMockObjectMapDiffRequest, InvalidStartSnap) {
+  MockTestImageCtx mock_image_ctx(*m_image_ctx);
+
+  InSequence seq;
+
+  C_SaferCond ctx;
+  auto req = new MockDiffRequest(&mock_image_ctx, CEPH_NOSNAP, 0,
+                                 &m_object_diff_state, &ctx);
+  req->send();
+  ASSERT_EQ(-EINVAL, ctx.wait());
+}
+
+TEST_F(TestMockObjectMapDiffRequest, StartEndSnapEqual) {
+  MockTestImageCtx mock_image_ctx(*m_image_ctx);
+
+  InSequence seq;
+
+  C_SaferCond ctx;
+  auto req = new MockDiffRequest(&mock_image_ctx, 1, 1,
+                                 &m_object_diff_state, &ctx);
+  req->send();
+  ASSERT_EQ(0, ctx.wait());
+  ASSERT_EQ(0U, m_object_diff_state.size());
+}
+
+TEST_F(TestMockObjectMapDiffRequest, FastDiffDisabled) {
+  // negative test -- object-map implicitly enables fast-diff
+  REQUIRE(!is_feature_enabled(RBD_FEATURE_OBJECT_MAP));
+
+  MockTestImageCtx mock_image_ctx(*m_image_ctx);
+
+  InSequence seq;
+
+  C_SaferCond ctx;
+  auto req = new MockDiffRequest(&mock_image_ctx, 0, CEPH_NOSNAP,
+                                 &m_object_diff_state, &ctx);
+  req->send();
+  ASSERT_EQ(-EINVAL, ctx.wait());
+}
+
+TEST_F(TestMockObjectMapDiffRequest, FastDiffInvalid) {
+  REQUIRE_FEATURE(RBD_FEATURE_FAST_DIFF);
+
+  MockTestImageCtx mock_image_ctx(*m_image_ctx);
+  mock_image_ctx.snap_info = {
+    {1U, {"snap1", {cls::rbd::UserSnapshotNamespace{}}, {}, {}, {}, {}, {}}}
+  };
+
+  InSequence seq;
+  expect_get_flags(mock_image_ctx, 1U, RBD_FLAG_FAST_DIFF_INVALID, 0);
+
+  C_SaferCond ctx;
+  auto req = new MockDiffRequest(&mock_image_ctx, 0, CEPH_NOSNAP,
+                                 &m_object_diff_state, &ctx);
+  req->send();
+  ASSERT_EQ(-EINVAL, ctx.wait());
+}
+
+TEST_F(TestMockObjectMapDiffRequest, FullDelta) {
+  REQUIRE_FEATURE(RBD_FEATURE_FAST_DIFF);
+
+  uint32_t object_count = 5;
+  m_image_ctx->size = object_count * (1 << m_image_ctx->order);
+
+  MockTestImageCtx mock_image_ctx(*m_image_ctx);
+  mock_image_ctx.snap_info = {
+    {1U, {"snap1", {cls::rbd::UserSnapshotNamespace{}}, mock_image_ctx.size, {},
+          {}, {}, {}}},
+    {2U, {"snap2", {cls::rbd::UserSnapshotNamespace{}}, mock_image_ctx.size, {},
+          {}, {}, {}}}
+  };
+
+  InSequence seq;
+
+  expect_get_flags(mock_image_ctx, 1U, 0, 0);
+
+  BitVector<2> object_map_1;
+  object_map_1.resize(object_count);
+  object_map_1[1] = OBJECT_EXISTS_CLEAN;
+  expect_load_map(mock_image_ctx, 1U, object_map_1, 0);
+
+  expect_get_flags(mock_image_ctx, 2U, 0, 0);
+
+  BitVector<2> object_map_2;
+  object_map_2.resize(object_count);
+  object_map_2[1] = OBJECT_EXISTS_CLEAN;
+  object_map_2[2] = OBJECT_EXISTS;
+  object_map_2[3] = OBJECT_EXISTS;
+  expect_load_map(mock_image_ctx, 2U, object_map_2, 0);
+
+  expect_get_flags(mock_image_ctx, CEPH_NOSNAP, 0, 0);
+
+  BitVector<2> object_map_head;
+  object_map_head.resize(object_count);
+  object_map_head[1] = OBJECT_EXISTS_CLEAN;
+  object_map_head[2] = OBJECT_EXISTS_CLEAN;
+  expect_load_map(mock_image_ctx, CEPH_NOSNAP, object_map_head, 0);
+
+  C_SaferCond ctx;
+  auto req = new MockDiffRequest(&mock_image_ctx, 0, CEPH_NOSNAP,
+                                 &m_object_diff_state, &ctx);
+  req->send();
+  ASSERT_EQ(0, ctx.wait());
+
+  BitVector<2> expected_diff_state;
+  expected_diff_state.resize(object_count);
+  expected_diff_state[1] = DIFF_STATE_UPDATED;
+  expected_diff_state[2] = DIFF_STATE_UPDATED;
+  expected_diff_state[3] = DIFF_STATE_HOLE;
+  ASSERT_EQ(expected_diff_state, m_object_diff_state);
+}
+
+TEST_F(TestMockObjectMapDiffRequest, IntermediateDelta) {
+  REQUIRE_FEATURE(RBD_FEATURE_FAST_DIFF);
+
+  uint32_t object_count = 5;
+  m_image_ctx->size = object_count * (1 << m_image_ctx->order);
+
+  MockTestImageCtx mock_image_ctx(*m_image_ctx);
+  mock_image_ctx.snap_info = {
+    {1U, {"snap1", {cls::rbd::UserSnapshotNamespace{}}, mock_image_ctx.size, {},
+          {}, {}, {}}},
+    {2U, {"snap2", {cls::rbd::UserSnapshotNamespace{}}, mock_image_ctx.size, {},
+          {}, {}, {}}}
+  };
+
+  InSequence seq;
+
+  expect_get_flags(mock_image_ctx, 1U, 0, 0);
+
+  BitVector<2> object_map_1;
+  object_map_1.resize(object_count);
+  object_map_1[1] = OBJECT_EXISTS_CLEAN;
+  object_map_1[2] = OBJECT_EXISTS_CLEAN;
+  expect_load_map(mock_image_ctx, 1U, object_map_1, 0);
+
+  expect_get_flags(mock_image_ctx, 2U, 0, 0);
+
+  BitVector<2> object_map_2;
+  object_map_2.resize(object_count);
+  object_map_2[1] = OBJECT_EXISTS_CLEAN;
+  object_map_2[2] = OBJECT_EXISTS;
+  object_map_2[3] = OBJECT_EXISTS;
+  expect_load_map(mock_image_ctx, 2U, object_map_2, 0);
+
+  C_SaferCond ctx;
+  auto req = new MockDiffRequest(&mock_image_ctx, 1, 2,
+                                 &m_object_diff_state, &ctx);
+  req->send();
+  ASSERT_EQ(0, ctx.wait());
+
+  BitVector<2> expected_diff_state;
+  expected_diff_state.resize(object_count);
+  expected_diff_state[2] = DIFF_STATE_UPDATED;
+  expected_diff_state[3] = DIFF_STATE_UPDATED;
+  ASSERT_EQ(expected_diff_state, m_object_diff_state);
+}
+
+TEST_F(TestMockObjectMapDiffRequest, EndDelta) {
+  REQUIRE_FEATURE(RBD_FEATURE_FAST_DIFF);
+
+  uint32_t object_count = 5;
+  m_image_ctx->size = object_count * (1 << m_image_ctx->order);
+
+  MockTestImageCtx mock_image_ctx(*m_image_ctx);
+  mock_image_ctx.snap_info = {
+    {1U, {"snap1", {cls::rbd::UserSnapshotNamespace{}}, mock_image_ctx.size, {},
+          {}, {}, {}}},
+    {2U, {"snap2", {cls::rbd::UserSnapshotNamespace{}}, mock_image_ctx.size, {},
+          {}, {}, {}}}
+  };
+
+  InSequence seq;
+
+  expect_get_flags(mock_image_ctx, 2U, 0, 0);
+
+  BitVector<2> object_map_2;
+  object_map_2.resize(object_count);
+  object_map_2[1] = OBJECT_EXISTS_CLEAN;
+  object_map_2[2] = OBJECT_EXISTS;
+  object_map_2[3] = OBJECT_EXISTS;
+  expect_load_map(mock_image_ctx, 2U, object_map_2, 0);
+
+  expect_get_flags(mock_image_ctx, CEPH_NOSNAP, 0, 0);
+
+  BitVector<2> object_map_head;
+  object_map_head.resize(object_count);
+  object_map_head[1] = OBJECT_EXISTS_CLEAN;
+  object_map_head[2] = OBJECT_EXISTS_CLEAN;
+  expect_load_map(mock_image_ctx, CEPH_NOSNAP, object_map_head, 0);
+
+  C_SaferCond ctx;
+  auto req = new MockDiffRequest(&mock_image_ctx, 2, CEPH_NOSNAP,
+                                 &m_object_diff_state, &ctx);
+  req->send();
+  ASSERT_EQ(0, ctx.wait());
+
+  BitVector<2> expected_diff_state;
+  expected_diff_state.resize(object_count);
+  expected_diff_state[3] = DIFF_STATE_HOLE;
+  ASSERT_EQ(expected_diff_state, m_object_diff_state);
+}
+
+TEST_F(TestMockObjectMapDiffRequest, StartSnapDNE) {
+  REQUIRE_FEATURE(RBD_FEATURE_FAST_DIFF);
+
+  uint32_t object_count = 5;
+  m_image_ctx->size = object_count * (1 << m_image_ctx->order);
+
+  MockTestImageCtx mock_image_ctx(*m_image_ctx);
+  mock_image_ctx.snap_info = {
+    {2U, {"snap2", {cls::rbd::UserSnapshotNamespace{}}, mock_image_ctx.size, {},
+          {}, {}, {}}}
+  };
+
+  InSequence seq;
+
+  C_SaferCond ctx;
+  auto req = new MockDiffRequest(&mock_image_ctx, 1, CEPH_NOSNAP,
+                                 &m_object_diff_state, &ctx);
+  req->send();
+  ASSERT_EQ(-ENOENT, ctx.wait());
+}
+
+TEST_F(TestMockObjectMapDiffRequest, EndSnapDNE) {
+  REQUIRE_FEATURE(RBD_FEATURE_FAST_DIFF);
+
+  uint32_t object_count = 5;
+  m_image_ctx->size = object_count * (1 << m_image_ctx->order);
+
+  MockTestImageCtx mock_image_ctx(*m_image_ctx);
+  mock_image_ctx.snap_info = {
+    {1U, {"snap1", {cls::rbd::UserSnapshotNamespace{}}, mock_image_ctx.size, {},
+          {}, {}, {}}}
+  };
+
+  InSequence seq;
+
+  expect_get_flags(mock_image_ctx, 1U, 0, 0);
+
+  BitVector<2> object_map_1;
+  object_map_1.resize(object_count);
+  expect_load_map(mock_image_ctx, 1U, object_map_1, 0);
+
+  C_SaferCond ctx;
+  auto req = new MockDiffRequest(&mock_image_ctx, 1, 2,
+                                 &m_object_diff_state, &ctx);
+  req->send();
+  ASSERT_EQ(-ENOENT, ctx.wait());
+}
+
+TEST_F(TestMockObjectMapDiffRequest, IntermediateSnapDNE) {
+  REQUIRE_FEATURE(RBD_FEATURE_FAST_DIFF);
+
+  uint32_t object_count = 5;
+  m_image_ctx->size = object_count * (1 << m_image_ctx->order);
+
+  MockTestImageCtx mock_image_ctx(*m_image_ctx);
+  mock_image_ctx.snap_info = {
+    {1U, {"snap1", {cls::rbd::UserSnapshotNamespace{}}, mock_image_ctx.size, {},
+          {}, {}, {}}},
+    {2U, {"snap2", {cls::rbd::UserSnapshotNamespace{}}, mock_image_ctx.size, {},
+          {}, {}, {}}}
+  };
+
+  InSequence seq;
+
+  expect_get_flags(mock_image_ctx, 1U, 0, 0);
+
+  BitVector<2> object_map_1;
+  object_map_1.resize(object_count);
+  object_map_1[1] = OBJECT_EXISTS_CLEAN;
+  expect_load_map(mock_image_ctx, 1U, object_map_1, 0,
+                  [&mock_image_ctx]() { mock_image_ctx.snap_info.erase(2); });
+
+  expect_get_flags(mock_image_ctx, CEPH_NOSNAP, 0, 0);
+
+  BitVector<2> object_map_head;
+  object_map_head.resize(object_count);
+  object_map_head[1] = OBJECT_EXISTS_CLEAN;
+  expect_load_map(mock_image_ctx, CEPH_NOSNAP, object_map_head, 0);
+
+  C_SaferCond ctx;
+  auto req = new MockDiffRequest(&mock_image_ctx, 0, CEPH_NOSNAP,
+                                 &m_object_diff_state, &ctx);
+  req->send();
+  ASSERT_EQ(0, ctx.wait());
+
+  BitVector<2> expected_diff_state;
+  expected_diff_state.resize(object_count);
+  expected_diff_state[1] = DIFF_STATE_UPDATED;
+  ASSERT_EQ(expected_diff_state, m_object_diff_state);
+}
+
+TEST_F(TestMockObjectMapDiffRequest, LoadObjectMapDNE) {
+  REQUIRE_FEATURE(RBD_FEATURE_FAST_DIFF);
+
+  uint32_t object_count = 5;
+  m_image_ctx->size = object_count * (1 << m_image_ctx->order);
+
+  MockTestImageCtx mock_image_ctx(*m_image_ctx);
+
+  InSequence seq;
+
+  expect_get_flags(mock_image_ctx, CEPH_NOSNAP, 0, 0);
+
+  BitVector<2> object_map_head;
+  expect_load_map(mock_image_ctx, CEPH_NOSNAP, object_map_head, -ENOENT);
+
+  C_SaferCond ctx;
+  auto req = new MockDiffRequest(&mock_image_ctx, 0, CEPH_NOSNAP,
+                                 &m_object_diff_state, &ctx);
+  req->send();
+  ASSERT_EQ(-ENOENT, ctx.wait());
+}
+
+TEST_F(TestMockObjectMapDiffRequest, LoadIntermediateObjectMapDNE) {
+  REQUIRE_FEATURE(RBD_FEATURE_FAST_DIFF);
+
+  uint32_t object_count = 5;
+  m_image_ctx->size = object_count * (1 << m_image_ctx->order);
+
+  MockTestImageCtx mock_image_ctx(*m_image_ctx);
+  mock_image_ctx.snap_info = {
+    {1U, {"snap1", {cls::rbd::UserSnapshotNamespace{}}, mock_image_ctx.size, {},
+          {}, {}, {}}}
+  };
+
+  InSequence seq;
+
+  expect_get_flags(mock_image_ctx, 1U, 0, 0);
+
+  BitVector<2> object_map_1;
+  expect_load_map(mock_image_ctx, 1U, object_map_1, -ENOENT);
+
+  expect_get_flags(mock_image_ctx, CEPH_NOSNAP, 0, 0);
+
+  BitVector<2> object_map_head;
+  object_map_head.resize(object_count);
+  object_map_head[1] = OBJECT_EXISTS_CLEAN;
+  expect_load_map(mock_image_ctx, CEPH_NOSNAP, object_map_head, 0);
+
+  C_SaferCond ctx;
+  auto req = new MockDiffRequest(&mock_image_ctx, 0, CEPH_NOSNAP,
+                                 &m_object_diff_state, &ctx);
+  req->send();
+  ASSERT_EQ(0, ctx.wait());
+
+  BitVector<2> expected_diff_state;
+  expected_diff_state.resize(object_count);
+  expected_diff_state[1] = DIFF_STATE_UPDATED;
+  ASSERT_EQ(expected_diff_state, m_object_diff_state);
+}
+
+TEST_F(TestMockObjectMapDiffRequest, LoadObjectMapError) {
+  REQUIRE_FEATURE(RBD_FEATURE_FAST_DIFF);
+
+  uint32_t object_count = 5;
+  m_image_ctx->size = object_count * (1 << m_image_ctx->order);
+
+  MockTestImageCtx mock_image_ctx(*m_image_ctx);
+  mock_image_ctx.snap_info = {
+    {1U, {"snap1", {cls::rbd::UserSnapshotNamespace{}}, mock_image_ctx.size, {},
+          {}, {}, {}}}
+  };
+
+  InSequence seq;
+
+  expect_get_flags(mock_image_ctx, 1U, 0, 0);
+
+  BitVector<2> object_map_1;
+  expect_load_map(mock_image_ctx, 1U, object_map_1, -EPERM);
+
+  C_SaferCond ctx;
+  auto req = new MockDiffRequest(&mock_image_ctx, 0, CEPH_NOSNAP,
+                                 &m_object_diff_state, &ctx);
+  req->send();
+  ASSERT_EQ(-EPERM, ctx.wait());
+}
+
+TEST_F(TestMockObjectMapDiffRequest, ObjectMapTooSmall) {
+  REQUIRE_FEATURE(RBD_FEATURE_FAST_DIFF);
+
+  uint32_t object_count = 5;
+  m_image_ctx->size = object_count * (1 << m_image_ctx->order);
+
+  MockTestImageCtx mock_image_ctx(*m_image_ctx);
+  mock_image_ctx.snap_info = {
+    {1U, {"snap1", {cls::rbd::UserSnapshotNamespace{}}, mock_image_ctx.size, {},
+          {}, {}, {}}}
+  };
+
+  InSequence seq;
+
+  expect_get_flags(mock_image_ctx, 1U, 0, 0);
+
+  BitVector<2> object_map_1;
+  expect_load_map(mock_image_ctx, 1U, object_map_1, 0);
+
+  C_SaferCond ctx;
+  auto req = new MockDiffRequest(&mock_image_ctx, 0, CEPH_NOSNAP,
+                                 &m_object_diff_state, &ctx);
+  req->send();
+  ASSERT_EQ(-EINVAL, ctx.wait());
+}
+
+} // namespace object_map
+} // librbd