-
Notifications
You must be signed in to change notification settings - Fork 417
Async FilesystemStore #3931
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Async FilesystemStore #3931
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,120 @@ | ||
// This file is Copyright its original authors, visible in version control | ||
// history. | ||
// | ||
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE | ||
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license | ||
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option. | ||
// You may not use this file except in accordance with one or both of these | ||
// licenses. | ||
|
||
// This file is auto-generated by gen_target.sh based on target_template.txt | ||
// To modify it, modify target_template.txt and run gen_target.sh instead. | ||
|
||
#![cfg_attr(feature = "libfuzzer_fuzz", no_main)] | ||
#![cfg_attr(rustfmt, rustfmt_skip)] | ||
|
||
#[cfg(not(fuzzing))] | ||
compile_error!("Fuzz targets need cfg=fuzzing"); | ||
|
||
#[cfg(not(hashes_fuzz))] | ||
compile_error!("Fuzz targets need cfg=hashes_fuzz"); | ||
|
||
#[cfg(not(secp256k1_fuzz))] | ||
compile_error!("Fuzz targets need cfg=secp256k1_fuzz"); | ||
|
||
extern crate lightning_fuzz; | ||
use lightning_fuzz::fs_store::*; | ||
|
||
#[cfg(feature = "afl")] | ||
#[macro_use] extern crate afl; | ||
#[cfg(feature = "afl")] | ||
fn main() { | ||
fuzz!(|data| { | ||
fs_store_run(data.as_ptr(), data.len()); | ||
}); | ||
} | ||
|
||
#[cfg(feature = "honggfuzz")] | ||
#[macro_use] extern crate honggfuzz; | ||
#[cfg(feature = "honggfuzz")] | ||
fn main() { | ||
loop { | ||
fuzz!(|data| { | ||
fs_store_run(data.as_ptr(), data.len()); | ||
}); | ||
} | ||
} | ||
|
||
#[cfg(feature = "libfuzzer_fuzz")] | ||
#[macro_use] extern crate libfuzzer_sys; | ||
#[cfg(feature = "libfuzzer_fuzz")] | ||
fuzz_target!(|data: &[u8]| { | ||
fs_store_run(data.as_ptr(), data.len()); | ||
}); | ||
|
||
#[cfg(feature = "stdin_fuzz")] | ||
fn main() { | ||
use std::io::Read; | ||
|
||
let mut data = Vec::with_capacity(8192); | ||
std::io::stdin().read_to_end(&mut data).unwrap(); | ||
fs_store_run(data.as_ptr(), data.len()); | ||
} | ||
|
||
#[test] | ||
fn run_test_cases() { | ||
use std::fs; | ||
use std::io::Read; | ||
use lightning_fuzz::utils::test_logger::StringBuffer; | ||
|
||
use std::sync::{atomic, Arc}; | ||
{ | ||
let data: Vec<u8> = vec![0]; | ||
fs_store_run(data.as_ptr(), data.len()); | ||
} | ||
let mut threads = Vec::new(); | ||
let threads_running = Arc::new(atomic::AtomicUsize::new(0)); | ||
if let Ok(tests) = fs::read_dir("test_cases/fs_store") { | ||
for test in tests { | ||
let mut data: Vec<u8> = Vec::new(); | ||
let path = test.unwrap().path(); | ||
fs::File::open(&path).unwrap().read_to_end(&mut data).unwrap(); | ||
threads_running.fetch_add(1, atomic::Ordering::AcqRel); | ||
|
||
let thread_count_ref = Arc::clone(&threads_running); | ||
let main_thread_ref = std::thread::current(); | ||
threads.push((path.file_name().unwrap().to_str().unwrap().to_string(), | ||
std::thread::spawn(move || { | ||
let string_logger = StringBuffer::new(); | ||
|
||
let panic_logger = string_logger.clone(); | ||
let res = if ::std::panic::catch_unwind(move || { | ||
fs_store_test(&data, panic_logger); | ||
}).is_err() { | ||
Some(string_logger.into_string()) | ||
} else { None }; | ||
thread_count_ref.fetch_sub(1, atomic::Ordering::AcqRel); | ||
main_thread_ref.unpark(); | ||
res | ||
}) | ||
)); | ||
while threads_running.load(atomic::Ordering::Acquire) > 32 { | ||
std::thread::park(); | ||
} | ||
} | ||
} | ||
let mut failed_outputs = Vec::new(); | ||
for (test, thread) in threads.drain(..) { | ||
if let Some(output) = thread.join().unwrap() { | ||
println!("\nOutput of {}:\n{}\n", test, output); | ||
failed_outputs.push(test); | ||
} | ||
} | ||
if !failed_outputs.is_empty() { | ||
println!("Test cases which failed: "); | ||
for case in failed_outputs { | ||
println!("{}", case); | ||
} | ||
panic!(); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,184 @@ | ||
use core::hash::{BuildHasher, Hasher}; | ||
use lightning::util::persist::{KVStore, KVStoreSync}; | ||
use lightning_persister::fs_store::FilesystemStore; | ||
use std::fs; | ||
use tokio::runtime::Runtime; | ||
|
||
use crate::utils::test_logger; | ||
|
||
struct TempFilesystemStore { | ||
temp_path: std::path::PathBuf, | ||
inner: FilesystemStore, | ||
} | ||
|
||
impl TempFilesystemStore { | ||
fn new() -> Self { | ||
const SHM_PATH: &str = "/dev/shm"; | ||
let mut temp_path = if std::path::Path::new(SHM_PATH).exists() { | ||
std::path::PathBuf::from(SHM_PATH) | ||
} else { | ||
std::env::temp_dir() | ||
}; | ||
|
||
let random_number = std::collections::hash_map::RandomState::new().build_hasher().finish(); | ||
let random_folder_name = format!("fs_store_fuzz_{:016x}", random_number); | ||
temp_path.push(random_folder_name); | ||
|
||
let inner = FilesystemStore::new(temp_path.clone()); | ||
TempFilesystemStore { inner, temp_path } | ||
} | ||
} | ||
|
||
impl Drop for TempFilesystemStore { | ||
fn drop(&mut self) { | ||
_ = fs::remove_dir_all(&self.temp_path) | ||
} | ||
} | ||
|
||
/// Actual fuzz test, method signature and name are fixed | ||
fn do_test<Out: test_logger::Output>(data: &[u8], out: Out) { | ||
let rt = Runtime::new().unwrap(); | ||
rt.block_on(do_test_internal(data, out)); | ||
} | ||
|
||
async fn do_test_internal<Out: test_logger::Output>(data: &[u8], _out: Out) { | ||
let mut read_pos = 0; | ||
macro_rules! get_slice { | ||
($len: expr) => {{ | ||
let slice_len = $len as usize; | ||
if data.len() < read_pos + slice_len { | ||
None | ||
} else { | ||
read_pos += slice_len; | ||
Some(&data[read_pos - slice_len..read_pos]) | ||
} | ||
}}; | ||
} | ||
|
||
let temp_fs_store = TempFilesystemStore::new(); | ||
let fs_store = &temp_fs_store.inner; | ||
|
||
let primary_namespace = "primary"; | ||
let secondary_namespace = "secondary"; | ||
let key = "key"; | ||
|
||
let mut next_data_value = 0u64; | ||
let mut get_next_data_value = || { | ||
let data_value = next_data_value.to_be_bytes().to_vec(); | ||
next_data_value += 1; | ||
|
||
data_value | ||
}; | ||
|
||
let mut current_data = None; | ||
|
||
let mut handles = Vec::new(); | ||
loop { | ||
let v = match get_slice!(1) { | ||
Some(b) => b[0], | ||
None => break, | ||
}; | ||
match v % 13 { | ||
// Sync write | ||
0 => { | ||
let data_value = get_next_data_value(); | ||
|
||
KVStoreSync::write( | ||
fs_store, | ||
primary_namespace, | ||
secondary_namespace, | ||
key, | ||
data_value.clone(), | ||
) | ||
.unwrap(); | ||
|
||
current_data = Some(data_value); | ||
}, | ||
// Sync remove | ||
1 => { | ||
KVStoreSync::remove(fs_store, primary_namespace, secondary_namespace, key, false) | ||
.unwrap(); | ||
|
||
current_data = None; | ||
}, | ||
// Sync list | ||
2 => { | ||
KVStoreSync::list(fs_store, primary_namespace, secondary_namespace).unwrap(); | ||
}, | ||
// Sync read | ||
3 => { | ||
_ = KVStoreSync::read(fs_store, primary_namespace, secondary_namespace, key); | ||
}, | ||
// Async write. Bias writes a bit. | ||
4..=9 => { | ||
let data_value = get_next_data_value(); | ||
|
||
let fut = KVStore::write( | ||
fs_store, | ||
primary_namespace, | ||
secondary_namespace, | ||
key, | ||
data_value.clone(), | ||
); | ||
|
||
// Already set the current_data, even though writing hasn't finished yet. This supports the call-time | ||
// ordering semantics. | ||
current_data = Some(data_value); | ||
|
||
let handle = tokio::task::spawn(fut); | ||
|
||
// Store the handle to later await the result. | ||
handles.push(handle); | ||
}, | ||
// Async remove | ||
10 | 11 => { | ||
let lazy = v == 10; | ||
let fut = | ||
KVStore::remove(fs_store, primary_namespace, secondary_namespace, key, lazy); | ||
|
||
// Already set the current_data, even though writing hasn't finished yet. This supports the call-time | ||
// ordering semantics. | ||
current_data = None; | ||
|
||
let handle = tokio::task::spawn(fut); | ||
handles.push(handle); | ||
}, | ||
// Join tasks. | ||
12 => { | ||
for handle in handles.drain(..) { | ||
let _ = handle.await.unwrap(); | ||
} | ||
}, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It shouldn't change anything, but do we want to throw in some coverage for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added. Only I don't think we can assert anything because things may be in flight. It does add some extra variation to the test to also list during async ops. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also added read. Some story, nothing to assert, but we do cover read execution during writes. |
||
_ => unreachable!(), | ||
} | ||
|
||
// If no more writes are pending, we can reliably see if the data is consistent. | ||
if handles.is_empty() { | ||
let data_value = | ||
KVStoreSync::read(fs_store, primary_namespace, secondary_namespace, key).ok(); | ||
assert_eq!(data_value, current_data); | ||
|
||
let list = KVStoreSync::list(fs_store, primary_namespace, secondary_namespace).unwrap(); | ||
assert_eq!(list.is_empty(), current_data.is_none()); | ||
|
||
assert_eq!(0, fs_store.state_size()); | ||
} | ||
} | ||
|
||
// Always make sure that all async tasks are completed before returning. Otherwise the temporary storage dir could | ||
// be removed, and then again recreated by unfinished tasks. | ||
for handle in handles.drain(..) { | ||
let _ = handle.await.unwrap(); | ||
} | ||
} | ||
|
||
/// Method that needs to be added manually, {name}_test | ||
pub fn fs_store_test<Out: test_logger::Output>(data: &[u8], out: Out) { | ||
do_test(data, out); | ||
} | ||
|
||
/// Method that needs to be added manually, {name}_run | ||
#[no_mangle] | ||
pub extern "C" fn fs_store_run(data: *const u8, datalen: usize) { | ||
do_test(unsafe { std::slice::from_raw_parts(data, datalen) }, test_logger::DevNull {}); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -16,6 +16,7 @@ rustdoc-args = ["--cfg", "docsrs"] | |
[dependencies] | ||
bitcoin = "0.32.2" | ||
lightning = { version = "0.2.0", path = "../lightning" } | ||
tokio = { version = "1.35", optional = true, default-features = false, features = ["rt-multi-thread"] } | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. While its true that we're relying on a multi-threaded runtime here, just setting the feature doesn't ensure we get one (you can still create a single-threaded runtime). I'm not entirely sure what the right answer is, but we need to at least document this requirement. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it a problem if it would be used with a single-threaded runtime, because isn't everything then just executed synchronously? I tried the test with There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, I figured it might be an issue but if you tested its definitely not. We can drop the feature here, then :) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can't drop the feature, because There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Right, spawning threads ofc. requires a multi-threaded runtime. Not sure why your test worked. |
||
|
||
[target.'cfg(windows)'.dependencies] | ||
windows-sys = { version = "0.48.0", default-features = false, features = ["Win32_Storage_FileSystem", "Win32_Foundation"] } | ||
|
@@ -26,6 +27,7 @@ criterion = { version = "0.4", optional = true, default-features = false } | |
[dev-dependencies] | ||
lightning = { version = "0.2.0", path = "../lightning", features = ["_test_utils"] } | ||
bitcoin = { version = "0.32.2", default-features = false } | ||
tokio = { version = "1.35", default-features = false, features = ["macros"] } | ||
|
||
[lints] | ||
workspace = true |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need to make sure all the spawned tasks have finished before we do this. Otherwise cleanup wont work because the async task will re-create the directory as a part of its write.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point. First I thought to do something smart with drop, but at that point we don't have the list of handles, and also can't await in
drop
I think. So just added the final wait to the end of the test fn and avoid early returns.