-
Notifications
You must be signed in to change notification settings - Fork 418
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
Draft
joostjager
wants to merge
4
commits into
lightningdevkit:main
Choose a base branch
from
joostjager:async-fsstore
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Async FilesystemStore #3931
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,155 @@ | ||
//! Objects related to [`FilesystemStoreAsync`] live here. | ||
|
||
use std::sync::{ | ||
atomic::{AtomicU64, Ordering}, | ||
Arc, | ||
}; | ||
|
||
use crate::fs_store::FilesystemStore; | ||
use core::future::Future; | ||
use core::pin::Pin; | ||
use lightning::util::persist::{KVStore, KVStoreSync}; | ||
|
||
/// An asynchronous extension of FilesystemStore, implementing the `KVStore` trait for async operations. It is shaped as | ||
/// a wrapper around an existing [`FilesystemStore`] so that the same locks are used. This allows both the sync and | ||
/// async interface to be used simultaneously. | ||
pub struct FilesystemStoreAsync { | ||
inner: Arc<FilesystemStore>, | ||
|
||
// Version counter to ensure that writes are applied in the correct order. It is assumed that read, list and remove | ||
// operations aren't sensitive to the order of execution. | ||
version_counter: AtomicU64, | ||
} | ||
|
||
impl FilesystemStoreAsync { | ||
/// Creates a new instance of [`FilesystemStoreAsync`]. | ||
pub fn new(inner: Arc<FilesystemStore>) -> Self { | ||
Self { inner, version_counter: AtomicU64::new(0) } | ||
} | ||
} | ||
|
||
impl KVStore for FilesystemStoreAsync { | ||
fn read( | ||
&self, primary_namespace: &str, secondary_namespace: &str, key: &str, | ||
) -> Pin<Box<dyn Future<Output = Result<Vec<u8>, lightning::io::Error>> + 'static + Send>> { | ||
let primary_namespace = primary_namespace.to_string(); | ||
let secondary_namespace = secondary_namespace.to_string(); | ||
let key = key.to_string(); | ||
let this = Arc::clone(&self.inner); | ||
|
||
Box::pin(async move { | ||
tokio::task::spawn_blocking(move || { | ||
this.read(&primary_namespace, &secondary_namespace, &key) | ||
}) | ||
.await | ||
.unwrap_or_else(|e| Err(lightning::io::Error::new(lightning::io::ErrorKind::Other, e))) | ||
}) | ||
} | ||
|
||
fn write( | ||
&self, primary_namespace: &str, secondary_namespace: &str, key: &str, buf: &[u8], | ||
) -> Pin<Box<dyn Future<Output = Result<(), lightning::io::Error>> + 'static + Send>> { | ||
let primary_namespace = primary_namespace.to_string(); | ||
let secondary_namespace = secondary_namespace.to_string(); | ||
let key = key.to_string(); | ||
let buf = buf.to_vec(); | ||
let this = Arc::clone(&self.inner); | ||
|
||
// Obtain a version number to retain the call sequence. | ||
let version = self.version_counter.fetch_add(1, Ordering::SeqCst); | ||
|
||
Box::pin(async move { | ||
tokio::task::spawn_blocking(move || { | ||
this.write_version( | ||
&primary_namespace, | ||
&secondary_namespace, | ||
&key, | ||
&buf, | ||
Some(version), | ||
) | ||
}) | ||
.await | ||
.unwrap_or_else(|e| Err(lightning::io::Error::new(lightning::io::ErrorKind::Other, e))) | ||
}) | ||
} | ||
|
||
fn remove( | ||
&self, primary_namespace: &str, secondary_namespace: &str, key: &str, lazy: bool, | ||
) -> Pin<Box<dyn Future<Output = Result<(), lightning::io::Error>> + 'static + Send>> { | ||
let primary_namespace = primary_namespace.to_string(); | ||
let secondary_namespace = secondary_namespace.to_string(); | ||
let key = key.to_string(); | ||
let this = Arc::clone(&self.inner); | ||
|
||
Box::pin(async move { | ||
tokio::task::spawn_blocking(move || { | ||
this.remove(&primary_namespace, &secondary_namespace, &key, lazy) | ||
}) | ||
.await | ||
.unwrap_or_else(|e| Err(lightning::io::Error::new(lightning::io::ErrorKind::Other, e))) | ||
}) | ||
} | ||
|
||
fn list( | ||
&self, primary_namespace: &str, secondary_namespace: &str, | ||
) -> Pin<Box<dyn Future<Output = Result<Vec<String>, lightning::io::Error>> + 'static + Send>> { | ||
let primary_namespace = primary_namespace.to_string(); | ||
let secondary_namespace = secondary_namespace.to_string(); | ||
let this = Arc::clone(&self.inner); | ||
|
||
Box::pin(async move { | ||
tokio::task::spawn_blocking(move || this.list(&primary_namespace, &secondary_namespace)) | ||
.await | ||
.unwrap_or_else(|e| { | ||
Err(lightning::io::Error::new(lightning::io::ErrorKind::Other, e)) | ||
}) | ||
}) | ||
} | ||
} | ||
|
||
mod test { | ||
use crate::{fs_store::FilesystemStore, fs_store_async::FilesystemStoreAsync}; | ||
use lightning::util::persist::KVStore; | ||
use std::sync::Arc; | ||
|
||
#[tokio::test] | ||
async fn read_write_remove_list_persist() { | ||
let mut temp_path = std::env::temp_dir(); | ||
temp_path.push("test_read_write_remove_list_persist"); | ||
let fs_store = Arc::new(FilesystemStore::new(temp_path)); | ||
let fs_store_async = FilesystemStoreAsync::new(Arc::clone(&fs_store)); | ||
|
||
let data1 = [42u8; 32]; | ||
let data2 = [43u8; 32]; | ||
|
||
let primary_namespace = "testspace"; | ||
let secondary_namespace = "testsubspace"; | ||
let key = "testkey"; | ||
|
||
// Test writing the same key twice with different data. Execute the asynchronous part out of order to ensure | ||
// that eventual consistency works. | ||
let fut1 = fs_store_async.write(primary_namespace, secondary_namespace, key, &data1); | ||
let fut2 = fs_store_async.write(primary_namespace, secondary_namespace, key, &data2); | ||
|
||
fut2.await.unwrap(); | ||
fut1.await.unwrap(); | ||
|
||
// Test list. | ||
let listed_keys = | ||
fs_store_async.list(primary_namespace, secondary_namespace).await.unwrap(); | ||
assert_eq!(listed_keys.len(), 1); | ||
assert_eq!(listed_keys[0], key); | ||
|
||
// Test read. We expect to read data2, as the write call was initiated later. | ||
let read_data = | ||
fs_store_async.read(primary_namespace, secondary_namespace, key).await.unwrap(); | ||
assert_eq!(data2, &*read_data); | ||
|
||
// Test remove. | ||
fs_store_async.remove(primary_namespace, secondary_namespace, key, false).await.unwrap(); | ||
|
||
let listed_keys = | ||
fs_store_async.list(primary_namespace, secondary_namespace).await.unwrap(); | ||
assert_eq!(listed_keys.len(), 0); | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,6 +9,7 @@ | |
extern crate criterion; | ||
|
||
pub mod fs_store; | ||
pub mod fs_store_async; | ||
|
||
mod utils; | ||
|
||
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
Mhh, so I'm not sure if spawning blocking tasks for every IO call is the way to go (see for example https://docs.rs/tokio/latest/tokio/fs/index.html#tuning-your-file-io: "To get good performance with file IO on Tokio, it is recommended to batch your operations into as few spawn_blocking calls as possible."). Maybe there are other designs that we should at least consider before moving forward with this approach. For example, we could create a dedicated pool of longer-lived worker task(s) that process a queue?
If we use
spawn_blocking
, can we give the user control over which runtime this exactly will be spawned on? Also, rather than just doing wrapping, should we be usingtokio::fs
?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.
If we should batch operations, I think the current approach is better than using tokio::fs? Because it already batches the various operations inside kvstoresync::write.
Further batching probably needs to happen at a higher level in LDK, and might be a bigger change. Not sure if that is worth it just for FIlesystemStore, especially when that store is not the preferred store for real world usage?
Isn't Tokio doing that already when a task is spawned?
With tokio::fs, the current runtime is used. I'd think that that is then also sufficient if we spawn ourselves, without a need to specifiy which runtime exactly?
More generally, I think the main purpose of this PR is to show how an async kvstore could be implemented, and to have something for testing potentially. Additionally if there are users that really want to use this type of store in production, they could. But I don't think it is something to spend too much time on. A remote database is probably the more important target to design for.