Skip to content

Commit e56a9f6

Browse files
committed
Synchronize TierStore backups before node startup
A newly configured backup starts empty, while a backup restored after primary-only operation may contain missing or stale values. Treating either backup as current could leave it unusable for recovery. In this commit, we: - Add exhaustive key enumeration to the dynamic store interface when tiered storage is enabled, while preserving the existing requirements for builds without tiered storage. - Recover pending journal operations before taking the primary-store snapshot. - Copy all durable primary values into the backup and remove values that no longer exist in primary. - Exclude ephemeral cache values from the backup and preserve TierStore's synchronization metadata during stale-value cleanup. - Write the backup completion record only after every synchronization step succeeds, ensuring interrupted attempts are retried safely. - Run required backup synchronization during node construction and fail the build if synchronization cannot complete. - Implement `MigratableKVStore` for existing test stores so the tiered-storage feature continues compiling and exercising their original behavior. - Add coverage for successful synchronization, backup-only configuration, journal recovery, stale-value removal, metadata preservation, and retries after failures at each stage. Assisted-by: Amp (AI coding agent)
1 parent b6e35ae commit e56a9f6

10 files changed

Lines changed: 1024 additions & 43 deletions

File tree

src/builder.rs

Lines changed: 206 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ use lightning::routing::scoring::{
4141
};
4242
use lightning::sign::{EntropySource, NodeSigner};
4343
use lightning::util::config::HTLCInterceptionFlags;
44+
#[cfg(feature = "storage-tier")]
45+
use lightning::util::persist::MigratableKVStore;
4446
use lightning::util::persist::{
4547
KVStore, PaginatedKVStore, CHANNEL_MANAGER_PERSISTENCE_KEY,
4648
CHANNEL_MANAGER_PERSISTENCE_PRIMARY_NAMESPACE, CHANNEL_MANAGER_PERSISTENCE_SECONDARY_NAMESPACE,
@@ -73,7 +75,7 @@ use crate::io::fs_store::open_or_migrate_fs_store;
7375
#[cfg(feature = "storage-sqlite")]
7476
use crate::io::sqlite_store::SqliteStore;
7577
#[cfg(feature = "storage-tier")]
76-
use crate::io::tier_store::{setup_index_store, TierStore};
78+
use crate::io::tier_store::{setup_index_store, BackupSyncStatus, TierStore};
7779
use crate::io::utils::{
7880
read_all_objects, read_event_queue, read_external_pathfinding_scores_from_cache,
7981
read_n_objects, read_network_graph, read_node_metrics, read_output_sweeper, read_peer_info,
@@ -959,13 +961,34 @@ impl NodeBuilder {
959961
///
960962
/// [`set_ephemeral_storage_dir_path`]: Self::set_ephemeral_storage_dir_path
961963
/// [`set_backup_storage_dir_path`]: Self::set_backup_storage_dir_path
964+
#[cfg(not(feature = "storage-tier"))]
962965
pub fn build_with_store<S: PaginatedKVStore + Send + Sync + 'static>(
963966
&self, node_entropy: NodeEntropy, kv_store: S,
964967
) -> Result<Node, BuildError> {
965968
let logger = setup_logger(&self.log_writer_config, &self.config)?;
966969
self.build_with_store_and_logger(node_entropy, kv_store, logger)
967970
}
968971

972+
/// Builds a [`Node`] instance according to the options previously configured.
973+
///
974+
/// The provided `kv_store` will be used as the primary storage backend. Optionally,
975+
/// an ephemeral store for frequently-accessed non-critical data (e.g., network graph, scorer)
976+
/// and a local SQLite backup store for disaster recovery can be configured via
977+
/// [`set_ephemeral_storage_dir_path`] and [`set_backup_storage_dir_path`].
978+
///
979+
/// The store must implement [`MigratableKVStore`] so a configured backup can be backfilled or
980+
/// resilvered from the complete set of primary-store keys before the node starts.
981+
///
982+
/// [`set_ephemeral_storage_dir_path`]: Self::set_ephemeral_storage_dir_path
983+
/// [`set_backup_storage_dir_path`]: Self::set_backup_storage_dir_path
984+
#[cfg(feature = "storage-tier")]
985+
pub fn build_with_store<S: PaginatedKVStore + MigratableKVStore + Send + Sync + 'static>(
986+
&self, node_entropy: NodeEntropy, kv_store: S,
987+
) -> Result<Node, BuildError> {
988+
let logger = setup_logger(&self.log_writer_config, &self.config)?;
989+
self.build_with_store_and_logger(node_entropy, kv_store, logger)
990+
}
991+
969992
fn setup_runtime(&self, logger: &Arc<Logger>) -> Result<Arc<Runtime>, BuildError> {
970993
if let Some(handle) = self.runtime_handle.as_ref() {
971994
Ok(Arc::new(Runtime::with_handle(handle.clone(), Arc::clone(logger))))
@@ -977,17 +1000,38 @@ impl NodeBuilder {
9771000
}
9781001
}
9791002

1003+
#[cfg(not(feature = "storage-tier"))]
9801004
fn build_with_store_and_logger<S: PaginatedKVStore + Send + Sync + 'static>(
9811005
&self, node_entropy: NodeEntropy, kv_store: S, logger: Arc<Logger>,
9821006
) -> Result<Node, BuildError> {
9831007
let runtime = self.setup_runtime(&logger)?;
9841008
self.build_with_store_runtime_and_logger(node_entropy, kv_store, runtime, logger)
9851009
}
9861010

1011+
#[cfg(feature = "storage-tier")]
1012+
fn build_with_store_and_logger<
1013+
S: PaginatedKVStore + MigratableKVStore + Send + Sync + 'static,
1014+
>(
1015+
&self, node_entropy: NodeEntropy, kv_store: S, logger: Arc<Logger>,
1016+
) -> Result<Node, BuildError> {
1017+
let runtime = self.setup_runtime(&logger)?;
1018+
self.build_with_store_runtime_and_logger(node_entropy, kv_store, runtime, logger)
1019+
}
1020+
1021+
#[cfg(not(feature = "storage-tier"))]
9871022
fn build_with_store_runtime_and_logger<S: PaginatedKVStore + Send + Sync + 'static>(
9881023
&self, node_entropy: NodeEntropy, kv_store: S, runtime: Arc<Runtime>, logger: Arc<Logger>,
9891024
) -> Result<Node, BuildError> {
990-
#[cfg(feature = "storage-tier")]
1025+
let store: Arc<DynStore> = Arc::new(DynStoreWrapper(kv_store));
1026+
self.build_with_dyn_store(node_entropy, store, runtime, logger)
1027+
}
1028+
1029+
#[cfg(feature = "storage-tier")]
1030+
fn build_with_store_runtime_and_logger<
1031+
S: PaginatedKVStore + MigratableKVStore + Send + Sync + 'static,
1032+
>(
1033+
&self, node_entropy: NodeEntropy, kv_store: S, runtime: Arc<Runtime>, logger: Arc<Logger>,
1034+
) -> Result<Node, BuildError> {
9911035
let store: Arc<DynStore> = {
9921036
let ts_config = self.tier_store_config.as_ref();
9931037
let primary_store = Arc::new(DynStoreWrapper(kv_store));
@@ -1040,15 +1084,27 @@ impl NodeBuilder {
10401084
tier_store.set_backup_store(backup_store);
10411085
}
10421086
}
1043-
runtime.block_on(tier_store.initialize_backup_synchronization()).map_err(|e| {
1044-
log_error!(logger, "Failed to initialize tier-store backup synchronization: {}", e);
1045-
BuildError::KVStoreSetupFailed
1046-
})?;
1087+
runtime
1088+
.block_on(async {
1089+
let status = tier_store.initialize_backup_synchronization().await?;
1090+
if status == BackupSyncStatus::Required {
1091+
tier_store.synchronize_backup().await?;
1092+
}
1093+
Ok::<(), bitcoin::io::Error>(())
1094+
})
1095+
.map_err(|e| {
1096+
log_error!(logger, "Failed to prepare or synchronize tier-store backup: {}", e);
1097+
BuildError::KVStoreSetupFailed
1098+
})?;
10471099
Arc::new(DynStoreWrapper(tier_store))
10481100
};
1049-
#[cfg(not(feature = "storage-tier"))]
1050-
let store: Arc<DynStore> = Arc::new(DynStoreWrapper(kv_store));
1101+
self.build_with_dyn_store(node_entropy, store, runtime, logger)
1102+
}
10511103

1104+
fn build_with_dyn_store(
1105+
&self, node_entropy: NodeEntropy, store: Arc<DynStore>, runtime: Arc<Runtime>,
1106+
logger: Arc<Logger>,
1107+
) -> Result<Node, BuildError> {
10521108
let seed_bytes = node_entropy.to_seed_bytes();
10531109
let config = Arc::new(self.config.clone());
10541110

@@ -1594,11 +1650,19 @@ impl ArcedNodeBuilder {
15941650
/// Builds a [`Node`] instance according to the options previously configured.
15951651
// Note that the generics here don't actually work for Uniffi, but we don't currently expose
15961652
// this so its not needed.
1653+
#[cfg(not(feature = "storage-tier"))]
15971654
pub fn build_with_store<S: PaginatedKVStore + Send + Sync + 'static>(
15981655
&self, node_entropy: Arc<NodeEntropy>, kv_store: S,
15991656
) -> Result<Arc<Node>, BuildError> {
16001657
self.inner.read().expect("lock").build_with_store(*node_entropy, kv_store).map(Arc::new)
16011658
}
1659+
1660+
#[cfg(feature = "storage-tier")]
1661+
pub fn build_with_store<S: PaginatedKVStore + MigratableKVStore + Send + Sync + 'static>(
1662+
&self, node_entropy: Arc<NodeEntropy>, kv_store: S,
1663+
) -> Result<Arc<Node>, BuildError> {
1664+
self.inner.read().expect("lock").build_with_store(*node_entropy, kv_store).map(Arc::new)
1665+
}
16021666
}
16031667

16041668
/// Builds a [`Node`] instance according to the options previously configured.
@@ -2698,30 +2762,66 @@ pub(crate) fn sanitize_alias(alias_str: &str) -> Result<NodeAlias, BuildError> {
26982762
#[cfg(test)]
26992763
mod tests {
27002764
use std::future::Future;
2765+
#[cfg(all(feature = "storage-tier", not(feature = "uniffi")))]
2766+
use std::path::PathBuf;
27012767
use std::sync::Arc;
27022768

27032769
use lightning::io;
27042770
use lightning::util::persist::{
2705-
KVStore, PageToken, PaginatedKVStore, PaginatedListResponse,
2771+
KVStore, MigratableKVStore, PageToken, PaginatedKVStore, PaginatedListResponse,
27062772
CHANNEL_MANAGER_PERSISTENCE_KEY, CHANNEL_MANAGER_PERSISTENCE_PRIMARY_NAMESPACE,
27072773
CHANNEL_MANAGER_PERSISTENCE_SECONDARY_NAMESPACE,
27082774
};
27092775

27102776
use super::{sanitize_alias, BuildError, NodeAlias, NodeBuilder};
27112777
use crate::entropy::NodeEntropy;
27122778
use crate::io::test_utils::InMemoryStore;
2779+
#[cfg(all(feature = "storage-tier", not(feature = "uniffi")))]
2780+
use crate::io::{
2781+
sqlite_store::{SqliteStore, KV_TABLE_NAME, SQLITE_BACKUP_DB_FILE_NAME},
2782+
test_utils::random_storage_path,
2783+
};
27132784
use crate::logger::Logger;
27142785

2715-
struct ChannelManagerReadFailingStore(InMemoryStore);
2786+
#[cfg(all(feature = "storage-tier", not(feature = "uniffi")))]
2787+
struct CleanupDir(PathBuf);
2788+
2789+
#[cfg(all(feature = "storage-tier", not(feature = "uniffi")))]
2790+
impl Drop for CleanupDir {
2791+
fn drop(&mut self) {
2792+
let _ = std::fs::remove_dir_all(&self.0);
2793+
}
2794+
}
2795+
2796+
#[derive(Clone, Copy)]
2797+
enum BuilderStoreBehavior {
2798+
#[cfg(all(feature = "storage-tier", not(feature = "uniffi")))]
2799+
Normal,
2800+
FailChannelManagerRead,
2801+
#[cfg(all(feature = "storage-tier", not(feature = "uniffi")))]
2802+
FailListAllKeys,
2803+
}
2804+
2805+
struct BuilderTestStore {
2806+
inner: InMemoryStore,
2807+
behavior: BuilderStoreBehavior,
2808+
}
2809+
2810+
impl BuilderTestStore {
2811+
fn new(behavior: BuilderStoreBehavior) -> Self {
2812+
Self { inner: InMemoryStore::new(), behavior }
2813+
}
2814+
}
27162815

2717-
impl KVStore for ChannelManagerReadFailingStore {
2816+
impl KVStore for BuilderTestStore {
27182817
fn read(
27192818
&self, primary_namespace: &str, secondary_namespace: &str, key: &str,
27202819
) -> impl Future<Output = Result<Vec<u8>, io::Error>> + 'static + Send {
2721-
let fail_read = primary_namespace == CHANNEL_MANAGER_PERSISTENCE_PRIMARY_NAMESPACE
2820+
let fail_read = matches!(self.behavior, BuilderStoreBehavior::FailChannelManagerRead)
2821+
&& primary_namespace == CHANNEL_MANAGER_PERSISTENCE_PRIMARY_NAMESPACE
27222822
&& secondary_namespace == CHANNEL_MANAGER_PERSISTENCE_SECONDARY_NAMESPACE
27232823
&& key == CHANNEL_MANAGER_PERSISTENCE_KEY;
2724-
let read = KVStore::read(&self.0, primary_namespace, secondary_namespace, key);
2824+
let read = KVStore::read(&self.inner, primary_namespace, secondary_namespace, key);
27252825
async move {
27262826
if fail_read {
27272827
Err(io::Error::new(io::ErrorKind::Other, "channel manager read failed"))
@@ -2734,36 +2834,56 @@ mod tests {
27342834
fn write(
27352835
&self, primary_namespace: &str, secondary_namespace: &str, key: &str, buf: Vec<u8>,
27362836
) -> impl Future<Output = Result<(), io::Error>> + 'static + Send {
2737-
KVStore::write(&self.0, primary_namespace, secondary_namespace, key, buf)
2837+
KVStore::write(&self.inner, primary_namespace, secondary_namespace, key, buf)
27382838
}
27392839

27402840
fn remove(
27412841
&self, primary_namespace: &str, secondary_namespace: &str, key: &str, lazy: bool,
27422842
) -> impl Future<Output = Result<(), io::Error>> + 'static + Send {
2743-
KVStore::remove(&self.0, primary_namespace, secondary_namespace, key, lazy)
2843+
KVStore::remove(&self.inner, primary_namespace, secondary_namespace, key, lazy)
27442844
}
27452845

27462846
fn list(
27472847
&self, primary_namespace: &str, secondary_namespace: &str,
27482848
) -> impl Future<Output = Result<Vec<String>, io::Error>> + 'static + Send {
2749-
KVStore::list(&self.0, primary_namespace, secondary_namespace)
2849+
KVStore::list(&self.inner, primary_namespace, secondary_namespace)
27502850
}
27512851
}
27522852

2753-
impl PaginatedKVStore for ChannelManagerReadFailingStore {
2853+
impl PaginatedKVStore for BuilderTestStore {
27542854
fn list_paginated(
27552855
&self, primary_namespace: &str, secondary_namespace: &str,
27562856
page_token: Option<PageToken>,
27572857
) -> impl Future<Output = Result<PaginatedListResponse, io::Error>> + 'static + Send {
27582858
PaginatedKVStore::list_paginated(
2759-
&self.0,
2859+
&self.inner,
27602860
primary_namespace,
27612861
secondary_namespace,
27622862
page_token,
27632863
)
27642864
}
27652865
}
27662866

2867+
impl MigratableKVStore for BuilderTestStore {
2868+
fn list_all_keys(
2869+
&self,
2870+
) -> impl Future<Output = Result<Vec<(String, String, String)>, io::Error>> + 'static + Send
2871+
{
2872+
#[cfg(all(feature = "storage-tier", not(feature = "uniffi")))]
2873+
let fail = matches!(self.behavior, BuilderStoreBehavior::FailListAllKeys);
2874+
#[cfg(any(not(feature = "storage-tier"), feature = "uniffi"))]
2875+
let fail = false;
2876+
let list = MigratableKVStore::list_all_keys(&self.inner);
2877+
async move {
2878+
if fail {
2879+
Err(io::Error::new(io::ErrorKind::Other, "list_all_keys failed"))
2880+
} else {
2881+
list.await
2882+
}
2883+
}
2884+
}
2885+
}
2886+
27672887
#[test]
27682888
fn channel_manager_read_failure_fails_build() {
27692889
let builder = NodeBuilder::new();
@@ -2775,13 +2895,80 @@ mod tests {
27752895

27762896
let result = builder.build_with_store_and_logger(
27772897
node_entropy,
2778-
ChannelManagerReadFailingStore(InMemoryStore::new()),
2898+
BuilderTestStore::new(BuilderStoreBehavior::FailChannelManagerRead),
27792899
logger,
27802900
);
27812901

27822902
assert!(matches!(result, Err(BuildError::ReadFailed)));
27832903
}
27842904

2905+
#[cfg(all(feature = "storage-tier", not(feature = "uniffi")))]
2906+
#[test]
2907+
fn builder_synchronizes_a_configured_backup_before_returning() {
2908+
let base_dir = random_storage_path();
2909+
let _cleanup = CleanupDir(base_dir.clone());
2910+
let storage_dir = base_dir.join("node");
2911+
let backup_dir = base_dir.join("backup");
2912+
let runtime = tokio::runtime::Runtime::new().unwrap();
2913+
let primary_store = BuilderTestStore::new(BuilderStoreBehavior::Normal);
2914+
runtime.block_on(primary_store.write("namespace", "", "current", vec![1])).unwrap();
2915+
let backup_store = SqliteStore::new(
2916+
backup_dir.clone(),
2917+
Some(SQLITE_BACKUP_DB_FILE_NAME.to_string()),
2918+
Some(KV_TABLE_NAME.to_string()),
2919+
)
2920+
.unwrap();
2921+
runtime
2922+
.block_on(async { backup_store.write("namespace", "", "stale", vec![2]).await })
2923+
.unwrap();
2924+
drop(backup_store);
2925+
drop(runtime);
2926+
2927+
let mut builder = NodeBuilder::new();
2928+
builder
2929+
.set_storage_dir_path(storage_dir.to_string_lossy().into_owned())
2930+
.set_backup_storage_dir_path(backup_dir.to_string_lossy().into_owned());
2931+
let node = builder
2932+
.build_with_store(NodeEntropy::from_seed_bytes([42; 64]), primary_store)
2933+
.unwrap();
2934+
2935+
let backup_store = SqliteStore::new(
2936+
backup_dir,
2937+
Some(SQLITE_BACKUP_DB_FILE_NAME.to_string()),
2938+
Some(KV_TABLE_NAME.to_string()),
2939+
)
2940+
.unwrap();
2941+
let runtime = tokio::runtime::Runtime::new().unwrap();
2942+
assert_eq!(
2943+
runtime
2944+
.block_on(async { backup_store.read("namespace", "", "current").await })
2945+
.unwrap(),
2946+
vec![1]
2947+
);
2948+
assert!(runtime
2949+
.block_on(async { backup_store.read("namespace", "", "stale").await })
2950+
.is_err());
2951+
drop(node);
2952+
}
2953+
2954+
#[cfg(all(feature = "storage-tier", not(feature = "uniffi")))]
2955+
#[test]
2956+
fn builder_fails_when_backup_synchronization_cannot_list_primary_keys() {
2957+
let base_dir = random_storage_path();
2958+
let _cleanup = CleanupDir(base_dir.clone());
2959+
let mut builder = NodeBuilder::new();
2960+
builder
2961+
.set_storage_dir_path(base_dir.join("node").to_string_lossy().into_owned())
2962+
.set_backup_storage_dir_path(base_dir.join("backup").to_string_lossy().into_owned());
2963+
2964+
let result = builder.build_with_store(
2965+
NodeEntropy::from_seed_bytes([42; 64]),
2966+
BuilderTestStore::new(BuilderStoreBehavior::FailListAllKeys),
2967+
);
2968+
2969+
assert!(matches!(result, Err(BuildError::KVStoreSetupFailed)));
2970+
}
2971+
27852972
#[test]
27862973
fn sanitize_empty_node_alias() {
27872974
// Empty node alias

0 commit comments

Comments
 (0)