Skip to content

Commit 0094d33

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 configured secondary stores using dedicated SQLite database files. Implement paginated listing through TierStore and update filesystem-backed tests to use FilesystemStoreV2. Add full-cycle integration coverage verifying durable backup mirroring.
1 parent e6548fb commit 0094d33

5 files changed

Lines changed: 306 additions & 9 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: PaginatedKVStore + 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: PaginatedKVStore + 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: 140 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,16 @@
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;
1110
use std::sync::atomic::{AtomicU64, Ordering};
1211
use std::sync::{Arc, Mutex};
1312

1413
use lightning::util::persist::{
15-
KVStore, NETWORK_GRAPH_PERSISTENCE_KEY, NETWORK_GRAPH_PERSISTENCE_PRIMARY_NAMESPACE,
16-
NETWORK_GRAPH_PERSISTENCE_SECONDARY_NAMESPACE, SCORER_PERSISTENCE_KEY,
17-
SCORER_PERSISTENCE_PRIMARY_NAMESPACE,
14+
KVStore, PageToken, PaginatedKVStore, PaginatedListResponse, NETWORK_GRAPH_PERSISTENCE_KEY,
15+
NETWORK_GRAPH_PERSISTENCE_PRIMARY_NAMESPACE, NETWORK_GRAPH_PERSISTENCE_SECONDARY_NAMESPACE,
16+
SCORER_PERSISTENCE_KEY, SCORER_PERSISTENCE_PRIMARY_NAMESPACE,
1817
};
1918
use lightning::{io, log_error};
2019
use tokio::sync::Mutex as TokioMutex;
@@ -163,6 +162,21 @@ impl KVStore for TierStore {
163162
}
164163
}
165164

165+
impl PaginatedKVStore for TierStore {
166+
fn list_paginated(
167+
&self, primary_namespace: &str, secondary_namespace: &str, page_token: Option<PageToken>,
168+
) -> impl Future<Output = Result<PaginatedListResponse, io::Error>> + 'static + Send {
169+
let inner = Arc::clone(&self.inner);
170+
171+
let primary_namespace = primary_namespace.to_string();
172+
let secondary_namespace = secondary_namespace.to_string();
173+
174+
async move {
175+
inner.list_paginated_internal(primary_namespace, secondary_namespace, page_token).await
176+
}
177+
}
178+
}
179+
166180
struct TierStoreInner {
167181
/// The authoritative store for durable data.
168182
primary_store: Arc<DynStore>,
@@ -493,6 +507,47 @@ impl TierStoreInner {
493507
}
494508
}
495509

510+
async fn list_paginated_internal(
511+
&self, primary_namespace: String, secondary_namespace: String,
512+
page_token: Option<PageToken>,
513+
) -> io::Result<PaginatedListResponse> {
514+
check_namespace_key_validity(
515+
primary_namespace.as_str(),
516+
secondary_namespace.as_str(),
517+
None,
518+
"list_paginated",
519+
)?;
520+
521+
match (primary_namespace.as_str(), secondary_namespace.as_str()) {
522+
(
523+
NETWORK_GRAPH_PERSISTENCE_PRIMARY_NAMESPACE,
524+
NETWORK_GRAPH_PERSISTENCE_SECONDARY_NAMESPACE,
525+
)
526+
| (SCORER_PERSISTENCE_PRIMARY_NAMESPACE, _) => {
527+
if let Some(eph_store) = self.ephemeral_store.as_ref() {
528+
// We don't retry ephemeral-store lists here. Local failures are treated as
529+
// terminal for this access path rather than falling back to another store.
530+
return PaginatedKVStore::list_paginated(
531+
eph_store.as_ref(),
532+
&primary_namespace,
533+
&secondary_namespace,
534+
page_token,
535+
)
536+
.await;
537+
}
538+
},
539+
_ => {},
540+
}
541+
542+
PaginatedKVStore::list_paginated(
543+
self.primary_store.as_ref(),
544+
&primary_namespace,
545+
&secondary_namespace,
546+
page_token,
547+
)
548+
.await
549+
}
550+
496551
fn handle_primary_backup_results(
497552
&self, op: &str, primary_namespace: &str, secondary_namespace: &str, key: &str,
498553
primary_res: io::Result<()>, backup_res: io::Result<()>,
@@ -560,6 +615,8 @@ mod tests {
560615
use lightning::util::persist::{
561616
CHANNEL_MANAGER_PERSISTENCE_KEY, CHANNEL_MANAGER_PERSISTENCE_PRIMARY_NAMESPACE,
562617
CHANNEL_MANAGER_PERSISTENCE_SECONDARY_NAMESPACE,
618+
CHANNEL_MONITOR_PERSISTENCE_PRIMARY_NAMESPACE,
619+
CHANNEL_MONITOR_PERSISTENCE_SECONDARY_NAMESPACE,
563620
};
564621
use lightning_persister::fs_store::v2::FilesystemStoreV2;
565622

@@ -670,6 +727,85 @@ mod tests {
670727
assert_eq!(primary_read_cm.unwrap(), data);
671728
}
672729

730+
#[tokio::test]
731+
async fn list_paginated_routes_to_selected_tier() {
732+
let base_dir = random_storage_path();
733+
let log_path = base_dir.join("tier_store_test.log").to_string_lossy().into_owned();
734+
let logger = Arc::new(Logger::new_fs_writer(log_path, Level::Trace).unwrap());
735+
736+
let _cleanup = CleanupDir(base_dir.clone());
737+
738+
let primary_store: Arc<DynStore> =
739+
Arc::new(DynStoreWrapper(FilesystemStoreV2::new(base_dir.join("primary")).unwrap()));
740+
let mut tier = setup_tier_store(Arc::clone(&primary_store), logger);
741+
742+
let ephemeral_store: Arc<DynStore> =
743+
Arc::new(DynStoreWrapper(FilesystemStoreV2::new(base_dir.join("ephemeral")).unwrap()));
744+
tier.set_ephemeral_store(Arc::clone(&ephemeral_store));
745+
746+
tier.write(
747+
CHANNEL_MONITOR_PERSISTENCE_PRIMARY_NAMESPACE,
748+
CHANNEL_MONITOR_PERSISTENCE_SECONDARY_NAMESPACE,
749+
"monitor-key",
750+
vec![1u8; 32],
751+
)
752+
.await
753+
.unwrap();
754+
755+
// This decoy uses the same namespace but the opposite physical store, so it
756+
// would show up if paginated listing routed to the wrong tier.
757+
ephemeral_store
758+
.write(
759+
CHANNEL_MONITOR_PERSISTENCE_PRIMARY_NAMESPACE,
760+
CHANNEL_MONITOR_PERSISTENCE_SECONDARY_NAMESPACE,
761+
"ephemeral-decoy",
762+
vec![2u8; 32],
763+
)
764+
.await
765+
.unwrap();
766+
767+
// Same decoy check in the other direction: this key should be ignored
768+
// because network graph listings route to the ephemeral store when set.
769+
primary_store
770+
.write(
771+
NETWORK_GRAPH_PERSISTENCE_PRIMARY_NAMESPACE,
772+
NETWORK_GRAPH_PERSISTENCE_SECONDARY_NAMESPACE,
773+
"primary-decoy",
774+
vec![3u8; 32],
775+
)
776+
.await
777+
.unwrap();
778+
779+
tier.write(
780+
NETWORK_GRAPH_PERSISTENCE_PRIMARY_NAMESPACE,
781+
NETWORK_GRAPH_PERSISTENCE_SECONDARY_NAMESPACE,
782+
NETWORK_GRAPH_PERSISTENCE_KEY,
783+
vec![4u8; 32],
784+
)
785+
.await
786+
.unwrap();
787+
788+
let primary_response = PaginatedKVStore::list_paginated(
789+
&tier,
790+
CHANNEL_MONITOR_PERSISTENCE_PRIMARY_NAMESPACE,
791+
CHANNEL_MONITOR_PERSISTENCE_SECONDARY_NAMESPACE,
792+
None,
793+
)
794+
.await
795+
.unwrap();
796+
assert_eq!(primary_response.keys, vec!["monitor-key".to_string()]);
797+
798+
let ephemeral_response = PaginatedKVStore::list_paginated(
799+
&tier,
800+
NETWORK_GRAPH_PERSISTENCE_PRIMARY_NAMESPACE,
801+
NETWORK_GRAPH_PERSISTENCE_SECONDARY_NAMESPACE,
802+
None,
803+
)
804+
.await
805+
.unwrap();
806+
assert_eq!(ephemeral_response.keys, vec![NETWORK_GRAPH_PERSISTENCE_KEY.to_string()]);
807+
}
808+
673809
#[tokio::test]
674810
async fn primary_backed_writes_preserve_latest_call_order() {
675811
let base_dir = random_storage_path();

tests/common/mod.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ use lightning::ln::msgs::SocketAddress;
5252
use lightning::routing::gossip::NodeAlias;
5353
use lightning::util::persist::{KVStore, PageToken, PaginatedKVStore, PaginatedListResponse};
5454
use lightning_invoice::{Bolt11InvoiceDescription, Description};
55-
use lightning_persister::fs_store::v1::FilesystemStore;
55+
use lightning_persister::fs_store::v2::FilesystemStoreV2;
5656
use lightning_types::payment::{PaymentHash, PaymentPreimage};
5757
use logging::TestLogWriter;
5858
use rand::distr::Alphanumeric;
@@ -1720,7 +1720,7 @@ impl PaginatedKVStore for TestSyncStore {
17201720
struct TestSyncStoreInner {
17211721
serializer: tokio::sync::RwLock<()>,
17221722
test_store: InMemoryStore,
1723-
fs_store: FilesystemStore,
1723+
fs_store: FilesystemStoreV2,
17241724
sqlite_store: SqliteStore,
17251725
}
17261726

@@ -1729,7 +1729,7 @@ impl TestSyncStoreInner {
17291729
let serializer = tokio::sync::RwLock::new(());
17301730
let mut fs_dir = dest_dir.clone();
17311731
fs_dir.push("fs_store");
1732-
let fs_store = FilesystemStore::new(fs_dir);
1732+
let fs_store = FilesystemStoreV2::new(fs_dir).unwrap();
17331733
let mut sql_dir = dest_dir.clone();
17341734
sql_dir.push("sqlite_store");
17351735
let sqlite_store = SqliteStore::new(

0 commit comments

Comments
 (0)