Skip to content

Commit 309e9ec

Browse files
committed
Integrate TierStore into NodeBuilder
Add native builder support for tiered storage by introducing `TierStoreConfig` and builder methods for configuring ephemeral storage and a local SQLite backup mirror. During node construction, wrap the configured primary store in `TierStore` and attach secondary tiers for cache-like ephemeral data and mirrored durable backup writes. The builder constructs the backup store internally using a dedicated SQLite database file and rejects configurations where the backup path conflicts with the primary storage path. Add test coverage for full-cycle backup mirroring, same-path rejection, and UniFFI-backed builder configuration. Update `setup_builder!` so FFI-backed builder tests can use mutable configuration helpers.
1 parent 6f5dca7 commit 309e9ec

6 files changed

Lines changed: 306 additions & 349 deletions

File tree

src/builder.rs

Lines changed: 137 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ use crate::event::EventQueue;
5757
use crate::fee_estimator::OnchainFeeEstimator;
5858
use crate::gossip::GossipSource;
5959
use crate::io::sqlite_store::SqliteStore;
60+
use crate::io::tier_store::TierStore;
6061
use crate::io::utils::{
6162
open_or_migrate_fs_store, read_all_objects, read_event_queue,
6263
read_external_pathfinding_scores_from_cache, read_network_graph, read_node_metrics,
@@ -150,6 +151,21 @@ impl std::fmt::Debug for LogWriterConfig {
150151
}
151152
}
152153

154+
#[derive(Default)]
155+
struct TierStoreConfig {
156+
ephemeral: Option<Arc<DynStore>>,
157+
backup_storage_dir_path: Option<PathBuf>,
158+
}
159+
160+
impl std::fmt::Debug for TierStoreConfig {
161+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
162+
f.debug_struct("TierStoreConfig")
163+
.field("ephemeral", &self.ephemeral.as_ref().map(|_| "Arc<DynStore>"))
164+
.field("backup_storage_dir_path", &self.backup_storage_dir_path)
165+
.finish()
166+
}
167+
}
168+
153169
/// An error encountered during building a [`Node`].
154170
///
155171
/// [`Node`]: crate::Node
@@ -196,6 +212,11 @@ pub enum BuildError {
196212
AsyncPaymentsConfigMismatch,
197213
/// An attempt to setup a DNS Resolver failed.
198214
DNSResolverSetupFailed,
215+
/// The configured backup storage path conflicts with the primary storage path.
216+
///
217+
/// Backup storage must use a distinct local directory so that the primary and
218+
/// backup stores do not point to the same SQLite database.
219+
BackupStorePathConflict,
199220
}
200221

201222
impl fmt::Display for BuildError {
@@ -233,6 +254,12 @@ impl fmt::Display for BuildError {
233254
Self::DNSResolverSetupFailed => {
234255
write!(f, "An attempt to setup a DNS resolver has failed.")
235256
},
257+
Self::BackupStorePathConflict => {
258+
write!(
259+
f,
260+
"The configured backup storage path conflicts with the primary storage path."
261+
)
262+
},
236263
}
237264
}
238265
}
@@ -285,6 +312,7 @@ pub struct NodeBuilder {
285312
liquidity_source_config: Option<LiquiditySourceConfig>,
286313
log_writer_config: Option<LogWriterConfig>,
287314
async_payments_role: Option<AsyncPaymentsRole>,
315+
tier_store_config: Option<TierStoreConfig>,
288316
runtime_handle: Option<tokio::runtime::Handle>,
289317
pathfinding_scores_sync_config: Option<PathfindingScoresSyncConfig>,
290318
recovery_mode: bool,
@@ -303,6 +331,7 @@ impl NodeBuilder {
303331
let gossip_source_config = None;
304332
let liquidity_source_config = None;
305333
let log_writer_config = None;
334+
let tier_store_config = None;
306335
let runtime_handle = None;
307336
let pathfinding_scores_sync_config = None;
308337
let recovery_mode = false;
@@ -312,6 +341,7 @@ impl NodeBuilder {
312341
gossip_source_config,
313342
liquidity_source_config,
314343
log_writer_config,
344+
tier_store_config,
315345
runtime_handle,
316346
async_payments_role: None,
317347
pathfinding_scores_sync_config,
@@ -612,6 +642,42 @@ impl NodeBuilder {
612642
self
613643
}
614644

645+
/// Configures a local SQLite backup store for disaster recovery.
646+
///
647+
/// When building with tiered storage, a SQLite store will be created at the
648+
/// given directory path using [`SQLITE_BACKUP_DB_FILE_NAME`] as its database
649+
/// file name. It receives a second durable copy of data written to the
650+
/// primary store.
651+
///
652+
/// Writes and removals for primary-backed data only succeed once both the
653+
/// primary and backup SQLite stores complete successfully.
654+
///
655+
/// The configured path must point to a distinct local directory from the
656+
/// primary storage path. If the backup path equals the primary storage path,
657+
/// building will fail with [`BuildError::BackupStorePathConflict`].
658+
///
659+
/// If not set, durable data will be stored only in the primary store.
660+
///
661+
/// [`SQLITE_BACKUP_DB_FILE_NAME`]: crate::io::sqlite_store::SQLITE_BACKUP_DB_FILE_NAME
662+
pub fn set_backup_storage_dir_path(&mut self, backup_storage_dir_path: String) -> &mut Self {
663+
let tier_store_config = self.tier_store_config.get_or_insert(TierStoreConfig::default());
664+
tier_store_config.backup_storage_dir_path = Some(backup_storage_dir_path.into());
665+
self
666+
}
667+
668+
/// Configures the ephemeral store for non-critical, frequently-accessed data.
669+
///
670+
/// When building with tiered storage, this store is used for ephemeral data like
671+
/// the network graph and scorer data to reduce latency for reads. Data stored here
672+
/// can be rebuilt if lost.
673+
///
674+
/// If not set, non-critical data will be stored in the primary store.
675+
pub fn set_ephemeral_store(&mut self, ephemeral_store: Arc<DynStore>) -> &mut Self {
676+
let tier_store_config = self.tier_store_config.get_or_insert(TierStoreConfig::default());
677+
tier_store_config.ephemeral = Some(ephemeral_store);
678+
self
679+
}
680+
615681
/// Builds a [`Node`] instance with a [`SqliteStore`] backend and according to the options
616682
/// previously configured.
617683
pub fn build(&self, node_entropy: NodeEntropy) -> Result<Node, BuildError> {
@@ -813,11 +879,18 @@ impl NodeBuilder {
813879
}
814880

815881
/// Builds a [`Node`] instance according to the options previously configured.
882+
///
883+
/// The provided `kv_store` will be used as the primary storage backend. Optionally,
884+
/// an ephemeral store for frequently-accessed non-critical data (e.g., network graph, scorer)
885+
/// and a local SQLite backup store for disaster recovery can be configured via
886+
/// [`set_ephemeral_store`] and [`set_backup_storage_dir_path`].
887+
///
888+
/// [`set_ephemeral_store`]: Self::set_ephemeral_store
889+
/// [`set_backup_storage_dir_path`]: Self::set_backup_storage_dir_path
816890
pub fn build_with_store<S: KVStore + Send + Sync + 'static>(
817891
&self, node_entropy: NodeEntropy, kv_store: S,
818892
) -> Result<Node, BuildError> {
819893
let logger = setup_logger(&self.log_writer_config, &self.config)?;
820-
821894
self.build_with_store_and_logger(node_entropy, kv_store, logger)
822895
}
823896

@@ -842,6 +915,36 @@ impl NodeBuilder {
842915
fn build_with_store_runtime_and_logger<S: KVStore + Send + Sync + 'static>(
843916
&self, node_entropy: NodeEntropy, kv_store: S, runtime: Arc<Runtime>, logger: Arc<Logger>,
844917
) -> Result<Node, BuildError> {
918+
let ts_config = self.tier_store_config.as_ref();
919+
let primary_store = Arc::new(DynStoreWrapper(kv_store));
920+
let mut tier_store = TierStore::new(primary_store, Arc::clone(&logger));
921+
if let Some(config) = ts_config {
922+
config.ephemeral.as_ref().map(|s| tier_store.set_ephemeral_store(Arc::clone(s)));
923+
if let Some(backup_storage_dir_path) = config.backup_storage_dir_path.as_ref() {
924+
let primary_storage_dir_path = PathBuf::from(&self.config.storage_dir_path);
925+
if primary_storage_dir_path == *backup_storage_dir_path {
926+
log_error!(
927+
logger,
928+
"Backup storage path must differ from primary storage path: {}",
929+
backup_storage_dir_path.display()
930+
);
931+
return Err(BuildError::BackupStorePathConflict);
932+
}
933+
934+
let backup_store = SqliteStore::new(
935+
backup_storage_dir_path.clone(),
936+
Some(io::sqlite_store::SQLITE_BACKUP_DB_FILE_NAME.to_string()),
937+
Some(io::sqlite_store::KV_TABLE_NAME.to_string()),
938+
)
939+
.map_err(|e| {
940+
log_error!(logger, "Failed to setup backup SQLite store: {}", e);
941+
BuildError::KVStoreSetupFailed
942+
})?;
943+
let backup_store: Arc<DynStore> = Arc::new(DynStoreWrapper(backup_store));
944+
tier_store.set_backup_store(backup_store);
945+
}
946+
}
947+
845948
let seed_bytes = node_entropy.to_seed_bytes();
846949
let config = Arc::new(self.config.clone());
847950

@@ -856,7 +959,7 @@ impl NodeBuilder {
856959
seed_bytes,
857960
runtime,
858961
logger,
859-
Arc::new(DynStoreWrapper(kv_store)),
962+
Arc::new(DynStoreWrapper(tier_store)),
860963
)
861964
}
862965
}
@@ -1148,6 +1251,38 @@ impl ArcedNodeBuilder {
11481251
self.inner.write().expect("lock").set_wallet_recovery_mode();
11491252
}
11501253

1254+
/// Configures a local SQLite backup store for disaster recovery.
1255+
///
1256+
/// When building with tiered storage, a SQLite store will be created at the
1257+
/// given directory path using [`SQLITE_BACKUP_DB_FILE_NAME`] as its database
1258+
/// file name. It receives a second durable copy of data written to the
1259+
/// primary store.
1260+
///
1261+
/// Writes and removals for primary-backed data only succeed once both the
1262+
/// primary and backup SQLite stores complete successfully.
1263+
///
1264+
/// The configured path must point to a distinct local directory from the
1265+
/// primary storage path. If the backup path equals the primary storage path,
1266+
/// building will fail with [`BuildError::BackupStorePathConflict`].
1267+
///
1268+
/// If not set, durable data will be stored only in the primary store.
1269+
///
1270+
/// [`SQLITE_BACKUP_DB_FILE_NAME`]: crate::io::sqlite_store::SQLITE_BACKUP_DB_FILE_NAME
1271+
pub fn set_backup_storage_dir_path(&self, backup_storage_dir_path: String) {
1272+
self.inner.write().expect("lock").set_backup_storage_dir_path(backup_storage_dir_path);
1273+
}
1274+
1275+
/// Configures the ephemeral store for non-critical, frequently-accessed data.
1276+
///
1277+
/// When building with tiered storage, this store is used for ephemeral data like
1278+
/// the network graph and scorer data to reduce latency for reads. Data stored here
1279+
/// can be rebuilt if lost.
1280+
///
1281+
/// If not set, non-critical data will be stored in the primary store.
1282+
pub fn set_ephemeral_store(&self, ephemeral_store: Arc<DynStore>) {
1283+
self.inner.write().expect("lock").set_ephemeral_store(ephemeral_store);
1284+
}
1285+
11511286
/// Builds a [`Node`] instance with a [`SqliteStore`] backend and according to the options
11521287
/// previously configured.
11531288
pub fn build(&self, node_entropy: Arc<NodeEntropy>) -> Result<Arc<Node>, BuildError> {

src/io/sqlite_store/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ mod migrations;
2424

2525
/// LDK Node's database file name.
2626
pub const SQLITE_DB_FILE_NAME: &str = "ldk_node_data.sqlite";
27+
/// LDK Node's backup database file name.
28+
pub const SQLITE_BACKUP_DB_FILE_NAME: &str = "ldk_node_data_backup.sqlite";
2729
/// LDK Node's table in which we store all data.
2830
pub const KV_TABLE_NAME: &str = "ldk_node_data";
2931

0 commit comments

Comments
 (0)