From 2b8bed9d5e9365f00f0659bb8a732f2a8d865d88 Mon Sep 17 00:00:00 2001 From: Ilya Dryomov Date: Sat, 26 Aug 2023 13:04:52 +0200 Subject: [PATCH] librbd: make CreatePrimaryRequest remove any unlinked mirror snapshots After commit ac552c9b4d65 ("librbd: localize snap_remove op for mirror snapshots"), rbd-mirror daemon no longer removes mirror snapshots when it's done syncing them -- instead it only unlinks from them. However, CreatePrimaryRequest state machine was not adjusted to compensate and hence two cases were missed: - primary demotion snapshot (rbd-mirror daemon unlinks from primary demotion snapshots just like it does from regular primary snapshots); this comes up when an image is demoted but then promoted on the same cluster - non-primary demotion snapshot (unlike regular non-primary snapshots, non-primary demotion snapshots store peer uuids and rbd-mirror daemon does unlinking just like in the case of primary snapshots); this comes up when an image is demoted and promoted on the other cluster Related is the case of orphan snapshots. Since they are dummy to begin with, CreatePrimaryRequest would now clean up the orphan snapshot after the creation of the force promote snapshot. Fixes: https://tracker.ceph.com/issues/61707 Co-authored-by: Christopher Hoffman Signed-off-by: Ilya Dryomov (cherry picked from commit 9c05d3d81f4b06af2cfd47376e9ad86369bdf8cf) --- .../mirror/snapshot/CreatePrimaryRequest.cc | 91 +++-- .../test_mock_CreatePrimaryRequest.cc | 366 +++++++++++++++++- 2 files changed, 406 insertions(+), 51 deletions(-) diff --git a/src/librbd/mirror/snapshot/CreatePrimaryRequest.cc b/src/librbd/mirror/snapshot/CreatePrimaryRequest.cc index fd6f9b5023d..c8e3a4fe7a6 100644 --- a/src/librbd/mirror/snapshot/CreatePrimaryRequest.cc +++ b/src/librbd/mirror/snapshot/CreatePrimaryRequest.cc @@ -177,6 +177,7 @@ void CreatePrimaryRequest::handle_refresh_image(int r) { template void CreatePrimaryRequest::unlink_peer() { + // TODO: Document semantics for unlink_peer uint64_t max_snapshots = m_image_ctx->config.template get_val( "rbd_mirroring_max_mirroring_snapshots"); ceph_assert(max_snapshots >= 3); @@ -184,55 +185,61 @@ void CreatePrimaryRequest::unlink_peer() { std::string peer_uuid; uint64_t snap_id = CEPH_NOSNAP; - for (auto &peer : m_mirror_peer_uuids) { + { std::shared_lock image_locker{m_image_ctx->image_lock}; - size_t count = 0; - uint64_t unlink_snap_id = 0; - for (auto &snap_it : m_image_ctx->snap_info) { - auto info = std::get_if( - &snap_it.second.snap_namespace); - if (info == nullptr) { - continue; - } - if (info->state != cls::rbd::MIRROR_SNAPSHOT_STATE_PRIMARY) { - // reset counters -- we count primary snapshots after the last promotion - count = 0; - unlink_snap_id = 0; - continue; - } - // call UnlinkPeerRequest only if the snapshot is linked with this peer - // or if it's not linked with any peer (happens if mirroring is enabled - // on a pool with no peers configured or if UnlinkPeerRequest gets - // interrupted) - if (!info->mirror_peer_uuids.empty() && - info->mirror_peer_uuids.count(peer) == 0) { - continue; - } - if (info->mirror_peer_uuids.empty() || !info->complete) { - peer_uuid = peer; - snap_id = snap_it.first; - break; - } - count++; - if (count == max_snapshots) { - unlink_snap_id = snap_it.first; - } - if (count > max_snapshots) { - peer_uuid = peer; - snap_id = unlink_snap_id; - break; + for (const auto& peer : m_mirror_peer_uuids) { + for (const auto& snap_info_pair : m_image_ctx->snap_info) { + auto info = std::get_if( + &snap_info_pair.second.snap_namespace); + if (info == nullptr) { + continue; + } + if (info->mirror_peer_uuids.empty() || + (info->mirror_peer_uuids.count(peer) != 0 && + info->is_primary() && !info->complete)) { + peer_uuid = peer; + snap_id = snap_info_pair.first; + goto do_unlink; + } } } - if (snap_id != CEPH_NOSNAP) { - break; + for (const auto& peer : m_mirror_peer_uuids) { + size_t count = 0; + uint64_t unlink_snap_id = 0; + for (const auto& snap_info_pair : m_image_ctx->snap_info) { + auto info = std::get_if( + &snap_info_pair.second.snap_namespace); + if (info == nullptr) { + continue; + } + if (info->state != cls::rbd::MIRROR_SNAPSHOT_STATE_PRIMARY) { + // reset counters -- we count primary snapshots after the last + // promotion + count = 0; + unlink_snap_id = 0; + continue; + } + if (info->mirror_peer_uuids.count(peer) == 0) { + // snapshot is not linked with this peer + continue; + } + count++; + if (count == max_snapshots) { + unlink_snap_id = snap_info_pair.first; + } + if (count > max_snapshots) { + peer_uuid = peer; + snap_id = unlink_snap_id; + goto do_unlink; + } + } } } - if (snap_id == CEPH_NOSNAP) { - finish(0); - return; - } + finish(0); + return; +do_unlink: CephContext *cct = m_image_ctx->cct; ldout(cct, 15) << "peer=" << peer_uuid << ", snap_id=" << snap_id << dendl; diff --git a/src/test/librbd/mirror/snapshot/test_mock_CreatePrimaryRequest.cc b/src/test/librbd/mirror/snapshot/test_mock_CreatePrimaryRequest.cc index 22692e15f28..8bfdcdeb1d8 100644 --- a/src/test/librbd/mirror/snapshot/test_mock_CreatePrimaryRequest.cc +++ b/src/test/librbd/mirror/snapshot/test_mock_CreatePrimaryRequest.cc @@ -233,6 +233,165 @@ TEST_F(TestMockMirrorSnapshotCreatePrimaryRequest, Success) { ASSERT_EQ(0, ctx.wait()); } +TEST_F(TestMockMirrorSnapshotCreatePrimaryRequest, SuccessPrimary) { + REQUIRE_FORMAT_V2(); + + librbd::ImageCtx *ictx; + ASSERT_EQ(0, open_image(m_image_name, &ictx)); + + MockTestImageCtx mock_image_ctx(*ictx); + cls::rbd::MirrorSnapshotNamespace ns{ + cls::rbd::MIRROR_SNAPSHOT_STATE_PRIMARY, {"uuid"}, "", CEPH_NOSNAP}; + ns.complete = true; + snap_create(mock_image_ctx, ns, "mirror_snap"); + + InSequence seq; + + expect_clone_md_ctx(mock_image_ctx); + MockUtils mock_utils; + expect_can_create_primary_snapshot(mock_utils, false, false, true); + expect_get_mirror_peers(mock_image_ctx, + {{"uuid", cls::rbd::MIRROR_PEER_DIRECTION_TX, "ceph", + "mirror", "mirror uuid"}}, 0); + expect_create_snapshot(mock_image_ctx, 0); + expect_refresh_image(mock_image_ctx, 0); + + C_SaferCond ctx; + auto req = new MockCreatePrimaryRequest(&mock_image_ctx, "gid", CEPH_NOSNAP, + 0U, 0U, nullptr, &ctx); + req->send(); + ASSERT_EQ(0, ctx.wait()); +} + +TEST_F(TestMockMirrorSnapshotCreatePrimaryRequest, SuccessPrimaryDemoted) { + REQUIRE_FORMAT_V2(); + + librbd::ImageCtx *ictx; + ASSERT_EQ(0, open_image(m_image_name, &ictx)); + + MockTestImageCtx mock_image_ctx(*ictx); + cls::rbd::MirrorSnapshotNamespace ns{ + cls::rbd::MIRROR_SNAPSHOT_STATE_PRIMARY_DEMOTED, {"uuid"}, "", CEPH_NOSNAP}; + ns.complete = true; + snap_create(mock_image_ctx, ns, "mirror_snap"); + + InSequence seq; + + expect_clone_md_ctx(mock_image_ctx); + MockUtils mock_utils; + expect_can_create_primary_snapshot(mock_utils, false, false, true); + expect_get_mirror_peers(mock_image_ctx, + {{"uuid", cls::rbd::MIRROR_PEER_DIRECTION_TX, "ceph", + "mirror", "mirror uuid"}}, 0); + expect_create_snapshot(mock_image_ctx, 0); + expect_refresh_image(mock_image_ctx, 0); + + C_SaferCond ctx; + auto req = new MockCreatePrimaryRequest(&mock_image_ctx, "gid", CEPH_NOSNAP, + 0U, 0U, nullptr, &ctx); + req->send(); + ASSERT_EQ(0, ctx.wait()); +} + +TEST_F(TestMockMirrorSnapshotCreatePrimaryRequest, SuccessNonPrimaryDemoted) { + REQUIRE_FORMAT_V2(); + + librbd::ImageCtx *ictx; + ASSERT_EQ(0, open_image(m_image_name, &ictx)); + + MockTestImageCtx mock_image_ctx(*ictx); + cls::rbd::MirrorSnapshotNamespace ns{ + cls::rbd::MIRROR_SNAPSHOT_STATE_NON_PRIMARY_DEMOTED, {"uuid"}, + "mirror uuid", 123}; + ns.complete = true; + snap_create(mock_image_ctx, ns, "mirror_snap"); + + InSequence seq; + + expect_clone_md_ctx(mock_image_ctx); + MockUtils mock_utils; + expect_can_create_primary_snapshot(mock_utils, false, false, true); + expect_get_mirror_peers(mock_image_ctx, + {{"uuid", cls::rbd::MIRROR_PEER_DIRECTION_TX, "ceph", + "mirror", "mirror uuid"}}, 0); + expect_create_snapshot(mock_image_ctx, 0); + expect_refresh_image(mock_image_ctx, 0); + + C_SaferCond ctx; + auto req = new MockCreatePrimaryRequest(&mock_image_ctx, "gid", CEPH_NOSNAP, + 0U, 0U, nullptr, &ctx); + req->send(); + ASSERT_EQ(0, ctx.wait()); +} + +TEST_F(TestMockMirrorSnapshotCreatePrimaryRequest, SuccessPrimaryBelowMaxSnapshots) { + REQUIRE_FORMAT_V2(); + + librbd::ImageCtx *ictx; + ASSERT_EQ(0, open_image(m_image_name, &ictx)); + ictx->config.set_val("rbd_mirroring_max_mirroring_snapshots", "3"); + + MockTestImageCtx mock_image_ctx(*ictx); + for (int i = 0; i < 2; i++) { + cls::rbd::MirrorSnapshotNamespace ns{ + cls::rbd::MIRROR_SNAPSHOT_STATE_PRIMARY, {"uuid"}, "", CEPH_NOSNAP}; + ns.complete = true; + snap_create(mock_image_ctx, ns, "mirror_snap"); + } + + InSequence seq; + + expect_clone_md_ctx(mock_image_ctx); + MockUtils mock_utils; + expect_can_create_primary_snapshot(mock_utils, false, false, true); + expect_get_mirror_peers(mock_image_ctx, + {{"uuid", cls::rbd::MIRROR_PEER_DIRECTION_TX, "ceph", + "mirror", "mirror uuid"}}, 0); + expect_create_snapshot(mock_image_ctx, 0); + expect_refresh_image(mock_image_ctx, 0); + + C_SaferCond ctx; + auto req = new MockCreatePrimaryRequest(&mock_image_ctx, "gid", CEPH_NOSNAP, + 0U, 0U, nullptr, &ctx); + req->send(); + ASSERT_EQ(0, ctx.wait()); +} + +TEST_F(TestMockMirrorSnapshotCreatePrimaryRequest, SuccessPrimaryBelowMaxSnapshotsReset) { + REQUIRE_FORMAT_V2(); + + librbd::ImageCtx *ictx; + ASSERT_EQ(0, open_image(m_image_name, &ictx)); + ictx->config.set_val("rbd_mirroring_max_mirroring_snapshots", "3"); + + MockTestImageCtx mock_image_ctx(*ictx); + for (int i = 0; i < 6; i++) { + cls::rbd::MirrorSnapshotNamespace ns{ + (i == 3 ? cls::rbd::MIRROR_SNAPSHOT_STATE_PRIMARY_DEMOTED : + cls::rbd::MIRROR_SNAPSHOT_STATE_PRIMARY), + {"uuid"}, "", CEPH_NOSNAP}; + ns.complete = true; + snap_create(mock_image_ctx, ns, "mirror_snap"); + } + + InSequence seq; + + expect_clone_md_ctx(mock_image_ctx); + MockUtils mock_utils; + expect_can_create_primary_snapshot(mock_utils, false, false, true); + expect_get_mirror_peers(mock_image_ctx, + {{"uuid", cls::rbd::MIRROR_PEER_DIRECTION_TX, "ceph", + "mirror", "mirror uuid"}}, 0); + expect_create_snapshot(mock_image_ctx, 0); + expect_refresh_image(mock_image_ctx, 0); + + C_SaferCond ctx; + auto req = new MockCreatePrimaryRequest(&mock_image_ctx, "gid", CEPH_NOSNAP, + 0U, 0U, nullptr, &ctx); + req->send(); + ASSERT_EQ(0, ctx.wait()); +} + TEST_F(TestMockMirrorSnapshotCreatePrimaryRequest, CanNotError) { REQUIRE_FORMAT_V2(); @@ -303,12 +462,152 @@ TEST_F(TestMockMirrorSnapshotCreatePrimaryRequest, CreateSnapshotError) { ASSERT_EQ(-EINVAL, ctx.wait()); } -TEST_F(TestMockMirrorSnapshotCreatePrimaryRequest, SuccessUnlinkIncomplete) { +TEST_F(TestMockMirrorSnapshotCreatePrimaryRequest, SuccessUnlinkPrimaryNoPeer) { + REQUIRE_FORMAT_V2(); + + librbd::ImageCtx *ictx; + ASSERT_EQ(0, open_image(m_image_name, &ictx)); + + MockTestImageCtx mock_image_ctx(*ictx); + cls::rbd::MirrorSnapshotNamespace ns{ + cls::rbd::MIRROR_SNAPSHOT_STATE_PRIMARY, {}, "", CEPH_NOSNAP}; + ns.complete = true; + snap_create(mock_image_ctx, ns, "mirror_snap"); + + InSequence seq; + + expect_clone_md_ctx(mock_image_ctx); + MockUtils mock_utils; + expect_can_create_primary_snapshot(mock_utils, false, false, true); + expect_get_mirror_peers(mock_image_ctx, + {{"uuid", cls::rbd::MIRROR_PEER_DIRECTION_TX, "ceph", + "mirror", "mirror uuid"}}, 0); + expect_create_snapshot(mock_image_ctx, 0); + expect_refresh_image(mock_image_ctx, 0); + MockUnlinkPeerRequest mock_unlink_peer_request; + auto it = mock_image_ctx.snap_info.rbegin(); + auto snap_id = it->first; + expect_unlink_peer(mock_image_ctx, mock_unlink_peer_request, snap_id, "uuid", + false, true, true, 0); + + C_SaferCond ctx; + auto req = new MockCreatePrimaryRequest(&mock_image_ctx, "gid", CEPH_NOSNAP, + 0U, 0U, nullptr, &ctx); + req->send(); + ASSERT_EQ(0, ctx.wait()); +} + +TEST_F(TestMockMirrorSnapshotCreatePrimaryRequest, SuccessUnlinkPrimaryDemotedNoPeer) { + REQUIRE_FORMAT_V2(); + + librbd::ImageCtx *ictx; + ASSERT_EQ(0, open_image(m_image_name, &ictx)); + + MockTestImageCtx mock_image_ctx(*ictx); + cls::rbd::MirrorSnapshotNamespace ns{ + cls::rbd::MIRROR_SNAPSHOT_STATE_PRIMARY_DEMOTED, {}, "", CEPH_NOSNAP}; + ns.complete = true; + snap_create(mock_image_ctx, ns, "mirror_snap"); + + InSequence seq; + + expect_clone_md_ctx(mock_image_ctx); + MockUtils mock_utils; + expect_can_create_primary_snapshot(mock_utils, false, false, true); + expect_get_mirror_peers(mock_image_ctx, + {{"uuid", cls::rbd::MIRROR_PEER_DIRECTION_TX, "ceph", + "mirror", "mirror uuid"}}, 0); + expect_create_snapshot(mock_image_ctx, 0); + expect_refresh_image(mock_image_ctx, 0); + MockUnlinkPeerRequest mock_unlink_peer_request; + auto it = mock_image_ctx.snap_info.rbegin(); + auto snap_id = it->first; + expect_unlink_peer(mock_image_ctx, mock_unlink_peer_request, snap_id, "uuid", + false, true, true, 0); + + C_SaferCond ctx; + auto req = new MockCreatePrimaryRequest(&mock_image_ctx, "gid", CEPH_NOSNAP, + 0U, 0U, nullptr, &ctx); + req->send(); + ASSERT_EQ(0, ctx.wait()); +} + +TEST_F(TestMockMirrorSnapshotCreatePrimaryRequest, SuccessUnlinkNonPrimaryDemotedNoPeer) { + REQUIRE_FORMAT_V2(); + + librbd::ImageCtx *ictx; + ASSERT_EQ(0, open_image(m_image_name, &ictx)); + + MockTestImageCtx mock_image_ctx(*ictx); + cls::rbd::MirrorSnapshotNamespace ns{ + cls::rbd::MIRROR_SNAPSHOT_STATE_NON_PRIMARY_DEMOTED, {}, + "mirror uuid", 123}; + ns.complete = true; + snap_create(mock_image_ctx, ns, "mirror_snap"); + + InSequence seq; + + expect_clone_md_ctx(mock_image_ctx); + MockUtils mock_utils; + expect_can_create_primary_snapshot(mock_utils, false, false, true); + expect_get_mirror_peers(mock_image_ctx, + {{"uuid", cls::rbd::MIRROR_PEER_DIRECTION_TX, "ceph", + "mirror", "mirror uuid"}}, 0); + expect_create_snapshot(mock_image_ctx, 0); + expect_refresh_image(mock_image_ctx, 0); + MockUnlinkPeerRequest mock_unlink_peer_request; + auto it = mock_image_ctx.snap_info.rbegin(); + auto snap_id = it->first; + expect_unlink_peer(mock_image_ctx, mock_unlink_peer_request, snap_id, "uuid", + false, true, true, 0); + + C_SaferCond ctx; + auto req = new MockCreatePrimaryRequest(&mock_image_ctx, "gid", CEPH_NOSNAP, + 0U, 0U, nullptr, &ctx); + req->send(); + ASSERT_EQ(0, ctx.wait()); +} + +TEST_F(TestMockMirrorSnapshotCreatePrimaryRequest, SuccessUnlinkOrphanNoPeer) { + REQUIRE_FORMAT_V2(); + + librbd::ImageCtx *ictx; + ASSERT_EQ(0, open_image(m_image_name, &ictx)); + + MockTestImageCtx mock_image_ctx(*ictx); + cls::rbd::MirrorSnapshotNamespace ns{ + cls::rbd::MIRROR_SNAPSHOT_STATE_NON_PRIMARY, {}, "", CEPH_NOSNAP}; + ns.complete = true; + snap_create(mock_image_ctx, ns, "mirror_snap"); + + InSequence seq; + + expect_clone_md_ctx(mock_image_ctx); + MockUtils mock_utils; + expect_can_create_primary_snapshot(mock_utils, false, false, true); + expect_get_mirror_peers(mock_image_ctx, + {{"uuid", cls::rbd::MIRROR_PEER_DIRECTION_TX, "ceph", + "mirror", "mirror uuid"}}, 0); + expect_create_snapshot(mock_image_ctx, 0); + expect_refresh_image(mock_image_ctx, 0); + MockUnlinkPeerRequest mock_unlink_peer_request; + auto it = mock_image_ctx.snap_info.rbegin(); + auto snap_id = it->first; + expect_unlink_peer(mock_image_ctx, mock_unlink_peer_request, snap_id, "uuid", + false, true, true, 0); + + C_SaferCond ctx; + auto req = new MockCreatePrimaryRequest(&mock_image_ctx, "gid", CEPH_NOSNAP, + 0U, 0U, nullptr, &ctx); + req->send(); + ASSERT_EQ(0, ctx.wait()); +} + +TEST_F(TestMockMirrorSnapshotCreatePrimaryRequest, SuccessUnlinkPrimaryIncomplete) { REQUIRE_FORMAT_V2(); librbd::ImageCtx *ictx; ASSERT_EQ(0, open_image(m_image_name, &ictx)); - ictx->config.set_val("rbd_mirroring_max_mirroring_snapshots", "3"); MockTestImageCtx mock_image_ctx(*ictx); cls::rbd::MirrorSnapshotNamespace ns{ @@ -331,6 +630,7 @@ TEST_F(TestMockMirrorSnapshotCreatePrimaryRequest, SuccessUnlinkIncomplete) { auto snap_id = it->first; expect_unlink_peer(mock_image_ctx, mock_unlink_peer_request, snap_id, "uuid", true, false, true, 0); + C_SaferCond ctx; auto req = new MockCreatePrimaryRequest(&mock_image_ctx, "gid", CEPH_NOSNAP, 0U, 0U, nullptr, &ctx); @@ -338,7 +638,7 @@ TEST_F(TestMockMirrorSnapshotCreatePrimaryRequest, SuccessUnlinkIncomplete) { ASSERT_EQ(0, ctx.wait()); } -TEST_F(TestMockMirrorSnapshotCreatePrimaryRequest, SuccessUnlinkPeer) { +TEST_F(TestMockMirrorSnapshotCreatePrimaryRequest, SuccessUnlinkPrimaryMaxSnapshots) { REQUIRE_FORMAT_V2(); librbd::ImageCtx *ictx; @@ -368,6 +668,7 @@ TEST_F(TestMockMirrorSnapshotCreatePrimaryRequest, SuccessUnlinkPeer) { auto snap_id = it->first; expect_unlink_peer(mock_image_ctx, mock_unlink_peer_request, snap_id, "uuid", true, true, true, 0); + C_SaferCond ctx; auto req = new MockCreatePrimaryRequest(&mock_image_ctx, "gid", CEPH_NOSNAP, 0U, 0U, nullptr, &ctx); @@ -375,7 +676,7 @@ TEST_F(TestMockMirrorSnapshotCreatePrimaryRequest, SuccessUnlinkPeer) { ASSERT_EQ(0, ctx.wait()); } -TEST_F(TestMockMirrorSnapshotCreatePrimaryRequest, SuccessUnlinkNoPeer) { +TEST_F(TestMockMirrorSnapshotCreatePrimaryRequest, SuccessUnlinkPrimaryMaxSnapshotsReset) { REQUIRE_FORMAT_V2(); librbd::ImageCtx *ictx; @@ -383,10 +684,14 @@ TEST_F(TestMockMirrorSnapshotCreatePrimaryRequest, SuccessUnlinkNoPeer) { ictx->config.set_val("rbd_mirroring_max_mirroring_snapshots", "3"); MockTestImageCtx mock_image_ctx(*ictx); - cls::rbd::MirrorSnapshotNamespace ns{ - cls::rbd::MIRROR_SNAPSHOT_STATE_PRIMARY, {}, "", CEPH_NOSNAP}; - ns.complete = true; - snap_create(mock_image_ctx, ns, "mirror_snap"); + for (int i = 0; i < 7; i++) { + cls::rbd::MirrorSnapshotNamespace ns{ + (i == 3 ? cls::rbd::MIRROR_SNAPSHOT_STATE_PRIMARY_DEMOTED : + cls::rbd::MIRROR_SNAPSHOT_STATE_PRIMARY), + {"uuid"}, "", CEPH_NOSNAP}; + ns.complete = true; + snap_create(mock_image_ctx, ns, "mirror_snap"); + } InSequence seq; @@ -402,7 +707,7 @@ TEST_F(TestMockMirrorSnapshotCreatePrimaryRequest, SuccessUnlinkNoPeer) { auto it = mock_image_ctx.snap_info.rbegin(); auto snap_id = it->first; expect_unlink_peer(mock_image_ctx, mock_unlink_peer_request, snap_id, "uuid", - false, true, true, 0); + true, true, true, 0); C_SaferCond ctx; auto req = new MockCreatePrimaryRequest(&mock_image_ctx, "gid", CEPH_NOSNAP, @@ -446,6 +751,49 @@ TEST_F(TestMockMirrorSnapshotCreatePrimaryRequest, SuccessUnlinkMultiplePeers) { true, true, true, 0); expect_unlink_peer(mock_image_ctx, mock_unlink_peer_request, snap_id, "uuid2", true, true, true, 0); + + C_SaferCond ctx; + auto req = new MockCreatePrimaryRequest(&mock_image_ctx, "gid", CEPH_NOSNAP, + 0U, 0U, nullptr, &ctx); + req->send(); + ASSERT_EQ(0, ctx.wait()); +} + +TEST_F(TestMockMirrorSnapshotCreatePrimaryRequest, SuccessUnlinkMultipleSnapshots) { + REQUIRE_FORMAT_V2(); + + librbd::ImageCtx *ictx; + ASSERT_EQ(0, open_image(m_image_name, &ictx)); + + MockTestImageCtx mock_image_ctx(*ictx); + cls::rbd::MirrorSnapshotNamespace ns1{ + cls::rbd::MIRROR_SNAPSHOT_STATE_PRIMARY, {}, "", CEPH_NOSNAP}; + ns1.complete = true; + snap_create(mock_image_ctx, ns1, "mirror_snap"); + cls::rbd::MirrorSnapshotNamespace ns2{ + cls::rbd::MIRROR_SNAPSHOT_STATE_PRIMARY_DEMOTED, {}, "", CEPH_NOSNAP}; + ns2.complete = true; + snap_create(mock_image_ctx, ns2, "mirror_snap"); + + InSequence seq; + + expect_clone_md_ctx(mock_image_ctx); + MockUtils mock_utils; + expect_can_create_primary_snapshot(mock_utils, false, false, true); + expect_get_mirror_peers(mock_image_ctx, + {{"uuid", cls::rbd::MIRROR_PEER_DIRECTION_TX, "ceph", + "mirror", "mirror uuid"}}, 0); + expect_create_snapshot(mock_image_ctx, 0); + expect_refresh_image(mock_image_ctx, 0); + MockUnlinkPeerRequest mock_unlink_peer_request; + auto it = mock_image_ctx.snap_info.begin(); + auto snap_id = it->first; + expect_unlink_peer(mock_image_ctx, mock_unlink_peer_request, snap_id, "uuid", + false, true, true, 0); + snap_id = (++it)->first; + expect_unlink_peer(mock_image_ctx, mock_unlink_peer_request, snap_id, "uuid", + false, true, true, 0); + C_SaferCond ctx; auto req = new MockCreatePrimaryRequest(&mock_image_ctx, "gid", CEPH_NOSNAP, 0U, 0U, nullptr, &ctx); -- 2.39.5