Skip to content

Commit 34577b6

Browse files
authored
Merge pull request #872 from benthecarman/filesystem-v2
Safely migrate to FileSystemStoreV2
2 parents 53268f2 + e8da5e8 commit 34577b6

4 files changed

Lines changed: 334 additions & 32 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: 263 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,108 @@ 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+
// fsync the renames
661+
fs::File::open(parent_dir)
662+
.and_then(|f| f.sync_all())
663+
.map_err(|_| BuildError::KVStoreSetupFailed)?;
664+
665+
FilesystemStoreV2::new(storage_dir_path).map_err(|_| BuildError::KVStoreSetupFailed)
666+
},
667+
Err(_) => Err(BuildError::KVStoreSetupFailed),
668+
}
669+
}
670+
671+
fn fs_store_sibling_path(storage_dir_path: &Path, file_name: &str) -> PathBuf {
672+
let mut sibling_path = storage_dir_path.to_path_buf();
673+
sibling_path.set_file_name(file_name);
674+
sibling_path
675+
}
676+
677+
fn recover_incomplete_fs_store_migration(storage_dir_path: &Path) -> Result<(), BuildError> {
678+
let v2_dir = fs_store_sibling_path(storage_dir_path, "fs_store_v2_migrating");
679+
let backup_dir = fs_store_sibling_path(storage_dir_path, "fs_store_v1_backup");
680+
681+
if storage_dir_path.exists() {
682+
if v2_dir.exists() {
683+
// The original store is still in place, so a temp migration dir is from a crash before
684+
// the rename step and can be discarded before retrying migration.
685+
fs::remove_dir_all(&v2_dir).map_err(|_| BuildError::KVStoreSetupFailed)?;
686+
}
687+
return Ok(());
688+
}
689+
690+
if backup_dir.exists() {
691+
if v2_dir.exists() {
692+
// Prefer retrying from the v1 backup instead of deciding here whether the temp v2 dir is
693+
// usable. open_or_migrate_fs_store owns the actual v1-to-v2 migration.
694+
fs::remove_dir_all(&v2_dir).map_err(|_| BuildError::KVStoreSetupFailed)?;
695+
}
696+
// The crash happened after moving v1 aside; restore it so normal startup can migrate it.
697+
fs::rename(&backup_dir, storage_dir_path).map_err(|_| BuildError::KVStoreSetupFailed)?;
698+
return Ok(());
699+
}
700+
701+
if v2_dir.exists() {
702+
// There is no v1 backup to retry from. Move the temp dir into place and let
703+
// open_or_migrate_fs_store decide whether it is a valid v2 store.
704+
fs::rename(&v2_dir, storage_dir_path).map_err(|_| BuildError::KVStoreSetupFailed)?;
705+
}
706+
707+
Ok(())
708+
}
709+
622710
#[cfg(test)]
623711
mod tests {
624-
use super::read_or_generate_seed_file;
712+
use std::fs;
713+
use std::path::{Path, PathBuf};
714+
715+
use lightning::util::persist::{migrate_kv_store_data, KVStoreSync};
716+
use lightning_persister::fs_store::v1::FilesystemStore;
717+
use lightning_persister::fs_store::v2::FilesystemStoreV2;
718+
625719
use super::test_utils::random_storage_path;
720+
use super::{open_or_migrate_fs_store, read_or_generate_seed_file};
721+
722+
const TEST_PRIMARY_NAMESPACE: &str = "test_primary_namespace";
723+
const TEST_SECONDARY_NAMESPACE: &str = "test_secondary_namespace";
724+
const TEST_KEY: &str = "test_key";
725+
const TEST_VALUE: &[u8] = b"test_value";
626726

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

tests/common/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,7 @@ pub(crate) enum TestChainSource<'a> {
416416
pub(crate) enum TestStoreType {
417417
TestSyncStore,
418418
Sqlite,
419+
FilesystemStore,
419420
}
420421

421422
impl Default for TestStoreType {
@@ -592,6 +593,9 @@ pub(crate) fn setup_node(chain_source: &TestChainSource, config: TestConfig) ->
592593
builder.build_with_store(config.node_entropy.into(), kv_store).unwrap()
593594
},
594595
TestStoreType::Sqlite => builder.build(config.node_entropy.into()).unwrap(),
596+
TestStoreType::FilesystemStore => {
597+
builder.build_with_fs_store(config.node_entropy.into()).unwrap()
598+
},
595599
};
596600

597601
if config.recovery_mode {

0 commit comments

Comments
 (0)