-
Notifications
You must be signed in to change notification settings - Fork 224
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
Database Transactions #581
Conversation
One thing I've been meaning to do is to run with a real database in our integration tests (versus in-memory mock). This could be a way we can test the RocksDB implementation works. |
I can work on this as part of getting this change checked in. |
fedimint-api/src/db/mod.rs
Outdated
@@ -46,6 +46,24 @@ pub trait Database: Send + Sync { | |||
fn raw_find_by_prefix(&self, key_prefix: &[u8]) -> PrefixIter<'_>; | |||
|
|||
fn raw_apply_batch(&self, batch: DbBatch) -> Result<()>; | |||
|
|||
fn new_transaction<'a>(&'a self) -> Box<dyn DbTransaction<'a> + 'a>; |
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 would advise to wrap Box<dyn DbTransaction<'a> + 'a>
into a neat newtype that Deref
s to &dyn DBTransaction` to avoid typing all that all the time. We're going to be using that pattern all over the modularization framework as well. Example: https://github.com/fedimint/fedimint/pull/546/files#diff-de9b10a6ed1a7ca9958bd8722504beb2edd17137c8ded2c261379e3fee1f9357R26
Appreciate the feedback on this. I have been thinking about the questions above and come to some conclusions.
Additionally, longer term I think we should migrate away from Optimistic Transactions and support transactions using DB locks. This will catch conflicts at write/read time rather than at commit time, which potentially could allow us to reject a write instead of throwing away the whole db transaction. This is a large change so I'll also leave this for another PR. Will get started working on integrating the feedback above. |
|
Sorry, I only meant to close the #428 and must have got confused. |
96d52bc
to
afe9755
Compare
Pushed a new version of the code that rebased on top of master, reduced the scope of the PR, and added a unit test for transactions for the memory db, rocksdb, and sled db. |
Box::new(MemDbIter { data }) | ||
} | ||
|
||
fn commit_tx(self: Box<Self>) -> Result<()> { |
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.
How does this deal with write isolation what are our requirements of transaction isolation anyway?
Basically:
- transaction A starts (gets a copy of data)
- transaction B starts (gets a copy of data)
- transaction A deletes a key X - it works, in the copy of the data
- transaction B deletes a key X - it works, in the copy of the data
- transaction A commits, it works, X gets deleted
- transaction B commits, it should fail as X is deleted and there was a conflict
Seems like rocksdb is implementing a snapshot level transaction isolation? https://www.sobyte.net/post/2022-01/rocksdb-tx/
So to implement it we would need to check which keys were modified since the tx started? Or have some shared map with a per-key lock, so we can lock per any modified key? https://en.wikipedia.org/wiki/Multiversion_concurrency_control basically, I guess, maybe (I'm not an expert)?
Honestly, I would consider not implementing/using any in-memory database, and just use an ephemeral (temporary, random named file) real database instance in tests. In my experience, database integration is so important and easy to isolate that it's better to just not mock/fake it, take a tiny performance hit and run more tests against the real thing.
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 chatted offline but I'll respond here for transparency. Yes the above description is exactly right, rocksdb Optimistic Concurrency has snapshot isolation and allows for transactions to read their own writes. The above example with the deletion of the key is how rocksdb would operate.
I very much agree that re-implementing snapshot isolation for an in-memory key value store is not worth our effort. I just didn't want to regress the existing database implementations (sled and in-memory), so essentially I added some hacky code that allows those implementations to operate on a snapshot, but they won't do conflict resolution during commit (because as you point out, that would require MVCC). Since @elsirion mentioned that we currently shouldn't have any write-write conflicts, I think this is ok for now.
I agree that the proper fix is to use rocksdb for the mock tests as well as the real tests and just remove the in-memory and sled support altogether.
Fixed merge conflict. |
Codecov ReportBase: 66.61% // Head: 67.28% // Increases project coverage by
Additional details and impacted files@@ Coverage Diff @@
## master #581 +/- ##
==========================================
+ Coverage 66.61% 67.28% +0.67%
==========================================
Files 87 87
Lines 12771 13112 +341
==========================================
+ Hits 8507 8823 +316
- Misses 4264 4289 +25
Help us with your feedback. Take ten seconds to tell us how you rate us. Have a feature suggestion? Share it here. ☔ View full report at Codecov. |
* Add support for lifetimes * `Box<...>` does not have to be `Sync` (that's kind of a whole purspose of using it over `Arc`, I guess)
@@ -317,6 +317,7 @@ async fn drop_peers_who_contribute_bad_sigs() { | |||
assert!(fed.subset_peers(&[0, 1, 2]).has_dropped_peer(3)); | |||
} | |||
|
|||
/* |
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.
@m1sterc001guy , why do we have to disable this test with the DB change?
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.
PS: disabling by comment is super easy to miss in reviews. I think we should use #[ignore]
macro
#[tokio::test(flavor = "multi_thread")]
#[ignore]
async fn my_test_case() { ... }
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.
Oops apologies, didn't mean to keep that in there. I will revert.
Will use ignore in the future, thanks for the tip.
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.
Re-enable the test: #743
The test is stalling for me locally (with and without my change) but I haven't had time to dig into why.
This PR is a work in progress. It adds a new trait for a Database Transaction and implements the database transaction for RocksDB. I have integrated the API into the federation module. The tests are still broken and I am currently testing the changes.
The main difference between this PR and the current code is that in this PR a single Database Transaction and passed around to subsequent functions via a reference. The old code use a "Transaction batch" concept and each function either had a copy of the parent transaction, or a "subtransaction" (which is just a copy of the parent).
Open Questions.