-
Notifications
You must be signed in to change notification settings - Fork 114
Require both KVStore and KVStoreSync implementations, switch BP to be fully-async
#633
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
Conversation
|
👋 Thanks for assigning @joostjager as a reviewer! |
2adf4a7 to
296b55c
Compare
296b55c to
85198d8
Compare
|
This now builds based on the just-merged lightningdevkit/rust-lightning#4069. We have yet to add write-version tracking for VSS. |
c4251d4 to
7158653
Compare
7158653 to
8c2ff8f
Compare
464e6b7 to
9035b71
Compare
|
Should be good for review. For the |
9035b71 to
750ab87
Compare
|
Rebased to address conflicts post-#652. |
| if primary_namespace.is_empty() { | ||
| key.to_owned() | ||
| } else { | ||
| format!("{}#{}#{}", primary_namespace, secondary_namespace, key) |
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.
Is it necessary to make a string out of this? It doesn't need to be mapped to a filename like for fs_store, so maybe it can also simply be a tuple?
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.
I think so, as the HashMap needs to hold an owned value. We could have it be a (String, String, String), but that's worse.
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.
Is that worse? The string concatenation looks a bit unnecessary. Or make it a struct that is used as the key?
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.
Is that worse? The string concatenation looks a bit unnecessary. Or make it a struct that is used as the key?
It at least requires three individual allocations instead of one? I.e. more clutter on the heap, and probably also some slowdown?
FWIW, I mirrored what we do for the obfuscated key. Unfortunately it's not super easy to just reuse that.
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.
Not ideal indeed, the heap allocs
src/io/vss_store.rs
Outdated
|
|
||
| Ok(self.storable_builder.deconstruct(storable)?.0) | ||
|
|
||
| self.execute_locked_read(locking_key, async move || { |
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.
I think we don't need the lock for reading?
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.
Dropped.
750ab87 to
70404ed
Compare
|
Addressed pending comments, probably still need to see why the CI job start hanging again. |
|
LGTM, can squash |
70404ed to
6c3fdf3
Compare
| let secondary_namespace = secondary_namespace.to_string(); | ||
| let key = key.to_string(); | ||
| let inner = Arc::clone(&self.inner); | ||
| let fut = tokio::task::spawn_blocking(move || { |
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.
During final look, I am now again wondering if this function is actually preserving order?
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.
Hmm, good point, it seems it would depend on how tokio exactly schedules the blocking tasks. I considered a few other options, but now simply also followed the tried and true 'write version locking' approach here, as we already use that in VSS and FS stores.
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.
Hmm but isn't this completely unnecessary because sqlite has its own global lock? With FS and VSS there is the actual possibility of parallel execution.
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.
Hmm but isn't this completely unnecessary because sqlite has its own global lock? With FS and VSS there is the actual possibility of parallel execution.
Well, I first thought so, too, but I think the issue is that tokio gives no guarantee in which order spawned tasks are executed/scheduled. I.e., AFAICT it could happen that we spawn to writes w1, w2 but the task for w2 gets polled first, acquiring the Mutex first. Same goes for the case where multiple writes wait on the same connection lock: say w1 currently holds the Mutex and two more writes w2, w3 would get get queued, AFAIU there is no guarantee that when w1 drops the lock w2 always acquires the lock next.
TLDR: it seems we unfortunately need to do the version dance here, too. Maybe there's an easier mechanism in the SQLite case (e.g., prepare should technically take care of that, but we can't lock the connection Mutex outside of the spawned task as the guard is not Send) we could lean on to guarantee the ordered writes, but applying the same approach seemed simplest for now?
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.
I just refuse to believe that we need that version dance really for this problem. Isn't block_in_place meant for this? If we ensure that the write has happened before the fn returns, it doesn't matter that during that execution there may be other writes that get processed in a certain order?
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.
Hmm? How do you imagine block_in_place to work here? block_in_place takes a future and drives it to completion, i.e., makes it a blocking operation. For the async KVStore we however exactly need a future, not a blocking operation, which is what spawn_blocking does for us.
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.
Does it? I saw
pub fn block_in_place<F, R>(f: F) -> R where F: FnOnce() -> R
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.
Does it? I saw
pub fn block_in_place<F, R>(f: F) -> R where F: FnOnce() -> R
Sorry, please replace block_in_place with block_on above. block_in_place is simply a wrapper that spawns a blocking task on the outer runtime context so that the inner block_on call doesn't starve (that is, if it is indeed called on the same runtime, which it isn't always).
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.
Discussed offline that even though block_in_place may work with caveats, we do prefer to implement sqlite async. And that for that to work, we need the versioning.
.. first step to make review easier.
.. as we're gonna reuse the `async` `_internal` methods shortly.
.. where the former holds the latter in an `Arc` that can be used in async/`Future` contexts more easily.
We implement the async `KVStore` trait for `VssStore`.
6c3fdf3 to
1a55ab8
Compare
.. to be easier reusable via `KVStore` also
.. where the former holds the latter in an `Arc` that can be used in async/`Future` contexts more easily.
1a55ab8 to
8dada08
Compare
|
Addressed remaining comments and changed base to |
|
CI breakage is unrelated (#654) |
|
Squash = ok |
.. to be easier reusable via `KVStore` also
.. where the former holds the latter in an `Arc` that can be used in async/`Future` contexts more easily.
As an intermediary step, we require any store to implement both `KVStore` and `KVStoreSync`, allowing us to switch over step-by-step. We already switch to the fully-async background processor variant here.
8dada08 to
c9e3f71
Compare
Squashed fixups and force-pushed with the following additional changes to address the pending comments: > git diff-tree -U2 8dada088 c9e3f715
diff --git a/src/io/sqlite_store/mod.rs b/src/io/sqlite_store/mod.rs
index abb45f94..6ba41f71 100644
--- a/src/io/sqlite_store/mod.rs
+++ b/src/io/sqlite_store/mod.rs
@@ -68,9 +68,5 @@ impl SqliteStore {
&self, primary_namespace: &str, secondary_namespace: &str, key: &str,
) -> String {
- if primary_namespace.is_empty() {
- key.to_owned()
- } else {
- format!("{}#{}#{}", primary_namespace, secondary_namespace, key)
- }
+ format!("{}#{}#{}", primary_namespace, secondary_namespace, key)
}
@@ -412,30 +408,30 @@ impl SqliteStoreInner {
self.execute_locked_write(inner_lock_ref, locking_key, version, || {
- let locked_conn = self.connection.lock().unwrap();
+ let locked_conn = self.connection.lock().unwrap();
- let sql = format!("DELETE FROM {} WHERE primary_namespace=:primary_namespace AND secondary_namespace=:secondary_namespace AND key=:key;", self.kv_table_name);
+ let sql = format!("DELETE FROM {} WHERE primary_namespace=:primary_namespace AND secondary_namespace=:secondary_namespace AND key=:key;", self.kv_table_name);
- let mut stmt = locked_conn.prepare_cached(&sql).map_err(|e| {
- let msg = format!("Failed to prepare statement: {}", e);
- io::Error::new(io::ErrorKind::Other, msg)
- })?;
+ let mut stmt = locked_conn.prepare_cached(&sql).map_err(|e| {
+ let msg = format!("Failed to prepare statement: {}", e);
+ io::Error::new(io::ErrorKind::Other, msg)
+ })?;
- stmt.execute(named_params! {
- ":primary_namespace": primary_namespace,
- ":secondary_namespace": secondary_namespace,
- ":key": key,
+ stmt.execute(named_params! {
+ ":primary_namespace": primary_namespace,
+ ":secondary_namespace": secondary_namespace,
+ ":key": key,
+ })
+ .map_err(|e| {
+ let msg = format!(
+ "Failed to delete key {}/{}/{}: {}",
+ PrintableString(primary_namespace),
+ PrintableString(secondary_namespace),
+ PrintableString(key),
+ e
+ );
+ io::Error::new(io::ErrorKind::Other, msg)
+ })?;
+ Ok(())
})
- .map_err(|e| {
- let msg = format!(
- "Failed to delete key {}/{}/{}: {}",
- PrintableString(primary_namespace),
- PrintableString(secondary_namespace),
- PrintableString(key),
- e
- );
- io::Error::new(io::ErrorKind::Other, msg)
- })?;
- Ok(())
- })
} |
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.
I have to admit that I am a bit nervous about the versioning code, because it is involved in every persistence action. After landing this PR, do we need to do anything extra with regards to testing?
Hmm, well, we have fuzzing test coverage at least on the |
As an intermediary step towards making our IO fully async, we now require any store to implement both
KVStoreandKVStoreSync, which allows us to switch over to the fully-async background processor and take further migration steps bit-by-bit when we make more and more of the core codebaseasync.To this end, we refactor
VssStoreandSqliteStoreto implementKVStoreTODOs:
KVStoreforTestStoreupstream to fix testsKVStoreimplementation.. draft until then.