Skip to content

Commit 2fac253

Browse files
committed
[BP] MB-56644: Make ItemExpel resilient to VBucket rollback
[Backport to 7.1.5] ItemExpel isn't an atomic operation as that acquires/releases the CM::lock multiple times. So in the middle of the Expel execution VBucket rollback might clear the CheckpointList and thus remove the checkpoint touched by Expel. Before this fix assertions in CM might trigger and crash the node. The fix makes ItemExpel resilient to the rollback scenario. ItemExpel now is capable of identifying the rollback post-conditions and just gives up if the checkpoint under processing doesn't exist anymore. Change-Id: I03bf6e82ca9fb799666f5265298e5bcc3ac2b269 Reviewed-on: https://review.couchbase.org/c/kv_engine/+/193528 Reviewed-by: Daniel Owen <[email protected]> Reviewed-by: Dave Rigby <[email protected]> Well-Formed: Restriction Checker Tested-by: Build Bot <[email protected]>
1 parent 6bcf6a9 commit 2fac253

File tree

3 files changed

+72
-6
lines changed

3 files changed

+72
-6
lines changed

engines/ep/src/checkpoint_manager.cc

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -628,16 +628,28 @@ CheckpointManager::expelUnreferencedCheckpointItems() {
628628
// queueLock already released here, O(N) deallocation is lock-free
629629
const auto queuedItemsMemReleased = extractRes.deleteItems();
630630

631+
// Test hook that executes before CM::lock is re-acquired
632+
expelHook();
633+
631634
{
632635
// Acquire the queueLock just for the very short time necessary for
633-
// updating the checkpoint's queued-items mem-usage and removing the
634-
// expel-cursor.
635-
636+
// updating the checkpoint's queued-items mem-usage.
637+
//
636638
// Note that the presence of the expel-cursor at this step ensures that
637-
// the checkpoint is still in the CheckpointList.
639+
// the checkpoint is still in the CheckpointList; unless the VBucket has
640+
// rolled-back (see the following).
641+
// Expel-cursor is released once extractRes is destroyed at caller.
638642
std::lock_guard<std::mutex> lh(queueLock);
639643
auto* checkpoint = extractRes.getCheckpoint();
640644
Expects(checkpoint);
645+
646+
// Expel always touches the oldest checkpoint in the list.
647+
// The checkpoint touched by Expel might not exist anymore if the
648+
// VBucket has rolled-back. Just give up in that case.
649+
if (checkpoint != checkpointList.begin()->get()) {
650+
return {0, 0};
651+
}
652+
641653
Expects(extractRes.getExpelCursor().getCheckpoint()->get() ==
642654
checkpoint);
643655
checkpoint->applyQueuedItemsMemUsageDecrement(queuedItemsMemReleased);
@@ -1965,8 +1977,7 @@ CheckpointManager::ExtractItemsResult CheckpointManager::extractItemsToExpel(
19651977
// with the lowest seqno should be in that checkpoint.
19661978
if (lowestCursor->getCheckpoint()->get() != oldestCheckpoint) {
19671979
std::stringstream ss;
1968-
ss << "CheckpointManager::expelUnreferencedCheckpointItems: ("
1969-
<< vb.getId()
1980+
ss << "CheckpointManager::extractItemsToExpel: (" << vb.getId()
19701981
<< ") lowest found cursor is not in the oldest "
19711982
"checkpoint. Oldest checkpoint ID: "
19721983
<< oldestCheckpoint->getId()

engines/ep/src/checkpoint_manager.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -612,6 +612,10 @@ class CheckpointManager {
612612
// Introduced in MB-45757 for testing a race condition on invalidate-cursor
613613
TestingHook<> removeCursorPreLockHook;
614614

615+
// Test hook that executes in the section where ItemExpel has released
616+
// (and not yet re-acquired) the CM::lock. Introduced in MB-56644.
617+
TestingHook<> expelHook;
618+
615619
protected:
616620
/**
617621
* Checks if eager checkpoint removal is enabled, then checks if the

engines/ep/tests/module_tests/checkpoint_test.cc

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1554,6 +1554,57 @@ TEST_F(SingleThreadedCheckpointTest, CursorDistance_ResetCursor) {
15541554
cursor.reset();
15551555
}
15561556

1557+
TEST_F(SingleThreadedCheckpointTest, ItemExpelResilientToVBucketRollback) {
1558+
setVBucketState(vbid, vbucket_state_active);
1559+
auto& vb = *store->getVBucket(vbid);
1560+
auto& manager = *vb.checkpointManager;
1561+
ASSERT_EQ(0, vb.getHighSeqno());
1562+
ASSERT_EQ(0, manager.getNumOpenChkItems());
1563+
1564+
// Note: We need at least 2 mutations for triggering ItemExpel, as we can't
1565+
// expel items pointed from cursors
1566+
store_item(vbid, makeStoredDocKey("key1"), "value");
1567+
store_item(vbid, makeStoredDocKey("key2"), "value");
1568+
ASSERT_EQ(2, vb.getHighSeqno());
1569+
ASSERT_EQ(2, manager.getNumOpenChkItems());
1570+
1571+
// Move the cursor, allow ItemExpel
1572+
flushVBucketToDiskIfPersistent(vbid, 2);
1573+
1574+
// Simulate the rollback behaviour.
1575+
// ItemExpel plants the expel-cursor in the checkpoint under processing and
1576+
// releases the CM::lock. At that point VB::rollback can clear the CM
1577+
// removing all checkpoint. When ItemExpel resumes it needs to be resilient
1578+
// to that state.
1579+
// The CM::expelHook executes in the section where ItemExpel has released
1580+
// (and not yet re-acquired) the CM::lock.
1581+
manager.expelHook = [&manager]() { manager.clear(); };
1582+
1583+
// Before the fix for MB-56644 this step fails by exception triggered,
1584+
// caused by that rollback has removed the checkpoint touched by ItemExpel
1585+
manager.expelUnreferencedCheckpointItems();
1586+
1587+
// Note: The CM was cleared, so mem-usage must track the correct allocation
1588+
// for the single/empty checkpoint in CheckpointList
1589+
auto config = CheckpointConfig(store->getEPEngine().getConfiguration());
1590+
const auto emptyManager =
1591+
CheckpointManager(store->getEPEngine().getEpStats(),
1592+
vb,
1593+
config,
1594+
0,
1595+
0,
1596+
0,
1597+
0,
1598+
0,
1599+
nullptr,
1600+
nullptr);
1601+
EXPECT_EQ(emptyManager.getQueuedItemsMemUsage(),
1602+
manager.getQueuedItemsMemUsage());
1603+
EXPECT_EQ(emptyManager.getMemOverheadQueue(),
1604+
manager.getMemOverheadQueue());
1605+
EXPECT_EQ(0, manager.getMemOverheadIndex());
1606+
}
1607+
15571608
// Test that when the same client registers twice, the first cursor 'dies'
15581609
TEST_P(CheckpointTest, reRegister) {
15591610
auto dcpCursor1 = manager->registerCursorBySeqno(

0 commit comments

Comments
 (0)