Skip to content

Commit dcc531e

Browse files
authored
Merge pull request #3481 from tnull/2024-12-add-kvstore-migration-ext
Add `MigratableKVStore` trait
2 parents 6ad40f9 + b0af39f commit dcc531e

File tree

3 files changed

+235
-59
lines changed

3 files changed

+235
-59
lines changed

lightning-persister/src/fs_store.rs

Lines changed: 162 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
//! Objects related to [`FilesystemStore`] live here.
22
use crate::utils::{check_namespace_key_validity, is_valid_kvstore_str};
33

4-
use lightning::util::persist::KVStore;
4+
use lightning::util::persist::{KVStore, MigratableKVStore};
55
use lightning::util::string::PrintableString;
66

77
use std::collections::HashMap;
@@ -316,96 +316,187 @@ impl KVStore for FilesystemStore {
316316
let entry = entry?;
317317
let p = entry.path();
318318

319-
if let Some(ext) = p.extension() {
320-
#[cfg(target_os = "windows")]
321-
{
322-
// Clean up any trash files lying around.
323-
if ext == "trash" {
324-
fs::remove_file(p).ok();
325-
continue;
326-
}
327-
}
328-
if ext == "tmp" {
329-
continue;
330-
}
319+
if !dir_entry_is_key(&p)? {
320+
continue;
331321
}
332322

333-
let metadata = p.metadata()?;
323+
let key = get_key_from_dir_entry(&p, &prefixed_dest)?;
334324

335-
// We allow the presence of directories in the empty primary namespace and just skip them.
336-
if metadata.is_dir() {
337-
continue;
325+
keys.push(key);
326+
}
327+
328+
self.garbage_collect_locks();
329+
330+
Ok(keys)
331+
}
332+
}
333+
334+
fn dir_entry_is_key(p: &Path) -> Result<bool, lightning::io::Error> {
335+
if let Some(ext) = p.extension() {
336+
#[cfg(target_os = "windows")]
337+
{
338+
// Clean up any trash files lying around.
339+
if ext == "trash" {
340+
fs::remove_file(p).ok();
341+
return Ok(false);
338342
}
343+
}
344+
if ext == "tmp" {
345+
return Ok(false);
346+
}
347+
}
339348

340-
// If we otherwise don't find a file at the given path something went wrong.
341-
if !metadata.is_file() {
349+
let metadata = p.metadata().map_err(|e| {
350+
let msg = format!(
351+
"Failed to list keys at path {}: {}",
352+
PrintableString(p.to_str().unwrap_or_default()),
353+
e
354+
);
355+
lightning::io::Error::new(lightning::io::ErrorKind::Other, msg)
356+
})?;
357+
358+
// We allow the presence of directories in the empty primary namespace and just skip them.
359+
if metadata.is_dir() {
360+
return Ok(false);
361+
}
362+
363+
// If we otherwise don't find a file at the given path something went wrong.
364+
if !metadata.is_file() {
365+
debug_assert!(
366+
false,
367+
"Failed to list keys at path {}: file couldn't be accessed.",
368+
PrintableString(p.to_str().unwrap_or_default())
369+
);
370+
let msg = format!(
371+
"Failed to list keys at path {}: file couldn't be accessed.",
372+
PrintableString(p.to_str().unwrap_or_default())
373+
);
374+
return Err(lightning::io::Error::new(lightning::io::ErrorKind::Other, msg));
375+
}
376+
377+
Ok(true)
378+
}
379+
380+
fn get_key_from_dir_entry(p: &Path, base_path: &Path) -> Result<String, lightning::io::Error> {
381+
match p.strip_prefix(&base_path) {
382+
Ok(stripped_path) => {
383+
if let Some(relative_path) = stripped_path.to_str() {
384+
if is_valid_kvstore_str(relative_path) {
385+
return Ok(relative_path.to_string());
386+
} else {
387+
debug_assert!(
388+
false,
389+
"Failed to list keys of path {}: file path is not valid key",
390+
PrintableString(p.to_str().unwrap_or_default())
391+
);
392+
let msg = format!(
393+
"Failed to list keys of path {}: file path is not valid key",
394+
PrintableString(p.to_str().unwrap_or_default())
395+
);
396+
return Err(lightning::io::Error::new(lightning::io::ErrorKind::Other, msg));
397+
}
398+
} else {
342399
debug_assert!(
343400
false,
344-
"Failed to list keys of {}/{}: file couldn't be accessed.",
345-
PrintableString(primary_namespace),
346-
PrintableString(secondary_namespace)
401+
"Failed to list keys of path {}: file path is not valid UTF-8",
402+
PrintableString(p.to_str().unwrap_or_default())
347403
);
348404
let msg = format!(
349-
"Failed to list keys of {}/{}: file couldn't be accessed.",
350-
PrintableString(primary_namespace),
351-
PrintableString(secondary_namespace)
405+
"Failed to list keys of path {}: file path is not valid UTF-8",
406+
PrintableString(p.to_str().unwrap_or_default())
352407
);
353408
return Err(lightning::io::Error::new(lightning::io::ErrorKind::Other, msg));
354409
}
410+
},
411+
Err(e) => {
412+
debug_assert!(
413+
false,
414+
"Failed to list keys of path {}: {}",
415+
PrintableString(p.to_str().unwrap_or_default()),
416+
e
417+
);
418+
let msg = format!(
419+
"Failed to list keys of path {}: {}",
420+
PrintableString(p.to_str().unwrap_or_default()),
421+
e
422+
);
423+
return Err(lightning::io::Error::new(lightning::io::ErrorKind::Other, msg));
424+
},
425+
}
426+
}
427+
428+
impl MigratableKVStore for FilesystemStore {
429+
fn list_all_keys(&self) -> Result<Vec<(String, String, String)>, lightning::io::Error> {
430+
let prefixed_dest = &self.data_dir;
431+
if !prefixed_dest.exists() {
432+
return Ok(Vec::new());
433+
}
434+
435+
let mut keys = Vec::new();
436+
437+
'primary_loop: for primary_entry in fs::read_dir(prefixed_dest)? {
438+
let primary_path = primary_entry?.path();
439+
440+
if dir_entry_is_key(&primary_path)? {
441+
let primary_namespace = String::new();
442+
let secondary_namespace = String::new();
443+
let key = get_key_from_dir_entry(&primary_path, prefixed_dest)?;
444+
keys.push((primary_namespace, secondary_namespace, key));
445+
continue 'primary_loop;
446+
}
447+
448+
// The primary_entry is actually also a directory.
449+
'secondary_loop: for secondary_entry in fs::read_dir(&primary_path)? {
450+
let secondary_path = secondary_entry?.path();
451+
452+
if dir_entry_is_key(&secondary_path)? {
453+
let primary_namespace = get_key_from_dir_entry(&primary_path, prefixed_dest)?;
454+
let secondary_namespace = String::new();
455+
let key = get_key_from_dir_entry(&secondary_path, &primary_path)?;
456+
keys.push((primary_namespace, secondary_namespace, key));
457+
continue 'secondary_loop;
458+
}
355459

356-
match p.strip_prefix(&prefixed_dest) {
357-
Ok(stripped_path) => {
358-
if let Some(relative_path) = stripped_path.to_str() {
359-
if is_valid_kvstore_str(relative_path) {
360-
keys.push(relative_path.to_string())
361-
}
460+
// The secondary_entry is actually also a directory.
461+
for tertiary_entry in fs::read_dir(&secondary_path)? {
462+
let tertiary_entry = tertiary_entry?;
463+
let tertiary_path = tertiary_entry.path();
464+
465+
if dir_entry_is_key(&tertiary_path)? {
466+
let primary_namespace =
467+
get_key_from_dir_entry(&primary_path, prefixed_dest)?;
468+
let secondary_namespace =
469+
get_key_from_dir_entry(&secondary_path, &primary_path)?;
470+
let key = get_key_from_dir_entry(&tertiary_path, &secondary_path)?;
471+
keys.push((primary_namespace, secondary_namespace, key));
362472
} else {
363473
debug_assert!(
364474
false,
365-
"Failed to list keys of {}/{}: file path is not valid UTF-8",
366-
PrintableString(primary_namespace),
367-
PrintableString(secondary_namespace)
475+
"Failed to list keys of path {}: only two levels of namespaces are supported",
476+
PrintableString(tertiary_path.to_str().unwrap_or_default())
368477
);
369478
let msg = format!(
370-
"Failed to list keys of {}/{}: file path is not valid UTF-8",
371-
PrintableString(primary_namespace),
372-
PrintableString(secondary_namespace)
479+
"Failed to list keys of path {}: only two levels of namespaces are supported",
480+
PrintableString(tertiary_path.to_str().unwrap_or_default())
373481
);
374482
return Err(lightning::io::Error::new(
375483
lightning::io::ErrorKind::Other,
376484
msg,
377485
));
378486
}
379-
},
380-
Err(e) => {
381-
debug_assert!(
382-
false,
383-
"Failed to list keys of {}/{}: {}",
384-
PrintableString(primary_namespace),
385-
PrintableString(secondary_namespace),
386-
e
387-
);
388-
let msg = format!(
389-
"Failed to list keys of {}/{}: {}",
390-
PrintableString(primary_namespace),
391-
PrintableString(secondary_namespace),
392-
e
393-
);
394-
return Err(lightning::io::Error::new(lightning::io::ErrorKind::Other, msg));
395-
},
487+
}
396488
}
397489
}
398-
399-
self.garbage_collect_locks();
400-
401490
Ok(keys)
402491
}
403492
}
404493

405494
#[cfg(test)]
406495
mod tests {
407496
use super::*;
408-
use crate::test_utils::{do_read_write_remove_list_persist, do_test_store};
497+
use crate::test_utils::{
498+
do_read_write_remove_list_persist, do_test_data_migration, do_test_store,
499+
};
409500

410501
use bitcoin::Txid;
411502

@@ -438,6 +529,19 @@ mod tests {
438529
do_read_write_remove_list_persist(&fs_store);
439530
}
440531

532+
#[test]
533+
fn test_data_migration() {
534+
let mut source_temp_path = std::env::temp_dir();
535+
source_temp_path.push("test_data_migration_source");
536+
let mut source_store = FilesystemStore::new(source_temp_path);
537+
538+
let mut target_temp_path = std::env::temp_dir();
539+
target_temp_path.push("test_data_migration_target");
540+
let mut target_store = FilesystemStore::new(target_temp_path);
541+
542+
do_test_data_migration(&mut source_store, &mut target_store);
543+
}
544+
441545
#[test]
442546
fn test_if_monitors_is_not_dir() {
443547
let store = FilesystemStore::new("test_monitors_is_not_dir".into());

lightning-persister/src/test_utils.rs

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@ use lightning::ln::functional_test_utils::{
33
connect_block, create_announced_chan_between_nodes, create_chanmon_cfgs, create_dummy_block,
44
create_network, create_node_cfgs, create_node_chanmgrs, send_payment,
55
};
6-
use lightning::util::persist::{read_channel_monitors, KVStore, KVSTORE_NAMESPACE_KEY_MAX_LEN};
6+
use lightning::util::persist::{
7+
migrate_kv_store_data, read_channel_monitors, KVStore, MigratableKVStore,
8+
KVSTORE_NAMESPACE_KEY_ALPHABET, KVSTORE_NAMESPACE_KEY_MAX_LEN,
9+
};
710
use lightning::util::test_utils;
811
use lightning::{check_added_monitors, check_closed_broadcast, check_closed_event};
912

@@ -59,6 +62,41 @@ pub(crate) fn do_read_write_remove_list_persist<K: KVStore + RefUnwindSafe>(kv_s
5962
assert_eq!(listed_keys.len(), 0);
6063
}
6164

65+
pub(crate) fn do_test_data_migration<S: MigratableKVStore, T: MigratableKVStore>(
66+
source_store: &mut S, target_store: &mut T,
67+
) {
68+
// We fill the source with some bogus keys.
69+
let dummy_data = [42u8; 32];
70+
let num_primary_namespaces = 2;
71+
let num_secondary_namespaces = 2;
72+
let num_keys = 3;
73+
for i in 0..num_primary_namespaces {
74+
let primary_namespace =
75+
format!("testspace{}", KVSTORE_NAMESPACE_KEY_ALPHABET.chars().nth(i).unwrap());
76+
for j in 0..num_secondary_namespaces {
77+
let secondary_namespace =
78+
format!("testsubspace{}", KVSTORE_NAMESPACE_KEY_ALPHABET.chars().nth(j).unwrap());
79+
for k in 0..num_keys {
80+
let key =
81+
format!("testkey{}", KVSTORE_NAMESPACE_KEY_ALPHABET.chars().nth(k).unwrap());
82+
source_store
83+
.write(&primary_namespace, &secondary_namespace, &key, &dummy_data)
84+
.unwrap();
85+
}
86+
}
87+
}
88+
let total_num_entries = num_primary_namespaces * num_secondary_namespaces * num_keys;
89+
let all_keys = source_store.list_all_keys().unwrap();
90+
assert_eq!(all_keys.len(), total_num_entries);
91+
92+
migrate_kv_store_data(source_store, target_store).unwrap();
93+
94+
assert_eq!(target_store.list_all_keys().unwrap().len(), total_num_entries);
95+
for (p, s, k) in &all_keys {
96+
assert_eq!(target_store.read(p, s, k).unwrap(), dummy_data);
97+
}
98+
}
99+
62100
// Integration-test the given KVStore implementation. Test relaying a few payments and check that
63101
// the persisted data is updated the appropriate number of times.
64102
pub(crate) fn do_test_store<K: KVStore>(store_0: &K, store_1: &K) {

lightning/src/util/persist.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,40 @@ pub trait KVStore {
163163
) -> Result<Vec<String>, io::Error>;
164164
}
165165

166+
/// Provides additional interface methods that are required for [`KVStore`]-to-[`KVStore`]
167+
/// data migration.
168+
pub trait MigratableKVStore: KVStore {
169+
/// Returns *all* known keys as a list of `primary_namespace`, `secondary_namespace`, `key` tuples.
170+
///
171+
/// This is useful for migrating data from [`KVStore`] implementation to [`KVStore`]
172+
/// implementation.
173+
///
174+
/// Must exhaustively return all entries known to the store to ensure no data is missed, but
175+
/// may return the items in arbitrary order.
176+
fn list_all_keys(&self) -> Result<Vec<(String, String, String)>, io::Error>;
177+
}
178+
179+
/// Migrates all data from one store to another.
180+
///
181+
/// This operation assumes that `target_store` is empty, i.e., any data present under copied keys
182+
/// might get overriden. User must ensure `source_store` is not modified during operation,
183+
/// otherwise no consistency guarantees can be given.
184+
///
185+
/// Will abort and return an error if any IO operation fails. Note that in this case the
186+
/// `target_store` might get left in an intermediate state.
187+
pub fn migrate_kv_store_data<S: MigratableKVStore, T: MigratableKVStore>(
188+
source_store: &mut S, target_store: &mut T,
189+
) -> Result<(), io::Error> {
190+
let keys_to_migrate = source_store.list_all_keys()?;
191+
192+
for (primary_namespace, secondary_namespace, key) in &keys_to_migrate {
193+
let data = source_store.read(primary_namespace, secondary_namespace, key)?;
194+
target_store.write(primary_namespace, secondary_namespace, key, &data)?;
195+
}
196+
197+
Ok(())
198+
}
199+
166200
/// Trait that handles persisting a [`ChannelManager`], [`NetworkGraph`], and [`WriteableScore`] to disk.
167201
///
168202
/// [`ChannelManager`]: crate::ln::channelmanager::ChannelManager

0 commit comments

Comments
 (0)