@@ -57,6 +57,7 @@ use crate::event::EventQueue;
5757use crate :: fee_estimator:: OnchainFeeEstimator ;
5858use crate :: gossip:: GossipSource ;
5959use crate :: io:: sqlite_store:: SqliteStore ;
60+ use crate :: io:: tier_store:: TierStore ;
6061use 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
205226impl 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 > {
0 commit comments