Skip to content

Commit ed08cd2

Browse files
committed
Integrate TierStore into NodeBuilder
TierStore remains internal until NodeBuilder provisions and installs its storage backends. Add builder options for local ephemeral and backup SQLite stores, wrap the configured primary store in TierStore, and pass the resulting store into node construction. When ephemeral storage is enabled, automatically create the persistent ordering index in the node's storage directory and require TierStore to own it exclusively. Update filesystem-backed tests and add integration coverage confirming that configured backup storage receives durable primary-backed data. Assisted-by: Amp (AI coding agent)
1 parent 2df10c0 commit ed08cd2

5 files changed

Lines changed: 174 additions & 6 deletions

File tree

src/builder.rs

Lines changed: 94 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ use crate::gossip::GossipSource;
7272
use crate::io::fs_store::open_or_migrate_fs_store;
7373
#[cfg(feature = "storage-sqlite")]
7474
use crate::io::sqlite_store::SqliteStore;
75+
use crate::io::tier_store::{setup_index_store, TierStore};
7576
use crate::io::utils::{
7677
read_all_objects, read_event_queue, read_external_pathfinding_scores_from_cache,
7778
read_n_objects, read_network_graph, read_node_metrics, read_output_sweeper, read_peer_info,
@@ -173,6 +174,12 @@ impl std::fmt::Debug for LogWriterConfig {
173174
}
174175
}
175176

177+
#[derive(Default, Debug)]
178+
struct TierStoreConfig {
179+
ephemeral_storage_dir_path: Option<PathBuf>,
180+
backup_storage_dir_path: Option<PathBuf>,
181+
}
182+
176183
/// An error encountered during building a [`Node`].
177184
///
178185
/// [`Node`]: crate::Node
@@ -326,6 +333,7 @@ pub struct NodeBuilder {
326333
liquidity_source_config: Option<LiquiditySourceConfig>,
327334
log_writer_config: Option<LogWriterConfig>,
328335
async_payments_role: Option<AsyncPaymentsRole>,
336+
tier_store_config: Option<TierStoreConfig>,
329337
runtime_handle: Option<tokio::runtime::Handle>,
330338
pathfinding_scores_sync_config: Option<PathfindingScoresSyncConfig>,
331339
probing_config: Option<ProbingConfig>,
@@ -347,6 +355,7 @@ impl NodeBuilder {
347355
let gossip_source_config = None;
348356
let liquidity_source_config = None;
349357
let log_writer_config = None;
358+
let tier_store_config = None;
350359
let runtime_handle = None;
351360
let pathfinding_scores_sync_config = None;
352361
let probing_config = None;
@@ -356,6 +365,7 @@ impl NodeBuilder {
356365
gossip_source_config,
357366
liquidity_source_config,
358367
log_writer_config,
368+
tier_store_config,
359369
runtime_handle,
360370
async_payments_role: None,
361371
pathfinding_scores_sync_config,
@@ -686,6 +696,41 @@ impl NodeBuilder {
686696
self
687697
}
688698

699+
/// Configures a local SQLite backup store for disaster recovery.
700+
///
701+
/// When building with tiered storage, a SQLite store will be created at the
702+
/// given directory path using [`SQLITE_BACKUP_DB_FILE_NAME`] as its database
703+
/// file name. It receives a second durable copy of data written to the
704+
/// primary store.
705+
///
706+
/// Writes and removals for primary-backed data only succeed once both the
707+
/// primary and backup SQLite stores complete successfully.
708+
///
709+
/// If not set, durable data will be stored only in the primary store.
710+
///
711+
/// [`SQLITE_BACKUP_DB_FILE_NAME`]: crate::io::sqlite_store::SQLITE_BACKUP_DB_FILE_NAME
712+
#[cfg(not(feature = "uniffi"))]
713+
pub fn set_backup_storage_dir_path(&mut self, backup_storage_dir_path: String) -> &mut Self {
714+
let tier_store_config = self.tier_store_config.get_or_insert(TierStoreConfig::default());
715+
tier_store_config.backup_storage_dir_path = Some(backup_storage_dir_path.into());
716+
self
717+
}
718+
719+
/// Configures the ephemeral storage directory path for non-critical, frequently-accessed data.
720+
///
721+
/// When set, a local SQLite store is created at this path for ephemeral data like
722+
/// the network graph and scorer. Data stored here can be rebuilt if lost.
723+
///
724+
/// If not set, non-critical data will be stored in the primary store.
725+
#[cfg(not(feature = "uniffi"))]
726+
pub fn set_ephemeral_storage_dir_path(
727+
&mut self, ephemeral_storage_dir_path: String,
728+
) -> &mut Self {
729+
let tier_store_config = self.tier_store_config.get_or_insert(TierStoreConfig::default());
730+
tier_store_config.ephemeral_storage_dir_path = Some(ephemeral_storage_dir_path.into());
731+
self
732+
}
733+
689734
/// Builds a [`Node`] instance with a [`SqliteStore`] backend and according to the options
690735
/// previously configured.
691736
#[cfg(feature = "storage-sqlite")]
@@ -901,11 +946,18 @@ impl NodeBuilder {
901946
}
902947

903948
/// Builds a [`Node`] instance according to the options previously configured.
949+
///
950+
/// The provided `kv_store` will be used as the primary storage backend. Optionally,
951+
/// an ephemeral store for frequently-accessed non-critical data (e.g., network graph, scorer)
952+
/// and a local SQLite backup store for disaster recovery can be configured via
953+
/// [`set_ephemeral_storage_dir_path`] and [`set_backup_storage_dir_path`].
954+
///
955+
/// [`set_ephemeral_storage_dir_path`]: Self::set_ephemeral_storage_dir_path
956+
/// [`set_backup_storage_dir_path`]: Self::set_backup_storage_dir_path
904957
pub fn build_with_store<S: PaginatedKVStore + Send + Sync + 'static>(
905958
&self, node_entropy: NodeEntropy, kv_store: S,
906959
) -> Result<Node, BuildError> {
907960
let logger = setup_logger(&self.log_writer_config, &self.config)?;
908-
909961
self.build_with_store_and_logger(node_entropy, kv_store, logger)
910962
}
911963

@@ -930,6 +982,46 @@ impl NodeBuilder {
930982
fn build_with_store_runtime_and_logger<S: PaginatedKVStore + Send + Sync + 'static>(
931983
&self, node_entropy: NodeEntropy, kv_store: S, runtime: Arc<Runtime>, logger: Arc<Logger>,
932984
) -> Result<Node, BuildError> {
985+
let ts_config = self.tier_store_config.as_ref();
986+
let primary_store = Arc::new(DynStoreWrapper(kv_store));
987+
let mut tier_store = TierStore::new(primary_store, Arc::clone(&logger));
988+
if let Some(config) = ts_config {
989+
if let Some(ephemeral_storage_dir_path) = config.ephemeral_storage_dir_path.as_ref() {
990+
let index_store = runtime
991+
.block_on(setup_index_store(self.config.storage_dir_path.clone().into()))
992+
.map_err(|e| {
993+
log_error!(logger, "Failed to setup tier-store index: {}", e);
994+
BuildError::KVStoreSetupFailed
995+
})?;
996+
let ephemeral_store = SqliteStore::new(
997+
ephemeral_storage_dir_path.clone(),
998+
Some(io::sqlite_store::SQLITE_EPHEMERAL_DB_FILE_NAME.to_string()),
999+
Some(io::sqlite_store::KV_TABLE_NAME.to_string()),
1000+
)
1001+
.map_err(|e| {
1002+
log_error!(logger, "Failed to setup ephemeral SQLite store: {}", e);
1003+
BuildError::KVStoreSetupFailed
1004+
})?;
1005+
let ephemeral_store: Arc<DynStore> = Arc::new(DynStoreWrapper(ephemeral_store));
1006+
tier_store.set_index_store(index_store);
1007+
tier_store.set_ephemeral_store(ephemeral_store);
1008+
}
1009+
1010+
if let Some(backup_storage_dir_path) = config.backup_storage_dir_path.as_ref() {
1011+
let backup_store = SqliteStore::new(
1012+
backup_storage_dir_path.clone(),
1013+
Some(io::sqlite_store::SQLITE_BACKUP_DB_FILE_NAME.to_string()),
1014+
Some(io::sqlite_store::KV_TABLE_NAME.to_string()),
1015+
)
1016+
.map_err(|e| {
1017+
log_error!(logger, "Failed to setup backup SQLite store: {}", e);
1018+
BuildError::KVStoreSetupFailed
1019+
})?;
1020+
let backup_store: Arc<DynStore> = Arc::new(DynStoreWrapper(backup_store));
1021+
tier_store.set_backup_store(backup_store);
1022+
}
1023+
}
1024+
9331025
let seed_bytes = node_entropy.to_seed_bytes();
9341026
let config = Arc::new(self.config.clone());
9351027

@@ -944,7 +1036,7 @@ impl NodeBuilder {
9441036
seed_bytes,
9451037
runtime,
9461038
logger,
947-
Arc::new(DynStoreWrapper(kv_store)),
1039+
Arc::new(DynStoreWrapper(tier_store)),
9481040
)
9491041
}
9501042
}

src/io/sqlite_store/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ mod migrations;
3030
pub const SQLITE_DB_FILE_NAME: &str = "ldk_node_data.sqlite";
3131
/// LDK Node's internal tier-store index database file name.
3232
pub(crate) const SQLITE_TIER_INDEX_DB_FILE_NAME: &str = "ldk_node_tier_index.sqlite";
33+
/// LDK Node's backup database file name.
34+
pub const SQLITE_BACKUP_DB_FILE_NAME: &str = "ldk_node_data_backup.sqlite";
35+
/// LDK Node's ephemeral database file name.
36+
pub const SQLITE_EPHEMERAL_DB_FILE_NAME: &str = "ldk_node_data_ephemeral.sqlite";
3337
/// LDK Node's table in which we store all data.
3438
pub const KV_TABLE_NAME: &str = "ldk_node_data";
3539

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;

tests/common/mod.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ use lightning::ln::msgs::SocketAddress;
6262
use lightning::routing::gossip::NodeAlias;
6363
use lightning::util::persist::{KVStore, PageToken, PaginatedKVStore, PaginatedListResponse};
6464
use lightning_invoice::{Bolt11InvoiceDescription, Description};
65-
use lightning_persister::fs_store::v1::FilesystemStore;
65+
use lightning_persister::fs_store::v2::FilesystemStoreV2;
6666
use lightning_types::payment::{PaymentHash, PaymentPreimage};
6767
use logging::TestLogWriter;
6868
use rand::distr::Alphanumeric;
@@ -1957,7 +1957,7 @@ impl PaginatedKVStore for TestSyncStore {
19571957
struct TestSyncStoreInner {
19581958
serializer: tokio::sync::RwLock<()>,
19591959
test_store: InMemoryStore,
1960-
fs_store: FilesystemStore,
1960+
fs_store: FilesystemStoreV2,
19611961
#[cfg(feature = "storage-sqlite")]
19621962
sqlite_store: SqliteStore,
19631963
}
@@ -1967,7 +1967,7 @@ impl TestSyncStoreInner {
19671967
let serializer = tokio::sync::RwLock::new(());
19681968
let mut fs_dir = dest_dir.clone();
19691969
fs_dir.push("fs_store");
1970-
let fs_store = FilesystemStore::new(fs_dir);
1970+
let fs_store = FilesystemStoreV2::new(fs_dir).unwrap();
19711971
#[cfg(feature = "storage-sqlite")]
19721972
let mut sql_dir = dest_dir.clone();
19731973
#[cfg(feature = "storage-sqlite")]

tests/integration_tests_rust.rs

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ use ldk_node::config::{
3838
AsyncPaymentsRole, EsploraSyncConfig, ADDRESS_POOL_SIZE, DEFAULT_FULL_SCAN_STOP_GAP,
3939
};
4040
use ldk_node::entropy::NodeEntropy;
41+
#[cfg(not(feature = "uniffi"))]
42+
use ldk_node::io::sqlite_store::SqliteStore;
4143
use ldk_node::liquidity::LSPS2ServiceConfig;
4244
use ldk_node::payment::{
4345
ConfirmationStatus, PayerProofOptions, PaymentDetails, PaymentDirection, PaymentKind,
@@ -4938,3 +4940,74 @@ async fn do_lsps2_multi_lsp_picks_cheapest(reverse_order: bool) {
49384940
cheap.stop().unwrap();
49394941
expensive.stop().unwrap();
49404942
}
4943+
4944+
// Builder backup-store configuration is not yet exposed via FFI (see #871)
4945+
#[cfg(not(feature = "uniffi"))]
4946+
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
4947+
async fn builder_configures_sqlite_backup_store() {
4948+
let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();
4949+
let chain_source = random_chain_source(&bitcoind, &electrsd);
4950+
4951+
let mut config_a = random_config();
4952+
config_a.store_type = TestStoreType::Sqlite;
4953+
let primary_dir = config_a.node_config.storage_dir_path.clone();
4954+
let backup_dir = common::random_storage_path();
4955+
4956+
// Build node_a with backup storage configured
4957+
setup_builder!(builder_a, config_a.node_config.clone());
4958+
builder_a.set_chain_source_esplora(
4959+
format!("http://{}", electrsd.esplora_url.as_ref().unwrap()),
4960+
None,
4961+
);
4962+
builder_a.set_filesystem_logger(None, None);
4963+
builder_a.set_backup_storage_dir_path(backup_dir.to_str().unwrap().to_owned());
4964+
4965+
let node_a = builder_a.build(config_a.node_entropy.into()).unwrap();
4966+
node_a.start().unwrap();
4967+
assert!(node_a.status().is_running);
4968+
assert!(node_a.status().latest_fee_rate_cache_update_timestamp.is_some());
4969+
4970+
let mut config_b = random_config();
4971+
config_b.node_config.manually_handle_unknown_bolt11_payments = true;
4972+
let node_b = setup_node(&chain_source, config_b);
4973+
4974+
do_channel_full_cycle(
4975+
node_a,
4976+
node_b,
4977+
&bitcoind.client,
4978+
&electrsd.client,
4979+
false,
4980+
true,
4981+
true,
4982+
false,
4983+
)
4984+
.await;
4985+
4986+
let primary_store = SqliteStore::new(
4987+
primary_dir.into(),
4988+
Some(ldk_node::io::sqlite_store::SQLITE_DB_FILE_NAME.to_string()),
4989+
Some(ldk_node::io::sqlite_store::KV_TABLE_NAME.to_string()),
4990+
)
4991+
.unwrap();
4992+
4993+
let backup_store = SqliteStore::new(
4994+
backup_dir,
4995+
Some(ldk_node::io::sqlite_store::SQLITE_BACKUP_DB_FILE_NAME.to_string()),
4996+
Some(ldk_node::io::sqlite_store::KV_TABLE_NAME.to_string()),
4997+
)
4998+
.unwrap();
4999+
5000+
for (pn, sn, key) in [
5001+
("bdk_wallet", "", "descriptor"),
5002+
("bdk_wallet", "", "change_descriptor"),
5003+
("bdk_wallet", "", "network"),
5004+
("", "", "node_metrics"),
5005+
("", "", "events"),
5006+
("", "", "peers"),
5007+
] {
5008+
let primary = primary_store.read(pn, sn, key).await.unwrap();
5009+
let backup = backup_store.read(pn, sn, key).await.unwrap();
5010+
5011+
assert_eq!(backup, primary, "backup mismatch for {pn}/{sn}/{key}");
5012+
}
5013+
}

0 commit comments

Comments
 (0)