Skip to content

Commit 3eef01d

Browse files
committed
Integrate TierStore into NodeBuilder
Add native builder support for configuring ephemeral storage and a local SQLite backup mirror. Wrap the primary store in TierStore during node construction and create the configured secondary stores using dedicated SQLite database files. Add full-cycle integration coverage verifying durable backup mirroring.
1 parent e670591 commit 3eef01d

6 files changed

Lines changed: 96 additions & 8 deletions

File tree

src/builder.rs

Lines changed: 85 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,12 @@ impl std::fmt::Debug for LogWriterConfig {
150151
}
151152
}
152153

154+
#[derive(Default, Debug)]
155+
struct TierStoreConfig {
156+
ephemeral_storage_dir_path: Option<PathBuf>,
157+
backup_storage_dir_path: Option<PathBuf>,
158+
}
159+
153160
/// An error encountered during building a [`Node`].
154161
///
155162
/// [`Node`]: crate::Node
@@ -285,6 +292,7 @@ pub struct NodeBuilder {
285292
liquidity_source_config: Option<LiquiditySourceConfig>,
286293
log_writer_config: Option<LogWriterConfig>,
287294
async_payments_role: Option<AsyncPaymentsRole>,
295+
tier_store_config: Option<TierStoreConfig>,
288296
runtime_handle: Option<tokio::runtime::Handle>,
289297
pathfinding_scores_sync_config: Option<PathfindingScoresSyncConfig>,
290298
recovery_mode: bool,
@@ -303,6 +311,7 @@ impl NodeBuilder {
303311
let gossip_source_config = None;
304312
let liquidity_source_config = None;
305313
let log_writer_config = None;
314+
let tier_store_config = None;
306315
let runtime_handle = None;
307316
let pathfinding_scores_sync_config = None;
308317
let recovery_mode = false;
@@ -312,6 +321,7 @@ impl NodeBuilder {
312321
gossip_source_config,
313322
liquidity_source_config,
314323
log_writer_config,
324+
tier_store_config,
315325
runtime_handle,
316326
async_payments_role: None,
317327
pathfinding_scores_sync_config,
@@ -612,6 +622,39 @@ impl NodeBuilder {
612622
self
613623
}
614624

625+
/// Configures a local SQLite backup store for disaster recovery.
626+
///
627+
/// When building with tiered storage, a SQLite store will be created at the
628+
/// given directory path using [`SQLITE_BACKUP_DB_FILE_NAME`] as its database
629+
/// file name. It receives a second durable copy of data written to the
630+
/// primary store.
631+
///
632+
/// Writes and removals for primary-backed data only succeed once both the
633+
/// primary and backup SQLite stores complete successfully.
634+
///
635+
/// If not set, durable data will be stored only in the primary store.
636+
///
637+
/// [`SQLITE_BACKUP_DB_FILE_NAME`]: crate::io::sqlite_store::SQLITE_BACKUP_DB_FILE_NAME
638+
pub fn set_backup_storage_dir_path(&mut self, backup_storage_dir_path: String) -> &mut Self {
639+
let tier_store_config = self.tier_store_config.get_or_insert(TierStoreConfig::default());
640+
tier_store_config.backup_storage_dir_path = Some(backup_storage_dir_path.into());
641+
self
642+
}
643+
644+
/// Configures the ephemeral storage directory path for non-critical, frequently-accessed data.
645+
///
646+
/// When set, a local SQLite store is created at this path for ephemeral data like
647+
/// the network graph and scorer. Data stored here can be rebuilt if lost.
648+
///
649+
/// If not set, non-critical data will be stored in the primary store.
650+
pub fn set_ephemeral_storage_dir_path(
651+
&mut self, ephemeral_storage_dir_path: String,
652+
) -> &mut Self {
653+
let tier_store_config = self.tier_store_config.get_or_insert(TierStoreConfig::default());
654+
tier_store_config.ephemeral_storage_dir_path = Some(ephemeral_storage_dir_path.into());
655+
self
656+
}
657+
615658
/// Builds a [`Node`] instance with a [`SqliteStore`] backend and according to the options
616659
/// previously configured.
617660
pub fn build(&self, node_entropy: NodeEntropy) -> Result<Node, BuildError> {
@@ -813,11 +856,18 @@ impl NodeBuilder {
813856
}
814857

815858
/// Builds a [`Node`] instance according to the options previously configured.
859+
///
860+
/// The provided `kv_store` will be used as the primary storage backend. Optionally,
861+
/// an ephemeral store for frequently-accessed non-critical data (e.g., network graph, scorer)
862+
/// and a local SQLite backup store for disaster recovery can be configured via
863+
/// [`set_ephemeral_storage_dir_path`] and [`set_backup_storage_dir_path`].
864+
///
865+
/// [`set_ephemeral_storage_dir_path`]: Self::set_ephemeral_storage_dir_path
866+
/// [`set_backup_storage_dir_path`]: Self::set_backup_storage_dir_path
816867
pub fn build_with_store<S: KVStore + Send + Sync + 'static>(
817868
&self, node_entropy: NodeEntropy, kv_store: S,
818869
) -> Result<Node, BuildError> {
819870
let logger = setup_logger(&self.log_writer_config, &self.config)?;
820-
821871
self.build_with_store_and_logger(node_entropy, kv_store, logger)
822872
}
823873

@@ -842,6 +892,39 @@ impl NodeBuilder {
842892
fn build_with_store_runtime_and_logger<S: KVStore + Send + Sync + 'static>(
843893
&self, node_entropy: NodeEntropy, kv_store: S, runtime: Arc<Runtime>, logger: Arc<Logger>,
844894
) -> Result<Node, BuildError> {
895+
let ts_config = self.tier_store_config.as_ref();
896+
let primary_store = Arc::new(DynStoreWrapper(kv_store));
897+
let mut tier_store = TierStore::new(primary_store, Arc::clone(&logger));
898+
if let Some(config) = ts_config {
899+
if let Some(ephemeral_storage_dir_path) = config.ephemeral_storage_dir_path.as_ref() {
900+
let ephemeral_store = SqliteStore::new(
901+
ephemeral_storage_dir_path.clone(),
902+
Some(io::sqlite_store::SQLITE_EPHEMERAL_DB_FILE_NAME.to_string()),
903+
Some(io::sqlite_store::KV_TABLE_NAME.to_string()),
904+
)
905+
.map_err(|e| {
906+
log_error!(logger, "Failed to setup ephemeral SQLite store: {}", e);
907+
BuildError::KVStoreSetupFailed
908+
})?;
909+
let ephemeral_store: Arc<DynStore> = Arc::new(DynStoreWrapper(ephemeral_store));
910+
tier_store.set_ephemeral_store(ephemeral_store);
911+
}
912+
913+
if let Some(backup_storage_dir_path) = config.backup_storage_dir_path.as_ref() {
914+
let backup_store = SqliteStore::new(
915+
backup_storage_dir_path.clone(),
916+
Some(io::sqlite_store::SQLITE_BACKUP_DB_FILE_NAME.to_string()),
917+
Some(io::sqlite_store::KV_TABLE_NAME.to_string()),
918+
)
919+
.map_err(|e| {
920+
log_error!(logger, "Failed to setup backup SQLite store: {}", e);
921+
BuildError::KVStoreSetupFailed
922+
})?;
923+
let backup_store: Arc<DynStore> = Arc::new(DynStoreWrapper(backup_store));
924+
tier_store.set_backup_store(backup_store);
925+
}
926+
}
927+
845928
let seed_bytes = node_entropy.to_seed_bytes();
846929
let config = Arc::new(self.config.clone());
847930

@@ -856,7 +939,7 @@ impl NodeBuilder {
856939
seed_bytes,
857940
runtime,
858941
logger,
859-
Arc::new(DynStoreWrapper(kv_store)),
942+
Arc::new(DynStoreWrapper(tier_store)),
860943
)
861944
}
862945
}

src/io/sqlite_store/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ 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";
29+
/// LDK Node's ephemeral database file name.
30+
pub const SQLITE_EPHEMERAL_DB_FILE_NAME: &str = "ldk_node_data_ephemeral.sqlite";
2731
/// LDK Node's table in which we store all data.
2832
pub const KV_TABLE_NAME: &str = "ldk_node_data";
2933

src/io/tier_store.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
55
// http://opensource.org/licenses/MIT>, at your option. You may not use this file except in
66
// accordance with one or both of these licenses.
7-
#![allow(dead_code)] // TODO: Temporal warning silencer. Will be removed in later commit.
87

98
use std::collections::HashMap;
109
use std::future::Future;

src/types.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ impl<'a> KVStore for dyn DynStoreTrait + 'a {
9595
}
9696
}
9797

98-
pub(crate) type DynStore = dyn DynStoreTrait;
98+
pub type DynStore = dyn DynStoreTrait;
9999

100100
// Newtype wrapper that implements `KVStore` for `Arc<DynStore>`. This is needed because `KVStore`
101101
// methods return `impl Future`, which is not object-safe. `DynStoreTrait` works around this by

tests/common/mod.rs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -532,6 +532,7 @@ pub(crate) fn setup_two_nodes_with_store(
532532

533533
pub(crate) fn setup_node(chain_source: &TestChainSource, config: TestConfig) -> TestNode {
534534
setup_builder!(builder, config.node_config);
535+
535536
match chain_source {
536537
TestChainSource::Esplora(electrsd) => {
537538
let esplora_url = format!("http://{}", electrsd.esplora_url.as_ref().unwrap());
@@ -601,10 +602,6 @@ pub(crate) fn setup_node(chain_source: &TestChainSource, config: TestConfig) ->
601602
},
602603
};
603604

604-
if config.recovery_mode {
605-
builder.set_wallet_recovery_mode();
606-
}
607-
608605
node.start().unwrap();
609606
assert!(node.status().is_running);
610607
assert!(node.status().latest_fee_rate_cache_update_timestamp.is_some());

tests/integration_tests_rust.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ use electrsd::corepc_node::Node as BitcoinD;
3030
use electrsd::ElectrsD;
3131
use ldk_node::config::{AsyncPaymentsRole, EsploraSyncConfig};
3232
use ldk_node::entropy::NodeEntropy;
33+
#[cfg(not(feature = "uniffi"))]
34+
use ldk_node::io::sqlite_store::SqliteStore;
3335
use ldk_node::liquidity::LSPS2ServiceConfig;
3436
use ldk_node::payment::{
3537
ConfirmationStatus, PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus,
@@ -39,6 +41,8 @@ use ldk_node::{Builder, Event, NodeError};
3941
use lightning::ln::channelmanager::PaymentId;
4042
use lightning::routing::gossip::{NodeAlias, NodeId};
4143
use lightning::routing::router::RouteParametersConfig;
44+
#[cfg(not(feature = "uniffi"))]
45+
use lightning::util::persist::KVStore;
4246
use lightning_invoice::{Bolt11InvoiceDescription, Description};
4347
use lightning_types::payment::{PaymentHash, PaymentPreimage};
4448
use log::LevelFilter;
@@ -3141,6 +3145,7 @@ async fn splice_in_with_all_balance() {
31413145
node_b.stop().unwrap();
31423146
}
31433147

3148+
#[cfg(not(feature = "uniffi"))]
31443149
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
31453150
async fn lsps2_multi_lsp_picks_cheapest() {
31463151
do_lsps2_multi_lsp_picks_cheapest(false).await;

0 commit comments

Comments
 (0)