@@ -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,
@@ -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
201222impl 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 > {
0 commit comments