Skip to content

Commit 7d30381

Browse files
committed
Safely migrate to FileSystemStoreV2
Before moving to PaginatedKVStore everywhere we need to use FileSystemStoreV2 instead of FileSystemStoreV1. This will safely migrate over to it on first start up. Also adds a test to make sure we handle it properly.
1 parent 62e43fb commit 7d30381

3 files changed

Lines changed: 312 additions & 19 deletions

File tree

src/builder.rs

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@ use lightning::util::persist::{
4343
use lightning::util::ser::ReadableArgs;
4444
use lightning::util::sweep::OutputSweeper;
4545
use lightning_dns_resolver::OMDomainResolver;
46-
use lightning_persister::fs_store::v1::FilesystemStore;
4746
use vss_client::headers::VssHeaderProvider;
4847

4948
use crate::chain::ChainSource;
@@ -59,8 +58,9 @@ use crate::fee_estimator::OnchainFeeEstimator;
5958
use crate::gossip::GossipSource;
6059
use crate::io::sqlite_store::SqliteStore;
6160
use crate::io::utils::{
62-
read_all_objects, read_event_queue, read_external_pathfinding_scores_from_cache,
63-
read_network_graph, read_node_metrics, read_output_sweeper, read_peer_info, read_scorer,
61+
open_or_migrate_fs_store, read_all_objects, read_event_queue,
62+
read_external_pathfinding_scores_from_cache, read_network_graph, read_node_metrics,
63+
read_output_sweeper, read_peer_info, read_scorer,
6464
};
6565
use crate::io::vss_store::VssStoreBuilder;
6666
use crate::io::{
@@ -644,18 +644,19 @@ impl NodeBuilder {
644644
self.build_with_store_and_logger(node_entropy, kv_store, logger)
645645
}
646646

647-
/// Builds a [`Node`] instance with a [`FilesystemStore`] backend and according to the options
647+
/// Builds a [`Node`] instance with a [`FilesystemStoreV2`] backend and according to the options
648648
/// previously configured.
649+
///
650+
/// If the storage directory contains data from a v1 filesystem store, it will be
651+
/// automatically migrated to the v2 format.
652+
///
653+
/// [`FilesystemStoreV2`]: lightning_persister::fs_store::v2::FilesystemStoreV2
649654
pub fn build_with_fs_store(&self, node_entropy: NodeEntropy) -> Result<Node, BuildError> {
650655
let logger = setup_logger(&self.log_writer_config, &self.config)?;
651656
let mut storage_dir_path: PathBuf = self.config.storage_dir_path.clone().into();
652657
storage_dir_path.push("fs_store");
653658

654-
fs::create_dir_all(storage_dir_path.clone()).map_err(|e| {
655-
log_error!(logger, "Failed to setup Filesystem store: {}", e);
656-
BuildError::StoragePathAccessFailed
657-
})?;
658-
let kv_store = FilesystemStore::new(storage_dir_path);
659+
let kv_store = open_or_migrate_fs_store(storage_dir_path)?;
659660
self.build_with_store_and_logger(node_entropy, kv_store, logger)
660661
}
661662

@@ -1115,7 +1116,7 @@ impl ArcedNodeBuilder {
11151116
self.inner.read().expect("lock").build(*node_entropy).map(Arc::new)
11161117
}
11171118

1118-
/// Builds a [`Node`] instance with a [`FilesystemStore`] backend and according to the options
1119+
/// Builds a [`Node`] instance with a [`FilesystemStoreV2`] backend and according to the options
11191120
/// previously configured.
11201121
pub fn build_with_fs_store(
11211122
&self, node_entropy: Arc<NodeEntropy>,

src/io/utils.rs

Lines changed: 258 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use std::io::Write;
1010
use std::ops::Deref;
1111
#[cfg(unix)]
1212
use std::os::unix::fs::OpenOptionsExt;
13-
use std::path::Path;
13+
use std::path::{Path, PathBuf};
1414
use std::sync::{Arc, RwLock};
1515

1616
use bdk_chain::indexer::keychain_txout::ChangeSet as BdkIndexerChangeSet;
@@ -26,14 +26,16 @@ use lightning::routing::scoring::{
2626
ChannelLiquidities, ProbabilisticScorer, ProbabilisticScoringDecayParameters,
2727
};
2828
use lightning::util::persist::{
29-
KVStore, KVStoreSync, KVSTORE_NAMESPACE_KEY_ALPHABET, KVSTORE_NAMESPACE_KEY_MAX_LEN,
30-
NETWORK_GRAPH_PERSISTENCE_KEY, NETWORK_GRAPH_PERSISTENCE_PRIMARY_NAMESPACE,
31-
NETWORK_GRAPH_PERSISTENCE_SECONDARY_NAMESPACE, OUTPUT_SWEEPER_PERSISTENCE_KEY,
32-
OUTPUT_SWEEPER_PERSISTENCE_PRIMARY_NAMESPACE, OUTPUT_SWEEPER_PERSISTENCE_SECONDARY_NAMESPACE,
33-
SCORER_PERSISTENCE_KEY, SCORER_PERSISTENCE_PRIMARY_NAMESPACE,
34-
SCORER_PERSISTENCE_SECONDARY_NAMESPACE,
29+
migrate_kv_store_data, KVStore, KVStoreSync, KVSTORE_NAMESPACE_KEY_ALPHABET,
30+
KVSTORE_NAMESPACE_KEY_MAX_LEN, NETWORK_GRAPH_PERSISTENCE_KEY,
31+
NETWORK_GRAPH_PERSISTENCE_PRIMARY_NAMESPACE, NETWORK_GRAPH_PERSISTENCE_SECONDARY_NAMESPACE,
32+
OUTPUT_SWEEPER_PERSISTENCE_KEY, OUTPUT_SWEEPER_PERSISTENCE_PRIMARY_NAMESPACE,
33+
OUTPUT_SWEEPER_PERSISTENCE_SECONDARY_NAMESPACE, SCORER_PERSISTENCE_KEY,
34+
SCORER_PERSISTENCE_PRIMARY_NAMESPACE, SCORER_PERSISTENCE_SECONDARY_NAMESPACE,
3535
};
3636
use lightning::util::ser::{Readable, ReadableArgs, Writeable};
37+
use lightning_persister::fs_store::v1::FilesystemStore;
38+
use lightning_persister::fs_store::v2::{FilesystemStoreV2, FilesystemStoreV2Error};
3739
use lightning_types::string::PrintableString;
3840

3941
use super::*;
@@ -47,7 +49,7 @@ use crate::logger::{log_error, LdkLogger, Logger};
4749
use crate::peer_store::PeerStore;
4850
use crate::types::{Broadcaster, DynStore, KeysManager, Sweeper};
4951
use crate::wallet::ser::{ChangeSetDeserWrapper, ChangeSetSerWrapper};
50-
use crate::{Error, EventQueue, NodeMetrics};
52+
use crate::{BuildError, Error, EventQueue, NodeMetrics};
5153

5254
pub const EXTERNAL_PATHFINDING_SCORES_CACHE_KEY: &str = "external_pathfinding_scores_cache";
5355

@@ -619,10 +621,103 @@ pub(crate) fn read_bdk_wallet_change_set(
619621
Ok(Some(change_set))
620622
}
621623

624+
/// Opens a [`FilesystemStoreV2`], automatically migrating from v1 format if necessary.
625+
///
626+
/// If the directory contains v1 data (files at the top level), the data is migrated to v2 format
627+
/// in a temporary directory, the original is renamed to `fs_store_v1_backup`, and the migrated
628+
/// directory is moved into place.
629+
pub(crate) fn open_or_migrate_fs_store(
630+
storage_dir_path: PathBuf,
631+
) -> Result<FilesystemStoreV2, BuildError> {
632+
let parent_dir = storage_dir_path.parent().ok_or(BuildError::StoragePathAccessFailed)?;
633+
fs::create_dir_all(parent_dir).map_err(|_| BuildError::StoragePathAccessFailed)?;
634+
recover_incomplete_fs_store_migration(&storage_dir_path)?;
635+
if !storage_dir_path.exists() {
636+
fs::create_dir_all(storage_dir_path.clone())
637+
.map_err(|_| BuildError::StoragePathAccessFailed)?;
638+
}
639+
640+
match FilesystemStoreV2::new(storage_dir_path.clone()) {
641+
Ok(store) => Ok(store),
642+
Err(FilesystemStoreV2Error::V1DataDetected(_)) => {
643+
// The directory contains v1 data, migrate to v2.
644+
let mut v1_store = FilesystemStore::new(storage_dir_path.clone());
645+
646+
let v2_dir = fs_store_sibling_path(&storage_dir_path, "fs_store_v2_migrating");
647+
fs::create_dir_all(v2_dir.clone()).map_err(|_| BuildError::StoragePathAccessFailed)?;
648+
let mut v2_store = FilesystemStoreV2::new(v2_dir.clone())
649+
.map_err(|_| BuildError::KVStoreSetupFailed)?;
650+
651+
migrate_kv_store_data(&mut v1_store, &mut v2_store)
652+
.map_err(|_| BuildError::KVStoreSetupFailed)?;
653+
654+
// Swap directories: rename v1 out of the way, move v2 into place.
655+
let backup_dir = fs_store_sibling_path(&storage_dir_path, "fs_store_v1_backup");
656+
fs::rename(&storage_dir_path, &backup_dir)
657+
.map_err(|_| BuildError::KVStoreSetupFailed)?;
658+
fs::rename(&v2_dir, &storage_dir_path).map_err(|_| BuildError::KVStoreSetupFailed)?;
659+
660+
FilesystemStoreV2::new(storage_dir_path).map_err(|_| BuildError::KVStoreSetupFailed)
661+
},
662+
Err(_) => Err(BuildError::KVStoreSetupFailed),
663+
}
664+
}
665+
666+
fn fs_store_sibling_path(storage_dir_path: &Path, file_name: &str) -> PathBuf {
667+
let mut sibling_path = storage_dir_path.to_path_buf();
668+
sibling_path.set_file_name(file_name);
669+
sibling_path
670+
}
671+
672+
fn recover_incomplete_fs_store_migration(storage_dir_path: &Path) -> Result<(), BuildError> {
673+
let v2_dir = fs_store_sibling_path(storage_dir_path, "fs_store_v2_migrating");
674+
let backup_dir = fs_store_sibling_path(storage_dir_path, "fs_store_v1_backup");
675+
676+
if storage_dir_path.exists() {
677+
if v2_dir.exists() {
678+
// The original store is still in place, so a temp migration dir is from a crash before
679+
// the rename step and can be discarded before retrying migration.
680+
fs::remove_dir_all(&v2_dir).map_err(|_| BuildError::KVStoreSetupFailed)?;
681+
}
682+
return Ok(());
683+
}
684+
685+
if backup_dir.exists() {
686+
if v2_dir.exists() {
687+
// Prefer retrying from the v1 backup instead of deciding here whether the temp v2 dir is
688+
// usable. open_or_migrate_fs_store owns the actual v1-to-v2 migration.
689+
fs::remove_dir_all(&v2_dir).map_err(|_| BuildError::KVStoreSetupFailed)?;
690+
}
691+
// The crash happened after moving v1 aside; restore it so normal startup can migrate it.
692+
fs::rename(&backup_dir, storage_dir_path).map_err(|_| BuildError::KVStoreSetupFailed)?;
693+
return Ok(());
694+
}
695+
696+
if v2_dir.exists() {
697+
// There is no v1 backup to retry from. Move the temp dir into place and let
698+
// open_or_migrate_fs_store decide whether it is a valid v2 store.
699+
fs::rename(&v2_dir, storage_dir_path).map_err(|_| BuildError::KVStoreSetupFailed)?;
700+
}
701+
702+
Ok(())
703+
}
704+
622705
#[cfg(test)]
623706
mod tests {
624-
use super::read_or_generate_seed_file;
707+
use std::fs;
708+
use std::path::{Path, PathBuf};
709+
710+
use lightning::util::persist::{migrate_kv_store_data, KVStoreSync};
711+
use lightning_persister::fs_store::v1::FilesystemStore;
712+
use lightning_persister::fs_store::v2::FilesystemStoreV2;
713+
625714
use super::test_utils::random_storage_path;
715+
use super::{open_or_migrate_fs_store, read_or_generate_seed_file};
716+
717+
const TEST_PRIMARY_NAMESPACE: &str = "test_primary_namespace";
718+
const TEST_SECONDARY_NAMESPACE: &str = "test_secondary_namespace";
719+
const TEST_KEY: &str = "test_key";
720+
const TEST_VALUE: &[u8] = b"test_value";
626721

627722
#[test]
628723
fn generated_seed_is_readable() {
@@ -632,4 +727,158 @@ mod tests {
632727
let read_seed_bytes = read_or_generate_seed_file(&rand_path.to_str().unwrap()).unwrap();
633728
assert_eq!(expected_seed_bytes, read_seed_bytes);
634729
}
730+
731+
#[test]
732+
fn fs_store_migration_recovers_before_v1_backup_rename() {
733+
let fs_store_path = fs_store_path();
734+
let mut v1_store = write_v1_test_data(&fs_store_path);
735+
let v2_migrating_path = sibling_path(&fs_store_path, "fs_store_v2_migrating");
736+
let mut v2_store = FilesystemStoreV2::new(v2_migrating_path.clone()).unwrap();
737+
migrate_kv_store_data(&mut v1_store, &mut v2_store).unwrap();
738+
739+
let migrated_store = open_or_migrate_fs_store(fs_store_path.clone()).unwrap();
740+
assert_eq!(
741+
KVStoreSync::read(
742+
&migrated_store,
743+
TEST_PRIMARY_NAMESPACE,
744+
TEST_SECONDARY_NAMESPACE,
745+
TEST_KEY
746+
)
747+
.unwrap(),
748+
TEST_VALUE
749+
);
750+
assert!(fs_store_path.exists());
751+
assert!(!v2_migrating_path.exists());
752+
}
753+
754+
#[test]
755+
fn fs_store_migration_recovers_after_v1_backup_rename() {
756+
let fs_store_path = fs_store_path();
757+
let mut v1_store = write_v1_test_data(&fs_store_path);
758+
let v2_migrating_path = sibling_path(&fs_store_path, "fs_store_v2_migrating");
759+
let mut v2_store = FilesystemStoreV2::new(v2_migrating_path.clone()).unwrap();
760+
migrate_kv_store_data(&mut v1_store, &mut v2_store).unwrap();
761+
762+
let backup_path = sibling_path(&fs_store_path, "fs_store_v1_backup");
763+
fs::rename(&fs_store_path, backup_path).unwrap();
764+
765+
let migrated_store = open_or_migrate_fs_store(fs_store_path.clone()).unwrap();
766+
assert_eq!(
767+
KVStoreSync::read(
768+
&migrated_store,
769+
TEST_PRIMARY_NAMESPACE,
770+
TEST_SECONDARY_NAMESPACE,
771+
TEST_KEY
772+
)
773+
.unwrap(),
774+
TEST_VALUE
775+
);
776+
assert!(fs_store_path.exists());
777+
assert!(!v2_migrating_path.exists());
778+
}
779+
780+
#[test]
781+
fn fs_store_migration_recovers_after_v2_rename() {
782+
let fs_store_path = fs_store_path();
783+
let mut v1_store = write_v1_test_data(&fs_store_path);
784+
let v2_migrating_path = sibling_path(&fs_store_path, "fs_store_v2_migrating");
785+
let mut v2_store = FilesystemStoreV2::new(v2_migrating_path.clone()).unwrap();
786+
migrate_kv_store_data(&mut v1_store, &mut v2_store).unwrap();
787+
788+
let backup_path = sibling_path(&fs_store_path, "fs_store_v1_backup");
789+
fs::rename(&fs_store_path, &backup_path).unwrap();
790+
fs::rename(&v2_migrating_path, &fs_store_path).unwrap();
791+
792+
let migrated_store = open_or_migrate_fs_store(fs_store_path.clone()).unwrap();
793+
assert_eq!(
794+
KVStoreSync::read(
795+
&migrated_store,
796+
TEST_PRIMARY_NAMESPACE,
797+
TEST_SECONDARY_NAMESPACE,
798+
TEST_KEY
799+
)
800+
.unwrap(),
801+
TEST_VALUE
802+
);
803+
assert!(fs_store_path.exists());
804+
assert!(backup_path.exists());
805+
assert!(!v2_migrating_path.exists());
806+
}
807+
808+
#[test]
809+
fn fs_store_migration_recovers_backup_without_migrating_dir() {
810+
let fs_store_path = fs_store_path();
811+
write_v1_test_data(&fs_store_path);
812+
813+
let backup_path = sibling_path(&fs_store_path, "fs_store_v1_backup");
814+
fs::rename(&fs_store_path, backup_path).unwrap();
815+
816+
let migrated_store = open_or_migrate_fs_store(fs_store_path.clone()).unwrap();
817+
assert_eq!(
818+
KVStoreSync::read(
819+
&migrated_store,
820+
TEST_PRIMARY_NAMESPACE,
821+
TEST_SECONDARY_NAMESPACE,
822+
TEST_KEY
823+
)
824+
.unwrap(),
825+
TEST_VALUE
826+
);
827+
assert!(fs_store_path.exists());
828+
assert!(!sibling_path(&fs_store_path, "fs_store_v1_backup").exists());
829+
}
830+
831+
#[test]
832+
fn fs_store_migration_recovers_unexpected_migrating_dir_without_backup() {
833+
let fs_store_path = fs_store_path();
834+
let v2_migrating_path = sibling_path(&fs_store_path, "fs_store_v2_migrating");
835+
let v2_store = FilesystemStoreV2::new(v2_migrating_path.clone()).unwrap();
836+
KVStoreSync::write(
837+
&v2_store,
838+
TEST_PRIMARY_NAMESPACE,
839+
TEST_SECONDARY_NAMESPACE,
840+
TEST_KEY,
841+
TEST_VALUE.to_vec(),
842+
)
843+
.unwrap();
844+
845+
let migrated_store = open_or_migrate_fs_store(fs_store_path.clone()).unwrap();
846+
assert_eq!(
847+
KVStoreSync::read(
848+
&migrated_store,
849+
TEST_PRIMARY_NAMESPACE,
850+
TEST_SECONDARY_NAMESPACE,
851+
TEST_KEY
852+
)
853+
.unwrap(),
854+
TEST_VALUE
855+
);
856+
assert!(fs_store_path.exists());
857+
assert!(!v2_migrating_path.exists());
858+
}
859+
860+
fn fs_store_path() -> PathBuf {
861+
let mut fs_store_path = random_storage_path();
862+
fs_store_path.push("fs_store");
863+
fs_store_path
864+
}
865+
866+
fn sibling_path(path: &Path, file_name: &str) -> PathBuf {
867+
let mut sibling_path = path.to_path_buf();
868+
sibling_path.set_file_name(file_name);
869+
sibling_path
870+
}
871+
872+
fn write_v1_test_data(fs_store_path: &Path) -> FilesystemStore {
873+
let v1_store = FilesystemStore::new(fs_store_path.to_path_buf());
874+
KVStoreSync::write(
875+
&v1_store,
876+
TEST_PRIMARY_NAMESPACE,
877+
TEST_SECONDARY_NAMESPACE,
878+
TEST_KEY,
879+
TEST_VALUE.to_vec(),
880+
)
881+
.unwrap();
882+
v1_store
883+
}
635884
}

0 commit comments

Comments
 (0)