Skip to content

Commit e0a3016

Browse files
committed
Recover TierStore writes after a backup is removed
A write can be interrupted while a backup is configured, leaving unfinished work in the journal. If the node then restarts without that backup, recovery must not wait forever for a store that was deliberately removed. Updates to existing keys were also not journaled. If the primary write succeeded but the backup write failed, TierStore did not retain the value needed to finish that update later. Separately, a matching primary generation alone does not prove a backup is current. If the local TierStore index has been replaced, the old index may have contained unfinished operations that were lost with it, and generation comparison alone would not detect this. In this commit, we: - Journal updates that must be written to both primary and backup storage. - Allow unfinished creates, updates, and removals to finish using only the primary store after restarting without the backup. - Rotate the primary synchronization generation first, ensuring that the missing backup is recognized as out of date when it is configured again. - Reopen an existing TierStore index even when the backup is no longer configured, so unfinished journal entries can still be recovered. - Store the index database identity alongside the primary generation in the backup completion record, and treat a completion from another index as requiring synchronization. - Add tests for failed updates, recovery without a backup, attempts to recover before backup synchronization has been initialized, completion encoding, and index replacement. A later commit will copy the current primary data into a new or outdated backup. Assisted-by: Amp (AI coding agent)
1 parent 97ed434 commit e0a3016

2 files changed

Lines changed: 411 additions & 85 deletions

File tree

src/builder.rs

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -992,15 +992,27 @@ impl NodeBuilder {
992992
let ts_config = self.tier_store_config.as_ref();
993993
let primary_store = Arc::new(DynStoreWrapper(kv_store));
994994
let mut tier_store = TierStore::new(primary_store, Arc::clone(&logger));
995+
let tier_index_exists = PathBuf::from(&self.config.storage_dir_path)
996+
.join(io::sqlite_store::SQLITE_TIER_INDEX_DB_FILE_NAME)
997+
.exists();
998+
let tier_index_required = ts_config
999+
.map(|config| {
1000+
config.ephemeral_storage_dir_path.is_some()
1001+
|| config.backup_storage_dir_path.is_some()
1002+
})
1003+
.unwrap_or(false);
1004+
if tier_index_required || tier_index_exists {
1005+
let index_store = runtime
1006+
.block_on(setup_index_store(self.config.storage_dir_path.clone().into()))
1007+
.map_err(|e| {
1008+
log_error!(logger, "Failed to setup tier-store index: {}", e);
1009+
BuildError::KVStoreSetupFailed
1010+
})?;
1011+
tier_store.set_index_store(index_store);
1012+
}
9951013
if let Some(config) = ts_config {
9961014
if let Some(ephemeral_storage_dir_path) = config.ephemeral_storage_dir_path.as_ref()
9971015
{
998-
let index_store = runtime
999-
.block_on(setup_index_store(self.config.storage_dir_path.clone().into()))
1000-
.map_err(|e| {
1001-
log_error!(logger, "Failed to setup tier-store index: {}", e);
1002-
BuildError::KVStoreSetupFailed
1003-
})?;
10041016
let ephemeral_store = SqliteStore::new(
10051017
ephemeral_storage_dir_path.clone(),
10061018
Some(io::sqlite_store::SQLITE_EPHEMERAL_DB_FILE_NAME.to_string()),
@@ -1011,7 +1023,6 @@ impl NodeBuilder {
10111023
BuildError::KVStoreSetupFailed
10121024
})?;
10131025
let ephemeral_store: Arc<DynStore> = Arc::new(DynStoreWrapper(ephemeral_store));
1014-
tier_store.set_index_store(index_store);
10151026
tier_store.set_ephemeral_store(ephemeral_store);
10161027
}
10171028

0 commit comments

Comments
 (0)