Skip to content

Commit 7c1d614

Browse files
SDSTOR-22729: index/wb_cache: Fix recovery corruption after root-split crash (#900)
A B-tree root split (tree height N → N+1) proceeds in three steps: 1. Allocate new_root and call on_root_changed(new_root), which updates the in-memory superblock (SB) and links meta_buf → new_root_buf in the CP flush DAG. 2. split_node(new_root, old_root) modifies old_root in memory (edge_info=EMPTY, next_bnode=child_node2) and calls transact_nodes({child_node2}, {}, old_root, new_root), which invokes link_buf(new_root_buf, old_root_buf). 3. The on-disk SB is written at the very end of CP flush, after all node buffers complete. A SIGKILL landing after step 2 but before the SB write exposed three latent bugs that combined to corrupt the tree. **Bug A — link_buf Condition 1 created a flat flush DAG** Condition 1 bypassed new_root_buf whenever it was newly created in the current CP, regardless of whether old_root_buf was new or old. This caused old_root_buf to link directly to meta_buf, making new_root_buf and old_root_buf siblings with no ordering between them. old_root could therefore reach disk in its transient split state (edge_info=EMPTY, next_bnode=child_node2) before new_root, widening the crash window. **Bug B — Recovery discarded a committed new_root** When the crash left old_root on disk in split state and new_root durable but the SB unwritten, the recovery loop could not reliably identify new_root as the intended root. Existing logic either discarded the committed new_root candidate or had no mechanism to promote it, leaving the persisted SB still pointing to old_root. **Bug C — repair_root_node applied an unsafe edge repair** With old_root as the recovered tree root (per the stale SB), repair_root_node read old_root.next_bnode (= child_node2, level N) and set it as old_root's edge child (also level N). This violated the B-tree invariant child.level == parent.level - 1, causing validate_node to abort with "Child node level mismatch" on the next B-tree access. **Fix 1 — Restore correct flush-DAG ordering (Bug A)** Changed link_buf Condition 1 to bypass up_buf only when BOTH up_buf AND down_buf were created in the current CP: Before: if (up_buf->m_created_cp_id == icp_ctx->id()) After: if (up_buf->m_created_cp_id == icp_ctx->id() && down_buf->m_created_cp_id == icp_ctx->id()) When down_buf is an older node (e.g. old_root), the dependency chain through up_buf (new_root) is preserved. The key guarantee this establishes is: if new_root is committed (on disk), then old_root and child_node2 are also committed, making Fix 2's root promotion safe. The mirror fix is applied in index_cp.cpp process_txn_record so that journal recovery rebuilds an identical DAG. The sanity check is tightened accordingly: only a buffer that was itself created in the current CP must not point to another same-CP-new up_buffer. **Fix 2 — Pre-flush new-root nodes and promote on recovery (Bug B)** Added a pre-flush phase in async_cp_flush: before the normal DAG flush starts, all newly-created nodes belonging to ordinals that had a root change are written to disk via async_write. Only after these writes complete does the normal DAG flush begin. This ensures new_root (and child_node2) are always durable before old_root can be written in its transient split state. During recovery, the journal is parsed to identify the final intended new-root BlkId per ordinal (m_recovered_root_ids). A candidate is promoted when: - The journal identifies it as the final root for its ordinal - was_node_committed() confirms it is durable on disk - persisted_root_was_committed() confirms the old (persisted SB) root was also written in this CP, meaning the tree is in post-split state This check is stronger than simply testing whether SB root is non-empty: if old_root was not yet written in split state, the tree is still in a pre-split consistent state and the new_root candidate is discarded. set_root_from_committed_buf() updates the in-memory SB (root_node, root_link_version, btree_depth) and the btree's root pointer before the forced recovery CP writes the corrected SB to disk. The first-CP path (SB root still empty) is excluded so that recovery_completed() can build a fresh root as before. **Fix 3 — Harden repair_root_node (Bug C)** Added guards before applying the edge repair: a. Buffer validity: skip if raw_buffer is null, node magic is invalid, or node_id does not match blkid — the buffer was never written. b. next_bnode empty: skip if next_bnode is already empty_bnodeid, meaning new_root was already promoted and nothing needs repairing. c. Level invariant: read the candidate next_bnode from disk; if its level >= old_root's level (partial root-split state), skip the repair rather than corrupting the edge. Also changed the raw pointer `n` to BtreeNodePtr `bn` to prevent a memory leak on the early-return paths added by this fix. The same validity guard is applied to repair_node to protect against same-CP new nodes that were never flushed (all-zero on disk).
1 parent 0d15abb commit 7c1d614

9 files changed

Lines changed: 615 additions & 99 deletions

File tree

conanfile.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
class HomestoreConan(ConanFile):
1111
name = "homestore"
12-
version = "7.5.16"
12+
version = "7.5.17"
1313

1414
homepage = "https://github.com/eBay/Homestore"
1515
description = "HomeStore Storage Engine"

src/include/homestore/index/index_internal.hpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,8 @@ class IndexTableBase {
9595
virtual void repair_node(IndexBufferPtr const& buf) = 0;
9696
virtual void repair_root_node(IndexBufferPtr const& buf) = 0;
9797
virtual void delete_stale_children(IndexBufferPtr const& buf) = 0;
98+
virtual bnodeid_t persisted_root_node_id() const = 0;
99+
virtual bool set_root_from_committed_buf(IndexBufferPtr const& buf) = 0;
98100
virtual void audit_tree() const = 0;
99101
virtual void update_sb() = 0;
100102
virtual void load_metrics(uint64_t interior, uint64_t leaf, uint8_t depth) = 0;

src/include/homestore/index/index_table.hpp

Lines changed: 124 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -208,37 +208,117 @@ class IndexTable : public IndexTableBase, public Btree< K, V > {
208208
void repair_root_node(IndexBufferPtr const& idx_buf) override {
209209
LOGTRACEMOD(wbcache, "check if this was the previous root node {} for buf {} ", m_sb->root_node,
210210
idx_buf->to_string());
211-
if (m_sb->root_node == idx_buf->blkid().to_integer()) {
212-
// This is the root node, we need to update the root node in superblk
213-
LOGTRACEMOD(wbcache, "{} is old root so we need to update the meta node ", idx_buf->to_string());
214-
BtreeNode* n = this->init_node(idx_buf->raw_buffer(), idx_buf->blkid().to_integer(), false /* init_buf */,
215-
BtreeNode::identify_leaf_node(idx_buf->raw_buffer()));
216-
static_cast< IndexBtreeNode* >(n)->attach_buf(idx_buf);
217-
auto edge_id = n->next_bnode();
218-
219-
if (n->has_valid_edge() && hs()->has_fc_service()) {
220-
auto const reason =
221-
fmt::format("root {} already has a valid edge {}, so we should have found the new root node",
222-
n->to_string(), n->get_edge_value().bnode_id());
223-
hs()->fc_service().trigger_fc(FaultContainmentEvent::ENTER, static_cast< void* >(&(m_sb->parent_uuid)),
224-
reason);
225-
return;
226-
} else {
227-
BT_REL_ASSERT(!n->has_valid_edge(),
228-
"root {} already has a valid edge {}, so we should have found the new root node",
229-
n->to_string(), n->get_edge_value().bnode_id());
230-
}
231-
n->set_next_bnode(empty_bnodeid);
232-
n->set_edge_value(BtreeLinkInfo{edge_id, 0});
233-
LOGTRACEMOD(wbcache, "change root node {}: edge updated to {} and invalidate the next node! ", n->node_id(),
234-
edge_id);
235-
auto cpg = cp_mgr().cp_guard();
236-
write_node_impl(n, (void*)cpg.context(cp_consumer_t::INDEX_SVC));
237-
238-
} else {
211+
if (m_sb->root_node != idx_buf->blkid().to_integer()) {
239212
LOGTRACEMOD(wbcache, "This is not the root node, so we can ignore this repair call for buf {}",
240213
idx_buf->to_string());
214+
return;
215+
}
216+
217+
LOGTRACEMOD(wbcache, "{} is old root so we need to update the meta node ", idx_buf->to_string());
218+
auto* const raw_buf = idx_buf->raw_buffer();
219+
if (raw_buf == nullptr || !BtreeNode::is_valid_node(sisl::blob{raw_buf, this->m_bt_cfg.node_size()})) {
220+
LOGERROR("repair_root_node: skip invalid/unwritten buf {}", idx_buf->to_string());
221+
return;
222+
}
223+
224+
auto const* phdr = r_cast< persistent_hdr_t const* >(raw_buf);
225+
if (phdr->node_id != idx_buf->blkid().to_integer()) {
226+
LOGERROR("repair_root_node: skip invalid/unwritten buf {}", idx_buf->to_string());
227+
return;
228+
}
229+
if (phdr->next_node == empty_bnodeid) {
230+
LOGTRACEMOD(wbcache, "repair_root_node: buf={} already has empty next_bnode; nothing to repair",
231+
idx_buf->to_string());
232+
return;
233+
}
234+
235+
BtreeNode* n = this->init_node(raw_buf, idx_buf->blkid().to_integer(), false /* init_buf */,
236+
BtreeNode::identify_leaf_node(raw_buf));
237+
static_cast< IndexBtreeNode* >(n)->attach_buf(idx_buf);
238+
BtreeNodePtr root{n};
239+
auto const edge_id = root->next_bnode();
240+
241+
BtreeNodePtr edge_node;
242+
auto const ret = read_node_impl(edge_id, edge_node);
243+
if (ret != btree_status_t::success || edge_node->level() >= root->level()) {
244+
LOGERROR("repair_root_node: skip unsafe edge repair for buf={} next_bnode={} ret={} "
245+
"candidate_level={} root_level={}",
246+
idx_buf->to_string(), edge_id, enum_name(ret),
247+
edge_node ? static_cast< int >(edge_node->level()) : -1, root->level());
248+
return;
241249
}
250+
251+
if (root->has_valid_edge() && hs()->has_fc_service()) {
252+
auto const reason =
253+
fmt::format("root {} already has a valid edge {}, so we should have found the new root node",
254+
root->to_string(), root->get_edge_value().bnode_id());
255+
hs()->fc_service().trigger_fc(FaultContainmentEvent::ENTER, static_cast< void* >(&(m_sb->parent_uuid)),
256+
reason);
257+
return;
258+
} else {
259+
BT_REL_ASSERT(!root->has_valid_edge(),
260+
"root {} already has a valid edge {}, so we should have found the new root node",
261+
root->to_string(), root->get_edge_value().bnode_id());
262+
}
263+
root->set_next_bnode(empty_bnodeid);
264+
root->set_edge_value(BtreeLinkInfo{edge_id, 0});
265+
LOGTRACEMOD(wbcache, "change root node {}: edge updated to {} and invalidate the next node! ", root->node_id(),
266+
edge_id);
267+
auto cpg = cp_mgr().cp_guard();
268+
write_node_impl(root, (void*)cpg.context(cp_consumer_t::INDEX_SVC));
269+
}
270+
271+
bnodeid_t persisted_root_node_id() const override { return m_sb->root_node; }
272+
273+
bool set_root_from_committed_buf(IndexBufferPtr const& idx_buf) override {
274+
auto* const raw_buf = idx_buf->raw_buffer();
275+
if (m_sb->root_node == empty_bnodeid || raw_buf == nullptr ||
276+
!BtreeNode::is_valid_node(sisl::blob{raw_buf, this->m_bt_cfg.node_size()})) {
277+
LOGERROR("set_root_from_committed_buf: reject invalid candidate {}", idx_buf->to_string());
278+
return false;
279+
}
280+
281+
auto const* candidate_hdr = r_cast< persistent_hdr_t const* >(raw_buf);
282+
if (candidate_hdr->node_id != idx_buf->blkid().to_integer()) {
283+
LOGERROR("set_root_from_committed_buf: reject invalid candidate {}", idx_buf->to_string());
284+
return false;
285+
}
286+
287+
try {
288+
this->validate_node(idx_buf->blkid().to_integer());
289+
} catch (std::exception const& e) {
290+
LOGERROR("set_root_from_committed_buf: candidate={} failed validation: {}", idx_buf->to_string(), e.what());
291+
return false;
292+
}
293+
294+
auto const candidate_level = candidate_hdr->level;
295+
if (m_sb->root_node == idx_buf->blkid().to_integer()) {
296+
if (candidate_level != m_sb->btree_depth) {
297+
LOGERROR("set_root_from_committed_buf: persisted root {} has level={} but SB depth={}",
298+
idx_buf->blkid().to_integer(), candidate_level, m_sb->btree_depth);
299+
return false;
300+
}
301+
this->m_btree_depth = candidate_level;
302+
this->set_root_node_info(BtreeLinkInfo{m_sb->root_node, m_sb->root_link_version});
303+
return true;
304+
}
305+
306+
BtreeNode* n = this->init_node(raw_buf, idx_buf->blkid().to_integer(), false /* init_buf */,
307+
BtreeNode::identify_leaf_node(raw_buf));
308+
static_cast< IndexBtreeNode* >(n)->attach_buf(idx_buf);
309+
BtreeNodePtr root{n};
310+
311+
LOGINFOMOD(wbcache, "Recovery promotes committed root {} -> {} at level {}", m_sb->root_node, root->node_id(),
312+
root->level());
313+
m_sb->root_node = root->node_id();
314+
m_sb->root_link_version = root->link_version();
315+
m_sb->btree_depth = root->level();
316+
this->m_btree_depth = root->level();
317+
this->set_root_node_info(BtreeLinkInfo{root->node_id(), root->link_version()});
318+
319+
// Recovery promotion must survive another crash even when no index buffer is dirty in the forced CP.
320+
m_sb.write();
321+
return true;
242322
}
243323

244324
void delete_stale_children(IndexBufferPtr const& idx_buf) override {
@@ -266,8 +346,21 @@ class IndexTable : public IndexTableBase, public Btree< K, V > {
266346
this->root_node_id());
267347
return;
268348
}
269-
BtreeNode* n = this->init_node(idx_buf->raw_buffer(), idx_buf->blkid().to_integer(), false /* init_buf */,
270-
BtreeNode::identify_leaf_node(idx_buf->raw_buffer()));
349+
350+
auto* const raw_buf = idx_buf->raw_buffer();
351+
if (raw_buf == nullptr || !BtreeNode::is_valid_node(sisl::blob{raw_buf, this->m_bt_cfg.node_size()})) {
352+
LOGERROR("repair_node: skip invalid/unwritten buf {}", idx_buf->to_string());
353+
return;
354+
}
355+
auto const* phdr = r_cast< persistent_hdr_t const* >(raw_buf);
356+
if (phdr->node_id != idx_buf->blkid().to_integer()) {
357+
LOGERROR("repair_node: skip buf {} whose persisted node_id={} does not match blkid={}",
358+
idx_buf->to_string(), phdr->node_id, idx_buf->blkid().to_integer());
359+
return;
360+
}
361+
362+
BtreeNode* n = this->init_node(raw_buf, idx_buf->blkid().to_integer(), false /* init_buf */,
363+
BtreeNode::identify_leaf_node(raw_buf));
271364
static_cast< IndexBtreeNode* >(n)->attach_buf(idx_buf);
272365
auto cpg = cp_mgr().cp_guard();
273366

@@ -307,7 +400,7 @@ class IndexTable : public IndexTableBase, public Btree< K, V > {
307400
node->set_checksum();
308401
auto prev_state = idx_node->m_idx_buf->m_state.exchange(index_buf_state_t::DIRTY);
309402
LOGTRACEMOD(wbcache, "write_node_impl: node_id={} cp_id={} prev_state={} -> DIRTY", node->node_id(),
310-
cp_ctx->id(), static_cast<int>(prev_state));
403+
cp_ctx->id(), static_cast< int >(prev_state));
311404
idx_node->m_idx_buf->m_node_level = node->level();
312405
if (prev_state == index_buf_state_t::CLEAN) {
313406
// It was clean before, dirtying it first time, add it to the wb_cache list to flush

src/lib/index/index_cp.cpp

Lines changed: 48 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ void IndexCPContext::add_to_txn_journal(uint32_t index_ordinal, const IndexBuffe
3131
auto record_size = txn_record::size_for_num_ids(created_bufs.size() + freed_bufs.size() + (left_child_buf ? 1 : 0) +
3232
(parent_buf ? 1 : 0));
3333
std::unique_lock< iomgr::FiberManagerLib::mutex > lg{m_txn_journal_mtx};
34+
if (parent_buf && parent_buf->is_meta_buf() && !left_child_buf && !created_bufs.empty()) {
35+
m_root_changed_ordinals.insert(index_ordinal);
36+
}
3437
if (m_txn_journal_buf.bytes() == nullptr) {
3538
m_txn_journal_buf =
3639
std::move(sisl::io_blob_safe{std::max(sizeof(txn_journal), 512ul), 512, sisl::buftag::metablk});
@@ -64,6 +67,27 @@ void IndexCPContext::add_to_txn_journal(uint32_t index_ordinal, const IndexBuffe
6467
}
6568
}
6669

70+
IndexBufferPtrList IndexCPContext::root_change_preflush_bufs() {
71+
IndexBufferPtrList bufs;
72+
std::set< BlkId > selected_blkids;
73+
std::unique_lock< iomgr::FiberManagerLib::mutex > lg{m_txn_journal_mtx};
74+
if (m_root_changed_ordinals.empty()) { return bufs; }
75+
76+
m_dirty_buf_list.foreach_entry([this, &bufs, &selected_blkids](IndexBufferPtr const& buf) {
77+
if (buf->is_meta_buf() || buf->m_node_freed || buf->m_created_cp_id != id() ||
78+
!m_root_changed_ordinals.contains(buf->m_index_ordinal)) {
79+
return;
80+
}
81+
if (selected_blkids.insert(buf->blkid()).second) { bufs.push_back(buf); }
82+
});
83+
return bufs;
84+
}
85+
86+
BlkId IndexCPContext::recovered_root_id(uint32_t ordinal) const {
87+
auto const it = m_recovered_root_ids.find(ordinal);
88+
return it == m_recovered_root_ids.end() ? BlkId{} : it->second;
89+
}
90+
6791
void IndexCPContext::add_to_dirty_list(const IndexBufferPtr& buf) {
6892
m_dirty_buf_list.push_back(buf);
6993
buf->set_state(index_buf_state_t::DIRTY);
@@ -246,6 +270,17 @@ std::map< BlkId, IndexBufferPtr > IndexCPContext::recover(sisl::byte_view sb) {
246270
txn_record const* rec = r_cast< txn_record const* >(cur_ptr);
247271
HS_DBG_ASSERT_GT(rec->total_ids(), 0, "Invalid txn_record, has no ids in it");
248272

273+
// Root-change records contain no split/merge side effects. Retaining the last record per ordinal identifies
274+
// the final intended root even if the same CP grows and then collapses the tree.
275+
if (rec->is_parent_meta && rec->num_freed_ids == 0) {
276+
bool const is_root_split = !rec->has_inplace_child && rec->num_new_ids == 1;
277+
bool const is_root_collapse = rec->has_inplace_child && rec->num_new_ids == 0;
278+
if (is_root_split || is_root_collapse) {
279+
auto const root_idx = rec->has_inplace_parent ? 1 : 0;
280+
m_recovered_root_ids[rec->index_ordinal] = rec->blk_id(root_idx);
281+
}
282+
}
283+
249284
process_txn_record(rec, buf_map);
250285
cur_ptr += rec->size();
251286
LOGTRACEMOD(wbcache, "Recovered txn record: {}: {}", t, rec->to_string());
@@ -264,24 +299,11 @@ std::map< BlkId, IndexBufferPtr > IndexCPContext::recover(sisl::byte_view sb) {
264299
buffer->m_up_buffer->to_string());
265300
}
266301
};
267-
#if 0
268-
auto dag_print = [](const std::map< BlkId, IndexBufferPtr >& dags, std::string delimiter) {
269-
int index = 1;
270-
for (const auto& [blkid, bufferPtr] : dags) {
271-
LOGTRACEMOD(wbcache, "{}{} - blkid {} buffer {} ", delimiter, index++, blkid.to_integer(),
272-
bufferPtr->to_string());
273-
}
274-
};
275-
LOGTRACEMOD(wbcache,"Before modify : \n ");
276-
dag_print(buf_map, "Before: ");
277-
#endif
278302
for (auto& [blkid, bufferPtr] : buf_map) {
279303
modifyBuffer(bufferPtr);
280304
}
281-
// LOGTRACEMOD(wbcache,"\n\n\nAFTER modify : \n ");
282-
// dag_print(buf_map, "After: ");
283305

284-
auto sanityCheck = [](const std::map< BlkId, IndexBufferPtr >& dags) {
306+
auto sanityCheck = [cp_id = id()](const std::map< BlkId, IndexBufferPtr >& dags) {
285307
for (const auto& [blkid, bufferPtr] : dags) {
286308
auto up_buffer = bufferPtr->m_up_buffer;
287309
if (up_buffer) {
@@ -290,9 +312,11 @@ std::map< BlkId, IndexBufferPtr > IndexCPContext::recover(sisl::byte_view sb) {
290312
"Sanity check failed: Buffer {} blkdid {} has an up_buffer {} blkid that is marked as freed.",
291313
bufferPtr->to_string(), blkid.to_integer(), up_buffer->to_string(),
292314
up_buffer->blkid().to_integer());
293-
HS_REL_ASSERT(up_buffer->m_created_cp_id == -1,
294-
"Sanity check failed: Buffer {} has an up_buffer {} that just created (created_cp_id={})",
295-
bufferPtr->to_string(), up_buffer->to_string(), up_buffer->m_created_cp_id);
315+
if (bufferPtr->m_created_cp_id == cp_id) {
316+
HS_REL_ASSERT(up_buffer->m_created_cp_id != cp_id,
317+
"Sanity check failed: new Buffer {} has an up_buffer {} created in the same CP ({})",
318+
bufferPtr->to_string(), up_buffer->to_string(), cp_id);
319+
}
296320
HS_REL_ASSERT(up_buffer->m_index_ordinal == bufferPtr->m_index_ordinal,
297321
"Sanity check failed: Buffer {} has an up_buffer {} with different index_ordinal "
298322
"(up_ordinal={}, buf_ordinal={})",
@@ -331,7 +355,8 @@ void IndexCPContext::process_txn_record(txn_record const* rec, std::map< BlkId,
331355
auto cpg = cp_mgr().cp_guard();
332356

333357
auto const rec_to_buf = [&buf_map, &cpg](txn_record const* rec, bool is_meta, BlkId const& bid,
334-
IndexBufferPtr const& up_buf) -> IndexBufferPtr {
358+
IndexBufferPtr const& up_buf,
359+
bool mark_created_in_cp = false) -> IndexBufferPtr {
335360
IndexBufferPtr buf;
336361
// MetaIndexBuffer always has blkid={0,0,0,0} regardless of which BTree table it belongs to.
337362
// When multiple tables have a root split in the same CP, all their MetaBufs share the same blkid
@@ -356,9 +381,11 @@ void IndexCPContext::process_txn_record(txn_record const* rec, std::map< BlkId,
356381
buf = it->second;
357382
}
358383

384+
if (mark_created_in_cp) { buf->m_created_cp_id = cpg->id(); }
385+
359386
if (up_buf) {
360387
auto real_up_buf = up_buf;
361-
if (up_buf->m_created_cp_id == cpg->id()) {
388+
if (up_buf->m_created_cp_id == cpg->id() && buf->m_created_cp_id == cpg->id()) {
362389
real_up_buf = up_buf->m_up_buffer;
363390
} else if (up_buf->m_node_freed) {
364391
real_up_buf = up_buf->m_up_buffer;
@@ -367,8 +394,6 @@ void IndexCPContext::process_txn_record(txn_record const* rec, std::map< BlkId,
367394
}
368395

369396
#ifndef NDEBUG
370-
// if (!is_sibling_link || (buf->m_up_buffer == real_up_buf)) { return buf;}
371-
// Already linked with same buf or its not a sibling link to override
372397
if (real_up_buf->is_in_down_buffers(buf)) { return buf; }
373398
#endif
374399

@@ -391,9 +416,8 @@ void IndexCPContext::process_txn_record(txn_record const* rec, std::map< BlkId,
391416
}
392417

393418
for (uint8_t idx{0}; idx < rec->num_new_ids; ++idx) {
394-
auto new_buf = rec_to_buf(rec, false /* is_meta */, rec->blk_id(cur_idx++),
395-
inplace_child_buf ? inplace_child_buf : parent_buf);
396-
new_buf->m_created_cp_id = cpg->id();
419+
rec_to_buf(rec, false /* is_meta */, rec->blk_id(cur_idx++), inplace_child_buf ? inplace_child_buf : parent_buf,
420+
true /* mark_created_in_cp */);
397421
}
398422

399423
for (uint8_t idx{0}; idx < rec->num_freed_ids; ++idx) {

src/lib/index/index_cp.hpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
*********************************************************************************/
1616
#pragma once
1717
#include <atomic>
18+
#include <set>
19+
#include <unordered_set>
1820
#include <sisl/fds/concurrent_insert_vector.hpp>
1921
#include <homestore/blk.h>
2022
#include <homestore/index/index_internal.hpp>
@@ -144,6 +146,8 @@ struct IndexCPContext : public VDevCPContext {
144146

145147
iomgr::FiberManagerLib::mutex m_txn_journal_mtx;
146148
sisl::io_blob_safe m_txn_journal_buf;
149+
std::unordered_set< uint32_t > m_root_changed_ordinals;
150+
std::map< uint32_t, BlkId > m_recovered_root_ids;
147151

148152
public:
149153
IndexCPContext(CP* cp);
@@ -156,6 +160,8 @@ struct IndexCPContext : public VDevCPContext {
156160
std::map< BlkId, IndexBufferPtr > recover(sisl::byte_view sb);
157161

158162
sisl::io_blob_safe const& journal_buf() const { return m_txn_journal_buf; }
163+
IndexBufferPtrList root_change_preflush_bufs();
164+
BlkId recovered_root_id(uint32_t ordinal) const;
159165

160166
void add_to_dirty_list(const IndexBufferPtr& buf);
161167
bool any_dirty_buffers() const;

src/lib/index/index_service.cpp

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@
2323
#include "common/homestore_assert.hpp"
2424
#include "device/virtual_dev.hpp"
2525
#include "device/physical_dev.hpp"
26+
27+
#ifdef _PRERELEASE
28+
#include <iomgr/iomgr_flip.hpp>
29+
#endif
2630
#include "device/chunk.h"
2731

2832
namespace homestore {
@@ -118,6 +122,11 @@ void IndexService::start() {
118122
tbl->audit_tree();
119123
#endif
120124
}
125+
#ifdef _PRERELEASE
126+
// Tests can keep the recovered journal current, then perform a second crash sequentially to verify replay
127+
// idempotence without racing nested HomeStore restarts.
128+
if (iomgr_flip::instance()->test_flip("skip_cp_after_index_root_recovery")) { return; }
129+
#endif
121130
// Force taking cp after recovery done. This makes sure that the index table is in consistent state and dirty
122131
// buffer after recovery can be added to dirty list for flushing in the new cp
123132
hs()->cp_mgr().trigger_cp_flush(true /* force */);

0 commit comments

Comments
 (0)