Skip to content

Commit 7c80b43

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 0459709 commit 7c80b43

6 files changed

Lines changed: 307 additions & 350 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,
@@ -154,6 +155,21 @@ impl std::fmt::Debug for LogWriterConfig {
154155
}
155156
}
156157

158+
#[derive(Default)]
159+
struct TierStoreConfig {
160+
ephemeral: Option<Arc<DynStore>>,
161+
backup_storage_dir_path: Option<PathBuf>,
162+
}
163+
164+
impl std::fmt::Debug for TierStoreConfig {
165+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
166+
f.debug_struct("TierStoreConfig")
167+
.field("ephemeral", &self.ephemeral.as_ref().map(|_| "Arc<DynStore>"))
168+
.field("backup_storage_dir_path", &self.backup_storage_dir_path)
169+
.finish()
170+
}
171+
}
172+
157173
/// An error encountered during building a [`Node`].
158174
///
159175
/// [`Node`]: crate::Node
@@ -200,6 +216,11 @@ pub enum BuildError {
200216
AsyncPaymentsConfigMismatch,
201217
/// An attempt to setup a DNS Resolver failed.
202218
DNSResolverSetupFailed,
219+
/// The configured backup storage path conflicts with the primary storage path.
220+
///
221+
/// Backup storage must use a distinct local directory so that the primary and
222+
/// backup stores do not point to the same SQLite database.
223+
BackupStorePathConflict,
203224
}
204225

205226
impl fmt::Display for BuildError {
@@ -237,6 +258,12 @@ impl fmt::Display for BuildError {
237258
Self::DNSResolverSetupFailed => {
238259
write!(f, "An attempt to setup a DNS resolver has failed.")
239260
},
261+
Self::BackupStorePathConflict => {
262+
write!(
263+
f,
264+
"The configured backup storage path conflicts with the primary storage path."
265+
)
266+
},
240267
}
241268
}
242269
}
@@ -289,6 +316,7 @@ pub struct NodeBuilder {
289316
liquidity_source_config: Option<LiquiditySourceConfig>,
290317
log_writer_config: Option<LogWriterConfig>,
291318
async_payments_role: Option<AsyncPaymentsRole>,
319+
tier_store_config: Option<TierStoreConfig>,
292320
runtime_handle: Option<tokio::runtime::Handle>,
293321
pathfinding_scores_sync_config: Option<PathfindingScoresSyncConfig>,
294322
recovery_mode: bool,
@@ -307,6 +335,7 @@ impl NodeBuilder {
307335
let gossip_source_config = None;
308336
let liquidity_source_config = None;
309337
let log_writer_config = None;
338+
let tier_store_config = None;
310339
let runtime_handle = None;
311340
let pathfinding_scores_sync_config = None;
312341
let recovery_mode = false;
@@ -316,6 +345,7 @@ impl NodeBuilder {
316345
gossip_source_config,
317346
liquidity_source_config,
318347
log_writer_config,
348+
tier_store_config,
319349
runtime_handle,
320350
async_payments_role: None,
321351
pathfinding_scores_sync_config,
@@ -625,6 +655,42 @@ impl NodeBuilder {
625655
self
626656
}
627657

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

828894
/// Builds a [`Node`] instance according to the options previously configured.
895+
///
896+
/// The provided `kv_store` will be used as the primary storage backend. Optionally,
897+
/// an ephemeral store for frequently-accessed non-critical data (e.g., network graph, scorer)
898+
/// and a local SQLite backup store for disaster recovery can be configured via
899+
/// [`set_ephemeral_store`] and [`set_backup_storage_dir_path`].
900+
///
901+
/// [`set_ephemeral_store`]: Self::set_ephemeral_store
902+
/// [`set_backup_storage_dir_path`]: Self::set_backup_storage_dir_path
829903
pub fn build_with_store<S: KVStore + Send + Sync + 'static>(
830904
&self, node_entropy: NodeEntropy, kv_store: S,
831905
) -> Result<Node, BuildError> {
832906
let logger = setup_logger(&self.log_writer_config, &self.config)?;
833-
834907
self.build_with_store_and_logger(node_entropy, kv_store, logger)
835908
}
836909

@@ -855,6 +928,36 @@ impl NodeBuilder {
855928
fn build_with_store_runtime_and_logger<S: KVStore + Send + Sync + 'static>(
856929
&self, node_entropy: NodeEntropy, kv_store: S, runtime: Arc<Runtime>, logger: Arc<Logger>,
857930
) -> Result<Node, BuildError> {
931+
let ts_config = self.tier_store_config.as_ref();
932+
let primary_store = Arc::new(DynStoreWrapper(kv_store));
933+
let mut tier_store = TierStore::new(primary_store, Arc::clone(&logger));
934+
if let Some(config) = ts_config {
935+
config.ephemeral.as_ref().map(|s| tier_store.set_ephemeral_store(Arc::clone(s)));
936+
if let Some(backup_storage_dir_path) = config.backup_storage_dir_path.as_ref() {
937+
let primary_storage_dir_path = PathBuf::from(&self.config.storage_dir_path);
938+
if primary_storage_dir_path == *backup_storage_dir_path {
939+
log_error!(
940+
logger,
941+
"Backup storage path must differ from primary storage path: {}",
942+
backup_storage_dir_path.display()
943+
);
944+
return Err(BuildError::BackupStorePathConflict);
945+
}
946+
947+
let backup_store = SqliteStore::new(
948+
backup_storage_dir_path.clone(),
949+
Some(io::sqlite_store::SQLITE_BACKUP_DB_FILE_NAME.to_string()),
950+
Some(io::sqlite_store::KV_TABLE_NAME.to_string()),
951+
)
952+
.map_err(|e| {
953+
log_error!(logger, "Failed to setup backup SQLite store: {}", e);
954+
BuildError::KVStoreSetupFailed
955+
})?;
956+
let backup_store: Arc<DynStore> = Arc::new(DynStoreWrapper(backup_store));
957+
tier_store.set_backup_store(backup_store);
958+
}
959+
}
960+
858961
let seed_bytes = node_entropy.to_seed_bytes();
859962
let config = Arc::new(self.config.clone());
860963

@@ -869,7 +972,7 @@ impl NodeBuilder {
869972
seed_bytes,
870973
runtime,
871974
logger,
872-
Arc::new(DynStoreWrapper(kv_store)),
975+
Arc::new(DynStoreWrapper(tier_store)),
873976
)
874977
}
875978
}
@@ -1164,6 +1267,38 @@ impl ArcedNodeBuilder {
11641267
self.inner.write().expect("lock").set_wallet_recovery_mode();
11651268
}
11661269

1270+
/// Configures a local SQLite backup store for disaster recovery.
1271+
///
1272+
/// When building with tiered storage, a SQLite store will be created at the
1273+
/// given directory path using [`SQLITE_BACKUP_DB_FILE_NAME`] as its database
1274+
/// file name. It receives a second durable copy of data written to the
1275+
/// primary store.
1276+
///
1277+
/// Writes and removals for primary-backed data only succeed once both the
1278+
/// primary and backup SQLite stores complete successfully.
1279+
///
1280+
/// The configured path must point to a distinct local directory from the
1281+
/// primary storage path. If the backup path equals the primary storage path,
1282+
/// building will fail with [`BuildError::BackupStorePathConflict`].
1283+
///
1284+
/// If not set, durable data will be stored only in the primary store.
1285+
///
1286+
/// [`SQLITE_BACKUP_DB_FILE_NAME`]: crate::io::sqlite_store::SQLITE_BACKUP_DB_FILE_NAME
1287+
pub fn set_backup_storage_dir_path(&self, backup_storage_dir_path: String) {
1288+
self.inner.write().expect("lock").set_backup_storage_dir_path(backup_storage_dir_path);
1289+
}
1290+
1291+
/// Configures the ephemeral store for non-critical, frequently-accessed data.
1292+
///
1293+
/// When building with tiered storage, this store is used for ephemeral data like
1294+
/// the network graph and scorer data to reduce latency for reads. Data stored here
1295+
/// can be rebuilt if lost.
1296+
///
1297+
/// If not set, non-critical data will be stored in the primary store.
1298+
pub fn set_ephemeral_store(&self, ephemeral_store: Arc<DynStore>) {
1299+
self.inner.write().expect("lock").set_ephemeral_store(ephemeral_store);
1300+
}
1301+
11671302
/// Builds a [`Node`] instance with a [`SqliteStore`] backend and according to the options
11681303
/// previously configured.
11691304
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)